screentinker/server/test/schedule-calendar-expansion.test.js
screentinker 70af0d0194
Merge Japanese localisation (#283) (#285)
* Add complete Japanese localization

* Checked and fixed translations

* Preserve selected calendar dates across timezones

---------

Co-authored-by: giyokun <gproux@gmail.com>
Co-authored-by: Dan Walters <dan.walters@bytetinker.net>
Co-authored-by: giyokun <giyokun@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 09:23:28 -05:00

109 lines
4.8 KiB
JavaScript

'use strict';
// The calendar is the operator's only view of what is scheduled, and it disagreed with the engine
// in both directions for the two most-used repeat presets.
//
// The old expansion stepped by the recurrence unit from the schedule's original start:
// - WEEKLY advanced a whole week at a time, so dayOfWeek never changed and a
// FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR rule matched only its start day. Created on a Monday it drew
// one event a week; created on a Saturday it drew nothing at all.
// - The walk began at the original start under a 366-iteration cap, so a schedule begun more than
// a year ago never reached the current week and drew nothing.
// Meanwhile the engine evaluates day-of-week directly, so those schedules ran Mon-Fri the whole
// time. Screens were switching content that the calendar said was not scheduled.
//
// The invariant: the calendar draws an event on every day the schedule actually fires.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('path');
const fs = require('fs');
const os = require('os');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'st-cal-'));
process.env.DATA_DIR = tmp;
process.env.JWT_SECRET = 'test-secret-calendar';
const { expandSchedule, parseCalendarDate } = require('../routes/schedules');
// A Monday-to-Sunday window well clear of the schedules' start dates.
const WEEK_START = new Date('2026-08-03T00:00:00'); // Monday
const WEEK_END = new Date('2026-08-09T23:59:59'); // Sunday
const mk = (recurrence, startISO, recurrenceEnd = null) => ({
id: 's', recurrence, recurrence_end: recurrenceEnd,
start_time: startISO,
end_time: new Date(new Date(startISO).getTime() + 60 * 60 * 1000).toISOString(),
});
const weekdaysOf = (events) => events.map(e => new Date(e.instance_start).getDay()).sort();
test('a date-only week anchor retains its calendar day west of UTC', () => {
const originalTz = process.env.TZ;
process.env.TZ = 'America/Los_Angeles';
try {
// new Date('2026-08-09') is UTC midnight, so it is still Saturday on a US server.
// The calendar date parser must retain the Sunday the browser selected.
assert.equal(new Date('2026-08-09').getDay(), 6, 'the legacy parser sees Saturday');
const selected = parseCalendarDate('2026-08-09');
assert.equal(selected.getFullYear(), 2026);
assert.equal(selected.getMonth(), 7);
assert.equal(selected.getDate(), 9);
assert.equal(selected.getDay(), 0, 'Sunday remains Sunday');
} finally {
process.env.TZ = originalTz;
}
});
test('THE BUG: a Mon-Fri rule draws five events, not one', () => {
const ev = expandSchedule(mk('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', '2026-07-27T09:00:00'), WEEK_START, WEEK_END);
assert.equal(ev.length, 5);
assert.deepEqual(weekdaysOf(ev), [1, 2, 3, 4, 5], 'Mon..Fri');
});
test('...and it does not matter which day the rule was created on', () => {
// Created on a Saturday, the old code drew nothing whatsoever.
const ev = expandSchedule(mk('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', '2026-08-01T09:00:00'), WEEK_START, WEEK_END);
assert.equal(ev.length, 5);
assert.deepEqual(weekdaysOf(ev), [1, 2, 3, 4, 5]);
});
test('a DAILY schedule older than a year still draws', () => {
// The 366-iteration cap meant the walk never reached the visible window.
const ev = expandSchedule(mk('FREQ=DAILY', '2024-05-01T09:00:00'), WEEK_START, WEEK_END);
assert.equal(ev.length, 7, 'every day of the week');
});
test('WEEKLY without byDay still means "the same weekday as the start"', () => {
const ev = expandSchedule(mk('FREQ=WEEKLY', '2026-07-27T09:00:00'), WEEK_START, WEEK_END); // a Monday
assert.equal(ev.length, 1);
assert.deepEqual(weekdaysOf(ev), [1]);
});
test('a DAILY interval is honoured rather than drawn every day', () => {
const ev = expandSchedule(mk('FREQ=DAILY;INTERVAL=2', '2026-08-03T09:00:00'), WEEK_START, WEEK_END);
assert.equal(ev.length, 4, 'Mon, Wed, Fri, Sun');
});
test('recurrence_end stops the drawing', () => {
const ev = expandSchedule(
mk('FREQ=DAILY', '2026-07-27T09:00:00', '2026-08-05T23:59:59'), WEEK_START, WEEK_END);
assert.equal(ev.length, 3, 'Mon, Tue, Wed then it ends');
});
test('a one-off schedule is unaffected', () => {
const ev = expandSchedule(
{ id: 's', recurrence: null, start_time: '2026-08-05T09:00:00', end_time: '2026-08-05T10:00:00' },
WEEK_START, WEEK_END);
assert.equal(ev.length, 1);
});
test('each drawn event keeps the schedule duration', () => {
const ev = expandSchedule(mk('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', '2026-07-27T09:00:00'), WEEK_START, WEEK_END);
for (const e of ev) {
const mins = (new Date(e.instance_end) - new Date(e.instance_start)) / 60000;
assert.equal(mins, 60);
}
});
test.after(() => { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {} });