Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
approval-report.md
logs/
*.log
.env
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@
"build-examples": "ts-node src/cli/build-examples.ts",
"map-implementation": "ts-node src/cli/map-implementation.ts",
"implementations-update": "ts-node src/cli/implementations-update.ts",
"approval-report": "ts-node src/cli/approval-report.ts",
"test": "jest",
"prepare": "husky install"
},
"dependencies": {
"@octokit/rest": "^22.0.0",
"axios": "^1.13.5",
"commander": "^14.0.3",
"debug": "^4.4.3",
"fastmatter": "^2.1.1",
"globby": "^11.0.4",
"js-yaml": "^4.1.0",
"jsonld": "^9.0.0",
"markdown-table": "2.0.0",
"moment": "^2.30.1",
Expand Down
217 changes: 217 additions & 0 deletions src/approval-report/__tests__/build-rule-approval-rows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
jest.mock("@octokit/rest", () => ({
Octokit: jest.fn(),
}));

import type { Parent } from "unist";
import {
buildRuleApprovalRows,
type ApprovalReportDeps,
} from "../build-rule-approval-rows";
import type { ApprovalReportOptions } from "../types";
import type { RulePage } from "../../types";

const emptyMdAst = { type: "root", children: [] } as Parent;

function atomicPage(
id: string,
extra: Partial<RulePage["frontmatter"]> = {},
): RulePage {
return {
body: "",
markdownAST: emptyMdAst,
filename: `${id}.md`,
assets: {},
frontmatter: {
id,
name: `Name ${id}`,
rule_type: "atomic",
description: "",
input_aspects: [],
...extra,
} as RulePage["frontmatter"],
};
}

const baseOpts: ApprovalReportOptions = {
rulesDir: "/rules",
glossaryDir: "/glossary",
testAssetsDir: "/assets",
actRulesRepo: "/repo",
wcagActRulesDir: "/wcag",
outFile: "/out/report.md",
githubOwner: "o",
githubRepo: "r",
};

function mockDeps(
overrides: Partial<ApprovalReportDeps>,
): Partial<ApprovalReportDeps> {
return {
getDefinitionPages: () => [],
loadApprovalByRuleId: () => ({}),
fetchOpenIssues: async () => [],
getRuleDefinitions: () => [],
getChangesSinceApproval: () => [],
getLatestCommitDateOnPaths: () => "2024-01-01",
...overrides,
};
}

/** One non-deprecated atomic rule with a default complete implementation for `ruleId`. */
function oneAtomic(
ruleId: string,
extra: Partial<ApprovalReportDeps> = {},
): Partial<ApprovalReportDeps> {
return mockDeps({
getRulePages: () => [atomicPage(ruleId)],
pathRelativeToRepo: () => `_rules/${ruleId}.md`,
loadCompleteImplementationsByRuleId: () => ({ [ruleId]: ["axe"] }),
...extra,
});
}

describe("buildRuleApprovalRows", () => {
it("skips deprecated rules", async () => {
const rows = await buildRuleApprovalRows(
baseOpts,
mockDeps({
getRulePages: () => [
atomicPage("gone", { deprecated: "true" }),
atomicPage("keep"),
],
loadCompleteImplementationsByRuleId: () => ({ keep: ["axe"] }),
pathRelativeToRepo: () => "_rules/keep.md",
}),
);
expect(rows).toHaveLength(1);
expect(rows[0].ruleId).toBe("keep");
});

it("strips issue body from row issues", async () => {
const rows = await buildRuleApprovalRows(
baseOpts,
oneAtomic("rid", {
fetchOpenIssues: async () => [
{
number: 7,
title: "rid bug",
html_url: "https://github.com/o/r/issues/7",
body: "do not leak",
labelNames: [],
},
],
getLatestCommitDateOnPaths: () => null,
}),
);
expect(rows[0].issues).toEqual([
{
number: 7,
title: "rid bug",
html_url: "https://github.com/o/r/issues/7",
},
]);
});

it("buckets notReady without complete implementation", async () => {
const rows = await buildRuleApprovalRows(
baseOpts,
oneAtomic("n", { loadCompleteImplementationsByRuleId: () => ({}) }),
);
expect(rows[0].reportBucket).toBe("notReady");
});

it("buckets notReady when a matched issue has Blocker label", async () => {
const rows = await buildRuleApprovalRows(
baseOpts,
oneAtomic("b", {
fetchOpenIssues: async () => [
{
number: 1,
title: "b",
html_url: "u",
body: "",
labelNames: ["Blocker"],
},
],
}),
);
expect(rows[0].reportBucket).toBe("notReady");
expect(rows[0].blockersCount).toBe(1);
});

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].waiApproved).toBe(false);
});

it("buckets approvedUpToDate when approved with no commits after approval", async () => {
const rows = await buildRuleApprovalRows(
baseOpts,
oneAtomic("a", {
loadApprovalByRuleId: () => ({
a: { approved: true, approvalIsoDate: "2023-01-01" },
}),
}),
);
expect(rows[0].reportBucket).toBe("approvedUpToDate");
expect(rows[0].commitsBehindSummary).toBe("0");
});

it("buckets approvedReadyForUpdate when there are changes after approval", async () => {
const change = {
hash: "e".repeat(40),
subject: "edit",
dateIso: "2024-01-01T00:00:00Z",
touchedRule: true,
touchedDefinitionKeys: [] as string[],
};
const rows = await buildRuleApprovalRows(
baseOpts,
oneAtomic("u", {
loadApprovalByRuleId: () => ({
u: { approved: true, approvalIsoDate: "2023-01-01" },
}),
getChangesSinceApproval: () => [change],
getLatestCommitDateOnPaths: () => "2024-06-01",
}),
);
expect(rows[0].reportBucket).toBe("approvedReadyForUpdate");
expect(rows[0].commitsBehindSummary).toBe("1");
expect(rows[0].changes).toEqual([change]);
});

it("does not call getChangesSinceApproval when rule is not WAI-approved", async () => {
const getChangesSinceApproval = jest.fn(() => []);
await buildRuleApprovalRows(
baseOpts,
oneAtomic("x", { getChangesSinceApproval }),
);
expect(getChangesSinceApproval).not.toHaveBeenCalled();
});

it("sets compositeInputs for composite rules", async () => {
const composite: RulePage = {
body: "",
markdownAST: emptyMdAst,
filename: "comp.md",
assets: {},
frontmatter: {
id: "comp",
name: "Composite",
rule_type: "composite",
description: "",
input_rules: ["in1", "in2"],
} as RulePage["frontmatter"],
};
const rows = await buildRuleApprovalRows(
baseOpts,
mockDeps({
getRulePages: () => [composite],
loadCompleteImplementationsByRuleId: () => ({ comp: ["axe"] }),
pathRelativeToRepo: () => "_rules/comp.md",
}),
);
expect(rows[0].compositeInputs).toEqual(["in1", "in2"]);
});
});
Loading