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
This commit is contained in:
273
docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md
Normal file
273
docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
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).
|
||||
Reference in New Issue
Block a user