React Testing Library skill for Codex
Find it by role, click it like a user. Codex tests React components through their accessible surface: role and label queries, awaited user-event interactions, findBy for loading states and a custom render that supplies the app's providers. It fixes act warnings by awaiting the real outcome rather than wrapping code in act, and treats a failed role query as a possible accessibility bug.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- react-testing-library Installs to ~/.agents/skills/react-testing-library/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install react-testing-library The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project react-testing-library Installs to .agents/skills/react-testing-library/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/react-testing-library.zipunzip -o react-testing-library.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) react-testing-library PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $react-testing-library, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Tests React components the way users interact with them using React Testing Library and user-event v14 - accessible query priority (getByRole first), findBy for async UI, queryBy for absence, within for scoping, custom render with providers, renderHook, jest-dom matchers, and fixes for act warnings. Use when writing or reviewing component tests in Jest or Vitest, when a query cannot find an element, tests emit "not wrapped in act(...)" warnings, or tests break whenever markup or class names change.
Use it when
- Writing component tests under Jest or Vitest with jsdom
- Queries cannot find elements or match too many
- Tests emit act warnings or hang with fake timers
- Tests break on markup or class-name changes
Use something else when
- The flow spans pages, real navigation or auth: use playwright-e2e
- You need to mock whole API surfaces for components: use api-mocking-msw
How to check the result
Tests pass without act warnings, still pass after a harmless markup change such as an extra wrapper div, and fail when an accessible label or handler is removed.
The mistake it prevents
Not awaiting user-event calls, or creating userEvent after render, which produces act warnings and assertions that run before the UI has updated.
A good component test renders with the same providers the app uses, finds elements by
role and accessible name, drives them with user-event, and asserts on what the user
can see or what the component calls back. It survives a markup refactor that keeps
behavior and accessibility intact.
Inspect first
- Read
AGENTS.md, the test runner config (Jest or Vitest) and the setup file that imports@testing-library/jest-dom. - Check versions in
package.json:@testing-library/react16+ needs@testing-library/dominstalled as a peer;@testing-library/user-eventshould be v14. - Look for an existing
test-utils.tsxwith a customrender. Use it; do not render bare components that need router, query client, store or theme. - If network calls are involved, see whether MSW is set up (
$api-mocking-msw).
Query priority
| Priority | Query | Typical target |
|---|---|---|
| 1 | getByRole(role, { name }) | buttons, links, headings, textboxes, checkboxes, dialogs, rows |
| 2 | getByLabelText | form fields with labels |
| 3 | getByPlaceholderText | only if no label exists (then report the missing label) |
| 4 | getByText | non-interactive text content |
| 5 | getByDisplayValue, getByAltText, getByTitle | specific cases |
| 6 | getByTestId | last resort: no accessible handle exists |
Variants:
| Prefix | Missing element | Multiple matches | Waits |
|---|---|---|---|
getBy | throws | throws | no |
queryBy | returns null | throws | no |
findBy | rejects after timeout | rejects | yes (polls, default 1000 ms) |
getAllBy / findAllBy | throws / rejects | returns array | findAll waits |
Use queryBy only to assert absence: expect(screen.queryByRole("alert")).not.toBeInTheDocument().
Baseline test
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
it("submits trimmed credentials", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn(); // jest.fn() under Jest
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), " [email protected] ");
await user.type(screen.getByLabelText("Password"), "s3cret!");
await user.click(screen.getByRole("button", { name: "Sign in" }));
expect(onSubmit).toHaveBeenCalledWith({ email: "[email protected]", password: "s3cret!" });
});
- Call
userEvent.setup()once per test, beforerender, andawaitevery interaction. - Prefer
user-eventoverfireEvent: it fires the full sequence (pointer, focus, keydown, input, keyup) and respects disabled elements. UsefireEventonly for events user-event does not model (for example a customtransitionend). - Query through
screen, not destructuredrenderresults; it stays valid after rerenders.
Async UI
it("shows the order after loading", async () => {
render(<OrderPage id="42" />, { wrapper: AppProviders });
expect(screen.getByRole("progressbar")).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "Order #42" })).toBeVisible();
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
});
findBy*for something that will appear.waitForonly for assertions that are not a single query (a mock being called, a count changing).- One assertion per
waitForcallback, no side effects inside it (no clicks), and never an empty callback. waitForElementToBeRemoved(() => screen.queryByText("Loading..."))for disappearance.
Scoping and lists
const row = screen.getByRole("row", { name: /INV-0042/ });
await user.click(within(row).getByRole("button", { name: "Download" }));
const dialog = await screen.findByRole("dialog", { name: "Delete invoice" });
await user.click(within(dialog).getByRole("button", { name: "Delete" }));
Accessible names come from labels, aria-label, aria-labelledby and text content.
When a role query fails, print what the tree exposes:
import { logRoles } from "@testing-library/dom";
logRoles(container); // container from render()
screen.debug(undefined, 20000);
Custom render with providers
// src/test/test-utils.tsx
import { render, type RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router";
export function renderWithProviders(
ui: React.ReactElement,
{ route = "/", ...options }: RenderOptions & { route?: string } = {},
) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }, // fail fast in tests
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>
</QueryClientProvider>,
options,
);
}
Create a new client or store per test; a shared one leaks cache between tests.
Hooks
import { renderHook, act } from "@testing-library/react";
it("toggles", () => {
const { result } = renderHook(() => useToggle(false));
act(() => result.current.toggle());
expect(result.current.on).toBe(true);
});
Test hooks directly only when they are reusable units. A hook used by one component is better tested through that component.
Fake timers with user-event
vi.useFakeTimers({ shouldAdvanceTime: true });
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
Without advanceTimers, user-event’s internal delays wait on frozen timers and the test
hangs. Under Jest pass jest.advanceTimersByTime.
Act warnings
“An update to X inside a test was not wrapped in act(…)” means state changed after the
test stopped waiting. Fix by awaiting the outcome, not by wrapping in act:
| Cause | Fix |
|---|---|
| Data loads after the last assertion | await screen.findBy... the loaded state |
| Interaction not awaited | await user.click(...) |
| Debounce or timer fires after test | fake timers and advance them, or await the result |
| Effect resolves after unmount | test ends correctly; cancel the effect in cleanup in the component |
jest-dom matchers worth using
toBeInTheDocument, toBeVisible, toBeDisabled, toHaveValue, toBeChecked,
toHaveAccessibleName, toHaveAccessibleDescription, toHaveAttribute, toHaveFocus,
toHaveTextContent. They read better and fail with clearer messages than
expect(el.disabled).toBe(true).
What not to test
- Implementation details: state variable names, hook call counts, class names, internal component instances.
- Snapshot of the full component tree as the only assertion.
- Third-party component internals (date picker popups); test your integration through their accessible surface.
Lint support
eslint-plugin-testing-library and eslint-plugin-jest-dom catch most misuse
(prefer-screen-queries, no-wait-for-side-effects, prefer-find-by,
no-node-access). Enable them if the repo lints tests.
Verification
- Run the test file in the project runner; no act warnings in the output.
- Rename a CSS class or wrap markup in an extra
div: the tests still pass. - Remove the button label or break the handler: a test fails with a message that names the missing role or call.
- Run the file several times; async tests must not depend on timing.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "React Testing Library"
short_description: "Find it by role, click it like a user"
default_prompt: "$react-testing-library Write tests for the LoginForm component with React Testing Library" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.