acbe0ad5ca
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { EntityObject } from '../../../../chrono_manager/src/models/entity'
|
|
import type { EventOccurrence, EventMutation } from '../../../../chrono_manager/src/types/event'
|
|
import { daysVisibleRange } from '../../../src/utils/dateRanges'
|
|
import { expandEntityOccurrences } from '../../../src/utils/recurrence'
|
|
|
|
function localIso(year: number, month: number, day: number, hour = 9): string {
|
|
return new Date(year, month, day, hour).toISOString()
|
|
}
|
|
|
|
function recurringEntity(
|
|
pattern: EventOccurrence,
|
|
mutations: Record<string, Partial<EventMutation>> = {},
|
|
startsOn = localIso(2026, 5, 1),
|
|
): EntityObject {
|
|
return {
|
|
identifier: 'calendar:event-1',
|
|
properties: {
|
|
startsOn,
|
|
endsOn: new Date(new Date(startsOn).getTime() + 60 * 60 * 1000).toISOString(),
|
|
timeless: false,
|
|
label: 'Standup',
|
|
color: '#123456',
|
|
pattern,
|
|
mutations,
|
|
},
|
|
} as unknown as EntityObject
|
|
}
|
|
|
|
describe('daily recurrence', () => {
|
|
it('expands interval rules only inside the requested range', () => {
|
|
const entity = recurringEntity({
|
|
pattern: 'absolute',
|
|
precision: 'daily',
|
|
interval: 2,
|
|
})
|
|
const occurrences = expandEntityOccurrences(
|
|
entity,
|
|
daysVisibleRange(new Date(2026, 5, 4), 6),
|
|
)
|
|
|
|
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([5, 7, 9])
|
|
expect(occurrences.every(occurrence => occurrence.durationMs === 60 * 60 * 1000)).toBe(true)
|
|
expect(new Set(occurrences.map(occurrence => occurrence.key)).size).toBe(3)
|
|
})
|
|
|
|
it('honors iteration limits before the requested range', () => {
|
|
const entity = recurringEntity({
|
|
pattern: 'absolute',
|
|
precision: 'daily',
|
|
interval: 1,
|
|
iterations: 3,
|
|
})
|
|
const occurrences = expandEntityOccurrences(
|
|
entity,
|
|
daysVisibleRange(new Date(2026, 5, 10), 3),
|
|
)
|
|
|
|
expect(occurrences).toEqual([])
|
|
})
|
|
|
|
it('treats a date-only conclusion as an inclusive local date', () => {
|
|
const entity = recurringEntity({
|
|
pattern: 'absolute',
|
|
precision: 'daily',
|
|
interval: 1,
|
|
concludes: '2026-06-03',
|
|
})
|
|
const occurrences = expandEntityOccurrences(
|
|
entity,
|
|
daysVisibleRange(new Date(2026, 5, 1), 5),
|
|
)
|
|
|
|
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('preserves local wall-clock time across daylight-saving changes', () => {
|
|
const entity = recurringEntity({
|
|
pattern: 'absolute',
|
|
precision: 'daily',
|
|
interval: 1,
|
|
}, {}, localIso(2026, 2, 7, 9))
|
|
const occurrences = expandEntityOccurrences(
|
|
entity,
|
|
daysVisibleRange(new Date(2026, 2, 7), 3),
|
|
)
|
|
|
|
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getHours())).toEqual([9, 9, 9])
|
|
})
|
|
})
|
|
|
|
describe('weekly recurrence', () => {
|
|
it('expands selected ISO weekdays in chronological order', () => {
|
|
const entity = recurringEntity({
|
|
pattern: 'absolute',
|
|
precision: 'weekly',
|
|
interval: 1,
|
|
onDayOfWeek: [1, 3],
|
|
})
|
|
const occurrences = expandEntityOccurrences(
|
|
entity,
|
|
daysVisibleRange(new Date(2026, 5, 1), 10),
|
|
)
|
|
|
|
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([1, 3, 8, 10])
|
|
})
|
|
})
|
|
|
|
describe('recurrence mutations', () => {
|
|
it('removes excluded occurrences', () => {
|
|
const excludedId = localIso(2026, 5, 2)
|
|
const entity = recurringEntity({
|
|
pattern: 'absolute',
|
|
precision: 'daily',
|
|
interval: 1,
|
|
}, {
|
|
[excludedId]: {
|
|
mutationId: excludedId,
|
|
mutationExclusion: true,
|
|
},
|
|
})
|
|
const occurrences = expandEntityOccurrences(
|
|
entity,
|
|
daysVisibleRange(new Date(2026, 5, 1), 3),
|
|
)
|
|
|
|
expect(occurrences.map(occurrence => new Date(occurrence.startMs).getDate())).toEqual([1, 3])
|
|
})
|
|
|
|
it('moves and restyles a modified occurrence without duplicating it', () => {
|
|
const sourceId = localIso(2026, 5, 2)
|
|
const movedStart = localIso(2026, 5, 4, 14)
|
|
const entity = recurringEntity({
|
|
pattern: 'absolute',
|
|
precision: 'daily',
|
|
interval: 1,
|
|
iterations: 2,
|
|
}, {
|
|
[sourceId]: {
|
|
mutationId: sourceId,
|
|
mutationExclusion: false,
|
|
startsOn: movedStart,
|
|
endsOn: new Date(new Date(movedStart).getTime() + 2 * 60 * 60 * 1000).toISOString(),
|
|
label: 'Moved standup',
|
|
color: '#abcdef',
|
|
},
|
|
})
|
|
const occurrences = expandEntityOccurrences(
|
|
entity,
|
|
daysVisibleRange(new Date(2026, 5, 1), 5),
|
|
)
|
|
const moved = occurrences.find(occurrence => occurrence.occurrenceId === sourceId)
|
|
|
|
expect(occurrences).toHaveLength(2)
|
|
expect(new Date(moved!.startMs).getHours()).toBe(14)
|
|
expect(moved!.durationMs).toBe(2 * 60 * 60 * 1000)
|
|
expect(moved!.label).toBe('Moved standup')
|
|
expect(moved!.color).toBe('#abcdef')
|
|
})
|
|
})
|