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:
Lucas Berger
2026-06-10 11:36:49 -04:00
parent 139ef00ed4
commit 9aa15c484b
+224
View File
@@ -108,6 +108,7 @@ const EDIT_OCCURRENCE: CalendarOccurrence = {
allDay: false, allDay: false,
location: 'Office', location: 'Office',
description: 'Weekly sync', description: 'Weekly sync',
hasRrule: false,
} }
// ── Import component (after mocks) ──────────────────────────────────────────── // ── Import component (after mocks) ────────────────────────────────────────────
@@ -487,6 +488,7 @@ const RECURRING_OCCURRENCE: CalendarOccurrence = {
allDay: false, allDay: false,
location: null, location: null,
description: null, description: null,
hasRrule: true,
// @ts-expect-error — recurrence is not on CalendarOccurrence type yet; the reset // @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) // effect reads it if present and defaults to 'none' when absent (WR-03, v1 comment)
recurrence: 'weekly', recurrence: 'weekly',
@@ -512,6 +514,7 @@ const LATE_OCCURRENCE: CalendarOccurrence = {
allDay: false, allDay: false,
location: null, location: null,
description: null, description: null,
hasRrule: false,
} }
describe('EventForm — Plan 03-12 gap closures', () => { describe('EventForm — Plan 03-12 gap closures', () => {
@@ -734,3 +737,224 @@ describe('EventForm — Plan 03-12 gap closures', () => {
expect(document.activeElement).toBe(lastElement) 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() }),
)
})
})
})