--- 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`