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 dist and 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:

  1. Creates a temp directory as HOME with its own .gitconfig, data dir, and SQLite DB
  2. Sets environment variables for isolation:
    • KANDEV_HOME_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 __backend and waits for /ready to return 200 (not /health, which flips green as soon as the listener is bound, before the mock-harness routes are mounted)

Frontend:

  1. Sets KANDEV_WEB_DIST_DIR to apps/web/dist
  2. Uses the worker backend URL as the browser URL
  3. The backend serves index.html, static assets, and injected boot payloads
  4. 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 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:

MethodEndpointPurpose
createWorkspace(name)POST /api/workspacesCreate workspace
createTask(workspaceId, title, desc)POST /api/tasksCreate task
listWorkflows(workspaceId)GET /api/workspaces/:id/workflowsList workflows
listWorkflowSteps(workflowId)GET /api/workflows/:id/workflow/stepsList steps
createWorkflowStep(name, pos)POST /api/workflow/stepsCreate step
moveTask(taskId, stepId)POST /api/tasks/:id/moveMove task to step

GitHub mock control:

MethodEndpointPurpose
mockGitHubReset()DELETE /api/v1/github/mock/resetClear all mock data
mockGitHubSetUser(username)PUT /api/v1/github/mock/userSet authenticated user
mockGitHubAddPRs(prs)POST /api/v1/github/mock/prsAdd mock PRs
mockGitHubAddOrgs(orgs)POST /api/v1/github/mock/orgsAdd mock organizations
mockGitHubAddRepos(org, repos)POST /api/v1/github/mock/reposAdd mock repositories
mockGitHubAddReviews(owner, repo, num, reviews)POST /api/v1/github/mock/reviewsAdd PR reviews
mockGitHubAddCheckRuns(owner, repo, ref, checks)POST /api/v1/github/mock/checksAdd CI check runs
mockGitHubGetStatus()GET /api/v1/github/statusVerify auth status

Page objects (pages/)

Page objects encapsulate selectors and navigation:

  • KanbanPagegoto(), board, createTaskButton, taskCard(id), taskCardByTitle(title)
  • TaskDetailPagegoto(taskId, sessionId), sessionChat, turnCompleteIndicator, waitForAgentResponse(text)

Selectors

Tests use data-testid attributes for stability across layout/styling changes:

data-testidComponent
kanban-boardKanban board container
task-card-{id}Individual task card
create-task-buttonCreate task button
create-task-dialogTask creation dialog
task-title-inputTitle input in dialog
task-description-inputDescription textarea in dialog
submit-start-agentStart agent button
submit-plan-modePlan mode option
session-chatSession chat panel
agent-turn-completeAgent 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

SettingValueNotes
fullyParalleltrueTests run in parallel across workers
workersauto (local), 2 (CI)Worker count
timeout60sPer-test timeout
retries0 (local), 2 (CI)Retry count
traceon-first-retryPlaywright trace recording
screenshotonly-on-failureAuto-screenshot
videoon-first-retryVideo recording
BrowserChromium onlySingle 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:

  1. Builds Go binaries (make build-backend)
  2. Installs pnpm dependencies
  3. Installs Playwright Chromium
  4. Runs make test-e2e
  5. Uploads playwright-report/ and test-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-web completed successfully
  • Check that apps/backend/bin/kandev, apps/backend/bin/mock-agent, and apps/web/dist/ exist
  • Run with E2E_DEBUG=1 to see backend and frontend stderr

Tests fail with "Cannot find module" or import errors

  • Run cd apps && pnpm install to 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.ts if needed