@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
@@ -41,10 +41,10 @@ const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  /** Annotation types that identify accessibility-relevant test data. */
43
43
  const A11Y_ANNOTATION_TYPES = new Set([
44
- '@a11y',
45
- 'Accessibility',
46
- 'Baselined a11y violation',
47
- 'Stale a11y baseline entry',
44
+ "@a11y",
45
+ "Accessibility",
46
+ "Baselined a11y violation",
47
+ "Stale a11y baseline entry",
48
48
  ]);
49
49
  /**
50
50
  * Escape a string for use inside a GitHub Actions workflow-command message
@@ -54,9 +54,9 @@ const A11Y_ANNOTATION_TYPES = new Set([
54
54
  */
55
55
  function escapeWorkflowCommand(value) {
56
56
  return String(value)
57
- .replace(/%/g, '%25')
58
- .replace(/\r/g, '%0D')
59
- .replace(/\n/g, '%0A');
57
+ .replace(/%/g, "%25")
58
+ .replace(/\r/g, "%0D")
59
+ .replace(/\n/g, "%0A");
60
60
  }
61
61
  /**
62
62
  * Escape a string for use inside a GitHub Actions workflow-command property
@@ -64,9 +64,7 @@ function escapeWorkflowCommand(value) {
64
64
  * escaping of `:` and `,` on top of the message-body escapes.
65
65
  */
