# Component Test Writer

> 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".

- Skill: `jayeshsojitra103/component-test-writer` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add jayeshsojitra103/component-test-writer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jayeshsojitra103/component-test-writer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: jayeshsojitra103 (https://skillmd.com/u/jayeshsojitra103)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jayeshsojitra103/component-test-writer

---


# 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.

```tsx
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.

```ts
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:

```ts
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:

1. It renders the meaningful content for representative props
2. Interaction produces the expected output or callback
3. Conditional branches — empty, loading, error, permission-denied
4. Accessibility contract — roles, names, and keyboard operability
5. 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:

```tsx
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.

```ts
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:

1. **Fixed timeouts** → replace with `findBy*` or a web-first assertion
2. **Shared state between tests** → reset handlers, storage, and the query cache in `afterEach`
3. **Unawaited async** → a missing `await` on a `user` interaction or an assertion
4. **Real timers with animations** → `vi.useFakeTimers()` or disable animations in E2E
5. **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.

