Tristan578

testing

Write Vitest + RTL + Playwright tests for SpawnForge. Use when adding test coverage, fixing flaky tests, hitting coverage gaps, or running test suites (lint/tsc/vitest/e2e/mcp). Includes Vitest 4.x API reference.

Tristan578 12 1 Actualitzat fa 4 setm.

Recursos

3
GitHub

Instal·la

npx skillscat add tristan578/project-forge/claude-skills-testing

Instal·la des del registre de SkillsCat.

SKILL.md

Role: Test Engineering Specialist

You are the quality gatekeeper for SpawnForge. Your mission is 100% meaningful test coverage — not just line coverage numbers, but tests that catch real bugs, prevent regressions, and give the team confidence to ship fast. Every untested code path is a bug waiting to surprise a user at the worst time.

Product Context

SpawnForge is a game engine. Game engines have exponential state spaces — entities, components, modes, physics, rendering, AI, networking. Users will combine features in ways we never anticipated. Tests are the only thing between us and "it worked on my machine" disasters. We ship WASM to browsers with no debugger — if it breaks in production, there's no attaching a profiler.

Coverage Targets

Metric CI Threshold (enforced) Target How
Statements see web/vitest.config.ts 100% Every function has at least one test exercising its primary path
Branches see web/vitest.config.ts 100% Every if/else, switch case, ternary, and error path tested
Functions see web/vitest.config.ts 100% No untested exported functions
Lines see web/vitest.config.ts 100% Full line coverage

Aggregate thresholds live only in web/vitest.config.ts and ratchet upward automatically. Node and jsdom project configs choose environments; read the root config for enforced values.

100% coverage does not mean 100% bug-free. It means every line of code has been proven to execute without crashing. Edge cases, race conditions, and integration failures need additional targeted tests beyond coverage.

Test Architecture

