Testing
Testing conventions and strategies
Test execution strategy + Playwright e2e tuning.
MUST
- Run only failing tests first; verify 2-3×; stop. Why: full suites are slow + noisy.
- Pre-test:
bun fixpasses, kill stale procs (pkill -9 -f "next"), clear results (rm -rf test-results). Why: clean baseline. - Cheapest faithful harness first — unit → focused integration → headless script → service smoke → full UI/e2e. Why: reproduce a bug at the lowest tier that shows it; don’t loop a 40s e2e on a logic bug.
- Paid-API tests (Anthropic, etc.) in dedicated files (
*.cost.test.ts,smoke-*), cheapest model + shortest prompt, run LAST after free checks (lint → unit → build). Why: pass the zero-cost gate before anything billable. - After a failed paid-API cycle, record actual cost (tokens × model price) before the next attempt. Why: silent retries compound the bill without tracking lesson-rate.
NEVER
- Never run the full suite unless explicitly asked. Cost: slow, noisy, masks the real failure.
- Never scale to full suites blindly before verifying. Cost: wasted wall-clock.
- Never run paid-API tests in a debug loop. Cost: real money per iteration.
Playwright E2E
| Scope | Timeout | Kill |
|---|---|---|
| Single test | 5s | 10s |
| Single file | 8s/test | 30s |
| Full suite | 10s/test | 180s |
| Symptom | Fix |
|---|---|
Hangs on fill()/click() | Check element visible/enabled |
networkidle hangs | Use waitForSelector() instead |
| Element not found | Check testid on element vs parent |
| Flaky counts | --workers=1 |
Playwright resolves a tsconfig extends by probing two paths, never by node resolution
- Extend the REPO ROOT tsconfig from every sub-package in a repo that runs Playwright —
"extends": "../../tsconfig.json"— and let the root keep the barelintmax/tsconfig. Why: the runner tries the path relative to the config's own directory and then<configdir>/node_modules/<spec>, so the bare specifier resolves at the ROOT, wherenode_modulessits beside it, and nowhere deeper — while the root-relative chain applies the same base options (tsc --showConfigmatches, the root adding only its owntypes) and names the gate once instead of once per package. - Sweep EVERY tsconfig a run loads, not the one beside
playwright.config.ts. Why: the runner loads a tsconfig for each file it pulls in, so a shared e2e helper package and a vendored UI package fail exactly as the app does, and a fix confined to the app clears the first error and leaves the next waiting. - Read
Failed to resolve "extends" pathas an UNREACHABLE dependency, never a malformed tsconfig. Why: the message names the tsconfig, so the instinct is to rewrite it or reinstall, while the package is installed and sits one directory above where the loader looks. Adding the.jsonextension changes nothing, and neither does a walk-up when the specifier is anexportssubpath alias — which the loader does not read. - Treat this error on an untouched repo as a TOOLCHAIN change and bisect the runner before editing any config. Why: an earlier version returned the unresolved path without checking it exists and its loader then skipped the missing file in silence, so the base config was never applied and nothing said so; running the previous version against the identical tree separates a newly-strict error from a broken config in one command, and it surfaces the finding that matters — the shared options were silently absent all along.
- Keep the resolvable spelling even after the runner stops erroring. Why: upstream reverted to not throwing and declined tsc-compatible resolution for now, so the bare specifier returns to being silently IGNORED rather than resolved — green with the shared strictness dropped, which is the failure this whole class is made of.
- Verify with the runner itself —
bunx playwright test --listfrom the package directory — and treat reaching a RUNTIME guard as the pass. Why:tscand every editor resolve the specifier correctly through node resolution, so neither can confirm or deny this; only the runner's own loader can.