Skip to main content

Testing

OpenInsure uses Vitest for unit and integration tests, Playwright for end-to-end browser tests, and MSW for API mocking. Every package and app has its own vitest.config.ts with environment-appropriate settings.


Vitest Setup

Root Configuration

The root vitest.config.ts sets the baseline for all packages:

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
oxc: {
jsx: {
runtime: 'automatic',
importSource: 'react',
},
},
test: {
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
},
},
});

OXC handles JSX transformation (not Babel or SWC). The default test environment is node -- browser-dependent apps override this to jsdom.

Per-App Overrides

Each app customizes the base config for its runtime context:

App / PackageEnvironmentKey Overrides
apps/apinodefileParallelism: false, 15s timeout, Cloudflare module aliases
apps/workbenchjsdom@vitejs/plugin-react, @testing-library/jest-dom setup file
apps/mobilenodeReact Native module mocks in test/setup.ts
packages/*nodeMinimal config, shared mergeV8Coverage preset

API Worker Test Config

The API worker has the most complex config because it stubs Cloudflare-only modules:

// apps/api/vitest.config.ts
export default defineConfig({
resolve: {
alias: {
// Cloudflare virtual modules don't exist in Node
'cloudflare:workers': path.resolve(__dirname, 'src/__stubs__/cloudflare-workers.ts'),
'@sentry/cloudflare': path.resolve(__dirname, 'src/__stubs__/sentry-cloudflare.ts'),
},
},
test: {
environment: 'node',
fileParallelism: false, // Stabilize under monorepo load
testTimeout: 15000, // Allow time for dynamic imports + async queues
},
});

Workbench Test Config

The workbench (Vite SPA) uses jsdom and stubs all @openinsure/ui subpath imports:

// apps/workbench/vitest.config.ts
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@openinsure/ui/button': path.resolve(__dirname, './src/test/ui-stubs.ts'),
'@openinsure/ui/input': path.resolve(__dirname, './src/test/ui-stubs.ts'),
// ... all @openinsure/ui/* subpath imports → ui-stubs.ts
},
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
},
});

The setup.ts file provides @testing-library/jest-dom matchers and mocks window.matchMedia:

// apps/workbench/src/test/setup.ts
import '@testing-library/jest-dom';
import { vi } from 'vitest';

Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});

Running Tests

# All tests (via Turborepo)
make test
# or: pnpm turbo test

# Single package
pnpm --filter @openinsure/api test
pnpm --filter @openinsure/compliance test

# Watch mode
make test-watch
# or: pnpm vitest

# With coverage
make test-coverage

Test Data Conventions

caution

Use realistic insurance data in tests -- never { foo: 'bar' }. Test data should reflect actual policy numbers, NAICS codes, premium amounts, and claim descriptions.

API Tests: Mutable DB State Pattern

API route tests use a vi.hoisted() pattern to create mutable stores that simulate database behavior:

const dbStore = vi.hoisted(() => ({
invoices: [] as Record<string, unknown>[],
payments: [] as Record<string, unknown>[],
policies: [] as Record<string, unknown>[],
}));

vi.mock('@openinsure/db', () => ({
invoices: { __table: 'invoices' },
payments: { __table: 'payments' },
createDb: () => {
// Return a chainable mock that reads from dbStore
const makeSelect = () => ({
from: (table) => ({
where: () => dbStore[table.__table] ?? [],
}),
});
return { select: makeSelect, insert: /* ... */ };
},
}));

Workbench Tests: UI Stub Pattern

The workbench stubs all @openinsure/ui components with minimal HTML equivalents. This avoids building the UI package for tests:

// src/test/ui-stubs.ts — shared across all workbench test files
export const Button = ({ children, onClick, type, disabled, ...props }) => (
<button type={type ?? 'button'} onClick={onClick} disabled={disabled} {...props}>
{children}
</button>
);

export const Input = React.forwardRef((props, ref) => <input ref={ref} {...props} />);
export const Label = ({ children, ...props }) => <label {...props}>{children}</label>;
export const cn = (...values) => values.filter(Boolean).join(' ');

Integration Tests with Real DB

Some API tests support running against a real database when TEST_DATABASE_URL is set:

const testDatabaseUrl = process.env.TEST_DATABASE_URL;
const describeIfDb = testDatabaseUrl ? describe : describe.skip;

describeIfDb('Claims integration (real DB)', () => {
// Uses actual PlanetScale/MySQL connection
// Cleans up after itself in afterAll
});

When the env var is absent, these tests are automatically skipped. CI always runs them against a PlanetScale dev branch.


Mocking Patterns

Cloudflare Bindings

The API worker tests mock Cloudflare-specific bindings (KV, Queues, R2, Durable Objects):

const kvStore = new Map<string, string>();
const queueMessages: Array<Record<string, unknown>> = [];

const env = {
HYPERDRIVE: { connectionString: testDatabaseUrl },
JWT_SECRET: 'test-secret-32-chars-minimum-len',
KV: {
get: (key: string) => kvStore.get(key) ?? null,
put: (key: string, value: string) => {
kvStore.set(key, value);
},
},
QUEUE: {
send: (msg: Record<string, unknown>) => {
queueMessages.push(msg);
},
},
};

Package Mocks

