// spec: e2e/plans/chore-scheduler.plan.md — Section 5: Parent Mode Chore Card Schedule States import { test, expect } from '@playwright/test' import { createChild, createChoreTask, assignChore, setDaysSchedule, choreCard, todayDayIndex, nonTodayDayIndex, } from './helpers' const CHILD_NAME = 'ParentCardChild' const ALWAYS_ACTIVE = 'AlwaysActiveChore' const SCHEDULED_TODAY = 'ScheduledTodayChore' const NOT_SCHEDULED_TODAY = 'NotScheduledTodayChore' const EXPIRED_CHORE = 'ExpiredChore' const ANYTIME_CHORE = 'AnytimeChore' test.describe('Parent Mode — Chore Card Schedule States', () => { test.describe.configure({ mode: 'serial' }) let childId = '' let alwaysId = '' let todayId = '' let notTodayId = '' let expiredId = '' let anytimeId = '' test.beforeAll(async ({ request }) => { childId = await createChild(request, CHILD_NAME) alwaysId = await createChoreTask(request, ALWAYS_ACTIVE) todayId = await createChoreTask(request, SCHEDULED_TODAY) notTodayId = await createChoreTask(request, NOT_SCHEDULED_TODAY) expiredId = await createChoreTask(request, EXPIRED_CHORE) anytimeId = await createChoreTask(request, ANYTIME_CHORE) await assignChore(request, childId, [alwaysId, todayId, notTodayId, expiredId, anytimeId]) const today = todayDayIndex() const nonToday = nonTodayDayIndex() // ScheduledTodayChore: scheduled for today, deadline 23:59 (far future) await setDaysSchedule(request, childId, todayId, [today], { hour: 23, minute: 59 }) // NotScheduledTodayChore: scheduled for a non-today day await setDaysSchedule(request, childId, notTodayId, [nonToday], { hour: 8, minute: 0 }) // ExpiredChore: scheduled for today, deadline 00:01 (guaranteed past) await setDaysSchedule(request, childId, expiredId, [today], { hour: 0, minute: 1 }) // AnytimeChore: scheduled for today, no deadline await setDaysSchedule(request, childId, anytimeId, [today], { hasDeadline: false }) // AlwaysActiveChore: no schedule (already default) }) test.afterAll(async ({ request }) => { if (childId) await request.delete(`/api/child/${childId}`) for (const id of [alwaysId, todayId, notTodayId, expiredId, anytimeId]) { if (id) await request.delete(`/api/task/${id}`) } }) test('5.1 Chore with no schedule — normal appearance, no annotations', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, ALWAYS_ACTIVE) await card.waitFor() await expect(card).not.toHaveClass(/chore-inactive/) await expect(card.locator('.chore-stamp')).not.toBeVisible() await expect(card.locator('.due-label')).not.toBeVisible() }) test('5.2 Chore scheduled for today — shows "Due by" label, not grayed out', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, SCHEDULED_TODAY) await card.waitFor() await expect(card).not.toHaveClass(/chore-inactive/) await expect(card.locator('.due-label')).toHaveText('Due by 11:59 PM', { timeout: 10000 }) }) test('5.3 Chore not scheduled for today — grayed out in parent mode but visible', async ({ page, }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, NOT_SCHEDULED_TODAY) await card.waitFor() // Always visible in parent mode await expect(card).toBeVisible() // Grayed out because off-day await expect(card).toHaveClass(/chore-inactive/) // No TOO LATE badge (not expired — just off-day) await expect(card.getByText('TOO LATE')).not.toBeVisible() }) test('5.4 Chore with expired deadline — grayed out with TOO LATE badge', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, EXPIRED_CHORE) await card.waitFor() await expect(card).toBeVisible() await expect(card).toHaveClass(/chore-inactive/) await expect(card.getByText('TOO LATE')).toBeVisible() }) test('5.5 Chore with "Anytime" deadline — no "Due by" label, no TOO LATE badge', async ({ page, }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, ANYTIME_CHORE) await card.waitFor() await expect(card).not.toHaveClass(/chore-inactive/) await expect(card.locator('.due-label')).not.toBeVisible() await expect(card.getByText('TOO LATE')).not.toBeVisible() }) test('5.6 Kebab menu always accessible on grayed-out chores', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, NOT_SCHEDULED_TODAY) await card.waitFor() // Grayed out card still has Options button after clicking await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) await expect(card.getByRole('button', { name: 'Options' })).toBeVisible() await card.getByRole('button', { name: 'Options' }).click() await expect(page.getByRole('button', { name: 'Schedule' })).toBeVisible() // Schedule modal opens normally await page.getByRole('button', { name: 'Schedule' }).click() await expect(page.getByText('Schedule Chore')).toBeVisible() }) test('5.7 Expired chore cannot be triggered by tapping — no confirm dialog', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, EXPIRED_CHORE) await card.waitFor() // First click selects the card even when expired await card.click() await expect(card).toHaveClass(/item-ready/, { timeout: 3000 }) // Second click attempts to trigger — but expired chores are blocked in triggerTask() await card.click() // Confirm dialog should NOT appear for expired chores await expect(page.locator('.modal-dialog')).not.toBeVisible({ timeout: 1000 }) }) })