component-testing
Component Testing
Vitest Browser Mode with Playwright. Tests run in real Chromium — every DOM API, CSS layout, and browser behavior is authentic.
Conventions
- Co-locate tests:
foo.browser.test.tsxnext tofoo.tsx - Always wrap in
describenamed after the component - Use
test, notit render()is async — alwaysawaitit- Test behavior through rendered output, not implementation details
- Do NOT test shadcn/ui primitives — only test your own components that compose them
describe("SearchFilter", () => {
test("filters results on input change", async () => { ... });
test("shows empty state when no matches", async () => { ... });
});
When to Test
Test when a component has:
useStateor state management logicuseEffector side effects- Event handlers with conditional logic
- Conditional rendering based on props or state
- Complex prop transformations
Skip when a component:
- Is a pure style wrapper (just applies CSS classes)
- Has no logic — only passes props through
- Is a thin layout component
- Is a shadcn/ui primitive
Vitest Browser API
import { render } from "vitest-browser-react";
import { page } from "vitest/browser";
import { describe, expect, test, vi } from "vitest";
| API | Use |
|---|---|
await render(<C />) |
Mount component |
page.getByRole("button", { name }) |
Query by ARIA role |
page.getByText("text") |
Query by text content |
page.getByLabelText("label") |
Query by label |
locator.click() |
Click interaction |
locator.fill("value") |
Type into input |
await expect.element(locator) |
Auto-retry assertion (1s) |
expect(value) |
Synchronous assertion |
expect.element() vs expect()
| Use | When |
|---|---|
await expect.element(locator) |
Page locator queries — auto-retries until pass |
expect(value) |
Mock assertions, container.querySelector(), non-DOM values |
What NOT to Test
- CSS classes — test visible behavior instead
- Internal state — verify through rendered output
- Implementation details — don't test hook call order
Examples
See examples/ for complete test examples by pattern:
| Example | When to use |
|---|---|
examples/render-and-query.md |
Basic rendering, role and text queries |
examples/user-interactions.md |
Clicks, toggles, stateful interactions |
examples/form-testing.md |
Form inputs, submission, validation |
examples/conditional-rendering.md |
Show/hide content based on props or state |
examples/container-queries.md |
DOM structure, data-* attributes, container.querySelector() |
examples/compound-components.md |
Multi-part composed components |
Acceptance Checklist
- Test file uses
.browser.test.tsxextension - Co-located next to the component
- Wrapped in
describenamed after the component - Uses
test, notit -
render()is awaited - Uses
expect.element()for locator assertions - Tests behavior, not implementation details
- Mocks only at system boundaries
More from kvnwolf/devtools
commit
Wraps up work by syncing documentation, committing, pushing, and opening a pull request. Use when committing code, finishing a task, pushing changes, or creating a PR.
10base
Scaffolds a new TypeScript project from scratch. Use when starting a new project, bootstrapping a codebase, or setting up a project from zero.
10unit-testing
Writes unit tests following behavior-driven conventions with vitest. Use when creating tests, adding test coverage, or writing regression tests.
9tanstack-start
Provides instructions for working with TanStack Start projects. Use when setting up, configuring, developing, creating routes, or creating server functions in a TanStack Start application.
5convex-auth
Provides instructions for working with Better Auth in Convex projects. Use when setting up auth, configuring Better Auth, migrating to a local install, getting the current user, or signing out.
4convex-resend
Provides instructions for sending emails with Resend in Convex projects using @convex-dev/resend. Use when adding email functionality, setting up Resend, sending transactional emails, using React Email templates, configuring email webhooks, or tracking email delivery status.
3