@lullabot/playwright-testing 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/github-a11y-summary +2 -2
- package/bin/github-failure-summary +4 -4
- package/lib/accessibility-baseline-file.d.ts +6 -6
- package/lib/accessibility-baseline-file.js +29 -23
- package/lib/accessibility-baseline.js +5 -3
- package/lib/accessible-screenshot.d.ts +4 -4
- package/lib/accessible-screenshot.js +110 -88
- package/lib/focus.js +4 -1
- package/lib/frames.js +4 -4
- package/lib/github/a11y-summary.js +74 -79
- package/lib/github/attachments.d.ts +1 -1
- package/lib/github/attachments.js +36 -34
- package/lib/github/failure-summary.d.ts +4 -4
- package/lib/github/failure-summary.js +140 -108
- package/lib/github/index.d.ts +6 -6
- package/lib/github/report-paths.js +17 -9
- package/lib/hover.js +17 -17
- package/lib/images.js +15 -12
- package/lib/index.d.ts +13 -13
- package/lib/interaction-states.d.ts +2 -2
- package/lib/interaction-states.js +7 -7
- package/lib/mock/index.d.ts +1 -1
- package/lib/mock/youtube.d.ts +1 -1
- package/lib/mock/youtube.js +1 -1
- package/lib/pseudo-state.d.ts +2 -2
- package/lib/pseudo-state.js +15 -7
- package/lib/videos.js +21 -10
- package/lib/visualdiff.d.ts +4 -4
- package/lib/visualdiff.js +35 -27
- package/package.json +3 -3
- package/src/accessibility-baseline-file.test.ts +190 -154
- package/src/accessibility-baseline-file.ts +107 -83
- package/src/accessibility-baseline.test.ts +869 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +358 -251
- package/src/focus.test.ts +23 -23
- package/src/focus.ts +7 -2
- package/src/fonts.test.ts +13 -13
- package/src/fonts.ts +1 -1
- package/src/frames.test.ts +10 -8
- package/src/frames.ts +5 -5
- package/src/github/a11y-summary.test.ts +436 -332
- package/src/github/a11y-summary.ts +189 -179
- package/src/github/attachments.test.ts +228 -167
- package/src/github/attachments.ts +144 -121
- package/src/github/failure-summary.test.ts +660 -445
- package/src/github/failure-summary.ts +376 -274
- package/src/github/index.test.ts +14 -14
- package/src/github/index.ts +6 -6
- package/src/github/report-paths.test.ts +211 -156
- package/src/github/report-paths.ts +75 -61
- package/src/hover.test.ts +50 -46
- package/src/hover.ts +18 -18
- package/src/images.test.ts +251 -206
- package/src/images.ts +52 -43
- package/src/index.ts +13 -13
- package/src/interaction-states.test.ts +72 -42
- package/src/interaction-states.ts +43 -29
- package/src/mock/index.ts +1 -1
- package/src/mock/youtube.test.ts +26 -26
- package/src/mock/youtube.ts +4 -6
- package/src/pseudo-state.test.ts +58 -51
- package/src/pseudo-state.ts +32 -27
- package/src/videos.test.ts +559 -399
- package/src/videos.ts +68 -44
- package/src/visualdiff.test.ts +416 -315
- package/src/visualdiff.ts +140 -99
|
@@ -1,439 +1,543 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as os from
|
|
4
|
-
import * as path from
|
|
5
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import {
|
|
6
|
+
parseA11yResults,
|
|
7
|
+
generateSummary,
|
|
8
|
+
generateAnnotations,
|
|
9
|
+
} from "./a11y-summary";
|
|
6
10
|
|
|
7
11
|
// A minimal Playwright JSON report with a11y data.
|
|
8
|
-
function makeReport(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
function makeReport(
|
|
13
|
+
overrides: {
|
|
14
|
+
violations?: any[];
|
|
15
|
+
annotations?: any[];
|
|
16
|
+
attachments?: any[];
|
|
17
|
+
specTitle?: string;
|
|
18
|
+
file?: string;
|
|
19
|
+
line?: number;
|
|
20
|
+
} = {},
|
|
21
|
+
) {
|
|
16
22
|
const {
|
|
17
|
-
|
|
18
|
-
annotations = [{ type: '@a11y' }],
|
|
23
|
+
annotations = [{ type: "@a11y" }],
|
|
19
24
|
attachments = [],
|
|
20
|
-
specTitle =
|
|
21
|
-
file =
|
|
25
|
+
specTitle = "homepage accessibility check",
|
|
26
|
+
file = "tests/a11y.spec.ts",
|
|
22
27
|
line = 10,
|
|
23
|
-
} = overrides
|
|
28
|
+
} = overrides;
|
|
24
29
|
|
|
25
30
|
return {
|
|
26
|
-
suites: [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
suites: [
|
|
32
|
+
{
|
|
33
|
+
title: "",
|
|
34
|
+
file,
|
|
35
|
+
specs: [
|
|
36
|
+
{
|
|
37
|
+
title: specTitle,
|
|
38
|
+
line,
|
|
39
|
+
tests: [
|
|
40
|
+
{
|
|
41
|
+
annotations: [],
|
|
42
|
+
results: [
|
|
43
|
+
{
|
|
44
|
+
// Playwright stores runtime annotations (from testInfo.annotations.push())
|
|
45
|
+
// at the result level, not the test level.
|
|
46
|
+
annotations,
|
|
47
|
+
attachments,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
function makeWcagAttachment(violations: any[]) {
|
|
47
|
-
const body = Buffer.from(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
const body = Buffer.from(
|
|
61
|
+
JSON.stringify({
|
|
62
|
+
violations: violations.map((v) => ({
|
|
63
|
+
id: v.rule ?? "color-contrast",
|
|
64
|
+
impact: v.impact ?? "serious",
|
|
65
|
+
description:
|
|
66
|
+
v.description ?? "Elements must have sufficient color contrast",
|
|
67
|
+
helpUrl:
|
|
68
|
+
v.helpUrl ??
|
|
69
|
+
"https://dequeuniversity.com/rules/axe/4.7/color-contrast",
|
|
70
|
+
nodes: (v.targets ?? ["#main"]).map((t: string) => ({ target: [t] })),
|
|
71
|
+
})),
|
|
72
|
+
}),
|
|
73
|
+
).toString("base64");
|
|
56
74
|
|
|
57
75
|
return {
|
|
58
|
-
name:
|
|
76
|
+
name: "a11y-wcag-scan-results",
|
|
59
77
|
body,
|
|
60
|
-
contentType:
|
|
61
|
-
}
|
|
78
|
+
contentType: "application/json",
|
|
79
|
+
};
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
describe(
|
|
65
|
-
let tmpDir: string
|
|
66
|
-
let reportPath: string
|
|
82
|
+
describe("parseA11yResults", () => {
|
|
83
|
+
let tmpDir: string;
|
|
84
|
+
let reportPath: string;
|
|
67
85
|
|
|
68
86
|
beforeEach(() => {
|
|
69
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
70
|
-
reportPath = path.join(tmpDir,
|
|
71
|
-
})
|
|
87
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "a11y-summary-"));
|
|
88
|
+
reportPath = path.join(tmpDir, "test-report.json");
|
|
89
|
+
});
|
|
72
90
|
|
|
73
91
|
afterEach(() => {
|
|
74
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
75
|
-
})
|
|
92
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
93
|
+
});
|
|
76
94
|
|
|
77
|
-
it(
|
|
78
|
-
expect(() => parseA11yResults(
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
})
|
|
95
|
+
it("throws when report file does not exist", () => {
|
|
96
|
+
expect(() => parseA11yResults("/nonexistent/report.json")).toThrow(
|
|
97
|
+
"Playwright JSON report not found",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
82
100
|
|
|
83
|
-
it(
|
|
84
|
-
const report = { suites: [{ specs: [{ tests: [{ annotations: [] }] }] }] }
|
|
85
|
-
fs.writeFileSync(reportPath, JSON.stringify(report))
|
|
86
|
-
const result = parseA11yResults(reportPath)
|
|
87
|
-
expect(result.tests).toHaveLength(0)
|
|
88
|
-
expect(result.totalViolations).toBe(0)
|
|
89
|
-
})
|
|
101
|
+
it("returns empty report when no a11y tests exist", () => {
|
|
102
|
+
const report = { suites: [{ specs: [{ tests: [{ annotations: [] }] }] }] };
|
|
103
|
+
fs.writeFileSync(reportPath, JSON.stringify(report));
|
|
104
|
+
const result = parseA11yResults(reportPath);
|
|
105
|
+
expect(result.tests).toHaveLength(0);
|
|
106
|
+
expect(result.totalViolations).toBe(0);
|
|
107
|
+
});
|
|
90
108
|
|
|
91
|
-
it(
|
|
109
|
+
it("extracts tests with a11y annotations", () => {
|
|
92
110
|
const report = makeReport({
|
|
93
111
|
annotations: [
|
|
94
|
-
{ type:
|
|
95
|
-
{
|
|
112
|
+
{ type: "@a11y" },
|
|
113
|
+
{
|
|
114
|
+
type: "Accessibility",
|
|
115
|
+
description: "WCAG scan: 1 violations (50 rules passed)",
|
|
116
|
+
},
|
|
96
117
|
],
|
|
97
|
-
attachments: [
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
expect(result.tests
|
|
104
|
-
expect(result.
|
|
105
|
-
|
|
118
|
+
attachments: [
|
|
119
|
+
makeWcagAttachment([{ rule: "color-contrast", targets: ["#header"] }]),
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
fs.writeFileSync(reportPath, JSON.stringify(report));
|
|
123
|
+
const result = parseA11yResults(reportPath);
|
|
124
|
+
expect(result.tests).toHaveLength(1);
|
|
125
|
+
expect(result.tests[0].violations).toHaveLength(1);
|
|
126
|
+
expect(result.tests[0].violations[0].rule).toBe("color-contrast");
|
|
127
|
+
expect(result.totalViolations).toBe(1);
|
|
128
|
+
});
|
|
106
129
|
|
|
107
|
-
it(
|
|
130
|
+
it("counts baselined and stale annotations", () => {
|
|
108
131
|
const report = makeReport({
|
|
109
132
|
annotations: [
|
|
110
|
-
{ type:
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
{ type: "@a11y" },
|
|
134
|
+
{
|
|
135
|
+
type: "Baselined a11y violation",
|
|
136
|
+
description: "color-contrast: known issue",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: "Baselined a11y violation",
|
|
140
|
+
description: "link-name: known issue",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: "Stale a11y baseline entry",
|
|
144
|
+
description: "image-alt on .hero — no longer detected",
|
|
145
|
+
},
|
|
114
146
|
],
|
|
115
|
-
})
|
|
116
|
-
fs.writeFileSync(reportPath, JSON.stringify(report))
|
|
117
|
-
const result = parseA11yResults(reportPath)
|
|
118
|
-
expect(result.totalBaselined).toBe(2)
|
|
119
|
-
expect(result.totalStale).toBe(1)
|
|
120
|
-
})
|
|
147
|
+
});
|
|
148
|
+
fs.writeFileSync(reportPath, JSON.stringify(report));
|
|
149
|
+
const result = parseA11yResults(reportPath);
|
|
150
|
+
expect(result.totalBaselined).toBe(2);
|
|
151
|
+
expect(result.totalStale).toBe(1);
|
|
152
|
+
});
|
|
121
153
|
|
|
122
|
-
it(
|
|
154
|
+
it("handles nested suites", () => {
|
|
123
155
|
const report = {
|
|
124
|
-
suites: [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
suites: [
|
|
157
|
+
{
|
|
158
|
+
title: "outer",
|
|
159
|
+
file: "tests/a11y.spec.ts",
|
|
160
|
+
specs: [],
|
|
161
|
+
suites: [
|
|
162
|
+
{
|
|
163
|
+
title: "inner",
|
|
164
|
+
specs: [
|
|
165
|
+
{
|
|
166
|
+
title: "nested test",
|
|
167
|
+
line: 5,
|
|
168
|
+
tests: [
|
|
169
|
+
{
|
|
170
|
+
annotations: [
|
|
171
|
+
{ type: "@a11y" },
|
|
172
|
+
{ type: "Accessibility", description: "passed" },
|
|
173
|
+
],
|
|
174
|
+
results: [{ attachments: [] }],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
fs.writeFileSync(reportPath, JSON.stringify(report));
|
|
185
|
+
const result = parseA11yResults(reportPath);
|
|
186
|
+
expect(result.tests).toHaveLength(1);
|
|
187
|
+
expect(result.tests[0].file).toBe("tests/a11y.spec.ts");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
147
190
|
|
|
148
|
-
describe(
|
|
149
|
-
it(
|
|
191
|
+
describe("generateSummary", () => {
|
|
192
|
+
it("shows green check when no violations", () => {
|
|
150
193
|
const md = generateSummary({
|
|
151
|
-
tests: [
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
194
|
+
tests: [
|
|
195
|
+
{
|
|
196
|
+
title: "passes a11y",
|
|
197
|
+
file: "tests/a11y.spec.ts",
|
|
198
|
+
line: 10,
|
|
199
|
+
annotations: [
|
|
200
|
+
{
|
|
201
|
+
type: "Accessibility",
|
|
202
|
+
description: "WCAG scan: 0 violations (50 rules passed)",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
violations: [],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
158
208
|
totalViolations: 0,
|
|
159
209
|
totalBaselined: 0,
|
|
160
210
|
totalStale: 0,
|
|
161
|
-
})
|
|
162
|
-
expect(md).toContain(
|
|
163
|
-
expect(md).toContain(
|
|
164
|
-
})
|
|
211
|
+
});
|
|
212
|
+
expect(md).toContain(":white_check_mark:");
|
|
213
|
+
expect(md).toContain("All accessibility checks passed");
|
|
214
|
+
});
|
|
165
215
|
|
|
166
|
-
it(
|
|
216
|
+
it("shows violation table when violations exist", () => {
|
|
167
217
|
const md = generateSummary({
|
|
168
|
-
tests: [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
218
|
+
tests: [
|
|
219
|
+
{
|
|
220
|
+
title: "homepage check",
|
|
221
|
+
file: "tests/a11y.spec.ts",
|
|
222
|
+
line: 10,
|
|
223
|
+
annotations: [
|
|
224
|
+
{ type: "Accessibility", description: "WCAG scan: 1 violations" },
|
|
225
|
+
],
|
|
226
|
+
violations: [
|
|
227
|
+
{
|
|
228
|
+
rule: "color-contrast",
|
|
229
|
+
impact: "serious",
|
|
230
|
+
description: "Elements must have sufficient color contrast",
|
|
231
|
+
helpUrl:
|
|
232
|
+
"https://dequeuniversity.com/rules/axe/4.7/color-contrast",
|
|
233
|
+
targets: ["#header"],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
181
238
|
totalViolations: 1,
|
|
182
239
|
totalBaselined: 0,
|
|
183
240
|
totalStale: 0,
|
|
184
|
-
})
|
|
185
|
-
expect(md).toContain(
|
|
186
|
-
expect(md).toContain(
|
|
187
|
-
expect(md).toContain(
|
|
188
|
-
expect(md).toContain(
|
|
189
|
-
})
|
|
241
|
+
});
|
|
242
|
+
expect(md).toContain("**1** violation(s)");
|
|
243
|
+
expect(md).toContain("color-contrast");
|
|
244
|
+
expect(md).toContain("| Rule |");
|
|
245
|
+
expect(md).toContain("`#header`");
|
|
246
|
+
});
|
|
190
247
|
|
|
191
|
-
it(
|
|
192
|
-
const md = generateSummary({
|
|
193
|
-
|
|
194
|
-
|
|
248
|
+
it("returns message when no tests found", () => {
|
|
249
|
+
const md = generateSummary({
|
|
250
|
+
tests: [],
|
|
251
|
+
totalViolations: 0,
|
|
252
|
+
totalBaselined: 0,
|
|
253
|
+
totalStale: 0,
|
|
254
|
+
});
|
|
255
|
+
expect(md).toContain("No accessibility tests were found");
|
|
256
|
+
});
|
|
195
257
|
|
|
196
|
-
it(
|
|
258
|
+
it("shows baselined and stale counts", () => {
|
|
197
259
|
const md = generateSummary({
|
|
198
|
-
tests: [
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
260
|
+
tests: [
|
|
261
|
+
{
|
|
262
|
+
title: "baseline test",
|
|
263
|
+
file: "test.ts",
|
|
264
|
+
line: 1,
|
|
265
|
+
annotations: [
|
|
266
|
+
{ type: "Baselined a11y violation", description: "known issue" },
|
|
267
|
+
{
|
|
268
|
+
type: "Stale a11y baseline entry",
|
|
269
|
+
description: "no longer detected",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
violations: [],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
208
275
|
totalViolations: 0,
|
|
209
276
|
totalBaselined: 1,
|
|
210
277
|
totalStale: 1,
|
|
211
|
-
})
|
|
212
|
-
expect(md).toContain(
|
|
213
|
-
expect(md).toContain(
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
})
|
|
278
|
+
});
|
|
279
|
+
expect(md).toContain("**1** baselined");
|
|
280
|
+
expect(md).toContain("**1** stale baseline entries");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
217
283
|
|
|
218
|
-
describe(
|
|
219
|
-
it(
|
|
284
|
+
describe("generateAnnotations", () => {
|
|
285
|
+
it("emits ::error for violations", () => {
|
|
220
286
|
const output = generateAnnotations({
|
|
221
|
-
tests: [
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
287
|
+
tests: [
|
|
288
|
+
{
|
|
289
|
+
title: "test",
|
|
290
|
+
file: "tests/a11y.spec.ts",
|
|
291
|
+
line: 15,
|
|
292
|
+
annotations: [],
|
|
293
|
+
violations: [
|
|
294
|
+
{
|
|
295
|
+
rule: "color-contrast",
|
|
296
|
+
impact: "serious",
|
|
297
|
+
description: "Elements must have sufficient color contrast",
|
|
298
|
+
helpUrl: "https://example.com",
|
|
299
|
+
targets: ["#main"],
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
234
304
|
totalViolations: 1,
|
|
235
305
|
totalBaselined: 0,
|
|
236
306
|
totalStale: 0,
|
|
237
|
-
})
|
|
238
|
-
expect(output).toContain(
|
|
239
|
-
|
|
240
|
-
|
|
307
|
+
});
|
|
308
|
+
expect(output).toContain(
|
|
309
|
+
"::error file=tests/a11y.spec.ts,line=15,title=color-contrast (serious)",
|
|
310
|
+
);
|
|
311
|
+
expect(output).toContain("#main");
|
|
312
|
+
});
|
|
241
313
|
|
|
242
|
-
it(
|
|
314
|
+
it("emits ::warning for stale baselines", () => {
|
|
243
315
|
const output = generateAnnotations({
|
|
244
|
-
tests: [
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
316
|
+
tests: [
|
|
317
|
+
{
|
|
318
|
+
title: "test",
|
|
319
|
+
file: "tests/a11y.spec.ts",
|
|
320
|
+
line: 10,
|
|
321
|
+
annotations: [
|
|
322
|
+
{
|
|
323
|
+
type: "Stale a11y baseline entry",
|
|
324
|
+
description: "no longer detected",
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
violations: [],
|
|
328
|
+
},
|
|
329
|
+
],
|
|
251
330
|
totalViolations: 0,
|
|
252
331
|
totalBaselined: 0,
|
|
253
332
|
totalStale: 1,
|
|
254
|
-
})
|
|
255
|
-
expect(output).toContain(
|
|
256
|
-
|
|
333
|
+
});
|
|
334
|
+
expect(output).toContain(
|
|
335
|
+
"::warning file=tests/a11y.spec.ts,line=10,title=Stale a11y baseline",
|
|
336
|
+
);
|
|
337
|
+
});
|
|
257
338
|
|
|
258
|
-
it(
|
|
339
|
+
it("returns empty string when no violations or stale entries", () => {
|
|
259
340
|
const output = generateAnnotations({
|
|
260
|
-
tests: [
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
341
|
+
tests: [
|
|
342
|
+
{
|
|
343
|
+
title: "test",
|
|
344
|
+
file: "test.ts",
|
|
345
|
+
line: 1,
|
|
346
|
+
annotations: [{ type: "Accessibility", description: "passed" }],
|
|
347
|
+
violations: [],
|
|
348
|
+
},
|
|
349
|
+
],
|
|
267
350
|
totalViolations: 0,
|
|
268
351
|
totalBaselined: 0,
|
|
269
352
|
totalStale: 0,
|
|
270
|
-
})
|
|
271
|
-
expect(output).toBe(
|
|
272
|
-
})
|
|
273
|
-
})
|
|
353
|
+
});
|
|
354
|
+
expect(output).toBe("");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
274
357
|
|
|
275
|
-
describe(
|
|
358
|
+
describe("workflow-command injection hardening", () => {
|
|
276
359
|
// Defence in depth: CSS target selectors are extracted by axe-core from
|
|
277
360
|
// page DOM, so a crafted page could inject workflow-command syntax.
|
|
278
|
-
it(
|
|
361
|
+
it("escapes newlines and % in ::error output", () => {
|
|
279
362
|
const output = generateAnnotations({
|
|
280
|
-
tests: [
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
363
|
+
tests: [
|
|
364
|
+
{
|
|
365
|
+
title: "test",
|
|
366
|
+
file: "tests/a11y.spec.ts",
|
|
367
|
+
line: 15,
|
|
368
|
+
annotations: [],
|
|
369
|
+
violations: [
|
|
370
|
+
{
|
|
371
|
+
rule: "color-contrast",
|
|
372
|
+
impact: "serious",
|
|
373
|
+
description: "bad\ndescription with 100%",
|
|
374
|
+
helpUrl: "https://example.com",
|
|
375
|
+
targets: ["#evil\n::error title=pwned::gotcha"],
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
293
380
|
totalViolations: 1,
|
|
294
381
|
totalBaselined: 0,
|
|
295
382
|
totalStale: 0,
|
|
296
|
-
})
|
|
383
|
+
});
|
|
297
384
|
// No raw newlines or literal '%' characters escape into the message body.
|
|
298
|
-
expect(output.split(
|
|
299
|
-
expect(output).toContain(
|
|
300
|
-
expect(output).toContain(
|
|
385
|
+
expect(output.split("\n")).toHaveLength(1);
|
|
386
|
+
expect(output).toContain("%0A");
|
|
387
|
+
expect(output).toContain("%25");
|
|
301
388
|
// The forged `::error` must not appear as a standalone command.
|
|
302
|
-
expect(output).not.toMatch(/\n::error title=pwned/)
|
|
303
|
-
})
|
|
389
|
+
expect(output).not.toMatch(/\n::error title=pwned/);
|
|
390
|
+
});
|
|
304
391
|
|
|
305
|
-
it(
|
|
392
|
+
it("escapes : and , in property values", () => {
|
|
306
393
|
const output = generateAnnotations({
|
|
307
|
-
tests: [
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
394
|
+
tests: [
|
|
395
|
+
{
|
|
396
|
+
title: "test",
|
|
397
|
+
file: "tests/with,comma:colon.spec.ts",
|
|
398
|
+
line: 1,
|
|
399
|
+
annotations: [],
|
|
400
|
+
violations: [
|
|
401
|
+
{
|
|
402
|
+
rule: "color-contrast",
|
|
403
|
+
impact: "serious",
|
|
404
|
+
description: "ok",
|
|
405
|
+
helpUrl: "https://example.com",
|
|
406
|
+
targets: ["#main"],
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
320
411
|
totalViolations: 1,
|
|
321
412
|
totalBaselined: 0,
|
|
322
413
|
totalStale: 0,
|
|
323
|
-
})
|
|
324
|
-
expect(output).toContain(
|
|
325
|
-
})
|
|
414
|
+
});
|
|
415
|
+
expect(output).toContain("file=tests/with%2Ccomma%3Acolon.spec.ts");
|
|
416
|
+
});
|
|
326
417
|
|
|
327
|
-
it(
|
|
418
|
+
it("escapes stale baseline warning content", () => {
|
|
328
419
|
const output = generateAnnotations({
|
|
329
|
-
tests: [
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
420
|
+
tests: [
|
|
421
|
+
{
|
|
422
|
+
title: "test",
|
|
423
|
+
file: "tests/a11y.spec.ts",
|
|
424
|
+
line: 10,
|
|
425
|
+
annotations: [
|
|
426
|
+
{
|
|
427
|
+
type: "Stale a11y baseline entry",
|
|
428
|
+
description: "stale\n::error::forged",
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
violations: [],
|
|
432
|
+
},
|
|
433
|
+
],
|
|
336
434
|
totalViolations: 0,
|
|
337
435
|
totalBaselined: 0,
|
|
338
436
|
totalStale: 1,
|
|
339
|
-
})
|
|
340
|
-
expect(output.split(
|
|
341
|
-
expect(output).toContain(
|
|
342
|
-
})
|
|
343
|
-
})
|
|
437
|
+
});
|
|
438
|
+
expect(output.split("\n")).toHaveLength(1);
|
|
439
|
+
expect(output).toContain("%0A");
|
|
440
|
+
});
|
|
441
|
+
});
|
|
344
442
|
|
|
345
|
-
describe(
|
|
346
|
-
it(
|
|
443
|
+
describe("markdown escaping in the summary table", () => {
|
|
444
|
+
it("escapes pipes and newlines in description cells", () => {
|
|
347
445
|
const md = generateSummary({
|
|
348
|
-
tests: [
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
446
|
+
tests: [
|
|
447
|
+
{
|
|
448
|
+
title: "homepage",
|
|
449
|
+
file: "tests/a11y.spec.ts",
|
|
450
|
+
line: 10,
|
|
451
|
+
annotations: [],
|
|
452
|
+
violations: [
|
|
453
|
+
{
|
|
454
|
+
rule: "color-contrast",
|
|
455
|
+
impact: "serious",
|
|
456
|
+
description: "pipe | in | description\nand a newline",
|
|
457
|
+
helpUrl: "https://example.com",
|
|
458
|
+
targets: ["#main"],
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
],
|
|
361
463
|
totalViolations: 1,
|
|
362
464
|
totalBaselined: 0,
|
|
363
465
|
totalStale: 0,
|
|
364
|
-
})
|
|
466
|
+
});
|
|
365
467
|
// Find the row line — the one that starts with `| [color-contrast]`.
|
|
366
|
-
const row = md.split(
|
|
367
|
-
expect(row).toBeDefined()
|
|
468
|
+
const row = md.split("\n").find((l) => l.startsWith("| [color-contrast]"));
|
|
469
|
+
expect(row).toBeDefined();
|
|
368
470
|
// Row should still have exactly 5 pipe-separated segments (4 columns + trailing).
|
|
369
|
-
expect(row!.split(
|
|
370
|
-
expect(row).toContain(
|
|
471
|
+
expect(row!.split(" | ").length).toBe(4);
|
|
472
|
+
expect(row).toContain("\\|");
|
|
371
473
|
// No literal newline inside the cell.
|
|
372
|
-
expect(row).not.toMatch(/\n/)
|
|
373
|
-
})
|
|
474
|
+
expect(row).not.toMatch(/\n/);
|
|
475
|
+
});
|
|
374
476
|
|
|
375
|
-
it(
|
|
477
|
+
it("escapes backticks in target code spans", () => {
|
|
376
478
|
const md = generateSummary({
|
|
377
|
-
tests: [
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
479
|
+
tests: [
|
|
480
|
+
{
|
|
481
|
+
title: "homepage",
|
|
482
|
+
file: "tests/a11y.spec.ts",
|
|
483
|
+
line: 10,
|
|
484
|
+
annotations: [],
|
|
485
|
+
violations: [
|
|
486
|
+
{
|
|
487
|
+
rule: "color-contrast",
|
|
488
|
+
impact: "serious",
|
|
489
|
+
description: "bad",
|
|
490
|
+
helpUrl: "https://example.com",
|
|
491
|
+
// A target containing a backtick — would escape a naive `${t}` code span.
|
|
492
|
+
targets: ["#foo`span"],
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
],
|
|
391
497
|
totalViolations: 1,
|
|
392
498
|
totalBaselined: 0,
|
|
393
499
|
totalStale: 0,
|
|
394
|
-
})
|
|
500
|
+
});
|
|
395
501
|
// The raw selector must not appear wrapped in single backticks
|
|
396
502
|
// (which would be the unescaped form).
|
|
397
|
-
expect(md).not.toMatch(/`#foo`span`/)
|
|
503
|
+
expect(md).not.toMatch(/`#foo`span`/);
|
|
398
504
|
// It should be wrapped in double backticks with padding instead.
|
|
399
|
-
expect(md).toContain(
|
|
400
|
-
})
|
|
401
|
-
})
|
|
505
|
+
expect(md).toContain("`` #foo`span ``");
|
|
506
|
+
});
|
|
507
|
+
});
|
|
402
508
|
|
|
403
|
-
describe(
|
|
404
|
-
let tmpDir: string
|
|
405
|
-
let origCwd: string
|
|
406
|
-
let origSummary: string | undefined
|
|
509
|
+
describe("main() error handling", () => {
|
|
510
|
+
let tmpDir: string;
|
|
511
|
+
let origCwd: string;
|
|
512
|
+
let origSummary: string | undefined;
|
|
407
513
|
|
|
408
514
|
beforeEach(() => {
|
|
409
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(),
|
|
410
|
-
origCwd = process.cwd()
|
|
411
|
-
origSummary = process.env.GITHUB_STEP_SUMMARY
|
|
412
|
-
delete process.env.GITHUB_STEP_SUMMARY
|
|
413
|
-
process.chdir(tmpDir)
|
|
414
|
-
})
|
|
515
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "a11y-main-"));
|
|
516
|
+
origCwd = process.cwd();
|
|
517
|
+
origSummary = process.env.GITHUB_STEP_SUMMARY;
|
|
518
|
+
delete process.env.GITHUB_STEP_SUMMARY;
|
|
519
|
+
process.chdir(tmpDir);
|
|
520
|
+
});
|
|
415
521
|
|
|
416
522
|
afterEach(() => {
|
|
417
|
-
process.chdir(origCwd)
|
|
523
|
+
process.chdir(origCwd);
|
|
418
524
|
if (origSummary !== undefined) {
|
|
419
|
-
process.env.GITHUB_STEP_SUMMARY = origSummary
|
|
525
|
+
process.env.GITHUB_STEP_SUMMARY = origSummary;
|
|
420
526
|
}
|
|
421
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
422
|
-
})
|
|
527
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
528
|
+
});
|
|
423
529
|
|
|
424
|
-
it(
|
|
425
|
-
const { main } = await import(
|
|
530
|
+
it("returns without throwing when the report is missing", async () => {
|
|
531
|
+
const { main } = await import("./a11y-summary");
|
|
426
532
|
expect(() =>
|
|
427
|
-
main([
|
|
428
|
-
).not.toThrow()
|
|
429
|
-
})
|
|
533
|
+
main(["--mode=summary", "--report-path=nonexistent.json"]),
|
|
534
|
+
).not.toThrow();
|
|
535
|
+
});
|
|
430
536
|
|
|
431
|
-
it(
|
|
432
|
-
const bad = path.join(tmpDir,
|
|
433
|
-
fs.writeFileSync(bad,
|
|
434
|
-
const { main } = await import(
|
|
435
|
-
expect(() =>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
})
|
|
439
|
-
})
|
|
537
|
+
it("propagates parse errors so CI surfaces real problems", async () => {
|
|
538
|
+
const bad = path.join(tmpDir, "bad.json");
|
|
539
|
+
fs.writeFileSync(bad, "{ this is not json");
|
|
540
|
+
const { main } = await import("./a11y-summary");
|
|
541
|
+
expect(() => main(["--mode=summary", `--report-path=${bad}`])).toThrow();
|
|
542
|
+
});
|
|
543
|
+
});
|