test-react-testing-library
Skill0 starsChanged 2 months agoMITView on GitHub
Tests React components with Vitest + Testing Library + happy-dom + axe-core (rendering, interaction, accessibility). Use when testing UI components, forms, or hooks that render in the DOM. Invoke for "Testing Library", "componente React", "render", "screen", "userEvent", "axe". DO NOT USE FOR unit tests, E2E tests, or backend tests.
In this repository20 other files it gives its agents.
- AGENTS.mdAGENTS.md
- code-backend-nestjsSkill
- code-frontend-library-reactSkill
- doc-changelog-generationSkill
- doc-design-generationSkill
- doc-domain-generationSkill
- doc-guardrail-generationSkill
- doc-policy-generationSkill
- doc-readme-generationSkill
- git-tbd-for-sddSkill
- git-worktrees-useSkill
- openspec-generate-baselineSkill
- openspec-init-configSkill
- security-auditSkill
- skill-masterSkill
- skill-test-evalsSkill
- test-cypress-cucumberSkill
- test-nestjs-jest-testing-moduleSkill
- test-nestjs-supertestSkill
- test-playwright-cucumberSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 5 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 18
Test React components with Vitest + Testing Library + happy-dom + axe-core, focusing on rendering, user interaction, and accessibility from …
- Distinctive: line 25
Check accessibility via role/label queries and axe-core.
- Distinctive: line 28
Not for backend tests or true browser E2E tests.
- Distinctive: line 29
Avoid testing implementation details (internal state, methods, lifecycle, child internals).
- Very specific: line 35
Test files co-located with the components they test (*.test.tsx).
- Distinctive: line 67
Use queries in this order of preference:
- Very specific: line 144
Always use @testing-library/user-event over fireEvent:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
React Testing Library Skill
Overview
Test React components with Vitest + Testing Library + happy-dom + axe-core, focusing on rendering, user interaction, and accessibility from the user's perspective.
Capabilities:
- Implement the RED phase tests of TDD.
- Render and query UI components, forms, and hooks in the DOM.
- Simulate real user interactions with
@testing-library/user-event. - Assert behavior with
findBy/waitForasync patterns and jest-dom matchers. - Check accessibility via role/label queries and axe-core.
Limitations:
- Not for backend tests or true browser E2E tests.
- Avoid testing implementation details (internal state, methods, lifecycle, child internals).
Prerequisites
- A React project with Vitest,
@testing-library/react,@testing-library/user-event, happy-dom and axe-core installed. @testing-library/jest-dommatchers configured in the test setup.- Test files co-located with the components they test (
*.test.tsx).
Examples
The patterns, query references, and snippets below show how to write tests with React Testing Library.
Core Philosophy
"The more your tests resemble the way your software is used, the more confidence they can give you."
Avoid testing:
- Internal state of components
- Internal methods
- Lifecycle methods
- Child component implementation details
Test instead:
- What users see and interact with
- Behavior from user's perspective
- Accessibility (queries by role, label)
Agreements tests location
- Unit / Component: Co-locate with same folder as the code (the code as *.test.ts). Colocate test files next to the component they test.
Query Priority
Use queries in this order of preference:
1. Accessible to Everyone (Preferred)
// Best — by ARIA role
getByRole("button", { name: /submit/i });
getByRole("textbox", { name: /email/i });
// Form fields — by label
getByLabelText("Email");
// Non-interactive content — by text
getByText("Welcome back!");2. Semantic Queries
// Images
getByAltText("Company logo");
// Title attribute (less reliable)
getByTitle("Close");3. Test IDs (Escape Hatch)
// Only when other queries don't work
getByTestId("custom-element");Query Types
| Type | No Match | 1 Match | >1 Match | Async |
getBy... | throw | return | throw | No |
queryBy... | null | return | throw | No |
findBy... | throw | return | throw | Yes |
getAllBy... | throw | array | array | No |
queryAllBy... | [] | array | array | No |
findAllBy... | throw | array | array | Yes |
When to use:
getBy*— element existsqueryBy*— element may not exist (assertions likeexpect(...).not.toBeInTheDocument())findBy*— element appears asynchronously
Basic Test Pattern
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("shows greeting after login", async () => {
const user = userEvent.setup();
render(<App />);
// Act — simulate user interactions
await user.type(screen.getByLabelText(/username/i), "john");
await user.click(screen.getByRole("button", { name: /login/i }));
// Assert — verify outcome
expect(await screen.findByText(/welcome, john/i)).toBeInTheDocument();
});User Events
Always use @testing-library/user-event over fireEvent:
import userEvent from "@testing-library/user-event";
test("user interactions", async () => {
const user = userEvent.setup();
// Click
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element);
// Type
await user.type(input, "Hello");
await user.clear(input);
// Select
await user.selectOptions(select, ["option1", "option2"]);
// Keyboard
await user.keyboard("{Enter}");
await user.keyboard("[ShiftLeft>]a[/ShiftLeft]"); // Shift+A
// Clipboard
await user.copy();
await user.paste();
// Pointer
await user.hover(element);
await user.unhover(element);
});Async Patterns
waitFor — Retry Until Success
await waitFor(() => {
expect(screen.getByText("Loaded")).toBeInTheDocument();
});
// With options
await waitFor(() => expect(callback).toHaveBeenCalled(), {
timeout: 5000,
interval: 100,
});findBy — Built-in waitFor
// Equivalent to: await waitFor(() => getByText('Loaded'))
const element = await screen.findByText("Loaded");waitForElementToBeRemoved
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));Common Patterns
Custom Render with Providers
// test-utils.tsx
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
import { AuthProvider } from "./AuthProvider";
function AllProviders({ children }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
const customRender = (ui, options) => render(ui, { wrapper: AllProviders, ...options });
export * from "@testing-library/react";
export { customRender as render };Testing Hooks
import { renderHook, act } from "@testing-library/react";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Rerender with New Props
const { rerender } = render(<Counter count={1} />);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
rerender(<Counter count={2} />);
expect(screen.getByText("Count: 2")).toBeInTheDocument();Query Within Container
import { within } from "@testing-library/react";
const modal = screen.getByRole("dialog");
const submitBtn = within(modal).getByRole("button", { name: /submit/i });Debugging
// Print entire DOM
screen.debug();
// Print specific element
screen.debug(screen.getByRole("button"));
// Log available roles
import { logRoles } from "@testing-library/react";
logRoles(container);
// With prettyDOM options
screen.debug(undefined, 10000); // max lengthjest-dom Matchers
// In a Vitest project always import the /vitest entry point — it is the one that
// registers the matchers on Vitest's `expect`.
import "@testing-library/jest-dom/vitest";
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEnabled();
expect(element).toBeDisabled();
expect(element).toHaveTextContent("Hello");
expect(element).toHaveValue("input value");
expect(element).toHaveAttribute("href", "/home");
expect(element).toHaveClass("active");
expect(element).toHaveFocus();
expect(element).toBeChecked();Configuration
import { configure } from "@testing-library/react";
configure({
// Custom test ID attribute
testIdAttribute: "data-my-test-id",
// Async timeout
asyncUtilTimeout: 5000,
// Default hidden
defaultHidden: true,
// Throw suggestions (debugging)
throwSuggestions: true,
});❌ Prohibitions (Anti-patterns)
// ❌ Don't query by class/id
container.querySelector(".my-class");
// ❌ Don't use container.firstChild
const { container } = render(<Component />);
expect(container.firstChild).toHaveClass("active");
// ❌ Don't use fireEvent when userEvent works
fireEvent.click(button); // Use userEvent.click instead
// ❌ Don't test implementation details
expect(component.state.loading).toBe(false);
// ❌ Don't use waitFor with findBy
await waitFor(() => screen.findByText("x")); // findBy already waits
// ❌ Don't assert inside waitFor callback (unless necessary)
await waitFor(() => {
expect(mockFn).toHaveBeenCalled(); // OK - need to wait for call
});✅ Best Practices
// ✅ Use screen for all queries
import { render, screen } from "@testing-library/react";
render(<Component />);
screen.getByRole("button"); // Good
// ✅ Prefer userEvent over fireEvent
const user = userEvent.setup();
await user.click(button);
// ✅ Use findBy for async elements
const element = await screen.findByText("Loaded");
// ✅ Use queryBy for non-existence assertions
expect(screen.queryByText("Error")).not.toBeInTheDocument();
// ✅ Use within for scoped queries
const form = screen.getByRole("form");
within(form).getByLabelText("Email");
// ✅ Use accessible queries (role, label, text)
getByRole("button", { name: /submit/i });TextMatch Options
// Exact match (default)
getByText("Hello World");
// Substring match
getByText("llo Worl", { exact: false });
// Regex
getByText(/hello world/i);
// Custom function
getByText((content, element) => {
return element.tagName === "SPAN" && content.startsWith("Hello");
});Quick Reference
| Import | Usage |
render | Render component to DOM |
screen | Query the rendered DOM |
cleanup | Unmount components (automatic in Vitest with globals: true) |
act | Wrap state updates |
renderHook | Test custom hooks |
within | Scope queries to element |
waitFor | Retry until assertion passes |
configure | Set global options |
userEvent.setup() | Create user event instance |
References
For more details, consult these reference files (loaded on demand):
| Topic | Link |
| Installation | references/testing-library-installation.md |
| Queries | references/testing-library-queries.md |
| User Events | references/testing-library-user-events.md |
| API | references/testing-library-api.md |
| Async | references/testing-library-async.md |
| Debugging | references/testing-library-debugging.md |
| Config | references/testing-library-config.md |
| Happy DOM | references/happy-dom-tests.md |
| Vitest | references/vitest-component.md |
| axe-core | references/axe-core.md |
| Type Testing | references/type-level-testing.md |
These files are only loaded if the agent needs additional context.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

