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