---
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).
