# Web E2E Tests Browser-based end-to-end tests that exercise the full stack: Next.js frontend, Go backend, and mock agent. Tests run in parallel with complete isolation — each Playwright worker gets its own backend process, frontend process, database, and temp directory. **Location:** `apps/web/e2e/` ## Architecture ``` ┌─ Playwright ──────────────────────────────────────────────┐ │ Worker 0 Worker 1 │ │ ┌──────────────────────┐ ┌──────────────────────┐│ │ │ Backend :18080 │ │ Backend :18081 ││ │ │ Frontend :13000 │ │ Frontend :13001 ││ │ │ SSR → localhost:18080│ │ SSR → localhost:18081││ │ │ HOME=/tmp/e2e-0 │ │ HOME=/tmp/e2e-1 ││ │ │ own SQLite DB │ │ own SQLite DB ││ │ │ mock-agent only │ │ mock-agent only ││ │ │ mock GitHub │ │ mock GitHub ││ │ └──────────────────────┘ └──────────────────────┘│ └───────────────────────────────────────────────────────────┘ ``` - **One backend per worker** — port `18080 + workerIndex`, isolated temp HOME, own SQLite DB - **One frontend per worker** — port `13000 + workerIndex`, SSR routed to that worker's backend via `KANDEV_API_BASE_URL` - **Shared `.next/` build** — all frontend processes serve from the same pre-built output (read-only) - **Mock agent** — deterministic responses, no API keys needed - **Mock GitHub client** — reports as authenticated, returns configurable data via REST API - **No external dependencies** — no Docker, no GitHub tokens, no real git credentials ## Prerequisites Build the backend binaries and web app before running tests: ```bash make build-backend build-web ``` This produces `apps/backend/bin/kandev`, `apps/backend/bin/mock-agent`, and `apps/web/.next/`. The global setup script verifies all exist. ## Running Tests All commands run from the repo root via Make targets (these build automatically): ```bash make test-e2e # Headless, parallel (default) make test-e2e-headed # Visible browser window make test-e2e-ui # Playwright UI mode (step through, inspect) make test-e2e-report # Open HTML report from last run ``` Or directly via pnpm (requires pre-built binaries and web app): ```bash cd apps pnpm --filter @kandev/web e2e # All tests pnpm --filter @kandev/web e2e -- --grep "task creation" # By name pnpm --filter @kandev/web e2e -- tests/create-task.spec.ts # Single file pnpm --filter @kandev/web e2e:headed # With browser pnpm --filter @kandev/web e2e:ui # UI mode ``` ### Debug output Set `E2E_DEBUG=1` to see backend and frontend stderr in the terminal: ```bash E2E_DEBUG=1 make test-e2e ``` ## Directory Structure ``` apps/web/e2e/ ├── playwright.config.ts # Playwright configuration ├── global-setup.ts # Verifies backend binaries and web build exist ├── fixtures/ │ ├── backend.ts # Worker-scoped backend + frontend process fixture │ └── test-base.ts # Extended test fixture (apiClient, seedData, testPage) ├── helpers/ │ └── api-client.ts # HTTP client for API seeding ├── pages/ │ ├── kanban-page.ts # Kanban board page object │ └── task-detail-page.ts # Task detail page object └── tests/ ├── kanban-board.spec.ts # Kanban board display ├── create-task.spec.ts # Task creation flows ├── task-detail.spec.ts # Task detail views └── workflow-steps.spec.ts # Workflow step progression ``` ## How It Works ### Backend + frontend fixture (`fixtures/backend.ts`) Each Playwright worker spawns an isolated `kandev` backend and a dedicated `next start` frontend: **Backend:** 1. Creates a temp directory as `HOME` with its own `.gitconfig`, data dir, and SQLite DB 2. Sets environment variables for isolation: - `KANDEV_DATA_DIR` — temp data directory - `KANDEV_SERVER_PORT` — unique port per worker (`18080 + workerIndex`) - `KANDEV_DATABASE_PATH` — temp SQLite path - `KANDEV_MOCK_AGENT=only` — only loads mock agent, skips agent discovery (use `"true"` in dev mode to enable mock alongside all agents) - `KANDEV_MOCK_GITHUB=true` — uses in-memory MockClient instead of real GitHub API - `KANDEV_DOCKER_ENABLED=false` — no Docker - `KANDEV_WORKTREE_ENABLED=false` — no worktrees - `GH_TOKEN` / `GITHUB_TOKEN` stripped — prevents accidental real API calls 3. Spawns `apps/backend/bin/kandev` and waits for `/health` to return 200 **Frontend:** 1. Spawns `npx next start --port {13000 + workerIndex}` from `apps/web/` 2. Sets `KANDEV_API_BASE_URL` to the worker's backend URL so SSR routes correctly 3. Waits for the frontend to become healthy 4. All `next start` processes share the same `.next/` build output (read-only) **Teardown:** sends SIGTERM to frontend then backend, falls back to SIGKILL after 5s, cleans up temp dir. ### Test base (`fixtures/test-base.ts`) Extends the backend fixture with: - **`apiClient`** (worker-scoped) — HTTP client for seeding data via the backend API - **`seedData`** (worker-scoped) — creates a workspace, discovers the default workflow and its steps - **`testPage`** (per-test) — Playwright page with a browser context whose `baseURL` points to the worker's frontend, pre-configured with: - `kandev.onboarding.completed = "true"` in localStorage — skips onboarding - `window.__KANDEV_API_PORT` injected via `addInitScript` — points to the worker's backend port ### API client (`helpers/api-client.ts`) Seeds test data via REST API. Available methods: | Method | Endpoint | Purpose | |--------|----------|---------| | `createWorkspace(name)` | `POST /api/workspaces` | Create workspace | | `createTask(workspaceId, title, desc)` | `POST /api/tasks` | Create task | | `listWorkflows(workspaceId)` | `GET /api/workspaces/:id/workflows` | List workflows | | `listWorkflowSteps(workflowId)` | `GET /api/workflows/:id/workflow/steps` | List steps | | `createWorkflowStep(name, pos)` | `POST /api/workflow/steps` | Create step | | `moveTask(taskId, stepId)` | `POST /api/tasks/:id/move` | Move task to step | **GitHub mock control:** | Method | Endpoint | Purpose | |--------|----------|---------| | `mockGitHubReset()` | `DELETE /api/v1/github/mock/reset` | Clear all mock data | | `mockGitHubSetUser(username)` | `PUT /api/v1/github/mock/user` | Set authenticated user | | `mockGitHubAddPRs(prs)` | `POST /api/v1/github/mock/prs` | Add mock PRs | | `mockGitHubAddOrgs(orgs)` | `POST /api/v1/github/mock/orgs` | Add mock organizations | | `mockGitHubAddRepos(org, repos)` | `POST /api/v1/github/mock/repos` | Add mock repositories | | `mockGitHubAddReviews(owner, repo, num, reviews)` | `POST /api/v1/github/mock/reviews` | Add PR reviews | | `mockGitHubAddCheckRuns(owner, repo, ref, checks)` | `POST /api/v1/github/mock/checks` | Add CI check runs | | `mockGitHubGetStatus()` | `GET /api/v1/github/status` | Verify auth status | ### Page objects (`pages/`) Page objects encapsulate selectors and navigation: - **`KanbanPage`** — `goto()`, `board`, `createTaskButton`, `taskCard(id)`, `taskCardByTitle(title)` - **`TaskDetailPage`** — `goto(taskId, sessionId)`, `sessionChat`, `turnCompleteIndicator`, `waitForAgentResponse(text)` ### Selectors Tests use `data-testid` attributes for stability across layout/styling changes: | `data-testid` | Component | |----------------|-----------| | `kanban-board` | Kanban board container | | `task-card-{id}` | Individual task card | | `create-task-button` | Create task button | | `create-task-dialog` | Task creation dialog | | `task-title-input` | Title input in dialog | | `task-description-input` | Description textarea in dialog | | `submit-start-agent` | Start agent button | | `submit-plan-mode` | Plan mode option | | `session-chat` | Session chat panel | | `agent-turn-complete` | Agent turn complete indicator | ## Writing New Tests ### 1. Create a spec file ```typescript // apps/web/e2e/tests/my-feature.spec.ts import { test, expect } from "../fixtures/test-base"; import { KanbanPage } from "../pages/kanban-page"; test.describe("my feature", () => { test("does something", async ({ testPage, seedData, apiClient }) => { // Seed data via API const task = await apiClient.createTask( seedData.workspaceId, "Test Task", "Description", ); // Navigate and interact const kanban = new KanbanPage(testPage); await kanban.goto(seedData.workspaceId); // Assert await expect(kanban.taskCardByTitle("Test Task")).toBeVisible(); }); }); ``` ### 2. Add data-testid attributes When testing new UI components, add `data-testid` to the relevant elements: ```tsx