@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.
- 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 +110 -88
- 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 +140 -108
- 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 +869 -433
- package/src/accessibility-baseline.ts +41 -21
- package/src/accessible-screenshot.test.ts +551 -423
- package/src/accessible-screenshot.ts +358 -251
- 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 +660 -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,542 @@ 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).
|
|
201
|
-
expect(summary).toContain(
|
|
202
|
-
expect(summary).toContain(
|
|
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(
|
|
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(
|
|
209
|
-
expect(summary).toContain(
|
|
210
|
-
})
|
|
259
|
+
expect(summary).not.toContain("<img");
|
|
260
|
+
expect(summary).toContain("not uploaded");
|
|
261
|
+
});
|
|
211
262
|
|
|
212
|
-
it(
|
|
263
|
+
it("says an absent token is why nothing was uploaded", () => {
|
|
213
264
|
const summary = generateSummary(reportWith(undefined), {
|
|
214
|
-
uploadReason:
|
|
215
|
-
})
|
|
265
|
+
uploadReason: "no upload token configured",
|
|
266
|
+
});
|
|
216
267
|
|
|
217
|
-
expect(summary).toContain(
|
|
218
|
-
})
|
|
268
|
+
expect(summary).toContain("not uploaded (no upload token configured)");
|
|
269
|
+
});
|
|
219
270
|
|
|
220
|
-
it(
|
|
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 =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
expect(summary).toContain(
|
|
231
|
-
expect(summary).toContain(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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(
|
|
254
|
-
expect(summary).toContain(
|
|
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(
|
|
257
|
-
})
|
|
320
|
+
expect(summary).toContain("## Test Screenshots");
|
|
321
|
+
});
|
|
258
322
|
|
|
259
|
-
it(
|
|
323
|
+
it("calls out flakes when retries recover every failure", () => {
|
|
260
324
|
const summary = generateSummary({
|
|
261
|
-
tests: [
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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(
|
|
275
|
-
expect(summary).toContain(
|
|
276
|
-
|
|
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(
|
|
280
|
-
const summary = generateSummary({
|
|
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(
|
|
283
|
-
expect(summary).not.toContain(
|
|
284
|
-
})
|
|
355
|
+
expect(summary).toContain("No failing tests");
|
|
356
|
+
expect(summary).not.toContain("<details>");
|
|
357
|
+
});
|
|
285
358
|
|
|
286
|
-
it(
|
|
287
|
-
const report = reportWith(
|
|
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(
|
|
290
|
-
|
|
362
|
+
expect(generateComment(report)).toContain(
|
|
363
|
+
'src="https://example.test/a?x=1&y=2"',
|
|
364
|
+
);
|
|
365
|
+
});
|
|
291
366
|
|
|
292
|
-
it(
|
|
293
|
-
const report = reportWith(
|
|
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(
|
|
297
|
-
|
|
298
|
-
|
|
371
|
+
expect(generateComment(report)).toContain(
|
|
372
|
+
'alt="diff for renders "quoted" text"',
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
299
376
|
|
|
300
|
-
describe(
|
|
377
|
+
describe("generateComment", () => {
|
|
301
378
|
const report: FailureReport = {
|
|
302
|
-
tests: [
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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(
|
|
315
|
-
const comment = generateComment(report, {
|
|
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(
|
|
318
|
-
expect(comment).toContain(
|
|
319
|
-
expect(comment).toContain(
|
|
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(
|
|
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, {
|
|
408
|
+
const comment = generateComment(report, {
|
|
409
|
+
uploadReason: "no upload token configured",
|
|
410
|
+
});
|
|
326
411
|
|
|
327
|
-
expect(comment).toContain(
|
|
328
|
-
|
|
412
|
+
expect(comment).toContain(
|
|
413
|
+
"1 screenshot(s) not shown — no upload token configured",
|
|
414
|
+
);
|
|
415
|
+
});
|
|
329
416
|
|
|
330
|
-
it(
|
|
417
|
+
it("distinguishes unreadable images from an absent token", () => {
|
|
331
418
|
const unreadable: FailureReport = {
|
|
332
419
|
...report,
|
|
333
|
-
tests: [
|
|
334
|
-
|
|
335
|
-
|
|
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(
|
|
342
|
-
|
|
430
|
+
expect(comment).toContain(
|
|
431
|
+
"1 could not be read from the path recorded in the report",
|
|
432
|
+
);
|
|
433
|
+
});
|
|
343
434
|
|
|
344
|
-
it(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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(
|
|
492
|
+
it("reports flaky tests and marks the comment as worth posting", () => {
|
|
387
493
|
const flaky: FailureReport = {
|
|
388
|
-
tests: [
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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(
|
|
404
|
-
expect(comment).toContain(
|
|
405
|
-
expect(comment).toContain(
|
|
406
|
-
expect(comment).toContain(
|
|
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(
|
|
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:
|
|
521
|
+
file: "tests/visual.spec.ts",
|
|
414
522
|
line: i,
|
|
415
|
-
status:
|
|
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(
|
|
424
|
-
})
|
|
425
|
-
})
|
|
531
|
+
expect(generateComment(many)).toContain("…and 2 more");
|
|
532
|
+
});
|
|
533
|
+
});
|
|
426
534
|
|
|
427
|
-
describe(
|
|
428
|
-
it(
|
|
429
|
-
const defused = defuseMaskTriggers(
|
|
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(
|
|
432
|
-
expect(defused.replace(/\u200B/g,
|
|
433
|
-
})
|
|
539
|
+
expect(defused).not.toContain("Bearer tokens");
|
|
540
|
+
expect(defused.replace(/\u200B/g, "")).toBe("sends Bearer tokens");
|
|
541
|
+
});
|
|
434
542
|
|
|
435
|
-
it(
|
|
436
|
-
expect(defuseMaskTriggers(
|
|
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(
|
|
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:
|
|
451
|
-
projects: [
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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(
|
|
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(
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
633
|
+
it("marks an image it cannot reach, rather than passing the path on", () => {
|
|
498
634
|
const reportPath = containerRun(
|
|
499
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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(
|
|
537
|
-
return {
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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(
|
|
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([
|
|
558
|
-
expect(report.tests[0].images[0].url).toBe(
|
|
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(
|
|
562
|
-
const filePath = path.join(tmpDir,
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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,
|
|
599
|
-
fs.writeFileSync(big, Buffer.alloc(200, 1))
|
|
600
|
-
const small = path.join(tmpDir,
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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({
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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(
|
|
845
|
+
expect(uploaded.url).toBe("https://example.test/small");
|
|
633
846
|
|
|
634
|
-
expect(generateComment(report)).toContain(
|
|
635
|
-
|
|
636
|
-
|
|
847
|
+
expect(generateComment(report)).toContain(
|
|
848
|
+
"1 screenshot(s) not shown — 1 too large to upload",
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
});
|