@lullabot/playwright-testing 1.0.0 → 1.0.1

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 +88 -76
  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 +139 -107
  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 +633 -433
  34. package/src/accessibility-baseline.ts +41 -21
  35. package/src/accessible-screenshot.test.ts +551 -423
  36. package/src/accessible-screenshot.ts +334 -245
  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 +659 -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,13 +1,13 @@
1
- import * as fs from 'fs'
2
- import * as path from 'path'
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
3
 
4
4
  /** Annotation types that identify accessibility-relevant test data. */
5
5
  const A11Y_ANNOTATION_TYPES = new Set([
6
- '@a11y',
7
- 'Accessibility',
8
- 'Baselined a11y violation',
9
- 'Stale a11y baseline entry',
10
- ])
6
+ "@a11y",
7
+ "Accessibility",
8
+ "Baselined a11y violation",
9
+ "Stale a11y baseline entry",
10
+ ]);
11
11
 
12
12
  /**
13
13
  * Escape a string for use inside a GitHub Actions workflow-command message
@@ -17,9 +17,9 @@ const A11Y_ANNOTATION_TYPES = new Set([
17
17
  */
18
18
  function escapeWorkflowCommand(value: string): string {
19
19
  return String(value)
20
- .replace(/%/g, '%25')
21
- .replace(/\r/g, '%0D')
22
- .replace(/\n/g, '%0A')
20
+ .replace(/%/g, "%25")
21
+ .replace(/\r/g, "%0D")
22
+ .replace(/\n/g, "%0A");
23
23
  }
24
24
 
25
25
  /**
@@ -28,9 +28,7 @@ function escapeWorkflowCommand(value: string): string {
28
28
  * escaping of `:` and `,` on top of the message-body escapes.
29
29
  */
