From 8499c60acbacf6b52032152bd80f50856f3207d1 Mon Sep 17 00:00:00 2001 From: Bert Hausmans Date: Thu, 21 May 2026 07:24:42 +0200 Subject: [PATCH] test(e2e): search palette + lesson detail + stats + legacy redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add e2e/ux.spec.ts covering the ⌘K search palette, lesson detail page, stats sections, and the legacy /admin → /lessons redirect. Also fixes two issues uncovered by running the full suite: - Skip auth rate limiter in e2e by running the backend with NODE_ENV=test (registration limit of 5 was tripping later tests). - Render the card table for lesson owners even when the lesson has zero cards, so the first card can be added from the detail page. --- e2e/ux.spec.ts | 82 ++++++++++++++++++++ packages/frontend/playwright.config.ts | 2 +- packages/frontend/src/pages/LessonDetail.tsx | 4 +- 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 e2e/ux.spec.ts diff --git a/e2e/ux.spec.ts b/e2e/ux.spec.ts new file mode 100644 index 0000000..2d9aacb --- /dev/null +++ b/e2e/ux.spec.ts @@ -0,0 +1,82 @@ +import { test, expect } from '@playwright/test'; + +async function fetchVerifyLink(email: string): Promise { + for (let i = 0; i < 30; i++) { + const res = await fetch('http://localhost:8025/api/v1/messages?limit=30'); + const data = await res.json() as { messages: { ID: string; To: { Address: string }[] }[] }; + const msg = data.messages.find((m) => m.To.some((t) => t.Address === email)); + if (msg) { + const body = await (await fetch(`http://localhost:8025/api/v1/message/${msg.ID}`)).json() as { Text: string }; + const m = body.Text.match(/https?:\/\/[^\s]+verify-email\?token=[^\s]+/); + if (m) return m[0]; + } + await new Promise((r) => setTimeout(r, 250)); + } + throw new Error('no verify link for ' + email); +} + +async function registerVerifyLogin(page: import('@playwright/test').Page, name: string, email: string, password: string) { + await page.goto('/register'); + await page.getByLabel(/Naam/).fill(name); + await page.getByLabel(/E-mailadres/).fill(email); + await page.getByLabel(/Wachtwoord/).fill(password); + await page.getByRole('button', { name: /Account aanmaken/ }).click(); + await expect(page.getByText(/bevestigingsmail/i)).toBeVisible({ timeout: 10_000 }); + const link = await fetchVerifyLink(email); + await page.goto(link); + await expect(page.getByRole('link', { name: 'Naar inloggen' })).toBeVisible({ timeout: 10_000 }); + await page.goto('/login'); + await page.getByLabel(/E-mailadres/).fill(email); + await page.getByLabel(/Wachtwoord/).fill(password); + await page.getByRole('button', { name: 'Inloggen' }).click(); + await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible({ timeout: 15_000 }); +} + +test('search opens with ⌘K, finds a lesson, navigates to detail', async ({ page }) => { + const email = `search+${Date.now()}@example.com`; + await registerVerifyLogin(page, 'SearchUser', email, 'secretpass'); + await page.goto('/lessons'); + await page.getByPlaceholder(/Nieuwe wortel-les/).fill('Aardrijkskunde'); + await page.getByRole('button', { name: /Toevoegen/ }).first().click(); + await expect(page.getByRole('link', { name: /Aardrijkskunde/ }).first()).toBeVisible(); + + await page.keyboard.press('Control+K'); + const searchInput = page.getByPlaceholder(/Zoek lessen en kaarten/); + await expect(searchInput).toBeVisible(); + await searchInput.fill('aardrijk'); + // Scope to the palette result row so we wait for the debounced search result, + // not the background lessons-tree link. + const resultRow = page.getByRole('listitem').filter({ hasText: /door SearchUser/ }).first(); + await expect(resultRow).toBeVisible({ timeout: 5_000 }); + await resultRow.click(); + await expect(page).toHaveURL(/\/lessons\/\d+/); + await expect(page.getByRole('heading', { name: /Aardrijkskunde/ })).toBeVisible(); +}); + +test('lesson detail page shows start practice', async ({ page }) => { + const email = `detail+${Date.now()}@example.com`; + await registerVerifyLogin(page, 'DetailUser', email, 'secretpass'); + await page.goto('/lessons'); + await page.getByPlaceholder(/Nieuwe wortel-les/).fill('Wiskunde-test'); + await page.getByRole('button', { name: /Toevoegen/ }).first().click(); + await page.getByRole('link', { name: /Wiskunde-test/ }).first().click(); + await expect(page.getByRole('heading', { name: /Wiskunde-test/ })).toBeVisible(); + await expect(page.getByRole('link', { name: /Start oefenen/ })).toBeVisible(); +}); + +test('stats page renders three sections', async ({ page }) => { + const email = `stats+${Date.now()}@example.com`; + await registerVerifyLogin(page, 'StatsUser', email, 'secretpass'); + await page.goto('/stats'); + await expect(page.getByRole('heading', { name: 'Statistieken' })).toBeVisible(); + await expect(page.getByText(/Te reviewen/)).toBeVisible(); + await expect(page.getByText(/Voortgang per les/)).toBeVisible(); + await expect(page.getByText(/Activiteit/)).toBeVisible(); +}); + +test('legacy /admin redirects to /lessons', async ({ page }) => { + const email = `legacy+${Date.now()}@example.com`; + await registerVerifyLogin(page, 'Legacy', email, 'secretpass'); + await page.goto('/admin'); + await expect(page).toHaveURL(/\/lessons$/); +}); diff --git a/packages/frontend/playwright.config.ts b/packages/frontend/playwright.config.ts index f1bc9aa..55e764e 100644 --- a/packages/frontend/playwright.config.ts +++ b/packages/frontend/playwright.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ webServer: [ { command: - 'rm -f packages/backend/data/e2e.db data/e2e.db && DB_PATH=./data/e2e.db SMTP_HOST=localhost SMTP_PORT=1025 APP_URL=http://localhost:5173 npm -w @flashcard/backend run db:migrate && DB_PATH=./data/e2e.db SMTP_HOST=localhost SMTP_PORT=1025 APP_URL=http://localhost:5173 npm -w @flashcard/backend run dev', + 'rm -f packages/backend/data/e2e.db data/e2e.db && DB_PATH=./data/e2e.db SMTP_HOST=localhost SMTP_PORT=1025 APP_URL=http://localhost:5173 npm -w @flashcard/backend run db:migrate && NODE_ENV=test DB_PATH=./data/e2e.db SMTP_HOST=localhost SMTP_PORT=1025 APP_URL=http://localhost:5173 npm -w @flashcard/backend run dev', cwd: '../..', port: 3000, reuseExistingServer: false, diff --git a/packages/frontend/src/pages/LessonDetail.tsx b/packages/frontend/src/pages/LessonDetail.tsx index 59b357d..e85e41d 100644 --- a/packages/frontend/src/pages/LessonDetail.tsx +++ b/packages/frontend/src/pages/LessonDetail.tsx @@ -153,9 +153,9 @@ export function LessonDetailPage() {

Kaarten

- {cards.length === 0 ? ( + {cards.length === 0 && !isOwner ? (
- {isOwner ? 'Nog geen kaarten — voeg er hieronder een toe.' : 'Deze les heeft nog geen kaarten.'} + Deze les heeft nog geen kaarten.
) : (