@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,7 +1,7 @@
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'
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
5
  import {
6
6
  parseFailures,
7
7
  resolveImagePaths,
@@ -10,434 +10,541 @@ import {
10
10
  uploadImages,
11
11
  defuseMaskTriggers,
12
12
  FailureReport,
13
- } from './failure-summary'
14
- import { createPathResolver } from './report-paths'
15
- import { AttachmentUploader, FetchLike } from './attachments'
13
+ } from "./failure-summary";
14
+ import { createPathResolver } from "./report-paths";
15
+ import { AttachmentUploader, FetchLike } from "./attachments";
16
16
 
17
- let tmpDir: string
17
+ let tmpDir: string;
18
18
 
19
19
  beforeEach(() => {
20
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'failure-summary-test-'))
21
- })
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "failure-summary-test-"));
21
+ });
22
22
 
23
23
  afterEach(() => {
24
- fs.rmSync(tmpDir, { recursive: true, force: true })
25
- })
24
+ fs.rmSync(tmpDir, { recursive: true, force: true });
25
+ });
26
26
 
27
27
  interface TestResultCase {
28
- status?: string
29
- attachments?: any[]
30
- error?: { message: string }
28
+ status?: string;
29
+ attachments?: any[];
30
+ error?: { message: string };
31
31
  }
32
32
 
33
33
  interface TestCase extends TestResultCase {
34
- title?: string
35
- outcome?: string
36
- results?: TestResultCase[]
34
+ title?: string;
35
+ outcome?: string;
36
+ results?: TestResultCase[];
37
37
  }
38
38
 
39
39
  /** Build a minimal Playwright JSON report and write it to disk. */
