feat: Refactor ScheduleModal to support interval scheduling with date input and deadline toggle
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m30s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m30s
- Updated ChoreSchedule model to include anchor_date and interval_has_deadline. - Refactored interval scheduling logic in scheduleUtils to use anchor_date. - Introduced DateInputField component for selecting anchor dates in ScheduleModal. - Enhanced ScheduleModal to include a stepper for interval days and a toggle for deadline. - Updated tests for ScheduleModal and scheduleUtils to reflect new interval scheduling logic. - Added DateInputField tests to ensure proper functionality and prop handling.
This commit is contained in:
@@ -8,9 +8,11 @@ import type { ChildTask, ChoreSchedule } from '../common/models'
|
||||
// Mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
const mockSetChoreSchedule = vi.fn()
|
||||
const mockDeleteChoreSchedule = vi.fn()
|
||||
|
||||
vi.mock('@/common/api', () => ({
|
||||
setChoreSchedule: (...args: unknown[]) => mockSetChoreSchedule(...args),
|
||||
deleteChoreSchedule: (...args: unknown[]) => mockDeleteChoreSchedule(...args),
|
||||
parseErrorResponse: vi.fn().mockResolvedValue({ msg: 'error', code: 'ERR' }),
|
||||
}))
|
||||
|
||||
@@ -19,11 +21,17 @@ const ModalDialogStub = {
|
||||
template: '<div><slot /></div>',
|
||||
props: ['imageUrl', 'title', 'subtitle'],
|
||||
}
|
||||
const TimeSelectorStub = {
|
||||
template: '<div class="time-selector-stub" />',
|
||||
const TimePickerPopoverStub = {
|
||||
template: '<div class="time-picker-popover-stub" />',
|
||||
props: ['modelValue'],
|
||||
emits: ['update:modelValue'],
|
||||
}
|
||||
const DateInputFieldStub = {
|
||||
template:
|
||||
'<input class="date-input-field-stub" type="date" :value="modelValue" :min="min" @change="$emit(\'update:modelValue\', $event.target.value)" />',
|
||||
props: ['modelValue', 'min'],
|
||||
emits: ['update:modelValue'],
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -35,16 +43,21 @@ function mountModal(schedule: ChoreSchedule | null = null) {
|
||||
return mount(ScheduleModal, {
|
||||
props: { task: TASK, childId: CHILD_ID, schedule },
|
||||
global: {
|
||||
stubs: { ModalDialog: ModalDialogStub, TimeSelector: TimeSelectorStub },
|
||||
stubs: {
|
||||
ModalDialog: ModalDialogStub,
|
||||
TimePickerPopover: TimePickerPopoverStub,
|
||||
DateInputField: DateInputFieldStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockSetChoreSchedule.mockReset()
|
||||
mockDeleteChoreSchedule.mockReset()
|
||||
mockSetChoreSchedule.mockResolvedValue({ ok: true })
|
||||
mockDeleteChoreSchedule.mockResolvedValue({ ok: true })
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mode toggle
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -89,57 +102,59 @@ describe('ScheduleModal mode toggle', () => {
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Specific Days form — check/uncheck days
|
||||
// Specific Days form — chip toggles
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('ScheduleModal Specific Days form', () => {
|
||||
it('renders 7 day rows', () => {
|
||||
it('renders 7 day chips', () => {
|
||||
const w = mountModal()
|
||||
expect(w.findAll('.day-row').length).toBe(7)
|
||||
expect(w.findAll('.chip').length).toBe(7)
|
||||
})
|
||||
|
||||
it('no days are checked by default (no existing schedule)', () => {
|
||||
it('no chips are active by default (no existing schedule)', () => {
|
||||
const w = mountModal()
|
||||
const checkboxes = w.findAll<HTMLInputElement>('input[type="checkbox"]')
|
||||
expect(checkboxes.every((cb) => !(cb.element as HTMLInputElement).checked)).toBe(true)
|
||||
const chips = w.findAll('.chip')
|
||||
expect(chips.every((c) => !c.classes().includes('active'))).toBe(true)
|
||||
})
|
||||
|
||||
it('checking a day reveals a TimeSelector for that day', async () => {
|
||||
it('clicking a chip makes it active', async () => {
|
||||
const w = mountModal()
|
||||
expect(w.findAll('.time-selector-stub').length).toBe(0)
|
||||
const checkboxes = w.findAll('input[type="checkbox"]')
|
||||
await checkboxes[1].trigger('change') // Monday (idx 1)
|
||||
await nextTick()
|
||||
expect(w.findAll('.time-selector-stub').length).toBe(1)
|
||||
const chips = w.findAll('.chip')
|
||||
await chips[1].trigger('click') // Monday
|
||||
expect(chips[1].classes()).toContain('active')
|
||||
})
|
||||
|
||||
it('unchecking a day removes its TimeSelector', async () => {
|
||||
it('clicking an active chip deactivates it', async () => {
|
||||
const w = mountModal()
|
||||
const checkboxes = w.findAll('input[type="checkbox"]')
|
||||
await checkboxes[0].trigger('change') // check Sunday
|
||||
await nextTick()
|
||||
expect(w.findAll('.time-selector-stub').length).toBe(1)
|
||||
await checkboxes[0].trigger('change') // uncheck Sunday
|
||||
await nextTick()
|
||||
expect(w.findAll('.time-selector-stub').length).toBe(0)
|
||||
const chips = w.findAll('.chip')
|
||||
await chips[1].trigger('click') // activate
|
||||
await chips[1].trigger('click') // deactivate
|
||||
expect(chips[1].classes()).not.toContain('active')
|
||||
})
|
||||
|
||||
it('Save is disabled when no days checked (days mode)', () => {
|
||||
it('selecting a chip shows the default-deadline-row', async () => {
|
||||
const w = mountModal()
|
||||
expect(w.find('.default-deadline-row').exists()).toBe(false)
|
||||
await w.findAll('.chip')[0].trigger('click')
|
||||
expect(w.find('.default-deadline-row').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('Save is disabled when no days selected (isDirty is false)', () => {
|
||||
const w = mountModal()
|
||||
const saveBtn = w.find('.btn-primary')
|
||||
expect((saveBtn.element as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('Save is enabled after checking at least one day', async () => {
|
||||
it('Save is enabled after selecting at least one day', async () => {
|
||||
const w = mountModal()
|
||||
const checkboxes = w.findAll('input[type="checkbox"]')
|
||||
await checkboxes[2].trigger('change') // Tuesday
|
||||
await w.findAll('.chip')[2].trigger('click') // Tuesday
|
||||
await nextTick()
|
||||
const saveBtn = w.find('.btn-primary')
|
||||
expect((saveBtn.element as HTMLButtonElement).disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('pre-populates checked days from an existing schedule', () => {
|
||||
it('pre-populates active chips from an existing schedule', () => {
|
||||
const existing: ChoreSchedule = {
|
||||
id: 's1',
|
||||
child_id: CHILD_ID,
|
||||
task_id: TASK.id,
|
||||
mode: 'days',
|
||||
@@ -147,14 +162,20 @@ describe('ScheduleModal Specific Days form', () => {
|
||||
{ day: 1, hour: 8, minute: 0 },
|
||||
{ day: 4, hour: 9, minute: 30 },
|
||||
],
|
||||
interval_days: 2,
|
||||
anchor_weekday: 0,
|
||||
default_hour: 8,
|
||||
default_minute: 0,
|
||||
interval_days: 1,
|
||||
anchor_date: '',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 0,
|
||||
interval_minute: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
}
|
||||
const w = mountModal(existing)
|
||||
// Two TimeSelectorStubs should already be visible
|
||||
expect(w.findAll('.time-selector-stub').length).toBe(2)
|
||||
const chips = w.findAll('.chip')
|
||||
expect(chips[1].classes()).toContain('active') // Monday idx 1
|
||||
expect(chips[4].classes()).toContain('active') // Thursday idx 4
|
||||
})
|
||||
})
|
||||
|
||||
@@ -164,9 +185,8 @@ describe('ScheduleModal Specific Days form', () => {
|
||||
describe('ScheduleModal save — days mode', () => {
|
||||
it('calls setChoreSchedule with correct days payload', async () => {
|
||||
const w = mountModal()
|
||||
// Check Monday (idx 1)
|
||||
const checkboxes = w.findAll('input[type="checkbox"]')
|
||||
await checkboxes[1].trigger('change')
|
||||
// Select Monday (chip idx 1)
|
||||
await w.findAll('.chip')[1].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
await w.find('.btn-primary').trigger('click')
|
||||
@@ -177,20 +197,45 @@ describe('ScheduleModal save — days mode', () => {
|
||||
TASK.id,
|
||||
expect.objectContaining({
|
||||
mode: 'days',
|
||||
day_configs: [{ day: 1, hour: 8, minute: 0 }],
|
||||
day_configs: [{ day: 1, hour: expect.any(Number), minute: expect.any(Number) }],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('calls deleteChoreSchedule when 0 days selected on an existing schedule', async () => {
|
||||
const existing: ChoreSchedule = {
|
||||
id: 's1',
|
||||
child_id: CHILD_ID,
|
||||
task_id: TASK.id,
|
||||
mode: 'days',
|
||||
day_configs: [{ day: 2, hour: 8, minute: 0 }],
|
||||
default_hour: 8,
|
||||
default_minute: 0,
|
||||
interval_days: 1,
|
||||
anchor_date: '',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 0,
|
||||
interval_minute: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
}
|
||||
const w = mountModal(existing)
|
||||
await w.findAll('.chip')[2].trigger('click') // deselect Wednesday
|
||||
await nextTick()
|
||||
await w.find('.btn-primary').trigger('click')
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
expect(mockDeleteChoreSchedule).toHaveBeenCalledWith(CHILD_ID, TASK.id)
|
||||
})
|
||||
|
||||
it('emits "saved" after successful save', async () => {
|
||||
const w = mountModal()
|
||||
const checkboxes = w.findAll('input[type="checkbox"]')
|
||||
await checkboxes[0].trigger('change') // check Sunday
|
||||
await w.findAll('.chip')[0].trigger('click') // Sunday
|
||||
await nextTick()
|
||||
|
||||
await w.find('.btn-primary').trigger('click')
|
||||
await nextTick()
|
||||
await nextTick() // await the async save()
|
||||
await nextTick()
|
||||
|
||||
expect(w.emitted('saved')).toBeTruthy()
|
||||
})
|
||||
@@ -198,8 +243,7 @@ describe('ScheduleModal save — days mode', () => {
|
||||
it('does not emit "saved" on API error', async () => {
|
||||
mockSetChoreSchedule.mockResolvedValue({ ok: false })
|
||||
const w = mountModal()
|
||||
const checkboxes = w.findAll('input[type="checkbox"]')
|
||||
await checkboxes[0].trigger('change')
|
||||
await w.findAll('.chip')[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
await w.find('.btn-primary').trigger('click')
|
||||
@@ -210,22 +254,138 @@ describe('ScheduleModal save — days mode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interval form — stepper
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('ScheduleModal interval form — stepper', () => {
|
||||
async function openInterval(schedule: ChoreSchedule | null = null) {
|
||||
const w = mountModal(schedule)
|
||||
await w.findAll('.mode-btn')[1].trigger('click')
|
||||
return w
|
||||
}
|
||||
|
||||
it('default stepper value is 1', async () => {
|
||||
const w = await openInterval()
|
||||
expect(w.find('.stepper-value').text()).toBe('1')
|
||||
})
|
||||
|
||||
it('increment button increases the value', async () => {
|
||||
const w = await openInterval()
|
||||
await w.findAll('.stepper-btn')[1].trigger('click')
|
||||
expect(w.find('.stepper-value').text()).toBe('2')
|
||||
})
|
||||
|
||||
it('decrement button is disabled when value is 1', async () => {
|
||||
const w = await openInterval()
|
||||
const decrementBtn = w.findAll('.stepper-btn')[0]
|
||||
expect((decrementBtn.element as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('increment button is disabled when value is 7', async () => {
|
||||
const w = await openInterval()
|
||||
const plusBtn = w.findAll('.stepper-btn')[1]
|
||||
for (let i = 0; i < 6; i++) await plusBtn.trigger('click')
|
||||
expect((plusBtn.element as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('value does not go above 7', async () => {
|
||||
const w = await openInterval()
|
||||
const plusBtn = w.findAll('.stepper-btn')[1]
|
||||
for (let i = 0; i < 10; i++) await plusBtn.trigger('click')
|
||||
expect(w.find('.stepper-value').text()).toBe('7')
|
||||
})
|
||||
|
||||
it('pre-populates interval_days from existing schedule', async () => {
|
||||
const existing: ChoreSchedule = {
|
||||
id: 's2',
|
||||
child_id: CHILD_ID,
|
||||
task_id: TASK.id,
|
||||
mode: 'interval',
|
||||
day_configs: [],
|
||||
interval_days: 4,
|
||||
anchor_date: '2026-03-10',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 0,
|
||||
interval_minute: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
}
|
||||
const w = await openInterval(existing)
|
||||
expect(w.find('.stepper-value').text()).toBe('4')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interval form — Anytime toggle
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('ScheduleModal interval form — Anytime toggle', () => {
|
||||
async function openInterval() {
|
||||
const w = mountModal()
|
||||
await w.findAll('.mode-btn')[1].trigger('click')
|
||||
return w
|
||||
}
|
||||
|
||||
it('shows TimePickerPopover by default (hasDeadline=true)', async () => {
|
||||
const w = await openInterval()
|
||||
expect(w.find('.time-picker-popover-stub').exists()).toBe(true)
|
||||
expect(w.find('.anytime-label').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows "Clear (Anytime)" link by default', async () => {
|
||||
const w = await openInterval()
|
||||
const link = w.find('.interval-time-row .link-btn')
|
||||
expect(link.text()).toContain('Clear')
|
||||
})
|
||||
|
||||
it('clicking "Clear (Anytime)" hides TimePickerPopover and shows anytime label', async () => {
|
||||
const w = await openInterval()
|
||||
await w.find('.interval-time-row .link-btn').trigger('click')
|
||||
await nextTick()
|
||||
expect(w.find('.time-picker-popover-stub').exists()).toBe(false)
|
||||
expect(w.find('.anytime-label').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('clicking "Set deadline" after clearing restores TimePickerPopover', async () => {
|
||||
const w = await openInterval()
|
||||
await w.find('.interval-time-row .link-btn').trigger('click')
|
||||
await nextTick()
|
||||
await w.find('.interval-time-row .link-btn').trigger('click')
|
||||
await nextTick()
|
||||
expect(w.find('.time-picker-popover-stub').exists()).toBe(true)
|
||||
expect(w.find('.anytime-label').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('pre-populates interval_has_deadline=false from existing schedule', () => {
|
||||
const existing: ChoreSchedule = {
|
||||
id: 's3',
|
||||
child_id: CHILD_ID,
|
||||
task_id: TASK.id,
|
||||
mode: 'interval',
|
||||
day_configs: [],
|
||||
interval_days: 2,
|
||||
anchor_date: '',
|
||||
interval_has_deadline: false,
|
||||
interval_hour: 0,
|
||||
interval_minute: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
}
|
||||
const w = mountModal(existing)
|
||||
expect(w.find('.anytime-label').exists()).toBe(true)
|
||||
expect(w.find('.time-picker-popover-stub').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Save emits correct ChoreSchedule shape — interval mode
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('ScheduleModal save — interval mode', () => {
|
||||
it('calls setChoreSchedule with correct interval payload', async () => {
|
||||
it('calls setChoreSchedule with anchor_date and interval_has_deadline in payload', async () => {
|
||||
const w = mountModal()
|
||||
// Switch to interval mode
|
||||
await w.findAll('.mode-btn')[1].trigger('click')
|
||||
|
||||
// Set interval_days input to 3
|
||||
const intervalInput = w.find<HTMLInputElement>('.interval-input')
|
||||
await intervalInput.setValue(3)
|
||||
|
||||
// Set anchor_weekday to 2 (Tuesday)
|
||||
const anchorSelect = w.find<HTMLSelectElement>('.anchor-select')
|
||||
await anchorSelect.setValue(2)
|
||||
const plusBtn = w.findAll('.stepper-btn')[1]
|
||||
await plusBtn.trigger('click') // interval_days = 2
|
||||
await plusBtn.trigger('click') // interval_days = 3
|
||||
|
||||
await w.find('.btn-primary').trigger('click')
|
||||
await nextTick()
|
||||
@@ -236,38 +396,72 @@ describe('ScheduleModal save — interval mode', () => {
|
||||
expect.objectContaining({
|
||||
mode: 'interval',
|
||||
interval_days: 3,
|
||||
anchor_weekday: 2,
|
||||
anchor_date: expect.any(String),
|
||||
interval_has_deadline: true,
|
||||
interval_hour: expect.any(Number),
|
||||
interval_minute: expect.any(Number),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('Save enabled by default in interval mode (interval_days defaults to 2)', async () => {
|
||||
it('payload has interval_has_deadline=false when Anytime toggle is active', async () => {
|
||||
const w = mountModal()
|
||||
await w.findAll('.mode-btn')[1].trigger('click')
|
||||
await w.find('.interval-time-row .link-btn').trigger('click') // Clear
|
||||
await nextTick()
|
||||
await w.find('.btn-primary').trigger('click')
|
||||
await nextTick()
|
||||
expect(mockSetChoreSchedule).toHaveBeenCalledWith(
|
||||
CHILD_ID,
|
||||
TASK.id,
|
||||
expect.objectContaining({ interval_has_deadline: false }),
|
||||
)
|
||||
})
|
||||
|
||||
it('Save enabled by default in interval mode', async () => {
|
||||
const w = mountModal()
|
||||
await w.findAll('.mode-btn')[1].trigger('click')
|
||||
const saveBtn = w.find('.btn-primary')
|
||||
expect((saveBtn.element as HTMLButtonElement).disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('pre-populates interval fields from existing interval schedule', () => {
|
||||
it('pre-populates anchor_date in the DateInputField stub', () => {
|
||||
const existing: ChoreSchedule = {
|
||||
id: 's4',
|
||||
child_id: CHILD_ID,
|
||||
task_id: TASK.id,
|
||||
mode: 'interval',
|
||||
day_configs: [],
|
||||
interval_days: 4,
|
||||
anchor_weekday: 3,
|
||||
interval_hour: 14,
|
||||
interval_minute: 30,
|
||||
interval_days: 2,
|
||||
anchor_date: '2026-04-01',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 8,
|
||||
interval_minute: 0,
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
}
|
||||
const w = mountModal(existing)
|
||||
// Should default to interval mode
|
||||
expect(w.find('.interval-form').exists()).toBe(true)
|
||||
const input = w.find<HTMLInputElement>('.interval-input')
|
||||
expect(Number((input.element as HTMLInputElement).value)).toBe(4)
|
||||
const select = w.find<HTMLSelectElement>('.anchor-select')
|
||||
expect(Number((select.element as HTMLSelectElement).value)).toBe(3)
|
||||
const dateInput = w.find('.date-input-field-stub')
|
||||
expect((dateInput.element as HTMLInputElement).value).toBe('2026-04-01')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interval form — next occurrence label
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('ScheduleModal interval form — next occurrence', () => {
|
||||
it('shows next occurrences label in interval mode', async () => {
|
||||
const w = mountModal()
|
||||
await w.findAll('.mode-btn')[1].trigger('click')
|
||||
await nextTick()
|
||||
expect(w.find('.next-occurrence-label').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('next occurrences label contains "Next occurrences:"', async () => {
|
||||
const w = mountModal()
|
||||
await w.findAll('.mode-btn')[1].trigger('click')
|
||||
await nextTick()
|
||||
expect(w.find('.next-occurrence-label').text()).toContain('Next occurrences:')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -31,26 +31,35 @@ describe('toLocalISODate', () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('intervalHitsToday', () => {
|
||||
it('anchor day hits on itself', () => {
|
||||
// Wednesday = weekday 3
|
||||
const wednesday = new Date(2025, 0, 8) // Jan 8, 2025 is a Wednesday
|
||||
expect(intervalHitsToday(3, 2, wednesday)).toBe(true)
|
||||
expect(intervalHitsToday('2025-01-08', 2, wednesday)).toBe(true)
|
||||
})
|
||||
|
||||
it('anchor=Wednesday, interval=2, Thursday does NOT hit', () => {
|
||||
const thursday = new Date(2025, 0, 9)
|
||||
expect(intervalHitsToday(3, 2, thursday)).toBe(false)
|
||||
it('anchor=Wednesday(2025-01-08), interval=2, Thursday does NOT hit', () => {
|
||||
const thursday = new Date(2025, 0, 9) // 1 day after anchor, 1 % 2 !== 0
|
||||
expect(intervalHitsToday('2025-01-08', 2, thursday)).toBe(false)
|
||||
})
|
||||
|
||||
it('anchor=Wednesday, interval=2, Friday hits (2 days after anchor)', () => {
|
||||
const friday = new Date(2025, 0, 10)
|
||||
expect(intervalHitsToday(3, 2, friday)).toBe(true)
|
||||
it('anchor=Wednesday(2025-01-08), interval=2, Friday hits (2 days after anchor)', () => {
|
||||
const friday = new Date(2025, 0, 10) // 2 days after anchor, 2 % 2 === 0
|
||||
expect(intervalHitsToday('2025-01-08', 2, friday)).toBe(true)
|
||||
})
|
||||
|
||||
it('interval=1 hits every day', () => {
|
||||
// interval of 1 means every day; diffDays % 1 === 0 always
|
||||
// Note: interval_days=1 is disallowed in UI (min 2) but logic should still work
|
||||
const monday = new Date(2025, 0, 6)
|
||||
expect(intervalHitsToday(1, 1, monday)).toBe(true)
|
||||
const monday = new Date(2025, 0, 6) // anchor is that same Monday
|
||||
expect(intervalHitsToday('2025-01-06', 1, monday)).toBe(true)
|
||||
const tuesday = new Date(2025, 0, 7)
|
||||
expect(intervalHitsToday('2025-01-06', 1, tuesday)).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for a date before the anchor', () => {
|
||||
const beforeAnchor = new Date(2025, 0, 7) // Jan 7 is before Jan 8 anchor
|
||||
expect(intervalHitsToday('2025-01-08', 2, beforeAnchor)).toBe(false)
|
||||
})
|
||||
|
||||
it('empty anchor string treats localDate as the anchor (always hits)', () => {
|
||||
const today = new Date(2025, 0, 8)
|
||||
expect(intervalHitsToday('', 2, today)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -67,7 +76,8 @@ describe('isScheduledToday', () => {
|
||||
{ day: 3, hour: 9, minute: 30 }, // Wednesday
|
||||
],
|
||||
interval_days: 2,
|
||||
anchor_weekday: 0,
|
||||
anchor_date: '',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 0,
|
||||
interval_minute: 0,
|
||||
}
|
||||
@@ -89,7 +99,8 @@ describe('isScheduledToday', () => {
|
||||
mode: 'interval',
|
||||
day_configs: [],
|
||||
interval_days: 2,
|
||||
anchor_weekday: 3, // Wednesday
|
||||
anchor_date: '2025-01-08', // Wednesday
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 8,
|
||||
interval_minute: 0,
|
||||
}
|
||||
@@ -110,7 +121,8 @@ describe('getDueTimeToday', () => {
|
||||
mode: 'days',
|
||||
day_configs: [{ day: 1, hour: 8, minute: 30 }],
|
||||
interval_days: 2,
|
||||
anchor_weekday: 0,
|
||||
anchor_date: '',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 0,
|
||||
interval_minute: 0,
|
||||
}
|
||||
@@ -132,7 +144,8 @@ describe('getDueTimeToday', () => {
|
||||
mode: 'interval',
|
||||
day_configs: [],
|
||||
interval_days: 2,
|
||||
anchor_weekday: 3,
|
||||
anchor_date: '2025-01-08',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 14,
|
||||
interval_minute: 45,
|
||||
}
|
||||
@@ -147,13 +160,31 @@ describe('getDueTimeToday', () => {
|
||||
mode: 'interval',
|
||||
day_configs: [],
|
||||
interval_days: 2,
|
||||
anchor_weekday: 3,
|
||||
anchor_date: '2025-01-08',
|
||||
interval_has_deadline: true,
|
||||
interval_hour: 14,
|
||||
interval_minute: 45,
|
||||
}
|
||||
const thursday = new Date(2025, 0, 9)
|
||||
expect(getDueTimeToday(intervalSchedule, thursday)).toBeNull()
|
||||
})
|
||||
|
||||
it('interval mode with interval_has_deadline=false returns null (Anytime)', () => {
|
||||
const intervalSchedule: ChoreSchedule = {
|
||||
child_id: 'c1',
|
||||
task_id: 't1',
|
||||
mode: 'interval',
|
||||
day_configs: [],
|
||||
interval_days: 2,
|
||||
anchor_date: '2025-01-08',
|
||||
interval_has_deadline: false,
|
||||
interval_hour: 14,
|
||||
interval_minute: 45,
|
||||
}
|
||||
const wednesday = new Date(2025, 0, 8)
|
||||
// Even on a hit day, Anytime means no deadline → null
|
||||
expect(getDueTimeToday(intervalSchedule, wednesday)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user