mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-18 16:13:51 -06:00
Follow-up to #285. Three corrections that rode in with the Japanese locale. 1. The ja key-parity check failed the build whenever en.js had a key ja.js lacked. i18n.js lookup() is already `registry[lang]?.[key] ?? fallback[key] ?? key`, so an untranslated string renders in English and nothing is broken by a gap - the only effect was that adding any English string blocked CI until a Japanese translation existed. It also singled out one locale; es/fr/de/pt/hi/it were never held to it, and hi.js is a deliberate skeleton whose own header explains that every key falls back to English on purpose. Replaced with two checks over EVERY locale: a locale may not define a key that English does not (dead weight after a rename, and fixable by whoever touched the file, whatever language they speak), and coverage is printed rather than gated. Help tips still have to exist everywhere - that test is unchanged and still fails. Current coverage: ja 100%, es 65.5%, fr/de/pt 63.7%, it 59.5%, hi 0%. 2. Applying the strict half to all locales immediately found add_display.smart_tv_note living in fr, pt, it and de but not in en.js and referenced by no view - a string dropped from English that left four translations behind. Removed. 3. The new timezone test restored process.env.TZ by assigning the saved value back. When TZ was not set to begin with - which is the case in CI - that assigns undefined, which writes the STRING "undefined"; Node cannot parse it and silently falls back to UTC for the rest of the process. Every test after it in that file is date arithmetic. It now deletes the key when it was previously unset. 4. package-lock.json removed from .gitignore. server/package-lock.json is tracked, so the rule was inert, but it would silently prevent a future lockfile and works against the SBOM and reproducible-install setup added in #282. Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56 Co-authored-by: Dan Walters <dan.walters@bytetinker.net> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
114 lines
5.2 KiB
JavaScript
114 lines
5.2 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', () => {
|
|
// ⚠️ Record WHETHER it was set, not just its value. `process.env.TZ = undefined` writes the
|
|
// STRING "undefined", which Node cannot parse and silently resolves to UTC - changing the zone
|
|
// for every test that runs after this one in this file, all of which are date arithmetic.
|
|
const hadTz = Object.prototype.hasOwnProperty.call(process.env, 'TZ');
|
|
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 {
|
|
if (hadTz) process.env.TZ = originalTz;
|
|
else delete process.env.TZ;
|
|
}
|
|
});
|
|
|
|
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 (_) {} });
|