import { test, expect, type APIRequestContext, type Page } from '@playwright/test' const CHILD_NAME = 'RoutineChildModeChild' const ROUTINE_NAME = 'RoutineChildModeTest' const ROUTINE_POINTS = 10 const TASK_NAME_1 = 'Brush Teeth' const TASK_NAME_2 = 'Wash Face' // ── helpers ─────────────────────────────────────────────────────────────────── async function createChild(request: APIRequestContext, name: string): Promise { const pre = await request.get('/api/child/list') for (const c of (await pre.json()).children ?? []) { if (c.name === name) await request.delete(`/api/child/${c.id}`) } await request.put('/api/child/add', { data: { name, age: 8 } }) const list = await request.get('/api/child/list') return (await list.json()).children?.find((c: any) => c.name === name)?.id ?? '' } async function createRoutine( request: APIRequestContext, name: string, points: number, ): Promise { const pre = await request.get('/api/routine/list') for (const r of (await pre.json()).routines ?? []) { if (r.name === name) await request.delete(`/api/routine/${r.id}`) } const res = await request.put('/api/routine/add', { data: { name, points } }) return (await res.json()).routine?.id ?? '' } function routineSection(page: Page) { return page.locator('.child-list-container').filter({ has: page.locator('h3', { hasText: 'Routines' }), }) } // ── suite ───────────────────────────────────────────────────────────────────── test.describe('Routine child-mode flow', () => { test.describe.configure({ mode: 'serial' }) let childId = '' let routineId = '' test.beforeAll(async ({ request }) => { childId = await createChild(request, CHILD_NAME) routineId = await createRoutine(request, ROUTINE_NAME, ROUTINE_POINTS) await request.put(`/api/routine/${routineId}/item/add`, { data: { name: TASK_NAME_1 } }) await request.put(`/api/routine/${routineId}/item/add`, { data: { name: TASK_NAME_2 } }) await request.post(`/api/child/${childId}/assign-routine`, { data: { routine_id: routineId }, }) }) test.afterAll(async ({ request }) => { if (childId) await request.delete(`/api/child/${childId}`) if (routineId) await request.delete(`/api/routine/${routineId}`) }) // Intercept parentAuth so the router treats this session as child-mode (not parent-authenticated). test.beforeEach(async ({ page }) => { await page.addInitScript(() => { const orig = Storage.prototype.getItem Storage.prototype.getItem = function (key: string) { if (key === 'parentAuth') return null return orig.call(this, key) } }) }) // ── 1. Routine section visible ──────────────────────────────────────────── test('Routine section appears in child view', async ({ page }) => { await page.goto(`/child/${childId}`) const section = routineSection(page) await expect(section).toBeVisible({ timeout: 5000 }) await expect(section.locator('.item-card').filter({ hasText: ROUTINE_NAME })).toBeVisible() }) // ── 2. Overlay opens on two-click ───────────────────────────────────────── test('Clicking routine twice opens overlay with correct header', async ({ page }) => { await page.goto(`/child/${childId}`) const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME }) await card.waitFor({ state: 'visible' }) // First click enters ready state await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) // Second click opens overlay await card.click() const dialog = page.getByRole('dialog') await expect(dialog).toBeVisible({ timeout: 3000 }) await expect(dialog).toContainText('Did you complete') await expect(dialog).toContainText(ROUTINE_NAME) }) // ── 3. Task cards inside overlay ────────────────────────────────────────── test('Overlay shows task cards for each routine item', async ({ page }) => { await page.goto(`/child/${childId}`) const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME }) await card.waitFor({ state: 'visible' }) await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() const dialog = page.getByRole('dialog') await expect(dialog).toBeVisible({ timeout: 3000 }) await expect(dialog.getByText(TASK_NAME_1)).toBeVisible() await expect(dialog.getByText(TASK_NAME_2)).toBeVisible() }) // ── 4. Closing overlay with No ──────────────────────────────────────────── test('Clicking No closes the overlay without changing state', async ({ page }) => { await page.goto(`/child/${childId}`) const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME }) await card.waitFor({ state: 'visible' }) await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() const dialog = page.getByRole('dialog') await expect(dialog).toBeVisible({ timeout: 3000 }) await dialog.getByRole('button', { name: 'No' }).click() await expect(dialog).not.toBeVisible({ timeout: 3000 }) await expect(card.locator('.pending-stamp')).not.toBeVisible() }) // ── 5. Yes! confirms routine → PENDING stamp ────────────────────────────── test('Clicking Yes! marks routine as PENDING', async ({ page }) => { await page.goto(`/child/${childId}`) const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME }) await card.waitFor({ state: 'visible' }) await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() const dialog = page.getByRole('dialog') await expect(dialog).toBeVisible({ timeout: 3000 }) await dialog.getByRole('button', { name: 'Yes!' }).click() // Dialog closes await expect(dialog).not.toBeVisible({ timeout: 5000 }) // Reload to pick up the refreshed state await page.reload() const reloadedCard = routineSection(page) .locator('.item-card') .filter({ hasText: ROUTINE_NAME }) await expect(reloadedCard.locator('.pending-stamp')).toBeVisible({ timeout: 5000 }) }) // ── 6. Cancel dialog for pending routine ───────────────────────────────── test('Clicking a pending routine shows the cancel dialog', async ({ page }) => { await page.goto(`/child/${childId}`) // Routine should still be pending from the previous test const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME }) await card.waitFor({ state: 'visible' }) await expect(card.locator('.pending-stamp')).toBeVisible({ timeout: 5000 }) await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() const dialog = page.getByRole('dialog') await expect(dialog).toBeVisible({ timeout: 3000 }) await expect(dialog).toContainText('This routine is pending') }) // ── 7. Canceling pending routine removes PENDING stamp ──────────────────── test('Confirming cancel removes the PENDING stamp', async ({ page }) => { await page.goto(`/child/${childId}`) const card = routineSection(page).locator('.item-card').filter({ hasText: ROUTINE_NAME }) await card.waitFor({ state: 'visible' }) await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() const dialog = page.getByRole('dialog') await expect(dialog).toBeVisible({ timeout: 3000 }) await expect(dialog).toContainText('This routine is pending') // "Yes" cancels the pending confirmation await dialog.getByRole('button', { name: 'Yes' }).click() await expect(dialog).not.toBeVisible({ timeout: 5000 }) await page.reload() const reloadedCard = routineSection(page) .locator('.item-card') .filter({ hasText: ROUTINE_NAME }) await expect(reloadedCard.locator('.pending-stamp')).not.toBeVisible({ timeout: 5000 }) }) })