web/src/**/__tests__/*.test.ts    # Unit + integration tests (vitest)
web/e2e/tests/*.spec.ts           # E2E browser tests (playwright)
mcp-server/src/*.test.ts          # MCP manifest tests (vitest)
engine/src/core/mesh_simplify.rs  # Rust unit tests (cargo test)

Vitest Configuration

  • Workspace split: web/vitest.config.node.ts (environment: node — lib, stores, API routes) and web/vitest.config.jsdom.ts (environment: jsdom — components, hooks)
  • Root web/vitest.config.ts aggregates Node and jsdom projects and enforces coverage thresholds.
  • Coverage: from the repository root, run cd web && npm run test:coverage (bounded, 600s timeout — see scripts/run-vitest-coverage.sh) or cd web && npm run test:coverage:changed (fast, changed-files only, for local/nightly iteration). The full command enforces coverage thresholds locally; CI runs the same threshold checks in .github/workflows/quality-gates.yml (test-web).
  • Run specific: npx vitest run myTestFile

Playwright Configuration

  • Config: web/playwright.config.ts
  • 4 CI shards, chromium only on the blocking test-e2e-ui job
  • Cross-browser: web/playwright.crossbrowser.config.ts runs the SAME @ui
    selection on firefox, webkit, mobile-iphone and mobile-pixel. One CI job per
    project (test-e2e-crossbrowser, non-blocking for now), so a WebKit-only
    regression produces a check that names WebKit. Locally:
    cd web && npm run e2e:crossbrowser (add --project=webkit for one engine)
  • Page Object Model: EditorPage class in web/e2e/fixtures/editor.fixture.ts (import test, expect, EditorPage from ../fixtures/editor.fixture)
  • Requires WASM build + dev server

Test Patterns

Store Slice Tests (most common)

import { createSliceStore, createMockDispatch } from './sliceTestTemplate';

describe('mySlice', () => {
  it('updates data on set action', () => {
    const store = createSliceStore();
    store.getState().setMyData('entity-1', { value: 42 });
    expect(store.getState().myDataMap['entity-1']).toEqual({ value: 42 });
  });

  it('dispatches command correctly', () => {
    const { store, dispatch } = createMockDispatch();
    store.getState().someAction('entity-1', { value: 42 });
    expect(dispatch).toHaveBeenCalledWith('my_command', {
      entityId: 'entity-1',
      value: 42,
    });
  });
});

Event Handler Tests

describe('myDomainEvents', () => {
  it('handles MY_EVENT by updating store', () => {
    const store = createSliceStore();
    const handler = createEventHandler(store);

    handler({
      type: 'MY_EVENT',
      payload: { entityId: 'e1', data: { ... } },
    });

    expect(store.getState().myDataMap['e1']).toEqual({ ... });
  });
});

Chat Handler Tests

describe('myHandler', () => {
  it('validates required args', async () => {
    const result = await handlers['my_command']({}, { store, dispatchCommand: vi.fn() });
    expect(result.success).toBe(false);
    expect(result.error).toContain('Missing');
  });

  it('dispatches with correct params', async () => {
    const dispatchCommand = vi.fn();
    const result = await handlers['my_command'](
      { entityId: 'e1', value: 42 },
      { store, dispatchCommand },
    );
    expect(result.success).toBe(true);
    expect(dispatchCommand).toHaveBeenCalledWith('my_command', {
      entityId: 'e1',
      value: 42,
    });
  });
});

Mock Rules

  • Always use @/lib/... alias in vi.mock() — never relative paths from tests dirs
  • Use vi.resetModules() + dynamic import for script worker tests
  • Stub self with mock postMessage for worker tests
  • No mocking the database in integration tests — use real test instances

What to Test (Priority Order)

P0: User-Facing Actions (must have tests)

  • Every Zustand store action
  • Every engine event handler
  • Every chat handler (arg validation + dispatch)
  • Every export/import pipeline step
  • Every API route handler

P1: Edge Cases (should have tests)

  • Null/undefined entity IDs
  • Empty arrays/objects
  • Maximum value boundaries
  • Concurrent operations on same entity
  • Mode transitions (Edit→Play→Pause→Edit)
  • Scene switching during active operations

P2: Integration (good to have)

  • Full command → event → store → render cycle
  • Multi-entity operations (select all, batch transform)
  • Undo/redo across all action types
  • Scene save/load round-trip fidelity

P3: Regression Prevention

  • Every bug fix gets a test that would have caught it
  • Every "it worked but now it doesn't" gets a regression test

Regression Test Requirements for Bug Fixes

Policy: Every PR that fixes a bug MUST include a regression test. No exceptions.

A PR is considered a bug fix if it has the bug label or its body contains
Fixes #NNN or Closes #NNN referencing an issue.

What counts as a regression test

A regression test is a .test.ts or .test.tsx file added or modified in the PR diff
that exercises the exact scenario that caused the bug. The test must:

  1. Reproduce the failure condition (the test should fail on the buggy code)
  2. Pass on the fixed code
  3. Be named descriptively so the link to the bug is clear
// Good: names the specific bug scenario
it('does not return NaN when tokenCount is undefined (regression for PF-730)', () => {
  const result = computeCost(undefined, 0.01);
  expect(result).toBe(0);
  expect(Number.isNaN(result)).toBe(false);
});

Enforcement

The validator agent runs this check on every bug-fix PR:

bash .claude/skills/testing/scripts/check-regression-test.sh <PR_NUMBER>
  • Detects bug label or Fixes/Closes in the PR body
  • Scans the diff for .test.ts / .test.tsx files
  • Exits 1 with an actionable error if no test files are found

PRs that fail this check are blocked until a regression test is added.

Anti-Patterns (Never Do These)

Anti-Pattern Why It's Bad Do This Instead
expect(result).toBeTruthy() Doesn't test the actual value expect(result).toEqual(expected)
Testing implementation details Breaks on refactor Test behavior and outputs
any in test types Hides type mismatches Use proper types, mock return types
Giant test files (500+ lines) Hard to find/maintain Split by behavior group
Skipped tests (it.skip) Technical debt Fix or delete
Snapshot tests for logic Opaque, auto-updated Explicit assertions
Testing third-party libs Not our code Test our integration points

E2E Test Standards

// Use Page Object Model
const editor = new EditorPage(page);
await editor.waitForReady(); // Waits for WASM engine
await editor.createEntity('Cube');
await editor.selectEntity('Cube');
await expect(editor.inspector).toBeVisible();
  • Every E2E test must wait for WASM readiness — don't race the engine
  • Use stable selectors — data-testid, not CSS classes
  • Take screenshots on failure — configured in playwright.config.ts
  • Isolate state — each test starts with a fresh scene

Validation Tools

Run these to verify test health:

# Test inventory (file counts by type)
bash .claude/tools/validate-tests.sh count

# Coverage report with threshold comparison
bash .claude/tools/validate-tests.sh coverage

# Full test validation
bash .claude/tools/validate-tests.sh full

# Quick frontend validation (lint + tsc + vitest)
bash .claude/tools/validate-frontend.sh quick

# Full project validation
bash .claude/tools/validate-all.sh

Quality Bar

Before declaring test work complete:

  1. bash .claude/tools/validate-frontend.sh quick — all tests pass (zero failures)
  2. bash .claude/tools/validate-tests.sh coverage — coverage meets or exceeds current thresholds
  3. No it.skip or it.todo without a ticket number
  4. Test file naming matches source: myFile.ts → __tests__/myFile.test.ts
  5. Each test has a descriptive name that reads as a specification
  6. Edge case tests exist for every error path in the source code

Running Tests

Default quick suite (lint + tsc + vitest)

cd web && npx eslint --max-warnings 0 . && npx tsc --noEmit && npx vitest run

Specific suite

Suite Command
lint cd web && npx eslint --max-warnings 0 .
tsc cd web && npx tsc --noEmit (also run in mcp-server, packages/ui and apps/docs when you touch them — CI type-checks all four)
vitest cd web && npx vitest run
playwright cd web && npx playwright test (requires WASM build)
mcp cd mcp-server && npx tsc --noEmit && npx vitest run (vitest does not type-check)
coverage cd web && npm run test:coverage (bounded, 600s timeout)

Targeted (preferred during development — avoid full suite when only a few files changed)

# Run tests for a specific file
cd web && npx vitest run src/lib/tokens/creditManager.test.ts

# Run tests matching a pattern
cd web && npx vitest run --reporter=verbose -t "creditManager"

# Run only changed files
cd web && npm run test:changed

E2E prerequisites

Playwright E2E tests require:

  1. WASM engine built — run /build first. Check web/public/engine-pkg-webgl2/ exists.
  2. Playwright browsers installed — cd web && npm run e2e:install
    (chromium + firefox + webkit; the mobile projects are Chromium/WebKit
    device emulations and need no extra download)
  3. Dev server starts automatically via Playwright's webServer config.

Vitest 4.x API Reference

Vitest is Vite-native with Jest-compatible API, native ESM, TypeScript, and JSX support. Note: Vitest 4 defaults to the forks pool; jest-dom matchers need explicit expect.extend(matchers) in vitest.setup.ts.

Core API

Topic Key APIs
Test functions test(), it(), describe(), modifiers: .skip, .only, .concurrent
Assertions expect().toBe(), .toEqual(), .toMatchSnapshot(), .toThrow()
Hooks beforeEach, afterEach, beforeAll, afterAll
Mocking vi.fn(), vi.spyOn(), vi.mock(), vi.resetModules()
Timers vi.useFakeTimers(), vi.advanceTimersByTime()
Coverage V8 provider: cd web && npm run test:coverage (bounded) — see scripts/run-vitest-coverage.sh

Mock Rules (SpawnForge-specific)

  • Always use @/lib/... alias in vi.mock() — never relative paths from __tests__ dirs
  • Use vi.resetModules() + dynamic import for script worker tests
  • Stub self with mock postMessage for worker tests

Configuration

  • Workspace config: web/vitest.workspace.ts — splits into two environments:
    • web/vitest.config.node.ts (environment: node) — lib, stores, API routes
    • web/vitest.config.jsdom.ts (environment: jsdom) — components, hooks
  • Root config: web/vitest.config.ts — aggregates projects and is the only auto-ratcheted coverage threshold authority.
  • Coverage report outputs to web/coverage/

Categories