test(e2e): search palette + lesson detail + stats + legacy redirect
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.
This commit is contained in:
82
e2e/ux.spec.ts
Normal file
82
e2e/ux.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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$/);
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
webServer: [
|
webServer: [
|
||||||
{
|
{
|
||||||
command:
|
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: '../..',
|
cwd: '../..',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
|
|||||||
@@ -153,9 +153,9 @@ export function LessonDetailPage() {
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-3 font-display text-xl font-bold">Kaarten</h2>
|
<h2 className="mb-3 font-display text-xl font-bold">Kaarten</h2>
|
||||||
{cards.length === 0 ? (
|
{cards.length === 0 && !isOwner ? (
|
||||||
<div className="surface p-6 text-center text-sm text-slate-500">
|
<div className="surface p-6 text-center text-sm text-slate-500">
|
||||||
{isOwner ? 'Nog geen kaarten — voeg er hieronder een toe.' : 'Deze les heeft nog geen kaarten.'}
|
Deze les heeft nog geen kaarten.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="surface overflow-hidden p-1">
|
<div className="surface overflow-hidden p-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user