E2E Test Harness
Cucumber + Playwright end-to-end tests for the project. The intent is that
every locked workflow has a .feature file before it gets a line of
implementation code (see "Phase 7.5 — Acceptance scenarios" in the
discovery playbook).
Layout
e2e/
package.json — cucumber, @playwright/test, tsx
cucumber.mjs — runner config
tests/
features/*.feature — Gherkin scenarios (one feature per locked workflow)
steps/*.steps.ts — step definitions; common.steps.ts is generic
support/
world.ts — AppWorld (browser, page, login, gotoApp, expectText)
hooks.ts — spawns servers (optional), DB reset hook, browser lifecycle
types.ts — config (URLs, timeouts) read from env
reports/ — generated HTML reports
Running
The harness assumes the app is reachable. Either:
A. Start servers yourself (recommended during dev):
yarn dev # in repo root — starts frontend + backend
yarn workspace @project/e2e test
B. Let the harness manage the servers:
E2E_MANAGE_SERVERS=1 \
E2E_BACKEND_CMD="yarn dev:backend" \
E2E_FRONTEND_CMD="yarn dev" \
yarn workspace @project/e2e test
Configuration
All env vars optional; defaults match the kanban-board-template's ports.
| Var | Default | Purpose |
|---|---|---|
APP_URL |
http://localhost:7104 |
Vite frontend URL |
API_URL |
http://localhost:4003 |
Pikku backend URL |
E2E_TIMEOUT |
30000 |
Per-step Playwright timeout (ms) |
E2E_MANAGE_SERVERS |
unset | Set to 1 to have hooks spawn dev servers |
E2E_BACKEND_CMD |
yarn dev:backend |
Used when E2E_MANAGE_SERVERS=1 |
E2E_FRONTEND_CMD |
yarn dev |
Used when E2E_MANAGE_SERVERS=1 |
E2E_RESET_URL |
unset | POST URL that resets DB to seed between tests |
HEADED |
unset | Set to 1 to run with a visible browser |
DB reset between scenarios
Scenarios pollute each other. To get a deterministic DB state per scenario,
implement a __test_reset Pikku function gated behind a test-only flag, and
point E2E_RESET_URL at it. The harness will POST to it in the Before
hook. If the URL is unset or unreachable the harness logs a warning and
continues.
Example shape:
export const testReset = pikkuSessionlessFunc({
expose: true,
description: 'Reset DB to seed state. Test-only — gated by NODE_ENV.',
func: async ({ kysely, config }) => {
if (config.env !== 'test') throw new Error('forbidden')
await kysely.deleteFrom('booking').execute()
// ... drop + reseed all mutable tables
return { ok: true as const }
},
})
Writing a feature
Feature: <one locked workflow>
Scenario: <one happy path>
Given I am logged in as "demo@yoga-retreat.example" with password "demo1234"
When I visit "/bookings"
Then I see "Summer Yoga Retreat"
Generic steps live in tests/steps/common.steps.ts:
Given I visit "<path>"Given I am logged in as "<email>" with password "<password>"When I log in as "..." with password "..."When I log outWhen I fill "<selector>" with "<value>"When I click "<button label>"Then I see "<text>"Then I do not see "<text>"Then the URL contains "<fragment>"
Project-specific steps go in their own *.steps.ts files (e.g.
bookings.steps.ts). Keep common.steps.ts framework-agnostic so the next
project that copies this harness inherits a clean baseline.
Tags
Run a subset by tag:
yarn workspace @project/e2e test:tag '@auth'
yarn workspace @project/e2e test:tag 'not @slow'
Mark scenarios as @skip to exclude from the default run.
Reports
After a run, open tests/reports/cucumber-report.html.