40
40
  function writeReport(cases: TestCase[]): string {
41
41
  const report = {
42
- suites: [{
43
- title: '',
44
- file: 'tests/visual.spec.ts',
45
- specs: cases.map((testCase, index) => ({
46
- title: testCase.title ?? `case ${index}`,
47
- line: 10 + index,
48
- tests: [{
49
- status: testCase.outcome,
50
- results: testCase.results ?? [{
51
- status: testCase.status ?? 'failed',
52
- attachments: testCase.attachments ?? [],
53
- error: testCase.error,
54
- }],
55
- }],
56
- })),
57
- }],
58
- }
42
+ suites: [
43
+ {
44
+ title: "",
45
+ file: "tests/visual.spec.ts",
46
+ specs: cases.map((testCase, index) => ({
47
+ title: testCase.title ?? `case ${index}`,
48
+ line: 10 + index,
49
+ tests: [
50
+ {
51
+ status: testCase.outcome,
52
+ results: testCase.results ?? [
53
+ {
54
+ status: testCase.status ?? "failed",
55
+ attachments: testCase.attachments ?? [],
56
+ error: testCase.error,
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ })),
62
+ },
63
+ ],
64
+ };
59
65
 
60
- const reportPath = path.join(tmpDir, 'results.json')
61
- fs.writeFileSync(reportPath, JSON.stringify(report))
62
- return reportPath
66
+ const reportPath = path.join(tmpDir, "results.json");
67
+ fs.writeFileSync(reportPath, JSON.stringify(report));
68
+ return reportPath;
63
69
  }
64
70
 
65
71
  function image(name: string, filePath = `/tmp/${name}`) {
66
- return { name, contentType: 'image/png', path: filePath }
72
+ return { name, contentType: "image/png", path: filePath };
67
73
  }
68
74
 
69
- describe('parseFailures', () => {
70
- it('collects failing tests and ignores passing ones', () => {
75
+ describe("parseFailures", () => {
76
+ it("collects failing tests and ignores passing ones", () => {
71
77
  const reportPath = writeReport([
72
- { title: 'homepage matches', status: 'failed', attachments: [image('homepage-diff.png')] },
73
- { title: 'about page matches', status: 'passed' },
74
- ])
75
-
76
- const report = parseFailures(reportPath)
77
-
78
- expect(report.tests).toHaveLength(1)
79
- expect(report.tests[0].title).toBe('homepage matches')
80
- expect(report.totalFailed).toBe(1)
81
- expect(report.totalFlaky).toBe(0)
82
- expect(report.totalImages).toBe(1)
83
- })
84
-
85
- it('collects a test that passes on retry as flaky', () => {
86
- const reportPath = writeReport([{
87
- title: 'homepage eventually matches',
88
- outcome: 'flaky',
89
- results: [
90
- {
91
- status: 'failed',
92
- error: { message: 'Screenshot comparison failed' },
93
- attachments: [image('homepage-diff.png')],
94
- },
95
- { status: 'passed' },
96
- ],
97
- }])
78
+ {
79
+ title: "homepage matches",
80
+ status: "failed",
81
+ attachments: [image("homepage-diff.png")],
82
+ },
83
+ { title: "about page matches", status: "passed" },
84
+ ]);
85
+
86
+ const report = parseFailures(reportPath);
98
87
 
99
- const report = parseFailures(reportPath)
88
+ expect(report.tests).toHaveLength(1);
89
+ expect(report.tests[0].title).toBe("homepage matches");
90
+ expect(report.totalFailed).toBe(1);
91
+ expect(report.totalFlaky).toBe(0);
92
+ expect(report.totalImages).toBe(1);
93
+ });
100
94
 
101
- expect(report.tests).toHaveLength(1)
95
+ it("collects a test that passes on retry as flaky", () => {
96
+ const reportPath = writeReport([
97
+ {
98
+ title: "homepage eventually matches",
99
+ outcome: "flaky",
100
+ results: [
101
+ {
102
+ status: "failed",
103
+ error: { message: "Screenshot comparison failed" },
104
+ attachments: [image("homepage-diff.png")],
105
+ },
106
+ { status: "passed" },
107
+ ],
108
+ },
109
+ ]);
110
+
111
+ const report = parseFailures(reportPath);
112
+
113
+ expect(report.tests).toHaveLength(1);
102
114
  expect(report.tests[0]).toMatchObject({
103
- title: 'homepage eventually matches',
104
- status: 'flaky',
105
- error: 'Screenshot comparison failed',
106
- })
107
- expect(report.tests[0].images.map(i => i.kind)).toEqual(['diff'])
108
- expect(report.totalFailed).toBe(0)
109
- expect(report.totalFlaky).toBe(1)
110
- expect(report.totalImages).toBe(1)
111
- })
112
-
113
- it('classifies the snapshot comparison images', () => {
114
- const reportPath = writeReport([{
115
- attachments: [
116
- image('homepage-diff.png'),
117
- image('homepage-actual.png'),
118
- image('homepage-expected.png'),
119
- ],
120
- }])
115
+ title: "homepage eventually matches",
116
+ status: "flaky",
117
+ error: "Screenshot comparison failed",
118
+ });
119
+ expect(report.tests[0].images.map((i) => i.kind)).toEqual(["diff"]);
120
+ expect(report.totalFailed).toBe(0);
121
+ expect(report.totalFlaky).toBe(1);
122
+ expect(report.totalImages).toBe(1);
123
+ });
124
+
125
+ it("classifies the snapshot comparison images", () => {
126
+ const reportPath = writeReport([
127
+ {
128
+ attachments: [
129
+ image("homepage-diff.png"),
130
+ image("homepage-actual.png"),
131
+ image("homepage-expected.png"),
132
+ ],
133
+ },
134
+ ]);
121
135
 
122
- const diffOnly = parseFailures(reportPath, 'diff')
123
- expect(diffOnly.tests[0].images.map(i => i.kind)).toEqual(['diff'])
136
+ const diffOnly = parseFailures(reportPath, "diff");
137
+ expect(diffOnly.tests[0].images.map((i) => i.kind)).toEqual(["diff"]);
124
138
 
125
- const all = parseFailures(reportPath, 'all')
126
- expect(all.tests[0].images.map(i => i.kind)).toEqual(['diff', 'actual', 'expected'])
127
- })
139
+ const all = parseFailures(reportPath, "all");
140
+ expect(all.tests[0].images.map((i) => i.kind)).toEqual([
141
+ "diff",
142
+ "actual",
143
+ "expected",
144
+ ]);
145
+ });
128
146
 
129
- it('keeps an inlined attachment body, which is how a11y screenshots arrive', () => {
130
- const reportPath = writeReport([{
131
- attachments: [
132
- { name: 'a11y-violation-screenshot', contentType: 'image/png', body: 'aGVsbG8=' },
133
- ],
134
- }])
147
+ it("keeps an inlined attachment body, which is how a11y screenshots arrive", () => {
148
+ const reportPath = writeReport([
149
+ {
150
+ attachments: [
151
+ {
152
+ name: "a11y-violation-screenshot",
153
+ contentType: "image/png",
154
+ body: "aGVsbG8=",
155
+ },
156
+ ],
157
+ },
158
+ ]);
135
159
 
136
- const image = parseFailures(reportPath).tests[0].images[0]
160
+ const image = parseFailures(reportPath).tests[0].images[0];
137
161
 
138
- expect(image.body).toBe('aGVsbG8=')
139
- expect(image.filePath).toBeUndefined()
140
- })
162
+ expect(image.body).toBe("aGVsbG8=");
163
+ expect(image.filePath).toBeUndefined();
164
+ });
141
165
 
142
- it('keeps an accessibility screenshot even when the test passed', () => {
166
+ it("keeps an accessibility screenshot even when the test passed", () => {
143
167
  // A baselined violation still passes, and its screenshot is still the
144
168
  // thing worth looking at.
145
- const reportPath = writeReport([{
146
- title: 'homepage a11y',
147
- status: 'passed',
148
- attachments: [image('a11y-violation-screenshot')],
149
- }])
150
-
151
- const report = parseFailures(reportPath)
152
-
153
- expect(report.tests).toHaveLength(1)
154
- expect(report.tests[0].images[0].kind).toBe('a11y')
155
- expect(report.totalFailed).toBe(0)
156
- })
157
-
158
- it('ignores attachments that are not images', () => {
159
- const reportPath = writeReport([{
160
- attachments: [
161
- { name: 'trace.zip', contentType: 'application/zip', path: '/tmp/trace.zip' },
162
- { name: 'stdout.txt', contentType: 'text/plain', body: 'aGk=' },
163
- ],
164
- }])
169
+ const reportPath = writeReport([
170
+ {
171
+ title: "homepage a11y",
172
+ status: "passed",
173
+ attachments: [image("a11y-violation-screenshot")],
174
+ },
175
+ ]);
176
+
177
+ const report = parseFailures(reportPath);
178
+
179
+ expect(report.tests).toHaveLength(1);
180
+ expect(report.tests[0].images[0].kind).toBe("a11y");
181
+ expect(report.totalFailed).toBe(0);
182
+ });
183
+
184
+ it("ignores attachments that are not images", () => {
185
+ const reportPath = writeReport([
186
+ {
187
+ attachments: [
188
+ {
189
+ name: "trace.zip",
190
+ contentType: "application/zip",
191
+ path: "/tmp/trace.zip",
192
+ },
193
+ { name: "stdout.txt", contentType: "text/plain", body: "aGk=" },
194
+ ],
195
+ },
196
+ ]);
165
197
 
166
- expect(parseFailures(reportPath).tests[0].images).toHaveLength(0)
167
- })
198
+ expect(parseFailures(reportPath).tests[0].images).toHaveLength(0);
199
+ });
168
200
 
169
- it('takes the first line of the error, without ANSI codes', () => {
170
- const reportPath = writeReport([{
171
- error: { message: '\u001b[31mScreenshot comparison failed\u001b[0m\n\n expected 1000 pixels' },
172
- }])
201
+ it("takes the first line of the error, without ANSI codes", () => {
202
+ const reportPath = writeReport([
203
+ {
204
+ error: {
205
+ message:
206
+ "\u001b[31mScreenshot comparison failed\u001b[0m\n\n expected 1000 pixels",
207
+ },
208
+ },
209
+ ]);
173
210
 
174
- expect(parseFailures(reportPath).tests[0].error).toBe('Screenshot comparison failed')
175
- })
176
- })
211
+ expect(parseFailures(reportPath).tests[0].error).toBe(
212
+ "Screenshot comparison failed",
213
+ );
214
+ });
215
+ });
177
216
 
