dorfteich/apps/web/e2e/sidebar.spec.ts
Claude Fable 5 2077d92c09 #166: Skip-Link, verstecktes Seiten-h1, Resizer in die Nav-Landmarke
Skip-Link als erster Tab-Stopp springt auf #main; die angemeldete
Seitenansicht bekommt ein visually-hidden h1 (der sichtbare Titel ist
ein Input, der jetzt auch ein aria-label trägt); der Sidebar-Resizer
wandert in die nav-Landmarke (absolut an der Kante positioniert), damit
kein Inhalt außerhalb von Landmarken liegt. Zwei e2e-Locator auf das
Sidebar-Formular gescoped — das Editor-Titelfeld matcht seit dem neuen
Label ebenfalls auf /title|titel/i.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 14:26:54 +02:00

148 lines
5.6 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { contextForUser } from './helpers';
/**
* Pond sidebar pack (issue #26). Runs against the local dev stack (api +
* web), no Mailpit needed. Creates its own pages per test via the api and
* navigates straight to `/p/:pondSlug/:pageSlug` (or `/p/:pondSlug`).
*/
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
async function personalPond(
context: Awaited<ReturnType<typeof contextForUser>>,
): Promise<{ id: string; slug: string }> {
const ponds = await context.request.get('/api/v1/ponds');
const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal');
return { id: pond.id, slug: pond.slug };
}
async function createPage(
context: Awaited<ReturnType<typeof contextForUser>>,
pondId: string,
title: string,
): Promise<{ id: string; slug: string }> {
const created = await context.request.post(`/api/v1/ponds/${pondId}/pages`, {
data: { title },
});
return created.json();
}
test('sort modes reorder the sidebar page list correctly', async ({ browser }) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const pond = await personalPond(context);
// Defensive: a previous failed run may have left this fixture pond on a
// non-default sort mode.
await context.request.patch(`/api/v1/ponds/${pond.id}`, { data: { sidebarSort: 'alpha' } });
const suffix = Date.now();
await createPage(context, pond.id, `Zebra ${suffix}`);
const apple = await createPage(context, pond.id, `Apple ${suffix}`);
await createPage(context, pond.id, `Mango ${suffix}`);
const page = await context.newPage();
await page.goto(`/p/${pond.slug}/${apple.slug}`);
const items = page.locator('.sidebar__pages .sidebar__page');
await expect(items.filter({ hasText: `Zebra ${suffix}` })).toBeVisible();
async function orderOf(titles: string[]): Promise<number[]> {
const texts = await items.allTextContents();
return titles.map((title) => texts.findIndex((text) => text === title));
}
// Default pond setting is 'alpha': relative order of the three fixture
// pages must be ascending regardless of whatever other pages exist.
function expectAscending(order: number[]): void {
expect(order.every((v, i) => i === 0 || order[i - 1]! < v)).toBe(true);
}
await expect(async () => {
expectAscending(await orderOf([`Apple ${suffix}`, `Mango ${suffix}`, `Zebra ${suffix}`]));
}).toPass();
try {
await page.getByLabel(/sort pages|seiten sortieren/i).selectOption('created');
await expect(async () => {
expectAscending(await orderOf([`Zebra ${suffix}`, `Apple ${suffix}`, `Mango ${suffix}`]));
}).toPass();
// Reload: the sort mode persisted server-side, not just in local state.
await page.reload();
await expect(page.getByLabel(/sort pages|seiten sortieren/i)).toHaveValue('created');
await expect(async () => {
expectAscending(await orderOf([`Zebra ${suffix}`, `Apple ${suffix}`, `Mango ${suffix}`]));
}).toPass();
} finally {
// Reset so this fixture pond doesn't leak state into other tests/runs.
await page.getByLabel(/sort pages|seiten sortieren/i).selectOption('alpha');
}
await context.close();
});
test('active page is highlighted in the sidebar', async ({ browser }) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const pond = await personalPond(context);
const suffix = Date.now();
const one = await createPage(context, pond.id, `Active One ${suffix}`);
const two = await createPage(context, pond.id, `Active Two ${suffix}`);
const page = await context.newPage();
await page.goto(`/p/${pond.slug}/${one.slug}`);
const activeLink = page.locator('.sidebar__page--active');
await expect(activeLink).toHaveText(`Active One ${suffix}`);
await page.goto(`/p/${pond.slug}/${two.slug}`);
await expect(page.locator('.sidebar__page--active')).toHaveText(`Active Two ${suffix}`);
await context.close();
});
test('new-page flow: button opens a title prompt and the editor opens on create', async ({
browser,
}) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const pond = await personalPond(context);
const suffix = Date.now();
const title = `Created via UI ${suffix}`;
const page = await context.newPage();
await page.goto(`/p/${pond.slug}`);
await page.getByRole('button', { name: /new page|neue seite/i }).click();
await page.locator('.sidebar').getByLabel(/title|titel/i).fill(title);
await page.getByRole('button', { name: /create|erstellen/i }).click();
await expect(page).toHaveURL(new RegExp(`/p/${pond.slug}/.+`));
await expect(page.locator('.editor-page__title')).toHaveValue(title);
await expect(page.locator('.sidebar__page--active')).toHaveText(title);
await context.close();
});
test('sidebar collapse state persists per user and via the keyboard shortcut', async ({
browser,
}) => {
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
const pond = await personalPond(context);
const suffix = Date.now();
const created = await createPage(context, pond.id, `Collapse Test ${suffix}`);
const page = await context.newPage();
await page.goto(`/p/${pond.slug}/${created.slug}`);
const sidebar = page.locator('nav.sidebar');
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
await page.keyboard.press('ControlOrMeta+Backslash');
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
await page.reload();
await expect(sidebar).toHaveAttribute('aria-hidden', 'true');
await page.keyboard.press('ControlOrMeta+Backslash');
await expect(sidebar).toHaveAttribute('aria-hidden', 'false');
await context.close();
});