CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | /**
* Block J10 — Badge renderer tests + route smokes.
*/
import { describe, it, expect } from "bun:test";
import app from "../app";
import {
colorForState,
escapeXml,
estimateTextWidth,
renderBadge,
} from "../lib/badge";
describe("badge — escapeXml", () => {
it("escapes the five standard chars", () => {
expect(escapeXml("<a & b > c \"d\" 'e'")).toBe(
"<a & b > c "d" 'e'"
);
});
it("returns empty for empty input", () => {
expect(escapeXml("")).toBe("");
});
});
describe("badge — estimateTextWidth", () => {
it("returns 0 for empty", () => {
expect(estimateTextWidth("")).toBe(0);
});
it("scales roughly linearly with string length", () => {
const a = estimateTextWidth("abcd");
const b = estimateTextWidth("abcdabcd");
expect(b).toBeGreaterThan(a);
expect(b).toBeLessThan(a * 2.5);
});
it("handles punctuation + digits without throwing", () => {
expect(() => estimateTextWidth("Hi! 12.3%")).not.toThrow();
});
});
describe("badge — colorForState", () => {
it("maps success / passed to green", () => {
expect(colorForState("success")).toBe("green");
expect(colorForState("passed")).toBe("green");
});
it("maps pending to yellow", () => {
expect(colorForState("pending")).toBe("yellow");
});
it("maps failure / failed / error to red", () => {
expect(colorForState("failure")).toBe("red");
expect(colorForState("failed")).toBe("red");
expect(colorForState("error")).toBe("red");
});
it("unknown → grey", () => {
expect(colorForState("unknown" as any)).toBe("grey");
});
});
describe("badge — renderBadge", () => {
it("emits well-formed SVG with label + value + title", () => {
const svg = renderBadge({ label: "build", value: "passing", color: "green" });
expect(svg.startsWith("<svg")).toBe(true);
expect(svg.includes("</svg>")).toBe(true);
expect(svg.includes("build")).toBe(true);
expect(svg.includes("passing")).toBe(true);
expect(svg.includes("<title>build: passing</title>")).toBe(true);
});
it("honours named colours", () => {
expect(renderBadge({ label: "x", value: "y", color: "red" })).toContain(
"#da3633"
);
expect(renderBadge({ label: "x", value: "y", color: "green" })).toContain(
"#2ea043"
);
});
it("falls back to grey for unknown colour", () => {
const svg = renderBadge({ label: "x", value: "y", color: "puce" });
expect(svg).toContain("#586069");
});
it("accepts hex colour literals", () => {
const svg = renderBadge({ label: "x", value: "y", color: "#abc" });
expect(svg).toContain("#abc");
});
it("escapes markup in label + value", () => {
const svg = renderBadge({ label: "<evil>", value: "\"v&v'", color: "blue" });
expect(svg).toContain("<evil>");
expect(svg).toContain(""v&v'");
expect(svg).not.toContain("<evil>");
});
it("clamps ridiculously long inputs to ≤64 chars each", () => {
const long = "z".repeat(200);
const svg = renderBadge({ label: long, value: long });
// "z" is not used in any SVG attribute / tag name, so every occurrence
// in the output comes from our label / value payloads. Each of label +
// value appears in: <title>, aria-label, shadow <text>, main <text> —
// so at most 64 chars × 2 payloads × 4 occurrences = 512. Unclamped a
// 200-char payload would produce well over 1000.
const matches = svg.match(/z/g) || [];
expect(matches.length).toBeLessThan(600);
expect(matches.length).toBeGreaterThan(0);
// Sanity: SVG should be well under the unclamped size.
expect(svg.length).toBeLessThan(2500);
});
});
describe("badge — routes", () => {
it("GET /:o/:r/badge/gates.svg returns SVG (even when repo missing)", async () => {
const res = await app.request("/alice/nope/badge/gates.svg");
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toContain("image/svg+xml");
const body = await res.text();
expect(body.startsWith("<svg")).toBe(true);
});
it("GET /:o/:r/badge/issues.svg returns SVG", async () => {
const res = await app.request("/alice/nope/badge/issues.svg");
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toContain("image/svg+xml");
});
it("GET /:o/:r/badge/prs.svg returns SVG", async () => {
const res = await app.request("/alice/nope/badge/prs.svg");
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toContain("image/svg+xml");
});
it("GET /:o/:r/badge/status.svg returns SVG", async () => {
const res = await app.request("/alice/nope/badge/status.svg");
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toContain("image/svg+xml");
});
it("GET /:o/:r/badge/status/:context.svg returns SVG", async () => {
const res = await app.request("/alice/nope/badge/status/ci.svg");
expect(res.status).toBe(200);
expect(res.headers.get("content-type")).toContain("image/svg+xml");
const body = await res.text();
expect(body).toContain("ci");
});
it("responses are Cache-Control aware", async () => {
const res = await app.request("/alice/nope/badge/gates.svg");
const cc = res.headers.get("cache-control") || "";
expect(cc).toContain("max-age");
});
});
|