Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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:watch

Writing 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:install

Running 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:debug

WebAuthn 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.debug

Running Tests

# iOS Simulator
pnpm test:e2e:ios
 
# Android Emulator
pnpm test:e2e:android

Writing 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.html

Coverage Targets

PackageTarget
@pwm/shared90%+
@pwm/api80%+
@pwm/web60%+

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:e2e

Best 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