// spec: e2e/plans/task-activated.plan.md import { test, expect, type APIRequestContext, type Locator, type Page } from '@playwright/test' const CHILD_NAME = 'ActivateRewardChild' const REWARD_NAME = 'TriggerTestReward' const REWARD_HELPER_NAME = 'TriggerRewardHelper' const REWARD_COST = 20 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 createReward( request: APIRequestContext, name: string, cost: number, ): Promise { const pre = await request.get('/api/reward/list') for (const r of (await pre.json()).rewards ?? []) { if (r.name === name) await request.delete(`/api/reward/${r.id}`) } await request.put('/api/reward/add', { data: { name, description: 'E2E test reward', cost } }) const list = await request.get('/api/reward/list') return (await list.json()).rewards?.find((r: any) => r.name === name)?.id ?? '' } async function getPoints(page: Page): Promise { const text = await page.locator('.points .value').textContent() return parseInt(text?.trim() ?? '0', 10) } async function activateItem(itemCard: Locator): Promise { await itemCard.click() await expect(itemCard).toHaveClass(/item-ready/, { timeout: 3000 }) await itemCard.click() } function rewardSection(page: Page): Locator { return page.locator('.child-list-container').filter({ has: page.locator('h3', { hasText: 'Rewards' }), }) } test.describe('Reward redemption', () => { test.describe.configure({ mode: 'serial' }) let childId = '' let rewardId = '' let rewardHelperId = '' test.beforeAll(async ({ request }) => { childId = await createChild(request, CHILD_NAME) rewardId = await createReward(request, REWARD_NAME, REWARD_COST) rewardHelperId = await createReward(request, REWARD_HELPER_NAME, REWARD_COST) await request.put(`/api/child/${childId}/set-rewards`, { data: { reward_ids: [rewardId, rewardHelperId] }, }) }) test.afterAll(async ({ request }) => { if (childId) await request.delete(`/api/child/${childId}`) if (rewardId) await request.delete(`/api/reward/${rewardId}`) if (rewardHelperId) await request.delete(`/api/reward/${rewardHelperId}`) }) test('Locked reward shows correct points needed text', async ({ page }) => { // Child starts at 0 pts; reward costs 20 → "20 more points" await page.goto(`/parent/${childId}`) const card = rewardSection(page).locator('.item-card').filter({ hasText: REWARD_NAME }) await card.waitFor({ state: 'visible' }) await expect(card.getByText(`${REWARD_COST} more points`)).toBeVisible() await expect(card.getByText('REWARD READY')).not.toBeVisible() }) test('Clicking a locked reward does not open confirmation dialog', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = rewardSection(page).locator('.item-card').filter({ hasText: REWARD_NAME }) await card.waitFor({ state: 'visible' }) // Two-click on locked reward await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await card.click() // triggerReward returns early when points_needed > 0 — no dialog await expect(page.locator('.points .value')).toHaveText('0') await expect(page.getByRole('button', { name: 'Yes' })).not.toBeVisible() }) test('Reward shows REWARD READY when child has sufficient points', async ({ page, request }) => { // Set child points to match reward cost await request.put(`/api/child/${childId}/edit`, { data: { points: REWARD_COST } }) await page.goto(`/parent/${childId}`) const card = rewardSection(page).locator('.item-card').filter({ hasText: REWARD_NAME }) await card.waitFor({ state: 'visible' }) await expect(card.getByText('REWARD READY')).toBeVisible() await expect(card.getByText(/more points/)).not.toBeVisible() }) test('Edit button appears on first click of a ready reward', async ({ page }) => { // Child has REWARD_COST pts (set in previous test) await page.goto(`/parent/${childId}`) const card = rewardSection(page).locator('.item-card').filter({ hasText: REWARD_NAME }) await card.waitFor({ state: 'visible' }) // Initially hidden await expect(card.getByTitle('Edit custom value')).not.toBeVisible() // First click: item enters ready state, edit button becomes visible await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await expect(card.getByTitle('Edit custom value')).toBeVisible() }) test('Edit button disappears when clicking a different reward card', async ({ page }) => { // Child has REWARD_COST pts (set in previous test); both rewards show REWARD READY await page.goto(`/parent/${childId}`) const section = rewardSection(page) const card = section.locator('.item-card').filter({ hasText: REWARD_NAME }) const helperCard = section.locator('.item-card').filter({ hasText: REWARD_HELPER_NAME }) await card.waitFor({ state: 'visible' }) await helperCard.waitFor({ state: 'visible' }) // First click on target card — edit button appears await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await expect(card.getByTitle('Edit custom value')).toBeVisible() // Click on a different reward card — target's edit button disappears await helperCard.click() await expect(card.getByTitle('Edit custom value')).not.toBeVisible() }) test('Cancel reward redemption — points not deducted', async ({ page }) => { // Child has REWARD_COST pts (from two tests ago; edit button test did not consume them) await page.goto(`/parent/${childId}`) const card = rewardSection(page).locator('.item-card').filter({ hasText: REWARD_NAME }) await card.waitFor({ state: 'visible' }) await expect(card.getByText('REWARD READY')).toBeVisible() const before = await getPoints(page) await activateItem(card) await expect(page.getByRole('button', { name: 'Yes' })).toBeVisible() await page.getByRole('button', { name: 'Cancel' }).click() await expect(page.getByRole('button', { name: 'Yes' })).not.toBeVisible() await expect(page.locator('.points .value')).toHaveText(String(before)) await expect(card.getByText('REWARD READY')).toBeVisible() }) test('Confirm reward redemption — points deducted and reward locks again', async ({ page }) => { // Child still has REWARD_COST pts after the cancelled redemption await page.goto(`/parent/${childId}`) const card = rewardSection(page).locator('.item-card').filter({ hasText: REWARD_NAME }) await card.waitFor({ state: 'visible' }) await expect(card.getByText('REWARD READY')).toBeVisible() const before = await getPoints(page) await activateItem(card) await expect(page.getByRole('button', { name: 'Yes' })).toBeVisible() await page.getByRole('button', { name: 'Yes' }).click() // Points reduced by reward cost await expect(page.locator('.points .value')).toHaveText(String(before - REWARD_COST)) // Reward is now locked again — shows points needed await expect(card.getByText(`${REWARD_COST} more points`)).toBeVisible() await expect(card.getByText('REWARD READY')).not.toBeVisible() }) })