37fa7b911e
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
106 lines
3.6 KiB
TypeScript
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)
|
|
})
|
|
})
|