testing
Esposter Vitest testing conventions — describe with function refs, canonical test values, takeOne for array access, destructuring from stores/composables, and the Windows UnoCSS test-skip rule. Apply
SKILL.md.claude/skills/testing/SKILL.md
GitHubTesting Conventions (Vitest)
Settled — do not re-propose
- A lint rule for constant scope — an AST selector cannot ask the two whole-Program questions the exceptions turn on (a helper file, a binding read from a hoisted
vi.mockfactory or an awaited initialiser);scripts/src/workspace/constantScope.test.tsis the enforcer and asks both. isolate: false, per project or globally. The packages that survive it come out level, and the apparent gain belongs to the ones that fail under it for the reason isolation exists (references/running-the-suite.md).- A count target for tests — a cap or quota per package measures how many tests exist, not whether a regression is caught. The keep test in
references/what-earns-a-test.mdis the only gate on a test.
Deep dives
Structure and helpers:
references/what-earns-a-test.md— when deciding whether a subject earns a test at all, and which one.references/test-placement.md— when deciding which file a test goes in, or whether two checks over one directory are one suite.references/suite-titles.md— when adescribe's subject has no function reference, or its export is not camelCase.references/module-scope.md— when a test declares anything outside itsdescribe, orconstantScope.test.tsreports one.references/case-tables.md— when a test runs over a table of cases.references/test-helper-files.md— when a helper, fixture or hook block is shared between suites: one function per file,constants.test.ts,setup*fixtures.references/wrapper-suites.md— when two suites match modulo constants, or a suite tests a thin wrapper.references/fixture-paths.md— when a test creates, reads or names a real filesystem path.references/type-tests.md— when writing a.test-d.ts, or asserting avoidreturn.references/router-test-setup.md— when writing a tRPC router test: callers, mock sessions, seeded rows, naming.
Assertions and waiting:
references/assertions.md— when writing a test'sexpectcalls: the matcher, how much of the value, a void return, a call's arguments.references/error-assertions.md— when filling in the inline snapshot a thrown or rejected error is asserted with.references/polling.md— when a test waits for something to happen.references/timers-and-hand-resolved-promises.md— when a test fakes timers, pins the clock, or holds a call in flight.references/awaiting-a-double.md— when a test proves a caller awaits its side effect, orders two overlapping calls, or reads what a tRPC call caused.
Doubles:
references/module-mocks.md— when reaching forvi.mockor a spy: whether the behaviour needs a double, and which seam.references/colocated-mocks.md— when a module is mocked in several suites, writing avi.mockfactory, or mockingdbor aProxyexport.references/client-trpc-calls.md— when code under test calls tRPC from the client:setupMswTrpcandtrpcMsw, never a mocked client.references/mock-cleanup.md— when choosing a mock's cleanup hook, queuing once-values, or stubbing a global or env var.references/fabricated-ids.md— when a mock returns a persisted entity, or a new foreign key turns a suite red.
Environment and running:
references/test-environment.md— when a test needs a DOM, a layout engine or the nuxt runtime, or tests a composable with lifecycle hooks.references/nuxt-environment-and-mounting.md— when a test mounts a component: a routed link, a mount attached to the body, a dispatched event.references/mounted-stores.md— when a test seeds a store a mounted component reads, room-scoped stores included, or a suite reaches for its own Pinia.references/platform-and-bundle-tests.md— when a suite is skipped on some hosts, or its output depends on the host.references/bundle-size.md— when a size snapshot fails or moves, or a new library package needs one.references/running-the-suite.md— when narrowing a run with-tor-u, or reading a failure only the full parallel run produces.references/timeouts.md— when a test, suite or config sets a timeout, or a test fails withTest timed out: the measurement and the ladder that decide whether a timeout is earned at all.
Structure
testnotit— alwaystest(...).- A test lives beside what it tests —
Foo.ts→Foo.test.ts, never folded into a nearby suite or moved to the module it scans (references/test-placement.md). - A file snapshot lives beside what it holds, as
<name>.snapshot.<extension>— a component's rendered HTML beside the component, a parsed map beside the map — never in a__snapshots__folder. The suffix is what the formatter and the JSON lint skip, so the file stays as its test wrote it. describe(functionRef, …), flat — a string only when no reference exists, naming the file's export;describe.eachover a matrix is not a group (references/suite-titles.md).- Nothing but imports, pure helpers and hoisted mocks lives at module scope — every constant is a
constinside thedescribe(references/module-scope.md). test.eachfor a table of cases, never a loop aroundtest(vitest/prefer-each), titled with%s(references/case-tables.md).expect.hasAssertions()— top of every test body.- Assertions after all assignments —
expectcalls follow that phase's operations and locals, after a blank line. - A
voidreturn is never assigned or asserted at runtime —await fn();bare, orresolves.toBeUndefined()(references/assertions.md). - Reuse utilities, and prefix factories
create*— look for an existing helper beside the code under test first; builders arecreateRow, nevermake*.
Test data — the test-values skill
Every literal, id, date, path and fixture a test writes is that skill's: the canonical values, dates computed from the epoch, the three checks a string literal passes, and the shared-data shapes. This page assumes them.
Assertions
toStrictEqualalways, exact counts, andarrayContainingnever for an argv or an ordered sequence (references/assertions.md).- Never fragment-match a deterministic output — the whole value with
.toBe(...)or an inline snapshot, a runtime value the test holds interpolated in, never an element sampled from a knowable sequence (references/assertions.md). - Once + args →
toHaveBeenCalledExactlyOnceWith(...), never the jest-extended once-with matcher (references/assertions.md). takeOne(arr, index)forarr[index]undernoUncheckedIndexedAccess— not universal, preferfindwhen more idiomatic.assert.exists(value)narrows nullables and fails fast instead of?? []. Cloning: thetypescriptskill (references/cloning.md).- No unnecessary destructure — for plain objects, read a property directly when used once. Stores and composables keep the destructure ordering of the
piniaskill'sreferences/consuming-a-store.md, unchanged in tests. toThrowErrorMatchingInlineSnapshot(...)is the only accepted error assertion, sync and async (vitest/no-restricted-matchers);not.toThrow()is not an error assertion and stays, as how a best-effort function proves it swallows what it should (references/error-assertions.md).
Mocking
- Mock the smallest seam that makes the behaviour reachable, never re-declare a mock another file owns, and drive real state before faking it (
references/module-mocks.md). - The cleanup hook follows how the mock was created, never habit — the wrong one leaks an implementation or a queued once-value into the next test (
references/mock-cleanup.md). vi.fn()always takes its signature —vi.fn<(input: CreateEmojiInput) => Promise<void>>(), the production input and return types imported rather than restated. A barevi.fn()infersunknownparameters, so destructuring a recorded call is an implicit-anylint error andmockResolvedValueaccepts anything.
Reactive Effects and Timers
- A
watchcallback and a re-render run on Vue's scheduler, never synchronously —await nextTick()flushes them after the write that queued them;flushPromises()from@vue/test-utilswhen the callback itself awaits. With nothing queued, neither is written. - Fake timers, and any promise the test resolves by hand, follow
references/timers-and-hand-resolved-promises.md— onevi.useFakeTimers({ now: 0 })inbeforeEachwith an unconditional restore inafterEach,toFakenarrowed rather than widened, andPromise.withResolversinstead of aletclosed over by an executor. - Await the real completion signal, never a poll — the repo-wide ban is
openspec/specs/architecture/no-polling/design.md, and the test forms arereferences/polling.md.
Running Tests
- The touched tests are a session's one check, run in the background once at the end — the rest are CI's (the
running-checksskill). - Never run the full suite locally —
pnpm test <paths> --runover what the change touched (references/running-the-suite.md). -t "name"is not a scope, and-ugets the narrowest path list — read the snapshot diff before committing (references/running-the-suite.md).
What to Test
Every test earns its line or it is deleted — the keep test in references/what-earns-a-test.md decides it.
A regression test is proven to fail before it is kept — the one end-of-work run (the
running-checksskill) is made with the fix it guards reverted, the test is watched failing for the reason it names, and the fix is restored only after that run's exit line is in its log, never while it is still in flight, since a test file imported after the restore runs the fixed code; the commit follows the restore. No second run follows the restore. A test that has only passed against fixed code has shown that it passes; the un-awaited form inreferences/awaiting-a-double.mdis the same step for an ordering contract.What never earns a line, and the subjects already decided:
references/what-earns-a-test.md.