chore: fix tests

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-03 21:08:51 -04:00
parent bdac31e8e7
commit dea5a61811
4 changed files with 114 additions and 130 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import {
monthGridVisibleRange, monthGridVisibleRange,
shiftLocalDays, shiftLocalDays,
shiftLocalMonths, shiftLocalMonths,
} from '../../../src/utils/dateRanges' } from '../../../src/utils/date'
describe('calendar visible ranges', () => { describe('calendar visible ranges', () => {
it('uses a half-open range for a day span', () => { it('uses a half-open range for a day span', () => {
+89
View File
@@ -0,0 +1,89 @@
import { describe, expect, it } from 'vitest'
import type { EntityObject } from '../../../../chrono_manager/src/models/entity'
import { daysVisibleRange } from '../../../src/utils/date'
import { expandEntityInstances } from '../../../src/utils/recurrence'
function localIso(year: number, month: number, day: number, hour = 0): string {
return new Date(year, month, day, hour).toISOString()
}
function eventEntity(
identifier: string,
startsOn: string | null,
endsOn: string | null,
timeless = false,
): EntityObject {
return {
identifier,
properties: {
startsOn,
endsOn,
timeless,
},
} as unknown as EntityObject
}
describe('entity instance expansion', () => {
it('normalizes timestamps and duration once', () => {
const [instance] = expandEntityInstances(
eventEntity('meeting', localIso(2026, 5, 15, 9), localIso(2026, 5, 15, 10)),
)
expect(instance.durationMs).toBe(60 * 60 * 1000)
expect(new Date(instance.startMs).getHours()).toBe(9)
expect(new Date(instance.dayStartMs).getHours()).toBe(0)
expect(instance.instanceId).toBeNull()
expect(instance.multiDay).toBe(false)
})
it('produces no instance for an invalid start timestamp', () => {
expect(expandEntityInstances(eventEntity('invalid', 'not-a-date', null))).toEqual([])
})
it('marks an overnight event as multi-day', () => {
const [instance] = expandEntityInstances(
eventEntity('overnight', localIso(2026, 5, 15, 23), localIso(2026, 5, 16, 1)),
)
expect(instance.multiDay).toBe(true)
expect(instance.timeless).toBe(true)
})
it('honors an exclusive midnight end', () => {
const [instance] = expandEntityInstances(
eventEntity('all-day', localIso(2026, 5, 20), localIso(2026, 5, 21), true),
)
const end = new Date(instance.endMs)
expect([end.getDate(), end.getHours()]).toEqual([21, 0])
expect(new Date(instance.dayStartMs).getDate()).toBe(20)
expect(instance.multiDay).toBe(false)
})
it('treats UTC timestamps on timeless events as floating calendar dates', () => {
const [instance] = expandEntityInstances(
eventEntity('utc-all-day', '2025-03-27T00:00:00+00:00', '2025-03-28T00:00:00+00:00', true),
)
const start = new Date(instance.startMs)
expect([start.getMonth(), start.getDate(), start.getHours()]).toEqual([2, 27, 0])
expect(new Date(instance.endMs).getDate()).toBe(28)
})
it('gives a zero-duration event a queryable minimum duration', () => {
const [instance] = expandEntityInstances(
eventEntity('instant', localIso(2026, 5, 15, 9), null),
)
expect(instance.durationMs).toBe(1)
})
it('omits a non-recurring event outside the requested range', () => {
const instances = expandEntityInstances(
eventEntity('outside', localIso(2026, 6, 1, 9), localIso(2026, 6, 1, 10)),
daysVisibleRange(new Date(2026, 5, 15), 2),
)
expect(instances).toEqual([])
})
})
-105
View File
@@ -1,105 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { EntityObject } from '../../../../chrono_manager/src/models/entity'
import {
buildOccurrenceIndex,
occurrencesForDay,
occurrencesForRange,
} from '../../../src/utils/occurrenceIndex'
import { daysVisibleRange } from '../../../src/utils/dateRanges'
function localIso(year: number, month: number, day: number, hour = 0): string {
return new Date(year, month, day, hour).toISOString()
}
function eventEntity(
identifier: string,
startsOn: string | null,
endsOn: string | null,
timeless = false,
): EntityObject {
return {
identifier,
properties: {
startsOn,
endsOn,
timeless,
},
} as unknown as EntityObject
}
describe('occurrence index', () => {
it('normalizes and sorts event timestamps once', () => {
const later = eventEntity('later', localIso(2026, 5, 15, 13), localIso(2026, 5, 15, 14))
const earlier = eventEntity('earlier', localIso(2026, 5, 15, 9), localIso(2026, 5, 15, 10))
const invalid = eventEntity('invalid', 'not-a-date', null)
const index = buildOccurrenceIndex([later, invalid, earlier])
expect(index.sorted.map(occurrence => occurrence.entity.identifier)).toEqual(['earlier', 'later'])
expect(index.byEntity.get('earlier')?.[0]?.durationMs).toBe(60 * 60 * 1000)
expect(index.byEntity.has('invalid')).toBe(false)
})
it('places an overlapping event in each occupied day bucket', () => {
const overnight = eventEntity(
'overnight',
localIso(2026, 5, 15, 23),
localIso(2026, 5, 16, 1),
)
const index = buildOccurrenceIndex([overnight])
expect(occurrencesForDay(index, new Date(2026, 5, 15))).toHaveLength(1)
expect(occurrencesForDay(index, new Date(2026, 5, 16))).toHaveLength(1)
expect(index.sorted[0].multiDay).toBe(true)
expect(index.sorted[0].timeless).toBe(true)
})
it('honors an exclusive midnight end', () => {
const allDay = eventEntity(
'all-day',
localIso(2026, 5, 20),
localIso(2026, 5, 21),
true,
)
const index = buildOccurrenceIndex([allDay])
expect(occurrencesForDay(index, new Date(2026, 5, 20))).toHaveLength(1)
expect(occurrencesForDay(index, new Date(2026, 5, 21))).toHaveLength(0)
})
it('treats UTC timestamps on timeless events as floating calendar dates', () => {
const allDay = eventEntity(
'utc-all-day',
'2025-03-27T00:00:00+00:00',
'2025-03-28T00:00:00+00:00',
true,
)
const index = buildOccurrenceIndex([allDay])
expect(occurrencesForDay(index, new Date(2025, 2, 26))).toHaveLength(0)
expect(occurrencesForDay(index, new Date(2025, 2, 27))).toHaveLength(1)
expect(occurrencesForDay(index, new Date(2025, 2, 28))).toHaveLength(0)
expect(new Date(index.sorted[0].startMs).getHours()).toBe(0)
})
it('retrieves overlapping occurrences without duplicates', () => {
const spanning = eventEntity(
'spanning',
localIso(2026, 5, 14),
localIso(2026, 5, 18),
true,
)
const outside = eventEntity('outside', localIso(2026, 6, 1), localIso(2026, 6, 1, 1))
const index = buildOccurrenceIndex([outside, spanning])
const occurrences = occurrencesForRange(index, daysVisibleRange(new Date(2026, 5, 15), 2))
expect(occurrences.map(occurrence => occurrence.entity.identifier)).toEqual(['spanning'])
})
it('gives a zero-duration event a queryable minimum duration', () => {
const instant = eventEntity('instant', localIso(2026, 5, 15, 9), null)
const index = buildOccurrenceIndex([instant])
expect(index.sorted[0].durationMs).toBe(1)
expect(occurrencesForDay(index, new Date(2026, 5, 15))).toHaveLength(1)
})
})
+24 -24
View File
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import type { EntityObject } from '../../../../chrono_manager/src/models/entity' import type { EntityObject } from '../../../../chrono_manager/src/models/entity'
import type { EventOccurrence, EventMutation } from '../../../../chrono_manager/src/types/event' import type { EventOccurrence, EventMutation } from '../../../../chrono_manager/src/types/event'
import { daysVisibleRange } from '../../../src/utils/dateRanges' import { daysVisibleRange } from '../../../src/utils/date'
import { expandEntityOccurrences } from '../../../src/utils/recurrence' import { expandEntityInstances } from '../../../src/utils/recurrence'
function localIso(year: number, month: number, day: number, hour = 9): string { function localIso(year: number, month: number, day: number, hour = 9): string {
return new Date(year, month, day, hour).toISOString() return new Date(year, month, day, hour).toISOString()
@@ -34,14 +34,14 @@ describe('daily recurrence', () => {
precision: 'daily', precision: 'daily',
interval: 2, interval: 2,
}) })
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 5, 4), 6), daysVisibleRange(new Date(2026, 5, 4), 6),
) )
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([5, 7, 9]) expect(instances.map(instance => new Date(instance.startMs).getDate())).toEqual([5, 7, 9])
expect(occurrences.every(occurrence => occurrence.durationMs === 60 * 60 * 1000)).toBe(true) expect(instances.every(instance => instance.durationMs === 60 * 60 * 1000)).toBe(true)
expect(new Set(occurrences.map(occurrence => occurrence.key)).size).toBe(3) expect(new Set(instances.map(instance => instance.key)).size).toBe(3)
}) })
it('honors iteration limits before the requested range', () => { it('honors iteration limits before the requested range', () => {
@@ -51,12 +51,12 @@ describe('daily recurrence', () => {
interval: 1, interval: 1,
iterations: 3, iterations: 3,
}) })
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 5, 10), 3), daysVisibleRange(new Date(2026, 5, 10), 3),
) )
expect(occurrences).toEqual([]) expect(instances).toEqual([])
}) })
it('treats a date-only conclusion as an inclusive local date', () => { it('treats a date-only conclusion as an inclusive local date', () => {
@@ -66,12 +66,12 @@ describe('daily recurrence', () => {
interval: 1, interval: 1,
concludes: '2026-06-03', concludes: '2026-06-03',
}) })
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 5, 1), 5), daysVisibleRange(new Date(2026, 5, 1), 5),
) )
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([1, 2, 3]) expect(instances.map(instance => new Date(instance.startMs).getDate())).toEqual([1, 2, 3])
}) })
it('preserves local wall-clock time across daylight-saving changes', () => { it('preserves local wall-clock time across daylight-saving changes', () => {
@@ -80,12 +80,12 @@ describe('daily recurrence', () => {
precision: 'daily', precision: 'daily',
interval: 1, interval: 1,
}, {}, localIso(2026, 2, 7, 9)) }, {}, localIso(2026, 2, 7, 9))
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 2, 7), 3), daysVisibleRange(new Date(2026, 2, 7), 3),
) )
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getHours())).toEqual([9, 9, 9]) expect(instances.map(instance => new Date(instance.startMs).getHours())).toEqual([9, 9, 9])
}) })
it('preserves exclusive calendar-day duration for recurring timeless events', () => { it('preserves exclusive calendar-day duration for recurring timeless events', () => {
@@ -101,15 +101,15 @@ describe('daily recurrence', () => {
event.endsOn = '2026-03-08T00:00:00+00:00' event.endsOn = '2026-03-08T00:00:00+00:00'
event.timeless = true event.timeless = true
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 2, 7), 3), daysVisibleRange(new Date(2026, 2, 7), 3),
) )
expect(occurrences).toHaveLength(3) expect(instances).toHaveLength(3)
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getHours())).toEqual([0, 0, 0]) expect(instances.map(instance => new Date(instance.startMs).getHours())).toEqual([0, 0, 0])
expect(occurrences.map(occurrence => { expect(instances.map(instance => {
const end = new Date(occurrence.endMs) const end = new Date(instance.endMs)
return [end.getDate(), end.getHours()] return [end.getDate(), end.getHours()]
})).toEqual([[8, 0], [9, 0], [10, 0]]) })).toEqual([[8, 0], [9, 0], [10, 0]])
}) })
@@ -123,12 +123,12 @@ describe('weekly recurrence', () => {
interval: 1, interval: 1,
onDayOfWeek: [1, 3], onDayOfWeek: [1, 3],
}) })
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 5, 1), 10), daysVisibleRange(new Date(2026, 5, 1), 10),
) )
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([1, 3, 8, 10]) expect(instances.map(instance => new Date(instance.startMs).getDate())).toEqual([1, 3, 8, 10])
}) })
}) })
@@ -145,12 +145,12 @@ describe('recurrence mutations', () => {
mutationExclusion: true, mutationExclusion: true,
}, },
}) })
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 5, 1), 3), daysVisibleRange(new Date(2026, 5, 1), 3),
) )
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([1, 3]) expect(instances.map(instance => new Date(instance.startMs).getDate())).toEqual([1, 3])
}) })
it('moves and restyles a modified occurrence without duplicating it', () => { it('moves and restyles a modified occurrence without duplicating it', () => {
@@ -171,13 +171,13 @@ describe('recurrence mutations', () => {
color: '#abcdef', color: '#abcdef',
}, },
}) })
const occurrences = expandEntityOccurrences( const instances = expandEntityInstances(
entity, entity,
daysVisibleRange(new Date(2026, 5, 1), 5), daysVisibleRange(new Date(2026, 5, 1), 5),
) )
const moved = occurrences.find(occurrence => occurrence.occurrenceId === sourceId) const moved = instances.find(instance => instance.instanceId === sourceId)
expect(occurrences).toHaveLength(2) expect(instances).toHaveLength(2)
expect(new Date(moved!.startMs).getHours()).toBe(14) expect(new Date(moved!.startMs).getHours()).toBe(14)
expect(moved!.durationMs).toBe(2 * 60 * 60 * 1000) expect(moved!.durationMs).toBe(2 * 60 * 60 * 1000)
expect(moved!.label).toBe('Moved standup') expect(moved!.label).toBe('Moved standup')