Web E2E Tests
August 24, 2026 · View on GitHub
Browser-based end-to-end tests that exercise the full stack: Go-served Vite SPA, Go backend, and mock agent. Tests run in parallel with complete isolation — each Playwright worker gets its own backend process, database, and temp directory.
Location: apps/web/e2e/
Architecture
┌─ Playwright ──────────────────────────────────────────────┐
│ Worker 0 Worker 1 │
│ ┌──────────────────────┐ ┌──────────────────────┐│
│ │ Backend :18080 │ │ Backend :18081 ││
│ │ Web UI :18080 │ │ Web UI :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 - Web UI served by backend — each worker's backend serves Vite
distand boot data on the same port as the API - Shared
dist/build — all workers serve from the same pre-built Vite 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:
make build-backend build-web
This produces apps/backend/bin/kandev, apps/backend/bin/mock-agent, and apps/web/dist/. The global setup script verifies all exist.
Running Tests
All commands run from the repo root via Make targets (these build automatically):
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):
cd apps
pnpm --filter @kandev/web e2e:raw # All tests
pnpm --filter @kandev/web e2e:raw -- --grep "task creation" # By name
pnpm --filter @kandev/web e2e:raw -- 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:
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 that also serves the SPA:
Backend:
- Creates a temp directory as
HOMEwith its own.gitconfig, data dir, and SQLite DB - Sets environment variables for isolation:
KANDEV_HOME_DIR— temp data directoryKANDEV_SERVER_PORT— unique port per worker (18080 + workerIndex)KANDEV_DATABASE_PATH— temp SQLite pathKANDEV_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 APIKANDEV_DOCKER_ENABLED=false— no DockerKANDEV_WORKTREE_ENABLED=false— no worktreesGH_TOKEN/GITHUB_TOKENstripped — prevents accidental real API calls
- Spawns
apps/backend/bin/kandev __backendand waits for/readyto return 200 (not/health, which flips green as soon as the listener is bound, before the mock-harness routes are mounted)
Frontend:
- Sets
KANDEV_WEB_DIST_DIRtoapps/web/dist - Uses the worker backend URL as the browser URL
- The backend serves
index.html, static assets, and injected boot payloads - All workers share the same
dist/build output (read-only)
Teardown: sends SIGTERM to the backend process group, falls back to SIGKILL after 7s, 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 APIseedData(worker-scoped) — creates a workspace, discovers the default workflow and its stepstestPage(per-test) — Playwright page with a browser context whosebaseURLpoints to the worker's frontend, pre-configured with:kandev.onboarding.completed = "true"in localStorage — skips onboardingwindow.__KANDEV_API_PORTinjected viaaddInitScript— 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
// 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:
<div data-testid="my-component">...</div>
Then reference them in page objects or directly:
page.getByTestId("my-component");
3. Seed mock GitHub data
The mock GitHub client (KANDEV_MOCK_GITHUB=true) reports as authenticated and serves data seeded via the /api/v1/github/mock/ endpoints. Use the apiClient helper methods:
test("shows PR feedback in settings", async ({ testPage, apiClient, seedData }) => {
// Seed a mock PR
await apiClient.mockGitHubAddPRs([{
number: 42,
title: "Add feature X",
state: "open",
head_branch: "feature-x",
base_branch: "main",
author_login: "mock-user",
repo_owner: "myorg",
repo_name: "myrepo",
html_url: "https://github.com/myorg/myrepo/pull/42",
}]);
// Seed orgs for the org picker
await apiClient.mockGitHubAddOrgs([{ login: "myorg" }]);
// Seed repos for the repo search
await apiClient.mockGitHubAddRepos("myorg", [
{ full_name: "myorg/myrepo", owner: "myorg", name: "myrepo" },
]);
// Navigate and test GitHub UI features...
});
Call mockGitHubReset() between tests if you need a clean slate (each worker already starts fresh).
4. Extend the API client
If your test needs new seed data, add methods to helpers/api-client.ts:
async createMyEntity(params: { ... }): Promise<MyEntity> {
return this.post("/api/my-entities", params);
}
Configuration
| Setting | Value | Notes |
|---|---|---|
fullyParallel | true | Tests run in parallel across workers |
workers | auto (local), 2 (CI) | Worker count |
timeout | 60s | Per-test timeout |
retries | 0 (local), 2 (CI) | Retry count |
trace | on-first-retry | Playwright trace recording |
screenshot | only-on-failure | Auto-screenshot |
video | on-first-retry | Video recording |
| Browser | Chromium only | Single browser project |
CI
The GitHub Actions workflow (.github/workflows/e2e-tests.yml) runs on pushes and PRs to main that touch apps/backend/, apps/web/, or apps/packages/. It:
- Builds Go binaries (
make build-backend) - Installs pnpm dependencies
- Installs Playwright Chromium
- Runs
make test-e2e - Uploads
playwright-report/andtest-results/as artifacts on failure
Troubleshooting
Tests fail with "Backend did not become healthy" or "Service did not become healthy"
- Ensure
make build-backend build-webcompleted successfully - Check that
apps/backend/bin/kandev,apps/backend/bin/mock-agent, andapps/web/dist/exist - Run with
E2E_DEBUG=1to see backend and frontend stderr
Tests fail with "Cannot find module" or import errors
- Run
cd apps && pnpm installto ensure dependencies are up to date
Port conflicts
- Tests use ports 18080+ for backends and 13000+ for frontends (one per worker)
- Ensure nothing else is listening on those ports
Flaky timeouts
- Backend and frontend health checks have a 30s timeout; if your machine is slow, this may need increasing in
fixtures/backend.ts - Per-test timeout is 60s; adjust in
playwright.config.tsif needed