- Add playwright-web-flake to flake.nix for NixOS browser support - Pin @playwright/test@1.56.1 to match nixpkgs version - Create playwright.config.ts with Chromium-only, auto-start dev server - Add e2e/smoke.spec.ts with initial smoke tests - Add .mcp.json for Claude browser control via MCP - Update .gitignore for playwright artifacts - Remove E2E test skip from spec.md Known Limitations - Update specs/testing.md to require three-tier testing approach Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
7.7 KiB
Testing Specification
Job to Be Done
When I make changes to the codebase, I want automated tests to catch regressions, so that I can deploy with confidence.
Testing Strategy
PhaseFlow uses a three-tier testing approach: unit tests, integration tests, and end-to-end tests.
Test Types
| Type | Scope | Tools | Location |
|---|---|---|---|
| Unit | Pure functions, utilities | Vitest | Colocated *.test.ts |
| Integration | API routes, PocketBase interactions | Vitest + supertest | Colocated *.test.ts |
| E2E | Full user flows, browser interactions | Playwright | e2e/*.spec.ts |
Framework
Vitest - Fast, Vite-native test runner with TypeScript support.
Configuration (vitest.config.ts):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
},
},
});
Unit Tests
Test pure functions in isolation without external dependencies.
Priority Targets
| Module | Functions | Priority |
|---|---|---|
src/lib/cycle.ts |
getCycleDay, getPhase, getPhaseConfig |
High |
src/lib/decision-engine.ts |
getTrainingDecision |
High |
src/lib/nutrition.ts |
getNutritionGuidance, getSeeds, getMacros |
Medium |
src/lib/ics.ts |
generatePhaseEvents, generateCalendarFeed |
Medium |
src/lib/encryption.ts |
encrypt, decrypt |
Medium |
Example Test
// src/lib/cycle.test.ts
import { describe, it, expect } from 'vitest';
import { getCycleDay, getPhase } from './cycle';
describe('getCycleDay', () => {
it('returns 1 on period start date', () => {
const lastPeriod = new Date('2024-01-01');
const today = new Date('2024-01-01');
expect(getCycleDay(lastPeriod, 31, today)).toBe(1);
});
it('handles cycle rollover', () => {
const lastPeriod = new Date('2024-01-01');
const today = new Date('2024-02-01'); // Day 32
expect(getCycleDay(lastPeriod, 31, today)).toBe(1);
});
});
describe('getPhase', () => {
it('returns MENSTRUAL for days 1-3', () => {
expect(getPhase(1, 31)).toBe('MENSTRUAL');
expect(getPhase(3, 31)).toBe('MENSTRUAL');
});
it('scales correctly for 28-day cycle', () => {
// LATE_LUTEAL should be days 22-28 for 28-day cycle
expect(getPhase(22, 28)).toBe('LATE_LUTEAL');
expect(getPhase(21, 28)).toBe('EARLY_LUTEAL');
});
});
Integration Tests
Test API routes and PocketBase interactions.
Setup
Use a test PocketBase instance or PocketBase's testing utilities.
// src/test/setup.ts
import PocketBase from 'pocketbase';
export const testPb = new PocketBase(process.env.TEST_POCKETBASE_URL);
beforeAll(async () => {
// Setup test data
});
afterAll(async () => {
// Cleanup
});
Priority Targets
| Route | Tests |
|---|---|
GET /api/today |
Returns decision with valid auth |
GET /api/cycle/current |
Returns correct phase info |
POST /api/cycle/period |
Updates user record |
GET /api/user |
Returns authenticated user |
PATCH /api/user |
Updates user fields |
GET /api/health |
Returns health status |
Example Test
// src/app/api/today/route.test.ts
import { describe, it, expect } from 'vitest';
import { GET } from './route';
describe('GET /api/today', () => {
it('returns 401 without auth', async () => {
const request = new Request('http://localhost/api/today');
const response = await GET(request);
expect(response.status).toBe(401);
});
it('returns decision with valid auth', async () => {
// Setup authenticated request
const request = new Request('http://localhost/api/today', {
headers: { Cookie: 'pb_auth=...' },
});
const response = await GET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toHaveProperty('decision');
expect(data).toHaveProperty('cycleDay');
});
});
End-to-End Tests
Test complete user flows through the browser using Playwright.
Framework
Playwright - Cross-browser E2E testing with auto-waiting and tracing.
Configuration (playwright.config.ts):
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [["html", { open: "never" }], ["list"]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
],
webServer: {
command: "pnpm dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
Priority Targets
| Flow | Tests |
|---|---|
| Authentication | Login page loads, login redirects, logout works |
| Dashboard | Shows decision, displays cycle info, override toggles work |
| Settings | Garmin token paste, preferences save |
| Calendar | ICS feed accessible, calendar view renders |
| Period logging | "Period Started" updates cycle |
Example Test
// e2e/dashboard.spec.ts
import { expect, test } from "@playwright/test";
test.describe("dashboard", () => {
test("shows training decision for authenticated user", async ({ page }) => {
// Login first (or use auth state)
await page.goto("/");
// Verify decision card is visible
await expect(page.getByTestId("decision-card")).toBeVisible();
// Verify cycle day is displayed
await expect(page.getByText(/Day \d+ of \d+/)).toBeVisible();
});
test("override toggles update decision", async ({ page }) => {
await page.goto("/");
// Enable flare mode
await page.getByLabel("Flare Mode").click();
// Decision should change to gentle
await expect(page.getByText(/GENTLE/)).toBeVisible();
});
});
NixOS Setup
E2E tests require browser binaries. On NixOS, use playwright-web-flake:
# In flake.nix inputs
inputs.playwright-web-flake.url = "github:pietdevries94/playwright-web-flake/1.56.1";
# In devShell
PLAYWRIGHT_BROWSERS_PATH = "${playwright-driver.browsers}";
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
File Naming
Tests colocated with source files (unit/integration) or in e2e/ directory (E2E):
src/
lib/
cycle.ts
cycle.test.ts
decision-engine.ts
decision-engine.test.ts
app/
api/
today/
route.ts
route.test.ts
e2e/
smoke.spec.ts
dashboard.spec.ts
auth.spec.ts
settings.spec.ts
Running Tests
# Run unit/integration tests
pnpm test:run
# Run in watch mode
pnpm test
# Run E2E tests (headless)
pnpm test:e2e
# Run E2E tests (visible browser)
pnpm test:e2e:headed
# Run E2E tests with UI mode
pnpm test:e2e:ui
# Run all tests (unit + E2E)
pnpm test:run && pnpm test:e2e
Coverage Expectations
No strict coverage thresholds for MVP, but aim for:
- 80%+ coverage on
src/lib/(core logic) - Key API routes tested for auth and happy path
Success Criteria
- All tests (unit, integration, E2E) pass in CI before merge
- Core decision engine logic has comprehensive unit tests
- Phase scaling tested for multiple cycle lengths
- API auth tested for protected routes
- Critical user flows covered by E2E tests
Acceptance Tests
pnpm test:runruns without errorspnpm test:e2eruns without errors- Unit tests cover decision engine logic
- Unit tests cover cycle phase calculations
- Integration tests verify API authentication
- E2E tests verify login flow
- E2E tests verify dashboard displays correctly
- E2E tests verify period logging works
- Tests run in CI pipeline