Testing Guide
Vault uses Vitest for unit tests, Playwright for web E2E tests, and Detox for mobile E2E tests.
Unit Tests (Vitest)
Running Tests
# All packages
pnpm test
# Specific package
pnpm --filter @pwm/shared test
pnpm --filter @pwm/api test
# Watch mode
pnpm --filter @pwm/shared test:watchWriting Tests
import { describe, it, expect } from "vitest";
import { generatePassword } from "../generator";
describe("generatePassword", () => {
it("generates password of correct length", () => {
const password = generatePassword({ length: 16 });
expect(password.length).toBe(16);
});
it("includes uppercase when enabled", () => {
const password = generatePassword({
length: 100,
uppercase: true,
lowercase: false,
numbers: false,
symbols: false
});
expect(password).toMatch(/[A-Z]/);
});
});API Tests
Test Hono routes directly without HTTP:
import { describe, it, expect } from "vitest";
import app from "../server";
const mockEnv = {
KV: {
get: vi.fn(),
put: vi.fn(),
delete: vi.fn()
},
JWT_SECRET: "test-secret"
};
describe("vault routes", () => {
it("returns 401 without auth", async () => {
const res = await app.fetch(
new Request("http://localhost/vault"),
mockEnv
);
expect(res.status).toBe(401);
});
it("returns vaults for authenticated user", async () => {
const res = await app.fetch(
new Request("http://localhost/vault", {
headers: { Authorization: `Bearer ${validToken}` }
}),
mockEnv
);
expect(res.status).toBe(200);
});
});Web E2E Tests (Playwright)
Setup
cd packages/web
# Install browsers (one-time)
pnpm playwright:installRunning Tests
# Requires dev servers running:
# Terminal 1: pnpm --filter @pwm/web dev
# Terminal 2: pnpm --filter @pwm/api dev
# Run all tests
pnpm --filter @pwm/web test:e2e
# Interactive UI mode
pnpm --filter @pwm/web test:e2e:ui
# With visible browser
pnpm --filter @pwm/web test:e2e:headed
# Debug mode
pnpm --filter @pwm/web test:e2e:debugWebAuthn Virtual Authenticator
Tests use Chrome DevTools Protocol to create virtual authenticators:
// e2e/fixtures.ts
import { test as base } from "@playwright/test";
export const test = base.extend({
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
// Create virtual authenticator
const client = await page.context().newCDPSession(page);
await client.send("WebAuthn.enable");
await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "internal",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true
}
});
await use(page);
}
});Writing E2E Tests
// e2e/auth.spec.ts
import { test, expect, generateTestEmail } from "./fixtures";
test("complete registration flow", async ({ authenticatedPage }) => {
const page = authenticatedPage;
const email = generateTestEmail();
await page.goto("/signup");
await page.fill("input[type='email']", email);
await page.click("button[type='submit']");
// Virtual authenticator handles WebAuthn
await expect(
page.getByPlaceholder("Create a strong password")
).toBeVisible({ timeout: 10000 });
});
test("login with existing account", async ({ authenticatedPage }) => {
const page = authenticatedPage;
// Setup: Register first
const email = generateTestEmail();
await page.goto("/signup");
await page.fill("input[type='email']", email);
await page.click("button[type='submit']");
await page.waitForURL("**/unlock");
// Test: Login
await page.goto("/login");
await page.fill("input[type='email']", email);
await page.click("button[type='submit']");
await expect(
page.getByPlaceholder("Enter your master password")
).toBeVisible();
});PRF Extension Mock
Virtual authenticators don't support PRF extension. Inject a mock:
await page.addInitScript(() => {
const originalGet = navigator.credentials.get;
navigator.credentials.get = async (options) => {
const result = await originalGet.call(navigator.credentials, options);
if (options?.publicKey?.extensions?.prf && result) {
result.getClientExtensionResults = () => ({
...result.getClientExtensionResults(),
prf: {
results: {
first: new Uint8Array(32).buffer
}
}
});
}
return result;
};
});Debugging E2E Tests
# Run with trace
pnpm --filter @pwm/web test:e2e --trace on
# View trace
npx playwright show-trace trace.zip
# Take screenshot during test
await page.screenshot({ path: "debug.png" });
# Add console logging
page.on("console", (msg) => console.log(`[browser] ${msg.text()}`));Mobile E2E Tests (Detox)
Setup
cd packages/mobile
# Install Detox CLI
npm install -g detox-cli
# Build app for testing
detox build --configuration ios.sim.debugRunning Tests
# iOS Simulator
pnpm test:e2e:ios
# Android Emulator
pnpm test:e2e:androidWriting Mobile Tests
// e2e/vault.test.ts
describe("Vault", () => {
beforeAll(async () => {
await device.launchApp();
});
it("shows vault entries after unlock", async () => {
// Login with dev mode
await element(by.text("Dev Login")).tap();
// Enter master password
await element(by.id("master-password")).typeText("testpassword");
await element(by.text("Unlock")).tap();
// Verify vault loads
await expect(element(by.id("vault-list"))).toBeVisible();
});
it("can add new entry", async () => {
await element(by.id("add-entry")).tap();
await element(by.id("entry-name")).typeText("Test Entry");
await element(by.id("entry-password")).typeText("testpass123");
await element(by.text("Save")).tap();
await expect(element(by.text("Test Entry"))).toBeVisible();
});
});Test Coverage
Checking Coverage
# Generate coverage report
pnpm --filter @pwm/shared test:coverage
# View HTML report
open packages/shared/coverage/index.htmlCoverage Targets
| Package | Target |
|---|---|
@pwm/shared | 90%+ |
@pwm/api | 80%+ |
@pwm/web | 60%+ |
CI/CD Integration
Tests run automatically on pull requests:
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- run: pnpm install
- run: pnpm typecheck
- run: pnpm test
- run: pnpm --filter @pwm/web test:e2eBest Practices
Unit Tests
- Test pure functions thoroughly
- Mock external dependencies
- Use descriptive test names
- One assertion per test when possible
E2E Tests
- Test user flows, not implementation
- Use realistic test data
- Handle async operations properly
- Clean up after tests
General
- Run tests before committing
- Don't skip failing tests
- Update tests when changing behavior
Related
- Contributing - Development workflow
- Project Structure - Package layout
- Deployment - CI/CD pipeline