Skip to content

Testing

All packages in the monorepo use Vitest as the test framework. Tests are co-located with source code and follow consistent conventions.

Test Configuration

The shared test configuration lives at shared/vitest.config.ts:

typescript
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: false,           // Explicit imports required
    environment: 'node',      // Default environment
    include: ['src/**/*.{test,spec}.{js,ts}'],
    passWithNoTests: true,
    coverage: {
      enabled: true,
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'dist/', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts'],
    },
  },
});

Each package has its own vitest.config.ts that extends the shared config. Some packages override the environment (e.g., jsdom for browser-dependent code).

Key Settings

  • globals: false: Test functions (describe, it, expect, vi) must be explicitly imported from vitest. This prevents accidental globals and ensures clarity.
  • environment: 'node': Tests run in a Node.js environment by default. Packages needing a DOM use jsdom or happy-dom.
  • Coverage provider: v8 (built into Node.js, no instrumentation overhead).

Running Tests

All Tests

bash
# Run all tests across the monorepo
pnpm test

# Run only tests affected by your changes (relative to origin/main)
pnpm test:affected

Single Package

bash
cd packages/core/ecs
pnpm test

Specific File

bash
cd packages/core/ecs
vitest run src/World.test.ts

Watch Mode

bash
cd packages/core/ecs
pnpm test:watch

With Coverage Report

bash
cd packages/core/ecs
vitest run --coverage

Writing Tests

File Naming

Test files are co-located with their source files using the .test.ts suffix:

src/
├── SelectionManager.ts
├── SelectionManager.test.ts
├── HistoryManager.ts
└── HistoryManager.test.ts

Explicit Imports

Since globals: false is set, always import test functions explicitly:

typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SelectionManager } from './SelectionManager.js';

describe('SelectionManager', () => {
  let manager: SelectionManager;

  beforeEach(() => {
    manager = new SelectionManager();
  });

  it('should start with empty selection', () => {
    expect(manager.count).toBe(0);
    expect(manager.selected).toEqual([]);
  });

  it('should select entities', () => {
    manager.select([1, 2, 3]);
    expect(manager.count).toBe(3);
    expect(manager.isSelected(1)).toBe(true);
  });
});

Test Structure

Follow the Arrange-Act-Assert pattern:

typescript
it('should toggle selection', () => {
  // Arrange
  manager.select([1, 2]);

  // Act
  manager.select([2], { mode: 'toggle' });

  // Assert
  expect(manager.isSelected(1)).toBe(true);
  expect(manager.isSelected(2)).toBe(false);
  expect(manager.count).toBe(1);
});

Mocking

Use vi.fn() for mock functions and vi.spyOn() for spying:

typescript
it('should notify on selection change', () => {
  const callback = vi.fn();
  manager.onSelectionChange(callback);

  manager.select([1]);

  expect(callback).toHaveBeenCalledOnce();
  expect(callback).toHaveBeenCalledWith(
    expect.objectContaining({ added: [1], removed: [] }),
  );
});

Testing Async Code

typescript
it('should load session', async () => {
  const session = new SessionManager(new MemoryStorage());
  await session.load();
  expect(session.preferences).toBeDefined();
});

Coverage Requirements

Each package targets 80% coverage across lines, functions, branches, and statements. This is enforced at the package level, not monorepo-wide.

Coverage reports are generated in three formats:

  • text: Summary in the terminal
  • json: Machine-readable for CI processing
  • html: Browsable report in coverage/ directory

Browser Tests

Some packages (particularly rendering) include browser-based tests:

bash
# Run browser tests
pnpm test:browser

# Run affected browser tests
pnpm test:browser:affected

Browser tests use Playwright for headless execution.

Integration Tests

Cross-package integration tests live in testing/integration/:

bash
pnpm test:integration

These tests use Playwright and verify end-to-end behavior across package boundaries.

Diagnostic Setup

The shared Vitest setup file (shared/vitest-diagnostic-setup.ts) automatically catches invariant() violations during tests via the DiagnosticBus. This means:

  • Invariant violations that would normally only produce diagnostic events in production will cause test failures
  • You do not need to manually subscribe to the diagnostic bus in tests

Tips

  • Test edge cases: Empty arrays, zero values, boundary conditions, concurrent operations
  • Test error paths: Invalid inputs, missing resources, network failures
  • Avoid test interdependence: Each test should be able to run in isolation
  • Use beforeEach for fresh state: Create new instances per test to prevent state leakage
  • Property-based testing: The fast-check library is available for property-based testing where appropriate

Proprietary software. All rights reserved.