Skip to main content

front-end-testing

Behavior-driven UI testing patterns. Covers Vitest Browser Mode (preferred) and DOM Testing Library. Use when testing any front-end application, writing UI tests, querying DOM elements, or simulating user interactions. For React-specific patterns, see the react-testing skill. Use when this capability is needed.

Aller à l'installation

Informations de source

Dépôt
tomevault-io/tomes
Dernière activité de la source
23 juillet 2026 à 21:48
Langue détectée de SKILL.md
anglais
Étoiles
1
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
front-end-testing
description
Behavior-driven UI testing patterns. Covers Vitest Browser Mode (preferred) and DOM Testing Library. Use when testing any front-end application, writing UI tests, querying DOM elements, or simulating user interactions. For React-specific patterns, see the react-testing skill. Use when this capability is needed.
metadata
{"author":"citypaul"}
# Front-End Testing For React-specific patterns (components, hooks, context), load the `react-testing` skill. For TDD workflow, load the `tdd` skill. For general testing patterns (factories, public API testing), load the `testing` skill. ## Vitest Browser Mode (Preferred) **Always prefer Vitest Browser Mode over jsdom/happy-dom.** Tests run in a real browser (via Playwright), giving production-accurate behavior for CSS, events, focus management, and accessibility. ### Why Browser Mode Over jsdom | Aspect | jsdom/happy-dom | Browser Mode | |---|---|---| | Environment | Simulated DOM in Node.js | Real browser (Chromium/Firefox/WebKit) | | CSS | Not rendered | Real CSS rendering, layout, computed styles | | Events | Synthetic JS events | CDP-based real browser events | | APIs | Subset of Web APIs | Full browser API surface | | Focus/a11y | Approximate | Real focus management, accessibility tree | | Debugging | Console only | Full browser DevTools | ### Setup ```bash npm install -D vitest @vitest/browser-playwright ``` ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' import { playwright } from '@vitest/browser-playwright' export default defineConfig({ test: { browser: { enabled: true, provider: playwright(), headless: true, instances: [{ browser: 'chromium' }], }, }, }) ``` Quick setup wizard: `npx vitest init browser` ### Built-in Locators Vitest Browser Mode has built-in locators that mirror Testing Library queries. **No separate `@testing-library/dom` import needed.** ```typescript import { page } from 'vitest/browser' // These work exactly like Testing Library queries page.getByRole('button', { name: /submit/i }) page.getByText(/welcome/i) page.getByLabelText(/email/i) page.getByPlaceholder(/search/i) page.getByAltText(/logo/i) page.getByTestId('my-element') // Last resort only ``` ### Built-in Assertions with Retry Use `expect.element()` for DOM assertions — it **automatically retries** until the assertion passes or times out, reducing flakiness: ```typescript // ✅ CORRECT - Auto-retrying assertion await expect.element(page.getByText(/success/i)).toBeVisible() await expect.element(page.getByRole('button')).toBeDisabled() // Available matchers (no @testing-library/jest-dom needed): await expect.element(el).toBeVisible() await expect.element(el).toBeDisabled() await expect.element(el).toHaveTextContent(/text/i) await expect.element(el).toHaveValue('input value') await expect.element(el).toHaveAttribute('aria-label', 'Close') await expect.element(el).toBeChecked() ``` ### Built-in User Events (CDP-based) ```typescript import { userEvent } from 'vitest/browser' // Real browser events via Chrome DevTools Protocol await userEvent.click(page.getByRole('button', { name: /submit/i })) await userEvent.fill(page.getByLabelText(/email/i), 'test@example.com') await userEvent.keyboard('{Enter}') await userEvent.selectOptions(page.getByLabelText(/country/i), 'USA') await userEvent.clear(page.getByLabelText(/search/i)) ``` Or use locator methods directly: ```typescript await page.getByRole('button', { name: /submit/i }).click() await page.getByLabelText(/email/i).fill('test@example.com') ``` ### Multi-Project Setup (Node + Browser) When you need both unit tests (Node) and UI tests (browser): ```typescript export default defineConfig({ test: { projects: [ { test: { include: ['tests/unit/**/*.test.ts'], name: 'unit', environment: 'node', }, }, { test: { include: ['tests/browser/**/*.test.ts'], name: 'browser', browser: { enabled: true, provider: playwright(), instances: [{ browser: 'chromium' }], }, }, }, ], }, }) ``` ### Browser Mode Gotchas - **`vi.spyOn` on imports**: ES module namespaces are sealed in real browsers. Use `vi.mock('./module', { spy: true })` instead. - **`alert()`/`confirm()`**: Thread-blocking dialogs halt browser execution. Mock them with `vi.spyOn(window, 'alert').mockImplementation(() => {})`. - **`act()` not needed**: CDP events + `expect.element()` retry handle timing automatically. ### Playwright / Browser Mode Test Idempotency **All Playwright-style tests MUST be idempotent.** Every test must produce the same result regardless of execution order, how many times it runs, or what other tests ran before it. **Rules:** - Each test creates its own state from scratch — never depend on another test's side effects - Clean up any persistent state (database rows, localStorage, cookies) created during the test - Use unique identifiers (e.g., timestamp-based) to avoid collisions when tests run in parallel - Never assume the DOM is in a particular state at the start of a test — render fresh - If tests share a server or database, use isolation strategies (transactions, test-specific data) ```typescript // ❌ WRONG - Tests depend on shared state it('creates a user', async () => { await page.getByRole('button', { name: /create/i }).click() // Creates user "Alice" in the database }) it('lists users', async () => { // Assumes "Alice" exists from previous test! await expect.element(page.getByText('Alice')).toBeVisible() }) // ✅ CORRECT - Each test is self-contained it('creates and displays a user', async () => { const uniqueName = `User-${Date.now()}` await page.getByLabelText(/name/i).fill(uniqueName) await page.getByRole('button', { name: /create/i }).click() await expect.element(page.getByText(uniqueName)).toBeVisible() }) ``` **Why this matters:** Browser Mode can run tests in parallel across multiple browser instances. Non-idempotent tests will produce flaky failures that are nearly impossible to debug. --- ## Legacy: DOM Testing Library Patterns The patterns below apply when using `@testing-library/dom` directly (e.g., with jsdom). **Prefer Vitest Browser Mode** for new projects — the query patterns are identical but built-in. --- ## 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: 1. **End-users**: Interact through the DOM (clicks, typing, reading text) 2. **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): ```typescript // ❌ 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); }); ``` **False positives** (bugs pass tests): ```typescript // ❌ WRONG - Testing wrong thing it('should render button', () => { render('<button data-testid="submit-btn">Submit</button>'); expect(screen.getByTestId('submit-btn')).toBeInTheDocument(); // Button exists but onClick is broken - test passes! }); ``` **Correct approach** (behavior-driven): ```typescript // ✅ 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> <label>Password: <input name="password" type="password" /></label> <button type="submit">Submit</button> </form> `); document.getElementById('login-form').addEventListener('submit', (e) => { e.preventDefault(); handleSubmit(new FormData(e.target)); }); await user.type(screen.getByLabelText(/email/i), 'test@example.com'); await user.type(screen.getByLabelText(/password/i), 'password123'); await user.click(screen.getByRole('button', { name: /submit/i })); expect(handleSubmit).toHaveBeenCalled(); }); ``` This test: - Survives refactoring (state → signals → stores) - Tests the contract (what users see) - Catches real bugs (broken onClick, validation errors) --- ## Query Selection Priority **Most critical Testing Library skill: choosing the right query.** ### Priority Order Use queries in this order (accessibility-first): 1. **`getByRole`** - Highest priority - Queries by ARIA role + accessible name - Mirrors screen reader experience - Forces semantic HTML 2. **`getByLabelText`** - Form fields - Finds inputs by associated `<label>` - Ensures accessible forms 3. **`getByPlaceholderText`** - Fallback for inputs - Only when label not present - Placeholder shouldn't replace label 4. **`getByText`** - Non-interactive content - Headings, paragraphs, list items - Content users read 5. **`getByDisplayValue`** - Current form values - Inputs with pre-filled values 6. **`getByAltText`** - Images - Ensures accessible images 7. **`getByTitle`** - SVG titles, title attributes - Rare, when other queries unavailable 8. **`getByTestId`** - Last resort only - When no other query works - Not user-facing ### Query Variants Three variants for every query: **`getBy*`** - Element must exist (throws if not found) ```typescript // ✅ Use when asserting element EXISTS const button = screen.getByRole('button', { name: /submit/i }); expect(button).toBeDisabled(); ``` **`queryBy*`** - Returns null if not found ```typescript // ✅ Use when asserting element DOESN'T exist expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); // ❌ WRONG - getBy throws, can't assert non-existence expect(() => screen.getByRole('dialog')).toThrow(); // Ugly! ``` **`findBy*`** - Async, waits for element to appear ```typescript // ✅ Use when element appears after async operation const message = await screen.findByText(/success/i); ``` ### Common Mistakes ❌ **Using `container.querySelector`** ```typescript const button = container.querySelector('.submit-button'); // DOM implementation detail ``` ✅ **CORRECT - Query by accessible role** ```typescript const button = screen.getByRole('button', { name: /submit/i }); // User-facing ``` --- ❌ **Using `getByTestId` when role available** ```typescript screen.getByTestId('submit-button'); // Not how users find button ``` ✅ **CORRECT - Query by role** ```typescript screen.getByRole('button', { name: /submit/i }); // How screen readers find it ``` --- ❌ **Not using accessible names** ```typescript screen.getByRole('button'); // Which button? Multiple on page! ``` ✅ **CORRECT - Specify accessible name** ```typescript screen.getByRole('button', { name: /submit/i }); // Specific button ``` --- ❌ **Using getBy to assert non-existence** ```typescript expect(() => screen.getByText(/error/i)).toThrow(); // Awkward ``` ✅ **CORRECT - Use queryBy** ```typescript expect(screen.queryByText(/error/i)).not.toBeInTheDocument(); ``` --- ## User Event Simulation **Always use `userEvent` over `fireEvent`** for realistic interactions. ### userEvent vs fireEvent **Why userEvent is superior:** - Simulates complete interaction sequence (hover → focus → click → blur) - Triggers all associated events - Respects browser timing and order - Catches more bugs ```typescript // ❌ WRONG - fireEvent (incomplete simulation)
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub