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.
172 lines
5.5 KiB
TypeScript
172 lines
5.5 KiB
TypeScript
// spec: e2e/plans/chore-scheduler.plan.md — Section 11: Schedule Loading / Backward Compatibility
|
|
|
|
import { test, expect } from '@playwright/test'
|
|
import {
|
|
createChild,
|
|
createChoreTask,
|
|
assignChore,
|
|
deleteSchedule,
|
|
setIntervalSchedule,
|
|
choreCard,
|
|
openScheduleModal,
|
|
futureDateISO,
|
|
nonTodayDayIndex,
|
|
} from './helpers'
|
|
|
|
const CHILD_NAME = 'LoadCompatChild'
|
|
const CHORE_NAME = 'LoadCompatChore'
|
|
|
|
test.describe('Schedule Loading — Backward Compatibility', () => {
|
|
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('11.1 Schedule without "enabled" field defaults to enabled in modal', async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
// Create schedule via raw API call — omit the `enabled` field
|
|
await request.put(`/api/child/${childId}/task/${choreId}/schedule`, {
|
|
data: {
|
|
mode: 'days',
|
|
day_configs: [{ day: nonTodayDayIndex(), hour: 8, minute: 0 }],
|
|
default_hour: 8,
|
|
default_minute: 0,
|
|
default_has_deadline: true,
|
|
// enabled intentionally omitted — backend defaults to true
|
|
},
|
|
})
|
|
|
|
await page.goto(`/parent/${childId}`)
|
|
const card = choreCard(page, CHORE_NAME)
|
|
await card.waitFor()
|
|
await openScheduleModal(page, card)
|
|
|
|
// Toggle should default to "Enabled"
|
|
await expect(page.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
|
await expect(page.locator('.toggle-label')).toHaveText('Enabled')
|
|
})
|
|
|
|
test('11.2 Schedule with interval_has_deadline=false shows "Anytime" in modal', async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
await setIntervalSchedule(request, childId, choreId, 2, futureDateISO(30), {
|
|
hasDeadline: false,
|
|
})
|
|
|
|
await page.goto(`/parent/${childId}`)
|
|
const card = choreCard(page, CHORE_NAME)
|
|
await card.waitFor()
|
|
await openScheduleModal(page, card)
|
|
|
|
// "Every X Days" mode is active
|
|
await expect(page.getByRole('button', { name: 'Every X Days' })).toHaveClass(/active/)
|
|
|
|
// Anytime label is shown instead of time picker
|
|
await expect(page.locator('.interval-time-row').getByText('Anytime')).toBeVisible()
|
|
await expect(
|
|
page.locator('.interval-time-row').locator('.time-picker-popover'),
|
|
).not.toBeVisible()
|
|
})
|
|
|
|
test('11.3 Days schedule with per-day exception loads deadline rows correctly', async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
// Monday=08:00, Wednesday=08:00 (default), Friday=10:30 (exception)
|
|
// Use raw API to set up mixed schedule
|
|
await request.put(`/api/child/${childId}/task/${choreId}/schedule`, {
|
|
data: {
|
|
mode: 'days',
|
|
day_configs: [
|
|
{ day: 1, hour: 8, minute: 0 }, // Monday
|
|
{ day: 3, hour: 8, minute: 0 }, // Wednesday
|
|
{ day: 5, hour: 10, minute: 30 }, // Friday — exception from default 08:00
|
|
],
|
|
default_hour: 8,
|
|
default_minute: 0,
|
|
default_has_deadline: true,
|
|
enabled: true,
|
|
},
|
|
})
|
|
|
|
await page.goto(`/parent/${childId}`)
|
|
const card = choreCard(page, CHORE_NAME)
|
|
await card.waitFor()
|
|
await openScheduleModal(page, card)
|
|
|
|
// Monday, Wednesday, Friday chips should be active
|
|
await expect(page.locator('.day-chips').getByRole('button', { name: 'Mo' })).toHaveClass(
|
|
/active/,
|
|
)
|
|
await expect(page.locator('.day-chips').getByRole('button', { name: 'We' })).toHaveClass(
|
|
/active/,
|
|
)
|
|
await expect(page.locator('.day-chips').getByRole('button', { name: 'Fr' })).toHaveClass(
|
|
/active/,
|
|
)
|
|
|
|
// Default deadline shows 08:00 AM
|
|
await expect(page.locator('.default-deadline-row').locator('.time-display')).toHaveText(
|
|
'08:00 AM',
|
|
)
|
|
|
|
// Friday has its own time picker (exception) — "Reset to default" button visible
|
|
const frRow = page.locator('.exception-row').filter({
|
|
has: page.locator('.exception-day-name', { hasText: 'Friday' }),
|
|
})
|
|
await expect(frRow.getByRole('button', { name: 'Reset to default' })).toBeVisible()
|
|
await expect(frRow.locator('.time-display')).toHaveText('10:30 AM')
|
|
|
|
// Monday has no exception — shows default label
|
|
const moRow = page.locator('.exception-row').filter({
|
|
has: page.locator('.exception-day-name', { hasText: 'Monday' }),
|
|
})
|
|
await expect(moRow.locator('.default-label')).toBeVisible()
|
|
})
|
|
|
|
test('11.4 Interval schedule restores anchor date and interval on reopen', async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const anchorDate = '2026-06-15'
|
|
const intervalDays = 3
|
|
|
|
await setIntervalSchedule(request, childId, choreId, intervalDays, anchorDate, {
|
|
hour: 9,
|
|
minute: 0,
|
|
})
|
|
|
|
await page.goto(`/parent/${childId}`)
|
|
const card = choreCard(page, CHORE_NAME)
|
|
await card.waitFor()
|
|
await openScheduleModal(page, card)
|
|
|
|
// "Every X Days" mode is active
|
|
await expect(page.getByRole('button', { name: 'Every X Days' })).toHaveClass(/active/)
|
|
|
|
// Stepper shows 3
|
|
await expect(page.locator('.stepper-value')).toHaveText(`${intervalDays}`)
|
|
|
|
// Date input has the correct anchor date
|
|
await expect(page.locator('input.date-input-hidden')).toHaveValue(anchorDate)
|
|
})
|
|
})
|