Files
chrono/tests/js/unit/occurrenceIndex.test.ts
T
Sebastian 37fa7b911e refactor: event instances
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-03 19:10:00 -04:00

106 lines
3.6 KiB
TypeScript

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)
})
})