mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-18 16:13:51 -06:00
Three separate defects all showed up as "I saved a schedule and it moved". 1. expandSchedule emitted two different wire formats. A one-off returned start_time untouched - a naive wall-clock string, which the browser parses in its own zone - while a recurring instance returned cursor.toISOString(), an absolute instant derived by reading that same string in the SERVER's zone. The browser converted it back and the two conversions do not cancel: an operator in Tokyo saving Wednesday 20:00 got Thursday 10:00 from a US-Central server. Only recurring schedules were affected, which is why it looked intermittent. The calendar was also the odd one out. services/scheduler.js compares start_time as a string and never builds a Date from it, so expandSchedule was the only place in the codebase treating a wall-clock time as an instant - the drawing disagreed with playback as well as with the browser. 2. Saving an edit moved the schedule to today. The save handler rebuilds start_time from `pendingCreateDate || new Date()`, and editSchedule() restored only HH:MM - it never recorded the date being edited. Changing a colour on a block dated 5 Aug rewrote it to this week. No timezone mismatch required; this one hit everybody and silently altered stored data. 3. A cancelled drag-create leaked its date into the next schedule. pendingCreateDate was cleared only on a successful save, and the modal's two dismissers are inline onclick="...display='none'" attributes that cannot reach that scope. The date is now assigned by every path that OPENS the modal, so no dismissal path can leave a stale value behind. Tests assert the property rather than a literal: that the wire value is wall-clock and that the day survives a server/browser zone mismatch in BOTH directions. A literal-string assertion would pass just as happily with the bug present on a differently-configured CI box. Both new files fail on the parent commit and pass here; the schedule, calendar and timezone suites are green at 85 tests. 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>
99 lines
4.4 KiB
JavaScript
99 lines
4.4 KiB
JavaScript
'use strict';
|
|
|
|
// TWO DATE BUGS IN THE SCHEDULE MODAL, both timezone-independent, both silent.
|
|
//
|
|
// 1. Saving an EDIT moved the schedule to today. The save handler rebuilds start_time from
|
|
// `pendingCreateDate || new Date()`, and editSchedule() restored only HH:MM - it never recorded
|
|
// the date it was editing. Change a colour on a block dated 5 Aug and it jumped to this week.
|
|
//
|
|
// 2. A cancelled drag-create leaked its date into the NEXT schedule. pendingCreateDate was cleared
|
|
// only on a successful save, and the modal's two dismissers are inline
|
|
// onclick="...display='none'" attributes that cannot reach this scope.
|
|
//
|
|
// Both are fixed by assigning the date on every path that OPENS the modal, which is the invariant
|
|
// pinned here. There is no DOM in this runner, so this is a source-level check - the same technique
|
|
// i18n-keys-exist.test.js uses to police the views. It is deliberately loose about HOW the value is
|
|
// assigned and strict about WHETHER each opener assigns it, so a refactor that keeps the invariant
|
|
// keeps passing.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const SRC = fs.readFileSync(
|
|
path.join(__dirname, '..', '..', 'frontend', 'js', 'views', 'schedule.js'), 'utf8');
|
|
|
|
/*
|
|
* Comment lines are dropped before any of this is matched.
|
|
*
|
|
* The first version of this test failed against the FIXED code because the save handler carries a
|
|
* comment explaining why it does not use toISOString() - and the test matched the explanation.
|
|
* A source-level check has to look at code, not at prose about code.
|
|
*
|
|
* Whole-line comments only: stripping mid-line would mean parsing strings, and "https://" inside a
|
|
* URL literal looks exactly like a line comment to anything simpler than a parser.
|
|
*/
|
|
function withoutComments(text) {
|
|
return text
|
|
.split('\n')
|
|
.filter((line) => {
|
|
const t = line.trim();
|
|
return !(t.startsWith('//') || t.startsWith('*') || t.startsWith('/*'));
|
|
})
|
|
.join('\n');
|
|
}
|
|
|
|
/* The text of a brace-balanced block starting at `from`. */
|
|
function blockAt(from) {
|
|
const open = SRC.indexOf('{', from);
|
|
let depth = 0;
|
|
for (let i = open; i < SRC.length; i++) {
|
|
if (SRC[i] === '{') depth++;
|
|
else if (SRC[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) return SRC.slice(open, i + 1);
|
|
}
|
|
}
|
|
throw new Error('unbalanced block');
|
|
}
|
|
|
|
const ASSIGNS_DATE = /pendingCreateDate\s*=/;
|
|
|
|
test('editSchedule records the date of the schedule it is editing', () => {
|
|
const at = SRC.indexOf('function editSchedule(');
|
|
assert.notEqual(at, -1, 'editSchedule() not found - has it been renamed?');
|
|
assert.match(withoutComments(blockAt(at)), ASSIGNS_DATE,
|
|
'editSchedule() must set pendingCreateDate, or saving an edit moves the schedule to today');
|
|
});
|
|
|
|
test('opening the blank Add form clears any date left over from a cancelled drag', () => {
|
|
const at = SRC.indexOf("getElementById('addScheduleBtn').onclick");
|
|
assert.notEqual(at, -1, 'the addScheduleBtn handler was not found');
|
|
assert.match(withoutComments(blockAt(at)), ASSIGNS_DATE,
|
|
'the Add handler must reset pendingCreateDate, or a cancelled drag-create stamps the next schedule');
|
|
});
|
|
|
|
test('the date is declared before anything assigns it', () => {
|
|
// It used to be declared BELOW the drag handler that assigns it. That happens to work only
|
|
// because the handler runs later; moving either one would turn it into a ReferenceError at a
|
|
// moment nobody is watching.
|
|
const code = withoutComments(SRC);
|
|
const decl = code.search(/\blet\s+pendingCreateDate\b/);
|
|
assert.notEqual(decl, -1, 'pendingCreateDate declaration not found');
|
|
const firstUse = code.search(/pendingCreateDate\s*=\s*(?!null\b)/);
|
|
assert.ok(decl < firstUse || firstUse === -1,
|
|
'pendingCreateDate is assigned before it is declared');
|
|
});
|
|
|
|
test('the saved date is built from local parts, never toISOString', () => {
|
|
// toISOString() is UTC: for anyone west of Greenwich it stamps the previous day for part of the
|
|
// evening, which is the same class of bug as the one on the server side.
|
|
const at = SRC.indexOf("getElementById('saveScheduleBtn').onclick");
|
|
assert.notEqual(at, -1, 'the save handler was not found');
|
|
const body = withoutComments(blockAt(at));
|
|
assert.ok(!/toISOString\(\)/.test(body),
|
|
'the save handler must not derive a calendar date from toISOString()');
|
|
assert.match(body, /getFullYear\(\)/, 'the date should be assembled from local parts');
|
|
});
|