mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: integrate ics calendar
This commit is contained in:
parent
b602093246
commit
63fb6cd45a
22 changed files with 845 additions and 193 deletions
114
test/calendarAvailability.test.js
Normal file
114
test/calendarAvailability.test.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { generateSlots, isBusyDuring, widenWindow } = require('../src/lib/calendarAvailability');
|
||||
|
||||
test('generateSlots: default 3 weeks produces one slot per day for 21 days', () => {
|
||||
const slots = generateSlots();
|
||||
assert.equal(slots.length, 21);
|
||||
});
|
||||
|
||||
test('generateSlots: caps weeks at 3 even if a larger value is requested', () => {
|
||||
const slots = generateSlots(10);
|
||||
assert.equal(slots.length, 21);
|
||||
});
|
||||
|
||||
test('generateSlots: every slot has upper strictly after lower', () => {
|
||||
for (const slot of generateSlots()) {
|
||||
assert.ok(slot.upper > slot.lower, `${slot.lower.toISOString()} .. ${slot.upper.toISOString()}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('generateSlots: weekday slots span 18:30-21:00 and weekend slots span 15:00-19:00, Paris-local', () => {
|
||||
for (const slot of generateSlots()) {
|
||||
const dow = new Date(slot.lower).toLocaleDateString('en-US', { timeZone: 'Europe/Paris', weekday: 'short' });
|
||||
const startLocal = slot.lower.toLocaleTimeString('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
|
||||
const endLocal = slot.upper.toLocaleTimeString('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
|
||||
const isWeekend = dow === 'Sat' || dow === 'Sun';
|
||||
if (isWeekend) {
|
||||
assert.equal(startLocal, '15:00');
|
||||
assert.equal(endLocal, '19:00');
|
||||
} else {
|
||||
assert.equal(startLocal, '18:30');
|
||||
assert.equal(endLocal, '21:00');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('isBusyDuring: true when a busy interval overlaps the slot', () => {
|
||||
const slotLower = new Date('2026-06-10T18:30:00Z');
|
||||
const slotUpper = new Date('2026-06-10T21:00:00Z');
|
||||
const intervals = [{ start: new Date('2026-06-10T19:00:00Z'), end: new Date('2026-06-10T20:00:00Z') }];
|
||||
assert.equal(isBusyDuring(intervals, slotLower, slotUpper), true);
|
||||
});
|
||||
|
||||
test('isBusyDuring: false when no interval overlaps the slot', () => {
|
||||
const slotLower = new Date('2026-06-10T18:30:00Z');
|
||||
const slotUpper = new Date('2026-06-10T21:00:00Z');
|
||||
const intervals = [{ start: new Date('2026-06-10T10:00:00Z'), end: new Date('2026-06-10T11:00:00Z') }];
|
||||
assert.equal(isBusyDuring(intervals, slotLower, slotUpper), false);
|
||||
});
|
||||
|
||||
test('isBusyDuring: false for an interval that ends exactly when the slot starts (half-open)', () => {
|
||||
const slotLower = new Date('2026-06-10T18:30:00Z');
|
||||
const slotUpper = new Date('2026-06-10T21:00:00Z');
|
||||
const intervals = [{ start: new Date('2026-06-10T17:00:00Z'), end: new Date('2026-06-10T18:30:00Z') }];
|
||||
assert.equal(isBusyDuring(intervals, slotLower, slotUpper), false);
|
||||
});
|
||||
|
||||
test('isBusyDuring: true for an all-day-style interval spanning the whole day', () => {
|
||||
const slotLower = new Date('2026-06-10T18:30:00Z');
|
||||
const slotUpper = new Date('2026-06-10T21:00:00Z');
|
||||
const intervals = [{ start: new Date('2026-06-10T00:00:00Z'), end: new Date('2026-06-11T00:00:00Z') }];
|
||||
assert.equal(isBusyDuring(intervals, slotLower, slotUpper), true);
|
||||
});
|
||||
|
||||
test('generateSlots: a custom slotConfig overrides the default hours', () => {
|
||||
const customConfig = {
|
||||
weekday: { startHour: 10, startMinute: 0, endHour: 11, endMinute: 30 },
|
||||
weekend: { startHour: 9, startMinute: 15, endHour: 10, endMinute: 0 },
|
||||
};
|
||||
for (const slot of generateSlots(3, customConfig)) {
|
||||
const dow = new Date(slot.lower).toLocaleDateString('en-US', { timeZone: 'Europe/Paris', weekday: 'short' });
|
||||
const startLocal = slot.lower.toLocaleTimeString('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
|
||||
const endLocal = slot.upper.toLocaleTimeString('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
|
||||
const isWeekend = dow === 'Sat' || dow === 'Sun';
|
||||
if (isWeekend) {
|
||||
assert.equal(startLocal, '09:15');
|
||||
assert.equal(endLocal, '10:00');
|
||||
} else {
|
||||
assert.equal(startLocal, '10:00');
|
||||
assert.equal(endLocal, '11:30');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('widenWindow: expands lower/upper by the margin on each side', () => {
|
||||
const lower = new Date('2026-06-10T18:30:00Z');
|
||||
const upper = new Date('2026-06-10T21:00:00Z');
|
||||
const widened = widenWindow(lower, upper, 30);
|
||||
assert.equal(widened.lower.toISOString(), '2026-06-10T18:00:00.000Z');
|
||||
assert.equal(widened.upper.toISOString(), '2026-06-10T21:30:00.000Z');
|
||||
});
|
||||
|
||||
test('widenWindow: zero or missing margin leaves the window unchanged', () => {
|
||||
const lower = new Date('2026-06-10T18:30:00Z');
|
||||
const upper = new Date('2026-06-10T21:00:00Z');
|
||||
assert.deepEqual(widenWindow(lower, upper, 0), { lower, upper });
|
||||
assert.deepEqual(widenWindow(lower, upper, undefined), { lower, upper });
|
||||
});
|
||||
|
||||
test('widenWindow + isBusyDuring: an event just outside the raw slot but inside the margin marks it busy', () => {
|
||||
const lower = new Date('2026-06-10T19:00:00Z');
|
||||
const upper = new Date('2026-06-10T20:30:00Z');
|
||||
// Ends 15 minutes before the slot starts — doesn't overlap the raw slot.
|
||||
const intervals = [{ start: new Date('2026-06-10T18:30:00Z'), end: new Date('2026-06-10T18:45:00Z') }];
|
||||
assert.equal(isBusyDuring(intervals, lower, upper), false);
|
||||
const { lower: checkLower, upper: checkUpper } = widenWindow(lower, upper, 30);
|
||||
assert.equal(isBusyDuring(intervals, checkLower, checkUpper), true);
|
||||
});
|
||||
|
||||
test('isBusyDuring: empty interval list is never busy', () => {
|
||||
const slotLower = new Date('2026-06-10T18:30:00Z');
|
||||
const slotUpper = new Date('2026-06-10T21:00:00Z');
|
||||
assert.equal(isBusyDuring([], slotLower, slotUpper), false);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { normalizeISO, slotDateParis, dayOfWeekParis } = require('../src/lib/calendarDates');
|
||||
const { normalizeISO, slotDateParis, dayOfWeekParis, parisWallClockToUTC } = require('../src/lib/calendarDates');
|
||||
|
||||
test('normalizeISO: collapses sub-minute noise to the same instant', () => {
|
||||
const a = normalizeISO('2026-06-10T18:30:12.345Z');
|
||||
|
|
@ -47,3 +47,30 @@ test('dayOfWeekParis: DST boundary days still resolve to Sunday (7)', () => {
|
|||
assert.equal(dayOfWeekParis('2026-03-29T00:30:00Z'), 7);
|
||||
assert.equal(dayOfWeekParis('2026-10-25T01:30:00Z'), 7);
|
||||
});
|
||||
|
||||
test('parisWallClockToUTC: winter (CET, UTC+1)', () => {
|
||||
const d = parisWallClockToUTC(2026, 1, 15, 18, 30);
|
||||
assert.equal(d.toISOString(), '2026-01-15T17:30:00.000Z');
|
||||
});
|
||||
|
||||
test('parisWallClockToUTC: summer (CEST, UTC+2)', () => {
|
||||
const d = parisWallClockToUTC(2026, 7, 15, 18, 30);
|
||||
assert.equal(d.toISOString(), '2026-07-15T16:30:00.000Z');
|
||||
});
|
||||
|
||||
test('parisWallClockToUTC: spring-forward DST boundary (2026-03-29, CET->CEST at 01:00 UTC)', () => {
|
||||
// Just before the jump: still CET (UTC+1).
|
||||
assert.equal(parisWallClockToUTC(2026, 3, 29, 1, 30).toISOString(), '2026-03-29T00:30:00.000Z');
|
||||
// Same evening, after the jump: already CEST (UTC+2).
|
||||
assert.equal(parisWallClockToUTC(2026, 3, 29, 18, 30).toISOString(), '2026-03-29T16:30:00.000Z');
|
||||
});
|
||||
|
||||
test('parisWallClockToUTC: fall-back DST boundary (2026-10-25, CEST->CET at 01:00 UTC)', () => {
|
||||
// Same evening, after the jump: already CET (UTC+1).
|
||||
assert.equal(parisWallClockToUTC(2026, 10, 25, 18, 30).toISOString(), '2026-10-25T17:30:00.000Z');
|
||||
});
|
||||
|
||||
test('parisWallClockToUTC and slotDateParis round-trip to the same local date', () => {
|
||||
const d = parisWallClockToUTC(2026, 3, 29, 18, 30);
|
||||
assert.equal(slotDateParis(d.toISOString()), '2026-03-29');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue