// spec: e2e/plans/chore-scheduler.plan.md — Section 3: Schedule Modal Every X Days Mode import { test, expect } from '@playwright/test' import { createChild, createChoreTask, assignChore, deleteSchedule, setIntervalSchedule, choreCard, openScheduleModal, setTimePicker, setDateInput, todayISO, futureDateISO, } from './helpers' const CHILD_NAME = 'ScheduleIntervalChild' const CHORE_NAME = 'ScheduleIntervalChore' test.describe('Schedule Modal — Every X Days mode', () => { test.describe.configure({ mode: 'serial' }) let childId = '' let choreId = '' test.beforeAll(async ({ request }) => { childId = await createChild(request, CHILD_NAME) choreId = await createChoreTask(request, CHORE_NAME) await assignChore(request, childId, [choreId]) }) test.afterAll(async ({ request }) => { if (childId) await request.delete(`/api/child/${childId}`) if (choreId) await request.delete(`/api/task/${choreId}`) }) test.beforeEach(async ({ request }) => { await deleteSchedule(request, childId, choreId) }) test('3.1 Interval stepper increments and decrements within 1–7', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, CHORE_NAME) await card.waitFor() await openScheduleModal(page, card) await page.getByRole('button', { name: 'Every X Days' }).click() // Default value is 1 await expect(page.locator('.stepper-value')).toHaveText('1') // Decrement is disabled at 1 const decBtn = page.locator('.stepper').getByRole('button').first() await expect(decBtn).toBeDisabled() // Increment 6 times → value becomes 7 const incBtn = page.locator('.stepper').getByRole('button').last() for (let i = 0; i < 6; i++) await incBtn.click() await expect(page.locator('.stepper-value')).toHaveText('7') // Increment is disabled at 7 await expect(incBtn).toBeDisabled() }) test("3.2 Anchor date field shows today's date by default", async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, CHORE_NAME) await card.waitFor() await openScheduleModal(page, card) await page.getByRole('button', { name: 'Every X Days' }).click() await expect(page.locator('input.date-input-hidden')).toHaveValue(todayISO()) }) test('3.3 Next occurrence preview label updates correctly', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, CHORE_NAME) await card.waitFor() await openScheduleModal(page, card) await page.getByRole('button', { name: 'Every X Days' }).click() // Set interval to 2 (click + once from default 1) await page.locator('.stepper').getByRole('button').last().click() await expect(page.locator('.stepper-value')).toHaveText('2') // Ensure anchor is today await setDateInput(page, todayISO()) // Next occurrence row should be visible await expect(page.locator('.next-occurrence-label')).toBeVisible() await expect(page.locator('.next-occurrence-label')).toContainText('Next occurrence:') }) test('3.4 Interval deadline "Anytime" toggle works', async ({ page }) => { await page.goto(`/parent/${childId}`) const card = choreCard(page, CHORE_NAME) await card.waitFor() await openScheduleModal(page, card) await page.getByRole('button', { name: 'Every X Days' }).click() // Time picker visible by default await expect(page.locator('.interval-time-row').locator('.time-picker-popover')).toBeVisible() // Click "Clear (Anytime)" await page .locator('.interval-time-row') .getByRole('button', { name: 'Clear (Anytime)' }) .click() await expect( page.locator('.interval-time-row').locator('.time-picker-popover'), ).not.toBeVisible() await expect(page.locator('.interval-time-row').getByText('Anytime')).toBeVisible() // Click "Set deadline" — time picker reappears await page.locator('.interval-time-row').getByRole('button', { name: 'Set deadline' }).click() await expect(page.locator('.interval-time-row').locator('.time-picker-popover')).toBeVisible() }) test('3.5 Save interval schedule — persists on reopen', async ({ page }) => { const anchor = futureDateISO(60) // 60 days ahead — well past today min constraint await page.goto(`/parent/${childId}`) const card = choreCard(page, CHORE_NAME) await card.waitFor() await openScheduleModal(page, card) await page.getByRole('button', { name: 'Every X Days' }).click() // Set interval to 3 (click + twice from default 1) const incBtn = page.locator('.stepper').getByRole('button').last() await incBtn.click() await incBtn.click() await expect(page.locator('.stepper-value')).toHaveText('3') // Set anchor date await setDateInput(page, anchor) // Set deadline to 04:30 PM const timePicker = page.locator('.interval-time-row').locator('.time-picker-popover') await setTimePicker(page, timePicker, 4, 30, 'PM') // Save await page.getByRole('button', { name: 'Save' }).click() await expect(page.locator('.modal-title')).not.toBeVisible() // Navigate fresh to reset card ready-state, then reopen to verify await page.goto(`/parent/${childId}`) await card.waitFor() await openScheduleModal(page, card) await expect(page.getByRole('button', { name: 'Every X Days' })).toHaveClass(/active/) await expect(page.locator('.stepper-value')).toHaveText('3') await expect(page.locator('input.date-input-hidden')).toHaveValue(anchor) await expect(page.locator('.interval-time-row').locator('.time-display')).toHaveText('04:30 PM') }) test('3.6 Dirty detection — changing interval enables Save', async ({ page, request }) => { await setIntervalSchedule(request, childId, choreId, 3, futureDateISO(30)) await page.goto(`/parent/${childId}`) const card = choreCard(page, CHORE_NAME) await card.waitFor() await openScheduleModal(page, card) // Save is disabled (no changes yet) await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() // Change interval from 3 to 5 (click + twice) const incBtn = page.locator('.stepper').getByRole('button').last() await incBtn.click() await incBtn.click() await expect(page.locator('.stepper-value')).toHaveText('5') // Save is now enabled await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled() }) })