30
30
  function escapeWorkflowCommandProperty(value: string): string {
31
- return escapeWorkflowCommand(value)
32
- .replace(/:/g, '%3A')
33
- .replace(/,/g, '%2C')
31
+ return escapeWorkflowCommand(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
34
32
  }
35
33
 
36
34
  /**
@@ -40,9 +38,9 @@ function escapeWorkflowCommandProperty(value: string): string {
40
38
  */
41
39
  function escapeMarkdownTableCell(value: string): string {
42
40
  return String(value)
43
- .replace(/\\/g, '\\\\')
44
- .replace(/\|/g, '\\|')
45
- .replace(/\r?\n/g, ' ')
41
+ .replace(/\\/g, "\\\\")
42
+ .replace(/\|/g, "\\|")
43
+ .replace(/\r?\n/g, " ");
46
44
  }
47
45
 
48
46
  /**
@@ -52,39 +50,39 @@ function escapeMarkdownTableCell(value: string): string {
52
50
  * with spaces, which is the standard CommonMark escape for code spans.
53
51
  */
54
52
  function escapeMarkdownCodeSpan(value: string): string {
55
- const s = String(value).replace(/\r?\n/g, ' ')
56
- if (!s.includes('`')) return `\`${s}\``
57
- return `\`\` ${s.replace(/``/g, '` `')} \`\``
53
+ const s = String(value).replace(/\r?\n/g, " ");
54
+ if (!s.includes("`")) return `\`${s}\``;
55
+ return `\`\` ${s.replace(/``/g, "` `")} \`\``;
58
56
  }
59
57
 
60
58
  /** A test that has accessibility-related annotations and/or attachments. */
61
59
  export interface A11yTestResult {
62
60
  /** Test title path, e.g. "a11y > homepage has accessibility violations" */
63
- title: string
61
+ title: string;
64
62
  /** Source file, e.g. "tests/a11y.spec.ts" */
65
- file: string
63
+ file: string;
66
64
  /** Line number in the source file. */
67
- line: number
65
+ line: number;
68
66
  /** Accessibility annotations from testInfo.annotations. */
69
- annotations: Array<{ type: string; description?: string }>
67
+ annotations: Array<{ type: string; description?: string }>;
70
68
  /** Parsed WCAG scan violations (from a11y-wcag-scan-results attachment). */
71
- violations: NormalizedViolation[]
69
+ violations: NormalizedViolation[];
72
70
  }
73
71
 
74
72
  interface NormalizedViolation {
75
- rule: string
76
- impact: string
77
- description: string
78
- helpUrl: string
79
- targets: string[]
73
+ rule: string;
74
+ impact: string;
75
+ description: string;
76
+ helpUrl: string;
77
+ targets: string[];
80
78
  }
81
79
 
82
80
  /** Parsed report with only accessibility-relevant data. */
83
81
  export interface A11yReport {
84
- tests: A11yTestResult[]
85
- totalViolations: number
86
- totalBaselined: number
87
- totalStale: number
82
+ tests: A11yTestResult[];
83
+ totalViolations: number;
84
+ totalBaselined: number;
85
+ totalStale: number;
88
86
  }
89
87
 
90
88
  /**
@@ -94,27 +92,27 @@ export function parseA11yResults(reportPath: string): A11yReport {
94
92
  if (!fs.existsSync(reportPath)) {
95
93
  throw new Error(
96
94
  `Playwright JSON report not found at ${reportPath}. ` +
97
- 'Ensure the Playwright JSON reporter is enabled.'
98
- )
95
+ "Ensure the Playwright JSON reporter is enabled.",
96
+ );
99
97
  }
100
98
 
101
- const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'))
102
- const tests: A11yTestResult[] = []
103
- let totalViolations = 0
104
- let totalBaselined = 0
105
- let totalStale = 0
99
+ const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
100
+ const tests: A11yTestResult[] = [];
101
+ let totalViolations = 0;
102
+ let totalBaselined = 0;
103
+ let totalStale = 0;
106
104
 
107
- walkSuites(report.suites ?? [], '', tests)
105
+ walkSuites(report.suites ?? [], "", tests);
108
106
 
109
107
  for (const test of tests) {
110
108
  for (const ann of test.annotations) {
111
- if (ann.type === 'Baselined a11y violation') totalBaselined++
112
- if (ann.type === 'Stale a11y baseline entry') totalStale++
109
+ if (ann.type === "Baselined a11y violation") totalBaselined++;
110
+ if (ann.type === "Stale a11y baseline entry") totalStale++;
113
111
  }
114
- totalViolations += test.violations.length
112
+ totalViolations += test.violations.length;
115
113
  }
116
114
 
117
- return { tests, totalViolations, totalBaselined, totalStale }
115
+ return { tests, totalViolations, totalBaselined, totalStale };
118
116
  }
119
117
 
120
118
  /**
@@ -124,52 +122,55 @@ export function parseA11yResults(reportPath: string): A11yReport {
124
122
  * on `result.annotations`. checkAccessibility() uses the latter.
125
123
  */
126
124
  function collectAnnotations(test: any, lastResult: any): any[] {
127
- return [
128
- ...(test.annotations ?? []),
129
- ...(lastResult?.annotations ?? []),
130
- ]
125
+ return [...(test.annotations ?? []), ...(lastResult?.annotations ?? [])];
131
126
  }
132
127
 
133
128
  /** Read and parse the WCAG scan attachment, if present. */
134
129
  function parseWcagViolations(attachment: any): NormalizedViolation[] {
135
- if (!attachment) return []
130
+ if (!attachment) return [];
136
131
  try {
137
132
  const raw = attachment.body
138
- ? Buffer.from(attachment.body, 'base64').toString('utf8')
133
+ ? Buffer.from(attachment.body, "base64").toString("utf8")
139
134
  : attachment.path
140
- ? fs.readFileSync(attachment.path, 'utf8')
141
- : null
142
- if (!raw) return []
143
- const json = JSON.parse(raw)
144
- if (!json?.violations) return []
135
+ ? fs.readFileSync(attachment.path, "utf8")
136
+ : null;
137
+ if (!raw) return [];
138
+ const json = JSON.parse(raw);
139
+ if (!json?.violations) return [];
145
140
  return json.violations.map((v: any) => ({
146
141
  rule: v.id,
147
- impact: v.impact ?? 'unknown',
142
+ impact: v.impact ?? "unknown",
148
143
  description: v.description,
149
144
  helpUrl: v.helpUrl,
150
- targets: v.nodes
151
- ?.flatMap((n: any) => n.target)
152
- .filter((t: any) => typeof t === 'string') ?? [],
153
- }))
145
+ targets:
146
+ v.nodes
147
+ ?.flatMap((n: any) => n.target)
148
+ .filter((t: any) => typeof t === "string") ?? [],
149
+ }));
154
150
  } catch {
155
151
  // Parse errors are non-fatal — we'll just report without violation details.
156
- return []
152
+ return [];
157
153
  }
158
154
  }
159
155
 
160
156
  /** Extract an A11yTestResult from a Playwright test, or null if not a11y-related. */
