CI/CD Pipeline
How tests run in CircleCI: format, lint, typecheck, test, build, deploy.
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:
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 / Package | Environment | Key Overrides |
|---|---|---|
apps/api | node | fileParallelism: false, 15s timeout, Cloudflare module aliases |
apps/workbench | jsdom | @vitejs/plugin-react, @testing-library/jest-dom setup file |
apps/mobile | node | React Native module mocks in test/setup.ts |
packages/* | node | Minimal config, shared mergeV8Coverage preset |
The API worker has the most complex config because it stubs Cloudflare-only modules:
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:
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:
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(), })),});# All tests (via Turborepo)make test# or: pnpm turbo test
# Single packagepnpm --filter @openinsure/api testpnpm --filter @openinsure/compliance test
# Watch modemake test-watch# or: pnpm vitest
# With coveragemake test-coverageAPI 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 filesexport 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:
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:
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 })); } },});# Run all E2E tests for the workbenchcd apps/workbenchnpx playwright test
# Run with UI modenpx playwright test --ui
# Run specific browsernpx playwright test --project=chromium
# Update visual snapshotsnpx playwright test --update-snapshotsAll 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:
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 planspackages/compliance/ — filing deadlines, sanctions screeningpackages/policy/ — state machine transitions, bind checklistspackages/rating/ — rate table lookups, factor waterfalls| Location | Type | Environment |
|---|---|---|
packages/<pkg>/__tests__/ | Unit tests for domain logic | node |
apps/api/src/__tests__/ | API route integration tests | node |
apps/workbench/src/__tests__/ | React component tests | jsdom |
apps/mobile/lib/__tests__/ | Validation, cache, auth store | node |
apps/<portal>/src/__tests__/ | Portal component tests | jsdom |
apps/<app>/e2e/ | Playwright E2E tests | Browser |
The API worker alone has 100+ test files covering routes, middleware, services, and integrations. Key test suites include:
billing.test.ts — invoices, payments, commissionsclaims-lifecycle.test.ts — FNOL through settlementbind-workflow.test.ts — submission to bound policycompliance.test.ts — filing deadlines, sanctionsledger-client.test.ts — TigerBeetle double-entry operationspolicy-cancel.test.ts — cancellation state machinevi.clearAllMocks() in beforeEach — but be aware it does not clear mockReturnValueOnce queuesvi.hoisted() for mutable test state that needs to be available before vi.mock() callsvi.mock() at the module level for package stubs; use vi.spyOn() for individual function assertionsdbStore, kvStore, etc.) in beforeEachwaitFor() from @testing-library/react rather than manual timeoutsdescribe, 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.