Compare commits
1 Commits
8b1b87e864
...
4368b778c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4368b778c9 |
@@ -14,7 +14,7 @@ fi
|
|||||||
|
|
||||||
# Check for uncommitted changes in backend/frontend source
|
# Check for uncommitted changes in backend/frontend source
|
||||||
HAS_BACKEND=$(git status --porcelain backend/src/ 2>/dev/null | head -1)
|
HAS_BACKEND=$(git status --porcelain backend/src/ 2>/dev/null | head -1)
|
||||||
HAS_FRONTEND=$(git status --porcelain frontend/src/ frontend/e2e/ 2>/dev/null | head -1)
|
HAS_FRONTEND=$(git status --porcelain frontend/src/ 2>/dev/null | head -1)
|
||||||
|
|
||||||
# Nothing changed -- skip
|
# Nothing changed -- skip
|
||||||
if [[ -z "$HAS_BACKEND" && -z "$HAS_FRONTEND" ]]; then
|
if [[ -z "$HAS_BACKEND" && -z "$HAS_FRONTEND" ]]; then
|
||||||
@@ -38,15 +38,9 @@ fi
|
|||||||
# Run frontend tests if TS/Vue sources changed
|
# Run frontend tests if TS/Vue sources changed
|
||||||
if [[ -n "$HAS_FRONTEND" ]]; then
|
if [[ -n "$HAS_FRONTEND" ]]; then
|
||||||
if OUTPUT=$(cd frontend && npm run test:unit -- --run 2>&1); then
|
if OUTPUT=$(cd frontend && npm run test:unit -- --run 2>&1); then
|
||||||
PASSED+="✓ Frontend unit tests passed. "
|
PASSED+="✓ Frontend tests passed. "
|
||||||
else
|
else
|
||||||
ERRORS+="Frontend unit tests failed:\n$OUTPUT\n\n"
|
ERRORS+="Frontend tests failed:\n$OUTPUT\n\n"
|
||||||
fi
|
|
||||||
|
|
||||||
if OUTPUT=$(cd frontend && npm run test:e2e 2>&1); then
|
|
||||||
PASSED+="✓ Frontend E2E tests passed. "
|
|
||||||
else
|
|
||||||
ERRORS+="Frontend E2E tests failed:\n$OUTPUT\n\n"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -46,35 +46,8 @@ jobs:
|
|||||||
- name: Production build
|
- name: Production build
|
||||||
run: cd frontend && npm run build
|
run: cd frontend && npm run build
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
build-and-publish:
|
build-and-publish:
|
||||||
needs: [backend-test, frontend-test, frontend-e2e]
|
needs: [backend-test, frontend-test]
|
||||||
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '.')
|
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '.')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,7 +9,6 @@ Thumbs.db
|
|||||||
|
|
||||||
# Claude Code (machine-local)
|
# Claude Code (machine-local)
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
.mcp.json
|
|
||||||
.rodney/
|
.rodney/
|
||||||
.agent-tests/
|
.agent-tests/
|
||||||
|
|
||||||
|
|||||||
@@ -65,11 +65,8 @@ fete/
|
|||||||
# Backend
|
# Backend
|
||||||
cd backend && ./mvnw test
|
cd backend && ./mvnw test
|
||||||
|
|
||||||
# Frontend unit tests
|
# Frontend
|
||||||
cd frontend && npm run test:unit
|
cd frontend && npm run test:unit
|
||||||
|
|
||||||
# Frontend E2E tests (requires Chromium: npx playwright install chromium)
|
|
||||||
cd frontend && npm run test:e2e
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the backend locally
|
### Running the backend locally
|
||||||
@@ -141,12 +138,11 @@ ArchUnit enforces hexagonal boundaries: domain must not depend on adapters, appl
|
|||||||
| TypeScript (strict) | `vue-tsc --noEmit` | Type errors |
|
| TypeScript (strict) | `vue-tsc --noEmit` | Type errors |
|
||||||
| oxlint + ESLint | `oxlint`, `eslint` | Lint violations |
|
| oxlint + ESLint | `oxlint`, `eslint` | Lint violations |
|
||||||
|
|
||||||
**When the agent finishes** (Stop hook — only if `frontend/src/` or `frontend/e2e/` has uncommitted changes):
|
**When the agent finishes** (Stop hook — only if `frontend/src/` has uncommitted changes):
|
||||||
|
|
||||||
| What | Command | Fails on |
|
| What | Command | Fails on |
|
||||||
|---------------------|------------------------------|---------------------------------------|
|
|---------------------|------------------------------|---------------------------------------|
|
||||||
| Vitest | `npm run test:unit -- --run` | Test failures (fail-fast, stops at 1) |
|
| Vitest | `npm run test:unit -- --run` | Test failures (fail-fast, stops at 1) |
|
||||||
| Playwright | `npm run test:e2e` | E2E test failures |
|
|
||||||
|
|
||||||
**Not hooked** (run manually or via editor):
|
**Not hooked** (run manually or via editor):
|
||||||
|
|
||||||
|
|||||||
@@ -80,23 +80,18 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Public token for the event URL
|
description: Public token for the event URL
|
||||||
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
||||||
organizerToken:
|
organizerToken:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: Secret token for organizer access
|
description: Secret token for organizer access
|
||||||
example: "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
|
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
example: "Summer BBQ"
|
|
||||||
dateTime:
|
dateTime:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
example: "2026-03-15T20:00:00+01:00"
|
|
||||||
expiryDate:
|
expiryDate:
|
||||||
type: string
|
type: string
|
||||||
format: date
|
format: date
|
||||||
example: "2026-06-15"
|
|
||||||
|
|
||||||
ProblemDetail:
|
ProblemDetail:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@@ -1,504 +0,0 @@
|
|||||||
---
|
|
||||||
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`
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
---
|
|
||||||
date: 2026-03-05T10:14:52+00:00
|
|
||||||
git_commit: ffea279b54ad84be09bd0e82b3ed9c89a95fc606
|
|
||||||
branch: master
|
|
||||||
topic: "End-to-End Testing for Vue 3 with Playwright"
|
|
||||||
tags: [research, e2e, playwright, testing, frontend]
|
|
||||||
status: complete
|
|
||||||
---
|
|
||||||
|
|
||||||
# Research: End-to-End Testing for Vue 3 with Playwright
|
|
||||||
|
|
||||||
## Research Question
|
|
||||||
|
|
||||||
How to set up and structure end-to-end tests for the fete Vue 3 + Vite frontend using Playwright?
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Playwright is Vue 3's officially recommended E2E testing framework. It integrates with Vite projects through a `webServer` config block (no Vite plugin needed), supports Chromium/Firefox/WebKit under a single API, and is fully free including parallelism. The fete project's existing vitest.config.ts already excludes `e2e/**`, making the integration path clean.
|
|
||||||
|
|
||||||
## Detailed Findings
|
|
||||||
|
|
||||||
### 1. Current Frontend Test Infrastructure
|
|
||||||
|
|
||||||
The project uses **Vitest 4.0.18** with jsdom for unit/component tests:
|
|
||||||
|
|
||||||
- **Config:** `frontend/vitest.config.ts` — merges with vite.config, uses jsdom environment, bail on first failure
|
|
||||||
- **Exclusion:** Already excludes `e2e/**` from Vitest's test discovery (`vitest.config.ts:10`)
|
|
||||||
- **Existing tests:** 3 test files with ~25 tests total:
|
|
||||||
- `src/composables/__tests__/useEventStorage.spec.ts` (6 tests)
|
|
||||||
- `src/views/__tests__/EventCreateView.spec.ts` (11 tests)
|
|
||||||
- `src/views/__tests__/EventStubView.spec.ts` (8 tests)
|
|
||||||
- **No E2E framework** is currently configured
|
|
||||||
|
|
||||||
### 2. Why Playwright
|
|
||||||
|
|
||||||
Vue's official testing guide ([vuejs.org/guide/scaling-up/testing](https://vuejs.org/guide/scaling-up/testing)) positions Playwright as the primary E2E recommendation. Key advantages over Cypress:
|
|
||||||
|
|
||||||
| Dimension | Playwright | Cypress |
|
|
||||||
|---|---|---|
|
|
||||||
| Browser support | Chromium, Firefox, WebKit | Chrome-family, Firefox (WebKit experimental) |
|
|
||||||
| Parallelism | Free, native | Requires paid Cypress Cloud |
|
|
||||||
| Architecture | Out-of-process (CDP/BiDi) | In-browser (same process) |
|
|
||||||
| Speed | 35-45% faster in parallel | Slower at scale |
|
|
||||||
| Pricing | 100% free, Apache 2.0 | Cloud features cost money |
|
|
||||||
| Privacy | No account, no cloud dependency | Cloud service integration |
|
|
||||||
|
|
||||||
Playwright aligns with fete's privacy constraints (no cloud dependency, no account required).
|
|
||||||
|
|
||||||
### 3. Playwright + Vite Integration
|
|
||||||
|
|
||||||
Playwright does **not** use a Vite plugin. Integration is purely through process management:
|
|
||||||
|
|
||||||
1. Playwright reads `webServer.command` and spawns the Vite dev server
|
|
||||||
2. Polls `webServer.url` until ready
|
|
||||||
3. Runs tests against `use.baseURL`
|
|
||||||
4. Kills the server after all tests finish
|
|
||||||
|
|
||||||
The existing Vite dev proxy (`/api` → `localhost:8080`) works transparently — E2E tests can hit the real backend or intercept via `page.route()` mocks.
|
|
||||||
|
|
||||||
Note: `@playwright/experimental-ct-vue` exists for component-level testing (mounting individual Vue components without a server), but is still experimental and is a different category from E2E.
|
|
||||||
|
|
||||||
### 4. Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install --save-dev @playwright/test
|
|
||||||
npx playwright install --with-deps chromium
|
|
||||||
```
|
|
||||||
|
|
||||||
Using `npm init playwright@latest` generates scaffolding automatically, but for an existing project manual setup is cleaner.
|
|
||||||
|
|
||||||
### 5. Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
frontend/
|
|
||||||
playwright.config.ts # Playwright config
|
|
||||||
e2e/ # E2E test directory
|
|
||||||
home.spec.ts
|
|
||||||
event-create.spec.ts
|
|
||||||
event-view.spec.ts
|
|
||||||
fixtures/ # shared test fixtures (optional)
|
|
||||||
helpers/ # page object models (optional)
|
|
||||||
playwright-report/ # generated HTML report (gitignored)
|
|
||||||
test-results/ # generated artifacts (gitignored)
|
|
||||||
```
|
|
||||||
|
|
||||||
The `e2e/` directory is already excluded from Vitest via `vitest.config.ts:10`.
|
|
||||||
|
|
||||||
### 6. Recommended 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'] },
|
|
||||||
},
|
|
||||||
// Uncomment for cross-browser coverage:
|
|
||||||
// { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
||||||
// { name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
||||||
],
|
|
||||||
|
|
||||||
webServer: {
|
|
||||||
command: 'npm run dev',
|
|
||||||
url: 'http://localhost:5173',
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
timeout: 120_000,
|
|
||||||
stdout: 'pipe',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Key decisions:
|
|
||||||
- `testDir: './e2e'` — separates E2E from Vitest unit tests
|
|
||||||
- `forbidOnly: !!process.env.CI` — prevents `test.only` from shipping to CI
|
|
||||||
- `workers: process.env.CI ? 1 : undefined` — single worker in CI avoids shared-state flakiness; locally uses all cores
|
|
||||||
- `reporter: 'github'` — GitHub Actions annotations in CI
|
|
||||||
- `command: 'npm run dev'` — runs `generate:api` first (via the existing npm script), then starts Vite
|
|
||||||
- `reuseExistingServer: !process.env.CI` — reuses running dev server locally for fast iteration
|
|
||||||
|
|
||||||
### 7. package.json Scripts
|
|
||||||
|
|
||||||
```json
|
|
||||||
"test:e2e": "playwright test",
|
|
||||||
"test:e2e:ui": "playwright test --ui",
|
|
||||||
"test:e2e:debug": "playwright test --debug"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. .gitignore Additions
|
|
||||||
|
|
||||||
```
|
|
||||||
playwright-report/
|
|
||||||
test-results/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. TypeScript Configuration
|
|
||||||
|
|
||||||
The existing `tsconfig.app.json` excludes `src/**/__tests__/*`. Since E2E tests live in `e2e/` (outside `src/`), they are already excluded from the app build.
|
|
||||||
|
|
||||||
A separate `tsconfig` for E2E tests is not strictly required — Playwright's own TypeScript support handles it. If needed, a minimal `e2e/tsconfig.json` can extend `tsconfig.node.json`.
|
|
||||||
|
|
||||||
### 10. Vue-Specific Testing Patterns
|
|
||||||
|
|
||||||
**Router navigation:**
|
|
||||||
```typescript
|
|
||||||
await page.goto('/events/abc-123')
|
|
||||||
await page.waitForURL('/events/abc-123') // confirms SPA router resolved
|
|
||||||
```
|
|
||||||
|
|
||||||
**Waiting for reactive content (auto-retry):**
|
|
||||||
```typescript
|
|
||||||
await expect(page.getByRole('heading', { name: 'My Event' })).toBeVisible()
|
|
||||||
// Playwright auto-retries assertions for up to the configured timeout
|
|
||||||
```
|
|
||||||
|
|
||||||
**URL assertions:**
|
|
||||||
```typescript
|
|
||||||
await expect(page).toHaveURL(/\/events\/.+/)
|
|
||||||
```
|
|
||||||
|
|
||||||
**API mocking (for isolated E2E tests):**
|
|
||||||
```typescript
|
|
||||||
await page.route('/api/events/**', async (route) => {
|
|
||||||
await route.fulfill({
|
|
||||||
status: 200,
|
|
||||||
contentType: 'application/json',
|
|
||||||
body: JSON.stringify({ title: 'Test Event', date: '2026-04-01' }),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**Locator strategy — prefer accessible locators:**
|
|
||||||
```typescript
|
|
||||||
page.getByRole('button', { name: 'RSVP' }) // best
|
|
||||||
page.getByLabel('Event Title') // form fields
|
|
||||||
page.getByTestId('event-card') // data-testid fallback
|
|
||||||
page.locator('.some-class') // last resort
|
|
||||||
```
|
|
||||||
|
|
||||||
### 11. CI Integration
|
|
||||||
|
|
||||||
**GitHub Actions workflow:**
|
|
||||||
```yaml
|
|
||||||
- name: Install Playwright browsers
|
|
||||||
run: npx playwright install --with-deps chromium
|
|
||||||
# --with-deps installs OS-level libraries (libglib, libnss, etc.)
|
|
||||||
# Specify 'chromium' to save ~2min vs installing all browsers
|
|
||||||
|
|
||||||
- name: Run E2E tests
|
|
||||||
run: npx playwright test
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: frontend/playwright-report/
|
|
||||||
retention-days: 30
|
|
||||||
```
|
|
||||||
|
|
||||||
**Docker:** Use official images `mcr.microsoft.com/playwright:v1.x.x-noble` (Ubuntu 24.04). Alpine is unsupported (browsers need glibc). Key flag: `--ipc=host` prevents Chromium memory exhaustion. The Playwright Docker image version must match the `@playwright/test` package version exactly.
|
|
||||||
|
|
||||||
For the fete project, E2E tests run as a separate CI step, not inside the app's Dockerfile.
|
|
||||||
|
|
||||||
### 12. Integration with Existing Backend
|
|
||||||
|
|
||||||
Two approaches for E2E tests:
|
|
||||||
|
|
||||||
1. **Mocked backend** (via `page.route()`): Fast, isolated, no backend dependency. Good for frontend-only testing.
|
|
||||||
2. **Real backend**: Start Spring Boot alongside Vite. Tests hit `/api` through the Vite proxy. More realistic but requires Java in CI. Could use Docker Compose.
|
|
||||||
|
|
||||||
The Vite proxy config (`vite.config.ts:19-23`) already forwards `/api` to `localhost:8080`, so both approaches work without changes.
|
|
||||||
|
|
||||||
## Code References
|
|
||||||
|
|
||||||
- `frontend/vitest.config.ts:10` — E2E exclusion pattern already in place
|
|
||||||
- `frontend/vite.config.ts:19-23` — API proxy configuration for backend integration
|
|
||||||
- `frontend/package.json:8-9` — `dev` script runs `generate:api` before Vite
|
|
||||||
- `frontend/src/router/index.ts` — Route definitions (Home, Create, Event views)
|
|
||||||
- `frontend/src/api/client.ts` — openapi-fetch client using `/api` base URL
|
|
||||||
- `frontend/tsconfig.app.json` — App TypeScript config (excludes test files)
|
|
||||||
|
|
||||||
## Architecture Documentation
|
|
||||||
|
|
||||||
### Test Pyramid in fete
|
|
||||||
|
|
||||||
| Layer | Framework | Directory | Purpose |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Unit | Vitest + jsdom | `src/**/__tests__/` | Composables, isolated logic |
|
|
||||||
| Component | Vitest + @vue/test-utils | `src/**/__tests__/` | Vue component behavior |
|
|
||||||
| E2E | Playwright (proposed) | `e2e/` | Full browser, user flows |
|
|
||||||
| Visual | browser-interactive-testing skill | `.agent-tests/` | Agent-driven screenshots |
|
|
||||||
|
|
||||||
### Decision Points for Implementation
|
|
||||||
|
|
||||||
1. **Start with Chromium only** — add Firefox/WebKit later if needed
|
|
||||||
2. **Use `npm run dev`** as webServer command (includes API type generation)
|
|
||||||
3. **API mocking by default** — use `page.route()` for E2E isolation; full-stack tests as a separate concern
|
|
||||||
4. **`data-testid` attributes** on key interactive elements for stable selectors
|
|
||||||
5. **Page Object Model** recommended once the test suite grows beyond 5-10 tests
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
- [Testing | Vue.js](https://vuejs.org/guide/scaling-up/testing) — official E2E recommendation
|
|
||||||
- [Installation | Playwright](https://playwright.dev/docs/intro)
|
|
||||||
- [webServer | Playwright](https://playwright.dev/docs/test-webserver) — Vite integration
|
|
||||||
- [CI Intro | Playwright](https://playwright.dev/docs/ci-intro)
|
|
||||||
- [Docker | Playwright](https://playwright.dev/docs/docker)
|
|
||||||
- [Cypress vs Playwright 2026 | BugBug](https://bugbug.io/blog/test-automation-tools/cypress-vs-playwright/)
|
|
||||||
- [Playwright vs Cypress | Katalon](https://katalon.com/resources-center/blog/playwright-vs-cypress)
|
|
||||||
|
|
||||||
## Decisions (2026-03-05)
|
|
||||||
|
|
||||||
- **Mocked backend only** — E2E tests use `page.route()` to mock API responses. No real Spring Boot backend in E2E.
|
|
||||||
- **Mocking stack:** `@msw/playwright` + `@msw/source` — reads OpenAPI spec at runtime, generates MSW handlers, per-test overrides via `network.use()`.
|
|
||||||
- **US-1 flows first** — Event creation is the only implemented user story; E2E tests cover that flow.
|
|
||||||
- **No CI caching yet** — Playwright browser binaries are not cached; CI runner needs reconfiguration first.
|
|
||||||
- **E2E tests are part of frontend tasks** — every frontend user story includes E2E test coverage going forward.
|
|
||||||
- **OpenAPI examples mandatory** — all response schemas in the OpenAPI spec must include `example:` fields (required for `@msw/source` mock generation).
|
|
||||||
4
frontend/.gitignore
vendored
4
frontend/.gitignore
vendored
@@ -38,7 +38,3 @@ __screenshots__/
|
|||||||
# Vite
|
# Vite
|
||||||
*.timestamp-*-*.mjs
|
*.timestamp-*-*.mjs
|
||||||
.rodney/
|
.rodney/
|
||||||
|
|
||||||
# Playwright
|
|
||||||
playwright-report/
|
|
||||||
test-results/
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import { http, HttpResponse } from 'msw'
|
|
||||||
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')
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: /create event/i }).click()
|
|
||||||
|
|
||||||
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\/.+/)
|
|
||||||
|
|
||||||
const storage = await page.evaluate(() => {
|
|
||||||
const raw = localStorage.getItem('fete:events')
|
|
||||||
return raw ? JSON.parse(raw) : null
|
|
||||||
})
|
|
||||||
expect(storage).not.toBeNull()
|
|
||||||
expect(storage).toHaveLength(1)
|
|
||||||
expect(storage[0]).toHaveProperty('eventToken')
|
|
||||||
expect(storage[0]).toHaveProperty('title')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows server error on API failure', async ({ page, network }) => {
|
|
||||||
network.use(
|
|
||||||
http.post('*/api/events', () => {
|
|
||||||
return HttpResponse.json(
|
|
||||||
{ title: 'Bad Request', status: 400, detail: 'Validation failed' },
|
|
||||||
{ status: 400, headers: { 'Content-Type': 'application/problem+json' } },
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
await expect(page.getByRole('alert')).toBeVisible()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import { resolve } from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { test as base, expect } from '@playwright/test'
|
|
||||||
import { defineNetworkFixture, type NetworkFixture } from '@msw/playwright'
|
|
||||||
import { fromOpenApi } from '@msw/source/open-api'
|
|
||||||
|
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
||||||
const specPath = resolve(__dirname, '../../backend/src/main/resources/openapi/api.yaml')
|
|
||||||
const spec = readFileSync(specPath, 'utf-8')
|
|
||||||
|
|
||||||
const handlers = await fromOpenApi(spec)
|
|
||||||
|
|
||||||
interface Fixtures {
|
|
||||||
network: NetworkFixture
|
|
||||||
}
|
|
||||||
|
|
||||||
export const test = base.extend<Fixtures>({
|
|
||||||
network: [
|
|
||||||
async ({ context }, use) => {
|
|
||||||
const network = defineNetworkFixture({ context, handlers })
|
|
||||||
await network.enable()
|
|
||||||
await use(network)
|
|
||||||
await network.disable()
|
|
||||||
},
|
|
||||||
{ auto: true },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
export { expect }
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
882
frontend/package-lock.json
generated
882
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,10 +14,7 @@
|
|||||||
"lint": "run-s lint:*",
|
"lint": "run-s lint:*",
|
||||||
"lint:oxlint": "oxlint . --fix",
|
"lint:oxlint": "oxlint . --fix",
|
||||||
"lint:eslint": "eslint . --fix --cache",
|
"lint:eslint": "eslint . --fix --cache",
|
||||||
"format": "prettier --write --experimental-cli src/",
|
"format": "prettier --write --experimental-cli src/"
|
||||||
"test:e2e": "playwright test",
|
|
||||||
"test:e2e:ui": "playwright test --ui",
|
|
||||||
"test:e2e:debug": "playwright test --debug"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"openapi-fetch": "^0.17.0",
|
"openapi-fetch": "^0.17.0",
|
||||||
@@ -25,9 +22,6 @@
|
|||||||
"vue-router": "^5.0.3"
|
"vue-router": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@msw/playwright": "^0.6.5",
|
|
||||||
"@msw/source": "^0.6.1",
|
|
||||||
"@playwright/test": "^1.58.2",
|
|
||||||
"@tsconfig/node24": "^24.0.4",
|
"@tsconfig/node24": "^24.0.4",
|
||||||
"@types/jsdom": "^28.0.0",
|
"@types/jsdom": "^28.0.0",
|
||||||
"@types/node": "^24.11.0",
|
"@types/node": "^24.11.0",
|
||||||
@@ -42,7 +36,6 @@
|
|||||||
"eslint-plugin-vue": "~10.8.0",
|
"eslint-plugin-vue": "~10.8.0",
|
||||||
"jiti": "^2.6.1",
|
"jiti": "^2.6.1",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"msw": "^2.12.10",
|
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"openapi-typescript": "^7.13.0",
|
"openapi-typescript": "^7.13.0",
|
||||||
"oxlint": "~1.50.0",
|
"oxlint": "~1.50.0",
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test'
|
|
||||||
|
|
||||||
// Suppress Node 25 warning from MSW's cookieStore accessing native localStorage
|
|
||||||
// without --localstorage-file being set. Harmless — MSW doesn't need file-backed storage.
|
|
||||||
const originalEmit = process.emit.bind(process)
|
|
||||||
process.emit = function (event: string, ...args: unknown[]) {
|
|
||||||
if (event === 'warning' && args[0] instanceof Error && args[0].message.includes('--localstorage-file')) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return originalEmit(event, ...args)
|
|
||||||
} as typeof process.emit
|
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user