Migrate project artifacts to spec-kit format

- Move cross-cutting docs (personas, design system, implementation phases,
  Ideen.md) to .specify/memory/
- Move cross-cutting research and plans to .specify/memory/research/ and
  .specify/memory/plans/
- Extract 5 setup tasks from spec/setup-tasks.md into individual
  specs/001-005/spec.md files with spec-kit template format
- Extract 20 user stories from spec/userstories.md into individual
  specs/006-026/spec.md files with spec-kit template format
- Relocate feature-specific research and plan docs into specs/[feature]/
- Add spec-kit constitution, templates, scripts, and slash commands
- Slim down CLAUDE.md to Claude-Code-specific config, delegate principles
  to .specify/memory/constitution.md
- Update ralph.sh with stream-json output and per-iteration logging
- Delete old spec/ and docs/agents/ directories
- Gitignore Ralph iteration JSONL logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 20:19:41 +01:00
parent 0b2b84dafc
commit 6aeb4b8bca
83 changed files with 6486 additions and 660 deletions

View File

@@ -0,0 +1,487 @@
---
date: 2026-03-04T01:40:21+01:00
git_commit: a55174b32333d0f46a55d94a50604344d1ba33f6
branch: master
topic: "Backpressure for Agentic Coding"
tags: [plan, backpressure, hooks, checkstyle, spotbugs, archunit, quality]
status: complete
---
# Backpressure for Agentic Coding — Implementation Plan
## Overview
Implement automated feedback mechanisms (backpressure) that force the AI agent to self-correct before a human reviews the output. The approach follows the 90/10 rule: 90% deterministic constraints (types, linting, architecture tests), 10% agentic review.
## Current State vs. Desired State
| Layer | Backend (now) | Backend (after) | Frontend (now) | Frontend (after) |
|-------|---------------|-----------------|----------------|------------------|
| Type System | Java 25 (strong) | *unchanged* | TS strict + `noUncheckedIndexedAccess` | *unchanged* |
| Static Analysis | **None** | Checkstyle (Google Style) + SpotBugs | ESLint + oxlint + Prettier | *unchanged* |
| Architecture Tests | **None** | ArchUnit (hexagonal enforcement) | N/A | N/A |
| Unit Tests | JUnit 5 | JUnit 5 + fail-fast (`skipAfterFailureCount: 1`) | Vitest | Vitest + fail-fast (`bail: 1`) |
| PostToolUse Hook | **None** | `./mvnw compile -q` (incl. Checkstyle) | **None** | `vue-tsc --noEmit` |
| Stop Hook | **None** | `./mvnw test` | **None** | `npm run test:unit -- --run` |
## What We're NOT Doing
- **Error Prone** — overlaps with SpotBugs, Java 25 compatibility uncertain, more invasive setup
- **Custom ESLint rules** — add later when recurring agent mistakes are observed
- **MCP LSP Server** — experimental, high setup cost, unclear benefit vs. hooks
- **Pre-commit git hooks** — orthogonal concern, not part of this plan
- **CI/CD pipeline** — out of scope, this is about local agent feedback
## Design Decisions
- **Hook matchers** are regex on **tool names** (not file paths). File-path filtering must happen inside the hook script via `tool_input.file_path` from stdin JSON.
- **Context-efficient output**: ✓ on success, full error on failure. Don't waste the agent's context window with passing output.
- **Fail-fast**: one failure at a time. Prevents context-switching between multiple bugs.
- **Stop hook** checks `git status --porcelain` to determine if code files changed. Skips test runs on conversational responses.
- **Checkstyle** bound to Maven `validate` phase — automatically triggered by `./mvnw compile`, which means the PostToolUse hook gets Checkstyle for free.
- **SpotBugs** bound to Maven `verify` phase — NOT hooked, run manually via `./mvnw verify`.
- **ArchUnit**: use `archunit-junit5` 1.4.1 only. Do NOT use `archunit-hexagonal` addon (dormant since Jul 2023, pulls Kotlin, pinned to ArchUnit 1.0.1, expects different package naming).
---
## Task List
Tasks are ordered by priority. Execute one task per iteration, in order. Each task includes the exact changes and a verification command.
### T-BP-01: Create backend compile-check hook script `[x]`
**File**: `.claude/hooks/backend-compile-check.sh` (new, create `.claude/hooks/` directory if needed)
```bash
#!/usr/bin/env bash
set -euo pipefail
# Read hook input from stdin (JSON with tool_input.file_path)
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
# Only run for Java files under backend/
case "$FILE_PATH" in
*/backend/src/*.java) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR/backend"
# Run compile (includes validate phase → Checkstyle if configured)
# Context-efficient: suppress output on success, show full output on failure
if OUTPUT=$(./mvnw compile -q 2>&1); then
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ Backend compile passed."}}'
else
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
fi
```
Make executable: `chmod +x .claude/hooks/backend-compile-check.sh`
**Verify**: `echo '{"tool_input":{"file_path":"backend/src/main/java/de/fete/FeteApplication.java"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` → should output JSON with "✓ Backend compile passed."
**Verify skip**: `echo '{"tool_input":{"file_path":"README.md"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` → should exit 0 silently.
---
### T-BP-02: Create frontend type-check hook script `[x]`
**File**: `.claude/hooks/frontend-type-check.sh` (new)
```bash
#!/usr/bin/env bash
set -euo pipefail
# Read hook input from stdin (JSON with tool_input.file_path)
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
# Only run for TS/Vue files under frontend/
case "$FILE_PATH" in
*/frontend/src/*.ts|*/frontend/src/*.vue) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR/frontend"
# Run type-check
# Context-efficient: suppress output on success, show full output on failure
if OUTPUT=$(npx vue-tsc --noEmit 2>&1); then
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ Frontend type-check passed."}}'
else
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
fi
```
Make executable: `chmod +x .claude/hooks/frontend-type-check.sh`
**Verify**: `echo '{"tool_input":{"file_path":"frontend/src/App.vue"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/frontend-type-check.sh` → should output JSON with "✓ Frontend type-check passed."
---
### T-BP-03: Create stop hook script (test gate) `[x]`
**File**: `.claude/hooks/run-tests.sh` (new)
Runs after the agent finishes its response. Checks `git status` for changed source files and runs the relevant test suites. Skips on conversational responses (no code changes).
```bash
#!/usr/bin/env bash
set -euo pipefail
cd "$CLAUDE_PROJECT_DIR"
# Check for uncommitted changes in backend/frontend source
HAS_BACKEND=$(git status --porcelain backend/src/ 2>/dev/null | head -1)
HAS_FRONTEND=$(git status --porcelain frontend/src/ 2>/dev/null | head -1)
# Nothing changed — skip
if [[ -z "$HAS_BACKEND" && -z "$HAS_FRONTEND" ]]; then
exit 0
fi
ERRORS=""
PASSED=""
# Run backend tests if Java sources changed
if [[ -n "$HAS_BACKEND" ]]; then
if OUTPUT=$(cd backend && ./mvnw test -q 2>&1); then
PASSED+="✓ Backend tests passed. "
else
ERRORS+="Backend tests failed:\n$OUTPUT\n\n"
fi
fi
# Run frontend tests if TS/Vue sources changed
if [[ -n "$HAS_FRONTEND" ]]; then
if OUTPUT=$(cd frontend && npm run test:unit -- --run 2>&1); then
PASSED+="✓ Frontend tests passed. "
else
ERRORS+="Frontend tests failed:\n$OUTPUT\n\n"
fi
fi
if [[ -n "$ERRORS" ]]; then
ESCAPED=$(printf '%s' "$ERRORS" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"additionalContext\":$ESCAPED}}"
else
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"additionalContext\":\"$PASSED\"}}"
fi
```
Make executable: `chmod +x .claude/hooks/run-tests.sh`
**Verify**: `CLAUDE_PROJECT_DIR=. .claude/hooks/run-tests.sh` → if no uncommitted changes in source dirs, should exit 0 silently.
---
### T-BP-04: Create `.claude/settings.json` with hook configuration `[x]`
**File**: `.claude/settings.json` (new — do NOT modify `.claude/settings.local.json`, that has permissions)
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/backend-compile-check.sh\"",
"timeout": 120
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/frontend-type-check.sh\"",
"timeout": 60
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh\"",
"timeout": 300
}
]
}
]
}
}
```
**Verify**: File exists and is valid JSON: `python3 -c "import json; json.load(open('.claude/settings.json'))"`
---
### T-BP-05: Configure Vitest fail-fast `[x]`
**File**: `frontend/vitest.config.ts` (modify)
Add `bail: 1` to the test configuration object. The result should look like:
```typescript
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
bail: 1,
},
}),
)
```
**Verify**: `cd frontend && npm run test:unit -- --run` passes.
---
### T-BP-06: Configure Maven Surefire fail-fast `[x]`
**File**: `backend/pom.xml` (modify)
Add `maven-surefire-plugin` configuration within `<build><plugins>`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Fail-fast: stop on first test failure -->
<skipAfterFailureCount>1</skipAfterFailureCount>
</configuration>
</plugin>
```
**Verify**: `cd backend && ./mvnw test` passes.
---
### T-BP-07: Add Checkstyle plugin + fix violations `[x]`
**File**: `backend/pom.xml` (modify)
Add `maven-checkstyle-plugin` within `<build><plugins>`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>13.3.0</version>
</dependency>
</dependencies>
<configuration>
<configLocation>google_checks.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
</configuration>
<executions>
<execution>
<id>checkstyle-validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
```
Then run `cd backend && ./mvnw checkstyle:check` to find violations. Fix all violations in existing source files (`FeteApplication.java`, `HealthController.java`, `FeteApplicationTest.java`, all `package-info.java` files). Google Style requires: 2-space indentation, specific import order, Javadoc on public types, max line length 100.
**Verify**: `cd backend && ./mvnw checkstyle:check` passes with zero violations AND `cd backend && ./mvnw compile` passes (Checkstyle now runs during validate phase).
---
### T-BP-08: Add SpotBugs plugin + verify `[x]`
**File**: `backend/pom.xml` (modify)
Add `spotbugs-maven-plugin` within `<build><plugins>`:
```xml
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.9.8.2</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<xmlOutput>true</xmlOutput>
<failOnError>true</failOnError>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
```
Run `cd backend && ./mvnw verify` — if SpotBugs finds issues, fix them.
**Verify**: `cd backend && ./mvnw verify` passes (includes compile + test + SpotBugs).
---
### T-BP-09: Add ArchUnit dependency + write architecture tests `[x]`
**File 1**: `backend/pom.xml` (modify) — add within `<dependencies>`:
```xml
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.1</version>
<scope>test</scope>
</dependency>
```
Do NOT use the `archunit-hexagonal` addon.
**File 2**: `backend/src/test/java/de/fete/HexagonalArchitectureTest.java` (new)
```java
package de.fete;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
@AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class)
class HexagonalArchitectureTest {
@ArchTest
static final ArchRule onion_architecture_is_respected = onionArchitecture()
.domainModels("de.fete.domain.model..")
.domainServices("de.fete.domain.port.in..", "de.fete.domain.port.out..")
.applicationServices("de.fete.application.service..")
.adapter("web", "de.fete.adapter.in.web..")
.adapter("persistence", "de.fete.adapter.out.persistence..")
.adapter("config", "de.fete.config..");
@ArchTest
static final ArchRule domain_must_not_depend_on_adapters = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter..");
@ArchTest
static final ArchRule domain_must_not_depend_on_application = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.application..");
@ArchTest
static final ArchRule domain_must_not_depend_on_config = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.config..");
@ArchTest
static final ArchRule inbound_ports_must_be_interfaces = classes()
.that().resideInAPackage("de.fete.domain.port.in..")
.should().beInterfaces();
@ArchTest
static final ArchRule outbound_ports_must_be_interfaces = classes()
.that().resideInAPackage("de.fete.domain.port.out..")
.should().beInterfaces();
@ArchTest
static final ArchRule domain_must_not_use_spring = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("org.springframework..");
@ArchTest
static final ArchRule web_must_not_depend_on_persistence = noClasses()
.that().resideInAPackage("de.fete.adapter.in.web..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.out.persistence..");
@ArchTest
static final ArchRule persistence_must_not_depend_on_web = noClasses()
.that().resideInAPackage("de.fete.adapter.out.persistence..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.in.web..");
}
```
**Verify**: `cd backend && ./mvnw test` passes and output shows `HexagonalArchitectureTest` with 9 tests.
---
### T-BP-10: Update CLAUDE.md `[x]`
**File**: `CLAUDE.md` (modify)
Add two rows to the Build Commands table:
| What | Command |
|------|---------|
| Backend checkstyle | `cd backend && ./mvnw checkstyle:check` |
| Backend full verify | `cd backend && ./mvnw verify` |
---
### T-BP-11: Final verification `[x]`
Run all verification commands to confirm the complete backpressure stack works:
1. `test -x .claude/hooks/backend-compile-check.sh`
2. `test -x .claude/hooks/frontend-type-check.sh`
3. `test -x .claude/hooks/run-tests.sh`
4. `python3 -c "import json; json.load(open('.claude/settings.json'))"`
5. `cd backend && ./mvnw verify` (triggers: Checkstyle → compile → test w/ ArchUnit → SpotBugs)
6. `cd frontend && npm run test:unit -- --run`
7. `echo '{"tool_input":{"file_path":"backend/src/main/java/de/fete/FeteApplication.java"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh`
8. `echo '{"tool_input":{"file_path":"frontend/src/App.vue"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/frontend-type-check.sh`
9. `echo '{"tool_input":{"file_path":"README.md"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` (should be silent)
All commands must exit 0. If any fail, go back and fix the issue before marking complete.
---
## Addendum: Implementation Deviations (2026-03-04)
Changes made during implementation that deviate from or extend the original plan:
1. **Stop hook JSON schema**: The plan used `hookSpecificOutput` with `hookEventName: "Stop"` for the stop hook output. This is invalid — `hookSpecificOutput` is only supported for `PreToolUse`, `PostToolUse`, and `UserPromptSubmit` events. Fixed to use top-level `"decision": "approve"/"block"` with `"reason"` field.
2. **Stop hook loop prevention**: Added `stop_hook_active` check from stdin JSON to prevent infinite re-engagement loops. Not in original plan.
3. **Context-efficient test output**: Added `logback-test.xml` (root level WARN), `redirectTestOutputToFile=true`, and `trimStackTrace=true` to Surefire config. The stop hook script filters output to `[ERROR]` lines only, stripping Maven boilerplate. Not in original plan — added after observing that raw test failure output consumed excessive context.
4. **Hook path matching**: Case patterns in hook scripts extended to match both absolute and relative file paths (`*/backend/src/*.java|backend/src/*.java`). Original plan only had `*/backend/src/*.java` which doesn't match relative paths.
5. **Checkstyle `includeTestSourceDirectory`**: Set to `true` so test sources also follow Google Style. Not in original plan. `FeteApplicationTest.java` was reformatted to 2-space indentation with correct import order (static imports first).
6. **ArchUnit field naming**: Changed from `snake_case` (`onion_architecture_is_respected`) to `camelCase` (`onionArchitectureIsRespected`) to comply with Google Checkstyle rules.
---
## References
- Research document: `docs/agents/research/2026-03-04-backpressure-agentic-coding.md`
- Geoffrey Huntley: [Don't waste your back pressure](https://ghuntley.com/pressure/)
- JW: [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure)
- HumanLayer: [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure)
- Claude Code: [Hooks Reference](https://code.claude.com/docs/en/hooks)
- ArchUnit: [User Guide](https://www.archunit.org/userguide/html/000_Index.html)

View File

@@ -0,0 +1,504 @@
---
date: 2026-03-05T10:29:08+00:00
git_commit: ffea279b54ad84be09bd0e82b3ed9c89a95fc606
branch: master
topic: "E2E Testing with Playwright — Setup & Initial Tests"
tags: [plan, e2e, playwright, testing, frontend, msw]
status: draft
---
# E2E Testing with Playwright — Setup & Initial Tests
## Overview
Set up Playwright E2E testing infrastructure for the fete Vue 3 frontend with mocked backend (via `@msw/playwright` + `@msw/source`), write initial smoke and US-1 event-creation tests, and integrate into CI.
## Current State Analysis
- **Vitest** is configured and already excludes `e2e/**` (`vitest.config.ts:10`)
- **Three routes** exist: `/` (Home), `/create` (EventCreate), `/events/:token` (EventStub)
- **No E2E framework** installed — no Playwright, no MSW
- **OpenAPI spec** at `backend/src/main/resources/openapi/api.yaml` defines `POST /events` with `CreateEventRequest` and `CreateEventResponse` schemas
- **`CreateEventResponse` lacks `example:` fields** — required for `@msw/source` mock generation
- **CI pipeline** (`.gitea/workflows/ci.yaml`) has backend-test and frontend-test jobs but no E2E step
- **`.gitignore`** does not include Playwright output directories
### Key Discoveries:
- `frontend/vitest.config.ts:10``e2e/**` already excluded from Vitest
- `frontend/vite.config.ts:18-25` — Dev proxy forwards `/api``localhost:8080`
- `frontend/package.json:8``dev` script runs `generate:api` first, then Vite
- `frontend/src/views/EventCreateView.vue` — Full form with client-side validation, API call via `openapi-fetch`, redirect to event stub on success
- `frontend/src/views/EventStubView.vue` — Shows "Event created!" confirmation with shareable link
- `frontend/src/views/HomeView.vue` — Empty state with "Create Event" CTA
## Desired End State
After this plan is complete:
- Playwright is installed and configured with Chromium-only
- `@msw/playwright` + `@msw/source` provide automatic API mocking from the OpenAPI spec
- A smoke test verifies the app loads and basic navigation works
- A US-1 E2E test covers the full event creation flow (form fill → mocked API → redirect → stub page)
- `npm run test:e2e` runs all E2E tests locally
- CI runs E2E tests after unit tests, uploading the report as artifact on failure
- OpenAPI response schemas include `example:` fields for mock generation
### Verification:
```bash
cd frontend && npm run test:e2e # all E2E tests pass locally
```
## What We're NOT Doing
- Firefox/WebKit browser testing — Chromium only for now
- Page Object Model pattern — premature with <5 tests
- CI caching of Playwright browser binaries — separate concern
- Full-stack E2E tests with real Spring Boot backend
- E2E tests for US-2 through US-20 — only US-1 flow + smoke test
- Service worker / PWA testing
- `data-testid` attributes — using accessible locators (`getByRole`, `getByLabel`) where possible
## Implementation Approach
Install Playwright and MSW packages, configure Playwright to spawn the Vite dev server, set up MSW to auto-generate handlers from the OpenAPI spec, then write two test files: a smoke test and a US-1 event creation flow test. Finally, add an E2E step to CI.
---
## Phase 1: Playwright Infrastructure
### Overview
Install dependencies, create configuration, add npm scripts, update `.gitignore`.
### Changes Required:
#### [x] 1. Install npm packages
**Command**: `cd frontend && npm install --save-dev @playwright/test @msw/playwright @msw/source msw`
Four packages:
- `@playwright/test` — Playwright test runner
- `msw` — Mock Service Worker core
- `@msw/playwright` — Playwright integration for MSW (intercepts at network level via `page.route()`)
- `@msw/source` — Reads OpenAPI spec and generates MSW request handlers
#### [x] 2. Install Chromium browser binary
**Command**: `cd frontend && npx playwright install --with-deps chromium`
Only Chromium — saves ~2 min vs installing all browsers. `--with-deps` installs OS-level libraries.
#### [x] 3. Create `playwright.config.ts`
**File**: `frontend/playwright.config.ts`
```typescript
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
stdout: 'pipe',
},
})
```
Key decisions per research doc:
- `testDir: './e2e'` — separate from Vitest unit tests
- `forbidOnly: !!process.env.CI` — prevents `.only` in CI
- `workers: 1` in CI — avoids shared-state flakiness
- `reuseExistingServer` locally — fast iteration when `npm run dev` is already running
#### [x] 4. Add npm scripts to `package.json`
**File**: `frontend/package.json`
Add to `"scripts"`:
```json
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
```
#### [x] 5. Update `.gitignore`
**File**: `frontend/.gitignore`
Append:
```
# Playwright
playwright-report/
test-results/
```
#### [x] 6. Create `e2e/` directory
**Command**: `mkdir -p frontend/e2e`
### Success Criteria:
#### Automated Verification:
- [ ] `cd frontend && npx playwright --version` outputs a version
- [ ] `cd frontend && npx playwright test --list` runs without error (shows 0 tests initially)
- [ ] `npm run test:e2e` script exists in package.json
#### Manual Verification:
- [ ] `playwright-report/` and `test-results/` are in `.gitignore`
- [ ] No unintended changes to existing config files
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 2: OpenAPI Response Examples
### Overview
Add `example:` fields to `CreateEventResponse` properties so `@msw/source` can generate realistic mock responses.
### Changes Required:
#### [x] 1. Add examples to `CreateEventResponse`
**File**: `backend/src/main/resources/openapi/api.yaml`
Update `CreateEventResponse` properties to include `example:` fields:
```yaml
CreateEventResponse:
type: object
required:
- eventToken
- organizerToken
- title
- dateTime
- expiryDate
properties:
eventToken:
type: string
format: uuid
description: Public token for the event URL
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
organizerToken:
type: string
format: uuid
description: Secret token for organizer access
example: "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
title:
type: string
example: "Summer BBQ"
dateTime:
type: string
format: date-time
example: "2026-03-15T20:00:00+01:00"
expiryDate:
type: string
format: date
example: "2026-06-15"
```
### Success Criteria:
#### Automated Verification:
- [ ] `cd backend && ./mvnw compile` succeeds (OpenAPI codegen still works)
- [ ] `cd frontend && npm run generate:api` succeeds (TypeScript types regenerate)
#### Manual Verification:
- [ ] All response schema properties have `example:` fields
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 3: MSW Integration
### Overview
Set up `@msw/source` to read the OpenAPI spec and generate MSW handlers, and configure `@msw/playwright` to intercept network requests in E2E tests.
### Changes Required:
#### [x] 1. Create MSW setup helper
**File**: `frontend/e2e/msw-setup.ts`
```typescript
import { fromOpenApi } from '@msw/source'
import { createWorkerFixture } from '@msw/playwright'
import { test as base, expect } from '@playwright/test'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const specPath = path.resolve(__dirname, '../../backend/src/main/resources/openapi/api.yaml')
// Generate MSW handlers from the OpenAPI spec.
// These return example values defined in the spec by default.
const handlers = await fromOpenApi(specPath)
// Create a Playwright fixture that intercepts network requests via page.route()
// and delegates them to MSW handlers.
export const test = base.extend(createWorkerFixture(handlers))
export { expect }
```
This module:
- Reads the OpenAPI spec at test startup
- Generates MSW request handlers that return `example:` values by default
- Exports a `test` fixture with MSW network interception built in
- Tests import `{ test, expect }` from this file instead of `@playwright/test`
#### [x] 2. Verify MSW integration works
Write a minimal test in Phase 4 that uses the fixture — if the import chain works and a test passes, MSW is correctly configured.
### Success Criteria:
#### Automated Verification:
- [ ] `frontend/e2e/msw-setup.ts` type-checks (no TS errors)
- [ ] Import path to OpenAPI spec resolves correctly
#### Manual Verification:
- [ ] MSW helper is clean and minimal
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 4: E2E Tests
### Overview
Write two test files: a smoke test for basic app functionality and a US-1 event creation flow test.
### Changes Required:
#### [x] 1. Smoke test
**File**: `frontend/e2e/smoke.spec.ts`
```typescript
import { test, expect } from './msw-setup'
test.describe('Smoke', () => {
test('home page loads and shows branding', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('heading', { name: 'fete' })).toBeVisible()
})
test('home page has create event CTA', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('link', { name: /create event/i })).toBeVisible()
})
test('navigating to /create shows the creation form', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: /create event/i }).click()
await expect(page).toHaveURL('/create')
await expect(page.getByLabel(/title/i)).toBeVisible()
})
})
```
#### [x] 2. US-1 event creation flow test
**File**: `frontend/e2e/event-create.spec.ts`
```typescript
import { test, expect } from './msw-setup'
test.describe('US-1: Create an event', () => {
test('shows validation errors for empty required fields', async ({ page }) => {
await page.goto('/create')
await page.getByRole('button', { name: /create event/i }).click()
await expect(page.getByText('Title is required.')).toBeVisible()
await expect(page.getByText('Date and time are required.')).toBeVisible()
await expect(page.getByText('Expiry date is required.')).toBeVisible()
})
test('creates an event and redirects to stub page', async ({ page }) => {
await page.goto('/create')
// Fill the form
await page.getByLabel(/title/i).fill('Summer BBQ')
await page.getByLabel(/description/i).fill('Bring your own drinks')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/location/i).fill('Central Park')
await page.getByLabel(/expiry/i).fill('2026-06-15')
// Submit — MSW returns the OpenAPI example response
await page.getByRole('button', { name: /create event/i }).click()
// Should redirect to the event stub page
await expect(page).toHaveURL(/\/events\/.+/)
await expect(page.getByText('Event created!')).toBeVisible()
})
test('stores event data in localStorage after creation', async ({ page }) => {
await page.goto('/create')
await page.getByLabel(/title/i).fill('Summer BBQ')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/expiry/i).fill('2026-06-15')
await page.getByRole('button', { name: /create event/i }).click()
await expect(page).toHaveURL(/\/events\/.+/)
// Verify localStorage was populated
const storage = await page.evaluate(() => {
const raw = localStorage.getItem('fete_events')
return raw ? JSON.parse(raw) : null
})
expect(storage).not.toBeNull()
expect(storage).toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Summer BBQ' }),
]),
)
})
test('shows server error on API failure', async ({ page, network }) => {
// Override the default MSW handler to return a 400 error
await network.use(
// Exact override syntax depends on @msw/playwright API —
// may need adjustment based on actual package API
)
await page.goto('/create')
await page.getByLabel(/title/i).fill('Test')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/expiry/i).fill('2026-06-15')
await page.getByRole('button', { name: /create event/i }).click()
// Should show error message, not redirect
await expect(page.getByRole('alert')).toBeVisible()
})
})
```
**Note on the server error test:** The exact override syntax for `network.use()` depends on the `@msw/playwright` API. During implementation, this will need to be adapted to the actual package API. The pattern is: override the `POST /api/events` handler to return a 400/500 response.
### Success Criteria:
#### Automated Verification:
- [ ] `cd frontend && npm run test:e2e` passes — all tests green
- [ ] No TypeScript errors in test files
#### Manual Verification:
- [ ] Tests cover: home page rendering, navigation, form validation, successful creation flow, localStorage persistence
- [ ] Test names are descriptive and map to acceptance criteria
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 5: CI Integration
### Overview
Add a Playwright E2E test step to the Gitea Actions CI pipeline.
### Changes Required:
#### [x] 1. Add E2E job to CI workflow
**File**: `.gitea/workflows/ci.yaml`
Add a new job `frontend-e2e` after the existing `frontend-test` job:
```yaml
frontend-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: cd frontend && npm ci
- name: Install Playwright browsers
run: cd frontend && npx playwright install --with-deps chromium
- name: Run E2E tests
run: cd frontend && npm run test:e2e
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
```
#### [x] 2. Add E2E to the `needs` array of `build-and-publish`
**File**: `.gitea/workflows/ci.yaml`
Update the `build-and-publish` job:
```yaml
build-and-publish:
needs: [backend-test, frontend-test, frontend-e2e]
```
This ensures Docker images are only published if E2E tests also pass.
### Success Criteria:
#### Automated Verification:
- [ ] CI YAML is valid (no syntax errors)
- [ ] `frontend-e2e` job uses `chromium` only (no full browser install)
#### Manual Verification:
- [ ] E2E job runs independently from backend-test (no unnecessary dependency)
- [ ] `build-and-publish` requires all three test jobs
- [ ] Report artifact is uploaded even on test failure (`!cancelled()`)
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Testing Strategy
### E2E Tests (this plan):
- Smoke test: app loads, branding visible, navigation works
- US-1 happy path: fill form → submit → redirect → stub page
- US-1 validation: empty required fields show errors
- US-1 localStorage: event data persisted after creation
- US-1 error handling: API failure shows error message
### Existing Unit/Component Tests (unchanged):
- `useEventStorage.spec.ts` — 6 tests
- `EventCreateView.spec.ts` — 11 tests
- `EventStubView.spec.ts` — 8 tests
### Future:
- Each new user story adds its own E2E tests
- Page Object Model when test suite grows beyond 5-10 tests
- Cross-browser testing (Firefox/WebKit) as needed
## Performance Considerations
- Chromium-only keeps install time and test runtime low
- `reuseExistingServer` in local dev avoids restarting Vite per test run
- Single worker in CI prevents flakiness from parallel state issues
- MSW intercepts at network level — no real backend needed, fast test execution
## References
- Research: `docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md`
- OpenAPI spec: `backend/src/main/resources/openapi/api.yaml`
- Existing views: `frontend/src/views/EventCreateView.vue`, `EventStubView.vue`, `HomeView.vue`
- CI pipeline: `.gitea/workflows/ci.yaml`
- Vitest config: `frontend/vitest.config.ts`