Files
familysync/apps/pwa/e2e/lists.spec.ts
T
Lucas Berger bfc49d199d chore(14-01): update spec headers + README for desktop profile, cosmetic CI step rename
- Updated calendar.spec.ts header to list all three profiles (iphone/pixel/desktop)
- Updated lists.spec.ts header to list all three profiles (iphone/pixel/desktop)
- Updated e2e/README.md preamble to add 'Desktop Chrome (1280x720)'
- Added --project=desktop example to README run-commands block
- Updated README full-suite command comment to name all three profiles
- Cosmetic: ci.yml step-name and comment updated to mention desktop (no plumbing change)
- Full suite verified: 85 passed, 5 skipped (3 desktop geometry + 2 parity guards), 0 failed
2026-06-12 08:23:10 -04:00

120 lines
5.3 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 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: <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.
});
});