.claude/skills/test/SKILL.md
Tests für RESA-Code generieren
npx skillsauth add AImitSK/resa-wp testInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generiere Tests für die angegebene Datei oder Komponente, orientiert an der RESA-Teststrategie.
Zieldatei analysieren:
Test-Framework bestimmen:
tests/php/)tests/js/)Teststrategie laden:
docs/planning/RESA-Teststrategie.md für KonventionenTests schreiben nach den Kategorien unten
free → immer ladbarpro → nur im Premium-Plan ladbarpro → REST-Endpoint gibt 403 im Free-Plantests/php/Unit/{Namespace}/{Klasse}Test.php
tests/php/Integration/{Namespace}/{Klasse}IntegrationTest.php
modules/{slug}/tests/{Klasse}Test.php # Modul-spezifische Tests
Beispiel: includes/Models/LeadRepository.php → tests/php/Unit/Models/LeadRepositoryTest.php
Beispiel: modules/rent-calculator/RentCalculatorService.php → modules/rent-calculator/tests/RentCalculatorServiceTest.php
<?php
declare(strict_types=1);
namespace Resa\Tests\Unit\Models;
use Resa\Models\LeadRepository;
use Brain\Monkey;
use Brain\Monkey\Functions;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;
class LeadRepositoryTest extends TestCase {
use MockeryPHPUnitIntegration;
protected function setUp(): void {
parent::setUp();
Monkey\setUp();
}
protected function tearDown(): void {
Monkey\tearDown();
parent::tearDown();
}
public function test_find_returns_lead_by_id(): void {
// Arrange
$wpdb = \Mockery::mock( \wpdb::class );
$wpdb->prefix = 'wp_';
$wpdb->shouldReceive( 'prepare' )->once()->andReturn( 'prepared_sql' );
$wpdb->shouldReceive( 'get_row' )->once()->andReturn( (object) [
'id' => 1,
'name' => 'Max Mustermann',
'email' => '[email protected]',
] );
// Act
$repo = new LeadRepository();
// ... inject $wpdb mock
// Assert
$this->assertNotNull( $lead );
$this->assertEquals( 'Max Mustermann', $lead->name );
}
}
// Funktionen stubs
Functions\stubs( [
'sanitize_text_field' => function ( $str ) { return $str; },
'sanitize_email' => function ( $str ) { return $str; },
'absint' => function ( $val ) { return abs( intval( $val ) ); },
'wp_unslash' => function ( $val ) { return $val; },
'esc_html__' => function ( $text, $domain = '' ) { return $text; },
'__' => function ( $text, $domain = '' ) { return $text; },
'current_time' => function () { return '2026-01-01 00:00:00'; },
] );
// Funktion erwarten
Functions\expect( 'current_user_can' )
->once()
->with( 'manage_resa_leads' )
->andReturn( true );
// WordPress-Hooks
Functions\expect( 'add_action' )
->once()
->with( 'init', \Mockery::type( 'callable' ) );
// resa_fs() Mock
Functions\expect( 'resa_fs' )->andReturn(
\Mockery::mock( \Freemius::class, [
'can_use_premium_code' => true,
'is_free_plan' => false,
'is_trial' => false,
'get_upgrade_url' => 'https://checkout.freemius.com/...',
] )
);
// Free-Plan Test
Functions\expect( 'resa_fs' )->andReturn(
\Mockery::mock( \Freemius::class, [
'can_use_premium_code' => false,
'is_free_plan' => true,
] )
);
tests/js/components/{Komponente}.test.tsx
tests/js/hooks/{Hook}.test.ts
tests/js/lib/{Modul}.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LeadForm } from '@/frontend/assets/shared/LeadForm';
// @wordpress/i18n Mock
vi.mock( '@wordpress/i18n', () => ( {
__: ( text: string ) => text,
_n: ( single: string, plural: string, count: number ) =>
count === 1 ? single : plural,
_x: ( text: string ) => text,
sprintf: ( format: string, ...args: any[] ) => {
let i = 0;
return format.replace( /%[sd]/g, () => String( args[ i++ ] ) );
},
} ) );
// resaConfig Mock
const mockResaConfig = {
apiBase: 'http://localhost/wp-json/resa/v1/',
nonce: 'test-nonce',
plan: {
isPremium: false,
isTrial: false,
isFreePlan: true,
planName: 'free',
upgradeUrl: 'https://checkout.freemius.com',
trialUrl: 'https://checkout.freemius.com/trial',
trialAvailable: true,
},
};
beforeEach( () => {
( window as any ).resaConfig = mockResaConfig;
} );
describe( 'LeadForm', () => {
it( 'renders all required fields', () => {
render( <LeadForm assetType="mietpreis" locationId={1} /> );
expect( screen.getByLabelText( /name/i ) ).toBeInTheDocument();
expect( screen.getByLabelText( /e-mail/i ) ).toBeInTheDocument();
expect( screen.getByLabelText( /datenschutz/i ) ).toBeInTheDocument();
} );
it( 'shows validation errors for empty required fields', async () => {
const user = userEvent.setup();
render( <LeadForm assetType="mietpreis" locationId={1} /> );
await user.click( screen.getByRole( 'button', { name: /absenden/i } ) );
await waitFor( () => {
expect( screen.getByText( /name ist erforderlich/i ) ).toBeInTheDocument();
} );
} );
it( 'submits valid form data', async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
render( <LeadForm assetType="mietpreis" locationId={1} onSubmit={onSubmit} /> );
await user.type( screen.getByLabelText( /name/i ), 'Max Mustermann' );
await user.type( screen.getByLabelText( /e-mail/i ), '[email protected]' );
await user.click( screen.getByLabelText( /datenschutz/i ) );
await user.click( screen.getByRole( 'button', { name: /absenden/i } ) );
await waitFor( () => {
expect( onSubmit ).toHaveBeenCalledWith(
expect.objectContaining( {
name: 'Max Mustermann',
email: '[email protected]',
} )
);
} );
} );
} );
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get( '*/resa/v1/leads', ( req, res, ctx ) => {
return res( ctx.json( [
{ id: 1, name: 'Max', email: '[email protected]' },
] ) );
} ),
rest.post( '*/resa/v1/leads/submit', ( req, res, ctx ) => {
return res( ctx.json( { success: true, lead_id: 42 } ) );
} ),
);
beforeAll( () => server.listen() );
afterEach( () => server.resetHandlers() );
afterAll( () => server.close() );
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useLeads } from '@/admin/hooks/useLeads';
const wrapper = ( { children }: { children: React.ReactNode } ) => {
const queryClient = new QueryClient( {
defaultOptions: { queries: { retry: false } },
} );
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe( 'useLeads', () => {
it( 'fetches leads successfully', async () => {
const { result } = renderHook( () => useLeads(), { wrapper } );
await waitFor( () => {
expect( result.current.isSuccess ).toBe( true );
} );
expect( result.current.data ).toHaveLength( 1 );
} );
} );
test_gibt_fehler_bei_ungueltigem_email()development
Use when building UIs leveraging the WordPress Design System (WPDS) and its components, tokens, patterns, etc.
tools
Use when working with WP-CLI (wp) for WordPress operations: safe search-replace, db export/import, plugin/theme/user/content management, cron, cache flushing, multisite, and scripting/automation with wp-cli.yml.
tools
WordPress Security Patterns für Plugin-Entwicklung. Automatisch anwenden: Sanitization, Escaping, Nonces, Capability Checks, $wpdb->prepare(), REST API Permission Callbacks.
development
Use when building, extending, or debugging WordPress REST API endpoints/routes: register_rest_route, WP_REST_Controller/controller classes, schema/argument validation, permission_callback/authentication, response shaping, register_rest_field/register_meta, or exposing CPTs/taxonomies via show_in_rest.