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.
119 lines
5.2 KiB
TypeScript
119 lines
5.2 KiB
TypeScript
/**
|
||
* 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.
|
||
});
|
||
});
|