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.
83 lines
4.1 KiB
TypeScript
83 lines
4.1 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
async function fetchVerifyLink(email: string): Promise<string> {
|
|
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$/);
|
|
});
|