Overview
Codapult ships with two testing frameworks:
- Vitest for unit and integration tests
- Playwright for end-to-end browser tests
Both are pre-configured and ready to use.
Unit Tests (Vitest)
Unit tests are co-located with the source code — each test file lives next to the module it tests.
src/lib/payments/
├── stripe.ts
├── stripe.test.ts ← unit test
├── lemonsqueezy.ts
└── lemonsqueezy.test.ts
Writing Tests
Vitest globals (describe, it, expect) are enabled — no imports needed:
import { calculateDiscount } from './pricing';
describe('calculateDiscount', () => {
it('applies percentage discount', () => {
const result = calculateDiscount(100, { type: 'percent', value: 20 });
expect(result).toBe(80);
});
it('applies fixed discount', () => {
const result = calculateDiscount(100, { type: 'fixed', value: 15 });
expect(result).toBe(85);
});
it('never returns negative', () => {
const result = calculateDiscount(10, { type: 'fixed', value: 50 });
expect(result).toBe(0);
});
});
Running Tests
pnpm test # run all unit tests
pnpm test -- --watch # watch mode (re-run on file changes)
pnpm test -- pricing # run tests matching "pricing"
Mocking
Use Vitest's built-in mocking for dependencies:
import { vi } from 'vitest';
import { sendEmail } from './email';
import { resend } from './resend-client';
vi.mock('./resend-client', () => ({
resend: { emails: { send: vi.fn() } },
}));
describe('sendEmail', () => {
it('calls Resend with correct parameters', async () => {
await sendEmail({ to: '[email protected]', subject: 'Welcome' });
expect(resend.emails.send).toHaveBeenCalledWith(
expect.objectContaining({
to: '[email protected]',
subject: 'Welcome',
}),
);
});
});
E2E Tests (Playwright)
End-to-end tests live in the e2e/ directory and run against a real browser.
Configuration
Playwright is configured in playwright.config.ts at the project root. It includes:
- Browsers: Chromium, Firefox, WebKit, and Mobile Chrome
- Dev server: Starts automatically via
pnpm devbefore tests run - Artifacts: Screenshots on failure, traces on retry
Writing Tests
Use the page object pattern for maintainable tests:
import { test, expect } from '@playwright/test';
test.describe('Sign in', () => {
test('shows validation error for invalid email', async ({ page }) => {
await page.goto('/sign-in');
await page.getByLabel('Email').fill('not-an-email');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Invalid email')).toBeVisible();
});
test('redirects to dashboard after sign in', async ({ page }) => {
await page.goto('/sign-in');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
});
});
Running Tests
pnpm test:e2e # run all E2E tests (headless)
pnpm test:e2e -- --headed # run with visible browser
pnpm test:e2e -- --ui # open Playwright UI mode
pnpm test:e2e -- --project=chromium # run in Chromium only
Debugging Failures
When a test fails, Playwright saves artifacts to help diagnose the issue:
- Screenshots are captured on failure
- Traces are retained on failure and retries — open with
npx playwright show-trace
npx playwright show-trace test-results/example-test/trace.zip
Scripts Reference
| Command | What it runs |
| ------------------- | --------------------- |
| pnpm test | Vitest unit tests |
| pnpm test:e2e | Playwright E2E tests |
| pnpm lint | ESLint |
| pnpm format | Prettier (write) |
| pnpm format:check | Prettier (check only) |
Best Practices
- Co-locate unit tests with the code they test (
*.test.tsnext to the source file). - Use descriptive test names that explain the expected behavior, not the implementation.
- Keep E2E tests focused on critical user flows — sign in, billing, core features.
- Use page objects in E2E tests to avoid duplicating selectors across test files.
- Run tests before committing — add
pnpm test && pnpm lintto your pre-commit workflow. - Mock external services in unit tests (Resend, Stripe, AI providers) to keep tests fast and deterministic.