Files
puppy-tracker/checks/run.mjs
T
Alexander Heldt 59cf567946 Add frontend checks, run from the repo
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.
2026-09-20 10:43:13 +00:00

54 lines
1.8 KiB
JavaScript

// Runs every check. Exits non-zero if any assertion failed.
//
// nix develop -c node checks/run.mjs
//
// These sit beside the Go tests rather than replacing them: the server has
// `go test`, and this is the frontend's half — the arithmetic behind the
// charts, the structure of the markup, and the width budgets that no one here
// can see. It needs no build step and no dependencies; node is already in the
// devShell for `node --check`.
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { readdirSync } from "node:fs";
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, "..");
let failed = 0;
// The frontend has no build step, so a syntax error would otherwise only show
// up in a browser nobody here is running.
for (const file of ["app.js", "sw.js"]) {
try {
execFileSync(process.execPath, ["--check", join(root, "src", file)], { stdio: "pipe" });
console.log(` ok src/${file} parses`);
} catch (e) {
console.log(` FAIL src/${file}\n${e.stderr?.toString() ?? e.message}`);
failed++;
}
}
try {
JSON.parse(readdirSync(join(root, "src")).includes("changelog.json")
? (await import("node:fs")).readFileSync(join(root, "src", "changelog.json"), "utf8")
: "[]");
console.log(" ok src/changelog.json parses");
} catch (e) {
console.log(` FAIL src/changelog.json: ${e.message}`);
failed++;
}
const suites = readdirSync(here)
.filter(f => f.endsWith(".mjs") && !["run.mjs", "extract.mjs", "assert.mjs"].includes(f))
.sort();
for (const file of suites) {
const mod = await import(join(here, file));
failed += mod.default ?? 0;
}
console.log(failed === 0
? `\nall checks passed (${suites.length} suites)`
: `\n${failed} assertion(s) FAILED`);
process.exit(failed === 0 ? 0 : 1);