178
- describe('generateSummary', () => {
217
+ describe("generateSummary", () => {
179
218
  function reportWith(url?: string): FailureReport {
180
219
  return {
181
- tests: [{
182
- title: 'homepage matches',
183
- file: 'tests/visual.spec.ts',
184
- line: 10,
185
- status: 'failed',
186
- error: 'Screenshot comparison failed',
187
- images: [{ name: 'homepage-diff.png', contentType: 'image/png', kind: 'diff', url }],
188
- }],
220
+ tests: [
221
+ {
222
+ title: "homepage matches",
223
+ file: "tests/visual.spec.ts",
224
+ line: 10,
225
+ status: "failed",
226
+ error: "Screenshot comparison failed",
227
+ images: [
228
+ {
229
+ name: "homepage-diff.png",
230
+ contentType: "image/png",
231
+ kind: "diff",
232
+ url,
233
+ },
234
+ ],
235
+ },
236
+ ],
189
237
  totalFailed: 1,
190
238
  totalFlaky: 0,
191
239
  totalImages: 1,
192
- }
240
+ };
193
241
  }
194
242
 
195
- it('points at the comment rather than embedding images', () => {
243
+ it("points at the comment rather than embedding images", () => {
196
244
  // A summary is rendered once when the job ends, before a just-uploaded
197
245
  // attachment resolves, and that dead render is cached for good.
198
- const summary = generateSummary(reportWith('https://example.test/a'))
246
+ const summary = generateSummary(reportWith("https://example.test/a"));
199
247
 
200
- expect(summary).not.toContain('<img')
201
- expect(summary).toContain('1 screenshot(s) uploaded — see the pull request comment.')
202
- expect(summary).toContain('homepage matches')
203
- })
248
+ expect(summary).not.toContain("<img");
249
+ expect(summary).toContain(
250
+ "1 screenshot(s) uploaded — see the pull request comment.",
251
+ );
252
+ expect(summary).toContain("homepage matches");
253
+ });
204
254
 
205
- it('points at the artifact when nothing was uploaded', () => {
206
- const summary = generateSummary(reportWith(undefined))
255
+ it("points at the artifact when nothing was uploaded", () => {
256
+ const summary = generateSummary(reportWith(undefined));
207
257
 
208
- expect(summary).not.toContain('<img')
209
- expect(summary).toContain('not uploaded')
210
- })
258
+ expect(summary).not.toContain("<img");
259
+ expect(summary).toContain("not uploaded");
260
+ });
211
261
 
212
- it('says an absent token is why nothing was uploaded', () => {
262
+ it("says an absent token is why nothing was uploaded", () => {
213
263
  const summary = generateSummary(reportWith(undefined), {
214
- uploadReason: 'no upload token configured',
215
- })
264
+ uploadReason: "no upload token configured",
265
+ });
216
266
 
217
- expect(summary).toContain('not uploaded (no upload token configured)')
218
- })
267
+ expect(summary).toContain("not uploaded (no upload token configured)");
268
+ });
219
269
 
220
- it('says so when the images were not where the report said they were', () => {
270
+ it("says so when the images were not where the report said they were", () => {
221
271
  // The reason a reader most needs, and the one that used to be silent: the
222
272
  // token is fine, the files are on the other side of a container boundary.
223
- const report = reportWith(undefined)
224
- report.tests[0].images[0].filePath = '/var/www/html/test/playwright/test-results/home-1-diff.png'
225
- report.tests[0].images[0].unreadable = true
226
-
227
- const summary = generateSummary(report)
228
-
229
- expect(summary).toContain('1 screenshot(s) could not be read')
230
- expect(summary).toContain('/var/www/html/test/playwright/test-results/home-1-diff.png')
231
- expect(summary).toContain('--path-prefix=')
232
- expect(summary).toContain('not uploaded (1 could not be read')
233
- })
234
-
235
- it('does not mention a path boundary when there is none', () => {
236
- expect(generateSummary(reportWith(undefined))).not.toContain('could not be read')
237
- })
238
-
239
- it('does not claim zero failing tests when only a11y captured something', () => {
273
+ const report = reportWith(undefined);
274
+ report.tests[0].images[0].filePath =
275
+ "/var/www/html/test/playwright/test-results/home-1-diff.png";
276
+ report.tests[0].images[0].unreadable = true;
277
+
278
+ const summary = generateSummary(report);
279
+
280
+ expect(summary).toContain("1 screenshot(s) could not be read");
281
+ expect(summary).toContain(
282
+ "/var/www/html/test/playwright/test-results/home-1-diff.png",
283
+ );
284
+ expect(summary).toContain("--path-prefix=");
285
+ expect(summary).toContain("not uploaded (1 could not be read");
286
+ });
287
+
288
+ it("does not mention a path boundary when there is none", () => {
289
+ expect(generateSummary(reportWith(undefined))).not.toContain(
290
+ "could not be read",
291
+ );
292
+ });
293
+
294
+ it("does not claim zero failing tests when only a11y captured something", () => {
240
295
  const summary = generateSummary({
241
- tests: [{
242
- title: 'homepage a11y',
243
- file: 'tests/a11y.spec.ts',
244
- line: 7,
245
- status: 'passed',
246
- images: [{ name: 'a11y-violation-screenshot', contentType: 'image/png', kind: 'a11y' }],
247
- }],
296
+ tests: [
297
+ {
298
+ title: "homepage a11y",
299
+ file: "tests/a11y.spec.ts",
300
+ line: 7,
301
+ status: "passed",
302
+ images: [
303
+ {
304
+ name: "a11y-violation-screenshot",
305
+ contentType: "image/png",
306
+ kind: "a11y",
307
+ },
308
+ ],
309
+ },
310
+ ],
248
311
  totalFailed: 0,
249
312
  totalFlaky: 0,
250
313
  totalImages: 1,
251
- })
314
+ });
252
315
 
253
- expect(summary).not.toContain('**0** failing test(s)')
254
- expect(summary).toContain('No failing tests · **1** screenshot(s)')
316
+ expect(summary).not.toContain("**0** failing test(s)");
317
+ expect(summary).toContain("No failing tests · **1** screenshot(s)");
255
318
  // Nothing failed, so do not head the section "Test Failures".
256
- expect(summary).toContain('## Test Screenshots')
257
- })
319
+ expect(summary).toContain("## Test Screenshots");
320
+ });
258
321
 
