The server has go test; the frontend had nothing, and the things most likely to break there are the ones hardest to see: the arithmetic behind the charts, a panel lost while shuffling tabs, a label that truncates on a phone none of us owns. There is no browser in this loop, so these are what can be checked without one. They read the real code rather than copying it. The app is one long IIFE with nothing exported, and adding a module system or a build step to make it testable would be a large change in service of a small one — so checks/extract.mjs reads src/app.js, brace-matches the declarations a check asks for, and evaluates them. Rename a function and it throws by name. A check quietly exercising a stale copy of the code would be worse than no check, and that is the failure mode this avoids. calendarGridStart is pulled out of renderCalendar as part of this. It is the one line of the month grid that is easy to get wrong and impossible to notice — a month starting on the week's first day needs no backing up, one starting the day before needs six — so it earns a name and a test. The width figures are estimates, not measurements: layout numbers come out of style.css so they cannot drift, text is sized from per-character advances, and the pass mark demands a few pixels of headroom because the estimate is only good to a few percent. They will catch a sixth tab or a longer label. They will not settle a two-pixel question, and nothing here replaces looking at a phone. No new dependencies: nodejs is already in the devShell for `node --check`, and checks/ sits outside src/ so it is not served with the app.
63 lines
2.6 KiB
JavaScript
63 lines
2.6 KiB
JavaScript
// The month grid behind the date button. Off-by-one week starts and month
|
|
// boundaries are how a hand-rolled calendar goes wrong, and none of it shows
|
|
// until somebody opens the month that breaks.
|
|
import { load } from "./extract.mjs";
|
|
import { suite, eq, report } from "./assert.mjs";
|
|
|
|
const app = load({ names: ["ymd", "calendarGridStart"] });
|
|
|
|
const MONDAY = 1, SUNDAY = 0;
|
|
const cells = (year, monthIdx, start) => {
|
|
const gs = app.calendarGridStart(new Date(year, monthIdx, 1), start);
|
|
return Array.from({ length: 42 }, (_, i) => {
|
|
const d = new Date(gs);
|
|
d.setDate(gs.getDate() + i);
|
|
return d;
|
|
});
|
|
};
|
|
|
|
suite("the grid starts on the locale's first weekday");
|
|
for (const [name, start] of [["Monday", MONDAY], ["Sunday", SUNDAY]]) {
|
|
const everyMonth = Array.from({ length: 12 }, (_, m) => cells(2026, m, start)[0].getDay());
|
|
eq(everyMonth.every(day => day === start), true,
|
|
`${name} start: all twelve months of 2026 open on a ${name}`);
|
|
}
|
|
|
|
suite("six rows, covering the month exactly once");
|
|
{
|
|
let alwaysSix = true, coversAll = true;
|
|
for (let m = 0; m < 12; m++) {
|
|
const cs = cells(2026, m, MONDAY);
|
|
if (cs.length !== 42) alwaysSix = false;
|
|
const inMonth = cs.filter(d => d.getMonth() === m).length;
|
|
if (inMonth !== new Date(2026, m + 1, 0).getDate()) coversAll = false;
|
|
}
|
|
eq(alwaysSix, true, "42 cells every month, so the panel never changes height");
|
|
eq(coversAll, true, "every day of every month appears exactly once");
|
|
}
|
|
|
|
suite("the months that catch people out");
|
|
{
|
|
// 1 Feb 2026 is a Sunday: under Monday weeks that needs six leading days from
|
|
// January, which is precisely what a naive `1 - getDay()` gets wrong.
|
|
const feb = cells(2026, 1, MONDAY);
|
|
eq(app.ymd(feb[0]), "2026-01-26", "Feb 2026 starts Sunday: the grid opens on 26 Jan");
|
|
eq(app.ymd(feb[6]), "2026-02-01", "…putting the 1st in the last column of row one");
|
|
eq(app.ymd(cells(2026, 1, SUNDAY)[0]), "2026-02-01",
|
|
"the same month under Sunday weeks needs no leading days at all");
|
|
eq(cells(2026, 7, MONDAY).filter(d => d.getMonth() === 7).length, 31,
|
|
"a 31-day month starting late in the week still fits");
|
|
eq(cells(2024, 1, MONDAY).filter(d => d.getMonth() === 1).length, 29, "Feb 2024 shows 29 days");
|
|
eq(cells(2026, 1, MONDAY).filter(d => d.getMonth() === 1).length, 28, "Feb 2026 shows 28");
|
|
}
|
|
|
|
suite("year boundaries");
|
|
{
|
|
eq(app.ymd(cells(2026, 0, MONDAY)[0]).startsWith("2025"), true,
|
|
"January's leading cells come from the previous December");
|
|
eq(app.ymd(cells(2026, 11, MONDAY)[41]).startsWith("2027"), true,
|
|
"December's trailing cells run into January");
|
|
}
|
|
|
|
export default report("calendar");
|