Front-End Testing with DOM Testing Library
Framework-agnostic DOM Testing Library patterns for behavior-driven testing. For React-specific patterns (renderHook, context, components), load the react-testing skill. For TDD workflow (RED-GREEN-REFACTOR), load the tdd skill.
Core Philosophy
Test behavior users see, not implementation details.
Testing Library exists to solve a fundamental problem: tests that break when you refactor (false negatives) and tests that pass when bugs exist (false positives).
Two Types of Users
Your UI components have two users:
-
End-users: Interact through the DOM (clicks, typing, reading text)
-
Developers: You, refactoring implementation
Kent C. Dodds principle: "The more your tests resemble the way your software is used, the more confidence they can give you."
Why This Matters
False negatives (tests break on refactor):
// ❌ WRONG - Testing implementation (will break on refactor) it("should update internal state", () => { const component = new CounterComponent(); component.setState({ count: 5 }); // Coupled to state implementation expect(component.state.count).toBe(5); });
Correct approach (behavior-driven):
// ✅ CORRECT - Testing user-visible behavior it("should submit form when user clicks submit", async () => { const handleSubmit = vi.fn(); const user = userEvent.setup();
render( <form id="login-form"> <label>Email: <input name="email" /></label> <button type="submit">Submit</button> </form> );
await user.type(screen.getByLabelText(/email/i), "test@example.com"); await user.click(screen.getByRole("button", { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalled(); });
Quick Reference
Topic Guide
Query selection priority and details queries.md
userEvent patterns and interactions user-events.md
Async testing (findBy, waitFor) async-testing.md
MSW for API mocking msw.md
Common mistakes and fixes anti-patterns.md
Accessibility-first testing principles accessibility-first-testing.md
When to Use Each Guide
Queries
Use queries.md when you need:
-
Query priority order (getByRole → getByLabelText → ...)
-
Query variant decisions (getBy vs queryBy vs findBy)
-
Common query mistakes and fixes
User Events
Use user-events.md when you need:
-
userEvent vs fireEvent guidance
-
userEvent.setup() pattern
-
Common interaction patterns (clicking, typing, keyboard)
Async Testing
Use async-testing.md when you need:
-
findBy queries for async elements
-
waitFor for complex conditions
-
waitForElementToBeRemoved
-
Loading states, API responses, debounced inputs
MSW
Use msw.md when you need:
-
Network-level API mocking
-
setupServer pattern
-
Per-test handler overrides
Anti-Patterns
Use anti-patterns.md when you need:
-
List of all common mistakes
-
Quick reference of what NOT to do
-
ESLint plugin setup
Accessibility-First Testing
Use accessibility-first-testing.md when you need:
-
Why accessible queries improve tests and accessibility
-
When to add ARIA attributes vs semantic HTML
-
Semantic HTML priority principles
Summary Checklist
Before merging UI tests, verify:
-
Using getByRole as first choice for queries
-
Using userEvent with setup() (not fireEvent )
-
Using screen object for all queries (not destructuring from render)
-
Using findBy* for async elements (loading, API responses)
-
Using jest-dom matchers (toBeInTheDocument , toBeDisabled , etc.)
-
Testing behavior users see, not implementation details
-
ESLint plugins installed (eslint-plugin-testing-library , eslint-plugin-jest-dom )
-
No manual cleanup() calls (automatic)
-
MSW for API mocking (not fetch/axios mocks)
-
Following TDD workflow (see tdd skill)
-
Using test factories for data (see testing skill)
-
For framework-specific patterns (React hooks, context, components), see react-testing skill