import { expect, test, type BrowserContext } from '@playwright/test'; import { contextForUser } from './helpers'; /** * Favorites pack (issue #132): the TopBar star toggle, the golden tree * icon, the latching favorites filter in the sidebar, and that favorites * are personal — stored per user, not per pond. Runs in its own shared * pond so fixture ponds stay untouched. Language-independent selectors * (CSS classes) throughout. */ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; async function json(context: BrowserContext, url: string, data: unknown): Promise { const response = await context.request.post(url, { data }); if (!response.ok()) throw new Error(`post ${url} → ${response.status()}`); return response.json() as Promise; } test('star toggle, golden tree icon, and the favorites filter', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const ts = Date.now(); const pond = await json<{ id: string; slug: string }>(context, '/api/v1/ponds', { name: `Fav Pack ${ts}`, }); const starred = await json<{ id: string; slug: string }>( context, `/api/v1/ponds/${pond.id}/pages`, { title: `Fav Starred ${ts}` }, ); await json(context, `/api/v1/ponds/${pond.id}/pages`, { title: `Fav Plain ${ts}` }); const page = await context.newPage(); await page.goto(`/p/${pond.slug}/${starred.slug}`); // Star the page from the TopBar (reading mode) — the button flips to // "remove" state (aria-pressed) and turns golden. const star = page.locator('.editor-page__favorite-toggle'); await expect(star).toHaveAttribute('aria-pressed', 'false'); await star.click(); await expect(star).toHaveAttribute('aria-pressed', 'true'); await expect(star).toHaveClass(/icon-button--favorite/); // The sidebar tree marks the favorite with a golden icon. const starredItem = page .locator('.sidebar__page-item') .filter({ has: page.locator(`.sidebar__page:text-is("Fav Starred ${ts}")`) }) .first(); await expect(starredItem.locator('.sidebar__page-icon--favorite')).toBeVisible(); const plainItem = page .locator('.sidebar__page-item') .filter({ has: page.locator(`.sidebar__page:text-is("Fav Plain ${ts}")`) }) .first(); await expect(plainItem.locator('.sidebar__page-icon--favorite')).toHaveCount(0); // The latching filter narrows the sidebar to favorites only. const filter = page.locator('.sidebar__view-btn--favorites'); await filter.click(); await expect(filter).toHaveAttribute('aria-pressed', 'true'); await expect(page.locator(`.sidebar__page:text-is("Fav Starred ${ts}")`)).toBeVisible(); await expect(page.locator(`.sidebar__page:text-is("Fav Plain ${ts}")`)).toHaveCount(0); await filter.click(); await expect(page.locator(`.sidebar__page:text-is("Fav Plain ${ts}")`)).toBeVisible(); // Unstar → the golden icon disappears. await star.click(); await expect(star).toHaveAttribute('aria-pressed', 'false'); await expect(starredItem.locator('.sidebar__page-icon--favorite')).toHaveCount(0); await context.close(); }); test('favorites are personal: another user does not see my star', async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const ts = Date.now(); const pond = await json<{ id: string; slug: string }>(owner, '/api/v1/ponds', { name: `Fav Personal ${ts}`, }); const target = await json<{ id: string; slug: string }>(owner, `/api/v1/ponds/${pond.id}/pages`, { title: `Fav Mine ${ts}`, }); // Grant the fixture viewer read access via the grants API (never direct // DB writes — the permission cache must see the change). const viewerContext = await contextForUser(browser, BASE_URL, 'fixture-viewer'); const meRes = await viewerContext.request.get('/api/v1/auth/me'); const viewerId = ((await meRes.json()) as { id: string }).id; await json(owner, `/api/v1/ponds/${pond.id}/grants`, { subjectType: 'user', subjectId: viewerId, role: 'reader', scopeType: 'pond', effect: 'allow', }); // Owner stars the page. const ownerPage = await owner.newPage(); await ownerPage.goto(`/p/${pond.slug}/${target.slug}`); await ownerPage.locator('.editor-page__favorite-toggle').click(); await expect(ownerPage.locator('.editor-page__favorite-toggle')).toHaveAttribute( 'aria-pressed', 'true', ); // The viewer sees the page, but no star and no golden icon. const viewerPage = await viewerContext.newPage(); await viewerPage.goto(`/p/${pond.slug}/${target.slug}`); await expect(viewerPage.locator('.editor-page__favorite-toggle')).toHaveAttribute( 'aria-pressed', 'false', ); await expect(viewerPage.locator('.sidebar__page-icon--favorite')).toHaveCount(0); await owner.close(); await viewerContext.close(); });