chore: fix tests
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -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', () => {
|
||||||
|
|||||||
@@ -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([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user