testing-react
Writes React/TypeScript tests using Vitest and React Testing Library. Use when "write react tests", "vitest", "component test", "hook test", "RTL", "testing library", "snapshot test", or testing React components, hooks, and utilities.
Install
npx skills add https://github.com/iliaal/whetstone/tree/master/distillery/generated-skills/testing-react
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install iliaal-whetstone@llmmart
git clone https://github.com/iliaal/whetstone.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole iliaal/whetstone collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Test Classification
| Type | Tool | Target | File pattern |
|---|---|---|---|
| Unit | Vitest | Pure functions, utilities, services | Co-located *.test.ts |
| Component | Vitest + RTL | React components | Co-located *.test.tsx |
| Hook | Vitest + RTL | Custom hooks | Co-located *.test.ts |
| E2E | Playwright | User flows, critical paths | Separate e2e/ directory |
Default to component tests for React components. Unit tests for pure functions and service classes. See e2e-testing for Playwright patterns.
Setup
Vitest config: environment: 'jsdom', globals: true, setupFiles pointing to a file that imports @testing-library/jest-dom/vitest. Use @vitejs/plugin-react and mirror path aliases from tsconfig.json.
Critical Rules
- Query priority:
getByRole>getByLabelText>getByPlaceholderText>getByText>getByTestId. Usedata-testidonly when no accessible query works. - Mock boundaries: Mock API services, navigation, and external providers. Render child components and UI libraries real for integration confidence.
- One behavior per test with AAA structure. Name tests
should <behavior> when <condition>. - Async: Use
findBy*for async elements,waitForafter state-triggering actions,vi.useFakeTimers()for debounce/timer logic. - User events: Prefer
userEventoverfireEventfor realistic interactions. - Cleanup:
vi.clearAllMocks()inbeforeEach. Recreate test state per test instead of sharing mutable variables. - Incremental workflow: When testing a directory, process one file at a time (simplest first). Run and verify each before proceeding.
- Tests expose bugs, not the reverse: If a test uncovers broken or buggy behavior, highlight the issue and propose a fix to the source code. Never adjust the test to match incorrect behavior.
Component Test
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
vi.mock('@/api/client');
describe('UserForm', () => {
beforeEach(() => { vi.clearAllMocks(); });
it('should submit valid form data', async () => {
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ email: 'test@example.com' }),
);
});
});
});
Hook Test
import { renderHook, act } from '@testing-library/react';
it('should debounce value updates', () => {
vi.useFakeTimers();
const { result, rerender } = renderHook(
({ value }) => useDebounce(value, 300),
{ initialProps: { value: 'initial' } },
);
rerender({ value: 'updated' });
expect(result.current).toBe('initial');
act(() => { vi.advanceTimersByTime(300); });
expect(result.current).toBe('updated');
vi.useRealTimers();
});
Mocking Patterns
// Service mock — mock the module, not the transport layer
vi.mock('@/server-api/me/me.service', () => ({
MeService: { retrieveMe: vi.fn() },
}));
// QueryClient wrapper for components using TanStack Query
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
render(<Component />, { wrapper: createWrapper() });
Running Tests
npx vitest # Watch mode
npx vitest run # Single run (CI)
npx vitest run src/features/ # Test specific directory
npx vitest --coverage # Coverage report
Files (whetstone)
-
references
-
e2e-testing.md 4.7 KB
# E2E Testing with Playwright ## Directory Structure ``` e2e/ ├── playwright.config.ts ├── fixtures/ │ ├── auth.fixture.ts │ └── test-data.fixture.ts ├── pages/ │ ├── base.page.ts │ └── <page-name>.page.ts ├── tests/ │ ├── auth/ │ │ └── login.spec.ts │ └── smoke/ │ └── critical-paths.spec.ts └── utils/ └── api-helpers.ts ``` Naming: tests `<feature>.spec.ts`, page objects `<page>.page.ts`, fixtures `<concern>.fixture.ts`. ## Configuration ```typescript import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e/tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:5173', trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'setup', testDir: './e2e/fixtures', testMatch: 'auth.fixture.ts' }, { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'e2e/.auth/user.json' }, dependencies: ['setup'], }, ], webServer: { command: 'npm run dev', url: 'http://localhost:5173', reuseExistingServer: !process.env.CI, }, }); ``` ## Page Object Model Tests never use selectors directly — page objects encapsulate all locators and actions. ```typescript // e2e/pages/base.page.ts import { type Page, type Locator } from '@playwright/test'; export abstract class BasePage { constructor(protected readonly page: Page) {} abstract goto(): Promise<void>; async waitForLoad() { await this.page.waitForLoadState('networkidle'); } get toast(): Locator { return this.page.getByRole('alert'); } } // e2e/pages/users.page.ts export class UsersPage extends BasePage { readonly createButton: Locator; readonly searchInput: Locator; constructor(page: Page) { super(page); this.createButton = page.getByRole('button', { name: /create/i }); this.searchInput = page.getByRole('searchbox', { name: /search/i }); } async goto() { await this.page.goto('/users'); await this.waitForLoad(); } async searchFor(query: string) { await this.searchInput.fill(query); await this.page.waitForResponse('**/api/users?*'); } } ``` Rules: locators as public readonly properties, actions as async methods with internal waits, no assertions in page objects, one PO per page. ## Selector Priority | Priority | Method | Use when | |----------|--------|----------| | 1 | `getByRole` | Buttons, links, headings, inputs | | 2 | `getByLabel` | Form inputs with labels | | 3 | `getByPlaceholder` | Search inputs | | 4 | `getByText` | Static text content | | 5 | `getByTestId` | No accessible selector available | Never use CSS selectors, XPath, or DOM structure selectors. When adding `data-testid`, use `<action>-<entity>-<type>` pattern: `create-user-btn`. ## Wait Strategies Never use `waitForTimeout` or `setTimeout`. Use explicit conditions: ```typescript await page.getByRole('heading', { name: 'Dashboard' }).waitFor(); await page.waitForURL('/dashboard'); await page.waitForResponse( (r) => r.url().includes('/api/users') && r.status() === 200, ); await page.getByTestId('spinner').waitFor({ state: 'hidden' }); ``` ## Auth State Reuse Save auth state once, reuse across all tests: ```typescript // e2e/fixtures/auth.fixture.ts import { test as setup } from '@playwright/test'; setup('authenticate', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('testuser@example.com'); await page.getByLabel('Password').fill('TestPassword123!'); await page.getByRole('button', { name: /sign in/i }).click(); await page.waitForURL('/dashboard'); await page.context().storageState({ path: 'e2e/.auth/user.json' }); }); ``` Tests receive auth state via `storageState` in config projects. ## Test Data & Network - Tests create own data via API helpers (faster than UI), clean up in `finally` blocks - Mock responses with `page.route('**/api/path', route => route.fulfill({ ... }))` - Simulate errors with `route.abort('failed')` - Wait for responses: `const resp = page.waitForResponse('**/api/users'); await click; await resp;` ## Flaky Test Fixes | Cause | Fix | |-------|-----| | Hardcoded waits | Explicit wait conditions | | Shared test data | Each test creates its own | | Animations | `animations: 'disabled'` in config | | Race conditions | Wait for API responses before assertions | ```bash npx playwright test --headed --debug # Debug mode npx playwright show-trace trace.zip # Trace viewer npx playwright test --ui # Interactive UI ```
-
-
manifest.json 1.4 KB
{ "query": "testing-react", "search_queries": [ "react testing", "javascript testing", "frontend testing", "vitest", "playwright e2e testing" ], "generated": "2026-02-20", "token_count": 1132, "instructions": "E2E testing patterns go in references/e2e-testing.md, not in main body. Must not conflict with testing-laravel skill triggers.", "sources": [ { "id": "antfu/skills/vitest", "installs": 5253, "sha1": "a205a2386c18d57dc658249b13600035e76ee6f6" }, { "id": "wshobson/agents/javascript-testing-patterns", "installs": 2916, "sha1": "c7753a42ad58898b607bdf70d9c98c690e5cbe84" }, { "id": "hieutrtr/ai1-skills/e2e-testing", "installs": 1388, "sha1": "ed2d000328b7038689b2aeb3660ae653a96740d9" }, { "id": "sergiodxa/agent-skills/frontend-testing-best-practices", "installs": 1236, "sha1": "121c019dcf7fa7bc1cc3fd3475e744c9d9e55d5a" }, { "id": "langgenius/dify/frontend-testing", "installs": 1152, "sha1": "87a164e4ef5947b86f7907b7c8008d391eb32844" }, { "id": "jeffallan/claude-skills/playwright-expert", "installs": 764, "sha1": "f53e2a86f8cbcb4dd87b293cdffc4c9e426e865e" }, { "id": "bobmatnyc/claude-mpm-skills/playwright-e2e-testing", "installs": 489, "sha1": "55631c9c746457a2e0b583675345f22af3a2d06e" } ] } -
SKILL.md 4.1 KB
--- name: testing-react description: Writes React/TypeScript tests using Vitest and React Testing Library. Use when "write react tests", "vitest", "component test", "hook test", "RTL", "testing library", "snapshot test", or testing React components, hooks, and utilities. --- ## Test Classification | Type | Tool | Target | File pattern | |------|------|--------|-------------| | Unit | Vitest | Pure functions, utilities, services | Co-located `*.test.ts` | | Component | Vitest + RTL | React components | Co-located `*.test.tsx` | | Hook | Vitest + RTL | Custom hooks | Co-located `*.test.ts` | | E2E | Playwright | User flows, critical paths | Separate `e2e/` directory | Default to component tests for React components. Unit tests for pure functions and service classes. See [e2e-testing](references/e2e-testing.md) for Playwright patterns. ## Setup Vitest config: `environment: 'jsdom'`, `globals: true`, `setupFiles` pointing to a file that imports `@testing-library/jest-dom/vitest`. Use `@vitejs/plugin-react` and mirror path aliases from `tsconfig.json`. ## Critical Rules - **Query priority**: `getByRole` > `getByLabelText` > `getByPlaceholderText` > `getByText` > `getByTestId`. Use `data-testid` only when no accessible query works. - **Mock boundaries**: Mock API services, navigation, and external providers. Render child components and UI libraries real for integration confidence. - **One behavior per test** with AAA structure. Name tests `should <behavior> when <condition>`. - **Async**: Use `findBy*` for async elements, `waitFor` after state-triggering actions, `vi.useFakeTimers()` for debounce/timer logic. - **User events**: Prefer `userEvent` over `fireEvent` for realistic interactions. - **Cleanup**: `vi.clearAllMocks()` in `beforeEach`. Recreate test state per test instead of sharing mutable variables. - **Incremental workflow**: When testing a directory, process one file at a time (simplest first). Run and verify each before proceeding. - **Tests expose bugs, not the reverse**: If a test uncovers broken or buggy behavior, highlight the issue and propose a fix to the source code. Never adjust the test to match incorrect behavior. ## Component Test ```tsx import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; vi.mock('@/api/client'); describe('UserForm', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should submit valid form data', async () => { const onSubmit = vi.fn(); render(<UserForm onSubmit={onSubmit} />); await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com'); await userEvent.click(screen.getByRole('button', { name: /submit/i })); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith( expect.objectContaining({ email: 'test@example.com' }), ); }); }); }); ``` ## Hook Test ```typescript import { renderHook, act } from '@testing-library/react'; it('should debounce value updates', () => { vi.useFakeTimers(); const { result, rerender } = renderHook( ({ value }) => useDebounce(value, 300), { initialProps: { value: 'initial' } }, ); rerender({ value: 'updated' }); expect(result.current).toBe('initial'); act(() => { vi.advanceTimersByTime(300); }); expect(result.current).toBe('updated'); vi.useRealTimers(); }); ``` ## Mocking Patterns ```typescript // Service mock — mock the module, not the transport layer vi.mock('@/server-api/me/me.service', () => ({ MeService: { retrieveMe: vi.fn() }, })); // QueryClient wrapper for components using TanStack Query const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); return ({ children }: { children: React.ReactNode }) => ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ); }; render(<Component />, { wrapper: createWrapper() }); ``` ## Running Tests ```bash npx vitest # Watch mode npx vitest run # Single run (CI) npx vitest run src/features/ # Test specific directory npx vitest --coverage # Coverage report ```
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.