frontend-testing-workflow
Author and maintain frontend tests — Jest unit suites (jsdom client + node server), Testing Library semantic queries, Playwright E2E and visual regression, seeded Faker builders, and mutation-killing
SKILL.mdplugins/react-frontend-sdlc/skills/frontend-testing-workflow/SKILL.md
GitHubFrontend Testing Workflow Skill
Profile keys consumed
make.test_unit_clientmake.test_unit_servermake.test_integrationmake.test_e2emake.test_visualmake.test_mutationmake.merge_mutation_reportsmake.startmake.start_prodquality.coverage_statementsquality.coverage_branchesquality.coverage_functionsquality.coverage_linesquality.mutation_msiquality.visual_diffscapabilities.visual_testingcapabilities.mutation_testingarchitecture.source_rootarchitecture.modulesarchitecture.component_prefixarchitecture.path_aliasesframework.bundlerframework.package_managerframework.graphql_mock
All test invocations go through the profile's make target map. A null
value for a make.* key means the capability is absent: skip that suite
with an explicit capability-absent note instead of failing or improvising
a raw command. Generic tooling (gh, the project package manager
framework.package_manager, git) may be invoked directly when needed,
but every suite runs through its mapped target.
Context (Input)
- A component, hook, store, repository, or feature flow needs new or updated tests.
- A failing suite needs debugging, or coverage / mutation gaps need closing.
- This skill governs how tests are authored (environments, selectors, builders, assertions). For the run-and-fix loop and the gate thresholds themselves, pair it with the testing-workflow skill.
Task (Function)
Write tests that exercise behavior through the public UI or exported API,
locate elements by user-facing semantics, use deterministic generated data,
and assert specifically enough to kill mutants — so the suite passes with
full coverage and a clean mutation score through the profile's make
targets.
Test Tiers & Where Tests Live
| Tier | Invocation | Environment | Typical location |
|---|---|---|---|
| Unit (client) | target mapped by make.test_unit_client |
Jest jsdom + RTL | tests/unit/ |
| Unit (server) | target mapped by make.test_unit_server |
Jest node | tests/apollo-server/ |
| Integration | target mapped by make.test_integration |
Jest jsdom, global 100% | tests/integration/ |
| E2E | target mapped by make.test_e2e |
Playwright | tests/e2e/ |
| Visual | target mapped by make.test_visual |
Playwright snapshots | tests/visual/ |
| Mutation | target mapped by make.test_mutation |
Stryker (driven by RTL) | component layer under root |
Mirror source ownership: a test's directory tracks the source area it
covers under architecture.source_root — typically one of the feature
modules declared in architecture.modules, or a shared layer. Reference
values from the canonical upstream profile:
make test-unit-client # make.test_unit_client → jsdom + Testing Library
make test-unit-server # make.test_unit_server → Apollo mock (node env)
make test-integration # make.test_integration → integration suite (global 100%)
make test-e2e # make.test_e2e → Playwright user flows
make test-visual # make.test_visual → visual regression
make test-mutation # make.test_mutation → Stryker mutation testing
Multi-repo note: a component library may collapse the client/server
split into a single unit target (make.test_unit_client /
make.test_unit_server map to the same target or one is null). Run the
non-null target and note the absent lane — never invent a host command.
Jest Environments
Two environments back the unit/integration tiers, selected by the runner's
TEST_ENV:
- Client (jsdom) — React component, hook, store, mapper, and utility tests. Render with Testing Library; assert on the rendered output and accessible tree, not on internal state.
- Server (node) — the local GraphQL mock (
framework.graphql_mock, e.g. an Apollo server) behaves correctly in a node environment. Test the resolver/handler contract, not the framework internals. - Integration (jsdom) — wires real collaborators together and enforces
a global 100% coverage floor over
architecture.source_root(quality.coverage_statements/quality.coverage_branches/quality.coverage_functions/quality.coverage_lines). A line left uncovered anywhere under the source root fails this tier.
Test behavior through the public UI or the exported API. Mock network and service boundaries intentionally; do not test the mock's internals. Keep setup state explicit so a reader can see what the component received.
Testing Library — Semantic Queries
Locate elements the way a user (or assistive technology) perceives them.
Source ships no data-testid; query priority is:
getByRole(withname) — buttons, links, headings, form controls.getByLabelText— inputs tied to a visible orarialabel.getByText/getByPlaceholderText— visible copy.- A stable
idonly when no semantic query fits.
// Good — user-facing semantics, generated data bound once
const user = buildUser();
render(<ProfileCard email={user.email} />);
expect(screen.getByRole('heading', { name: user.fullName })).toBeVisible();
expect(screen.getByText(user.email)).toBeInTheDocument();
// Bad — test-only hook the source does not ship
screen.getByTestId('profile-card');
This is enforced: ESLint flags data-testid as an error under the
source root and warns on *ByTestId queries in tests (mock-stub queries
stay valid). Satisfy the gate by refactoring to a semantic query or adding
a real accessible name — never with a suppression directive or a test-only
attribute. When a component cannot be queried semantically, fix the
component's accessibility, then the test follows. See the
accessibility-audit skill and
frontend-component-development skill.
Playwright — E2E & Visual Authoring
E2E specs drive real user flows in a browser; visual specs assert pixel
stability. Both use accessible Playwright locators first
(getByRole / getByLabel / getByText), falling back to a stable id
only when no semantic locator fits — the same priority as the unit tier.
// Accessible locator, explicit state, no test-id
await page.goto('/');
await page.getByRole('button', { name: submitLabel }).click();
await expect(page.getByRole('status')).toHaveText(submittingLabel);
Authoring rules:
- Keep setup state explicit; do not rely on prior-spec side effects.
- Use the project's mocked API responses (e.g. Mockoon-backed, available per the project's test stack) so flows are deterministic and offline.
- Add a page helper only when it removes real duplication, not speculatively.
- The default E2E/visual path builds production parity (target mapped by
make.start_prod); a dev-container fast path against the running dev server (target mapped bymake.start) exists for a single-spec inner loop — scope it to one spec while iterating.
Visual regression is capability-gated:
SKIPPED: make.test_visualwhencapabilities.visual_testingisfalseormake.test_visualisnull— record a capability-absent note and skip; do not author visual specs for a repo without the capability.- The bar is
quality.visual_diffs(ceiling0): zero unintended pixel diffs. On a diff, open the diff image and decide whether the change is intended before doing anything else.- Unintended → it is a regression: fix the component or style.
- Intended → regenerate the baseline with the snapshot-update variant
next to the
make.test_visualtarget (find it in the repository Makefile), but only after inspecting the diff. Never update snapshots blind to silence a failing run.
- Keep test data deterministic (seeded Faker, below) so baselines stay stable across runs and workers.
Faker Builders
Tests generate arbitrary user/domain data (emails, names, passwords, ids,
tokens) with @faker-js/faker via shared builders under tests/builders/,
imported through a builders path alias (one of architecture.path_aliases,
e.g. @tests/builders, resolving in both the Jest and Playwright
runners). Each builder returns domain-valid data by construction and
accepts an overrides object:
import { buildUser, buildCredentials, buildEmail } from '@tests/builders';
const user = buildUser(); // all fields generated and valid
const creds = buildCredentials({ // override only what the test pins
email: buildEmail(),
});
- Seeded determinism: Faker is seeded via
seedFaker()in each runner's setup (defaultDEFAULT_FAKER_SEED, override withFAKER_SEED=<integer>; the seed is reported once per worker), so the suite is reproducible and visual snapshots stay stable. - Bind once, reuse: assign a generated value to a
constand reuse it across the input and the assertion — never call a generator twice and compare its two results. - Keep literals only when the value IS the case: invalid/edge-case inputs, golden text, config, URLs, error codes/messages, i18n strings, and mock sentinels stay hardcoded because the literal is the contract.
FAKER_SEED=12345 make test-unit-client # reproduce a seed-specific failure
Writing Tests That Kill Mutants
Mutation coverage proves assertions are specific, not just present. This tier is capability-gated:
SKIPPED: make.test_mutation(andmake.merge_mutation_reports) whencapabilities.mutation_testingisfalseormake.test_mutationisnull— record a capability-absent note and skip.- The mutated scope is the component layer
(
<source_root>/components/**/*.tsx, where reusable components carry thearchitecture.component_prefixsuch asUIButton), driven by the client unit suite. The bar is MSI ≥quality.mutation_msi(default read from the project's Strykerbreak), raise-only — never lower the configuredbreakto make a run pass.
When a mutant escapes, the assertion was too loose. Strengthen it:
- Read the mutation diff: what change went undetected?
- Add the boundary or branch case the diff exposes — e.g. a mutant
flipping
>to>=needs an exact-boundary test; a removed prop needs an assertion that the prop's effect is rendered. - Assert on the observable result (rendered text, role, attribute, callback argument), not on the fact that a function ran.
- If a component resists testing, refactor it for testability — see the frontend-component-development skill.
Never silence a mutant with a suppression annotation. The run-and-merge
gate (CI shards make.test_mutation and re-enforces break via
make.merge_mutation_reports) lives in the
testing-workflow skill.
Constraints (Parameters)
NEVER:
- Cancel long-running tests mid-execution or commit with failing tests.
- Accept coverage below the enforced floors (
quality.coverage_statements,quality.coverage_branches,quality.coverage_functions,quality.coverage_lines— all100). - Allow escaped mutants or lower
quality.mutation_msi/ the Strykerbreak. - Accept an unintended visual diff or update snapshots without inspecting
them (
quality.visual_diffsceiling0). - Add a
data-testidso a test can find an element — refactor to a semantic query. - Add
eslint-disable/@ts-ignore/ suppression annotations to dodge a check — fix the root cause. - Run tests outside the profile's
maketarget map (no barejest/playwright/strykeron the host; the targets wrap the containerized toolchain and the package-manager-managed dependencies).
ALWAYS:
- Use seeded Faker builders for dynamic test data; bind a generated value
to a
constonce and reuse it. - Test behavior through the public UI or exported API; mock network and service boundaries, and do not test mock internals.
- Locate elements by accessible role/label/text (Testing Library and
Playwright) before any
idfallback. - Keep the mocked API behavior explicit in test setup.
- Add regression coverage for a bug fix before changing behavior.
- Keep results deterministic across runs and workers.
Format (Output)
Functional tests success:
PASS tests (X suites, Y tests)
Coverage: 100% statements / branches / functions / lines (over source root)
Visual: 0 unintended diffs
Mutation testing success:
MSI >= quality.mutation_msi (default = Stryker `break`)
0 escaped mutants on the mutated component scope
After authoring, format then lint through the
frontend-quality-workflow skill
(targets mapped by make.format then make.lint). Fixes surfaced during
review or QA route back to the react-implementer agent.
Verification Checklist
- Tests exercise behavior through the public UI / exported API, not internals
- Elements located by accessible queries; no
data-testidadded - Dynamic data comes from seeded Faker builders, bound once and reused
- Correct Jest environment chosen (client jsdom / server node / integration), suites mirror source ownership
- Coverage at the enforced floors over
architecture.source_root - Visual diffs inspected before any snapshot update
(
SKIPPEDnoted whencapabilities.visual_testingis false) - Mutants killed by specific assertions; MSI ≥
quality.mutation_msi(SKIPPEDnoted whencapabilities.mutation_testingis false) - Ran through the profile's
maketarget map (containerized); no suppression added
Related Skills
- testing-workflow - Run-and-fix loop, gate enforcement, and the sharded mutation merge gate
- frontend-quality-workflow - Format + lint after authoring tests
- accessibility-audit - Fix the accessibility that makes a semantic query possible
- frontend-component-development - Refactor a component for testability when a mutant resists
- code-organization - Place and name test files to mirror source ownership