feat(07-04): add lists.spec.ts — populated and empty-state tests (TEST-01)

- 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
This commit is contained in:
Lucas Berger
2026-06-11 02:11:44 -04:00
parent 17b625b6fe
commit b074b4abb2
+121
View File
@@ -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: <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()
// 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')
})
})