259
- it('calls out flakes when retries recover every failure', () => {
322
+ it("calls out flakes when retries recover every failure", () => {
260
323
  const summary = generateSummary({
261
- tests: [{
262
- title: 'homepage eventually matches',
263
- file: 'tests/visual.spec.ts',
264
- line: 10,
265
- status: 'flaky',
266
- error: 'Screenshot comparison failed',
267
- images: [],
268
- }],
324
+ tests: [
325
+ {
326
+ title: "homepage eventually matches",
327
+ file: "tests/visual.spec.ts",
328
+ line: 10,
329
+ status: "flaky",
330
+ error: "Screenshot comparison failed",
331
+ images: [],
332
+ },
333
+ ],
269
334
  totalFailed: 0,
270
335
  totalFlaky: 1,
271
336
  totalImages: 0,
272
- })
337
+ });
273
338
 
274
- expect(summary).toContain('## Test Flakes')
275
- expect(summary).toContain('No failing tests · :warning: **1** flaky test(s)')
276
- expect(summary).toContain('`tests/visual.spec.ts:10` — flaky')
277
- })
339
+ expect(summary).toContain("## Test Flakes");
340
+ expect(summary).toContain(
341
+ "No failing tests · :warning: **1** flaky test(s)",
342
+ );
343
+ expect(summary).toContain("`tests/visual.spec.ts:10` — flaky");
344
+ });
278
345
 
279
- it('says so plainly when there is nothing to report', () => {
280
- const summary = generateSummary({ tests: [], totalFailed: 0, totalFlaky: 0, totalImages: 0 })
346
+ it("says so plainly when there is nothing to report", () => {
347
+ const summary = generateSummary({
348
+ tests: [],
349
+ totalFailed: 0,
350
+ totalFlaky: 0,
351
+ totalImages: 0,
352
+ });
281
353
 
282
- expect(summary).toContain('No failing tests')
283
- expect(summary).not.toContain('<details>')
284
- })
354
+ expect(summary).toContain("No failing tests");
355
+ expect(summary).not.toContain("<details>");
356
+ });
285
357
 
286
- it('escapes the asset URL, which may one day carry query parameters', () => {
287
- const report = reportWith('https://example.test/a?x=1&y=2')
358
+ it("escapes the asset URL, which may one day carry query parameters", () => {
359
+ const report = reportWith("https://example.test/a?x=1&y=2");
288
360
 
289
- expect(generateComment(report)).toContain('src="https://example.test/a?x=1&amp;y=2"')
290
- })
361
+ expect(generateComment(report)).toContain(
362
+ 'src="https://example.test/a?x=1&amp;y=2"',
363
+ );
364
+ });
291
365
 
292
- it('escapes quotes coming from a test title', () => {
293
- const report = reportWith('https://example.test/a')
294
- report.tests[0].title = 'renders "quoted" text'
366
+ it("escapes quotes coming from a test title", () => {
367
+ const report = reportWith("https://example.test/a");
368
+ report.tests[0].title = 'renders "quoted" text';
295
369
 
296
- expect(generateComment(report)).toContain('alt="diff for renders &quot;quoted&quot; text"')
297
- })
298
- })
370
+ expect(generateComment(report)).toContain(
371
+ 'alt="diff for renders &quot;quoted&quot; text"',
372
+ );
373
+ });
374
+ });
299
375
 
