This skill should be used when writing, reviewing, or debugging React component tests. It covers Testing Library query priority, user event simulation, form testing, custom hook testing, Playwright E2E testing, accessibility testing with axe-core, and keyboard navigation testing.
From mnpx claudepluginhub molcajeteai/plugin --plugin mThis skill uses the workspace's default tool permissions.
references/accessibility.mdreferences/component-testing.mdreferences/playwright.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Audits UI buttons and touchpoints by tracing state changes in handlers to find canceling side effects, race conditions, and inconsistent final states after refactors or for user-reported bugs.
Quick reference for testing React components. Builds on the typescript-testing skill (Vitest config, mocking, coverage) — this skill covers React-specific patterns. Reference files provide full examples and edge cases.
Always prefer queries that reflect how users see the page:
| Priority | Query | Example |
|---|---|---|
| 1 (best) | getByRole | screen.getByRole("button", { name: /guardar/i }) |
| 2 | getByLabelText | screen.getByLabelText(/correo/i) |
| 3 | getByText | screen.getByText(/bienvenido/i) |
| 4 | getByAltText | screen.getByAltText("Doctor photo") |
| Last resort | getByTestId | screen.getByTestId("complex-widget") |
import { renderWithI18n, screen } from "@drzum/ui/test";
import userEvent from "@testing-library/user-event";
it("submits login form", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
renderWithI18n(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/correo/i), "test@example.com");
await user.type(screen.getByLabelText(/contraseña/i), "password123");
await user.click(screen.getByRole("button", { name: /iniciar sesión/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
Rules:
userEvent.setup() before renderawait all user.* methodsuser.type not fireEvent.changeuser.click not fireEvent.click// Element should NOT exist
expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
// Element should exist
expect(screen.getByRole("button")).toBeInTheDocument();
// Wait for async element
const msg = await screen.findByText(/éxito/i);
See references/component-testing.md for form testing, async patterns, hook testing, GraphQL mocking, and anti-patterns.
it("shows validation errors", async () => {
const user = userEvent.setup();
renderWithI18n(<LoginForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole("button", { name: /iniciar sesión/i }));
expect(await screen.findByText(/correo.*requerido/i)).toBeInTheDocument();
});
it("disables button during submission", async () => {
const user = userEvent.setup();
renderWithI18n(<LoginForm onSubmit={() => new Promise((r) => setTimeout(r, 100))} />);
await user.type(screen.getByLabelText(/correo/i), "test@example.com");
await user.type(screen.getByLabelText(/contraseña/i), "pass");
await user.click(screen.getByRole("button", { name: /iniciar sesión/i }));
expect(screen.getByRole("button")).toBeDisabled();
});
import { renderHook, act } from "@testing-library/react";
it("debounces the value", () => {
vi.useFakeTimers();
const { result, rerender } = renderHook(
({ value }) => useDebounce(value, 300),
{ initialProps: { value: "hello" } }
);
rerender({ value: "world" });
expect(result.current).toBe("hello"); // Not yet debounced
act(() => vi.advanceTimersByTime(300));
expect(result.current).toBe("world"); // Debounced
vi.useRealTimers();
});
Use the project's renderWithI18n from @drzum/ui/test for all component tests:
import { renderWithI18n, screen } from "@drzum/ui/test";
it("renders in Spanish", () => {
renderWithI18n(<Welcome />, { locale: "es" });
expect(screen.getByText("Bienvenido")).toBeInTheDocument();
});
it("renders in English", () => {
renderWithI18n(<Welcome />, { locale: "en" });
expect(screen.getByText("Welcome")).toBeInTheDocument();
});
it("renders with router context", () => {
renderWithI18n(<Navigation />, { withRouter: true });
expect(screen.getByRole("link", { name: /inicio/i })).toHaveAttribute("href", "/");
});
// e2e/pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
readonly emailInput = this.page.getByLabel(/correo/i);
readonly submitButton = this.page.getByRole("button", { name: /iniciar sesión/i });
async goto() { await this.page.goto("/signin"); }
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.page.getByLabel(/contraseña/i).fill(password);
await this.submitButton.click();
}
}
Save auth state after setup, reuse in test projects:
// playwright.config.ts
projects: [
{ name: "setup", testMatch: /.*\.setup\.ts/ },
{
name: "authenticated",
dependencies: ["setup"],
use: { storageState: "e2e/.auth/patient.json" },
},
]
test("shows empty state", async ({ page }) => {
await page.route("**/patient/graphql", async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify({ data: { viewer: { appointments: [] } } }),
});
});
await page.goto("/appointments");
await expect(page.getByText(/no tienes citas/i)).toBeVisible();
});
await expect(...).toBeVisible() not waitForTimeoutdevices["Pixel 5"] projectSee references/playwright.md for config, fixtures, auth setup, visual assertions, and multi-step flows.
import { axe, toHaveNoViolations } from "vitest-axe";
expect.extend(toHaveNoViolations);
it("has no accessibility violations", async () => {
const { container } = renderWithI18n(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("follows logical tab order", async () => {
const user = userEvent.setup();
renderWithI18n(<LoginForm />);
await user.tab();
expect(screen.getByLabelText(/correo/i)).toHaveFocus();
await user.tab();
expect(screen.getByLabelText(/contraseña/i)).toHaveFocus();
await user.tab();
expect(screen.getByRole("button", { name: /iniciar sesión/i })).toHaveFocus();
});
import AxeBuilder from "@axe-core/playwright";
test("page has no a11y violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa"]).analyze();
expect(results.violations).toEqual([]);
});
<label> associationsaria-describedbySee references/accessibility.md for WCAG criteria, keyboard testing patterns, screen reader testing, and the full checklist.
After writing or modifying tests, run the full verification protocol:
pnpm --filter <app> validate
All 4 steps must pass. See typescript-writing-code skill for details.
| File | Description |
|---|---|
| references/component-testing.md | Testing Library queries, user events, form testing, hooks, async, GraphQL mocking |
| references/playwright.md | Config, Page Object Model, fixtures, auth state, API mocking, visual assertions |
| references/accessibility.md | axe-core, WCAG 2.1, keyboard testing, screen reader testing, focus management |