Now that every token migration has landed, lock them in: - radius-contract.test.ts: every border-radius in src/ must resolve to the --dc3-radius-* scale (directly or through an Element Plus radius variable). Geometric circles (50%/100%), inherit and single-corner 0 resets stay literal — hand-rolled px values are the exact drift this cleared once. - spacing-contract.test.ts: macro spacing (page edges, card walls, section rhythm) must resolve to --dc3-space-* / --dc3-page-padding / --dc3-gutter. Semantic tokens defined exactly once in theme.scss; a whitelist of layout-bearing containers may only declare margin/ padding/gap with tokens; clamp() in spacing and el-row :gutter values other than 8 are banned repo-wide (the JS prop cannot read CSS vars, so the literal must mirror --dc3-gutter's desktop value); a // spacing-exempt marker opts out line-by-line. Intra-component micro spacing stays out of scope this round. Document the v8 spacing-system decision in docs/design/frontend-three-terminal-ux.md and add the A5 rules to dc3/agents/frontend.md so agents follow the scales. Co-Authored-By: Claude Code <noreply@anthropic.com>
Test Strategy
The frontend test suite is intentionally layered so failures point at the right level of the application.
Commands
pnpm test: run all Vitest suites.pnpm run test:unit: run utility, store, and infrastructure unit tests.pnpm run test:api: run API wrapper contract tests.pnpm run test:component: run Vue component contract tests.pnpm run test:views: run route-level view tests (mounted views with mocked APIs).pnpm run test:guard: run meta tests that keep the test suite itself safe.pnpm run test:impact: print the recommended checks for the current change.pnpm run test:ci: run Vitest with coverage thresholds.pnpm run test:coverage: run Vitest with coverage.pnpm run test:e2e: run Playwright Test specs.pnpm run test:e2e:responsive: run the three-terminal gate against the self-contained mock build.pnpm run test:e2e:headed: run Playwright Test specs in a visible browser.pnpm run test:e2e:sweep: run the deep browser sweep.pnpm run test:e2e:sweep:headed: run the deep browser sweep in a visible browser.
Verification
Run the checks yourself instead of trusting recorded results, which go stale as the suite changes.
pnpm check,pnpm lint:check,pnpm build, andpnpm test:cicover types, lint, the build, and coverage-gated Vitest (thresholds stay enforced byvitest.config.ts).pnpm run test:e2e:responsiveruns the three-terminal gate against the self-contained mock build, so it needs no backend.pnpm run test:e2eruns the full Playwright suite; it needs a reachable gateway unless you setE2E_SERVER_MODE=mock.
The automated suites do not replace release gating: full real-backend business E2E, Lighthouse budgets, full axe-core
scans, and a full-route manual visual review must still run against a disposable backend dataset before release.
Layers
tests/unit: deterministic unit tests for utils, stores, and shared infrastructure such as Axios interceptors.tests/api: frontend API wrapper contracts. These tests mock the transport layer and snapshot the method, URL, body, and config used by every exported wrapper.tests/component: Vue component contracts using@vue/test-utils. Reuse the shared Element Plus stubs intests/setup/stubs/element-plus.tsinstead of redefining them per file.tests/views: route-level view contracts. Same toolchain astests/componentbut the subject is asrc/views/**page mounted with mocked APIs. Use this layer when the unit-of-behaviour is a routed page rather than a reusable building block.tests/e2e: Playwright browser tests. The specs cover authentication, route availability, page health, and safe UI interactions.tests/e2e/browser-sweep.mjs: entrypoint for the deep browser sweep. The actual runner lives undertests/e2e/browser-sweep; keep new browser scenarios in Playwright specs unless they specifically need the sweep runner.tests/guardrails: meta tests for AI-assisted development. These tests block focused or disabled tests, fixed E2E business IDs, missing guard scripts, and missing testing policy documentation.
Adding a new test
- Pick the right layer (
tests/{unit,component,views,api,e2e}). - Copy the matching template from
tests/_templates/:
store.test.template.tsfor Pinia storescomposable.test.template.tsforsrc/composables/*component.test.template.tsforsrc/components/*(and views)api.test.template.tsfor new API wrappers (which actually means extendingtests/api/api-contracts.test.ts)
- Reuse fixtures from
tests/fixtures/when the data shape already exists there — don't inline a duplicatesampleMenuTree/ credentials block. - Reuse stubs from
tests/setup/stubs/element-plus.tsfor any Element Plus components. - Run
pnpm testlocally before committing. If it fails with a Vue warn, see test-debugging.
Conventions
These rules are mechanically enforced by tests/guardrails/ai-guardrails.test.ts. Style points without a guardrail are
listed for reviewer reference.
File naming (enforced)
tests/{unit,component,views}/<kebab-case>.test.ts. The base name should mirror the source module under test (auth.ts→auth-store.test.ts,usePagedList.ts→use-paged-list.test.ts,validationUtil.ts→validation-util.test.ts).- Avoid suffixes that describe the test itself (
-coverage,-bonus); name for the subject, not the goal.
Mocking (enforced for multi-mock files)
- More than one
vi.mockin a file → bundle the spies in onevi.hoisted(() => ({…}))block at the top, conventionally namedxxxMocks(tokenMocks,apiMocks). Keeps top-level state minimal and avoids "Cannot access X before initialization" hoisting errors. - A single
vi.mockmay inline its factory.
Element Plus stubs (enforced)
- Reuse
tests/setup/stubs/element-plus.ts. Do not redefineElButton,ElForm,ElPagination,ElInput,ElSelect, etc. inline. - Add new layout/decorator stubs to
layoutStubsin that file when a new Element Plus component is needed.
Type assertions (enforced)
- No
as unknown as Tdouble assertions. Use a type-correct fixture builder or// @ts-expect-errorfor the one line that intentionally violates the contract. - No
as never. Build a properly-typed fixture instead.
wrapper.vm is forbidden in component/view tests (enforced)
- Drive the component through props, slots, emits, and DOM events — the public contract. Calling
wrapper.vm.someInternalMethod()couples tests to the component's internal API, which churns whenever<script setup>details change.
Describe nesting (style)
- One top-level
describeper file, matching the subject (describe('auth store', …)). - Multi-action subjects use a second-level
describeper action (describe('login', …),describe('logout', …)). - Single-purpose subjects (e.g.
interval storewith one method) stay flat.
Naming inside files (style)
- Stores: lowercase prose —
describe('auth store'),describe('menu store'). - Components/views: PascalCase symbol —
describe('ToolCard'),describe('AlarmNotify view'). - Utility describe blocks mirror the
src/utils/<file>base —describe('storageUtil').
Vue warnings are errors (enforced via tests/setup/vitest.setup.ts)
[Vue warn]and[Vue error]messages are promoted to thrown errors during tests. If you genuinely need to silence one, add a regex toVUE_WARN_ALLOWLISTwith a comment explaining the source — don't broaden the check.- Set
VITEST_ALLOW_VUE_WARN=1to bypass while debugging locally.
AI Guardrails
The mandatory policy lives in tests/frontend-testing-guardrails.md. In short:
- API wrapper changes require
tests/apicoverage. - Shared utility, store, composable, and Axios changes require
tests/unit. - Reusable component changes require
tests/component. - Route, page, menu, and permission changes require Playwright coverage.
- New test data must be created dynamically and cleaned up; do not hard-code business IDs.
E2E Environment
Playwright defaults to http://127.0.0.1:4174, runs with one worker, and starts
pnpm run serve:e2e unless E2E_START_SERVER=0 is set. The full E2E suite uses the development build and proxies
/api to http://localhost:8000 by default. The test server listens on the dedicated
http://127.0.0.1:4174 endpoint so it cannot silently reuse an unrelated service on the development UI port.
pnpm run test:e2e:responsive switches the local server to the
self-contained mock build, so three-terminal checks do not require a backend. Use E2E_BASE_URL with
E2E_START_SERVER=0 to test an already running environment, E2E_API_TARGET to select another gateway, or
E2E_WORKERS=N to opt into parallelism against an isolated backend dataset.
Use the headed scripts when you want to watch the browser operate:
pnpm run test:e2e:headedpnpm run test:e2e:sweep:headed
The tests/e2e/browser-sweep.mjs script is kept for deeper manual/full-environment sweeps. It performs destructive
delete checks against seeded data, so run it only against a disposable test dataset.
Both the Playwright specs and the browser sweep discover required route IDs at runtime. If a required entity does not
exist, the test creates
e2e_* fixture data through the backend API and deletes those records at the end of the run.