feat: add enable/disable toggle for chore scheduling in ScheduleModal
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m39s

- Introduced a toggle button to enable or disable chores in the scheduler.
- The toggle will be available in both types of schedulers.
- Added UI design proposal and considerations for mobile dimensions.
- Included E2E tests to ensure toggle state persistence and correct behavior in child view.
This commit is contained in:
2026-03-20 16:42:13 -04:00
parent db6e0a7ce8
commit ef9cb01d92
45 changed files with 3543 additions and 232 deletions

View File

@@ -0,0 +1,178 @@
// 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 17', 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()
})
})