Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 133 additions & 3 deletions src/approval-report/__tests__/build-rule-approval-rows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ jest.mock("@octokit/rest", () => ({
import type { Parent } from "unist";
import {
buildRuleApprovalRows,
classifyRuleStatus,
type ApprovalReportDeps,
} from "../build-rule-approval-rows";
import type { ApprovalReportOptions } from "../types";
Expand Down Expand Up @@ -71,7 +72,7 @@ function oneAtomic(
}

describe("buildRuleApprovalRows", () => {
it("skips deprecated rules", async () => {
it("includes deprecated rules with highest-priority status", async () => {
const rows = await buildRuleApprovalRows(
baseOpts,
mockDeps({
Expand All @@ -83,8 +84,11 @@ describe("buildRuleApprovalRows", () => {
pathRelativeToRepo: () => "_rules/keep.md",
}),
);
expect(rows).toHaveLength(1);
expect(rows[0].ruleId).toBe("keep");
expect(rows).toHaveLength(2);
expect(rows[0]).toMatchObject({
ruleId: "gone",
status: "Deprecated",
});
});

it("strips issue body from row issues", async () => {
Expand All @@ -108,6 +112,7 @@ describe("buildRuleApprovalRows", () => {
number: 7,
title: "rid bug",
html_url: "https://github.com/o/r/issues/7",
labelNames: [],
},
]);
});
Expand All @@ -118,6 +123,7 @@ describe("buildRuleApprovalRows", () => {
oneAtomic("n", { loadCompleteImplementationsByRuleId: () => ({}) }),
);
expect(rows[0].reportBucket).toBe("notReady");
expect(rows[0].status).toBe("Awaiting implementation");
});

it("buckets notReady when a matched issue has Blocker label", async () => {
Expand All @@ -136,13 +142,17 @@ describe("buildRuleApprovalRows", () => {
}),
);
expect(rows[0].reportBucket).toBe("notReady");
expect(rows[0].status).toBe("Blocked by issue");
expect(rows[0].blockersCount).toBe(1);
expect(rows[0].blockers).toEqual(rows[0].issues);
});

it("buckets proposedReadyForUpdate when not WAI-approved but has implementation", async () => {
const rows = await buildRuleApprovalRows(baseOpts, oneAtomic("p"));
expect(rows[0].reportBucket).toBe("proposedReadyForUpdate");
expect(rows[0].status).toBe("Proposed, reviewable");
expect(rows[0].waiApproved).toBe(false);
expect(rows[0].reviewPrUrl).toBeNull();
});

it("buckets approvedUpToDate when approved with no commits after approval", async () => {
Expand All @@ -155,6 +165,7 @@ describe("buildRuleApprovalRows", () => {
}),
);
expect(rows[0].reportBucket).toBe("approvedUpToDate");
expect(rows[0].status).toBe("Approved, current");
expect(rows[0].commitsBehindSummary).toBe("0");
});

Expand All @@ -177,10 +188,51 @@ describe("buildRuleApprovalRows", () => {
}),
);
expect(rows[0].reportBucket).toBe("approvedReadyForUpdate");
expect(rows[0].status).toBe("Approved, unpublished changes");
expect(rows[0].commitsBehindSummary).toBe("1");
expect(rows[0].changes).toEqual([change]);
});

it("splits rule commits from definition-only commits", async () => {
const changes = [
{
hash: "a".repeat(40),
subject: "rule and definition",
dateIso: "2024-03-01T00:00:00Z",
touchedRule: true,
touchedDefinitionKeys: ["foo"],
},
{
hash: "b".repeat(40),
subject: "definition only",
dateIso: "2024-02-01T00:00:00Z",
touchedRule: false,
touchedDefinitionKeys: ["foo"],
},
{
hash: "c".repeat(40),
subject: "rule only",
dateIso: "2024-01-01T00:00:00Z",
touchedRule: true,
touchedDefinitionKeys: [],
},
];
const rows = await buildRuleApprovalRows(
baseOpts,
oneAtomic("split", {
loadApprovalByRuleId: () => ({
split: { approved: true, approvalIsoDate: "2023-01-01" },
}),
getChangesSinceApproval: () => changes,
}),
);

expect(rows[0]).toMatchObject({
ruleCommitCount: 2,
definitionCommitCount: 1,
});
});

it("does not call getChangesSinceApproval when rule is not WAI-approved", async () => {
const getChangesSinceApproval = jest.fn(() => []);
await buildRuleApprovalRows(
Expand Down Expand Up @@ -215,3 +267,81 @@ describe("buildRuleApprovalRows", () => {
expect(rows[0].compositeInputs).toEqual(["in1", "in2"]);
});
});

describe("classifyRuleStatus", () => {
const reviewable = {
deprecated: false,
reviewPrUrl: null,
blockersCount: 0,
completeImplementationCount: 1,
waiApproved: false,
changesCount: 0,
};

it.each([
[
{
...reviewable,
deprecated: true,
reviewPrUrl: "https://example.test/pr/1",
blockersCount: 1,
completeImplementationCount: 0,
},
"Deprecated",
],
[
{
...reviewable,
reviewPrUrl: "https://example.test/pr/1",
blockersCount: 1,
completeImplementationCount: 0,
},
"In review",
],
[
{
...reviewable,
blockersCount: 1,
completeImplementationCount: 0,
waiApproved: true,
},
"Blocked by issue",
],
[
{
...reviewable,
completeImplementationCount: 0,
waiApproved: true,
},
"Awaiting implementation",
],
[{ ...reviewable, waiApproved: true }, "Approved, current"],
[
{ ...reviewable, waiApproved: true, changesCount: 1 },
"Approved, unpublished changes",
],
[reviewable, "Proposed, reviewable"],
])("applies status inputs in precedence order", (inputs, expected) => {
expect(classifyRuleStatus(inputs)).toBe(expected);
});

it("does not let a non-blocker issue affect status", async () => {
const rows = await buildRuleApprovalRows(
baseOpts,
oneAtomic("open-issue", {
fetchOpenIssues: async () => [
{
number: 2,
title: "open-issue discussion",
html_url: "https://example.test/issues/2",
labelNames: ["enhancement"],
},
],
}),
);

expect(rows[0].status).toBe("Proposed, reviewable");
expect(rows[0].issues).toHaveLength(1);
expect(rows[0].blockers).toHaveLength(0);
});
});
7 changes: 7 additions & 0 deletions src/approval-report/__tests__/generate-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@ function baseRow(
name: ruleId,
ruleTypeSummary: "atomic",
waiApproved: false,
status: "Approved, current",
reviewPrUrl: null,
reportBucket: "approvedUpToDate",
implementations: ["axe"],
issues: [],
blockers: [],
changes: [],
approvalIsoDate: null,
lastUpdatedIsoDate: "2024-01-01",
ruleCommitCount: 0,
definitionCommitCount: 0,
lastApprovedSummary: "2023-01-01",
lastUpdatedSummary: "2024-01-01",
commitsBehindSummary: "0",
Expand Down
15 changes: 15 additions & 0 deletions src/approval-report/__tests__/load-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ describe("loadCompleteImplementationsByRuleId", () => {
expect(loadCompleteImplementationsByRuleId(dir)["rx"]).toEqual(["custom"]);
});

it("uses the act-implementations name when JSON name is missing", () => {
writeTree(dir, {
"_data/wcag-act-rules/act-implementations.yml": `
- uniqueKey: custom
name: Custom Accessibility Tool
`,
"_data/wcag-act-rules/implementations/custom.json": JSON.stringify({
actRuleMapping: [{ ruleId: "rx", consistency: "complete" }],
}),
});
expect(loadCompleteImplementationsByRuleId(dir)["rx"]).toEqual([
"Custom Accessibility Tool",
]);
});

it("returns empty object when implementations dir has no json", () => {
fs.mkdirSync(path.join(dir, "_data/wcag-act-rules/implementations"), {
recursive: true,
Expand Down
31 changes: 27 additions & 4 deletions src/approval-report/__tests__/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,17 @@ describe("runApprovalReport", () => {
name: "Only rule",
ruleTypeSummary: "atomic",
waiApproved: true,
status: "Approved, current",
reviewPrUrl: null,
reportBucket: "approvedUpToDate",
implementations: ["axe"],
issues: [],
blockers: [],
changes: [],
approvalIsoDate: "2023-01-01",
lastUpdatedIsoDate: "2024-01-01",
ruleCommitCount: 0,
definitionCommitCount: 0,
lastApprovedSummary: "2023-01-01",
lastUpdatedSummary: "2024-01-01",
commitsBehindSummary: "0",
Expand All @@ -65,16 +71,33 @@ describe("runApprovalReport", () => {
fs.rmSync(tmp, { recursive: true, force: true });
});

it("writes markdown from rows and logs path", async () => {
it("writes JSON row array and logs path", async () => {
const log = jest.spyOn(console, "log").mockImplementation(() => undefined);
await runApprovalReport({ ...baseOpts, outFile });
const written = fs.readFileSync(outFile, "utf8");
expect(written).toContain("# ACT rules ready for approval");
expect(written).toContain("[only](#only)");
const written = JSON.parse(fs.readFileSync(outFile, "utf8"));
expect(written).toEqual([
expect.objectContaining({
ruleId: "only",
status: "Approved, current",
ruleCommitCount: 0,
definitionCommitCount: 0,
}),
]);
expect(log).toHaveBeenCalledWith(
expect.stringContaining(path.resolve(outFile)),
);
expect(log).toHaveBeenCalledWith(expect.stringContaining("(1 rules)"));
log.mockRestore();
});

it("writes markdown only when explicitly requested", async () => {
const markdownFile = path.join(tmp, "debug", "report.md");
const log = jest.spyOn(console, "log").mockImplementation(() => undefined);
await runApprovalReport({ ...baseOpts, outFile, markdownFile });

expect(fs.readFileSync(markdownFile, "utf8")).toContain(
"# ACT rules ready for approval",
);
log.mockRestore();
});
});
Loading