mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
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.
227 lines
8.4 KiB
JavaScript
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);
|