/**
* Unit tests for the pure helpers. No network, subprocess, or filesystem.
*/
import { assert, assertEquals, assertStringIncludes } from "@std/assert";
import { maxOf, minOf } from "@commonfabric/utils/math";
import { budgetStatus, ciDurationSub, clampInt, compactSpan, concDot, daysLabel, durationTag, escapeHtml, friendlyError, groupDigits, humanDur, humanSpan, jsonFromZip, landingHref, lighten, median, multiSparkline, pullRequestLinks, readBudget, sparkline, strip, thin, usd } from "./lib.ts";
import { artifactZip, bytes, makeZip } from "./test/artifact-zip.ts";
Deno.test("pullRequestLinks: each (#N) links to its PR, the rest to the given href", () => {
const link = (attributes: string, href: string, text: string) =>
`${text}`;
assertEquals(
pullRequestLinks('Revert "a (#12)" (#34)', "o/r", "https://run"),
link("", "https://run", "Revert "a ") + link(' class="pr"', "https://github.com/o/r/pull/12", "(#12)") +
link("", "https://run", "" ") + link(' class="pr"', "https://github.com/o/r/pull/34", "(#34)"),
);
assertEquals(
pullRequestLinks("a (#12) b", "o/r", "https://run", "t"),
link(' data-focus-key="t-0"', "https://run", "a ") +
link(' class="pr" data-focus-key="t-1"', "https://github.com/o/r/pull/12", "(#12)") +
link(' data-focus-key="t-2"', "https://run", " b"),
);
});
Deno.test("pullRequestLinks: a #N without parentheses stays part of the text", () => {
assertEquals(
pullRequestLinks("fix: follow-ups from #4401 ", "o/r", "https://run"),
`fix: follow-ups from #4401 <review>`,
);
});
Deno.test("landingHref: squash-merge trailing (#N) -> the PR", () => {
assertEquals(
landingHref("packages/ts-transformers tests (#4435)", "sha", "commonfabric/labs"),
"https://github.com/commonfabric/labs/pull/4435",
);
});
Deno.test("landingHref: merge commit -> the PR", () => {
assertEquals(landingHref("Merge pull request #12 from x/y", "sha", "o/r"), "https://github.com/o/r/pull/12");
});
Deno.test("landingHref: a mid-message #ref does NOT mislink -> commit page", () => {
assertEquals(landingHref("fix: follow-ups from #4401 review", "sha1", "o/r"), "https://github.com/o/r/commit/sha1");
});
Deno.test("landingHref: parses only the first line", () => {
assertEquals(landingHref("title (#7)\n\nbody mentioning (#999)", "sha", "o/r"), "https://github.com/o/r/pull/7");
});
Deno.test("concDot: only genuine failures are red", () => {
assertEquals(concDot("success", 1), "green");
assertEquals(concDot("success", 2), "gray"); // passed only on retry
assertEquals(concDot("failure", 1), "red");
assertEquals(concDot("timed_out", 1), "red");
assertEquals(concDot("cancelled", 1), "gray"); // non-verdict, not a failure
assertEquals(concDot(null, 1), "gray");
});
Deno.test("compactSpan: minutes, then hours, then days", () => {
assertEquals(compactSpan(0), "0m");
assertEquals(compactSpan(12 * 60_000), "12m");
assertEquals(compactSpan(3_600_000), "1h");
assertEquals(compactSpan(28 * 3_600_000), "28h");
assertEquals(compactSpan(48 * 3_600_000), "2d");
assertEquals(compactSpan(7 * 86_400_000), "7d");
assertEquals(compactSpan(-5_000), "0m");
});
Deno.test("groupDigits: separates thousands, rounding first", () => {
assertEquals(groupDigits(78101), "78,101");
assertEquals(groupDigits(999), "999");
assertEquals(groupDigits(1000), "1,000");
assertEquals(groupDigits(1234567), "1,234,567");
assertEquals(groupDigits(0), "0");
assertEquals(groupDigits(30.6), "31");
});
Deno.test("ciDurationSub: counts the runs in the time window or the most recent ones", () => {
assertEquals(ciDurationSub(25, 6), "median of 25 PR runs in 6h");
assertEquals(ciDurationSub(1, 6), "median of 1 PR run in 6h");
assertEquals(ciDurationSub(20), "median of last 20 PR runs");
assertEquals(ciDurationSub(1), "median of last 1 PR run");
assertEquals(ciDurationSub(0), "no passing PR runs");
assertEquals(ciDurationSub(0, 6), "no passing PR runs");
});
Deno.test("daysLabel: consistent 'x days' text", () => {
assertEquals(daysLabel(5), "5 days");
assertEquals(daysLabel(1), "1 day");
assertEquals(daysLabel(0), "<1 day");
});
Deno.test("humanSpan: days, else hours, else minutes", () => {
assertEquals(humanSpan(5 * 86_400_000), "5 days"); // >= 1 day -> daysLabel
assertEquals(humanSpan(86_400_000), "1 day");
assertEquals(humanSpan(8 * 3_600_000), "8 hours"); // sub-day -> hours
assertEquals(humanSpan(3_600_000), "1 hour");
assertEquals(humanSpan(30 * 60_000), "30 min"); // sub-hour -> minutes
assertEquals(humanSpan(0), "1 min");
});
Deno.test("humanDur", () => {
assertEquals(humanDur(5 * 60_000), "5m");
assertEquals(humanDur(90 * 60_000), "1h 30m");
assertEquals(humanDur(26 * 3_600_000), "1d 2h");
});
Deno.test("clampInt: clamps to bounds and falls back on empty/invalid", () => {
assertEquals(clampInt("50", 60, 10, 150), 50);
assertEquals(clampInt("500", 60, 10, 150), 150);
assertEquals(clampInt("5", 60, 10, 150), 10);
assertEquals(clampInt("", 60, 10, 150), 60);
assertEquals(clampInt(null, 60, 10, 150), 60);
assertEquals(clampInt("abc", 60, 10, 150), 60);
});
Deno.test("friendlyError: raw errors become short calm phrases", () => {
assertEquals(
friendlyError("GitHub API repos/o/r/actions/workflows/deno.yml/runs failed: error connecting to api"),
"source unreachable",
);
assertEquals(friendlyError("error sending request for url"), "source unreachable");
assertEquals(friendlyError("HTTP 404: Not Found"), "not found");
assertEquals(friendlyError("HTTP 403: rate limit exceeded"), "rate limit hit");
assertEquals(friendlyError("Bad credentials"), "auth failed");
assertEquals(friendlyError("GitHub API x: set GH_TOKEN or GITHUB_TOKEN"), "set GH_TOKEN");
assertEquals(friendlyError("something weird"), "temporarily unavailable");
});
Deno.test("budgetStatus / readBudget: good ≤ budget, warn ≤ 1.25×, bad beyond; unset never alarms", () => {
assertEquals(budgetStatus(50, 100), "good");
assertEquals(budgetStatus(100, 100), "good");
assertEquals(budgetStatus(125, 100), "warn");
assertEquals(budgetStatus(126, 100), "bad");
assertEquals(budgetStatus(9999, NaN), "good"); // unset budget never alarms
assert(Number.isNaN(readBudget(undefined)));
assert(Number.isNaN(readBudget("")));
assert(Number.isNaN(readBudget(" ")));
assertEquals(readBudget("100"), 100);
});
Deno.test("usd: whole dollars, or cents under a dollar", () => {
assertEquals(usd(0), "$0");
assertEquals(usd(0.45), "45¢");
assertEquals(usd(0.05), "5¢");
assertEquals(usd(0.004), "$0"); // rounds to 0¢ -> $0
assertEquals(usd(0.99), "99¢");
assertEquals(usd(0.999), "$1"); // 99.9¢ rounds up to a dollar
assertEquals(usd(1), "$1");
assertEquals(usd(1955.4), "$1955");
});
Deno.test("escapeHtml", () => {
assertEquals(escapeHtml(`&`), "<a href="x">&");
});
Deno.test("sparkline: highlight overdraws the trailing count on the same scale", () => {
const vals = Array.from({ length: 50 }, (_, i) => i); // strictly increasing
const svg = sparkline(vals, "#111", { count: 10, color: "#eee" });
const lines = [...svg.matchAll(/ {
const y = (pt: string) => parseFloat(pt.split(",")[1]);
const ysOf = (svg: string) => svg.match(/ 12, "recent runs should span most of the height");
// Without the spike, the same recent runs land on the same scale (spike-independent).
const clean = ysOf(sparkline([12, 10, 11, 12, 13], "#111", { count: 4, color: "#eee" }));
assertEquals(clean.slice(1), recent);
});
Deno.test("sparkline: a trimmed scale ignores ranked extremes without reordering points", () => {
const values = [101, 10, 0, 25, 100, 11, 1, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
const scale = { trim: 2, minValues: 20 };
const svg = sparkline(values, "#111", undefined, undefined, undefined, scale);
const ys = svg.match(/ parseFloat(point.split(",")[1]));
assertEquals(values, [101, 10, 0, 25, 100, 11, 1, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]); // sorting the scale does not mutate the line
assert(ys[0] < 0 && ys[4] < 0, "the two highest values should clip above the chart");
assert(ys[2] > 26 && ys[6] > 26, "the two lowest values should clip below the chart");
assert(ys[1] >= 0 && ys[1] <= 26 && ys[3] >= 0 && ys[3] <= 26, "20 points should enable trimming");
const short = Array.from({ length: 19 }, (_, value) => value);
assertEquals(
sparkline(short, "#111", undefined, undefined, undefined, scale),
sparkline(short, "#111"),
);
});
Deno.test("sparkline: highlight scaleAll keeps the whole series in view, still drawing the tail", () => {
const y = (pt: string) => parseFloat(pt.split(",")[1]);
const polys = (svg: string) => [...svg.matchAll(/ m[1].trim().split(" ").map(y));
// Same one-huge-old / modest-recent data, but scaleAll scales to every point.
const lines = polys(sparkline([100, 10, 11, 12, 13], "#111", { count: 4, color: "#eee", scaleAll: true }));
assertEquals(lines.length, 2); // base + highlighted tail
const [base, tail] = lines;
// The spike is now the series max, so it stays in view (not clipped past the top).
assert(base[0] >= 0, `spike should stay in view with scaleAll, got y=${base[0]}`);
assertEquals(base[0], minOf(base), "the spike is the highest point (smallest y)");
assertEquals(tail.length, 4); // the tail still covers the trailing 4 points
});
Deno.test("durationTag renders an auto-formatted span; sparkline itself has no label", () => {
const tag = durationTag(25 * 86_400_000);
assert(tag.includes(">25 days"), "the ms duration is auto-formatted to '25 days'");
assert(/position:absolute[^"]*left:1px[^"]*bottom:0/.test(tag), "pinned to the bottom-left corner");
assert(!/ element");
// sparkline no longer draws any label itself — it returns a bare