Files
funkpower f68f8a7c02 the year 50 is the year 50, not 1950
new Date(50, 0, 1) is 1950: the Date constructor reads a year of 0..99
as 1900 + n, and the setter does not. This calendar answers from the
year 1, so pascha() for an early year had been quietly reporting a
twentieth-century date.
2026-09-18 14:56:57 -04:00

227 lines
8.4 KiB
JavaScript

// The npm package, exercised as a consumer would use it, against the same
// answers the command line gives.
//
// npm/build.sh && node npm/test.mjs
//
// It imports by package path through a temporary node_modules link, so what is
// tested is the exports map and the built files, not a lucky relative import.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, symlinkSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const root = resolve(here, "..");
const cli = join(root, "target", "release", "typikon");
const stage = mkdtempSync(join(tmpdir(), "typikon-npm-"));
mkdirSync(join(stage, "node_modules"));
symlinkSync(join(root, "npm", "dist"), join(stage, "node_modules", "typikon"), "dir");
// Resolve "typikon" from inside the staged directory, so the exports map is
// what decides which build loads.
const { createRequire } = await import("node:module");
const req = createRequire(join(stage, "probe.js"));
const entry = req.resolve("typikon");
const typikon = await import(pathToFileURL(entry).href);
let failures = 0;
function check(name, fn) {
try {
fn();
console.log(` ok ${name}`);
} catch (e) {
failures++;
console.log(` FAIL ${name}\n ${e.message}`);
}
}
console.log(`typikon ${typikon.version()} from ${entry}`);
check("node resolves to the node build", () => {
assert.match(entry, /dist\/node\/typikon\.js$/);
});
check("a day is a plain object, not a handle", () => {
const day = typikon.describeIso("2026-04-12", { jurisdiction: "moscow" });
assert.equal(Object.getPrototypeOf(day), Object.prototype);
assert.equal(typeof day.free, "undefined");
assert.doesNotThrow(() => JSON.stringify(day));
});
check("it answers what the command line answers", () => {
for (const [iso, jurisdiction] of [
["2026-04-12", "moscow"],
["2026-01-07", "goarch"],
["2026-02-16", "ruthenian"],
["2025-12-25", "old"],
["2026-08-15", "antiochian"],
]) {
const expected = JSON.parse(
execFileSync(cli, ["--date", iso, "--jurisdiction", jurisdiction, "--json"], {
encoding: "utf8",
env: { ...process.env, XDG_CONFIG_HOME: stage, XDG_DATA_HOME: stage },
}),
);
const got = typikon.describeIso(iso, { jurisdiction });
assert.deepEqual(got, expected, `${iso} ${jurisdiction}`);
}
});
check("a year of days matches the command line, day for day", () => {
// Five dates prove the wiring; a year proves the engine crossed the boundary
// whole. The CLI emits an array for --days, so this is one process per
// jurisdiction rather than one per day.
for (const [jurisdiction, opts] of [
["moscow", {}],
["goarch", {}],
["ruthenian", { typikon: true }],
["finnish", {}],
]) {
const args = ["--date", "2026-01-01", "--days", "365", "--jurisdiction", jurisdiction, "--json"];
if (opts.typikon) args.push("--typikon");
const expected = JSON.parse(
execFileSync(cli, args, {
encoding: "utf8",
maxBuffer: 64 << 20,
env: { ...process.env, XDG_CONFIG_HOME: stage, XDG_DATA_HOME: stage },
}),
).days; // a run of days is wrapped, a single day is not
const got = typikon.week(new Date(2026, 0, 1), 365, { jurisdiction, ...opts });
assert.equal(got.length, expected.length, jurisdiction);
for (let i = 0; i < expected.length; i++) {
assert.deepEqual(got[i], expected[i], `${jurisdiction} ${expected[i].date}`);
}
}
});
check("a Date and an ISO string agree", () => {
// JS months are zero-based, which is the single easiest thing to get wrong
// on this boundary.
const a = typikon.describe(new Date(2026, 3, 12), { jurisdiction: "moscow" });
const b = typikon.describeIso("2026-04-12", { jurisdiction: "moscow" });
assert.deepEqual(a, b);
});
check("options are optional", () => {
const day = typikon.today();
assert.equal(day.jurisdiction, "new");
});
check("a week is consecutive and matches day by day", () => {
const days = typikon.week(new Date(2026, 3, 12), 7, { jurisdiction: "moscow" });
assert.equal(days.length, 7);
assert.equal(days[0].date, "2026-04-12");
assert.equal(days[6].date, "2026-04-18");
for (const d of days) {
assert.deepEqual(d, typikon.describeIso(d.date, { jurisdiction: "moscow" }));
}
});
check("pascha matches the published dates", () => {
const iso = (d) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
assert.equal(iso(typikon.pascha(2026, "old")), "2026-04-12");
assert.equal(iso(typikon.pascha(2026, "western")), "2026-04-05");
assert.equal(iso(typikon.pascha(2027, "old")), "2027-05-02");
});
check("a two-digit year is that year, not 19xx", () => {
// The Date constructor reads 0..99 as 1900 + n, which would have made the
// Paschalion for the year 50 answer for 1950 without a word.
assert.equal(typikon.pascha(50, "old").getFullYear(), 50);
assert.equal(typikon.pascha(99, "old").getFullYear(), 99);
assert.equal(typikon.pascha(100, "old").getFullYear(), 100);
});
check("every listed jurisdiction reckons", () => {
const all = typikon.jurisdictions();
assert.ok(all.length > 20);
for (const j of all) {
assert.equal(typeof j.key, "string");
assert.equal(typeof j.name, "string");
const day = typikon.describeIso("2026-04-12", { jurisdiction: j.key });
assert.equal(day.jurisdiction, j.key);
}
});
check("the discipline flag moves what is shown but not what binds", () => {
const law = typikon.describeIso("2026-03-04", { jurisdiction: "ruthenian", typikon: false });
const full = typikon.describeIso("2026-03-04", { jurisdiction: "ruthenian", typikon: true });
assert.notEqual(law.fast, full.fast);
assert.equal(law.binds, full.binds);
});
check("the usage can be overridden", () => {
const greek = typikon.describeIso("2025-12-09", { jurisdiction: "old", tradition: "greek" });
const slavic = typikon.describeIso("2025-12-09", { jurisdiction: "old", tradition: "slavic" });
assert.equal(greek.tradition, "greek");
assert.equal(slavic.tradition, "slavic");
assert.notEqual(greek.fast, slavic.fast);
});
check("a jurisdiction nobody keeps throws", () => {
assert.throws(() => typikon.describeIso("2026-04-12", { jurisdiction: "shire" }), /shire/);
});
check("a date outside the years throws", () => {
assert.throws(() => typikon.describeIso("not-a-date"));
assert.throws(() => typikon.describe(new Date(-5000, 0, 1)));
});
check("the web build is there", () => {
assert.match(req.resolve("typikon/web"), /dist\/web\/typikon\.js$/);
});
// The web build, driven the way a browser drives it. Node can hand it the
// bytes, which is enough to prove the glue and the module agree; what it
// cannot prove is that a browser will fetch them, and no test here can.
{
const webEntry = req.resolve("typikon/web");
const web = await import(pathToFileURL(webEntry).href);
const { readFileSync } = await import("node:fs");
await web.default({
module_or_path: readFileSync(join(dirname(webEntry), "typikon_bg.wasm")),
});
check("the web build answers what the node build answers", () => {
assert.deepEqual(
web.describeIso("2026-04-12", { jurisdiction: "moscow" }),
typikon.describeIso("2026-04-12", { jurisdiction: "moscow" }),
);
assert.equal(web.version(), typikon.version());
});
}
// TypeScript, if it is to hand. The Python bindings are gated on `mypy
// --strict`; this is the same gate, and the same reason: a published type is a
// promise, and a promise nobody checks drifts.
{
const tsc = process.env.TSC ?? "tsc";
let have = true;
try {
execFileSync(tsc, ["--version"], { stdio: "ignore" });
} catch {
have = false;
console.log(" skip typescript --strict (no tsc on PATH; set TSC=)");
}
if (have) {
const { copyFileSync } = await import("node:fs");
copyFileSync(join(here, "typecheck.ts"), join(stage, "typecheck.ts"));
check("the published types pass tsc --strict", () => {
execFileSync(
tsc,
["--strict", "--noEmit", "--target", "es2022", "--module", "nodenext",
"--moduleResolution", "nodenext", "typecheck.ts"],
{ cwd: stage, stdio: "pipe", encoding: "utf8" },
);
});
}
}
rmSync(stage, { recursive: true, force: true });
console.log(failures ? `\n${failures} failed` : "\nall passed");
process.exit(failures ? 1 : 0);