dorfteich/apps/web/e2e/sidebar.spec.ts
Claude Sonnet 5 49beb45b3e
All checks were successful
CD / Build and push images (push) Successful in 1m59s
CI / Lint, typecheck, test (push) Successful in 1m38s
CI / Auth e2e pack (push) Successful in 1m48s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 9s
Add pond sidebar with page list, sort modes, and pond switcher (#26)
GET /ponds/:id/pages lists a pond's pages ordered by the pond's
persisted sidebarSort setting (alpha/created; manual arrives with #45).
The sidebar consumes it to show the page list with an active-page
highlight, an owner-only sort switch (persists via the existing
PATCH /ponds/:id), and an inline "new page" flow. The top bar gains a
pond switcher; a new /p/:pondSlug route gives it somewhere to land,
redirecting to the pond's first page once loaded. Sidebar collapse
gains a Ctrl/Cmd+\ shortcut and a slightly refined transition.

Closes #26
2026-07-06 12:37:44 +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.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();
});