component-test-writer
Skill0 starsChanged 3 months agoMITView on GitHub
Write React component, hook, and end-to-end tests using Vitest, React Testing Library, MSW, and Playwright — chosen for behaviour coverage rather than line coverage, with accessible queries and no implementation-detail assertions. Use this skill whenever the user asks for tests, mentions Vitest, Jest, React Testing Library, MSW, Playwright, Cypress, test coverage, flaky tests, mocking API calls, or testing hooks, forms, async UI, or user flows — including "add tests for this component" and "why is this test flaky".
In this repository5 other files it gives its agents.
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Query priority: Use getByRole first, then getByLabelText for form fields, then getByText.
- Distinctive: line 43
Assert on output, never internals: No assertions on state variables, hook call counts, class names, or child component props.
- Very specific: line 90
Test through a component when the hook is used by one, since that is how it will actually run.
- Very specific: line 114
Use web-first assertions (toBeVisible, toHaveText) — they retry automatically.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Component Test Writer
A test suite is worth what it catches minus what it costs to maintain. Tests coupled to implementation break on every refactor while catching nothing, which is how teams end up with 90% coverage and no confidence. This skill writes tests against behaviour the user can observe.
Choose the level first
| Level | Tool | Use for | Keep it |
| Unit | Vitest | pure functions, reducers, formatters, validation | many, fast |
| Component | Vitest + RTL | rendering, interaction, conditional UI, a11y contract | most of the suite |
| Integration | RTL + MSW | data fetching, forms with real submit, routing | a solid layer |
| E2E | Playwright | critical revenue paths across real pages | few, high value |
The common failure is inversion: a hundred shallow component tests and no test that a user can actually check out. Ask what breaking would be most expensive, and start there.
Component tests
Query priority. Use getByRole first, then getByLabelText for form fields, then
getByText. getByTestId is the last resort. This is not stylistic — role-based queries
fail when the component becomes inaccessible, so the suite doubles as an accessibility
regression net.
Interact as a user. userEvent over fireEvent: it dispatches the full event sequence
(pointerdown, focus, keydown, input) that real interaction produces, so it catches bugs
fireEvent.change walks straight past.
const user = userEvent.setup();
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(await screen.findByRole('alert')).toHaveTextContent(/invalid credentials/i);Assert on output, never internals. No assertions on state variables, hook call counts, class names, or child component props. If the rendered output and the callbacks are correct, the internals are free to change — that is the entire value of the test.
Async. findBy* for appearance, waitForElementToBeRemoved for disappearance, and
waitFor only when neither fits. Never await new Promise(r => setTimeout(r, 500)); a
fixed sleep is either flaky on slow CI or wasted time on fast CI.
Network with MSW
Mock at the network boundary, not the module boundary. Mocking axios or a useQuery hook
tests your mock; intercepting HTTP tests your code.
export const server = setupServer(
http.get('/api/users/:id', ({ params }) =>
HttpResponse.json({ id: params.id, name: 'Ada Lovelace' }),
),
);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());onUnhandledRequest: 'error' is worth setting from day one — it turns a silently missing
handler into a clear failure instead of a mysterious empty render.
Override per test for the paths that matter more than the happy one:
server.use(http.get('/api/users/:id', () => new HttpResponse(null, { status: 500 })));What to test in a component
Cover these five and stop; anything beyond is usually restating the implementation:
- It renders the meaningful content for representative props
- Interaction produces the expected output or callback
- Conditional branches — empty, loading, error, permission-denied
- Accessibility contract — roles, names, and keyboard operability
- Edge inputs — empty list, very long string, zero, null
Skip: prop-type validation, that a library works, styling, and trivially derived values.
Hooks
Test through a component when the hook is used by one, since that is how it will actually
run. Use renderHook only for genuinely standalone hooks:
const { result } = renderHook(() => useCounter(5));
act(() => result.current.increment());
expect(result.current.count).toBe(6);Playwright
Reserve for flows where failure costs real money: signup, checkout, payment, the primary task of the app. Each one is expensive to run and to maintain.
test('completes checkout', async ({ page }) => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByRole('button', { name: 'Pay' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
});Use web-first assertions (toBeVisible, toHaveText) — they retry automatically. Never
waitForTimeout. Seed state through the API rather than the UI so a login page change does
not break twenty unrelated tests.
Diagnosing flaky tests
Flakiness is almost always one of five causes. Check in this order:
- Fixed timeouts → replace with
findBy*or a web-first assertion - Shared state between tests → reset handlers, storage, and the query cache in
afterEach - Unawaited async → a missing
awaiton auserinteraction or an assertion - Real timers with animations →
vi.useFakeTimers()or disable animations in E2E - Test order dependence → run with
--shuffle; a suite that fails when shuffled has hidden coupling
A quarantined flaky test is worse than a deleted one — it trains the team to ignore red.
Setup
references/setup.md has the Vitest config, the shared setup file, a provider-aware custom
render, MSW wiring, and Playwright config with auth-state reuse. Read it before writing the
first test in a repo that has no test infrastructure yet — the provider render and the
per-test QueryClient in particular prevent a class of order-dependent failures that is
painful to debug later.
Output format
Deliver: the test file, any MSW handlers or fixtures it needs, the run command, and one short note on what is deliberately not covered and why. That last line is what makes a reviewer trust the rest.
Aim for coverage of behaviour, and treat a line-coverage percentage as a diagnostic rather than a target — chasing the number produces tests that assert nothing.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