161
- function extractTestResult(spec: any, test: any, file: string): A11yTestResult | null {
162
- const lastResult = test.results?.[test.results.length - 1]
163
- const attachments = lastResult?.attachments ?? []
157
+ function extractTestResult(
158
+ spec: any,
159
+ test: any,
160
+ file: string,
161
+ ): A11yTestResult | null {
162
+ const lastResult = test.results?.[test.results.length - 1];
163
+ const attachments = lastResult?.attachments ?? [];
164
164
 
165
- const a11yAnnotations = collectAnnotations(test, lastResult)
166
- .filter((a: any) => A11Y_ANNOTATION_TYPES.has(a.type))
165
+ const a11yAnnotations = collectAnnotations(test, lastResult).filter(
166
+ (a: any) => A11Y_ANNOTATION_TYPES.has(a.type),
167
+ );
167
168
 
168
- if (a11yAnnotations.length === 0) return null
169
+ if (a11yAnnotations.length === 0) return null;
169
170
 
170
171
  const violations = parseWcagViolations(
171
- attachments.find((a: any) => a.name === 'a11y-wcag-scan-results'),
172
- )
172
+ attachments.find((a: any) => a.name === "a11y-wcag-scan-results"),
173
+ );
173
174
 
174
175
  return {
175
176
  title: spec.title,
@@ -177,66 +178,66 @@ function extractTestResult(spec: any, test: any, file: string): A11yTestResult |
177
178
  line: spec.line ?? 1,
178
179
  annotations: a11yAnnotations,
179
180
  violations,
180
- }
181
+ };
181
182
  }
182
183
 
