@lullabot/playwright-testing 1.0.0 → 1.1.0

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