testing-library
SkillProductivityTesting Library patterns for user-centric, accessibility-driven component testing. Covers query priority, user events, async utilities, and practical patterns for testing React, Vue, and DOM components the way users interact with them. Use when the task involves `Testing Library`, `@testing-library/react`, `React component testing`, `accessible testing`, or `user-centric testing`.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the testing-library skill
What this skill tells your AI
The instructions your AI receives, as published by yacosta738/yacosta738.github.io in .agents/skills/testing-library/SKILL.md and read by ahel’s review.
When to Use
- Testing UI components with
@testing-library/react,@testing-library/vue, or@testing-library/dom. - Choosing the right query method for elements.
- Simulating user interactions (typing, clicking, selecting).
- Testing async behavior (data fetching, loading states, debounced input).
- Writing tests that enforce and verify accessibility.
Critical Patterns
- Test User Behavior, Not Implementation: Your tests should resemble how users interact with your app. Query by role, label, and text — not by class, test ID, or component internals.
- Query Priority: Follow this order strictly — it maps to accessibility:
getByRole— accessible roles (button, textbox, heading)getByLabelText— form fields with labelsgetByPlaceholderText— when no label existsgetByText— visible text contentgetByDisplayValue— current input valuesgetByAltText— imagesgetByTitle— title attributegetByTestId— LAST RESORT only
- userEvent Over fireEvent: Always use
@testing-library/user-eventfor interactions. It simulates real browser behavior (focus, keydown, keyup, input) whilefireEventdispatches a single synthetic event. - No Implementation Details: NEVER assert on component state, props, or internal methods. NEVER query by class name or component structure.
- Async by Default: Use
findByqueries (which return Promises) or wrap assertions inwaitForfor any UI that updates asynchronously.
Code Examples
Query Priority in Practice
import { render, screen } from "@testing-library/react";
import { SignupForm } from "./SignupForm";
it("renders the signup form with accessible elements", () => {
render(<SignupForm />);
// BEST — getByRole with accessible name
screen.getByRole("heading", { name: /create account/i });
screen.getByRole("textbox", { name: /email/i });
screen.getByRole("button", { name: /sign up/i });
// GOOD — getByLabelText for form fields
screen.getByLabelText(/password/i);
// ACCEPTABLE — getByText for static content
screen.getByText(/already have an account/i);
// LAST RESORT — only when element has no accessible role or text
screen.getByTestId("captcha-widget");
});
User Events — The Right Way
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SearchBar } from "./SearchBar";
describe("SearchBar", () => {
it("calls onSearch when user types and submits", async () => {
const user = userEvent.setup(); // Always call setup() first
const onSearch = jest.fn();
render(<SearchBar onSearch={onSearch} />);
const input = screen.getByRole("searchbox", { name: /search/i });
await user.type(input, "testing library");
await user.keyboard("{Enter}");
expect(onSearch).toHaveBeenCalledWith("testing library");
});
it("clears input when clear button is clicked", async () => {
const user = userEvent.setup();
render(<SearchBar onSearch={jest.fn()} />);
const input = screen.getByRole("searchbox", { name: /search/i });
await user.type(input, "some query");
expect(input).toHaveValue("some query");
await user.click(screen.getByRole("button", { name: /clear/i }));
expect(input).toHaveValue("");
});
});
Async Testing Patterns
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { UserProfile } from "./UserProfile";
it("loads and displays user data", async () => {
render(<UserProfile userId="1" />);
// findBy queries wait for the element to appear (default timeout: 1000ms)
const heading = await screen.findByRole("heading", { name: /alice/i });
expect(heading).toBeInTheDocument();
});
it("shows error state when fetch fails", async () => {
server.use(
rest.get("/api/users/1", (_req, res, ctx) =>
res(ctx.status(500))
)
);
render(<UserProfile userId="1" />);
// waitFor retries the assertion until it passes
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent(/failed to load/i);
});
});
it("shows loading then content", async () => {
render(<UserProfile userId="1" />);
// Loading state appears first
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Content replaces loading
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
expect(screen.getByRole("heading", { name: /alice/i })).toBeInTheDocument();
});
Testing Forms
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ContactForm } from "./ContactForm";
describe("ContactForm", () => {
it("submits form with valid data", async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<ContactForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/name/i), "Alice");
await user.type(screen.getByLabelText(/email/i), "alice@example.com");
// Select from dropdown — use selectOptions
await user.selectOptions(
screen.getByRole("combobox", { name: /topic/i }),
"support"
);
// Checkbox
await user.click(screen.getByRole("checkbox", { name: /agree to terms/i }));
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({
name: "Alice",
email: "alice@example.com",
topic: "support",
agreedToTerms: true,
});
});
it("displays validation errors for empty required fields", async () => {
const user = userEvent.setup();
render(<ContactForm onSubmit={jest.fn()} />);
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(screen.getByText(/name is required/i)).toBeInTheDocument();
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
});
Testing Modals and Portals
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ConfirmDialog } from "./ConfirmDialog";
it("opens and confirms deletion", async () => {
const user = userEvent.setup();
const onConfirm = jest.fn();
render(<ConfirmDialog onConfirm={onConfirm} trigger="Delete Item" />);
// Open dialog
await user.click(screen.getByRole("button", { name: /delete item/i }));
// Scope assertions to the dialog
const dialog = screen.getByRole("dialog");
expect(within(dialog).getByText(/are you sure/i)).toBeInTheDocument();
// Confirm
await user.click(within(dialog).getByRole("button", { name: /confirm/i }));
expect(onConfirm).toHaveBeenCalledTimes(1);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
Accessibility Assertions
import { render } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import { Navigation } from "./Navigation";
expect.extend(toHaveNoViolations);
it("has no accessibility violations", async () => {
const { container } = render(<Navigation />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Custom Render with Providers
// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
function AllProviders({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>{children}</ThemeProvider>
</QueryClientProvider>
);
}
export function renderWithProviders(
ui: React.ReactElement,
options?: Omit<RenderOptions, "wrapper">,
) {
return render(ui, { wrapper: AllProviders, ...options });
}
// Re-export everything from testing-library
export * from "@testing-library/react";
export { renderWithProviders as render };
Best Practices
DO
- Use
screenfor all queries — it's always scoped todocument.bodyand requires no destructuring. - Use
within()to scope queries to a specific container (dialog, list item, section). - Use
getByRoleas your default query — it verifies accessibility and finds elements reliably. - Use regex with case-insensitive flag (
/submit/i) for text matching — it's resilient to casing changes. - Create a custom
renderfunction that includes your app's providers (theme, router, query client). - Use MSW (Mock Service Worker) for API mocking — it works at the network level and shares mocks between tests and development.
DON'T
- DON'T use
container.querySelector(".my-class")— it tests implementation details, not user behavior. - DON'T use
fireEventwhenuserEventis available —fireEventdoesn't simulate real browser behavior. - DON'T use
getByTestIdas your first choice — it means the element lacks proper accessible roles or labels. Fix the component first. - DON'T assert on component state or hooks directly — test what the user sees and interacts with.
- DON'T use
act()manually in most cases —userEventandfindByhandle it internally. - DON'T use
waitForwithgetByinside — usefindByinstead, which isgetBy+waitForcombined. - DON'T wrap non-async operations in
waitFor— it adds unnecessary complexity and hides synchronous bugs.
Signals
- GitHub stars
- 48
- Forks
- 7
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
testing-library- Source
- github.com/yacosta738/yacosta738.github.io