/** * The page behind the ci tile: what each of its states renders, the order it * puts jobs in, and what it says about a job it has no verdict or no reading * for. The page is a pure function of one collection, so every test here hands * it one and reads back the HTML of the live page that frames it. */ import { assert, assertEquals, assertStringIncludes, assertThrows, } from "@std/assert"; import { type CiJobs, CI_JOBS_PATH, ciJobsPage, ciJobsStatus, type Job, followSorting, sortTable, } from "./ci-jobs-page.ts"; import { LIVE_PAGE_UPDATE } from "./live-page-client.ts"; import { livePage, livePageResponse } from "./live-page.ts"; import { faviconHref } from "./favicon.ts"; const MINUTE = 60_000; const HOUR = 3_600_000; const NOW = Date.UTC(2026, 8, 22, 14, 30); function job(over: Partial = {}): Job { return { repo: "labs", workflow: "CI", path: ".github/workflows/ci.yml", pinned: false, status: "good", failing: false, result: "success", event: "push", startedAt: NOW - HOUR, ranMs: 17 * MINUTE + 3_000, href: "https://github.com/commonfabric/labs/actions/runs/1", ...over, }; } function collection(over: Partial = {}): CiJobs { return { jobs: [job()], repos: ["labs"], unreadableRepos: [], collectedAt: NOW - 2 * MINUTE, ...over, }; } // The page as the ci tile's route serves it. function pageHtml(collected: CiJobs | undefined, now: number): string { return livePage(ciJobsPage(collected, now)); } // The repository cell of each row, in the order the page put them in. function rowRepos(html: string): string[] { return [...html.matchAll(/]*>.*?<\/span>]*>([^<]*) match[1]); } Deno.test("ci jobs page: a job carries its result, duration, and when it ran", () => { const html = pageHtml(collection(), NOW); assertStringIncludes(html, "CI jobs"); assertStringIncludes(html, `href="/"`); assertStringIncludes(html, "collected 2026-09-22 14:28 UTC · 2m ago"); assertStringIncludes(html, ">labs<"); assertStringIncludes(html, ">CI"); assertStringIncludes(html, ">success<"); assertStringIncludes(html, ">push<"); assertStringIncludes(html, ">17m 03s<"); assertStringIncludes(html, ">2026-09-22 13:30 UTC<"); assertStringIncludes(html, ">1h ago<"); assertStringIncludes( html, `href="https://github.com/commonfabric/labs/actions/runs/1"`, ); }); Deno.test("ci jobs page: the summary counts each state of a job", () => { const html = pageHtml( collection({ jobs: [ job(), job({ repo: "loom", status: "bad", failing: true, result: "failure" }), // A failure old enough to have gone orange is still a failure. job({ repo: "amp", status: "warn", failing: true, result: "failure" }), job({ repo: "raia", status: "warn", result: "rate limit hit" }), job({ repo: "pond", status: "unknown", result: "no completed run" }), ], repos: ["labs", "loom", "amp", "raia", "gvisor"], unreadableRepos: ["commonfabric/gvisor"], }), NOW, ); for (const [term, value] of [ // The job with no verdict is not one the counts speak for. ["jobs", "4"], ["repositories", "5"], ["passing", "1"], ["failing", "2"], // The unreadable repository counts beside the job that could not be read, // and the orange failure counts with the failures rather than here. ["unreadable", "2"], ["no verdict", "1"], ["running", "0"], ]) { assertStringIncludes(html, `
${term}
${value}
`); } }); Deno.test("ci jobs page: a job with a run in progress has a running dot after its name", () => { const running = "https://github.com/commonfabric/loom/actions/runs/7"; const html = pageHtml( collection({ jobs: [ job(), job({ repo: "loom", runningHref: running }), // A workflow whose first run is still going has no verdict yet. job({ repo: "pond", status: "unknown", result: "no completed run", runningHref: "https://github.com/commonfabric/pond/actions/runs/8", }), ], repos: ["labs", "loom", "pond"], }), NOW, ); // The dot follows the workflow's own link and leads to the run in progress. assertStringIncludes( html, `>CI`, ); assertStringIncludes( html, `>CIrunning
2
`); }); Deno.test("ci jobs page: jobs are ordered worst first, then by name", () => { const html = pageHtml( collection({ jobs: [ job({ repo: "zed" }), job({ repo: "pond", status: "unknown", result: "no completed run" }), job({ repo: "loom", status: "bad", failing: true, result: "failure" }), job({ repo: "arc" }), job({ repo: "bay", status: "warn", result: "auth failed" }), job({ repo: "amp", status: "bad", result: "timed_out" }), ], repos: ["zed", "pond", "loom", "arc", "bay", "amp"], }), NOW, ); // The job with no verdict is listed under the table, not through it. assertEquals(rowRepos(html), ["amp", "loom", "bay", "arc", "zed", "pond"]); assertStringIncludes(html, "Workflows with no verdict · 1"); }); Deno.test("ci jobs page: two jobs in one repository sort by workflow", () => { const html = pageHtml( collection({ jobs: [ job({ workflow: "Nightly" }), job({ workflow: "Audit" }), ], }), NOW, ); const workflows = [...html.matchAll(/>([^<]*)<\/a>/g)].map((m) => m[1]); assertEquals(workflows.filter((name) => name === "Audit").length, 1); assert( html.indexOf(">Audit
") < html.indexOf(">Nightly"), "the earlier name comes first", ); }); Deno.test("ci jobs page: a job with no run is listed apart, linked at its workflow", () => { const html = pageHtml( collection({ jobs: [ job(), job({ repo: "pond", workflow: "Preview", status: "unknown", result: "no completed run", startedAt: undefined, ranMs: undefined, href: "https://github.com/commonfabric/labs/actions/workflows/x.yml", }), ], }), NOW, ); assertStringIncludes(html, "Workflows with no verdict · 1"); assertStringIncludes(html, "/actions/workflows/x.yml"); assertStringIncludes(html, `
no verdict
1
`); // It says why it has no verdict, and carries none of the table's measures. assertStringIncludes(html, `no completed run`); assertEquals([...html.matchAll(/data-sort="-1"/g)].length, 0); }); Deno.test("ci jobs page: a job whose workflow changed since it failed says so", () => { const html = pageHtml( collection({ jobs: [ job(), job({ repo: "crm", workflow: "Nightly export", status: "unknown", result: "changed since it failed", href: "https://github.com/commonfabric/crm/actions/runs/9", }), ], }), NOW, ); // It leaves the table for the section below it, linked at the failure it // no longer counts, with the reason beside it. assertStringIncludes(html, "Workflows with no verdict · 1"); assertStringIncludes( html, `href="https://github.com/commonfabric/crm/actions/runs/9"`, ); assertStringIncludes( html, `changed since it failed`, ); assertStringIncludes(html, `
failing
0
`); }); Deno.test("ci jobs page: a job whose run left no timing shows dashes, not a made-up time", () => { const html = pageHtml( collection({ jobs: [job({ startedAt: undefined, ranMs: undefined })], }), NOW, ); assertEquals([...html.matchAll(/ { const html = pageHtml( collection({ unreadableRepos: ["commonfabric/pond"] }), NOW, ); assertStringIncludes(html, "Repositories that could not be read · 1"); assertStringIncludes(html, `pond`); assertStringIncludes(html, `https://github.com/commonfabric/pond/actions`); const none = pageHtml(collection(), NOW); assert(!none.includes("Repositories that could not be read")); }); Deno.test("ci jobs page: a repository or workflow name carrying markup is escaped", () => { const html = pageHtml( collection({ jobs: [job({ workflow: `` })], unreadableRepos: [`commonfabric/