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