src/skills/web-testing-react-testing-library/SKILL.md
React Testing Library patterns - query hierarchy, userEvent, async utilities, renderHook, custom render with providers, accessibility-first testing
npx skillsauth add agents-inc/skills web-testing-react-testing-libraryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Test React components through user interactions and accessible queries. Use
getByRoleas your primary query. PreferuserEventoverfireEvent. UsefindBy*for async content. Test user behavior, not implementation details.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use the query priority hierarchy: getByRole > getByLabelText > getByText > getByTestId)
(You MUST use userEvent instead of fireEvent for realistic user interactions)
(You MUST use findBy* queries for async content instead of waitFor + getBy*)
(You MUST test user-visible behavior, NOT implementation details like internal state)
(You MUST use screen object for queries, NOT destructured render returns)
</critical_requirements>
Auto-detection: React Testing Library, @testing-library/react, render, screen, userEvent, fireEvent, waitFor, findBy, getByRole, getByLabelText, renderHook, within, cleanup, prettyDOM, configure, logRoles, logTestingPlaygroundURL
When to use:
renderHookWhen NOT to use:
Key patterns covered:
withinDetailed Resources:
examples/ folder:
React Testing Library is built on the guiding principle: "The more your tests resemble the way your software is used, the more confidence they can give you."
Core Principles:
When to use React Testing Library:
When NOT to use:
Select queries based on accessibility hierarchy. This ensures tests align with how users (including those using assistive technology) interact with your UI.
// Priority 1: Accessible to Everyone
getByRole(); // BEST - queries accessibility tree
getByLabelText(); // Form fields - how users navigate forms
getByPlaceholderText(); // When no label (not ideal, but sometimes necessary)
getByText(); // Non-interactive content (divs, spans, paragraphs)
getByDisplayValue(); // Form elements by current value
// Priority 2: Semantic Queries
getByAltText(); // Images, areas, inputs with alt
getByTitle(); // Least reliable - not consistently read by screen readers
// Priority 3: Test IDs (Last Resort)
getByTestId(); // Only when other methods fail
See examples/core.md for complete query examples.
Why this hierarchy: Users interact with your app through visible text, labels, and semantic roles - not through test IDs or CSS classes. Testing this way ensures your app is accessible.
Use userEvent for realistic user interaction simulation. It triggers the full event chain that real interactions produce.
| Action | fireEvent | userEvent |
| -------- | --------------------- | ----------------------------------------------------------- |
| Typing | Single change event | keyDown, keyPress, keyUp per character |
| Clicking | Single click event | pointerDown, mouseDown, pointerUp, mouseUp, click |
| Focus | Manual management | Automatic focus management |
import userEvent from "@testing-library/user-event";
// Setup BEFORE interactions - creates isolated user session
const user = userEvent.setup();
// Then use throughout test
await user.click(button);
await user.type(input, "Hello");
See examples/user-events.md for complete userEvent examples.
Why userEvent: fireEvent dispatches DOM events directly, bypassing browser event handling. userEvent simulates actual user behavior, triggering the complete event chain including focus, keyboard, and pointer events.
Use findBy* queries for elements that appear asynchronously. Use waitFor only for assertions, not element queries.
// GOOD: findBy for async elements
const button = await screen.findByRole("button", { name: /submit/i });
// BAD: waitFor + getBy for async elements
await waitFor(() => {
screen.getByRole("button", { name: /submit/i }); // DON'T DO THIS
});
// GOOD: Single assertion in waitFor
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
// BAD: Multiple assertions in waitFor
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
expect(screen.getByText(/complete/i)).toBeInTheDocument(); // DON'T
});
// BAD: Side effects in waitFor
await waitFor(() => {
user.click(button); // DON'T - side effects outside waitFor
expect(result).toBe(true);
});
See examples/async-testing.md for complete async testing examples.
Why this matters: waitFor polls until the callback stops throwing. Multiple assertions or side effects in the callback cause unpredictable behavior and slower test failures.
Use renderHook for testing custom hooks in isolation. Prefer testing hooks through components when possible.
import { renderHook, act } from "@testing-library/react";
const { result } = renderHook(() => useCounter());
// Access current value
expect(result.current.count).toBe(0);
// Update state with act()
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme="dark">{children}</ThemeProvider>
);
const { result } = renderHook(() => useTheme(), { wrapper });
See examples/hooks.md for complete renderHook examples.
When to use renderHook:
When to prefer component testing:
Create a custom render function that wraps components with all necessary providers.
// test-utils.tsx
import { render, type RenderOptions } from "@testing-library/react";
import type { ReactElement } from "react";
interface AllProvidersProps {
children: React.ReactNode;
}
function AllProviders({ children }: AllProvidersProps) {
// Wrap with your app's providers in correct nesting order
// return (
// <ThemeProvider>
// <AuthProvider>
// {children}
// </AuthProvider>
// </ThemeProvider>
// );
return <>{children}</>;
}
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) {
return render(ui, { wrapper: AllProviders, ...options });
}
// Re-export everything
export * from "@testing-library/react";
export { customRender as render };
See examples/custom-render.md for complete custom render examples.
Why custom render: Avoids repeating provider boilerplate in every test. Creates a consistent test environment matching your app.
Use queries that enforce accessibility. If your test struggles to find an element, your UI likely has accessibility issues.
// GOOD: Tests that element is accessible
screen.getByRole("button", { name: /submit/i });
screen.getByRole("textbox", { name: /email/i });
screen.getByRole("checkbox", { name: /agree to terms/i });
screen.getByRole("combobox", { name: /country/i });
// GOOD: Verify accessible names
expect(screen.getByRole("button", { name: /submit/i })).toBeEnabled();
// BAD: Using test IDs when accessible queries work
screen.getByTestId("submit-button"); // DON'T when getByRole works
import { logRoles } from "@testing-library/react";
// Log all accessible roles in a container
logRoles(container);
See examples/accessibility.md for complete accessibility testing examples.
Why this matters: Screen readers and assistive technologies use the accessibility tree. Testing with accessible queries ensures your app works for all users.
Use debug utilities to understand what's rendered and troubleshoot failing tests.
// Debug entire document
screen.debug();
// Debug specific element
screen.debug(screen.getByRole("form"));
// Debug multiple elements
screen.debug(screen.getAllByRole("listitem"));
import { prettyDOM } from "@testing-library/react";
// Get formatted DOM string (for logging, assertions)
const domString = prettyDOM(element);
console.log(domString);
// Customize output length
const domString = prettyDOM(element, 15000); // Increase from 7000 default
import { logTestingPlaygroundURL } from "@testing-library/react";
// Logs URL to Testing Playground with current DOM
logTestingPlaygroundURL();
// Visit the URL to get suggested queries
When to use debug:
Remove before committing: Debug statements are for development only.
Use within to scope queries to a specific container element. Essential when testing components with repeated structures.
import { render, screen, within } from "@testing-library/react";
test("selects item in specific section", () => {
render(<Dashboard />);
// Get a specific section
const sidebar = screen.getByRole("navigation");
// Query only within that section
const homeLink = within(sidebar).getByRole("link", { name: /home/i });
expect(homeLink).toBeInTheDocument();
});
test("each row has edit button", () => {
render(<UserTable users={mockUsers} />);
const rows = screen.getAllByRole("row");
// Skip header row, check each data row
rows.slice(1).forEach((row) => {
const editButton = within(row).getByRole("button", { name: /edit/i });
expect(editButton).toBeInTheDocument();
});
});
See examples/scoped-queries.md for complete within() examples.
When to use within:
Configure Testing Library defaults for your project using configure.
import { configure } from "@testing-library/react";
// In test setup file
configure({
// Custom test ID attribute (default: "data-testid")
testIdAttribute: "data-test-id",
// Async utility timeout (default: 1000ms)
asyncUtilTimeout: 5000,
// Enable React strict mode warnings in tests
reactStrictMode: true,
});
import userEvent from "@testing-library/user-event";
// With fake timers - pass your test runner's timer advance function
const user = userEvent.setup({
advanceTimers: vi.advanceTimersByTime, // Required when using fake timers
});
// Skip pointer events check (for elements with pointer-events: none)
const user = userEvent.setup({
pointerEventsCheck: 0, // 0 = never check, 1 = check once, 2 = check per API
});
// Custom delay between events
const user = userEvent.setup({
delay: null, // null = no delay (faster tests)
});
See examples/configuration.md for complete configuration examples.
When to configure:
Test runner setup:
toBeInTheDocument, toHaveValue, etc.)afterEach(cleanup) neededMocking approach:
Framework providers:
<red_flags>
High Priority Issues:
getByTestId when accessible queries work - Indicates UI may not be accessible, test doesn't reflect user experiencewaitFor to find elements - Use findBy* instead, produces better error messages and cleaner codefireEvent for user interactions - Use userEvent for realistic event chainswaitFor - Causes slow test failures and unpredictable behaviorMedium Priority Issues:
screen - screen provides cleaner, more maintainable codecleanup calls - Modern frameworks handle cleanup automaticallyrender or fireEvent in act() - They already wrap in act, double-wrapping is unnecessaryquerySelector or CSS selectors - Use Testing Library queries for accessibility-aligned testsCommon Mistakes:
await userEvent methods (all are async in v14+)getBy* for elements that appear asynchronouslywaitFor callbacksconst user = userEvent.setup())Gotchas & Edge Cases:
userEvent.setup() must be called before any interactions (v14+ requirement)queryBy* returns null for missing elements (use for absence assertions only)findBy* has default timeout of 1000ms (configurable via options)result.current in renderHook is a ref - value updates on each accesswaitFor(() => {}) creates fragile timing-dependent testsscreen.debug() calls before committing - they are for development only</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST use the query priority hierarchy: getByRole > getByLabelText > getByText > getByTestId)
(You MUST use userEvent instead of fireEvent for realistic user interactions)
(You MUST use findBy* queries for async content instead of waitFor + getBy*)
(You MUST test user-visible behavior, NOT implementation details like internal state)
(You MUST use screen object for queries, NOT destructured render returns)
Failure to follow these rules will produce brittle tests that don't reflect real user interactions and miss accessibility issues.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety