Task-Checkboxen tragen in beiden Renderpfaden einen Namen: docToHtml setzt aria-label aus dem Aufgabentext, die Editor-NodeView ebenso. Die NodeView rendert ihr Host-Element jetzt selbst als li (ReactNodeView- Renderer as/attrs) — TipTaps zusätzliches div-Host-Element zwischen ul und li brach die Listensemantik; der Wrapper flacht per display:contents ab, die #137-Pixel-Abstimmung bleibt erhalten (Selektor auf die neue Tiefe nachgeführt, Ausrichtung nachgemessen: 1px-Versatz unverändert). Der Wissensgraph-SVG bekommt ein beschreibendes aria-label inklusive Verweis auf die Backlinks als gleichwertige Listenform. Der Bild-Alt-Editor existierte bereits (Bild-Controls bei Auswahl) — kein Änderungsbedarf. Hinweis: gecachte Seiten übernehmen das Checkbox-Label wie bei jeder docToHtml-Änderung erst mit dem nächsten Persist ihrer Inhalte. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
151 lines
5.6 KiB
TypeScript
151 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();
|
|
});
|