Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
65 changes: 65 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.
5 changes: 4 additions & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
41 changes: 41 additions & 0 deletions packages/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
});
50 changes: 50 additions & 0 deletions packages/frontend/tests/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading