Files
fete/docs/agents/plan/2026-03-05-e2e-testing-playwright-setup.md
nitrix 9cf199dd9f Add Playwright E2E tests with MSW API mocking
- Playwright + @msw/playwright + @msw/source for OpenAPI-driven mocks
- Chromium-only configuration with Vite dev server integration
- Smoke tests: home page, CTA, navigation
- US-1 tests: validation, event creation flow, localStorage, error handling
- Suppress Node 25 --localstorage-file warning from MSW cookieStore
2026-03-06 18:16:07 +01:00

17 KiB

date, git_commit, branch, topic, tags, status
date git_commit branch topic tags status
2026-03-05T10:29:08+00:00 ffea279b54 master E2E Testing with Playwright — Setup & Initial Tests
plan
e2e
playwright
testing
frontend
msw
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:10e2e/** already excluded from Vitest
  • frontend/vite.config.ts:18-25 — Dev proxy forwards /apilocalhost:8080
  • frontend/package.json:8dev 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:

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

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":

"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:

    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

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

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

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:

  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:

  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