Files
iot-dc3/docs/test-debugging.md
T
pnoker 365cf71c0a test: harden test infrastructure to A-grade with fixtures, stubs, guardrails, and e2e auth coverage
Adds shared fixtures/stubs/templates, 15 mechanically-enforced guardrails
(filename casing, ban wrapper.vm direct calls, double assertions, weak
matchers, tautologies, debug residue), splits grab-bag util test files
by source module, promotes Vue warnings to test failures, fills logout
and login-validation gaps in e2e, and replaces networkidle+hard-wait
recipes with web-first assertions.

Net: 355 unit/component/api/guardrail tests (+98), 36 playwright tests
(+11), 80.55% line coverage with 2-3% buffer above tightened thresholds
(65/75/78/78).
2026-05-19 19:01:15 +08:00

132 lines
5.0 KiB
Markdown

# Test Debugging FAQ
Quick answers to the failures that recur most often when writing or running
the frontend tests. Pair this with [tests/README.md](../tests/README.md) for
the conventions, and `tests/guardrails/ai-guardrails.test.ts` for the
mechanically-enforced rules.
## "Unexpected Vue warning: ..." thrown from a test
The setup file in `tests/setup/vitest.setup.ts` promotes `[Vue warn]` /
`[Vue error]` to thrown errors. The most common causes:
- **Failed to resolve component: el-xxx** — the component template uses an
Element Plus component you haven't stubbed. Add it to `layoutStubs` in
`tests/setup/stubs/element-plus.ts` (preferred) or pass it under
`global.stubs` in the mount call.
- **injection "Symbol(router)" not found** — the component calls
`useRouter()` / `useRoute()` but the test didn't mount with a router.
Use a memory router:
```ts
import { createMemoryHistory, createRouter } from 'vue-router';
const router = createRouter({ history: createMemoryHistory(), routes: [...] });
mount(Comp, { global: { plugins: [i18n, router] } });
```
- **Failed setting prop "modelValue"** — already in the allowlist; if you
see a different prop, add the regex to `VUE_WARN_ALLOWLIST` with a comment
explaining why it can't be fixed at the source.
To temporarily bypass while debugging locally:
```bash
VITEST_ALLOW_VUE_WARN=1 pnpm test
```
Don't commit code that needs the bypass — the warning indicates a real
contract gap.
## "Cannot access X before initialization" in vi.mock factory
Symptom: `ReferenceError: Cannot access 'someMock' before initialization`
pointing at a `vi.mock(...)` call. Root cause: the factory references a
top-level `const`, but `vi.mock` is hoisted to the top of the file by
Vitest, so the const isn't initialized yet.
Fix: wrap the spies in `vi.hoisted`:
```ts
const apiMocks = vi.hoisted(() => ({
doSomething: vi.fn(),
}));
vi.mock('@/api/foo', () => apiMocks);
```
The shared-mocks guardrail enforces this for files with more than one
`vi.mock`.
## Test passes locally but fails on CI for "leftover state"
Look for module-level state that survives between tests:
- **Module-level reactive cache** (e.g. `useEntityNames`'s `cache` /
`inflight` objects). Reset by `vi.resetModules()` + re-importing in
`beforeEach`.
- **Pinia store persisted across tests**. Always
`setActivePinia(createPinia())` in `beforeEach`.
- **localStorage / sessionStorage**. The global setup clears these in
`afterEach`, but if your test relies on data being absent at the start
of the run, clear in `beforeEach` too.
## "as unknown as T" or "as never" guardrail failure
The guardrail forbids erasing types via double assertion. Replace with:
- A typed fixture builder: `function makeRequest(): Request { return { … }; }`
- `// @ts-expect-error — intentionally invalid input for whitelist test`
on the single line that violates the contract.
If the test really needs `as never` to satisfy a generic constraint, that
usually means the production code is mistyped; fix it at the source.
## "tests/component/foo.test.ts contains wrapper.vm.x()" guardrail failure
Drive the component through its public surface — props, slots, emits, DOM
events. Calling `wrapper.vm.someMethod()` couples the test to internals
that change whenever the component is refactored. If you genuinely need
an escape hatch, use `defineExpose` in the component and call through the
expose surface (still `wrapper.vm.someMethod()`, but tests survive the
refactor because expose is part of the contract).
## "describe must use lowercase verb (no should...)" guardrail failure
Rephrase. The describe block names the subject, the it block names the
behaviour:
```ts
// ❌
it('should validate before searching', …);
// ✅
it('validates before searching', …);
```
## Coverage drops below threshold
Run `pnpm test:coverage` locally. The console report shows per-file gaps.
If you genuinely added new untested code, add the test; if you removed
covered code (intentional refactor), the percentage may rise enough to
hide the drop, otherwise lower the threshold in `vitest.config.ts` and
flag it in the PR description.
Don't game the threshold by writing tautological tests — the
"forbids tautological assertions" guardrail catches `expect(true).toBe(true)`.
## Snapshot diff is huge after a small API change
The `tests/api/api-contracts.test.ts.snap` file is ~2800 lines because it
covers every API wrapper. A small change to a wrapper triggers a small
focused diff inside it; a giant diff means many wrappers shifted. Ask:
- Did the auth headers change shape? (storage-format change)
- Did the URL prefix move? (proxy / version bump)
- Was a new wrapper added to a tracked module?
Run `pnpm test:api -u` to update only after manually reading the diff.
## Where do new fixtures go?
`tests/fixtures/`. Don't inline a 30-line sample tree in a test if a
sibling test could use the same shape. The guardrail checks the directory
exists; convention asks you to keep adding files (`auth.ts`, `menu.ts`,
`rows.ts` as starting points).