test(06-06): add failing tests for end-tracking wiring and recurrence-bound control
- Add hasRrule to EDIT_OCCURRENCE, RECURRING_OCCURRENCE, LATE_OCCURRENCE fixtures - Add ALL_DAY_OCCURRENCE fixture for D-05 round-trip test - Add test cases: D-04 timed/all-day end-tracking on start change - Add test cases: D-05 all-day edit pre-fills inclusive end (no drift) - Add test cases: D-06 Ends control visibility, On date/After N times reveals - Add test cases: D-06 validation (count < 1), payload emission (count, never)
This commit is contained in:
@@ -108,6 +108,7 @@ const EDIT_OCCURRENCE: CalendarOccurrence = {
|
||||
allDay: false,
|
||||
location: 'Office',
|
||||
description: 'Weekly sync',
|
||||
hasRrule: false,
|
||||
}
|
||||
|
||||
// ── Import component (after mocks) ────────────────────────────────────────────
|
||||
@@ -487,6 +488,7 @@ const RECURRING_OCCURRENCE: CalendarOccurrence = {
|
||||
allDay: false,
|
||||
location: null,
|
||||
description: null,
|
||||
hasRrule: true,
|
||||
// @ts-expect-error — recurrence is not on CalendarOccurrence type yet; the reset
|
||||
// effect reads it if present and defaults to 'none' when absent (WR-03, v1 comment)
|
||||
recurrence: 'weekly',
|
||||
@@ -512,6 +514,7 @@ const LATE_OCCURRENCE: CalendarOccurrence = {
|
||||
allDay: false,
|
||||
location: null,
|
||||
description: null,
|
||||
hasRrule: false,
|
||||
}
|
||||
|
||||
describe('EventForm — Plan 03-12 gap closures', () => {
|
||||
@@ -734,3 +737,224 @@ describe('EventForm — Plan 03-12 gap closures', () => {
|
||||
expect(document.activeElement).toBe(lastElement)
|
||||
})
|
||||
})
|
||||
|
||||
// ── Plan 06-06: end-tracking wiring + recurrence-bound control (TDD RED) ─────
|
||||
|
||||
/**
|
||||
* All-day occurrence for D-05 round-trip verification.
|
||||
* The `end` field is the EXCLUSIVE end (the day AFTER the last day), as stored by the API.
|
||||
* The form must pre-fill the INCLUSIVE end (day before the exclusive end) so re-saving
|
||||
* does not grow the event.
|
||||
*/
|
||||
const ALL_DAY_OCCURRENCE: CalendarOccurrence = {
|
||||
id: 'allday-uid-001::2026-06-10',
|
||||
uid: 'allday-uid-001',
|
||||
calendarId: 1,
|
||||
calendarName: 'My Calendar',
|
||||
ownerUserId: 1,
|
||||
ownerName: 'Alice',
|
||||
color: '#4A90D9',
|
||||
isShared: false,
|
||||
title: 'All Day Event',
|
||||
start: '2026-06-10',
|
||||
end: '2026-06-11', // exclusive end (single-day event → end = start + 1 day)
|
||||
allDay: true,
|
||||
location: null,
|
||||
description: null,
|
||||
hasRrule: false,
|
||||
}
|
||||
|
||||
describe('EventForm — Plan 06-06 end-tracking + recurrence-bound', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEventFormOpen = true
|
||||
mockEventFormMode = 'create'
|
||||
mockEventFormUid = null
|
||||
})
|
||||
|
||||
// ── D-04: end-tracking — start date change ────────────────────────────────
|
||||
|
||||
it('D-04 timed: changing start date recomputes end to preserve 1h duration', () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
const dateInputs = document.querySelectorAll('input[type="date"]')
|
||||
const timeInputs = document.querySelectorAll('input[type="time"]')
|
||||
expect(dateInputs.length).toBeGreaterThanOrEqual(2)
|
||||
expect(timeInputs.length).toBeGreaterThanOrEqual(2)
|
||||
|
||||
// Set start to 2026-06-10 09:00, end to 2026-06-10 10:00 (1h span)
|
||||
fireEvent.change(dateInputs[0], { target: { value: '2026-06-10' } })
|
||||
fireEvent.change(timeInputs[0], { target: { value: '09:00' } })
|
||||
fireEvent.change(dateInputs[1], { target: { value: '2026-06-10' } })
|
||||
fireEvent.change(timeInputs[1], { target: { value: '10:00' } })
|
||||
|
||||
// Move start to 2026-06-11 09:00 → end should follow to 2026-06-11 10:00
|
||||
fireEvent.change(dateInputs[0], { target: { value: '2026-06-11' } })
|
||||
|
||||
const endDateAfter = (document.querySelectorAll('input[type="date"]')[1] as HTMLInputElement).value
|
||||
expect(endDateAfter).toBe('2026-06-11')
|
||||
})
|
||||
|
||||
it('D-04 timed: changing start time recomputes end to preserve duration', () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
const dateInputs = document.querySelectorAll('input[type="date"]')
|
||||
const timeInputs = document.querySelectorAll('input[type="time"]')
|
||||
|
||||
// Establish a 1h span on the same date
|
||||
fireEvent.change(dateInputs[0], { target: { value: '2026-06-10' } })
|
||||
fireEvent.change(timeInputs[0], { target: { value: '09:00' } })
|
||||
fireEvent.change(dateInputs[1], { target: { value: '2026-06-10' } })
|
||||
fireEvent.change(timeInputs[1], { target: { value: '10:00' } })
|
||||
|
||||
// Move start time to 14:00 → end should become 15:00 (still 1h)
|
||||
fireEvent.change(timeInputs[0], { target: { value: '14:00' } })
|
||||
|
||||
const endTimeAfter = (document.querySelectorAll('input[type="time"]')[1] as HTMLInputElement).value
|
||||
expect(endTimeAfter).toBe('15:00')
|
||||
})
|
||||
|
||||
it('D-04 all-day: changing start date preserves day-span', () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
// Toggle all-day ON
|
||||
const allDaySwitch = screen.getByRole('switch')
|
||||
fireEvent.click(allDaySwitch)
|
||||
|
||||
const dateInputs = document.querySelectorAll('input[type="date"]')
|
||||
// Set a 2-day span: start=2026-06-10, end=2026-06-11 (inclusive, 2 days)
|
||||
fireEvent.change(dateInputs[0], { target: { value: '2026-06-10' } })
|
||||
fireEvent.change(dateInputs[1], { target: { value: '2026-06-11' } })
|
||||
|
||||
// Move start to 2026-06-20 → end should become 2026-06-21 (same 2-day span)
|
||||
fireEvent.change(dateInputs[0], { target: { value: '2026-06-20' } })
|
||||
|
||||
const endDateAfter = (document.querySelectorAll('input[type="date"]')[1] as HTMLInputElement).value
|
||||
expect(endDateAfter).toBe('2026-06-21')
|
||||
})
|
||||
|
||||
// ── D-05: all-day edit round-trip — no drift ──────────────────────────────
|
||||
|
||||
it('D-05 all-day edit: pre-fills inclusive end (no +1 drift on round-trip)', () => {
|
||||
// ALL_DAY_OCCURRENCE has exclusive end '2026-06-11' (single day 2026-06-10)
|
||||
// The form should pre-fill 2026-06-10 (inclusive), not 2026-06-11 (exclusive)
|
||||
renderForm({
|
||||
mode: 'edit',
|
||||
uid: 'allday-uid-001',
|
||||
eventOccurrence: ALL_DAY_OCCURRENCE,
|
||||
})
|
||||
|
||||
const dateInputs = document.querySelectorAll('input[type="date"]')
|
||||
// End input should show the inclusive date 2026-06-10, not the exclusive 2026-06-11
|
||||
expect((dateInputs[1] as HTMLInputElement).value).toBe('2026-06-10')
|
||||
})
|
||||
|
||||
// ── D-06: recurrence bound control ───────────────────────────────────────
|
||||
|
||||
it('D-06: "Ends" control is hidden when recurrence is "None"', () => {
|
||||
renderForm({ mode: 'create' })
|
||||
// Ends label should not be visible when recurrence=none (default)
|
||||
expect(screen.queryByText('Ends')).toBeNull()
|
||||
})
|
||||
|
||||
it('D-06: "Ends" control appears when recurrence is set to weekly', () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
const recurrenceSelect = document.querySelector('#event-recurrence') as HTMLSelectElement
|
||||
fireEvent.change(recurrenceSelect, { target: { value: 'weekly' } })
|
||||
|
||||
expect(screen.getByText('Ends')).toBeDefined()
|
||||
})
|
||||
|
||||
it('D-06: selecting "On date" reveals a date input labeled "End date"', () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
const recurrenceSelect = document.querySelector('#event-recurrence') as HTMLSelectElement
|
||||
fireEvent.change(recurrenceSelect, { target: { value: 'weekly' } })
|
||||
|
||||
const endsSelect = document.querySelector('#recurrence-bound') as HTMLSelectElement
|
||||
expect(endsSelect).not.toBeNull()
|
||||
fireEvent.change(endsSelect, { target: { value: 'until' } })
|
||||
|
||||
expect(screen.getByText('End date')).toBeDefined()
|
||||
})
|
||||
|
||||
it('D-06: selecting "After N times" reveals a number input labeled "Occurrences"', () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
const recurrenceSelect = document.querySelector('#event-recurrence') as HTMLSelectElement
|
||||
fireEvent.change(recurrenceSelect, { target: { value: 'weekly' } })
|
||||
|
||||
const endsSelect = document.querySelector('#recurrence-bound') as HTMLSelectElement
|
||||
expect(endsSelect).not.toBeNull()
|
||||
fireEvent.change(endsSelect, { target: { value: 'count' } })
|
||||
|
||||
expect(screen.getByText('Occurrences')).toBeDefined()
|
||||
})
|
||||
|
||||
it('D-06: validation error when count < 1', async () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Event title'), { target: { value: 'Test' } })
|
||||
|
||||
const recurrenceSelect = document.querySelector('#event-recurrence') as HTMLSelectElement
|
||||
fireEvent.change(recurrenceSelect, { target: { value: 'weekly' } })
|
||||
|
||||
const endsSelect = document.querySelector('#recurrence-bound') as HTMLSelectElement
|
||||
fireEvent.change(endsSelect, { target: { value: 'count' } })
|
||||
|
||||
const countInput = document.querySelector('#recurrence-count') as HTMLInputElement
|
||||
expect(countInput).not.toBeNull()
|
||||
fireEvent.change(countInput, { target: { value: '0' } })
|
||||
|
||||
fireEvent.click(screen.getByText('Create Event'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Must be at least 1 occurrence')).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('D-06: payload includes recurrenceCount when bound=count and count >= 1', async () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Event title'), { target: { value: 'Weekly Event' } })
|
||||
|
||||
const recurrenceSelect = document.querySelector('#event-recurrence') as HTMLSelectElement
|
||||
fireEvent.change(recurrenceSelect, { target: { value: 'weekly' } })
|
||||
|
||||
const endsSelect = document.querySelector('#recurrence-bound') as HTMLSelectElement
|
||||
fireEvent.change(endsSelect, { target: { value: 'count' } })
|
||||
|
||||
const countInput = document.querySelector('#recurrence-count') as HTMLInputElement
|
||||
fireEvent.change(countInput, { target: { value: '5' } })
|
||||
|
||||
fireEvent.click(screen.getByText('Create Event'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ recurrenceCount: 5 }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('D-06: payload does NOT include recurrenceUntil/recurrenceCount when bound=never', async () => {
|
||||
renderForm({ mode: 'create' })
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Event title'), { target: { value: 'Weekly Event' } })
|
||||
|
||||
const recurrenceSelect = document.querySelector('#event-recurrence') as HTMLSelectElement
|
||||
fireEvent.change(recurrenceSelect, { target: { value: 'weekly' } })
|
||||
// Bound stays "Never" (default)
|
||||
|
||||
fireEvent.click(screen.getByText('Create Event'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEvent).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ recurrenceUntil: expect.anything() }),
|
||||
)
|
||||
expect(mockCreateEvent).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ recurrenceCount: expect.anything() }),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user