The check I added for `?template=` links read README.md unconditionally, and the image build is the one place that runs this suite without it: `.dockerignore` omits the README along with docs/ and AGENTS.md, so that editing a doc does not invalidate the layer that installs, typechecks, tests and builds. `.github/` is copied in for the opposite reason, which is why every other check in the file ran there fine. So this failed in a way CI could not see — the check job has the whole repository and stayed green, while the container build went red on a file that context leaves out on purpose. Skipping it there keeps the gate honest about what it can actually inspect; the assertion still runs in CI and locally, which is where a README link is edited in the first place. Verified by running the suite inside the build stage: 516 pass, 1 skip, 0 fail, against 517 pass and no skips in the full tree. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
190 lines
7.1 KiB
TypeScript
190 lines
7.1 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import {
|
|
BUG_URL,
|
|
Colophon,
|
|
FEATURE_URL,
|
|
bugReportUrl,
|
|
} from "../src/client/components/Colophon.tsx";
|
|
|
|
/**
|
|
* The issue forms, and the app's links into them.
|
|
*
|
|
* These pin a seam nothing else can see: GitHub answers a malformed form with a
|
|
* rendered error only a visitor notices, and it drops a prefill parameter that
|
|
* names no field with no error at all. Both failures look exactly like a working
|
|
* link from here, so the only place they can be caught is a test that reads the
|
|
* YAML the links point at.
|
|
*
|
|
* Reads repo files, never build output — see AGENTS.md § Commands.
|
|
*/
|
|
|
|
const DIR = ".github/ISSUE_TEMPLATE";
|
|
|
|
/**
|
|
* Whether the README is in this context at all.
|
|
*
|
|
* The image build runs this suite as its gate, and `.dockerignore` excludes the
|
|
* README along with `docs/` and `AGENTS.md` — deliberately, so that editing a
|
|
* doc does not invalidate the layer that installs, tests and builds. `.github/`
|
|
* is copied in for the opposite reason, which is why every other check here runs
|
|
* there unchanged.
|
|
*
|
|
* So the README check stands down where the file was left out on purpose, rather
|
|
* than failing the image on the absence of something that context does not ship.
|
|
* It still runs in CI and locally, which is where a README link gets edited.
|
|
*/
|
|
const HAS_README = await Bun.file("README.md").exists();
|
|
|
|
type Field = {
|
|
type: string;
|
|
id?: string;
|
|
attributes?: { label?: string };
|
|
validations?: { required?: boolean };
|
|
};
|
|
type Form = { name?: string; description?: string; labels?: string[]; body?: Field[] };
|
|
|
|
async function form(file: string): Promise<Form> {
|
|
const text = await Bun.file(`${DIR}/${file}`).text();
|
|
return Bun.YAML.parse(text) as Form;
|
|
}
|
|
|
|
/** The `template=` filename a link points at, and its prefill parameters. */
|
|
function link(url: string): { template: string | null; params: URLSearchParams } {
|
|
const parsed = new URL(url);
|
|
const params = parsed.searchParams;
|
|
return { template: params.get("template"), params };
|
|
}
|
|
|
|
describe("issue forms", () => {
|
|
for (const file of ["bug_report.yml", "feature_request.yml"]) {
|
|
test(`${file} is a well-formed issue form`, async () => {
|
|
const doc = await form(file);
|
|
|
|
expect(doc.name).toBeTruthy();
|
|
expect(doc.description).toBeTruthy();
|
|
expect(doc.body?.length).toBeGreaterThan(0);
|
|
|
|
for (const field of doc.body ?? []) {
|
|
// GitHub rejects an unknown type outright.
|
|
expect([
|
|
"markdown",
|
|
"input",
|
|
"textarea",
|
|
"dropdown",
|
|
"checkboxes",
|
|
]).toContain(field.type);
|
|
|
|
// Every field that collects an answer needs a label to collect it under,
|
|
// and an id, or its answer cannot be prefilled or referenced.
|
|
if (field.type !== "markdown") {
|
|
expect(field.attributes?.label).toBeTruthy();
|
|
expect(field.id).toBeTruthy();
|
|
}
|
|
}
|
|
});
|
|
|
|
test(`${file} has unique field ids`, async () => {
|
|
const ids = (await form(file)).body?.flatMap((f) => f.id ?? []) ?? [];
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
});
|
|
|
|
test(`${file} asks for something, but not for everything`, async () => {
|
|
const answered = (await form(file)).body?.filter((f) => f.type !== "markdown") ?? [];
|
|
const required = answered.filter((f) => f.validations?.required === true);
|
|
|
|
// At least one required field, or the form accepts an empty report.
|
|
expect(required.length).toBeGreaterThan(0);
|
|
// A form that demands every field is one a reader abandons — and this app
|
|
// has no account to look anything up in, so an abandoned report is the
|
|
// whole loss.
|
|
expect(required.length).toBeLessThan(answered.length);
|
|
});
|
|
|
|
test(`${file} uses only labels a fresh repository already has`, async () => {
|
|
// A template naming a label that does not exist applies nothing, silently.
|
|
for (const label of (await form(file)).labels ?? []) {
|
|
expect(["bug", "enhancement"]).toContain(label);
|
|
}
|
|
});
|
|
}
|
|
|
|
test("config.yml keeps the blank-issue escape hatch open", async () => {
|
|
const text = await Bun.file(`${DIR}/config.yml`).text();
|
|
expect(Bun.YAML.parse(text)).toEqual({ blank_issues_enabled: true });
|
|
});
|
|
});
|
|
|
|
describe("the app's links into them", () => {
|
|
test("each points at a template file that exists", async () => {
|
|
for (const url of [BUG_URL, FEATURE_URL]) {
|
|
const { template } = link(url);
|
|
expect(template).toBeTruthy();
|
|
expect(await Bun.file(`${DIR}/${template}`).exists()).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("the prefilled freshness line names a real field on the bug form", async () => {
|
|
const ids = (await form("bug_report.yml")).body?.flatMap((f) => f.id ?? []) ?? [];
|
|
const { params } = link(bugReportUrl("15 Aug 2026, 04:12 — 2 days ago"));
|
|
|
|
for (const key of params.keys()) {
|
|
if (key === "template") continue;
|
|
// The failure this catches: renaming the field id leaves the link working
|
|
// and the prefill gone, with nothing anywhere to say so.
|
|
expect(ids).toContain(key);
|
|
}
|
|
expect(params.get("refreshed")).toBe("15 Aug 2026, 04:12 — 2 days ago");
|
|
});
|
|
|
|
test.skipIf(!HAS_README)("every template link in the README names a template that exists", async () => {
|
|
// A `?template=` that names nothing lands the reader on the chooser instead,
|
|
// which looks close enough to working that nobody reports it.
|
|
const readme = await Bun.file("README.md").text();
|
|
const named = [...readme.matchAll(/issues\/new\?template=([\w.-]+)/g)].map(
|
|
(m) => m[1],
|
|
);
|
|
|
|
expect(named.length).toBeGreaterThan(0);
|
|
for (const template of named) {
|
|
expect(await Bun.file(`${DIR}/${template}`).exists()).toBe(true);
|
|
}
|
|
// Both forms are reachable from the README, not just the one.
|
|
expect(new Set(named)).toEqual(
|
|
new Set(["bug_report.yml", "feature_request.yml"]),
|
|
);
|
|
});
|
|
|
|
test("the footer offers both, and carries its own freshness line into the bug form", () => {
|
|
const NOW = Date.parse("2026-08-17T12:00:00.000Z");
|
|
const html = renderToStaticMarkup(
|
|
<Colophon
|
|
sources={[
|
|
{
|
|
sourceId: "genshin-game8-events",
|
|
game: "genshin",
|
|
url: "https://game8.co/games/Genshin-Impact/archives/301601",
|
|
lastSuccessAt: new Date(NOW - 3 * 60 * 60 * 1000).toISOString(),
|
|
eventCount: 9,
|
|
},
|
|
]}
|
|
now={NOW}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Report a problem");
|
|
expect(html).toContain("request a feature");
|
|
expect(html).toContain("template=feature_request.yml");
|
|
// The age the reader is being shown is the age the report will carry.
|
|
expect(html).toContain("3h 0m ago");
|
|
expect(html).toContain(encodeURIComponent("3h 0m ago"));
|
|
});
|
|
|
|
test("an unfetched feed prefills nothing rather than an empty value", () => {
|
|
// "" in the field reads as an answer the reader gave, and a report claiming
|
|
// the app never refreshed is worse than one that leaves the question open.
|
|
expect(bugReportUrl(null)).toBe(BUG_URL);
|
|
expect(link(bugReportUrl(null)).params.has("refreshed")).toBe(false);
|
|
});
|
|
});
|