frontend-testing
USE WHEN testing non-React UI (vanilla JavaScript or TypeScript DOM code, a web component, or a Vue, Svelte or Angular component) with DOM Testing Library in Vitest or Jest with jsdom: writing, fixing or reviewing a test, or a request to add an end-to-end journey or a layout check to that suite; NOT WHEN the code or project uses React or React Testing Library (use web:react-testing), for XState actors (web:xstate), or for Playwright, Cypress or screenshot-comparison work in a real browser; covers queries, accessible names, userEvent, async results, network stubs and assertions that pass for the wrong reason, and declines what jsdom cannot prove while offering the part that fits.
- License 许可
- MIT
- 所属仓库
- mintuz/skills
- 最近更新
- 2026-11-13
- Stars
- ★ 35
- Forks
- 7
01 前言 Front Matter
- name
- frontend-testing
- description
- USE WHEN testing non-React UI (vanilla JavaScript or TypeScript DOM code, a web component, or a Vue, Svelte or Angular component) with DOM Testing Library in Vitest or Jest with jsdom: writing, fixing or reviewing a test, or a request to add an end-to-end journey or a layout check to that suite; NOT WHEN the code or project uses React or React Testing Library (use web:react-testing), for XState actors (web:xstate), or for Playwright, Cypress or screenshot-comparison work in a real browser; covers queries, accessible names, userEvent, async results, network stubs and assertions that pass for the wrong reason, and declines what jsdom cannot prove while offering the part that fits.
02 资源文件 6
- 参考文档references/accessibility-first-testing.md1.3 KB
- 参考文档references/anti-patterns.md3.7 KB
- 参考文档references/async-testing.md4.1 KB
- 参考文档references/msw.md2.5 KB
- 参考文档references/queries.md2.6 KB
- 参考文档references/user-events.md1.9 KB
03 SKILL.md 正文
Front-end testing with DOM Testing Library
Write behaviour-first DOM Testing Library tests for UI built without React. For React components, hooks and providers, load web:react-testing. For the TDD workflow, load tdd.
Scope
This skill covers tests that run against a DOM inside a test runner, such as Vitest or Jest with jsdom. Decline these parts of a request:
- End-to-end and cross-page journeys: cross-document navigation, redirects, real servers, downloads, extra browser tabs, and real cookie behaviour such as
HttpOnly,SecureandSameSite. Reasons: jsdom does not navigate between documents or open real tabs. A real server sets anHttpOnlycookie, and page scripts cannot read it, sodocument.cookiein jsdom cannot prove it. Exception: jsdom holds a URL and a cookie jar, so a client-side URL ordocument.cookiecheck inside one document is in scope. - Visual appearance: computed layout, real fonts, screenshots and visual-regression diffs. Reason: jsdom does not lay out or paint, so it cannot report a size, a position or a rendered colour.
To decline (principle-state-the-scope):
- For each declined part, write its reason from the list above. A named replacement tool is not a reason.
- Name the replacement: Playwright or Cypress for a journey; a screenshot or visual-regression comparison in a real browser for appearance.
- Offer the part of the request that fits in the DOM, if any.
Do not offer a jsdom test that fakes the navigation, the server or the rendering as an equivalent.
@testing-library/dom exports no render. Mount the markup yourself. When a framework wrapper such as @testing-library/vue supplies render, mount through it; this skill still governs the queries and the interactions.
Use the project's test libraries
Use the runner, matchers, network stub and helpers the project already has. Do not add a testing dependency or a lint plugin unasked (principle-reuse-before-building).
- Interactions: Use
userEventfrom@testing-library/user-event, created withuserEvent.setup()in each test. UsefireEventonly for an event thatuserEventdoes not support, or when the project shows thatuser-eventis not installed. In the second case, say thatuser-eventwould model the interaction more closely. - Matchers: Use jest-dom matchers such as
toBeInTheDocument()andtoBeDisabled(). Only when the project shows that its setup does not register jest-dom, assert absence withtoBeNull(), text withtextContent, and state with the element's own property, such asdisabled. - Network: Use the project's existing stub: its MSW
serverand handlers, otherwise its own stub helper. Override an MSW handler for one test withserver.use(...). When the project has an MSW server, do not add afetchoraxiosspy. When the code receives its request function by injection, pass a mock for that function. - Lint: When
eslint-plugin-testing-libraryandeslint-plugin-jest-domare missing, recommend them. Install them only when asked.
Test behaviour, not implementation
Test what users see and do through the DOM: clicks, typing and visible text (principle-test-behavior-not-implementation). For each assertion, ask which plausible wrong implementation would still pass. Write the assertion that fails on it.
Mount the real component under test. Let it own its markup and event wiring. Do not read or set its internal state. Do not write your own listener and then assert that it ran. Return a runnable test that imports the code under test. When the task does not give the module path, import from an assumed path beside the test file and say that the path is assumed.
Queries and interactions
- Choose the variant by timing:
getBy*for state already present,findBy*for state that appears later,queryBy*for absence. - Query through
screen. Usewithin(element)to scope a query to one region. Do not bind queries to a container, for example withgetQueriesForElement. Do not use a CSS class selector when an accessible query reaches the element. - Re-query an element by its new role or accessible name when you must prove it is discoverable under that identity, or when the update can replace the node. An existing reference stays valid when the same node is reused.
- Interact once, outside
waitForand every other retrying callback. Put only assertions inside a retrying callback. - Wait for an observable state with
findBy*orwaitFor. Do not put a fixed delay in place of that wait, such assetTimeout, a sleep promise or a fake-timer advance. - Reset markup that you mounted into
document.bodyyourself between tests, for example withdocument.body.innerHTML = ''inafterEach. Do not callcleanup()in a test file.
Accessible names come from the markup
A role query's name option matches the accessible name, not the text. These roles take a name only from aria-label or aria-labelledby, never from their content: status, alert, region, dialog, navigation and form. Without those attributes they have no name, and { name: /order placed/i } finds nothing. Find such an element by role alone, then assert its text:
const status = await screen.findByRole("status");
expect(status).toHaveTextContent(/order placed/i);
Use this form only when the role first appears in its terminal state. When an earlier state uses the same role, such as a pending status, findByRole returns that earlier element at once. Then retry on the terminal text:
await waitFor(() => expect(screen.getByRole("status")).toHaveTextContent(/order placed/i));
Use { name } for roles named by their content, such as button, link, heading and option. <input type="password"> has no ARIA role, so getByRole('textbox') does not find it. Find it with getByLabelText.
When no accessible query fits
Work down the priority order in queries.md. When an element exposes no role, label, text, alt text or title:
- Name the queries you tried and say why each fails on the supplied markup.
- When you own the markup, fix it. Prefer semantic HTML, then a
<label>, then an ARIA attribute. - When you cannot change the markup, for example a third-party widget, use
getByTestId. Record the reason in a comment beside the query. - Read raw attributes and descendants with plain DOM access on the element you already hold, such as
element.querySelector('canvas')andgetAttribute('data-render-count').screenhas no attribute query;screen.getByAttributedoes not exist.queryHelpersandbuildQueriescan build one; do this only for a pattern repeated across many tests.
Never assume a role, label or accessible name that the supplied markup lacks; the test then fails on the real component. Never invent a query name. The built-in queries on screen are the eight *By* families in queries.md, each with its get, query, find and All forms.
Prove the whole rule
A test proves a rule only when it observes both sides of that rule.
- Call counts: Assert
toHaveBeenCalledTimes(n).toHaveBeenCalledWith(...)proves the arguments of one call, not how many calls happened. - Conditional enablement: Assert that the control is disabled while its condition is unmet, and enabled once the condition is met.
- Repeated interactions: Assert the full state cycle for every repetition. A rising call count does not prove that the second attempt reached the same pending and terminal states.
- Absence: First await a state that exists only after the async work finished, such as the success text. Then assert absence. An absence assertion straight after an interaction can run before the component reacts, so it passes whether the component is correct or broken. Assert absence with
queryBy*, which returns null when nothing matches; never with a throwinggetBy*insideexpect(...).toThrow().queryBy*still throws when several elements match. Only when the supplied markup can render several matches, usequeryAllBy*and assert an empty array.waitForElementToBeRemovedthrows when the element is already gone at call time; use it only for a removal that has not happened yet.
References
Read a reference when the task needs its detail:
| Reference | Read for |
|---|---|
| queries.md | Query priority, variants, query mistakes |
| user-events.md | userEvent against fireEvent, userEvent.setup(), clicking, typing, keyboard, fake timers |
| async-testing.md | findBy*, waitFor, waitForElementToBeRemoved; loading states, API responses, debounced inputs |
| msw.md | MSW setupServer, per-test handler overrides |
| anti-patterns.md | Mistakes and fixes, cleanup, ESLint plugin configuration |
| accessibility-first-testing.md | ARIA against semantic HTML |
04 同仓库更多 Skills
xstate
WHEN React state changes in response to an external answer — async submit, in-flight or loading flags, retries, cancellation, polling, optimistic updates, stale responses — or to an interaction with an interruptible middle, such as a drag or a gesture; or WHEN building, reviewing, testing, or visualising XState 5 actors and @xstate/react integrations; NOT for a value one event sets completely, such as a text draft, a focus flag, or a plain open and closed toggle; models typed lifecycles and renders legal Mermaid stateDiagram-v2 charts.
web-design
WHEN refining UI layout, typography, color, or polish; NOT code implementation; provides concise principles for intentional, legible design.
tailwind
USE WHEN writing, fixing or reviewing styling in a project that uses Tailwind CSS, even a one-class change or a request that does not name Tailwind: colours, focus rings, spacing, class names chosen by a prop, map or CVA variants, theme tokens and dark mode in the Tailwind stylesheet or tailwind.config, setup or upgrades (if unsure whether the project uses Tailwind, check package.json or the main stylesheet first); NOT WHEN writing plain CSS or Sass in a project without Tailwind; produces class names Tailwind can find, syntax for the installed major version, visible focus indicators, and no unrequested dependency or invented brand value.
refactoring
WHEN you need a plan to refactor, restructure, or clean up working code and the test result is known, either green or a recorded failing set; NOT for new feature delivery or any behavior change; reversible-baseline safety with prioritized improvements.
react
USE WHEN changing the logic, data, props, state, Effects or rendered text of a React component, hook or page, even a one-line change; NOT WHEN the request is styling only (class names, Tailwind, CSS, dark mode, spacing, shadows) or tests only, or when it models submit, in-flight, loading, retry or failure states, even for a named React form (xstate covers that, including its React wiring); applies React 19 composition, state, Effect, structure, API and performance rules with only the project's installed libraries.
react-testing
USE WHEN writing, fixing or reviewing a React Testing Library test for a component, hook or context provider, even one small test, or when asked for an end-to-end journey inside a Vitest or React Testing Library suite; NOT WHEN changing the component itself (react), testing XState actors or @xstate/react (xstate), or writing a Playwright or Cypress test; covers renderHook, providers, async results, forms and false-pass anti-patterns, and declines end-to-end journeys with the testable slices.