From 9cf199dd9fdb26efa1e3c75b757f626279d4ee29 Mon Sep 17 00:00:00 2001 From: nitrix Date: Fri, 6 Mar 2026 18:14:11 +0100 Subject: [PATCH] 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 --- .gitignore | 1 + ...2026-03-05-e2e-testing-playwright-setup.md | 504 ++++++++++ .../2026-03-05-e2e-testing-playwright-vue3.md | 273 ++++++ frontend/.gitignore | 4 + frontend/e2e/event-create.spec.ts | 69 ++ frontend/e2e/msw-setup.ts | 30 + frontend/e2e/smoke.spec.ts | 20 + frontend/package-lock.json | 882 ++++++++++++++++++ frontend/package.json | 9 +- frontend/playwright.config.ts | 41 + 10 files changed, 1832 insertions(+), 1 deletion(-) create mode 100644 docs/agents/plan/2026-03-05-e2e-testing-playwright-setup.md create mode 100644 docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md create mode 100644 frontend/e2e/event-create.spec.ts create mode 100644 frontend/e2e/msw-setup.ts create mode 100644 frontend/e2e/smoke.spec.ts create mode 100644 frontend/playwright.config.ts diff --git a/.gitignore b/.gitignore index 5937e87..05a2d00 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ Thumbs.db # Claude Code (machine-local) .claude/settings.local.json +.mcp.json .rodney/ .agent-tests/ diff --git a/docs/agents/plan/2026-03-05-e2e-testing-playwright-setup.md b/docs/agents/plan/2026-03-05-e2e-testing-playwright-setup.md new file mode 100644 index 0000000..8efdcb1 --- /dev/null +++ b/docs/agents/plan/2026-03-05-e2e-testing-playwright-setup.md @@ -0,0 +1,504 @@ +--- +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` diff --git a/docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md b/docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md new file mode 100644 index 0000000..8c115c3 --- /dev/null +++ b/docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md @@ -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). diff --git a/frontend/.gitignore b/frontend/.gitignore index 2fbe504..1d3db3c 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -38,3 +38,7 @@ __screenshots__/ # Vite *.timestamp-*-*.mjs .rodney/ + +# Playwright +playwright-report/ +test-results/ diff --git a/frontend/e2e/event-create.spec.ts b/frontend/e2e/event-create.spec.ts new file mode 100644 index 0000000..0db9f01 --- /dev/null +++ b/frontend/e2e/event-create.spec.ts @@ -0,0 +1,69 @@ +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() + }) +}) diff --git a/frontend/e2e/msw-setup.ts b/frontend/e2e/msw-setup.ts new file mode 100644 index 0000000..a4e06b7 --- /dev/null +++ b/frontend/e2e/msw-setup.ts @@ -0,0 +1,30 @@ +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({ + network: [ + async ({ context }, use) => { + const network = defineNetworkFixture({ context, handlers }) + await network.enable() + await use(network) + await network.disable() + }, + { auto: true }, + ], +}) + +export { expect } diff --git a/frontend/e2e/smoke.spec.ts b/frontend/e2e/smoke.spec.ts new file mode 100644 index 0000000..72cc5c7 --- /dev/null +++ b/frontend/e2e/smoke.spec.ts @@ -0,0 +1,20 @@ +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() + }) +}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f0ce2e2..890ea1f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,9 @@ "vue-router": "^5.0.3" }, "devDependencies": { + "@msw/playwright": "^0.6.5", + "@msw/source": "^0.6.1", + "@playwright/test": "^1.58.2", "@tsconfig/node24": "^24.0.4", "@types/jsdom": "^28.0.0", "@types/node": "^24.11.0", @@ -27,6 +30,7 @@ "eslint-plugin-vue": "~10.8.0", "jiti": "^2.6.1", "jsdom": "^28.1.0", + "msw": "^2.12.10", "npm-run-all2": "^8.0.4", "openapi-typescript": "^7.13.0", "oxlint": "~1.50.0", @@ -1279,6 +1283,23 @@ } } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1331,6 +1352,170 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1394,6 +1579,83 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@msw/playwright": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@msw/playwright/-/playwright-0.6.5.tgz", + "integrity": "sha512-inqYTLiJk0dnsQD5XMo5dIt2aZYTw8DtSr1Unfa4tLJmGIU6+qwpCkmWrNksqkBuc/nCz6nxmqSgkkZ87b+btA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mswjs/interceptors": "^0.41.3", + "outvariant": "^1.4.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "msw": "^2.12.10" + } + }, + "node_modules/@msw/source": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@msw/source/-/source-0.6.1.tgz", + "integrity": "sha512-eKX3x85/Ejo2HoOgfilfB4cMqdzie58F+7vMvuKH2N0m4Hs3GhfqjLSLlWXkNx09O7NIUSilygmrlEeWOPDNUA==", + "dev": true, + "dependencies": { + "@mswjs/interceptors": "^0.40.0", + "@stoplight/json": "^3.21.7", + "@types/har-format": "^1.2.16", + "@yellow-ticket/seed-json-schema": "^0.1.7", + "openapi-types": "^12.1.3", + "outvariant": "^1.4.3", + "yaml": "^2.8.2" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "msw": "^2.10.0" + } + }, + "node_modules/@msw/source/node_modules/@mswjs/interceptors": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", + "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1439,6 +1701,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxlint/binding-android-arm-eabi": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.50.0.tgz", @@ -1773,6 +2060,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -2227,6 +2530,65 @@ "dev": true, "license": "MIT" }, + "node_modules/@stoplight/json": { + "version": "3.21.7", + "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", + "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stoplight/ordered-object-literal": "^1.0.3", + "@stoplight/path": "^1.3.2", + "@stoplight/types": "^13.6.0", + "jsonc-parser": "~2.2.1", + "lodash": "^4.17.21", + "safe-stable-stringify": "^1.1" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/@stoplight/json/node_modules/jsonc-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", + "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stoplight/ordered-object-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", + "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/path": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", + "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@stoplight/types": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", + "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.4", + "utility-types": "^3.10.0" + }, + "engines": { + "node": "^12.20 || >=14.13" + } + }, "node_modules/@tsconfig/node24": { "version": "24.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz", @@ -2266,6 +2628,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/jsdom": { "version": "28.0.0", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-28.0.0.tgz", @@ -2303,6 +2672,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -3065,6 +3441,19 @@ } } }, + "node_modules/@yellow-ticket/seed-json-schema": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@yellow-ticket/seed-json-schema/-/seed-json-schema-0.1.7.tgz", + "integrity": "sha512-OQkwqMt6VMMExa74b6ODrDwMyvTAQ4wXXodb0kJuQhI1DkhFm4PIUwj9D1foE5gZwfyGlogtTZsjhyQzhwkyyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@types/json-schema": "^7.0.15", + "outvariant": "^1.4.2", + "randexp": "^0.5.3" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -3404,6 +3793,110 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3465,6 +3958,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3618,6 +4125,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4211,6 +4728,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -4279,6 +4806,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/graphql": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", + "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -4434,6 +4978,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4752,6 +5303,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.2.6", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", @@ -4898,12 +5456,83 @@ "dev": true, "license": "MIT" }, + "node_modules/msw": { + "version": "2.12.10", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.10.tgz", + "integrity": "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.41.2", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.0.2", + "graphql": "^16.12.0", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.10.1", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^5.2.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "license": "MIT" }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5087,6 +5716,13 @@ "openapi-typescript-helpers": "^0.1.0" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, "node_modules/openapi-typescript": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", @@ -5132,6 +5768,13 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/oxlint": { "version": "1.50.0", "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.50.0.tgz", @@ -5298,6 +5941,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5353,6 +6003,53 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -5485,6 +6182,20 @@ ], "license": "MIT" }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/read-package-json-fast": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", @@ -5512,6 +6223,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5522,6 +6243,23 @@ "node": ">=0.10.0" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rettime": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", + "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", + "dev": true, + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5615,6 +6353,13 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", + "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", + "dev": true, + "license": "MIT" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -5734,6 +6479,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -5741,6 +6496,13 @@ "dev": true, "license": "MIT" }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5865,6 +6627,19 @@ "dev": true, "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6160,6 +6935,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6215,6 +7000,16 @@ "dev": true, "license": "MIT" }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -6900,6 +7695,16 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6929,6 +7734,25 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -6939,6 +7763,51 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6951,6 +7820,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/package.json b/frontend/package.json index cddac1a..b8f2620 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,10 @@ "lint": "run-s lint:*", "lint:oxlint": "oxlint . --fix", "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": { "openapi-fetch": "^0.17.0", @@ -22,6 +25,9 @@ "vue-router": "^5.0.3" }, "devDependencies": { + "@msw/playwright": "^0.6.5", + "@msw/source": "^0.6.1", + "@playwright/test": "^1.58.2", "@tsconfig/node24": "^24.0.4", "@types/jsdom": "^28.0.0", "@types/node": "^24.11.0", @@ -36,6 +42,7 @@ "eslint-plugin-vue": "~10.8.0", "jiti": "^2.6.1", "jsdom": "^28.1.0", + "msw": "^2.12.10", "npm-run-all2": "^8.0.4", "openapi-typescript": "^7.13.0", "oxlint": "~1.50.0", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..4f925fc --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,41 @@ +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', + }, +})