@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.
Files changed (68) hide show
  1. package/bin/github-a11y-summary +2 -2
  2. package/bin/github-failure-summary +4 -4
  3. package/lib/accessibility-baseline-file.d.ts +6 -6
  4. package/lib/accessibility-baseline-file.js +29 -23
  5. package/lib/accessibility-baseline.js +5 -3
  6. package/lib/accessible-screenshot.d.ts +4 -4
  7. package/lib/accessible-screenshot.js +110 -88
  8. package/lib/focus.js +4 -1
  9. package/lib/frames.js +4 -4
  10. package/lib/github/a11y-summary.js +74 -79
  11. package/lib/github/attachments.d.ts +1 -1
  12. package/lib/github/attachments.js +36 -34
  13. package/lib/github/failure-summary.d.ts +4 -4
  14. package/lib/github/failure-summary.js +140 -108
  15. package/lib/github/index.d.ts +6 -6
  16. package/lib/github/report-paths.js +17 -9
  17. package/lib/hover.js +17 -17
  18. package/lib/images.js +15 -12
  19. package/lib/index.d.ts +13 -13
  20. package/lib/interaction-states.d.ts +2 -2
  21. package/lib/interaction-states.js +7 -7
  22. package/lib/mock/index.d.ts +1 -1
  23. package/lib/mock/youtube.d.ts +1 -1
  24. package/lib/mock/youtube.js +1 -1
  25. package/lib/pseudo-state.d.ts +2 -2
  26. package/lib/pseudo-state.js +15 -7
  27. package/lib/videos.js +21 -10
  28. package/lib/visualdiff.d.ts +4 -4
  29. package/lib/visualdiff.js +35 -27
  30. package/package.json +3 -3
  31. package/src/accessibility-baseline-file.test.ts +190 -154
  32. package/src/accessibility-baseline-file.ts +107 -83
  33. package/src/accessibility-baseline.test.ts +869 -433
  34. package/src/accessibility-baseline.ts +41 -21
  35. package/src/accessible-screenshot.test.ts +551 -423
  36. package/src/accessible-screenshot.ts +358 -251
  37. package/src/focus.test.ts +23 -23
  38. package/src/focus.ts +7 -2
  39. package/src/fonts.test.ts +13 -13
  40. package/src/fonts.ts +1 -1
  41. package/src/frames.test.ts +10 -8
  42. package/src/frames.ts +5 -5
  43. package/src/github/a11y-summary.test.ts +436 -332
  44. package/src/github/a11y-summary.ts +189 -179
  45. package/src/github/attachments.test.ts +228 -167
  46. package/src/github/attachments.ts +144 -121
  47. package/src/github/failure-summary.test.ts +660 -445
  48. package/src/github/failure-summary.ts +376 -274
  49. package/src/github/index.test.ts +14 -14
  50. package/src/github/index.ts +6 -6
  51. package/src/github/report-paths.test.ts +211 -156
  52. package/src/github/report-paths.ts +75 -61
  53. package/src/hover.test.ts +50 -46
  54. package/src/hover.ts +18 -18
  55. package/src/images.test.ts +251 -206
  56. package/src/images.ts +52 -43
  57. package/src/index.ts +13 -13
  58. package/src/interaction-states.test.ts +72 -42
  59. package/src/interaction-states.ts +43 -29
  60. package/src/mock/index.ts +1 -1
  61. package/src/mock/youtube.test.ts +26 -26
  62. package/src/mock/youtube.ts +4 -6
  63. package/src/pseudo-state.test.ts +58 -51
  64. package/src/pseudo-state.ts +32 -27
  65. package/src/videos.test.ts +559 -399
  66. package/src/videos.ts +68 -44
  67. package/src/visualdiff.test.ts +416 -315
  68. package/src/visualdiff.ts +140 -99
