Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

badges.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

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