/** * lists.spec.ts — TEST-01 * * Route-specific state assertions for /lists (UI-SPEC Rules 4/5): * - Populated state: seeded 'E2E Grocery List' card visible, ListsEmptyState absent * - Empty state (network-simulated): 'No lists yet' + 'Tap + to create' visible * - No horizontal overflow in both states (Rule 2) * * The seeded populated state comes from global-setup (Plan 02): * - 'E2E Grocery List' (owner_id=1, is_shared=true) + list_shares row for user_id=1 * - Items: 'Milk' (rank 'a0'), 'Eggs' (rank 'a1') * * The empty state is simulated by routing /api/lists to return [] BEFORE navigation — * this keeps the seeded DB intact (T-07-11 / D-06 deterministic seed). * * Runs on all three device profiles automatically (playwright.config.ts matrix): * iphone: iPhone 14 / WebKit / 390×844 * pixel: Pixel 7 / Chromium / 412×915 * desktop: Desktop Chrome / Chromium / 1280×720 (all tests pass unchanged) * * Run: * pnpm --filter @familysync/pwa test:e2e * pnpm --filter @familysync/pwa exec playwright test --project=pixel lists.spec.ts */ import { test, expect } from '@playwright/test'; // ── Rule 5: Populated state ─────────────────────────────────────────────────── test.describe('Rule 5 — populated lists state', () => { test.beforeEach(async ({ page }) => { await page.goto('/lists'); // Wait for auth (DEV_AUTH_BYPASS) and lists content to load await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); }); test('seeded "E2E Grocery List" card is visible', async ({ page }) => { // ListCard renders a button with aria-label="Open list: " (ListCard.tsx). // This is the primary stable locator — prefer role+name over text content. const listCard = page.getByRole('button', { name: 'Open list: E2E Grocery List' }); await expect(listCard).toBeVisible(); }); test('at least one list item is present in the populated state', async ({ page }) => { // The content area has role="list" (ListsIndex.tsx) with ListCard children // that each have role="listitem". Assert ≥1 listitem when seeded. const listitems = page.getByRole('listitem'); await expect(listitems).not.toHaveCount(0); }); test('ListsEmptyState "No lists yet" is NOT present when lists are seeded', async ({ page }) => { // With the seeded list, the empty state must not appear. await expect(page.getByText('No lists yet')).toHaveCount(0); }); test('no horizontal overflow on populated /lists (Rule 2)', async ({ page }) => { const overflow = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, })); expect( overflow.scrollWidth, `scrollWidth (${overflow.scrollWidth}) must be ≤ clientWidth (${overflow.clientWidth}) on populated /lists`, ).toBeLessThanOrEqual(overflow.clientWidth); }); }); // ── Rule 5: Empty state (network-simulated) ─────────────────────────────────── test.describe('Rule 5 — lists empty state (network-simulated, seeded DB untouched)', () => { test('empty-state heading and body visible when /api/lists returns []', async ({ page }) => { // Route /api/lists to return an empty list BEFORE goto (Pattern 5). // This keeps the seeded DB intact — no DB mutation from the spec (T-07-11 / D-06). await page.route('/api/lists', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ lists: [] }), }), ); await page.goto('/lists'); await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); // ListsEmptyState renders "No lists yet" heading (ListsEmptyState.tsx) await expect(page.getByText('No lists yet')).toBeVisible(); // Body contains "Tap + to create your first shared list" — use partial match await expect(page.getByText(/Tap \+ to create/)).toBeVisible(); // No manual unroute (WR-05): Playwright gives each test a fresh page/context, so route // handlers do not leak across tests; a trailing unroute also never runs if an `expect` // above throws. }); test('no horizontal overflow in empty lists state (Rule 2)', async ({ page }) => { await page.route('/api/lists', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ lists: [] }), }), ); await page.goto('/lists'); await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible(); // Wait for empty state to render before measuring overflow await expect(page.getByText('No lists yet')).toBeVisible(); const overflow = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, })); expect( overflow.scrollWidth, `scrollWidth (${overflow.scrollWidth}) must be ≤ clientWidth (${overflow.clientWidth}) in empty lists state`, ).toBeLessThanOrEqual(overflow.clientWidth); // No manual unroute (WR-05): per-test context isolation handles route cleanup. }); });