Files
familysync/apps/pwa/e2e/lists.spec.ts
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

119 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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: <name>" (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.
});
});