@lullabot/playwright-testing 0.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/README.md +61 -0
- package/bin/github-a11y-summary +5 -0
- package/bin/github-failure-summary +8 -0
- package/lib/accessibility-baseline-file.d.ts +47 -0
- package/lib/accessibility-baseline-file.js +205 -0
- package/lib/accessibility-baseline.d.ts +19 -0
- package/lib/accessibility-baseline.js +38 -0
- package/lib/accessible-screenshot.d.ts +181 -0
- package/lib/accessible-screenshot.js +519 -0
- package/lib/focus.d.ts +14 -0
- package/lib/focus.js +28 -0
- package/lib/fonts.d.ts +18 -0
- package/lib/fonts.js +24 -0
- package/lib/frames.d.ts +7 -0
- package/lib/frames.js +27 -0
- package/lib/github/a11y-summary.d.ts +55 -0
- package/lib/github/a11y-summary.js +383 -0
- package/lib/github/attachments.d.ts +98 -0
- package/lib/github/attachments.js +297 -0
- package/lib/github/failure-summary.d.ts +144 -0
- package/lib/github/failure-summary.js +567 -0
- package/lib/github/index.d.ts +6 -0
- package/lib/github/index.js +35 -0
- package/lib/github/report-paths.d.ts +38 -0
- package/lib/github/report-paths.js +200 -0
- package/lib/hover.d.ts +13 -0
- package/lib/hover.js +61 -0
- package/lib/images.d.ts +118 -0
- package/lib/images.js +260 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +29 -0
- package/lib/interaction-states.d.ts +22 -0
- package/lib/interaction-states.js +75 -0
- package/lib/mock/index.d.ts +1 -0
- package/lib/mock/index.js +5 -0
- package/lib/mock/youtube.d.ts +5 -0
- package/lib/mock/youtube.js +38 -0
- package/lib/pseudo-state.d.ts +17 -0
- package/lib/pseudo-state.js +50 -0
- package/lib/videos.d.ts +134 -0
- package/lib/videos.js +349 -0
- package/lib/visualdiff.d.ts +154 -0
- package/lib/visualdiff.js +197 -0
- package/package.json +47 -0
- package/src/accessibility-baseline-file.test.ts +181 -0
- package/src/accessibility-baseline-file.ts +208 -0
- package/src/accessibility-baseline.test.ts +601 -0
- package/src/accessibility-baseline.ts +50 -0
- package/src/accessible-screenshot.test.ts +597 -0
- package/src/accessible-screenshot.ts +809 -0
- package/src/focus.test.ts +34 -0
- package/src/focus.ts +27 -0
- package/src/fonts.test.ts +17 -0
- package/src/fonts.ts +23 -0
- package/src/frames.test.ts +75 -0
- package/src/frames.ts +26 -0
- package/src/github/a11y-summary.test.ts +439 -0
- package/src/github/a11y-summary.ts +421 -0
- package/src/github/attachments.test.ts +248 -0
- package/src/github/attachments.ts +328 -0
- package/src/github/failure-summary.test.ts +636 -0
- package/src/github/failure-summary.ts +720 -0
- package/src/github/index.test.ts +24 -0
- package/src/github/index.ts +35 -0
- package/src/github/report-paths.test.ts +222 -0
- package/src/github/report-paths.ts +208 -0
- package/src/hover.test.ts +76 -0
- package/src/hover.ts +64 -0
- package/src/images.test.ts +355 -0
- package/src/images.ts +299 -0
- package/src/index.ts +13 -0
- package/src/interaction-states.test.ts +48 -0
- package/src/interaction-states.ts +94 -0
- package/src/mock/index.ts +1 -0
- package/src/mock/youtube.test.ts +38 -0
- package/src/mock/youtube.ts +39 -0
- package/src/pseudo-state.test.ts +83 -0
- package/src/pseudo-state.ts +69 -0
- package/src/videos.test.ts +637 -0
- package/src/videos.ts +389 -0
- package/src/visualdiff.test.ts +452 -0
- package/src/visualdiff.ts +381 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AttachmentUploader = void 0;
|
|
37
|
+
exports.mimeTypeFor = mimeTypeFor;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* Uploads files to GitHub's user-attachments endpoint — the programmatic
|
|
42
|
+
* equivalent of dragging an image into a comment box, which is the only way to
|
|
43
|
+
* get an image into a job summary or a PR comment.
|
|
44
|
+
*
|
|
45
|
+
* The endpoint is undocumented, and two things about it drive this design:
|
|
46
|
+
*
|
|
47
|
+
* 1. It only accepts *user* tokens. The Actions `GITHUB_TOKEN` and GitHub App
|
|
48
|
+
* installation tokens are both refused with a 404, so the feature is opt-in
|
|
49
|
+
* behind a PAT and every caller must cope with it being switched off.
|
|
50
|
+
* 2. The URL it returns is a renderer-only handle. Fetching it directly is a
|
|
51
|
+
* 404 with or without credentials; GitHub rewrites it to a signed, short
|
|
52
|
+
* lived URL when it renders the surrounding Markdown. Never try to verify
|
|
53
|
+
* an upload by reading it back — a 201 with a URL in the body is the only
|
|
54
|
+
* success signal there is.
|
|
55
|
+
*
|
|
56
|
+
* What it accepts is no longer guesswork, even though the endpoint has no
|
|
57
|
+
* documentation of its own: `gh --attach` (CLI 2.99.0) is a supported wrapper
|
|
58
|
+
* over the same upload, and GitHub documents its formats and size limits. That
|
|
59
|
+
* flag needs the same kind of user token, which is why it is no help here —
|
|
60
|
+
* see docs/working-with-tests/screenshots-in-ci.md.
|
|
61
|
+
*
|
|
62
|
+
* A single endpoint failure still disables the uploader for the rest of the
|
|
63
|
+
* run: retrying an endpoint that has answered 404 once only wastes CI time,
|
|
64
|
+
* and nothing here is worth failing a build over.
|
|
65
|
+
*/
|
|
66
|
+
const UPLOAD_ORIGIN = 'https://uploads.github.com';
|
|
67
|
+
const DEFAULT_MAX_UPLOADS = 20;
|
|
68
|
+
const DEFAULT_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
69
|
+
/**
|
|
70
|
+
* GitHub's own ceiling for a single image or GIF, as documented for the
|
|
71
|
+
* `gh --attach` flag over this endpoint. Video is 10 MB on free plans and
|
|
72
|
+
* 100 MB on paid ones; this uploader carries screenshots, so the image number
|
|
73
|
+
* is the one that binds. Checking it here turns a rejection that would stop
|
|
74
|
+
* the run into one skipped file.
|
|
75
|
+
*/
|
|
76
|
+
const DEFAULT_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
77
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
78
|
+
const MIME_TYPES = {
|
|
79
|
+
'.png': 'image/png',
|
|
80
|
+
'.jpg': 'image/jpeg',
|
|
81
|
+
'.jpeg': 'image/jpeg',
|
|
82
|
+
'.gif': 'image/gif',
|
|
83
|
+
'.webp': 'image/webp',
|
|
84
|
+
'.svg': 'image/svg+xml',
|
|
85
|
+
'.webm': 'video/webm',
|
|
86
|
+
'.mp4': 'video/mp4',
|
|
87
|
+
'.mov': 'video/quicktime',
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Guess a MIME type from a file extension. The endpoint requires one, and it
|
|
91
|
+
* is what decides whether GitHub renders the attachment inline.
|
|
92
|
+
*/
|
|
93
|
+
function mimeTypeFor(filePath) {
|
|
94
|
+
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
95
|
+
}
|
|
96
|
+
class AttachmentUploader {
|
|
97
|
+
token;
|
|
98
|
+
repositoryId;
|
|
99
|
+
maxUploads;
|
|
100
|
+
maxTotalBytes;
|
|
101
|
+
maxFileBytes;
|
|
102
|
+
timeoutMs;
|
|
103
|
+
fetchImpl;
|
|
104
|
+
log;
|
|
105
|
+
disabled;
|
|
106
|
+
reason = null;
|
|
107
|
+
lastSkip = null;
|
|
108
|
+
stats = { uploaded: 0, skipped: 0, bytes: 0 };
|
|
109
|
+
constructor(options = {}) {
|
|
110
|
+
this.token = String(options.token ?? '');
|
|
111
|
+
this.repositoryId = String(options.repositoryId ?? '');
|
|
112
|
+
this.maxUploads = options.maxUploads ?? DEFAULT_MAX_UPLOADS;
|
|
113
|
+
this.maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
114
|
+
this.maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
115
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
116
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
117
|
+
this.log = options.log ?? (() => { });
|
|
118
|
+
if (!this.token) {
|
|
119
|
+
this.disabled = true;
|
|
120
|
+
this.reason = 'no upload token configured';
|
|
121
|
+
}
|
|
122
|
+
else if (!this.repositoryId) {
|
|
123
|
+
this.disabled = true;
|
|
124
|
+
this.reason = 'no repository ID available';
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.disabled = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
get enabled() {
|
|
131
|
+
return !this.disabled;
|
|
132
|
+
}
|
|
133
|
+
/** Why uploads stopped, or null while they are still running. */
|
|
134
|
+
get disabledReason() {
|
|
135
|
+
return this.reason;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Why the most recent call returned null, or null when it returned a URL.
|
|
139
|
+
*
|
|
140
|
+
* A file that could not be read and one the size limits refused both come
|
|
141
|
+
* back as null from a still-running uploader, and a caller that cannot tell
|
|
142
|
+
* them apart reports the wrong problem — an oversized screenshot reads as a
|
|
143
|
+
* missing one, which sends the reader hunting for a path mapping that is not
|
|
144
|
+
* wrong.
|
|
145
|
+
*/
|
|
146
|
+
get lastSkipReason() {
|
|
147
|
+
return this.lastSkip;
|
|
148
|
+
}
|
|
149
|
+
getStats() {
|
|
150
|
+
return { ...this.stats };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Upload one file and return the URL to embed, or null if the upload did not
|
|
154
|
+
* happen. Never throws and never rejects: a broken undocumented endpoint
|
|
155
|
+
* must not fail anyone's build.
|
|
156
|
+
*/
|
|
157
|
+
async upload(filePath, displayName) {
|
|
158
|
+
if (this.disabled)
|
|
159
|
+
return this.skip('disabled');
|
|
160
|
+
let body;
|
|
161
|
+
try {
|
|
162
|
+
body = fs.readFileSync(filePath);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// A missing file is this file's problem, not a reason to stop the run.
|
|
166
|
+
return this.skip('unreadable', `Attachment upload skipped, unreadable file: ${filePath}`);
|
|
167
|
+
}
|
|
168
|
+
return this.uploadBuffer(body, displayName ?? path.basename(filePath), mimeTypeFor(filePath));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Upload bytes that never touched the disk. Playwright's JSON reporter
|
|
172
|
+
* inlines attachments added with `testInfo.attach({ body })` as base64, which
|
|
173
|
+
* is how the accessibility screenshots arrive.
|
|
174
|
+
*/
|
|
175
|
+
async uploadBuffer(body, displayName, contentType) {
|
|
176
|
+
if (this.disabled)
|
|
177
|
+
return this.skip('disabled');
|
|
178
|
+
const size = body.length;
|
|
179
|
+
const name = sanitizeName(displayName);
|
|
180
|
+
// A count budget is reached once and stays reached, so stop there.
|
|
181
|
+
if (this.stats.uploaded >= this.maxUploads) {
|
|
182
|
+
return this.disable(`upload limit of ${this.maxUploads} reached`);
|
|
183
|
+
}
|
|
184
|
+
// Size limits are the individual file's problem. GitHub refuses one over
|
|
185
|
+
// its own ceiling, and a run has only so many bytes to spend; either way
|
|
186
|
+
// one large screenshot must not cost the run every smaller one behind it,
|
|
187
|
+
// so skip it and carry on. `maxUploads` bounds how often this can repeat.
|
|
188
|
+
if (size > this.maxFileBytes) {
|
|
189
|
+
return this.skip('too-large', `Attachment skipped, ${size} bytes is over the ${this.maxFileBytes} byte limit: ${name}`);
|
|
190
|
+
}
|
|
191
|
+
if (this.stats.bytes + size > this.maxTotalBytes) {
|
|
192
|
+
const left = this.maxTotalBytes - this.stats.bytes;
|
|
193
|
+
return this.skip('budget', `Attachment skipped, ${size} bytes does not fit the ${left} bytes left in the budget: ${name}`);
|
|
194
|
+
}
|
|
195
|
+
const query = new URLSearchParams({
|
|
196
|
+
name,
|
|
197
|
+
content_type: contentType,
|
|
198
|
+
repository_id: this.repositoryId,
|
|
199
|
+
});
|
|
200
|
+
try {
|
|
201
|
+
const response = await this.fetchImpl(`${UPLOAD_ORIGIN}/user-attachments/assets?${query}`, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: {
|
|
204
|
+
Authorization: `Bearer ${this.token}`,
|
|
205
|
+
Accept: 'application/json',
|
|
206
|
+
// Required. Without it the endpoint answers 400 "Invalid
|
|
207
|
+
// Content-Type", and fetch sends no default for a Buffer body.
|
|
208
|
+
'Content-Type': contentType,
|
|
209
|
+
},
|
|
210
|
+
body,
|
|
211
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
212
|
+
});
|
|
213
|
+
const text = await response.text();
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
return this.disable(`HTTP ${response.status} from the attachments endpoint: ${summarize(text)}`);
|
|
216
|
+
}
|
|
217
|
+
const url = parseUploadUrl(text);
|
|
218
|
+
if (!url) {
|
|
219
|
+
return this.disable('the attachments endpoint returned no URL');
|
|
220
|
+
}
|
|
221
|
+
this.stats.uploaded++;
|
|
222
|
+
this.stats.bytes += size;
|
|
223
|
+
this.lastSkip = null;
|
|
224
|
+
return url;
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
228
|
+
return this.disable(`upload failed: ${message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Switch the uploader off for the rest of the run and say why. */
|
|
232
|
+
disable(reason) {
|
|
233
|
+
this.disabled = true;
|
|
234
|
+
this.reason = reason;
|
|
235
|
+
this.log(`Attachment uploads disabled — ${reason}.`);
|
|
236
|
+
return this.skip('disabled');
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Record one file that was not uploaded, and why. Most reasons leave the
|
|
240
|
+
* uploader running; `disable()` routes through here for the one that does
|
|
241
|
+
* not, so `lastSkipReason` is set however a call came back null.
|
|
242
|
+
*/
|
|
243
|
+
skip(reason, message) {
|
|
244
|
+
this.stats.skipped++;
|
|
245
|
+
this.lastSkip = reason;
|
|
246
|
+
if (message)
|
|
247
|
+
this.log(message);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
exports.AttachmentUploader = AttachmentUploader;
|
|
252
|
+
/**
|
|
253
|
+
* Pull the asset URL out of a response body. The endpoint has been seen to
|
|
254
|
+
* answer with `url`; `href` appears in third-party write-ups, so accept both.
|
|
255
|
+
*/
|
|
256
|
+
function parseUploadUrl(text) {
|
|
257
|
+
try {
|
|
258
|
+
const parsed = JSON.parse(text);
|
|
259
|
+
const url = parsed?.url ?? parsed?.href ?? parsed?.asset?.href;
|
|
260
|
+
return typeof url === 'string' && url ? url : null;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Condense a response body into one line worth putting in a CI log. */
|
|
267
|
+
function summarize(text) {
|
|
268
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
269
|
+
if (!collapsed)
|
|
270
|
+
return 'no response body';
|
|
271
|
+
return collapsed.length > 200 ? `${collapsed.slice(0, 200)}…` : collapsed;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Trim leading and trailing dashes.
|
|
275
|
+
*
|
|
276
|
+
* An index scan rather than `/^-+|-+$/`, because that pattern backtracks
|
|
277
|
+
* quadratically over a long run of dashes and this input is derived from test
|
|
278
|
+
* titles — which nobody controls.
|
|
279
|
+
*/
|
|
280
|
+
function trimDashes(value) {
|
|
281
|
+
let start = 0;
|
|
282
|
+
let end = value.length;
|
|
283
|
+
while (start < end && value[start] === '-')
|
|
284
|
+
start++;
|
|
285
|
+
while (end > start && value[end - 1] === '-')
|
|
286
|
+
end--;
|
|
287
|
+
return value.slice(start, end);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Reduce a file name to something safe to put in a query string. Test titles
|
|
291
|
+
* reach this by way of snapshot file names and can contain anything.
|
|
292
|
+
*/
|
|
293
|
+
function sanitizeName(name) {
|
|
294
|
+
// Truncate before trimming, so a trailing dash left by the cut goes too.
|
|
295
|
+
const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 120);
|
|
296
|
+
return trimDashes(cleaned) || 'attachment';
|
|
297
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AttachmentUploader, AttachmentUploaderOptions } from './attachments';
|
|
2
|
+
import { PathPrefix, PathResolution } from './report-paths';
|
|
3
|
+
export type ImageKind = 'diff' | 'actual' | 'expected' | 'previous' | 'a11y' | 'screenshot';
|
|
4
|
+
export interface FailureImage {
|
|
5
|
+
/** Attachment name as it appears in the report. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Path on disk. Attachments the reporter inlined have none. */
|
|
8
|
+
filePath?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Base64 bytes, for attachments added with `testInfo.attach({ body })`. The
|
|
11
|
+
* JSON reporter inlines those rather than writing them out, which is how the
|
|
12
|
+
* accessibility screenshots arrive.
|
|
13
|
+
*/
|
|
14
|
+
body?: string;
|
|
15
|
+
contentType: string;
|
|
16
|
+
kind: ImageKind;
|
|
17
|
+
/** Populated once the image has been uploaded. */
|
|
18
|
+
url?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Set when the recorded path could not be found on this side of a container
|
|
21
|
+
* boundary. Distinguishes "there was nothing to upload with" from "there was
|
|
22
|
+
* nowhere to upload to", which otherwise render identically.
|
|
23
|
+
*/
|
|
24
|
+
unreadable?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Set when the uploader would not carry the image: over GitHub's 10 MB
|
|
27
|
+
* ceiling, or over what is left of the run's byte budget. Uploading carried
|
|
28
|
+
* on around it, so this is one image's story rather than the run's.
|
|
29
|
+
*/
|
|
30
|
+
oversize?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface FailedTest {
|
|
33
|
+
title: string;
|
|
34
|
+
file: string;
|
|
35
|
+
line: number;
|
|
36
|
+
/** Display status, including the synthetic `flaky` status for a retry that passed. */
|
|
37
|
+
status: string;
|
|
38
|
+
/** First error message, trimmed to something a comment can carry. */
|
|
39
|
+
error?: string;
|
|
40
|
+
images: FailureImage[];
|
|
41
|
+
}
|
|
42
|
+
export interface FailureReport {
|
|
43
|
+
tests: FailedTest[];
|
|
44
|
+
totalFailed: number;
|
|
45
|
+
totalFlaky: number;
|
|
46
|
+
totalImages: number;
|
|
47
|
+
}
|
|
48
|
+
/** Which snapshot images to include. `diff` is the one that shows the problem. */
|
|
49
|
+
export type IncludeMode = 'diff' | 'all';
|
|
50
|
+
/**
|
|
51
|
+
* Prefix of the HTML comment carrying the failure count in a generated comment
|
|
52
|
+
* body. Invisible when rendered, and greppable by a workflow that has to decide
|
|
53
|
+
* whether the run is worth commenting on.
|
|
54
|
+
*/
|
|
55
|
+
export declare const FAILURE_MARKER_PREFIX = "<!-- playwright-testing-failures: ";
|
|
56
|
+
/** Machine-readable flaky-test count used by the comment assembly action. */
|
|
57
|
+
export declare const FLAKE_MARKER_PREFIX = "<!-- playwright-testing-flakes: ";
|
|
58
|
+
/**
|
|
59
|
+
* Neutralise text that the Actions runner would redact.
|
|
60
|
+
*
|
|
61
|
+
* Job summaries pass through the runner's secret masking (comments posted over
|
|
62
|
+
* the API do not). The masker replaces `Bearer <value>` with `***` and takes
|
|
63
|
+
* the following characters with it, so a test name or error containing the
|
|
64
|
+
* word can silently swallow the Markdown that follows it. A zero-width space
|
|
65
|
+
* inside the word reads identically and no longer matches.
|
|
66
|
+
*/
|
|
67
|
+
export declare function defuseMaskTriggers(text: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Parse a Playwright JSON report into the failures worth reporting.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseFailures(reportPath: string, include?: IncludeMode): FailureReport;
|
|
72
|
+
/** What re-rooting the report's attachment paths achieved. */
|
|
73
|
+
export interface PathResolutionSummary {
|
|
74
|
+
/** Attachments whose recorded path had to be rewritten to be readable. */
|
|
75
|
+
remapped: number;
|
|
76
|
+
/** The mappings that were used, for the log line. */
|
|
77
|
+
used: PathPrefix[];
|
|
78
|
+
/** Recorded paths with no readable file behind them, on either side. */
|
|
79
|
+
unreadable: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Point every image at a file this process can open, and note the ones where
|
|
83
|
+
* that was not possible.
|
|
84
|
+
*
|
|
85
|
+
* Runs whether or not uploads are switched on: an unreachable attachment is
|
|
86
|
+
* worth reporting even on a fork build that was never going to upload it,
|
|
87
|
+
* because it is the same misconfiguration either way.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveImagePaths(report: FailureReport, resolve: (filePath: string) => PathResolution): PathResolutionSummary;
|
|
90
|
+
/**
|
|
91
|
+
* Upload every collected image, annotating each one with its URL. Images the
|
|
92
|
+
* uploader declines are simply left without a URL, which the renderers treat
|
|
93
|
+
* as "link to the artifact instead".
|
|
94
|
+
*/
|
|
95
|
+
export declare function uploadImages(report: FailureReport, uploader: AttachmentUploader): Promise<void>;
|
|
96
|
+
export interface SummaryOptions {
|
|
97
|
+
artifactHint?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Why uploading did not happen at all, phrased to sit inside a sentence —
|
|
100
|
+
* `no upload token configured`, say. Leave unset when uploads ran.
|
|
101
|
+
*/
|
|
102
|
+
uploadReason?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Render the job summary: a heading, a headline, and one collapsed block per
|
|
106
|
+
* test holding its images.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately table-free. Test titles and error messages are arbitrary text,
|
|
109
|
+
* and the runner's masking can eat a cell delimiter and corrupt a whole row.
|
|
110
|
+
*/
|
|
111
|
+
export declare function generateSummary(report: FailureReport, options?: SummaryOptions): string;
|
|
112
|
+
/**
|
|
113
|
+
* Render the pull request comment. This is where the screenshots go.
|
|
114
|
+
*
|
|
115
|
+
* A comment is rendered afresh every time it is read, so an attachment resolves
|
|
116
|
+
* however recently it was uploaded. A job summary is rendered once when the job
|
|
117
|
+
* ends, before a just-uploaded attachment is resolvable, and that result is
|
|
118
|
+
* what everyone sees from then on.
|
|
119
|
+
*
|
|
120
|
+
* The cost is emailed notifications: those are rendered once when sent, and the
|
|
121
|
+
* signed URLs behind these images expire minutes later, so the images will be
|
|
122
|
+
* broken in the email even though they are fine on the web.
|
|
123
|
+
*/
|
|
124
|
+
export interface FailureCommentOptions {
|
|
125
|
+
summaryUrl?: string;
|
|
126
|
+
title?: string;
|
|
127
|
+
uploadReason?: string;
|
|
128
|
+
/** Adapter namespace for the machine-readable failure marker. */
|
|
129
|
+
failureMarkerPrefix?: string;
|
|
130
|
+
/** Adapter namespace for the machine-readable flaky-test marker. */
|
|
131
|
+
flakeMarkerPrefix?: string;
|
|
132
|
+
}
|
|
133
|
+
export declare function generateComment(report: FailureReport, options?: FailureCommentOptions): string;
|
|
134
|
+
export declare function uploaderFromEnvironment(overrides?: AttachmentUploaderOptions): AttachmentUploader;
|
|
135
|
+
/**
|
|
136
|
+
* CLI entry point. Runs once and writes both outputs, so images are never
|
|
137
|
+
* uploaded twice for the same run.
|
|
138
|
+
*/
|
|
139
|
+
export interface FailureSummaryMainOptions {
|
|
140
|
+
commandName?: string;
|
|
141
|
+
failureMarkerPrefix?: string;
|
|
142
|
+
flakeMarkerPrefix?: string;
|
|
143
|
+
}
|
|
144
|
+
export declare function main(args?: string[], adapter?: FailureSummaryMainOptions): Promise<void>;
|