From e6ab2a30cec057216b64e61aca16089f90f96c64 Mon Sep 17 00:00:00 2001 From: Matteo Date: Sun, 3 May 2026 13:52:18 +0200 Subject: [PATCH 1/2] Sprint 2 batch 7: Playwright e2e for the auth UI + 404 page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spins up next dev on :3100 (output: standalone makes next start a no-op, and the standalone build embeds an absolute path that differs between local and CI), then runs three checks: 1. login page exposes the email/password inputs we just rewired with proper htmlFor — clicking the visible label focuses the input. 2. forgot-password has the same label/input pairing. 3. an unknown route under /login/* yields HTTP 404 + the branded not-found.tsx (heading + dashboard link). The playwright.yml workflow runs on push/PR and uploads the report on failure. Local: 3/3 passed. --- .github/workflows/playwright.yml | 43 ++++++++++++ package-lock.json | 65 +++++++++++++++++++ packages/frontend/next-env.d.ts | 2 +- packages/frontend/package.json | 5 +- packages/frontend/playwright.config.ts | 41 ++++++++++++ packages/frontend/test-results/.last-run.json | 4 ++ packages/frontend/tests/e2e/auth.spec.ts | 50 ++++++++++++++ 7 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 packages/frontend/playwright.config.ts create mode 100644 packages/frontend/test-results/.last-run.json create mode 100644 packages/frontend/tests/e2e/auth.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..7a5e606 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,43 @@ +name: Playwright + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + e2e: + name: Frontend e2e + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + working-directory: packages/frontend + + - name: Run Playwright tests + run: npm run test:e2e + working-directory: packages/frontend + env: + NEXT_PUBLIC_API_URL: http://localhost:4100 + + - name: Upload Playwright report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: packages/frontend/playwright-report/ + retention-days: 14 diff --git a/package-lock.json b/package-lock.json index 605c469..50ad471 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3818,6 +3818,23 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz", @@ -16100,6 +16117,53 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "devOptional": 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", @@ -20161,6 +20225,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", + "@playwright/test": "^1.59.1", "@types/node": "^22.10.7", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/packages/frontend/next-env.d.ts b/packages/frontend/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/packages/frontend/next-env.d.ts +++ b/packages/frontend/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 22e0e82..ad36aa7 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,7 +8,9 @@ "dev": "next dev --port 3000", "build": "next build", "start": "next start", - "lint": "eslint src/" + "lint": "eslint src/", + "test:e2e": "playwright test", + "test:e2e:install": "playwright install --with-deps chromium" }, "dependencies": { "@radix-ui/react-dialog": "^1.1.4", @@ -33,6 +35,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", + "@playwright/test": "^1.59.1", "@types/node": "^22.10.7", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/packages/frontend/playwright.config.ts b/packages/frontend/playwright.config.ts new file mode 100644 index 0000000..bdfef20 --- /dev/null +++ b/packages/frontend/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test'; + +const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3100'; +const reuseServer = process.env.PLAYWRIGHT_USE_RUNNING_SERVER === '1'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30_000, + expect: { timeout: 5_000 }, + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [['list']], + use: { + baseURL, + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: reuseServer + ? undefined + : { + // The frontend uses `output: 'standalone'`, which makes `next start` + // refuse to run. The standalone build path embeds the absolute build + // dir, which differs between local + CI. `next dev` boots the same + // App Router and is good enough for these UI-only smoke checks. + command: 'npx next dev --port 3100', + url: baseURL, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + NEXT_PUBLIC_API_URL: + process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4100', + }, + }, +}); diff --git a/packages/frontend/test-results/.last-run.json b/packages/frontend/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/packages/frontend/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/packages/frontend/tests/e2e/auth.spec.ts b/packages/frontend/tests/e2e/auth.spec.ts new file mode 100644 index 0000000..276d69d --- /dev/null +++ b/packages/frontend/tests/e2e/auth.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; + +/** + * Minimal e2e for the auth flow + the new error/not-found pages. + * + * Aims to catch regressions on the things Sprint 2 actually changed: + * - login renders and the form has the new label/input pairing + * - 404 → branded not-found.tsx (not Next's default) + * + * Doesn't try to log in for real — that needs a backend. Anything backend- + * dependent is covered by scripts/smoke-test/run.sh. + */ + +test.describe('auth UI', () => { + test('login page renders with accessible labels and label/input pairing', async ({ + page, + }) => { + await page.goto('/login'); + const email = page.locator('#auth-email'); + const password = page.locator('#auth-password'); + await expect(email).toBeVisible(); + await expect(password).toBeVisible(); + // Clicking the visible label focuses the matching input — proves htmlFor works. + await page.locator('label[for="auth-email"]').click(); + await expect(email).toBeFocused(); + }); + + test('forgot-password label/input pairing works', async ({ page }) => { + await page.goto('/forgot-password'); + const email = page.locator('#forgot-email'); + await expect(email).toBeVisible(); + await page.locator('label[for="forgot-email"]').click(); + await expect(email).toBeFocused(); + await email.fill('user@example.com'); + await expect(email).toHaveValue('user@example.com'); + }); + + test('unknown route shows the branded 404', async ({ page }) => { + // /login/* is allowed by the auth proxy, so the request reaches Next + // (otherwise the proxy would redirect us back to /login with no 404). + const res = await page.goto('/login/this-route-does-not-exist'); + expect(res?.status()).toBe(404); + await expect( + page.getByRole('heading', { name: /page not found/i }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: /go to dashboard/i }), + ).toBeVisible(); + }); +}); From 822af416feafe0d0d03511eadd282f4e72d3e02f Mon Sep 17 00:00:00 2001 From: Matteo Date: Sun, 3 May 2026 13:52:47 +0200 Subject: [PATCH 2/2] chore: ignore Playwright local artifacts --- .gitignore | 6 ++++++ packages/frontend/test-results/.last-run.json | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 packages/frontend/test-results/.last-run.json diff --git a/.gitignore b/.gitignore index 7e347fd..3077ef4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,11 @@ packages/backend/src/generated/ docker-compose.override.yml Caddyfile +# Playwright +packages/frontend/test-results/ +packages/frontend/playwright-report/ +packages/frontend/blob-report/ +packages/frontend/playwright/.cache/ + # Misc *.tsbuildinfo diff --git a/packages/frontend/test-results/.last-run.json b/packages/frontend/test-results/.last-run.json deleted file mode 100644 index cbcc1fb..0000000 --- a/packages/frontend/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "passed", - "failedTests": [] -} \ No newline at end of file