// spec: e2e/plans/parent-notifications.plan.md §7 — approve chore import { test, expect, type APIRequestContext } from '@playwright/test' const CHILD_NAME = 'DigestApproveChoreChild' const CHORE_NAME = 'DigestApproveChoreChore' const CHORE_POINTS = 8 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 createTask( request: APIRequestContext, name: string, points: number, ): Promise { const pre = await request.get('/api/task/list') for (const t of (await pre.json()).tasks ?? []) { if (t.name === name) await request.delete(`/api/task/${t.id}`) } await request.put('/api/task/add', { data: { name, points, type: 'chore' } }) const list = await request.get('/api/task/list') return (await list.json()).tasks?.find((t: any) => t.name === name)?.id ?? '' } test.describe('Digest Action Token — Approve Chore', () => { test.describe.configure({ mode: 'serial' }) let childId = '' let choreId = '' let approveToken = '' test.beforeAll(async ({ request, playwright }) => { childId = await createChild(request, CHILD_NAME) choreId = await createTask(request, CHORE_NAME, CHORE_POINTS) await request.put(`/api/child/${childId}/set-tasks`, { data: { task_ids: [choreId], type: 'chore' }, }) // Set interval schedule so chore persists post-approval await request.put(`/api/child/${childId}/task/${choreId}/schedule`, { data: { mode: 'interval', interval_days: 1, anchor_date: new Date().toLocaleDateString('en-CA'), interval_has_deadline: false, }, }) await request.post(`/api/child/${childId}/confirm-chore`, { data: { task_id: choreId }, }) // Obtain a valid approve-chore token via the E2E test helper const tokenRes = await request.post('/api/admin/test/digest-token', { data: { child_id: childId, entity_id: choreId, entity_type: 'chore', action: 'approve', }, }) expect(tokenRes.status()).toBe(200) approveToken = (await tokenRes.json()).token }) test.afterAll(async ({ request }) => { if (childId) await request.delete(`/api/child/${childId}`) if (choreId) await request.delete(`/api/task/${choreId}`) }) // 7.1 Approve-chore token returns 302 redirect to correct ParentView URL test('Approve-chore token returns 302 redirect to correct ParentView URL', async ({ playwright, }) => { const unauthCtx = await playwright.request.newContext({ ignoreHTTPSErrors: true, baseURL: 'https://localhost:5173', }) const res = await unauthCtx.get(`/api/digest-action/${approveToken}`, { maxRedirects: 0, }) expect(res.status()).toBe(302) const location = res.headers()['location'] ?? '' expect(location).toContain(`/parent/${childId}`) expect(location).toContain(`scrollTo=${choreId}`) expect(location).toContain('entityType=chore') await unauthCtx.dispose() }) // 7.2 Approve-chore action is performed: pending confirmation removed, points awarded test('Approve-chore action is performed: points awarded and confirmation removed', async ({ request, }) => { // Execute the action via authenticated POST (GET only redirects, POST consumes the token) const execRes = await request.post(`/api/digest-action/${approveToken}`) expect(execRes.status()).toBe(200) const childRes = await request.get(`/api/child/list`) const children = (await childRes.json()).children ?? [] const child = children.find((c: any) => c.id === childId) expect(child).toBeTruthy() expect(child.points).toBeGreaterThanOrEqual(CHORE_POINTS) const pendingRes = await request.get('/api/pending-confirmations') const confirmations = (await pendingRes.json()).confirmations ?? [] const pending = confirmations.filter( (c: any) => c.child_id === childId && c.entity_id === choreId, ) // Should be no pending (status == 'pending') confirmation expect(pending.filter((c: any) => c.status === 'pending')).toHaveLength(0) }) })