Test frontend behavior and change tests, stories, fixtures, mocks, or test configuration. Load when behavior needs coverage; always pair with write-frontend.
Installs into .claude/skills of the current project.
Are you the author of Write Frontend Tests?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/a-novel-kit-write-frontend-tests)
---
name: write-frontend-tests
description: >
Test frontend behavior and change tests, stories, fixtures, mocks, or test configuration. Load
when behavior needs coverage; always pair with write-frontend.
---
# Frontend Test Conventions
Load `write-frontend`, plus `write-svelte` for Svelte targets and `write-design-system` for uikit.
Read the production behavior before writing tests. Read neighboring tests
and the official documentation for the installed testing tools. Preserve existing tests unless the
behavior they specify has been intentionally removed.
## Timing
Follow [develop-feature](../develop-feature/SKILL.md). Early stories make a draft reviewable; early
tests should only resolve uncertainty, reproduce a defect, or protect a risky invariant. Complete
the behavior matrix and full relevant regression suite after issue scope is approved. These
conventions govern the quality of any test written at either stage, without requiring exhaustive
coverage during exploration.
## Test at the nearest truthful layer
Use the smallest layer that proves the user-visible contract:
1. Test pure parsing, formatting, and state transitions as unit tests.
2. Test component rendering and interaction through the DOM with Testing Library.
3. Use Storybook stories as the exhaustive catalog of reusable component states; add interaction and
accessibility assertions where supported.
4. Use real-browser end-to-end tests for routing, focus across pages, browser APIs, hydration,
multi-component journeys, and behavior an emulated DOM cannot prove.
Do not duplicate the same assertion at every layer. Keep one strong contract test and add broader
tests only for integration risks.
## User-centered assertions
- Query elements by accessible role and name first, then label, text, or other user-visible meaning.
Use test IDs only when no semantic query exists.
- Interact as a user would: focus, type, click, submit, and press keys. Do not call component methods
or mutate internal state to reach a condition.
- Assert observable output, accessible state, navigation, focus, and network contract. Do not assert
private classes, framework internals, reactive calls, or implementation sequence.
- Use async queries for async UI. Wait for the condition the user observes, not an arbitrary timeout.
- Cover keyboard behavior and focus order for every custom interaction pattern.
- Prefer explicit assertions over broad snapshots. Use a snapshot only for a stable serialized
contract whose meaningful review is easier as a whole than as targeted assertions.
## Behavior matrix
Cover the states relevant to the changed contract:
- Default, supported variants, and boundary values.
- Loading, empty, error, retry, stale, and success.
- Disabled, readonly, required, invalid, expanded, selected, and pressed states where applicable.
- Text shorthand, composed snippets, and omitted optional regions for reusable content APIs.
- Keyboard-only operation, focus entry/exit, and focus restoration.
- Long text, missing optional content, localization, RTL, narrow viewport, and zoom/reflow.
- Dark theme, forced colors, reduced motion, and contrast-sensitive states for shared UI.
- Variant-by-state comparisons on every materially different backdrop: canvas, opaque island,
translucent surface, popover, and dialog where applicable.
- Cancellation, out-of-order responses, and duplicate submission for asynchronous actions.
Do not manufacture cases that the public contract cannot reach.
## Isolation and doubles
- Keep tests deterministic and independent of order. Restore timers, globals, storage, fetch mocks,
and DOM mutations after each test.
- Mock at system boundaries, not inside the unit under test. Prefer a lightweight fake with behavior
over a deep mock that repeats implementation details.
- Use real parsing, validation, and component composition when inexpensive. Over-mocking creates a
test for the mock graph rather than the product.
- Control clock, randomness, locale, and network explicitly when they affect output.
- Use framework-supported request interception for network tests. Never call a live third-party
service from unit or component tests. The unit project of nodelib-config's `SvelteKitVitest` runs
offline: its global `fetch` rejects every request a test does not stub, and
`vi.unstubAllGlobals()` restores that rejection.
- Make mock failures loud: reject unhandled requests and unexpected calls.
- Build a fresh `Response` per call (`mockImplementation(async () => Response.json(…))`). A body
reads once, so `mockResolvedValue(response)` hands every later call a consumed body, and the
failure looks like a parsing bug in the code under test.
- Pin performance contracts with direct assertions. To prove a body is released, keep the stubbed
`Response` and assert `response.bodyUsed` after the call. To prove calls run in parallel, return a
promise that never resolves and assert synchronously that every independent call has started. A
serial loop then fails at once instead of timing out.
## Vitest and Testing Library
- Use Vitest APIs documented for the installed version. Prefer typed dynamic module paths when a
module mock is genuinely necessary.
- Give every Vitest project config file an explicit `root`:
`fileURLToPath(new URL(".", import.meta.url))`. Since Vitest 5.0.4, a project gets its root after
`svelte()` has read it. Without one, the plugin starts from the workspace root, leaves Svelte
libraries untransformed (`Unknown file extension ".svelte"`) and loads a second Svelte runtime
(`lifecycle_outside_component`).
- Name a config that holds only Vitest options `vitest.config.ts`, never `vite.config.ts`. Since
4.7.7, `svelte-check` reads `.svelte` files from symlinked workspace packages at their real path
and configures each from the nearest `vite.config.*` or `svelte.config.*`, the Vite one winning
within a directory. A Vite config without the Svelte plugin then fails with
`No Svelte configuration found in vite config`, as uikit's `packages/storybook-config` did.
Vitest tries `vitest.config.*` first, so the rename costs nothing.
- Keep `vi.mock` scope and hoisting rules visible. Refactor an inseparable module boundary rather
than relying on a mock the runtime cannot intercept.
- Render DOM, not component instances. Follow Testing Library query priority and guiding principle:
tests should resemble real use.
- Use the repository user-interaction helper when present. Reserve low-level event dispatch for the
browser primitive being tested.
- Put native lifecycle and teardown races in a browser project, not jsdom. Exercise the real element
API and event queue, including events delivered as a component unmounts, and assert the user-visible
outcome rather than Svelte internals.
- Name tests as observable behavior in plain language. Group by public function, component, or user
journey.
## Storybook
- Give every public component stories for meaningful variants and states, including interactive,
disabled/error, long-content, and narrow-layout cases.
- Show related variants together. Include the concise text API and one realistic composed example;
verify that optional regions can be omitted without leaving empty structure or spacing.
- Render sizes, colors, variants, and persistent/transient state combinations in aligned matrices.
Keep docs wrappers interaction-neutral so table or tile hover styles cannot masquerade as component
behavior.
- Keep stories deterministic, self-contained, and free of production side effects. Use loaders and
decorators only for shared, explicit environment contracts.
- Use realistic interface content in visual examples. Explain controller transitions and keyboard
mechanics in docs and tests instead of making them the example's title or body.
- Isolate screens and top-layer content in subdocuments on docs pages. Show desktop/mobile examples
together with responsive wrapping; modest scaling helps comparison, but verify geometry and touch
targets at the native viewport size. Check the docs page itself as well as individual stories.
- Pin visual-review stories through fixed-state controllers. Clicking, submitting, pressing Escape,
or clicking outside must not change the displayed scenario. Show distinct states as separate
examples; exercise live transitions in controller/component tests. Keep locale, theme, and story
inputs configurable instead of duplicating scenarios per locale. Preserve native focus, scrolling,
and text selection; do not freeze previews
with event-blocking overlays, `inert`, or `pointer-events: none`.
- Add a docs page for every public component: intent, composition, accessibility contract, and
examples. Use generated controls and ArgTypes for API reference instead of repeating a manual
table. Document foundations separately from components.
- Enable accessibility analysis globally and make violations fail automated story tests when the
installed Storybook integration supports it. Any exception must identify a documented false
positive or intentional antipattern story.
- Run contrast assertions in the real browser against the exact rendered semantic pairs and states,
not palette swatches alone. Automated tooling does not credit glow and may not understand every
gradient, transparency stack, pseudo-element, or forced-colors result; inspect computed output and
visually compare state separation in the supported surface contexts.
- Treat a near-threshold contrast violation as a real failure. Fix the owning semantic token and keep
a rendered safety margin; do not disable the rule or add a consumer override merely because a
calculated source value appeared to meet the minimum.
- Treat automated accessibility output as a first pass. Manually verify keyboard/focus behavior and
relevant browser/assistive-technology combinations for complex widgets.
- Keep stories and docs in a private workbench. Keep reusable Storybook theme, preview, and test
configuration in a separately consumable development package; never publish stories or runtime
workbench code as part of the component package.
- List each dependency subpath a story imports in Storybook's `optimizeDeps.include`
(`@a-novel-kit/uikit-storybook/ScrollPreview.svelte`, not only the package). Vite matches entries
exactly; an unlisted subpath is optimized mid-run, which reloads the iframes and fails every story
file on a cold cache. CI is always cold, so prove such fixes after deleting `node_modules/.cache`
and `node_modules/.vite`.
- Keep a representative framework or extraction fixture with the shared nodelib preset it validates.
A platform tests its product catalogs, screen states, and wiring instead of copying that generic
fixture locally.
## End-to-end tests and screenshot comparisons
During stage 3 of `develop-feature`, create or update all affected Playwright tests for client-side
work. Cover the changed journeys and browser contracts, then run the relevant browser suite. Reuse
the existing harness; when one is missing, include the smallest suitable Playwright setup in the
agreed scope, following `choose-dependency`. Do not silently defer required browser validation.
Separate a journey's phases with blank lines. When a loop wraps `test.step`, move the step body into
a named function, so the loop reads as the list of cases.
For rendered changes, capture deterministic screenshots of the affected states and supported
viewports and compare them with reviewed baselines. Stabilize data, fonts, locale, theme, and motion.
Inspect expected, actual, and diff images. Fix unexpected differences; update a baseline only when
the new rendering matches the agreed UI, and review initial baselines for new screens. Screenshots
complement live app/Storybook review and behavioral assertions. For non-visual client changes,
verify relevant existing visual baselines where applicable and state when no rendered surface is
affected instead of inventing a screenshot test.
- Test critical journeys and browser contracts, not every visual variant.
- Use accessible locators and observable readiness conditions. Never use fixed sleeps.
- Keep test data isolated and clean it through supported product interfaces.
- Point every URL the server under test reads at a local fixture started in a global setup, never at
a live or production file. When the server caches what it reads, give each state its own
`webServer` entry: entries start in order, so later ones run `node build` on the first entry's
build. platform-studio's downtime journeys follow this pattern.
- Make a compose service that a journey stops and starts, as the outage journeys stop
`authentication`, join a holder's network: `network_mode: service:<holder>`, where a
`registry.k8s.io/pause` holder publishes the ports. A restarted container otherwise gets a new
host interface, and Chromium fails the page then loading with `net::ERR_NETWORK_CHANGED`.
platform-template's `builds/compose.e2e.yaml` follows this pattern.
- Serve another platform's production image over plain http with
`PROTOCOL_HEADER: x-forwarded-proto` on its container, and send `X-Forwarded-Proto: http` from the
journeys' browser (Playwright `extraHTTPHeaders`). adapter-node otherwise assumes https, and
SvelteKit then rejects every form post to that platform as cross-site. The account platform also
names an `https` issuer, which its clients refuse.
- Run Authentication in a platform's stack with the callers production gives it.
service-authentication's `service-token-issuer` image stands in for the cluster's issuer, which
Authentication trusts through `SERVICE_TOKEN_ISSUER`, `SERVICE_TOKEN_AUDIENCE` and
`SERVICE_TOKEN_CALLERS`. A Playwright global setup draws the platform's token from the issuer into
the file `SERVICE_TOKEN_PATH` names. Fixtures call Authentication through a `fetch` that adds a
caller's token from the issuer in `X-Service-Authorization`.
- Run the account platform's image in a client's stack, with a token drawn from the issuer at
start, and register the client in `OAUTH_CLIENTS` on the address of the journeys' server. The
account platform's own stack registers a stub client instead, whose callback the test serves and
whose code the test exchanges. platform-studio's `builds/compose.e2e.yaml` and
`tests/e2e/service-token.ts` follow this pattern.
- A change to a platform's shell or pages can break the sibling journeys that drive its published
image, and they fail only when that image's pin moves. Search every other platform's `tests/e2e/`
for each accessible name you move or remove, and fix the journeys that reach it in a pull request
that also bumps their pin. platform-account v1.2 moved its "Agora apps" list into the rail, and
Studio's sign-in journey, still clicking the removed button, broke Renovate's account image bump
until platform-studio#439.
- A `page.route` handler sees only the first URL of a redirect chain. To alter a later step, walk
the chain with `page.request.get(url, { maxRedirects: 0 })`, read each `location`, and open the
altered URL with `page.goto`. platform-studio's failed-exchange journey forges its code this way.
- Capture traces/screenshots on failure where the existing runner supports them; do not commit
transient artifacts.
- Run at least the repository browser baseline for compatibility-sensitive behavior.
Before a repository first compares its screenshots against Drive reference batches, read
[Subscribe a repository to Drive visual tests](references/drive-subscription.md) for its identities,
folders, variables and seeding order. Read its recovery section when master's `test-browser` keeps
failing on screenshot changes that merged pull requests already approved.
A Vitest browser project, such as uikit's Storybook stories, compares its screenshots against Drive
reference batches through `node-actions/test-playwright` with `runner: vitest`. The workflows repo's
`docs/migrations/v1.64.0.md` is the adoption guide. Vitest's browser mode adds these rules:
- Capture `expect(page).toMatchScreenshot()`. Modal dialogs and popovers render in the top layer,
outside every container, so a container capture misses them. In Storybook's Vitest run, the first
`body` div is the hidden `sb-preparing-story` wrapper.
- Declare each screenshot with a `visual-snapshot` annotation in `beforeEach` or before the first
assertion. Vitest accepts `annotate()` only while the test state is `run`, and rejects it in
`afterEach` or after a failed soft assertion.
- Name each screenshot flat, from `[A-Za-z0-9_-]` with single hyphens:
`` `${task.file.name}-${task.name}`.replace(/\W+/g, "-") ``. Vitest resolves only the base name and
strips every other character, so a Storybook story ID or a dotted name stops matching its annotation.
- Treat the annotations as the inventory. Vitest records a `toMatchScreenshot` artifact only on
failure, so the Drive runner lists screenshots from annotations and fails a run that writes an
undeclared one.
- A style assertion in a play function turns a visual change into a functional failure, which
`allow-screenshot-change` cannot waive. uikit's Toolbar story asserts that its link keeps the button
radius, so a Button radius change fails until the link follows.
## Coverage and completion
Treat coverage as a map, not a percentage target; 100% is not required. Missing error, keyboard,
security, or state-transition paths matter; uncovered generated glue does not justify a test.
Run focused checks during iteration. After scope approval, complete meaningful coverage and run
the applicable suites; after cleanup, rerun checks affected by the final edits:
```bash
pnpm lint
a-novel test --type=pnpm -y
a-novel build --type=pnpm -y
```
The change is complete only when new behavior has a regression test at the truthful layer and all
affected existing tests remain green.