Files
familysync/apps/pwa/src/lib/hydrateEvents.test.ts
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

150 lines
5.2 KiB
TypeScript

/**
* RED test stubs for hydrateEvents() — Wave 0 state.
*
* These tests encode the concrete contracts that Plan 03 (GREEN phase) must satisfy.
* The tests reference the not-yet-built module apps/pwa/src/lib/hydrateEvents.ts and
* are expected to fail until that module is implemented.
*
* Contracts locked here:
* 1. All-day occurrence (allDay:true, start 'YYYY-MM-DD') → start is Temporal.PlainDate
* 2. Timed occurrence (allDay:false) → start is Temporal.ZonedDateTime
* 3. Shared occurrence (isShared:true) → Schedule-X calendarId === 'shared'
* 4. Personal occurrence (isShared:false, ownerUserId:7, calendarId:99) →
* Schedule-X calendarId === '7' (String(ownerUserId)), NOT '99' (String(calendarId))
* This assertion locks the Plan 03 routing fix.
*/
import 'temporal-polyfill/global';
import { describe, it, expect } from 'vitest';
import { hydrateEvents } from './hydrateEvents.js';
// Minimal CalendarOccurrence shape for test purposes
interface TestOccurrence {
id: string;
uid: string;
calendarId: number;
calendarName: string;
ownerUserId: number;
color: string;
isShared: boolean;
title: string;
start: string;
end: string;
allDay: boolean;
location: string | null;
description: string | null;
}
function makeOccurrence(overrides: Partial<TestOccurrence>): TestOccurrence {
return {
id: 'test::2026-06-15',
uid: 'test-uid',
calendarId: 1,
calendarName: 'Test Calendar',
ownerUserId: 1,
color: '#4A90D9',
isShared: false,
title: 'Test Event',
start: '2026-06-15T10:00:00-04:00[America/New_York]',
end: '2026-06-15T11:00:00-04:00[America/New_York]',
allDay: false,
location: null,
description: null,
...overrides,
};
}
describe('hydrateEvents — RED stubs (Wave 0)', () => {
describe('Temporal type conversion', () => {
it('converts all-day occurrence (allDay:true) to Temporal.PlainDate for start and end', () => {
const occurrences = [
makeOccurrence({
allDay: true,
start: '2026-06-15',
end: '2026-06-15',
}),
];
const result = hydrateEvents(occurrences);
expect(result).toHaveLength(1);
const evt = result[0];
// Schedule-X requires Temporal.PlainDate for all-day events
expect(evt.start).toBeInstanceOf(Temporal.PlainDate);
expect(evt.end).toBeInstanceOf(Temporal.PlainDate);
});
it('converts an all-day exclusive DTEND to an inclusive last day for Schedule-X', () => {
// Single-day event: iCal DTSTART:24 / DTEND:25 (exclusive). Schedule-X end is
// inclusive, so a 1-day event must have start === end (renders on one day only).
const single = hydrateEvents([
makeOccurrence({ allDay: true, start: '2026-06-24', end: '2026-06-25' }),
])[0];
expect((single.start as Temporal.PlainDate).toString()).toBe('2026-06-24');
expect((single.end as Temporal.PlainDate).toString()).toBe('2026-06-24');
// Two-day event: DTSTART:26 / DTEND:28 (exclusive) → inclusive last day = 27.
const multi = hydrateEvents([
makeOccurrence({ allDay: true, start: '2026-06-26', end: '2026-06-28' }),
])[0];
expect((multi.start as Temporal.PlainDate).toString()).toBe('2026-06-26');
expect((multi.end as Temporal.PlainDate).toString()).toBe('2026-06-27');
});
it('converts timed occurrence (allDay:false) to Temporal.ZonedDateTime for start and end', () => {
const occurrences = [
makeOccurrence({
allDay: false,
start: '2026-06-15T10:00:00-04:00[America/New_York]',
end: '2026-06-15T11:00:00-04:00[America/New_York]',
}),
];
const result = hydrateEvents(occurrences);
expect(result).toHaveLength(1);
const evt = result[0];
expect(evt.start).toBeInstanceOf(Temporal.ZonedDateTime);
expect(evt.end).toBeInstanceOf(Temporal.ZonedDateTime);
});
});
describe('calendarId routing — Plan 03 contract', () => {
it('shared occurrence (isShared:true) gets Schedule-X calendarId "shared"', () => {
const occurrences = [
makeOccurrence({
isShared: true,
calendarId: 5,
ownerUserId: 2,
}),
];
const result = hydrateEvents(occurrences);
expect(result).toHaveLength(1);
// Shared-family events must route to the 'shared' calendar slot in Schedule-X config
expect(result[0].calendarId).toBe('shared');
});
it('personal occurrence (isShared:false) gets Schedule-X calendarId === String(ownerUserId), NOT String(calendarId)', () => {
// This assertion locks the routing decision: personal events are grouped by MEMBER (ownerUserId),
// not by DB calendarId. If two calendars belong to the same user, they share the same color slot.
const occurrences = [
makeOccurrence({
isShared: false,
calendarId: 99, // DB calendar row id
ownerUserId: 7, // DB user id — this is the correct Schedule-X key
}),
];
const result = hydrateEvents(occurrences);
expect(result).toHaveLength(1);
// Must be '7' (String(ownerUserId)), NOT '99' (String(calendarId))
expect(result[0].calendarId).toBe('7');
expect(result[0].calendarId).not.toBe('99');
});
});
});