Domain packages are mocked at the module level to isolate route handler logic:

vi.mock('@openinsure/agents', () => ({
routeAgents: () => Promise.resolve(null),
SubmissionAgent: class {},
ClaimAgent: class {},
}));

vi.mock('@openinsure/analytics', () => ({
computeMGAKPIs: () => ({}),
computeCaptiveKPIs: () => ({}),
}));

vi.mock('@openinsure/policy', () => ({
SubmissionService: class {
quoteSubmission() {
return Promise.resolve({ premium: 0, decision: 'accept' });
}
},
}));

Form Mocking (Workbench)

The workbench mocks @openinsure/form and @openinsure/react-query to test form behavior without real API calls:

vi.mock('@openinsure/form', () => ({
useZodForm: ({ defaultValues, onSubmit }) => {
const [values, setValues] = React.useState(defaultValues);
const [touched, setTouched] = React.useState({});
return {
reset: () => setValues(defaultValues),
handleSubmit: () => onSubmit({ value: values }),
Field: ({ name, validators, children }) =>
children({
state: { value: values[name] ?? '', meta: { isTouched: touched[name], errors: [] } },
handleChange: (value) => setValues((c) => ({ ...c, [name]: value })),
handleBlur: () => setTouched((c) => ({ ...c, [name]: true })),
}),
};
},
}));

Playwright E2E

Configuration

Each app with a UI has a playwright.config.ts:

// apps/workbench/playwright.config.ts
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',

expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.01,
animations: 'disabled',
},
},

use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},

projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],

webServer: {
command: 'PLAYWRIGHT=1 pnpm dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});

Coverage Collection

E2E tests collect V8 coverage via a custom Playwright fixture:

// apps/workbench/e2e/test.ts
export const test = base.extend({
page: async ({ page, browserName }, use, testInfo) => {
const shouldCollect = process.env.PLAYWRIGHT_COVERAGE === '1' && browserName === 'chromium';
if (shouldCollect) await page.coverage.startJSCoverage({ resetOnNavigation: false });
await use(page);
if (shouldCollect) {
const result = await page.coverage.stopJSCoverage();
await fs.writeFile(testInfo.outputPath('v8-coverage.json'), JSON.stringify({ result }));
}
},
});

Running E2E Tests

# Run all E2E tests for the workbench
cd apps/workbench
npx playwright test

# Run with UI mode
npx playwright test --ui

# Run specific browser
npx playwright test --project=chromium

# Update visual snapshots
npx playwright test --update-snapshots

Coverage Enforcement

V8 Coverage Preset

All packages use a shared mergeV8Coverage preset from vitest.coverage.preset.ts:

import { mergeV8Coverage } from '../../vitest.coverage.preset';

export default defineConfig({
test: {
coverage: mergeV8Coverage({
include: ['src/**/*.{ts,tsx}'],
}),
},
});

Coverage Exclusions (API Worker)

The API worker excludes Cloudflare-runtime code that cannot be tested in Node:

  • Queue handlers (require Cloudflare Queue bindings)
  • Durable Objects (require Cloudflare DO runtime)
  • External SDK wrappers (require live credentials)
  • Workflow orchestrators (require Cloudflare Workflows runtime)

These paths are covered by E2E and integration suites instead.

Required Coverage Paths

Financial, compliance, and state-transition code paths require 100% coverage:

  • packages/billing/ -- premium calculation, installment plans
  • packages/compliance/ -- filing deadlines, sanctions screening
  • packages/policy/ -- state machine transitions, bind checklists
  • packages/rating/ -- rate table lookups, factor waterfalls

Test Organization

LocationTypeEnvironment
packages/<pkg>/__tests__/Unit tests for domain logicnode
apps/api/src/__tests__/API route integration testsnode
apps/workbench/src/__tests__/React component testsjsdom
apps/mobile/lib/__tests__/Validation, cache, auth storenode
apps/<portal>/src/__tests__/Portal component testsjsdom
apps/<app>/e2e/Playwright E2E testsBrowser

Test Count by Area

The API worker alone has 100+ test files covering routes, middleware, services, and integrations. Key test suites include:

  • billing.test.ts -- invoices, payments, commissions
  • claims-lifecycle.test.ts -- FNOL through settlement
  • bind-workflow.test.ts -- submission to bound policy
  • compliance.test.ts -- filing deadlines, sanctions
  • ledger-client.test.ts -- TigerBeetle double-entry operations
  • policy-cancel.test.ts -- cancellation state machine

Best Practices

  • Use vi.clearAllMocks() in beforeEach -- but be aware it does not clear mockReturnValueOnce queues

  • Use vi.hoisted() for mutable test state that needs to be available before vi.mock() calls

  • Prefer vi.mock() at the module level for package stubs; use vi.spyOn() for individual function assertions

  • Always reset mutable stores (dbStore, kvStore, etc.) in beforeEach

  • For async operations, use waitFor() from @testing-library/react rather than manual timeouts

  • Import describe, expect, it, vi from vitest explicitly (not relying on globals in non-workbench packages)

    CI/CD Pipeline

    How tests run in CircleCI: format, lint, typecheck, test, build, deploy.

    Local Development

    Docker Compose services, environment setup, and dev server commands.