From b074b4abb21e9ded01327f5bd7e3b59c1d5b15e1 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 11 Jun 2026 02:11:44 -0400 Subject: [PATCH] =?UTF-8?q?feat(07-04):=20add=20lists.spec.ts=20=E2=80=94?= =?UTF-8?q?=20populated=20and=20empty-state=20tests=20(TEST-01)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Populated state: asserts 'Open list: E2E Grocery List' button visible + listitem count ≥1 + 'No lists yet' absent + no overflow - Empty state: routes /api/lists to 200 [] before goto, asserts 'No lists yet' + 'Tap + to create' visible + no overflow; unroutes after - Seeded DB not mutated — empty state is network-simulated (T-07-11 / D-06) - No absolute URLs; both states pass Rule 2 overflow check --- apps/pwa/e2e/lists.spec.ts | 121 +++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 apps/pwa/e2e/lists.spec.ts diff --git a/apps/pwa/e2e/lists.spec.ts b/apps/pwa/e2e/lists.spec.ts new file mode 100644 index 0000000..fc197ea --- /dev/null +++ b/apps/pwa/e2e/lists.spec.ts @@ -0,0 +1,121 @@ +/** + * 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 both device profiles automatically (playwright.config.ts matrix): + * iphone: iPhone 14 / WebKit / 390×844 + * pixel: Pixel 7 / Chromium / 412×915 + * + * 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() + + // Unroute so the mock does not leak to subsequent tests (T-07-11) + await page.unroute('/api/lists') + }) + + 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) + + await page.unroute('/api/lists') + }) +})