Write component tests using Vitest Browser Mode with Playwright for real-browser testing. Consult this skill whenever testing React components, writing browser-based tests, verifying UI behavior, testing forms or interactive elements, or deciding whether a component needs a test.
Install
npx skillscat add kvnwolf/devtools/create-component-test Install via the SkillsCat registry.
Component Tests
Write component tests using Vitest Browser Mode with Playwright for real-browser testing. Tests run in a real Chromium instance, not jsdom — every DOM API, CSS layout, and browser behavior is authentic.
When to Test a Component
A component merits a test when it has:
useStateor state management logicuseEffector side effects- Event handlers with conditional logic
- Conditional rendering based on props or state
- Complex prop transformations
A component does NOT need a test when it:
- Is a pure style wrapper (just applies CSS classes)
- Has no logic — only passes props through
- Is a thin layout component
Imports
import { render } from "vitest-browser-react";
import { page } from "vitest/browser";
import { describe, expect, test } from "vitest";Core Patterns
Render and Query
test("renders heading", async () => {
render(<Welcome name="Alice" />);
await expect.element(page.getByRole("heading")).toHaveTextContent("Hello, Alice");
});User Interactions
test("increments counter on click", async () => {
render(<Counter />);
await expect.element(page.getByText("Count: 0")).toBeInTheDocument();
await page.getByRole("button", { name: "Increment" }).click();
await expect.element(page.getByText("Count: 1")).toBeInTheDocument();
});Form Testing
test("submits form with user input", async () => {
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await page.getByLabelText("Email").fill("alice@test.com");
await page.getByLabelText("Password").fill("secret123");
await page.getByRole("button", { name: "Sign in" }).click();
expect(onSubmit).toHaveBeenCalledWith({
email: "alice@test.com",
password: "secret123",
});
});Auto-Retry with expect.element()
expect.element() automatically retries assertions until they pass (default 1s timeout). Use it for DOM queries that may need to wait for renders:
// Auto-retries — use for DOM assertions
await expect.element(page.getByText("Loading...")).toBeInTheDocument();
await expect.element(page.getByText("Data loaded")).toBeInTheDocument();
// No retry — use for non-DOM values
expect(mockFn).toHaveBeenCalledOnce();Compound Component Testing
For shadcn compound components (e.g., Empty.Root, Empty.Title), test the assembled component as users would see it:
test("renders empty state with title and description", async () => {
render(
<Empty.Root>
<Empty.Header>
<Empty.Title>No results</Empty.Title>
<Empty.Description>Try adjusting your search.</Empty.Description>
</Empty.Header>
</Empty.Root>
);
await expect.element(page.getByText("No results")).toBeInTheDocument();
await expect.element(page.getByText("Try adjusting your search.")).toBeInTheDocument();
});Testing Conditional Rendering
test("shows error message for invalid input", async () => {
render(<EmailInput />);
await page.getByLabelText("Email").fill("not-an-email");
await page.getByRole("button", { name: "Submit" }).click();
await expect.element(page.getByText("Invalid email address")).toBeInTheDocument();
});
test("hides error message for valid input", async () => {
render(<EmailInput />);
await page.getByLabelText("Email").fill("valid@test.com");
await page.getByRole("button", { name: "Submit" }).click();
await expect.element(page.getByText("Invalid email address")).not.toBeInTheDocument();
});What NOT to Test
- CSS classes — Don't assert on
className. Test visible behavior instead. - Internal state — Don't reach into component internals. Verify through rendered output.
- Implementation details — Don't test which hooks are called or in what order.