300
- describe('generateComment', () => {
376
+ describe("generateComment", () => {
301
377
  const report: FailureReport = {
302
- tests: [{
303
- title: 'homepage matches',
304
- file: 'tests/visual.spec.ts',
305
- line: 10,
306
- status: 'failed',
307
- images: [{ name: 'homepage-diff.png', contentType: 'image/png', kind: 'diff' }],
308
- }],
378
+ tests: [
379
+ {
380
+ title: "homepage matches",
381
+ file: "tests/visual.spec.ts",
382
+ line: 10,
383
+ status: "failed",
384
+ images: [
385
+ { name: "homepage-diff.png", contentType: "image/png", kind: "diff" },
386
+ ],
387
+ },
388
+ ],
309
389
  totalFailed: 1,
310
390
  totalFlaky: 0,
311
391
  totalImages: 1,
312
- }
392
+ };
313
393
 
314
- it('summarises the failures and links to the summary', () => {
315
- const comment = generateComment(report, { summaryUrl: 'https://example.test/run/1' })
394
+ it("summarises the failures and links to the summary", () => {
395
+ const comment = generateComment(report, {
396
+ summaryUrl: "https://example.test/run/1",
397
+ });
316
398
 
317
- expect(comment).toContain('**1** failing test(s)')
318
- expect(comment).toContain('homepage matches')
319
- expect(comment).toContain('(https://example.test/run/1)')
320
- })
399
+ expect(comment).toContain("**1** failing test(s)");
400
+ expect(comment).toContain("homepage matches");
401
+ expect(comment).toContain("(https://example.test/run/1)");
402
+ });
321
403
 
322
- it('says why the screenshots it promised are not there', () => {
404
+ it("says why the screenshots it promised are not there", () => {
323
405
  // The bullet list still advertises the screenshots, so a comment with no
324
406
  // images has to account for them or it reads as a broken feature.
325
- const comment = generateComment(report, { uploadReason: 'no upload token configured' })
407
+ const comment = generateComment(report, {
408
+ uploadReason: "no upload token configured",
409
+ });
326
410
 
327
- expect(comment).toContain('1 screenshot(s) not shown — no upload token configured')
328
- })
411
+ expect(comment).toContain(
412
+ "1 screenshot(s) not shown — no upload token configured",
413
+ );
414
+ });
329
415
 
330
- it('distinguishes unreadable images from an absent token', () => {
416
+ it("distinguishes unreadable images from an absent token", () => {
331
417
  const unreadable: FailureReport = {
332
418
  ...report,
333
- tests: [{
334
- ...report.tests[0],
335
- images: [{ ...report.tests[0].images[0], unreadable: true }],
336
- }],
337
- }
419
+ tests: [
420
+ {
421
+ ...report.tests[0],
422
+ images: [{ ...report.tests[0].images[0], unreadable: true }],
423
+ },
424
+ ],
425
+ };
338
426
 
339
- const comment = generateComment(unreadable)
427
+ const comment = generateComment(unreadable);
340
428
 
341
- expect(comment).toContain('1 could not be read from the path recorded in the report')
342
- })
429
+ expect(comment).toContain(
430
+ "1 could not be read from the path recorded in the report",
431
+ );
432
+ });
343
433
 
344
- it('embeds uploaded images in a collapsed block', () => {
434
+ it("embeds uploaded images in a collapsed block", () => {
345
435
  // A comment re-renders on every read, so an attachment resolves however
346
436
  // recently it was uploaded. This is the only place they display.
347
437
  const withUrls: FailureReport = {
348
438
  ...report,
349
- tests: [{
350
- ...report.tests[0],
351
- images: [{
352
- name: 'homepage-diff.png',
353
- contentType: 'image/png',
354
- kind: 'diff',
355
- url: 'https://github.com/user-attachments/assets/abc',
356
- }],
357
- }],
358
- }
359
-
360
- const comment = generateComment(withUrls)
361
-
362
- expect(comment).toContain('<details>')
363
- expect(comment).toContain('<img src="https://github.com/user-attachments/assets/abc"')
364
- expect(comment).toContain('width="640"')
365
- })
366
-
367
- it('shows no image block when nothing was uploaded', () => {
368
- const comment = generateComment(report)
369
-
370
- expect(comment).not.toContain('<img')
371
- expect(comment).not.toContain('<details>')
372
- })
373
-
374
- it('marks the failure and flake counts for a workflow to read', () => {
439
+ tests: [
440
+ {
441
+ ...report.tests[0],
442
+ images: [
443
+ {
444
+ name: "homepage-diff.png",
445
+ contentType: "image/png",
446
+ kind: "diff",
447
+ url: "https://github.com/user-attachments/assets/abc",
448
+ },
449
+ ],
450
+ },
451
+ ],
452
+ };
453
+
454
+ const comment = generateComment(withUrls);
455
+
456
+ expect(comment).toContain("<details>");
457
+ expect(comment).toContain(
458
+ '<img src="https://github.com/user-attachments/assets/abc"',
459
+ );
460
+ expect(comment).toContain('width="640"');
461
+ });
462
+
463
+ it("shows no image block when nothing was uploaded", () => {
464
+ const comment = generateComment(report);
465
+
466
+ expect(comment).not.toContain("<img");
467
+ expect(comment).not.toContain("<details>");
468
+ });
469
+
470
+ it("marks the failure and flake counts for a workflow to read", () => {
375
471
  // The empty-state sentence contains the words "failing test", so anything
376
472
  // grepping the prose would treat a green run as a failure.
377
- expect(generateComment(report)).toContain('<!-- playwright-testing-failures: 1 -->')
378
- expect(generateComment(report)).toContain('<!-- playwright-testing-flakes: 0 -->')
379
-
380
- const green = generateComment({ tests: [], totalFailed: 0, totalFlaky: 0, totalImages: 0 })
381
- expect(green).toContain('<!-- playwright-testing-failures: 0 -->')
382
- expect(green).toContain('<!-- playwright-testing-flakes: 0 -->')
383
- expect(green).toMatch(/No failing tests/)
384
- })
473
+ expect(generateComment(report)).toContain(
474
+ "<!-- playwright-testing-failures: 1 -->",
475
+ );
476
+ expect(generateComment(report)).toContain(
477
+ "<!-- playwright-testing-flakes: 0 -->",
478
+ );
479
+
480
+ const green = generateComment({
481
+ tests: [],
482
+ totalFailed: 0,
483
+ totalFlaky: 0,
484
+ totalImages: 0,
485
+ });
486
+ expect(green).toContain("<!-- playwright-testing-failures: 0 -->");
487
+ expect(green).toContain("<!-- playwright-testing-flakes: 0 -->");
488
+ expect(green).toMatch(/No failing tests/);
489
+ });
385
490
 
386
- it('reports flaky tests and marks the comment as worth posting', () => {
491
+ it("reports flaky tests and marks the comment as worth posting", () => {
387
492
  const flaky: FailureReport = {
388
- tests: [{
389
- title: 'homepage eventually matches',
390
- file: 'tests/visual.spec.ts',
391
- line: 10,
392
- status: 'flaky',
393
- error: 'Screenshot comparison failed',
394
- images: [],
395
- }],
493
+ tests: [
494
+ {
495
+ title: "homepage eventually matches",
496
+ file: "tests/visual.spec.ts",
497
+ line: 10,
498
+ status: "flaky",
499
+ error: "Screenshot comparison failed",
500
+ images: [],
501
+ },
502
+ ],
396
503
  totalFailed: 0,
397
504
  totalFlaky: 1,
398
505
  totalImages: 0,
399
- }
506
+ };
400
507
 
401
- const comment = generateComment(flaky)
508
+ const comment = generateComment(flaky);
402
509
 
403
- expect(comment).toContain(':warning: **1** flaky test(s)')
404
- expect(comment).toContain('`flaky` homepage eventually matches')
405
- expect(comment).toContain('<!-- playwright-testing-failures: 0 -->')
406
- expect(comment).toContain('<!-- playwright-testing-flakes: 1 -->')
407
- })
510
+ expect(comment).toContain(":warning: **1** flaky test(s)");
511
+ expect(comment).toContain("`flaky` homepage eventually matches");
512
+ expect(comment).toContain("<!-- playwright-testing-failures: 0 -->");
513
+ expect(comment).toContain("<!-- playwright-testing-flakes: 1 -->");
514
+ });
408
515
 
