Skip to 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.


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.

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

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
},
});

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(),
})),
});

Terminal window
# 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

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: /* ... */ };
},
}));

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(' ');

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.


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);
},
},
};

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' });
}
},
}));

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 })),
}),
};
},
}));

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,
},
});

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 }));
}
},
});
Terminal window
# 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

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}'],
}),
},
});

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.

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

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

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

  • 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.