@@ -1,439 +1,543 @@
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 { parseA11yResults, generateSummary, generateAnnotations } from './a11y-summary'
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(overrides: {
9
- violations?: any[]
10
- annotations?: any[]
11
- attachments?: any[]
12
- specTitle?: string
13
- file?: string
14
- line?: number
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
- violations = [],
18
- annotations = [{ type: '@a11y' }],
23
+ annotations = [{ type: "@a11y" }],
19
24
  attachments = [],
20
- specTitle = 'homepage accessibility check',
21
- file = 'tests/a11y.spec.ts',
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
- title: '',
28
- file,
29
- specs: [{
30
- title: specTitle,
31
- line,
32
- tests: [{
33
- annotations: [],
34
- results: [{
35
- // Playwright stores runtime annotations (from testInfo.annotations.push())
36
- // at the result level, not the test level.
37
- annotations,
38
- attachments,
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(JSON.stringify({
48
- violations: violations.map(v => ({
49
- id: v.rule ?? 'color-contrast',
50
- impact: v.impact ?? 'serious',
51
- description: v.description ?? 'Elements must have sufficient color contrast',
52
- helpUrl: v.helpUrl ?? 'https://dequeuniversity.com/rules/axe/4.7/color-contrast',
53
- nodes: (v.targets ?? ['#main']).map((t: string) => ({ target: [t] })),
54
- })),
55
- })).toString('base64')
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: 'a11y-wcag-scan-results',
76
+ name: "a11y-wcag-scan-results",
59
77
  body,
60
- contentType: 'application/json',
61
- }
78
+ contentType: "application/json",
79
+ };
62
80
  }
63
81
 
64
- describe('parseA11yResults', () => {
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(), 'a11y-summary-'))
70
- reportPath = path.join(tmpDir, 'test-report.json')
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('throws when report file does not exist', () => {
78
- expect(() => parseA11yResults('/nonexistent/report.json')).toThrow(
79
- 'Playwright JSON report not found',
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('returns empty report when no a11y tests exist', () => {
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('extracts tests with a11y annotations', () => {
109
+ it("extracts tests with a11y annotations", () => {
92
110
  const report = makeReport({
93
111
  annotations: [
94
- { type: '@a11y' },
95
- { type: 'Accessibility', description: 'WCAG scan: 1 violations (50 rules passed)' },
112
+ { type: "@a11y" },
113
+ {
114
+ type: "Accessibility",
115
+ description: "WCAG scan: 1 violations (50 rules passed)",
116
+ },
96
117
  ],
97
- attachments: [makeWcagAttachment([{ rule: 'color-contrast', targets: ['#header'] }])],
98
- })
99
- fs.writeFileSync(reportPath, JSON.stringify(report))
100
- const result = parseA11yResults(reportPath)
101
- expect(result.tests).toHaveLength(1)
102
- expect(result.tests[0].violations).toHaveLength(1)
103
- expect(result.tests[0].violations[0].rule).toBe('color-contrast')
104
- expect(result.totalViolations).toBe(1)
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('counts baselined and stale annotations', () => {
130
+ it("counts baselined and stale annotations", () => {
108
131
  const report = makeReport({
109
132
  annotations: [
110
- { type: '@a11y' },
111
- { type: 'Baselined a11y violation', description: 'color-contrast: known issue' },
112
- { type: 'Baselined a11y violation', description: 'link-name: known issue' },
113
- { type: 'Stale a11y baseline entry', description: 'image-alt on .hero — no longer detected' },
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('handles nested suites', () => {
154
+ it("handles nested suites", () => {
123
155
  const report = {
124
- suites: [{
125
- title: 'outer',
126
- file: 'tests/a11y.spec.ts',
127
- specs: [],
128
- suites: [{
129
- title: 'inner',
130
- specs: [{
131
- title: 'nested test',
132
- line: 5,
133
- tests: [{
134
- annotations: [{ type: '@a11y' }, { type: 'Accessibility', description: 'passed' }],
135
- results: [{ attachments: [] }],
136
- }],
137
- }],
138
- }],
139
- }],
140
- }
141
- fs.writeFileSync(reportPath, JSON.stringify(report))
142
- const result = parseA11yResults(reportPath)
143
- expect(result.tests).toHaveLength(1)
144
- expect(result.tests[0].file).toBe('tests/a11y.spec.ts')
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('generateSummary', () => {
149
- it('shows green check when no violations', () => {
191
+ describe("generateSummary", () => {
192
+ it("shows green check when no violations", () => {
150
193
  const md = generateSummary({
151
- tests: [{
152
- title: 'passes a11y',
153
- file: 'tests/a11y.spec.ts',
154
- line: 10,
155
- annotations: [{ type: 'Accessibility', description: 'WCAG scan: 0 violations (50 rules passed)' }],
156
- violations: [],
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(':white_check_mark:')
163
- expect(md).toContain('All accessibility checks passed')
164
- })
211
+ });
212
+ expect(md).toContain(":white_check_mark:");
213
+ expect(md).toContain("All accessibility checks passed");
214
+ });
165
215
 
166
- it('shows violation table when violations exist', () => {
216
+ it("shows violation table when violations exist", () => {
167
217
  const md = generateSummary({
168
- tests: [{
169
- title: 'homepage check',
170
- file: 'tests/a11y.spec.ts',
171
- line: 10,
172
- annotations: [{ type: 'Accessibility', description: 'WCAG scan: 1 violations' }],
173
- violations: [{
174
- rule: 'color-contrast',
175
- impact: 'serious',
176
- description: 'Elements must have sufficient color contrast',
177
- helpUrl: 'https://dequeuniversity.com/rules/axe/4.7/color-contrast',
178
- targets: ['#header'],
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('**1** violation(s)')
186
- expect(md).toContain('color-contrast')
187
- expect(md).toContain('| Rule |')
188
- expect(md).toContain('`#header`')
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('returns message when no tests found', () => {
192
- const md = generateSummary({ tests: [], totalViolations: 0, totalBaselined: 0, totalStale: 0 })
193
- expect(md).toContain('No accessibility tests were found')
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('shows baselined and stale counts', () => {
258
+ it("shows baselined and stale counts", () => {
197
259
  const md = generateSummary({
198
- tests: [{
199
- title: 'baseline test',
200
- file: 'test.ts',
201
- line: 1,
202
- annotations: [
203
- { type: 'Baselined a11y violation', description: 'known issue' },
204
- { type: 'Stale a11y baseline entry', description: 'no longer detected' },
205
- ],
206
- violations: [],
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('**1** baselined')
213
- expect(md).toContain('**1** stale baseline entries')
214
- })
215
-
216
- })
278
+ });
279
+ expect(md).toContain("**1** baselined");
280
+ expect(md).toContain("**1** stale baseline entries");
281
+ });
282
+ });
217
283
 
218
- describe('generateAnnotations', () => {
219
- it('emits ::error for violations', () => {
284
+ describe("generateAnnotations", () => {
285
+ it("emits ::error for violations", () => {
220
286
  const output = generateAnnotations({
221
- tests: [{
222
- title: 'test',
223
- file: 'tests/a11y.spec.ts',
224
- line: 15,
225
- annotations: [],
226
- violations: [{
227
- rule: 'color-contrast',
228
- impact: 'serious',
229
- description: 'Elements must have sufficient color contrast',
230
- helpUrl: 'https://example.com',
231
- targets: ['#main'],
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('::error file=tests/a11y.spec.ts,line=15,title=color-contrast (serious)')
239
- expect(output).toContain('#main')
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('emits ::warning for stale baselines', () => {
314
+ it("emits ::warning for stale baselines", () => {
243
315
  const output = generateAnnotations({
244
- tests: [{
245
- title: 'test',
246
- file: 'tests/a11y.spec.ts',
247
- line: 10,
248
- annotations: [{ type: 'Stale a11y baseline entry', description: 'no longer detected' }],
249
- violations: [],
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('::warning file=tests/a11y.spec.ts,line=10,title=Stale a11y baseline')
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('returns empty string when no violations or stale entries', () => {
339
+ it("returns empty string when no violations or stale entries", () => {
259
340
  const output = generateAnnotations({
260
- tests: [{
261
- title: 'test',
262
- file: 'test.ts',
263
- line: 1,
264
- annotations: [{ type: 'Accessibility', description: 'passed' }],
265
- violations: [],
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('workflow-command injection hardening', () => {
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('escapes newlines and % in ::error output', () => {
361
+ it("escapes newlines and % in ::error output", () => {
279
362
  const output = generateAnnotations({
280
- tests: [{
281
- title: 'test',
282
- file: 'tests/a11y.spec.ts',
283
- line: 15,
284
- annotations: [],
285
- violations: [{
286
- rule: 'color-contrast',
287
- impact: 'serious',
288
- description: 'bad\ndescription with 100%',
289
- helpUrl: 'https://example.com',
290
- targets: ['#evil\n::error title=pwned::gotcha'],
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('\n')).toHaveLength(1)
299
- expect(output).toContain('%0A')
300
- expect(output).toContain('%25')
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('escapes : and , in property values', () => {
392
+ it("escapes : and , in property values", () => {
306
393
  const output = generateAnnotations({
307
- tests: [{
308
- title: 'test',
309
- file: 'tests/with,comma:colon.spec.ts',
310
- line: 1,
311
- annotations: [],
312
- violations: [{
313
- rule: 'color-contrast',
314
- impact: 'serious',
315
- description: 'ok',
316
- helpUrl: 'https://example.com',
317
- targets: ['#main'],
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('file=tests/with%2Ccomma%3Acolon.spec.ts')
325
- })
414
+ });
415
+ expect(output).toContain("file=tests/with%2Ccomma%3Acolon.spec.ts");
416
+ });
326
417
 
327
- it('escapes stale baseline warning content', () => {
418
+ it("escapes stale baseline warning content", () => {
328
419
  const output = generateAnnotations({
329
- tests: [{
330
- title: 'test',
331
- file: 'tests/a11y.spec.ts',
332
- line: 10,
333
- annotations: [{ type: 'Stale a11y baseline entry', description: 'stale\n::error::forged' }],
334
- violations: [],
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('\n')).toHaveLength(1)
341
- expect(output).toContain('%0A')
342
- })
343
- })
437
+ });
438
+ expect(output.split("\n")).toHaveLength(1);
439
+ expect(output).toContain("%0A");
440
+ });
441
+ });
344
442
 
345
- describe('markdown escaping in the summary table', () => {
346
- it('escapes pipes and newlines in description cells', () => {
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
- title: 'homepage',
350
- file: 'tests/a11y.spec.ts',
351
- line: 10,
352
- annotations: [],
353
- violations: [{
354
- rule: 'color-contrast',
355
- impact: 'serious',
356
- description: 'pipe | in | description\nand a newline',
357
- helpUrl: 'https://example.com',
358
- targets: ['#main'],
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('\n').find(l => l.startsWith('| [color-contrast]'))
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(' | ').length).toBe(4)
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('escapes backticks in target code spans', () => {
477
+ it("escapes backticks in target code spans", () => {
376
478
  const md = generateSummary({
377
- tests: [{
378
- title: 'homepage',
379
- file: 'tests/a11y.spec.ts',
380
- line: 10,
381
- annotations: [],
382
- violations: [{
383
- rule: 'color-contrast',
384
- impact: 'serious',
385
- description: 'bad',
386
- helpUrl: 'https://example.com',
387
- // A target containing a backtick — would escape a naive `${t}` code span.
388
- targets: ['#foo`span'],
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('`` #foo`span ``')
400
- })
401
- })
505
+ expect(md).toContain("`` #foo`span ``");
506
+ });
507
+ });
402
508
 
403
- describe('main() error handling', () => {
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(), 'a11y-main-'))
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('returns without throwing when the report is missing', async () => {
425
- const { main } = await import('./a11y-summary')
530
+ it("returns without throwing when the report is missing", async () => {
531
+ const { main } = await import("./a11y-summary");
426
532
  expect(() =>
427
- main(['--mode=summary', '--report-path=nonexistent.json']),
428
- ).not.toThrow()
429
- })
533
+ main(["--mode=summary", "--report-path=nonexistent.json"]),
534
+ ).not.toThrow();
535
+ });
430
536
 
431
- it('propagates parse errors so CI surfaces real problems', async () => {
432
- const bad = path.join(tmpDir, 'bad.json')
433
- fs.writeFileSync(bad, '{ this is not json')
434
- const { main } = await import('./a11y-summary')
435
- expect(() =>
436
- main(['--mode=summary', `--report-path=${bad}`]),
437
- ).toThrow()
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
+ });