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:
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 fromvitest. This prevents accidental globals and ensures clarity.environment: 'node': Tests run in a Node.js environment by default. Packages needing a DOM usejsdomorhappy-dom.- Coverage provider: v8 (built into Node.js, no instrumentation overhead).
Running Tests
All Tests
# Run all tests across the monorepo
pnpm test
# Run only tests affected by your changes (relative to origin/main)
pnpm test:affectedSingle Package
cd packages/core/ecs
pnpm testSpecific File
cd packages/core/ecs
vitest run src/World.test.tsWatch Mode
cd packages/core/ecs
pnpm test:watchWith Coverage Report
cd packages/core/ecs
vitest run --coverageWriting 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.tsExplicit Imports
Since globals: false is set, always import test functions explicitly:
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:
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:
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
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:
# Run browser tests
pnpm test:browser
# Run affected browser tests
pnpm test:browser:affectedBrowser tests use Playwright for headless execution.
Integration Tests
Cross-package integration tests live in testing/integration/:
pnpm test:integrationThese 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
beforeEachfor fresh state: Create new instances per test to prevent state leakage - Property-based testing: The
fast-checklibrary is available for property-based testing where appropriate