import { expect, test } from '@playwright/test'; import { contextForUser } from './helpers'; /** * Manual page ordering pack (issue #45). Exercises the keyboard reorder path * (up/down buttons) — the same `moveTo` → `PATCH /pages/:id/position` code the * drag-and-drop uses — because native HTML5 drag events are unreliable to * simulate. Verifies the order is server-authoritative (persists across a * reload and is identical on a fresh read) and that moves are announced via * aria-live. Selectors are language-independent (CSS classes + arrow glyphs). */ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; type Ctx = Awaited>; async function personalPond(context: Ctx): 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: Ctx, pondId: string, title: string): Promise<{ slug: string }> { const created = await context.request.post(`/api/v1/ponds/${pondId}/pages`, { data: { title } }); return created.json(); } /** Relative order of the given titles among the sidebar's page links. */ function relativeOrder(listTexts: string[], titles: string[]): number[] { return titles.map((title) => listTexts.findIndex((text) => text === title)); } test('manual reorder persists server-side and is announced', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const pond = await personalPond(context); await context.request.patch(`/api/v1/ponds/${pond.id}`, { data: { sidebarSort: 'manual' } }); const ts = Date.now(); const first = `R-First ${ts}`; const second = `R-Second ${ts}`; const third = `R-Third ${ts}`; const created = await createPage(context, pond.id, first); await createPage(context, pond.id, second); await createPage(context, pond.id, third); const page = await context.newPage(); try { await page.goto(`/p/${pond.slug}/${created.slug}`); const links = page.locator('.sidebar__pages .sidebar__page'); await expect(links.filter({ hasText: third }).first()).toBeVisible(); // Manual mode shows creation order: first, second, third (relatively). const before = relativeOrder(await links.allTextContents(), [first, second, third]); expect(before[0]! < before[1]! && before[1]! < before[2]!).toBe(true); // Move "first" down once via the keyboard button → order second, first, third. const firstRow = page.locator('.sidebar__page-item', { has: page.getByText(first, { exact: true }), }); await firstRow.locator('.sidebar__reorder-btn', { hasText: '↓' }).click(); // aria-live announced the move (contains the title and a position number). const status = page.locator('.sidebar__announce'); await expect(status).toContainText(first); await expect(status).toContainText(/\d/); await expect(async () => { const order = relativeOrder(await links.allTextContents(), [second, first, third]); expect(order[0]! < order[1]! && order[1]! < order[2]!).toBe(true); }).toPass(); // Persisted server-side: a reload keeps the new order… await page.reload(); await expect(links.filter({ hasText: first }).first()).toBeVisible(); await expect(async () => { const order = relativeOrder(await links.allTextContents(), [second, first, third]); expect(order[0]! < order[1]! && order[1]! < order[2]!).toBe(true); }).toPass(); // …and a fresh read (any other user's view) sees the identical order. const reread = await (await context.request.get(`/api/v1/ponds/${pond.id}/pages`)).json(); const titles = reread.map((p: { title: string }) => p.title); const idx = (t: string) => titles.indexOf(t); expect(idx(second) < idx(first) && idx(first) < idx(third)).toBe(true); } finally { // Reset so this fixture pond does not leak a non-default sort mode. await context.request.patch(`/api/v1/ponds/${pond.id}`, { data: { sidebarSort: 'alpha' } }); } await context.close(); });