Skip to content
Back to skills

Write Frontend Tests

ASecurity

Test frontend behavior and change tests, stories, fixtures, mocks, or test configuration. Load when behavior needs coverage; always pair with write-frontend.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmentrustgoshellbashreactnodeawstestingapifrontend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add a-novel-kit/stack --skill write-frontend-tests --agent claude-code

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.

Security grade badge for Write Frontend Tests
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/a-novel-kit-write-frontend-tests/badge)](https://www.skillsdirectory.com/skills/a-novel-kit-write-frontend-tests)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Files in this skill

  • SKILL.md17.6 KB
  • agents/openai.yaml213 B
  • references/drive-subscription.md5.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.