frontend-testing
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
getByRoleas first choice for queries - Using
userEventwithsetup()(notfireEvent) - Using
screenobject for all queries (not destructuring from render) - Using
findBy*for async elements (loading, API responses) - Using
jest-dommatchers (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
tddskill) - Using test factories for data (see
testingskill) - For framework-specific patterns (React hooks, context, components), see
react-testingskill