- 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
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 |
|
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.yamldefinesPOST /eventswithCreateEventRequestandCreateEventResponseschemas CreateEventResponselacksexample:fields — required for@msw/sourcemock generation- CI pipeline (
.gitea/workflows/ci.yaml) has backend-test and frontend-test jobs but no E2E step .gitignoredoes not include Playwright output directories
Key Discoveries:
frontend/vitest.config.ts:10—e2e/**already excluded from Vitestfrontend/vite.config.ts:18-25— Dev proxy forwards/api→localhost:8080frontend/package.json:8—devscript runsgenerate:apifirst, then Vitefrontend/src/views/EventCreateView.vue— Full form with client-side validation, API call viaopenapi-fetch, redirect to event stub on successfrontend/src/views/EventStubView.vue— Shows "Event created!" confirmation with shareable linkfrontend/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/sourceprovide 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:e2eruns 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-testidattributes — 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 runnermsw— Mock Service Worker core@msw/playwright— Playwright integration for MSW (intercepts at network level viapage.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 testsforbidOnly: !!process.env.CI— prevents.onlyin CIworkers: 1in CI — avoids shared-state flakinessreuseExistingServerlocally — fast iteration whennpm run devis 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 --versionoutputs a versioncd frontend && npx playwright test --listruns without error (shows 0 tests initially)npm run test:e2escript exists in package.json
Manual Verification:
playwright-report/andtest-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 compilesucceeds (OpenAPI codegen still works)cd frontend && npm run generate:apisucceeds (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
testfixture 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.tstype-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:e2epasses — 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-e2ejob useschromiumonly (no full browser install)
Manual Verification:
- E2E job runs independently from backend-test (no unnecessary dependency)
build-and-publishrequires 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 testsEventCreateView.spec.ts— 11 testsEventStubView.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
reuseExistingServerin 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