Phase 8: Gitea CI — runner probe + PR gating jobs (fast-checks + api) #3
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user