183
- function walkSuites(
184
- suites: any[],
185
- parentFile: string,
186
- out: A11yTestResult[],
187
- ) {
184
+ function walkSuites(suites: any[], parentFile: string, out: A11yTestResult[]) {
188
185
  for (const suite of suites) {
189
- const file = suite.file || parentFile
186
+ const file = suite.file || parentFile;
190
187
 
191
188
  for (const spec of suite.specs ?? []) {
192
189
  for (const test of spec.tests ?? []) {
193
- const result = extractTestResult(spec, test, file)
194
- if (result) out.push(result)
190
+ const result = extractTestResult(spec, test, file);
191
+ if (result) out.push(result);
195
192
  }
196
193
  }
197
194
 
198
195
  if (suite.suites) {
199
- walkSuites(suite.suites, file, out)
196
+ walkSuites(suite.suites, file, out);
200
197
  }
201
198
  }
202
199
  }
203
200
 
204
201
  /** Headline with pass/fail counts. */
205
202
  function renderHeadline(report: A11yReport): string {
206
- const hasViolations = report.totalViolations > 0
207
- const hasBaselined = report.totalBaselined > 0
208
- const hasStale = report.totalStale > 0
203
+ const hasViolations = report.totalViolations > 0;
204
+ const hasBaselined = report.totalBaselined > 0;
205
+ const hasStale = report.totalStale > 0;
209
206
 
210
207
  if (!hasViolations && !hasBaselined && !hasStale) {
211
- return ':white_check_mark: All accessibility checks passed.\n'
208
+ return ":white_check_mark: All accessibility checks passed.\n";
212
209
  }
213
210
 
214
- const parts: string[] = []
215
- if (hasViolations) parts.push(`**${report.totalViolations}** violation(s)`)
216
- if (hasBaselined) parts.push(`**${report.totalBaselined}** baselined`)
217
- if (hasStale) parts.push(`**${report.totalStale}** stale baseline entries`)
218
- return parts.join(' · ') + '\n'
211
+ const parts: string[] = [];
212
+ if (hasViolations) parts.push(`**${report.totalViolations}** violation(s)`);
213
+ if (hasBaselined) parts.push(`**${report.totalBaselined}** baselined`);
214
+ if (hasStale) parts.push(`**${report.totalStale}** stale baseline entries`);
215
+ return parts.join(" · ") + "\n";
219
216
  }
220
217
 
221
218
  /** Icon for an `Accessibility` annotation based on its violation count. */
222
219
  function iconForAccessibilityAnnotation(description: string): string {
223
220
  // Descriptions look like "WCAG scan: 0 violations ..." or "WCAG scan: 3 violations ...".
224
- const colonIdx = description.lastIndexOf(': ')
225
- const afterColon = colonIdx >= 0 ? description.substring(colonIdx + 2) : description
226
- const violationCount = parseInt(afterColon, 10)
227
- return violationCount > 0 ? ':x:' : ':white_check_mark:'
221
+ const colonIdx = description.lastIndexOf(": ");
222
+ const afterColon =
223
+ colonIdx >= 0 ? description.substring(colonIdx + 2) : description;
224
+ const violationCount = parseInt(afterColon, 10);
225
+ return violationCount > 0 ? ":x:" : ":white_check_mark:";
228
226
  }
229
227
 
230
- function iconForAnnotation(ann: { type: string; description?: string }): string {
228
+ function iconForAnnotation(ann: {
229
+ type: string;
230
+ description?: string;
231
+ }): string {
231
232
  switch (ann.type) {
232
- case 'Baselined a11y violation':
233
- return ':white_check_mark:'
234
- case 'Stale a11y baseline entry':
235
- return ':warning:'
236
- case 'Accessibility':
237
- return iconForAccessibilityAnnotation(ann.description ?? '')
233
+ case "Baselined a11y violation":
234
+ return ":white_check_mark:";
235
+ case "Stale a11y baseline entry":
236
+ return ":warning:";
237
+ case "Accessibility":
238
+ return iconForAccessibilityAnnotation(ann.description ?? "");
238
239
  default:
239
- return ':information_source:'
240
+ return ":information_source:";
240
241
  }
241
242
  }
242
243
 
@@ -244,36 +245,39 @@ function iconForAnnotation(ann: { type: string; description?: string }): string
244
245
  function renderAnnotations(
245
246
  annotations: Array<{ type: string; description?: string }>,
246
247
  ): string[] {
247
- const lines: string[] = []
248
- const seen = new Set<string>()
248
+ const lines: string[] = [];
249
+ const seen = new Set<string>();
249
250
  for (const ann of annotations) {
250
- const key = `${ann.type}:${ann.description ?? ''}`
251
- if (seen.has(key)) continue
252
- seen.add(key)
253
- lines.push(`${iconForAnnotation(ann)} **${ann.type}**: ${ann.description ?? ''}\n`)
251
+ const key = `${ann.type}:${ann.description ?? ""}`;
252
+ if (seen.has(key)) continue;
253
+ seen.add(key);
254
+ lines.push(
255
+ `${iconForAnnotation(ann)} **${ann.type}**: ${ann.description ?? ""}\n`,
256
+ );
254
257
  }
255
- return lines
258
+ return lines;
256
259
  }
257
260
 
258
261
  /** Render the violation table for a single test. */
259
262
  function renderViolationTable(violations: NormalizedViolation[]): string[] {
260
- if (violations.length === 0) return []
263
+ if (violations.length === 0) return [];
261
264
  const lines: string[] = [
262
- '| Rule | Impact | Description | Targets |',
263
- '|------|--------|-------------|---------|',
264
- ]
265
+ "| Rule | Impact | Description | Targets |",
266
+ "|------|--------|-------------|---------|",
267
+ ];
265
268
  for (const v of violations) {
266
- const shown = v.targets.slice(0, 3).map(escapeMarkdownCodeSpan).join(', ')
267
- const targets = v.targets.length > 3
268
- ? `${shown}, +${v.targets.length - 3} more`
269
- : shown
270
- const rule = escapeMarkdownTableCell(v.rule)
271
- const impact = escapeMarkdownTableCell(v.impact)
272
- const description = escapeMarkdownTableCell(v.description)
273
- lines.push(`| [${rule}](${v.helpUrl}) | ${impact} | ${description} | ${targets} |`)
269
+ const shown = v.targets.slice(0, 3).map(escapeMarkdownCodeSpan).join(", ");
270
+ const targets =
271
+ v.targets.length > 3 ? `${shown}, +${v.targets.length - 3} more` : shown;
272
+ const rule = escapeMarkdownTableCell(v.rule);
273
+ const impact = escapeMarkdownTableCell(v.impact);
274
+ const description = escapeMarkdownTableCell(v.description);
275
+ lines.push(
276
+ `| [${rule}](${v.helpUrl}) | ${impact} | ${description} | ${targets} |`,
277
+ );
274
278
  }
275
- lines.push('')
276
- return lines
279
+ lines.push("");
280
+ return lines;
277
281
  }
278
282
 
279
283
  /**
@@ -281,29 +285,32 @@ function renderViolationTable(violations: NormalizedViolation[]): string[] {
281
285
  */
282
286
  export function generateSummary(report: A11yReport): string {
283
287
  if (report.tests.length === 0) {
284
- return '## Accessibility Results\n\nNo accessibility tests were found in the report.\n'
288
+ return "## Accessibility Results\n\nNo accessibility tests were found in the report.\n";
285
289
  }
286
290
 
287
- const lines: string[] = ['## Accessibility Results\n', renderHeadline(report)]
288
- const seenTitles = new Set<string>()
291
+ const lines: string[] = [
292
+ "## Accessibility Results\n",
293
+ renderHeadline(report),
294
+ ];
295
+ const seenTitles = new Set<string>();
289
296
 
290
297
  for (const test of report.tests) {
291
- const testAnnotations = test.annotations.filter(a => a.type !== '@a11y')
292
- const hasTestViolations = test.violations.length > 0
293
- if (testAnnotations.length === 0 && !hasTestViolations) continue
298
+ const testAnnotations = test.annotations.filter((a) => a.type !== "@a11y");
299
+ const hasTestViolations = test.violations.length > 0;
300
+ if (testAnnotations.length === 0 && !hasTestViolations) continue;
294
301
 
295
302
  // Skip duplicate test titles (different browsers report the same spec).
296
- if (seenTitles.has(test.title)) continue
297
- seenTitles.add(test.title)
303
+ if (seenTitles.has(test.title)) continue;
304
+ seenTitles.add(test.title);
298
305
 
299
- const testLines: string[] = [`### ${test.title}\n`]
300
- testLines.push(...renderAnnotations(testAnnotations))
301
- testLines.push(...renderViolationTable(test.violations))
306
+ const testLines: string[] = [`### ${test.title}\n`];
307
+ testLines.push(...renderAnnotations(testAnnotations));
308
+ testLines.push(...renderViolationTable(test.violations));
302
309
 
303
- lines.push(...testLines)
310
+ lines.push(...testLines);
304
311
  }
305
312
 
306
- return lines.join('\n')
313
+ return lines.join("\n");
307
314
  }
308
315
 
309
316
  /**
@@ -314,33 +321,35 @@ export function generateSummary(report: A11yReport): string {
314
321
  * that would otherwise let a crafted page forge additional workflow commands.
315
322
  */
316
323
  export function generateAnnotations(report: A11yReport): string {
317
- const lines: string[] = []
324
+ const lines: string[] = [];
318
325
 
319
326
  for (const test of report.tests) {
320
- const file = escapeWorkflowCommandProperty(test.file)
321
- const line = escapeWorkflowCommandProperty(String(test.line))
327
+ const file = escapeWorkflowCommandProperty(test.file);
328
+ const line = escapeWorkflowCommandProperty(String(test.line));
322
329
 
323
330
  // Emit ::error for each violation.
324
331
  for (const v of test.violations) {
325
- const targets = v.targets.slice(0, 3).join(', ')
326
- const title = escapeWorkflowCommandProperty(`${v.rule} (${v.impact})`)
332
+ const targets = v.targets.slice(0, 3).join(", ");
333
+ const title = escapeWorkflowCommandProperty(`${v.rule} (${v.impact})`);
327
334
  const msg = escapeWorkflowCommand(
328
335
  `a11y: ${v.rule} (${v.impact}) — ${v.description}. Targets: ${targets}`,
329
- )
330
- lines.push(`::error file=${file},line=${line},title=${title}::${msg}`)
336
+ );
337
+ lines.push(`::error file=${file},line=${line},title=${title}::${msg}`);
331
338
  }
332
339
 
333
340
  // Emit ::warning for stale baseline entries.
334
341
  for (const ann of test.annotations) {
335
- if (ann.type === 'Stale a11y baseline entry') {
336
- const title = escapeWorkflowCommandProperty('Stale a11y baseline')
337
- const msg = escapeWorkflowCommand(ann.description ?? '')
338
- lines.push(`::warning file=${file},line=${line},title=${title}::${msg}`)
342
+ if (ann.type === "Stale a11y baseline entry") {
343
+ const title = escapeWorkflowCommandProperty("Stale a11y baseline");
344
+ const msg = escapeWorkflowCommand(ann.description ?? "");
345
+ lines.push(
346
+ `::warning file=${file},line=${line},title=${title}::${msg}`,
347
+ );
339
348
  }
340
349
  }
341
350
  }
342
351
 
343
- return lines.join('\n')
352
+ return lines.join("\n");
344
353
  }
345
354
 
346
355
  /**
@@ -352,34 +361,35 @@ export function main(
352
361
  args: string[] = process.argv.slice(2),
353
362
  adapter: { commandName?: string } = {},
354
363
  ): void {
355
- if (args.includes('--help') || args.includes('-h')) {
364
+ if (args.includes("--help") || args.includes("-h")) {
356
365
  process.stdout.write(
357
- `Usage: ${adapter.commandName ?? 'playwright-testing-a11y-summary'} [options]\n\n` +
358
- 'Options:\n' +
359
- ' --report-path=PATH Playwright JSON report (default: test-results/results.json)\n' +
360
- ' --mode=summary Write a Markdown accessibility summary (default)\n' +
361
- ' --mode=annotations Write GitHub Actions annotations\n' +
362
- ' -h, --help Show this help\n',
363
- )
364
- return
366
+ `Usage: ${adapter.commandName ?? "playwright-testing-a11y-summary"} [options]\n\n` +
367
+ "Options:\n" +
368
+ " --report-path=PATH Playwright JSON report (default: test-results/results.json)\n" +
369
+ " --mode=summary Write a Markdown accessibility summary (default)\n" +
370
+ " --mode=annotations Write GitHub Actions annotations\n" +
371
+ " -h, --help Show this help\n",
372
+ );
373
+ return;
365
374
  }
366
375
 
367
- let mode = 'summary'
368
- let reportPath = 'test-results/results.json'
376
+ let mode = "summary";
377
+ let reportPath = "test-results/results.json";
369
378
 
370
379
  for (const arg of args) {
371
- if (arg.startsWith('--mode=')) mode = arg.slice('--mode='.length)
372
- if (arg.startsWith('--report-path=')) reportPath = arg.slice('--report-path='.length)
380
+ if (arg.startsWith("--mode=")) mode = arg.slice("--mode=".length);
381
+ if (arg.startsWith("--report-path="))
382
+ reportPath = arg.slice("--report-path=".length);
373
383
  }
374
384
 
375
- const validModes = new Set(['summary', 'annotations'])
385
+ const validModes = new Set(["summary", "annotations"]);
376
386
  if (!validModes.has(mode)) {
377
- console.error(`Unknown mode: ${mode}. Use summary or annotations.`)
378
- process.exit(2)
387
+ console.error(`Unknown mode: ${mode}. Use summary or annotations.`);
388
+ process.exit(2);
379
389
  }
380
390
 
381
391
  // Resolve relative to cwd.
382
- const resolvedPath = path.resolve(reportPath)
392
+ const resolvedPath = path.resolve(reportPath);
383
393
 
384
394
  // Missing reports are intentionally non-fatal — the JSON report may be
385
395
  // absent if no tests ran. Any other failure (parse error, permissions, etc.)
@@ -388,34 +398,34 @@ export function main(
388
398
  if (!fs.existsSync(resolvedPath)) {
389
399
  console.error(
390
400
  `Playwright JSON report not found at ${resolvedPath} — skipping a11y summary.`,
391
- )
392
- return
401
+ );
402
+ return;
393
403
  }
394
404
 
395
- const report = parseA11yResults(resolvedPath)
405
+ const report = parseA11yResults(resolvedPath);
396
406
 
397
407
  switch (mode) {
398
- case 'summary': {
399
- const md = generateSummary(report)
400
- const summaryFile = process.env.GITHUB_STEP_SUMMARY
408
+ case "summary": {
409
+ const md = generateSummary(report);
410
+ const summaryFile = process.env.GITHUB_STEP_SUMMARY;
401
411
  if (summaryFile) {
402
- fs.appendFileSync(summaryFile, md)
403
- console.log('Accessibility summary written to $GITHUB_STEP_SUMMARY')
412
+ fs.appendFileSync(summaryFile, md);
413
+ console.log("Accessibility summary written to $GITHUB_STEP_SUMMARY");
404
414
  } else {
405
415
  // Not in GitHub Actions — just print.
406
- process.stdout.write(md)
416
+ process.stdout.write(md);
407
417
  }
408
- break
418
+ break;
409
419
  }
410
- case 'annotations': {
411
- const output = generateAnnotations(report)
412
- if (output) process.stdout.write(output + '\n')
413
- break
420
+ case "annotations": {
421
+ const output = generateAnnotations(report);
422
+ if (output) process.stdout.write(output + "\n");
423
+ break;
414
424
  }
415
425
  }
416
426
  }
417
427
 
418
428
  // Auto-invoke when run directly (node lib/github/a11y-summary.js).
419
429
  if (require.main === module) {
420
- main()
430
+ main();
421
431
  }