409
- it('truncates a long list rather than posting a wall of text', () => {
516
+ it("truncates a long list rather than posting a wall of text", () => {
410
517
  const many: FailureReport = {
411
518
  tests: Array.from({ length: 12 }, (_, i) => ({
412
519
  title: `case ${i}`,
413
- file: 'tests/visual.spec.ts',
520
+ file: "tests/visual.spec.ts",
414
521
  line: i,
415
- status: 'failed',
522
+ status: "failed",
416
523
  images: [],
417
524
  })),
418
525
  totalFailed: 12,
419
526
  totalFlaky: 0,
420
527
  totalImages: 0,
421
- }
528
+ };
422
529
 
423
- expect(generateComment(many)).toContain('…and 2 more')
424
- })
425
- })
530
+ expect(generateComment(many)).toContain("…and 2 more");
531
+ });
532
+ });
426
533
 
427
- describe('defuseMaskTriggers', () => {
428
- it('breaks up the pattern the runner redacts in job summaries', () => {
429
- const defused = defuseMaskTriggers('sends Bearer tokens')
534
+ describe("defuseMaskTriggers", () => {
535
+ it("breaks up the pattern the runner redacts in job summaries", () => {
536
+ const defused = defuseMaskTriggers("sends Bearer tokens");
430
537
 
431
- expect(defused).not.toContain('Bearer tokens')
432
- expect(defused.replace(/\u200B/g, '')).toBe('sends Bearer tokens')
433
- })
538
+ expect(defused).not.toContain("Bearer tokens");
539
+ expect(defused.replace(/\u200B/g, "")).toBe("sends Bearer tokens");
540
+ });
434
541
 
435
- it('leaves the bare word alone, which the runner does not redact', () => {
436
- expect(defuseMaskTriggers('Bearer')).toBe('Bearer')
437
- })
438
- })
542
+ it("leaves the bare word alone, which the runner does not redact", () => {
543
+ expect(defuseMaskTriggers("Bearer")).toBe("Bearer");
544
+ });
545
+ });
439
546
 