66
66
  function escapeWorkflowCommandProperty(value) {
67
- return escapeWorkflowCommand(value)
68
- .replace(/:/g, '%3A')
69
- .replace(/,/g, '%2C');
67
+ return escapeWorkflowCommand(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
70
68
  }
71
69
  /**
72
70
  * Escape a string for use inside a Markdown table cell. Backslashes must be
@@ -75,9 +73,9 @@ function escapeWorkflowCommandProperty(value) {
75
73
  */
76
74
  function escapeMarkdownTableCell(value) {
77
75
  return String(value)
78
- .replace(/\\/g, '\\\\')
79
- .replace(/\|/g, '\\|')
80
- .replace(/\r?\n/g, ' ');
76
+ .replace(/\\/g, "\\\\")
77
+ .replace(/\|/g, "\\|")
78
+ .replace(/\r?\n/g, " ");
81
79
  }
82
80
  /**
83
81
  * Escape a string for use inside a Markdown inline code span (backticks).
@@ -86,10 +84,10 @@ function escapeMarkdownTableCell(value) {
86
84
  * with spaces, which is the standard CommonMark escape for code spans.
87
85
  */
88
86
  function escapeMarkdownCodeSpan(value) {
89
- const s = String(value).replace(/\r?\n/g, ' ');
90
- if (!s.includes('`'))
87
+ const s = String(value).replace(/\r?\n/g, " ");
88
+ if (!s.includes("`"))
91
89
  return `\`${s}\``;
92
- return `\`\` ${s.replace(/``/g, '` `')} \`\``;
90
+ return `\`\` ${s.replace(/``/g, "` `")} \`\``;
93
91
  }
94
92
  /**
95
93
  * Parse a Playwright JSON report and extract accessibility-related test data.
@@ -97,19 +95,19 @@ function escapeMarkdownCodeSpan(value) {
97
95
  function parseA11yResults(reportPath) {
98
96
  if (!fs.existsSync(reportPath)) {
99
97
  throw new Error(`Playwright JSON report not found at ${reportPath}. ` +
100
- 'Ensure the Playwright JSON reporter is enabled.');
98
+ "Ensure the Playwright JSON reporter is enabled.");
101
99
  }
102
- const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
100
+ const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
103
101
  const tests = [];
104
102
  let totalViolations = 0;
105
103
  let totalBaselined = 0;
106
104
  let totalStale = 0;
107
- walkSuites(report.suites ?? [], '', tests);
105
+ walkSuites(report.suites ?? [], "", tests);
108
106
  for (const test of tests) {
109
107
  for (const ann of test.annotations) {
110
- if (ann.type === 'Baselined a11y violation')
108
+ if (ann.type === "Baselined a11y violation")
111
109
  totalBaselined++;
112
- if (ann.type === 'Stale a11y baseline entry')
110
+ if (ann.type === "Stale a11y baseline entry")
113
111
  totalStale++;
114
112
  }
115
113
  totalViolations += test.violations.length;
@@ -123,10 +121,7 @@ function parseA11yResults(reportPath) {
123
121
  * on `result.annotations`. checkAccessibility() uses the latter.
124
122
  */
125
123
  function collectAnnotations(test, lastResult) {
126
- return [
127
- ...(test.annotations ?? []),
128
- ...(lastResult?.annotations ?? []),
129
- ];
124
+ return [...(test.annotations ?? []), ...(lastResult?.annotations ?? [])];
130
125
  }
131
126
  /** Read and parse the WCAG scan attachment, if present. */
132
127
  function parseWcagViolations(attachment) {
@@ -134,9 +129,9 @@ function parseWcagViolations(attachment) {
134
129
  return [];
135
130
  try {
136
131
  const raw = attachment.body
137
- ? Buffer.from(attachment.body, 'base64').toString('utf8')
132
+ ? Buffer.from(attachment.body, "base64").toString("utf8")
138
133
  : attachment.path
139
- ? fs.readFileSync(attachment.path, 'utf8')
134
+ ? fs.readFileSync(attachment.path, "utf8")
140
135
  : null;
141
136
  if (!raw)
142
137
  return [];
@@ -145,12 +140,12 @@ function parseWcagViolations(attachment) {
145
140
  return [];
146
141
  return json.violations.map((v) => ({
147
142
  rule: v.id,
148
- impact: v.impact ?? 'unknown',
143
+ impact: v.impact ?? "unknown",
149
144
  description: v.description,
150
145
  helpUrl: v.helpUrl,
151
146
  targets: v.nodes
152
147
  ?.flatMap((n) => n.target)
153
- .filter((t) => typeof t === 'string') ?? [],
148
+ .filter((t) => typeof t === "string") ?? [],
154
149
  }));
155
150
  }
156
151
  catch {
@@ -162,11 +157,10 @@ function parseWcagViolations(attachment) {
162
157
  function extractTestResult(spec, test, file) {
163
158
  const lastResult = test.results?.[test.results.length - 1];
164
159
  const attachments = lastResult?.attachments ?? [];
165
- const a11yAnnotations = collectAnnotations(test, lastResult)
166
- .filter((a) => A11Y_ANNOTATION_TYPES.has(a.type));
160
+ const a11yAnnotations = collectAnnotations(test, lastResult).filter((a) => A11Y_ANNOTATION_TYPES.has(a.type));
167
161
  if (a11yAnnotations.length === 0)
168
162
  return null;
169
- const violations = parseWcagViolations(attachments.find((a) => a.name === 'a11y-wcag-scan-results'));
163
+ const violations = parseWcagViolations(attachments.find((a) => a.name === "a11y-wcag-scan-results"));
170
164
  return {
171
165
  title: spec.title,
172
166
  file,
@@ -196,7 +190,7 @@ function renderHeadline(report) {
196
190
  const hasBaselined = report.totalBaselined > 0;
197
191
  const hasStale = report.totalStale > 0;
198
192
  if (!hasViolations && !hasBaselined && !hasStale) {
199
- return ':white_check_mark: All accessibility checks passed.\n';
193
+ return ":white_check_mark: All accessibility checks passed.\n";
200
194
  }
201
195
  const parts = [];
202
196
  if (hasViolations)
@@ -205,26 +199,26 @@ function renderHeadline(report) {
205
199
  parts.push(`**${report.totalBaselined}** baselined`);
206
200
  if (hasStale)
207
201
  parts.push(`**${report.totalStale}** stale baseline entries`);
208
- return parts.join(' · ') + '\n';
202
+ return parts.join(" · ") + "\n";
209
203
  }
210
204
  /** Icon for an `Accessibility` annotation based on its violation count. */
211
205
  function iconForAccessibilityAnnotation(description) {
212
206
  // Descriptions look like "WCAG scan: 0 violations ..." or "WCAG scan: 3 violations ...".
213
- const colonIdx = description.lastIndexOf(': ');
207
+ const colonIdx = description.lastIndexOf(": ");
214
208
  const afterColon = colonIdx >= 0 ? description.substring(colonIdx + 2) : description;
215
209
  const violationCount = parseInt(afterColon, 10);
216
- return violationCount > 0 ? ':x:' : ':white_check_mark:';
210
+ return violationCount > 0 ? ":x:" : ":white_check_mark:";
217
211
  }
218
212
  function iconForAnnotation(ann) {
219
213
  switch (ann.type) {
220
- case 'Baselined a11y violation':
221
- return ':white_check_mark:';
222
- case 'Stale a11y baseline entry':
223
- return ':warning:';
224
- case 'Accessibility':
225
- return iconForAccessibilityAnnotation(ann.description ?? '');
214
+ case "Baselined a11y violation":
215
+ return ":white_check_mark:";
216
+ case "Stale a11y baseline entry":
217
+ return ":warning:";
218
+ case "Accessibility":
219
+ return iconForAccessibilityAnnotation(ann.description ?? "");
226
220
  default:
227
- return ':information_source:';
221
+ return ":information_source:";
228
222
  }
229
223
  }
230
224
  /** Render annotation lines, deduplicated by type+description. */
@@ -232,11 +226,11 @@ function renderAnnotations(annotations) {
232
226
  const lines = [];
233
227
  const seen = new Set();
234
228
  for (const ann of annotations) {
235
- const key = `${ann.type}:${ann.description ?? ''}`;
229
+ const key = `${ann.type}:${ann.description ?? ""}`;
236
230
  if (seen.has(key))
237
231
  continue;
238
232
  seen.add(key);
239
- lines.push(`${iconForAnnotation(ann)} **${ann.type}**: ${ann.description ?? ''}\n`);
233
+ lines.push(`${iconForAnnotation(ann)} **${ann.type}**: ${ann.description ?? ""}\n`);
240
234
  }
241
235
  return lines;
242
236
  }
@@ -245,20 +239,18 @@ function renderViolationTable(violations) {
245
239
  if (violations.length === 0)
246
240
  return [];
247
241
  const lines = [
248
- '| Rule | Impact | Description | Targets |',
249
- '|------|--------|-------------|---------|',
242
+ "| Rule | Impact | Description | Targets |",
243
+ "|------|--------|-------------|---------|",
250
244
  ];
251
245
  for (const v of violations) {
252
- const shown = v.targets.slice(0, 3).map(escapeMarkdownCodeSpan).join(', ');
253
- const targets = v.targets.length > 3
254
- ? `${shown}, +${v.targets.length - 3} more`
255
- : shown;
246
+ const shown = v.targets.slice(0, 3).map(escapeMarkdownCodeSpan).join(", ");
247
+ const targets = v.targets.length > 3 ? `${shown}, +${v.targets.length - 3} more` : shown;
256
248
  const rule = escapeMarkdownTableCell(v.rule);
257
249
  const impact = escapeMarkdownTableCell(v.impact);
258
250
  const description = escapeMarkdownTableCell(v.description);
259
251
  lines.push(`| [${rule}](${v.helpUrl}) | ${impact} | ${description} | ${targets} |`);
260
252
  }
261
- lines.push('');
253
+ lines.push("");
262
254
  return lines;
263
255
  }
264
256
  /**
@@ -266,12 +258,15 @@ function renderViolationTable(violations) {
266
258
  */
267
259
  function generateSummary(report) {
268
260
  if (report.tests.length === 0) {
269
- return '## Accessibility Results\n\nNo accessibility tests were found in the report.\n';
261
+ return "## Accessibility Results\n\nNo accessibility tests were found in the report.\n";
270
262
  }
271
- const lines = ['## Accessibility Results\n', renderHeadline(report)];
263
+ const lines = [
264
+ "## Accessibility Results\n",
265
+ renderHeadline(report),
266
+ ];
272
267
  const seenTitles = new Set();
273
268
  for (const test of report.tests) {
274
- const testAnnotations = test.annotations.filter(a => a.type !== '@a11y');
269
+ const testAnnotations = test.annotations.filter((a) => a.type !== "@a11y");
275
270
  const hasTestViolations = test.violations.length > 0;
276
271
  if (testAnnotations.length === 0 && !hasTestViolations)
277
272
  continue;
@@ -284,7 +279,7 @@ function generateSummary(report) {
284
279
  testLines.push(...renderViolationTable(test.violations));
285
280
  lines.push(...testLines);
286
281
  }
287
- return lines.join('\n');
282
+ return lines.join("\n");
288
283
  }
289
284
  /**
290
285
  * Generate GitHub Actions annotation commands (::error, ::warning).
@@ -300,21 +295,21 @@ function generateAnnotations(report) {
300
295
  const line = escapeWorkflowCommandProperty(String(test.line));
301
296
  // Emit ::error for each violation.
302
297
  for (const v of test.violations) {
303
- const targets = v.targets.slice(0, 3).join(', ');
298
+ const targets = v.targets.slice(0, 3).join(", ");
304
299
  const title = escapeWorkflowCommandProperty(`${v.rule} (${v.impact})`);
305
300
  const msg = escapeWorkflowCommand(`a11y: ${v.rule} (${v.impact}) — ${v.description}. Targets: ${targets}`);
306
301
  lines.push(`::error file=${file},line=${line},title=${title}::${msg}`);
307
302
  }
308
303
  // Emit ::warning for stale baseline entries.
309
304
  for (const ann of test.annotations) {
310
- if (ann.type === 'Stale a11y baseline entry') {
311
- const title = escapeWorkflowCommandProperty('Stale a11y baseline');
312
- const msg = escapeWorkflowCommand(ann.description ?? '');
305
+ if (ann.type === "Stale a11y baseline entry") {
306
+ const title = escapeWorkflowCommandProperty("Stale a11y baseline");
307
+ const msg = escapeWorkflowCommand(ann.description ?? "");
313
308
  lines.push(`::warning file=${file},line=${line},title=${title}::${msg}`);
314
309
  }
315
310
  }
316
311
  }
317
- return lines.join('\n');
312
+ return lines.join("\n");
318
313
  }
319
314
  /**
320
315
  * CLI entry point. Called automatically when this file is run directly
@@ -322,24 +317,24 @@ function generateAnnotations(report) {
322
317
  * called from the bin wrapper.
323
318
  */
324
319
  function main(args = process.argv.slice(2), adapter = {}) {
325
- if (args.includes('--help') || args.includes('-h')) {
326
- process.stdout.write(`Usage: ${adapter.commandName ?? 'playwright-testing-a11y-summary'} [options]\n\n` +
327
- 'Options:\n' +
328
- ' --report-path=PATH Playwright JSON report (default: test-results/results.json)\n' +
329
- ' --mode=summary Write a Markdown accessibility summary (default)\n' +
330
- ' --mode=annotations Write GitHub Actions annotations\n' +
331
- ' -h, --help Show this help\n');
320
+ if (args.includes("--help") || args.includes("-h")) {
321
+ process.stdout.write(`Usage: ${adapter.commandName ?? "playwright-testing-a11y-summary"} [options]\n\n` +
322
+ "Options:\n" +
323
+ " --report-path=PATH Playwright JSON report (default: test-results/results.json)\n" +
324
+ " --mode=summary Write a Markdown accessibility summary (default)\n" +
325
+ " --mode=annotations Write GitHub Actions annotations\n" +
326
+ " -h, --help Show this help\n");
332
327
  return;
333
328
  }
334
- let mode = 'summary';
335
- let reportPath = 'test-results/results.json';
329
+ let mode = "summary";
330
+ let reportPath = "test-results/results.json";
336
331
  for (const arg of args) {
337
- if (arg.startsWith('--mode='))
338
- mode = arg.slice('--mode='.length);
339
- if (arg.startsWith('--report-path='))
340
- reportPath = arg.slice('--report-path='.length);
332
+ if (arg.startsWith("--mode="))
333
+ mode = arg.slice("--mode=".length);
334
+ if (arg.startsWith("--report-path="))
335
+ reportPath = arg.slice("--report-path=".length);
341
336
  }
342
- const validModes = new Set(['summary', 'annotations']);
337
+ const validModes = new Set(["summary", "annotations"]);
343
338
  if (!validModes.has(mode)) {
344
339
  console.error(`Unknown mode: ${mode}. Use summary or annotations.`);
345
340
  process.exit(2);
@@ -356,12 +351,12 @@ function main(args = process.argv.slice(2), adapter = {}) {
356
351
  }
357
352
  const report = parseA11yResults(resolvedPath);
358
353
  switch (mode) {
359
- case 'summary': {
354
+ case "summary": {
360
355
  const md = generateSummary(report);
361
356
  const summaryFile = process.env.GITHUB_STEP_SUMMARY;
362
357
  if (summaryFile) {
363
358
  fs.appendFileSync(summaryFile, md);
364
- console.log('Accessibility summary written to $GITHUB_STEP_SUMMARY');
359
+ console.log("Accessibility summary written to $GITHUB_STEP_SUMMARY");
365
360
  }
366
361
  else {
367
362
  // Not in GitHub Actions — just print.
@@ -369,10 +364,10 @@ function main(args = process.argv.slice(2), adapter = {}) {
369
364
  }
370
365
  break;
371
366
  }
372
- case 'annotations': {
367
+ case "annotations": {
373
368
  const output = generateAnnotations(report);
374
369
  if (output)
375
- process.stdout.write(output + '\n');
370
+ process.stdout.write(output + "\n");
376
371
  break;
377
372
  }
378
373
  }
@@ -36,7 +36,7 @@ export interface AttachmentUploaderOptions {
36
36
  * never throws, and they need different things said about them: a file that
37
37
  * could not be read is a path problem, one over the size limit is not.
38
38
  */
39
- export type SkipReason = 'disabled' | 'unreadable' | 'too-large' | 'budget';
39
+ export type SkipReason = "disabled" | "unreadable" | "too-large" | "budget";
40
40
  export interface UploadStats {
41
41
  uploaded: number;
42
42
  skipped: number;
@@ -63,7 +63,7 @@ const path = __importStar(require("path"));
63
63
  * run: retrying an endpoint that has answered 404 once only wastes CI time,
64
64
  * and nothing here is worth failing a build over.
65
65
  */
66
- const UPLOAD_ORIGIN = 'https://uploads.github.com';
66
+ const UPLOAD_ORIGIN = "https://uploads.github.com";
67
67
  const DEFAULT_MAX_UPLOADS = 20;
68
68
  const DEFAULT_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
69
69
  /**
@@ -74,24 +74,25 @@ const DEFAULT_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
74
74
  * the run into one skipped file.
75
75
  */
76
76
  const DEFAULT_MAX_FILE_BYTES = 10 * 1024 * 1024;
77
- const DEFAULT_TIMEOUT_MS = 15000;
77
+ const DEFAULT_TIMEOUT_MS = 15_000;
78
78
  const MIME_TYPES = {
79
- '.png': 'image/png',
80
- '.jpg': 'image/jpeg',
81
- '.jpeg': 'image/jpeg',
82
- '.gif': 'image/gif',
83
- '.webp': 'image/webp',
84
- '.svg': 'image/svg+xml',
85
- '.webm': 'video/webm',
86
- '.mp4': 'video/mp4',
87
- '.mov': 'video/quicktime',
79
+ ".png": "image/png",
80
+ ".jpg": "image/jpeg",
81
+ ".jpeg": "image/jpeg",
82
+ ".gif": "image/gif",
83
+ ".webp": "image/webp",
84
+ ".svg": "image/svg+xml",
85
+ ".webm": "video/webm",
86
+ ".mp4": "video/mp4",
87
+ ".mov": "video/quicktime",
88
88
  };
89
89
  /**
90
90
  * Guess a MIME type from a file extension. The endpoint requires one, and it
91
91
  * is what decides whether GitHub renders the attachment inline.
92
92
  */
93
93
  function mimeTypeFor(filePath) {
94
- return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
94
+ return (MIME_TYPES[path.extname(filePath).toLowerCase()] ??
95
+ "application/octet-stream");
95
96
  }
96
97
  class AttachmentUploader {
97
98
  token;
@@ -107,21 +108,22 @@ class AttachmentUploader {
107
108
  lastSkip = null;
108
109
  stats = { uploaded: 0, skipped: 0, bytes: 0 };
109
110
  constructor(options = {}) {
110
- this.token = String(options.token ?? '');
111
- this.repositoryId = String(options.repositoryId ?? '');
111
+ this.token = String(options.token ?? "");
112
+ this.repositoryId = String(options.repositoryId ?? "");
112
113
  this.maxUploads = options.maxUploads ?? DEFAULT_MAX_UPLOADS;
113
114
  this.maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
114
115
  this.maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
115
116
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
116
- this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
117
+ this.fetchImpl =
118
+ options.fetchImpl ?? globalThis.fetch;
117
119
  this.log = options.log ?? (() => { });
118
120
  if (!this.token) {
119
121
  this.disabled = true;
120
- this.reason = 'no upload token configured';
122
+ this.reason = "no upload token configured";
121
123
  }
122
124
  else if (!this.repositoryId) {
123
125
  this.disabled = true;
124
- this.reason = 'no repository ID available';
126
+ this.reason = "no repository ID available";
125
127
  }
126
128
  else {
127
129
  this.disabled = false;
@@ -156,14 +158,14 @@ class AttachmentUploader {
156
158
  */
157
159
  async upload(filePath, displayName) {
158
160
  if (this.disabled)
159
- return this.skip('disabled');
161
+ return this.skip("disabled");
160
162
  let body;
161
163
  try {
162
164
  body = fs.readFileSync(filePath);
163
165
  }
164
166
  catch {
165
167
  // A missing file is this file's problem, not a reason to stop the run.
166
- return this.skip('unreadable', `Attachment upload skipped, unreadable file: ${filePath}`);
168
+ return this.skip("unreadable", `Attachment upload skipped, unreadable file: ${filePath}`);
167
169
  }
168
170
  return this.uploadBuffer(body, displayName ?? path.basename(filePath), mimeTypeFor(filePath));
169
171
  }
@@ -174,7 +176,7 @@ class AttachmentUploader {
174
176
  */
175
177
  async uploadBuffer(body, displayName, contentType) {
176
178
  if (this.disabled)
177
- return this.skip('disabled');
179
+ return this.skip("disabled");
178
180
  const size = body.length;
179
181
  const name = sanitizeName(displayName);
180
182
  // A count budget is reached once and stays reached, so stop there.
@@ -186,11 +188,11 @@ class AttachmentUploader {
186
188
  // one large screenshot must not cost the run every smaller one behind it,
187
189
  // so skip it and carry on. `maxUploads` bounds how often this can repeat.
188
190
  if (size > this.maxFileBytes) {
189
- return this.skip('too-large', `Attachment skipped, ${size} bytes is over the ${this.maxFileBytes} byte limit: ${name}`);
191
+ return this.skip("too-large", `Attachment skipped, ${size} bytes is over the ${this.maxFileBytes} byte limit: ${name}`);
190
192
  }
191
193
  if (this.stats.bytes + size > this.maxTotalBytes) {
192
194
  const left = this.maxTotalBytes - this.stats.bytes;
193
- return this.skip('budget', `Attachment skipped, ${size} bytes does not fit the ${left} bytes left in the budget: ${name}`);
195
+ return this.skip("budget", `Attachment skipped, ${size} bytes does not fit the ${left} bytes left in the budget: ${name}`);
194
196
  }
195
197
  const query = new URLSearchParams({
196
198
  name,
@@ -199,13 +201,13 @@ class AttachmentUploader {
199
201
  });
200
202
  try {
201
203
  const response = await this.fetchImpl(`${UPLOAD_ORIGIN}/user-attachments/assets?${query}`, {
202
- method: 'POST',
204
+ method: "POST",
203
205
  headers: {
204
206
  Authorization: `Bearer ${this.token}`,
205
- Accept: 'application/json',
207
+ Accept: "application/json",
206
208
  // Required. Without it the endpoint answers 400 "Invalid
207
209
  // Content-Type", and fetch sends no default for a Buffer body.
208
- 'Content-Type': contentType,
210
+ "Content-Type": contentType,
209
211
  },
210
212
  body,
211
213
  signal: AbortSignal.timeout(this.timeoutMs),
@@ -216,7 +218,7 @@ class AttachmentUploader {
216
218
  }
217
219
  const url = parseUploadUrl(text);
218
220
  if (!url) {
219
- return this.disable('the attachments endpoint returned no URL');
221
+ return this.disable("the attachments endpoint returned no URL");
220
222
  }
221
223
  this.stats.uploaded++;
222
224
  this.stats.bytes += size;
@@ -233,7 +235,7 @@ class AttachmentUploader {
233
235
  this.disabled = true;
234
236
  this.reason = reason;
235
237
  this.log(`Attachment uploads disabled — ${reason}.`);
236
- return this.skip('disabled');
238
+ return this.skip("disabled");
237
239
  }
238
240
  /**
239
241
  * Record one file that was not uploaded, and why. Most reasons leave the
@@ -257,7 +259,7 @@ function parseUploadUrl(text) {
257
259
  try {
258
260
  const parsed = JSON.parse(text);
259
261
  const url = parsed?.url ?? parsed?.href ?? parsed?.asset?.href;
260
- return typeof url === 'string' && url ? url : null;
262
+ return typeof url === "string" && url ? url : null;
261
263
  }
262
264
  catch {
263
265
  return null;
@@ -265,9 +267,9 @@ function parseUploadUrl(text) {
265
267
  }
266
268
  /** Condense a response body into one line worth putting in a CI log. */
267
269
  function summarize(text) {
268
- const collapsed = text.replace(/\s+/g, ' ').trim();
270
+ const collapsed = text.replace(/\s+/g, " ").trim();
269
271
  if (!collapsed)
270
- return 'no response body';
272
+ return "no response body";
271
273
  return collapsed.length > 200 ? `${collapsed.slice(0, 200)}…` : collapsed;
272
274
  }
273
275
  /**
@@ -280,9 +282,9 @@ function summarize(text) {
280
282
  function trimDashes(value) {
281
283
  let start = 0;
282
284
  let end = value.length;
283
- while (start < end && value[start] === '-')
285
+ while (start < end && value[start] === "-")
284
286
  start++;
285
- while (end > start && value[end - 1] === '-')
287
+ while (end > start && value[end - 1] === "-")
286
288
  end--;
287
289
  return value.slice(start, end);
288
290
  }
@@ -292,6 +294,6 @@ function trimDashes(value) {
292
294
  */
293
295
  function sanitizeName(name) {
294
296
  // Truncate before trimming, so a trailing dash left by the cut goes too.
295
- const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 120);
296
- return trimDashes(cleaned) || 'attachment';
297
+ const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, "-").slice(0, 120);
298
+ return trimDashes(cleaned) || "attachment";
297
299
  }
@@ -1,6 +1,6 @@
1
- import { AttachmentUploader, AttachmentUploaderOptions } from './attachments';
2
- import { PathPrefix, PathResolution } from './report-paths';
3
- export type ImageKind = 'diff' | 'actual' | 'expected' | 'previous' | 'a11y' | 'screenshot';
1
+ import { AttachmentUploader, AttachmentUploaderOptions } from "./attachments";
2
+ import { PathPrefix, PathResolution } from "./report-paths";
3
+ export type ImageKind = "diff" | "actual" | "expected" | "previous" | "a11y" | "screenshot";
4
4
  export interface FailureImage {
5
5
  /** Attachment name as it appears in the report. */
6
6
  name: string;
@@ -46,7 +46,7 @@ export interface FailureReport {
46
46
  totalImages: number;
47
47
  }
48
48
  /** Which snapshot images to include. `diff` is the one that shows the problem. */
49
- export type IncludeMode = 'diff' | 'all';
49
+ export type IncludeMode = "diff" | "all";
50
50
  /**
51
51
  * Prefix of the HTML comment carrying the failure count in a generated comment
52
52
  * body. Invisible when rendered, and greppable by a workflow that has to decide