@lullabot/playwright-testing 0.1.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.
- package/bin/github-a11y-summary +2 -2
- package/bin/github-failure-summary +4 -4
- package/lib/accessibility-baseline-file.d.ts +6 -6
- package/lib/accessibility-baseline-file.js +29 -23
- package/lib/accessibility-baseline.js +5 -3
- package/lib/accessible-screenshot.d.ts +4 -4
- package/lib/accessible-screenshot.js +88 -76
- package/lib/focus.js +4 -1
- package/lib/frames.js +4 -4
- package/lib/github/a11y-summary.js +74 -79
- package/lib/github/attachments.d.ts +1 -1
- package/lib/github/attachments.js +36 -34
- package/lib/github/failure-summary.d.ts +4 -4
- package/lib/github/failure-summary.js +139 -107
- package/lib/github/index.d.ts +6 -6
- package/lib/github/report-paths.js +17 -9
- package/lib/hover.js +17 -17
- package/lib/images.js +15 -12
- package/lib/index.d.ts +13 -13
- package/lib/interaction-states.d.ts +2 -2
- package/lib/interaction-states.js +7 -7
- package/lib/mock/index.d.ts +1 -1
- package/lib/mock/youtube.d.ts +1 -1
- package/lib/mock/youtube.js +1 -1
- package/lib/pseudo-state.d.ts +2 -2
- package/lib/pseudo-state.js +15 -7
- package/lib/videos.js +21 -10
- package/lib/visualdiff.d.ts +4 -4
- package/lib/visualdiff.js +35 -27
- package/package.json +3 -3
- package/src/accessibility-baseline-file.test.ts +190 -154
- package/src/accessibility-baseline-file.ts +107 -83
- package/src/accessibility-baseline.test.ts +633 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +334 -245
- package/src/focus.test.ts +23 -23
- package/src/focus.ts +7 -2
- package/src/fonts.test.ts +13 -13
- package/src/fonts.ts +1 -1
- package/src/frames.test.ts +10 -8
- package/src/frames.ts +5 -5
- package/src/github/a11y-summary.test.ts +436 -332
- package/src/github/a11y-summary.ts +189 -179
- package/src/github/attachments.test.ts +228 -167
- package/src/github/attachments.ts +144 -121
- package/src/github/failure-summary.test.ts +659 -445
- package/src/github/failure-summary.ts +376 -274
- package/src/github/index.test.ts +14 -14
- package/src/github/index.ts +6 -6
- package/src/github/report-paths.test.ts +211 -156
- package/src/github/report-paths.ts +75 -61
- package/src/hover.test.ts +50 -46
- package/src/hover.ts +18 -18
- package/src/images.test.ts +251 -206
- package/src/images.ts +52 -43
- package/src/index.ts +13 -13
- package/src/interaction-states.test.ts +72 -42
- package/src/interaction-states.ts +43 -29
- package/src/mock/index.ts +1 -1
- package/src/mock/youtube.test.ts +26 -26
- package/src/mock/youtube.ts +4 -6
- package/src/pseudo-state.test.ts +58 -51
- package/src/pseudo-state.ts +32 -27
- package/src/videos.test.ts +559 -399
- package/src/videos.ts +68 -44
- package/src/visualdiff.test.ts +416 -315
- package/src/visualdiff.ts +140 -99
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from
|
|
2
|
-
import * as fs from
|
|
3
|
-
import * as os from
|
|
4
|
-
import * as path from
|
|
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
|
|
14
|
-
import { createPathResolver } from
|
|
15
|
-
import { AttachmentUploader, FetchLike } from
|
|
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(),
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
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:
|
|
72
|
+
return { name, contentType: "image/png", path: filePath };
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
describe(
|
|
70
|
-
it(
|
|
75
|
+
describe("parseFailures", () => {
|
|
76
|
+
it("collects failing tests and ignores passing ones", () => {
|
|
71
77
|
const reportPath = writeReport([
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
104
|
-
status:
|
|
105
|
-
error:
|
|
106
|
-
})
|
|
107
|
-
expect(report.tests[0].images.map(i => i.kind)).toEqual([
|
|
108
|
-
expect(report.totalFailed).toBe(0)
|
|
109
|
-
expect(report.totalFlaky).toBe(1)
|
|
110
|
-
expect(report.totalImages).toBe(1)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it(
|
|
114
|
-
const reportPath = writeReport([
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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,
|
|
123
|
-
expect(diffOnly.tests[0].images.map(i => i.kind)).toEqual([
|
|
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,
|
|
126
|
-
expect(all.tests[0].images.map(i => i.kind)).toEqual([
|
|
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(
|
|
130
|
-
const reportPath = writeReport([
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
139
|
-
expect(image.filePath).toBeUndefined()
|
|
140
|
-
})
|
|
162
|
+
expect(image.body).toBe("aGVsbG8=");
|
|
163
|
+
expect(image.filePath).toBeUndefined();
|
|
164
|
+
});
|
|
141
165
|
|
|
142
|
-
it(
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
expect(report.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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(
|
|
170
|
-
const reportPath = writeReport([
|
|
171
|
-
|
|
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(
|
|
175
|
-
|
|
176
|
-
|
|
211
|
+
expect(parseFailures(reportPath).tests[0].error).toBe(
|
|
212
|
+
"Screenshot comparison failed",
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
177
216
|
|
|
178
|
-
describe(
|
|
217
|
+
describe("generateSummary", () => {
|
|
179
218
|
function reportWith(url?: string): FailureReport {
|
|
180
219
|
return {
|
|
181
|
-
tests: [
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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(
|
|
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(
|
|
246
|
+
const summary = generateSummary(reportWith("https://example.test/a"));
|
|
199
247
|
|
|
200
|
-
expect(summary).not.toContain(
|
|
201
|
-
expect(summary).toContain(
|
|
202
|
-
|
|
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(
|
|
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(
|
|
209
|
-
expect(summary).toContain(
|
|
210
|
-
})
|
|
258
|
+
expect(summary).not.toContain("<img");
|
|
259
|
+
expect(summary).toContain("not uploaded");
|
|
260
|
+
});
|
|
211
261
|
|
|
212
|
-
it(
|
|
262
|
+
it("says an absent token is why nothing was uploaded", () => {
|
|
213
263
|
const summary = generateSummary(reportWith(undefined), {
|
|
214
|
-
uploadReason:
|
|
215
|
-
})
|
|
264
|
+
uploadReason: "no upload token configured",
|
|
265
|
+
});
|
|
216
266
|
|
|
217
|
-
expect(summary).toContain(
|
|
218
|
-
})
|
|
267
|
+
expect(summary).toContain("not uploaded (no upload token configured)");
|
|
268
|
+
});
|
|
219
269
|
|
|
220
|
-
it(
|
|
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 =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
expect(summary).toContain(
|
|
231
|
-
expect(summary).toContain(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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(
|
|
254
|
-
expect(summary).toContain(
|
|
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(
|
|
257
|
-
})
|
|
319
|
+
expect(summary).toContain("## Test Screenshots");
|
|
320
|
+
});
|
|
258
321
|
|
|
259
|
-
it(
|
|
322
|
+
it("calls out flakes when retries recover every failure", () => {
|
|
260
323
|
const summary = generateSummary({
|
|
261
|
-
tests: [
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
275
|
-
expect(summary).toContain(
|
|
276
|
-
|
|
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(
|
|
280
|
-
const summary = generateSummary({
|
|
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(
|
|
283
|
-
expect(summary).not.toContain(
|
|
284
|
-
})
|
|
354
|
+
expect(summary).toContain("No failing tests");
|
|
355
|
+
expect(summary).not.toContain("<details>");
|
|
356
|
+
});
|
|
285
357
|
|
|
286
|
-
it(
|
|
287
|
-
const report = reportWith(
|
|
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(
|
|
290
|
-
|
|
361
|
+
expect(generateComment(report)).toContain(
|
|
362
|
+
'src="https://example.test/a?x=1&y=2"',
|
|
363
|
+
);
|
|
364
|
+
});
|
|
291
365
|
|
|
292
|
-
it(
|
|
293
|
-
const report = reportWith(
|
|
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(
|
|
297
|
-
|
|
298
|
-
|
|
370
|
+
expect(generateComment(report)).toContain(
|
|
371
|
+
'alt="diff for renders "quoted" text"',
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
299
375
|
|
|
300
|
-
describe(
|
|
376
|
+
describe("generateComment", () => {
|
|
301
377
|
const report: FailureReport = {
|
|
302
|
-
tests: [
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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(
|
|
315
|
-
const comment = generateComment(report, {
|
|
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(
|
|
318
|
-
expect(comment).toContain(
|
|
319
|
-
expect(comment).toContain(
|
|
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(
|
|
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, {
|
|
407
|
+
const comment = generateComment(report, {
|
|
408
|
+
uploadReason: "no upload token configured",
|
|
409
|
+
});
|
|
326
410
|
|
|
327
|
-
expect(comment).toContain(
|
|
328
|
-
|
|
411
|
+
expect(comment).toContain(
|
|
412
|
+
"1 screenshot(s) not shown — no upload token configured",
|
|
413
|
+
);
|
|
414
|
+
});
|
|
329
415
|
|
|
330
|
-
it(
|
|
416
|
+
it("distinguishes unreadable images from an absent token", () => {
|
|
331
417
|
const unreadable: FailureReport = {
|
|
332
418
|
...report,
|
|
333
|
-
tests: [
|
|
334
|
-
|
|
335
|
-
|
|
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(
|
|
342
|
-
|
|
429
|
+
expect(comment).toContain(
|
|
430
|
+
"1 could not be read from the path recorded in the report",
|
|
431
|
+
);
|
|
432
|
+
});
|
|
343
433
|
|
|
344
|
-
it(
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
expect(comment).
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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(
|
|
491
|
+
it("reports flaky tests and marks the comment as worth posting", () => {
|
|
387
492
|
const flaky: FailureReport = {
|
|
388
|
-
tests: [
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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(
|
|
404
|
-
expect(comment).toContain(
|
|
405
|
-
expect(comment).toContain(
|
|
406
|
-
expect(comment).toContain(
|
|
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(
|
|
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:
|
|
520
|
+
file: "tests/visual.spec.ts",
|
|
414
521
|
line: i,
|
|
415
|
-
status:
|
|
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(
|
|
424
|
-
})
|
|
425
|
-
})
|
|
530
|
+
expect(generateComment(many)).toContain("…and 2 more");
|
|
531
|
+
});
|
|
532
|
+
});
|
|
426
533
|
|
|
427
|
-
describe(
|
|
428
|
-
it(
|
|
429
|
-
const defused = defuseMaskTriggers(
|
|
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(
|
|
432
|
-
expect(defused.replace(/\u200B/g,
|
|
433
|
-
})
|
|
538
|
+
expect(defused).not.toContain("Bearer tokens");
|
|
539
|
+
expect(defused.replace(/\u200B/g, "")).toBe("sends Bearer tokens");
|
|
540
|
+
});
|
|
434
541
|
|
|
435
|
-
it(
|
|
436
|
-
expect(defuseMaskTriggers(
|
|
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(
|
|
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:
|
|
451
|
-
projects: [
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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(
|
|
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(
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
632
|
+
it("marks an image it cannot reach, rather than passing the path on", () => {
|
|
498
633
|
const reportPath = containerRun(
|
|
499
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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(
|
|
537
|
-
return {
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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(
|
|
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([
|
|
558
|
-
expect(report.tests[0].images[0].url).toBe(
|
|
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(
|
|
562
|
-
const filePath = path.join(tmpDir,
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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,
|
|
599
|
-
fs.writeFileSync(big, Buffer.alloc(200, 1))
|
|
600
|
-
const small = path.join(tmpDir,
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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({
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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(
|
|
844
|
+
expect(uploaded.url).toBe("https://example.test/small");
|
|
633
845
|
|
|
634
|
-
expect(generateComment(report)).toContain(
|
|
635
|
-
|
|
636
|
-
|
|
846
|
+
expect(generateComment(report)).toContain(
|
|
847
|
+
"1 screenshot(s) not shown — 1 too large to upload",
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
});
|