440
- describe('resolveImagePaths', () => {
547
+ describe("resolveImagePaths", () => {
441
548
  /**
442
549
  * A report as it arrives from a run inside DDEV: every path absolute and
443
550
  * under /var/www/html, read from a checkout that has no such directory.
@@ -447,190 +554,297 @@ describe('resolveImagePaths', () => {
447
554
  config: {
448
555
  // What Playwright actually records: the *test* directory, which is why
449
556
  // the report's own config is no use for locating the mount point.
450
- rootDir: '/var/www/html/test/playwright/tests',
451
- projects: [{ name: 'chromium', outputDir: '/var/www/html/test/playwright/test-results' }],
557
+ rootDir: "/var/www/html/test/playwright/tests",
558
+ projects: [
559
+ {
560
+ name: "chromium",
561
+ outputDir: "/var/www/html/test/playwright/test-results",
562
+ },
563
+ ],
452
564
  },
453
- suites: [{
454
- title: '',
455
- file: 'tests/visual.spec.ts',
456
- specs: [{
457
- title: 'homepage matches',
458
- line: 10,
459
- tests: [{
460
- results: [{
461
- status: 'failed',
462
- attachments: [{ name: 'homepage-1-diff.png', contentType: 'image/png', path: diffPath }],
463
- }],
464
- }],
465
- }],
466
- }],
467
- }
468
-
469
- const reportPath = path.join(tmpDir, 'test/playwright/test-results/results.json')
470
- fs.mkdirSync(path.dirname(reportPath), { recursive: true })
471
- fs.writeFileSync(reportPath, JSON.stringify(raw))
472
-
473
- return reportPath
565
+ suites: [
566
+ {
567
+ title: "",
568
+ file: "tests/visual.spec.ts",
569
+ specs: [
570
+ {
571
+ title: "homepage matches",
572
+ line: 10,
573
+ tests: [
574
+ {
575
+ results: [
576
+ {
577
+ status: "failed",
578
+ attachments: [
579
+ {
580
+ name: "homepage-1-diff.png",
581
+ contentType: "image/png",
582
+ path: diffPath,
583
+ },
584
+ ],
585
+ },
586
+ ],
587
+ },
588
+ ],
589
+ },
590
+ ],
591
+ },
592
+ ],
593
+ };
594
+
595
+ const reportPath = path.join(
596
+ tmpDir,
597
+ "test/playwright/test-results/results.json",
598
+ );
599
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
600
+ fs.writeFileSync(reportPath, JSON.stringify(raw));
601
+
602
+ return reportPath;
474
603
  }
475
604
 
476
- it('finds a diff image written by a container and read from outside it', () => {
605
+ it("finds a diff image written by a container and read from outside it", () => {
477
606
  // The bind mount that makes the two views differ also makes this work:
478
607
  // the same file is under the checkout, at a different prefix.
479
- const diffPath = path.join(tmpDir, 'test/playwright/test-results/homepage-chromium/homepage-1-diff.png')
480
- fs.mkdirSync(path.dirname(diffPath), { recursive: true })
481
- fs.writeFileSync(diffPath, 'png')
608
+ const diffPath = path.join(
609
+ tmpDir,
610
+ "test/playwright/test-results/homepage-chromium/homepage-1-diff.png",
611
+ );
612
+ fs.mkdirSync(path.dirname(diffPath), { recursive: true });
613
+ fs.writeFileSync(diffPath, "png");
482
614
 
483
615
  const reportPath = containerRun(
484
- '/var/www/html/test/playwright/test-results/homepage-chromium/homepage-1-diff.png',
485
- )
486
- const report = parseFailures(reportPath)
616
+ "/var/www/html/test/playwright/test-results/homepage-chromium/homepage-1-diff.png",
617
+ );
618
+ const report = parseFailures(reportPath);
487
619
 
488
- const summary = resolveImagePaths(report, createPathResolver({ reportPath }))
620
+ const summary = resolveImagePaths(
621
+ report,
622
+ createPathResolver({ reportPath }),
623
+ );
489
624
 
490
- expect(summary.unreadable).toEqual([])
491
- expect(summary.remapped).toBe(1)
492
- expect(summary.used).toEqual([{ from: '/var/www/html', to: tmpDir }])
493
- expect(report.tests[0].images[0].filePath).toBe(diffPath)
494
- expect(report.tests[0].images[0].unreadable).toBeUndefined()
495
- })
625
+ expect(summary.unreadable).toEqual([]);
626
+ expect(summary.remapped).toBe(1);
627
+ expect(summary.used).toEqual([{ from: "/var/www/html", to: tmpDir }]);
628
+ expect(report.tests[0].images[0].filePath).toBe(diffPath);
629
+ expect(report.tests[0].images[0].unreadable).toBeUndefined();
630
+ });
496
631
 
497
- it('marks an image it cannot reach, rather than passing the path on', () => {
632
+ it("marks an image it cannot reach, rather than passing the path on", () => {
498
633
  const reportPath = containerRun(
499
- '/var/www/html/test/playwright/test-results/never-written/never-written-diff.png',
500
- )
501
- const report = parseFailures(reportPath)
634
+ "/var/www/html/test/playwright/test-results/never-written/never-written-diff.png",
635
+ );
636
+ const report = parseFailures(reportPath);
502
637
 
503
- const summary = resolveImagePaths(report, createPathResolver({ reportPath }))
638
+ const summary = resolveImagePaths(
639
+ report,
640
+ createPathResolver({ reportPath }),
641
+ );
504
642
 
505
- expect(summary.remapped).toBe(0)
643
+ expect(summary.remapped).toBe(0);
506
644
  expect(summary.unreadable).toEqual([
507
- '/var/www/html/test/playwright/test-results/never-written/never-written-diff.png',
508
- ])
509
- expect(report.tests[0].images[0].unreadable).toBe(true)
510
- })
645
+ "/var/www/html/test/playwright/test-results/never-written/never-written-diff.png",
646
+ ]);
647
+ expect(report.tests[0].images[0].unreadable).toBe(true);
648
+ });
511
649
 
512
- it('leaves an inlined accessibility screenshot alone', () => {
650
+ it("leaves an inlined accessibility screenshot alone", () => {
513
651
  // These carry a body rather than a path, which is why they were the one
514
652
  // attachment kind that never hit the boundary.
515
- const report = parseFailures(writeReport([{
516
- title: 'homepage a11y',
517
- status: 'failed',
518
- attachments: [{
519
- name: 'a11y-violation-screenshot',
520
- contentType: 'image/png',
521
- body: 'aGVsbG8=',
522
- }],
523
- }]))
524
-
525
- const summary = resolveImagePaths(report, createPathResolver({ reportPath: '/nowhere/results.json' }))
526
-
527
- expect(summary).toEqual({ remapped: 0, used: [], unreadable: [] })
528
- expect(report.tests[0].images[0].body).toBe('aGVsbG8=')
529
- })
530
- })
531
-
532
- describe('uploadImages', () => {
533
- it('uploads an inlined body, giving it a name derived from the content type', async () => {
534
- const names: string[] = []
653
+ const report = parseFailures(
654
+ writeReport([
655
+ {
656
+ title: "homepage a11y",
657
+ status: "failed",
658
+ attachments: [
659
+ {
660
+ name: "a11y-violation-screenshot",
661
+ contentType: "image/png",
662
+ body: "aGVsbG8=",
663
+ },
664
+ ],
665
+ },
666
+ ]),
667
+ );
668
+
669
+ const summary = resolveImagePaths(
670
+ report,
671
+ createPathResolver({ reportPath: "/nowhere/results.json" }),
672
+ );
673
+
674
+ expect(summary).toEqual({ remapped: 0, used: [], unreadable: [] });
675
+ expect(report.tests[0].images[0].body).toBe("aGVsbG8=");
676
+ });
677
+ });
678
+
679
+ describe("uploadImages", () => {
680
+ it("uploads an inlined body, giving it a name derived from the content type", async () => {
681
+ const names: string[] = [];
535
682
  const impl: FetchLike = async (url) => {
536
- names.push(new URL(url).searchParams.get('name') ?? '')
537
- return { ok: true, status: 201, text: async () => '{"url":"https://example.test/a"}' }
538
- }
683
+ names.push(new URL(url).searchParams.get("name") ?? "");
684
+ return {
685
+ ok: true,
686
+ status: 201,
687
+ text: async () => '{"url":"https://example.test/a"}',
688
+ };
689
+ };
539
690
 
540
691
  const report: FailureReport = {
541
- tests: [{
542
- title: 'a', file: 'f', line: 1, status: 'passed',
543
- images: [{
544
- name: 'a11y-violation-screenshot',
545
- body: Buffer.from('png bytes').toString('base64'),
546
- contentType: 'image/png',
547
- kind: 'a11y',
548
- }],
549
- }],
692
+ tests: [
693
+ {
694
+ title: "a",
695
+ file: "f",
696
+ line: 1,
697
+ status: "passed",
698
+ images: [
699
+ {
700
+ name: "a11y-violation-screenshot",
701
+ body: Buffer.from("png bytes").toString("base64"),
702
+ contentType: "image/png",
703
+ kind: "a11y",
704
+ },
705
+ ],
706
+ },
707
+ ],
550
708
  totalFailed: 0,
551
709
  totalFlaky: 0,
552
710
  totalImages: 1,
553
- }
711
+ };
554
712
 
555
- await uploadImages(report, new AttachmentUploader({ token: 't', repositoryId: '1', fetchImpl: impl }))
713
+ await uploadImages(
714
+ report,
715
+ new AttachmentUploader({
716
+ token: "t",
717
+ repositoryId: "1",
718
+ fetchImpl: impl,
719
+ }),
720
+ );
556
721
 
557
- expect(names).toEqual(['a11y-violation-screenshot.png'])
558
- expect(report.tests[0].images[0].url).toBe('https://example.test/a')
559
- })
722
+ expect(names).toEqual(["a11y-violation-screenshot.png"]);
723
+ expect(report.tests[0].images[0].url).toBe("https://example.test/a");
724
+ });
560
725
 
561
- it('records a URL per image and leaves the rest alone once uploads stop', async () => {
562
- const filePath = path.join(tmpDir, 'diff.png')
563
- fs.writeFileSync(filePath, Buffer.alloc(8, 1))
726
+ it("records a URL per image and leaves the rest alone once uploads stop", async () => {
727
+ const filePath = path.join(tmpDir, "diff.png");
728
+ fs.writeFileSync(filePath, Buffer.alloc(8, 1));
564
729
 
565
730
  const responses = [
566
731
  { ok: true, status: 201, body: '{"url":"https://example.test/one"}' },
567
732
  { ok: false, status: 404, body: '{"message":"Not Found"}' },
568
- ]
733
+ ];
569
734
  const impl: FetchLike = async () => {
570
- const next = responses.shift()!
571
- return { ok: next.ok, status: next.status, text: async () => next.body }
572
- }
735
+ const next = responses.shift()!;
736
+ return { ok: next.ok, status: next.status, text: async () => next.body };
737
+ };
573
738
 
574
739
  const report: FailureReport = {
575
- tests: [{
576
- title: 'a', file: 'f', line: 1, status: 'failed',
577
- images: [
578
- { name: 'one.png', filePath, contentType: 'image/png', kind: 'diff' },
579
- { name: 'two.png', filePath, contentType: 'image/png', kind: 'diff' },
580
- { name: 'three.png', filePath, contentType: 'image/png', kind: 'diff' },
581
- ],
582
- }],
740
+ tests: [
741
+ {
742
+ title: "a",
743
+ file: "f",
744
+ line: 1,
745
+ status: "failed",
746
+ images: [
747
+ {
748
+ name: "one.png",
749
+ filePath,
750
+ contentType: "image/png",
751
+ kind: "diff",
752
+ },
753
+ {
754
+ name: "two.png",
755
+ filePath,
756
+ contentType: "image/png",
757
+ kind: "diff",
758
+ },
759
+ {
760
+ name: "three.png",
761
+ filePath,
762
+ contentType: "image/png",
763
+ kind: "diff",
764
+ },
765
+ ],
766
+ },
767
+ ],
583
768
  totalFailed: 1,
584
769
  totalFlaky: 0,
585
770
  totalImages: 3,
586
- }
771
+ };
587
772
 
588
- await uploadImages(report, new AttachmentUploader({ token: 't', repositoryId: '1', fetchImpl: impl }))
589
-
590
- const urls = report.tests[0].images.map(i => i.url)
591
- expect(urls).toEqual(['https://example.test/one', undefined, undefined])
592
- })
593
-
594
- it('marks an image the size limit refused without calling it unreadable', async () => {
773
+ await uploadImages(
774
+ report,
775
+ new AttachmentUploader({
776
+ token: "t",
777
+ repositoryId: "1",
778
+ fetchImpl: impl,
779
+ }),
780
+ );
781
+
782
+ const urls = report.tests[0].images.map((i) => i.url);
783
+ expect(urls).toEqual(["https://example.test/one", undefined, undefined]);
784
+ });
785
+
786
+ it("marks an image the size limit refused without calling it unreadable", async () => {
595
787
  // The two render as the same absence, and only one of them is fixed by a
596
788
  // path mapping — so the summary must not send the reader after the wrong
597
789
  // one.
598
- const big = path.join(tmpDir, 'big.png')
599
- fs.writeFileSync(big, Buffer.alloc(200, 1))
600
- const small = path.join(tmpDir, 'small.png')
601
- fs.writeFileSync(small, Buffer.alloc(8, 1))
790
+ const big = path.join(tmpDir, "big.png");
791
+ fs.writeFileSync(big, Buffer.alloc(200, 1));
792
+ const small = path.join(tmpDir, "small.png");
793
+ fs.writeFileSync(small, Buffer.alloc(8, 1));
602
794
 
603
795
  const impl: FetchLike = async () => ({
604
796
  ok: true,
605
797
  status: 201,
606
798
  text: async () => '{"url":"https://example.test/small"}',
607
- })
799
+ });
608
800
 
609
801
  const report: FailureReport = {
610
- tests: [{
611
- title: 'a', file: 'f', line: 1, status: 'failed',
612
- images: [
613
- { name: 'big.png', filePath: big, contentType: 'image/png', kind: 'diff' },
614
- { name: 'small.png', filePath: small, contentType: 'image/png', kind: 'diff' },
615
- ],
616
- }],
802
+ tests: [
803
+ {
804
+ title: "a",
805
+ file: "f",
806
+ line: 1,
807
+ status: "failed",
808
+ images: [
809
+ {
810
+ name: "big.png",
811
+ filePath: big,
812
+ contentType: "image/png",
813
+ kind: "diff",
814
+ },
815
+ {
816
+ name: "small.png",
817
+ filePath: small,
818
+ contentType: "image/png",
819
+ kind: "diff",
820
+ },
821
+ ],
822
+ },
823
+ ],
617
824
  totalFailed: 1,
618
825
  totalFlaky: 0,
619
826
  totalImages: 2,
620
- }
827
+ };
621
828
 
622
829
  await uploadImages(
623
830
  report,
624
- new AttachmentUploader({ token: 't', repositoryId: '1', maxFileBytes: 100, fetchImpl: impl }),
625
- )
626
-
627
- const [oversize, uploaded] = report.tests[0].images
628
- expect(oversize.oversize).toBe(true)
629
- expect(oversize.unreadable).toBeUndefined()
831
+ new AttachmentUploader({
832
+ token: "t",
833
+ repositoryId: "1",
834
+ maxFileBytes: 100,
835
+ fetchImpl: impl,
836
+ }),
837
+ );
838
+
839
+ const [oversize, uploaded] = report.tests[0].images;
840
+ expect(oversize.oversize).toBe(true);
841
+ expect(oversize.unreadable).toBeUndefined();
630
842
  // The one behind it still went, which is the point of skipping rather
631
843
  // than stopping.
632
- expect(uploaded.url).toBe('https://example.test/small')
844
+ expect(uploaded.url).toBe("https://example.test/small");
633
845
 
634
- expect(generateComment(report)).toContain('1 screenshot(s) not shown — 1 too large to upload')
635
- })
636
- })
846
+ expect(generateComment(report)).toContain(
847
+ "1 screenshot(s) not shown — 1 too large to upload",
848
+ );
849
+ });
850
+ });