ScuttleBot

scuttlebot / tests / e2e / node_modules / playwright / lib / matchers / toMatchSnapshot.js
Blame History Raw 343 lines
1
"use strict";
2
var __create = Object.create;
3
var __defProp = Object.defineProperty;
4
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
var __getOwnPropNames = Object.getOwnPropertyNames;
6
var __getProtoOf = Object.getPrototypeOf;
7
var __hasOwnProp = Object.prototype.hasOwnProperty;
8
var __export = (target, all) => {
9
for (var name in all)
10
__defProp(target, name, { get: all[name], enumerable: true });
11
};
12
var __copyProps = (to, from, except, desc) => {
13
if (from && typeof from === "object" || typeof from === "function") {
14
for (let key of __getOwnPropNames(from))
15
if (!__hasOwnProp.call(to, key) && key !== except)
16
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
}
18
return to;
19
};
20
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
// If the importer is in node compatibility mode or this is not an ESM
22
// file that has been converted to a CommonJS file using a Babel-
23
// compatible transform (i.e. "__esModule" has not been set), then set
24
// "default" to the CommonJS "module.exports" for node compatibility.
25
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
mod
27
));
28
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
var toMatchSnapshot_exports = {};
30
__export(toMatchSnapshot_exports, {
31
toHaveScreenshot: () => toHaveScreenshot,
32
toHaveScreenshotStepTitle: () => toHaveScreenshotStepTitle,
33
toMatchSnapshot: () => toMatchSnapshot
34
});
35
module.exports = __toCommonJS(toMatchSnapshot_exports);
36
var import_fs = __toESM(require("fs"));
37
var import_path = __toESM(require("path"));
38
var import_utils = require("playwright-core/lib/utils");
39
var import_utils2 = require("playwright-core/lib/utils");
40
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
41
var import_util = require("../util");
42
var import_globals = require("../common/globals");
43
const NonConfigProperties = [
44
"clip",
45
"fullPage",
46
"mask",
47
"maskColor",
48
"omitBackground",
49
"timeout"
50
];
51
class SnapshotHelper {
52
constructor(state, testInfo, matcherName, locator, anonymousSnapshotExtension, configOptions, nameOrOptions, optOptions) {
53
let name;
54
if (Array.isArray(nameOrOptions) || typeof nameOrOptions === "string") {
55
name = nameOrOptions;
56
this.options = { ...optOptions };
57
} else {
58
const { name: nameFromOptions, ...options } = nameOrOptions;
59
this.options = options;
60
name = nameFromOptions;
61
}
62
this.name = Array.isArray(name) ? name.join(import_path.default.sep) : name || "";
63
const resolvedPaths = testInfo._resolveSnapshotPaths(matcherName === "toHaveScreenshot" ? "screenshot" : "snapshot", name, "updateSnapshotIndex", anonymousSnapshotExtension);
64
this.expectedPath = resolvedPaths.absoluteSnapshotPath;
65
this.attachmentBaseName = resolvedPaths.relativeOutputPath;
66
const outputBasePath = testInfo._getOutputPath(resolvedPaths.relativeOutputPath);
67
this.legacyExpectedPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-expected");
68
this.previousPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-previous");
69
this.actualPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-actual");
70
this.diffPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-diff");
71
const filteredConfigOptions = { ...configOptions };
72
for (const prop of NonConfigProperties)
73
delete filteredConfigOptions[prop];
74
this.options = {
75
...filteredConfigOptions,
76
...this.options
77
};
78
if (this.options._comparator) {
79
this.options.comparator = this.options._comparator;
80
delete this.options._comparator;
81
}
82
if (this.options.maxDiffPixels !== void 0 && this.options.maxDiffPixels < 0)
83
throw new Error("`maxDiffPixels` option value must be non-negative integer");
84
if (this.options.maxDiffPixelRatio !== void 0 && (this.options.maxDiffPixelRatio < 0 || this.options.maxDiffPixelRatio > 1))
85
throw new Error("`maxDiffPixelRatio` option value must be between 0 and 1");
86
this.matcherName = matcherName;
87
this.locator = locator;
88
this.updateSnapshots = testInfo.config.updateSnapshots;
89
this.mimeType = import_utilsBundle.mime.getType(import_path.default.basename(this.expectedPath)) ?? "application/octet-stream";
90
this.comparator = (0, import_utils.getComparator)(this.mimeType);
91
this.testInfo = testInfo;
92
this.state = state;
93
this.kind = this.mimeType.startsWith("image/") ? "Screenshot" : "Snapshot";
94
}
95
createMatcherResult(message, pass, log) {
96
const unfiltered = {
97
name: this.matcherName,
98
expected: this.expectedPath,
99
actual: this.actualPath,
100
diff: this.diffPath,
101
pass,
102
message: () => message,
103
log
104
};
105
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== void 0));
106
}
107
handleMissingNegated() {
108
const isWriteMissingMode = this.updateSnapshots !== "none";
109
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? `, matchers using ".not" won't write them automatically.` : "."}`;
110
return this.createMatcherResult(message, true);
111
}
112
handleDifferentNegated() {
113
return this.createMatcherResult("", false);
114
}
115
handleMatchingNegated() {
116
const message = [
117
import_utils2.colors.red(`${this.kind} comparison failed:`),
118
"",
119
indent("Expected result should be different from the actual one.", " ")
120
].join("\n");
121
return this.createMatcherResult(message, true);
122
}
123
handleMissing(actual, step) {
124
const isWriteMissingMode = this.updateSnapshots !== "none";
125
if (isWriteMissingMode)
126
writeFileSync(this.expectedPath, actual);
127
step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-expected"), contentType: this.mimeType, path: this.expectedPath });
128
writeFileSync(this.actualPath, actual);
129
step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-actual"), contentType: this.mimeType, path: this.actualPath });
130
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ", writing actual." : "."}`;
131
if (this.updateSnapshots === "all" || this.updateSnapshots === "changed") {
132
console.log(message);
133
return this.createMatcherResult(message, true);
134
}
135
if (this.updateSnapshots === "missing") {
136
this.testInfo._hasNonRetriableError = true;
137
this.testInfo._failWithError(new Error(message));
138
return this.createMatcherResult("", true);
139
}
140
return this.createMatcherResult(message, false);
141
}
142
handleDifferent(actual, expected, previous, diff, header, diffError, log, step) {
143
const output = [`${header}${indent(diffError, " ")}`];
144
if (this.name) {
145
output.push("");
146
output.push(` Snapshot: ${this.name}`);
147
}
148
if (expected !== void 0) {
149
writeFileSync(this.legacyExpectedPath, expected);
150
step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-expected"), contentType: this.mimeType, path: this.expectedPath });
151
}
152
if (previous !== void 0) {
153
writeFileSync(this.previousPath, previous);
154
step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-previous"), contentType: this.mimeType, path: this.previousPath });
155
}
156
if (actual !== void 0) {
157
writeFileSync(this.actualPath, actual);
158
step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-actual"), contentType: this.mimeType, path: this.actualPath });
159
}
160
if (diff !== void 0) {
161
writeFileSync(this.diffPath, diff);
162
step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-diff"), contentType: this.mimeType, path: this.diffPath });
163
}
164
if (log?.length)
165
output.push((0, import_utils.callLogText)(this.state.utils, log));
166
else
167
output.push("");
168
return this.createMatcherResult(output.join("\n"), false, log);
169
}
170
handleMatching() {
171
return this.createMatcherResult("", true);
172
}
173
}
174
function toMatchSnapshot(received, nameOrOptions = {}, optOptions = {}) {
175
const testInfo = (0, import_globals.currentTestInfo)();
176
if (!testInfo)
177
throw new Error(`toMatchSnapshot() must be called during the test`);
178
if (received instanceof Promise)
179
throw new Error("An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.");
180
if (testInfo._projectInternal.ignoreSnapshots)
181
return { pass: !this.isNot, message: () => "", name: "toMatchSnapshot", expected: nameOrOptions };
182
const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
183
const helper = new SnapshotHelper(
184
this,
185
testInfo,
186
"toMatchSnapshot",
187
void 0,
188
"." + determineFileExtension(received),
189
configOptions,
190
nameOrOptions,
191
optOptions
192
);
193
if (this.isNot) {
194
if (!import_fs.default.existsSync(helper.expectedPath))
195
return helper.handleMissingNegated();
196
const isDifferent = !!helper.comparator(received, import_fs.default.readFileSync(helper.expectedPath), helper.options);
197
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
198
}
199
if (!import_fs.default.existsSync(helper.expectedPath))
200
return helper.handleMissing(received, this._stepInfo);
201
const expected = import_fs.default.readFileSync(helper.expectedPath);
202
if (helper.updateSnapshots === "all") {
203
if (!(0, import_utils.compareBuffersOrStrings)(received, expected))
204
return helper.handleMatching();
205
writeFileSync(helper.expectedPath, received);
206
console.log(helper.expectedPath + " is not the same, writing actual.");
207
return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
208
}
209
if (helper.updateSnapshots === "changed") {
210
const result2 = helper.comparator(received, expected, helper.options);
211
if (!result2)
212
return helper.handleMatching();
213
writeFileSync(helper.expectedPath, received);
214
console.log(helper.expectedPath + " does not match, writing actual.");
215
return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
216
}
217
const result = helper.comparator(received, expected, helper.options);
218
if (!result)
219
return helper.handleMatching();
220
const header = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toMatchSnapshot", receiver: (0, import_utils.isString)(received) ? "string" : "Buffer", expectation: "expected" });
221
return helper.handleDifferent(received, expected, void 0, result.diff, header, result.errorMessage, void 0, this._stepInfo);
222
}
223
function toHaveScreenshotStepTitle(nameOrOptions = {}, optOptions = {}) {
224
let name;
225
if (typeof nameOrOptions === "object" && !Array.isArray(nameOrOptions))
226
name = nameOrOptions.name;
227
else
228
name = nameOrOptions;
229
return Array.isArray(name) ? name.join(import_path.default.sep) : name || "";
230
}
231
async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions = {}) {
232
const testInfo = (0, import_globals.currentTestInfo)();
233
if (!testInfo)
234
throw new Error(`toHaveScreenshot() must be called during the test`);
235
if (testInfo._projectInternal.ignoreSnapshots)
236
return { pass: !this.isNot, message: () => "", name: "toHaveScreenshot", expected: nameOrOptions };
237
(0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot");
238
const [page, locator] = pageOrLocator.constructor.name === "Page" ? [pageOrLocator, void 0] : [pageOrLocator.page(), pageOrLocator];
239
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
240
const helper = new SnapshotHelper(this, testInfo, "toHaveScreenshot", locator, void 0, configOptions, nameOrOptions, optOptions);
241
if (!helper.expectedPath.toLowerCase().endsWith(".png"))
242
throw new Error(`Screenshot name "${import_path.default.basename(helper.expectedPath)}" must have '.png' extension`);
243
(0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot");
244
const style = await loadScreenshotStyles(helper.options.stylePath);
245
const timeout = helper.options.timeout ?? this.timeout;
246
const expectScreenshotOptions = {
247
locator,
248
animations: helper.options.animations ?? "disabled",
249
caret: helper.options.caret ?? "hide",
250
clip: helper.options.clip,
251
fullPage: helper.options.fullPage,
252
mask: helper.options.mask,
253
maskColor: helper.options.maskColor,
254
omitBackground: helper.options.omitBackground,
255
scale: helper.options.scale ?? "css",
256
style,
257
isNot: !!this.isNot,
258
timeout,
259
comparator: helper.options.comparator,
260
maxDiffPixels: helper.options.maxDiffPixels,
261
maxDiffPixelRatio: helper.options.maxDiffPixelRatio,
262
threshold: helper.options.threshold
263
};
264
const hasSnapshot = import_fs.default.existsSync(helper.expectedPath);
265
if (this.isNot) {
266
if (!hasSnapshot)
267
return helper.handleMissingNegated();
268
expectScreenshotOptions.expected = await import_fs.default.promises.readFile(helper.expectedPath);
269
const isDifferent = !(await page._expectScreenshot(expectScreenshotOptions)).errorMessage;
270
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
271
}
272
if (helper.updateSnapshots === "none" && !hasSnapshot)
273
return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.expectedPath}.`, false);
274
if (!hasSnapshot) {
275
const { actual: actual2, previous: previous2, diff: diff2, errorMessage: errorMessage2, log: log2, timedOut: timedOut2 } = await page._expectScreenshot(expectScreenshotOptions);
276
if (errorMessage2) {
277
const header2 = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toHaveScreenshot", locator: locator?.toString(), expectation: "expected", timeout, timedOut: timedOut2 });
278
return helper.handleDifferent(actual2, void 0, previous2, diff2, header2, errorMessage2, log2, this._stepInfo);
279
}
280
return helper.handleMissing(actual2, this._stepInfo);
281
}
282
const expected = await import_fs.default.promises.readFile(helper.expectedPath);
283
expectScreenshotOptions.expected = helper.updateSnapshots === "all" ? void 0 : expected;
284
const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions);
285
const writeFiles = (actualBuffer) => {
286
writeFileSync(helper.expectedPath, actualBuffer);
287
writeFileSync(helper.actualPath, actualBuffer);
288
console.log(helper.expectedPath + " is re-generated, writing actual.");
289
return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
290
};
291
if (!errorMessage) {
292
if (helper.updateSnapshots === "all" && actual && (0, import_utils.compareBuffersOrStrings)(actual, expected)) {
293
console.log(helper.expectedPath + " is re-generated, writing actual.");
294
return writeFiles(actual);
295
}
296
return helper.handleMatching();
297
}
298
if (helper.updateSnapshots === "changed" || helper.updateSnapshots === "all") {
299
if (actual)
300
return writeFiles(actual);
301
let header2 = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toHaveScreenshot", locator: locator?.toString(), expectation: "expected", timeout, timedOut });
302
header2 += " Failed to re-generate expected.\n";
303
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header2, errorMessage, log, this._stepInfo);
304
}
305
const header = (0, import_utils.formatMatcherMessage)(this.utils, { promise: this.promise, isNot: this.isNot, matcherName: "toHaveScreenshot", locator: locator?.toString(), expectation: "expected", timeout, timedOut });
306
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo);
307
}
308
function writeFileSync(aPath, content) {
309
import_fs.default.mkdirSync(import_path.default.dirname(aPath), { recursive: true });
310
import_fs.default.writeFileSync(aPath, content);
311
}
312
function indent(lines, tab) {
313
return lines.replace(/^(?=.+$)/gm, tab);
314
}
315
function determineFileExtension(file) {
316
if (typeof file === "string")
317
return "txt";
318
if (compareMagicBytes(file, [137, 80, 78, 71, 13, 10, 26, 10]))
319
return "png";
320
if (compareMagicBytes(file, [255, 216, 255]))
321
return "jpg";
322
return "dat";
323
}
324
function compareMagicBytes(file, magicBytes) {
325
return Buffer.compare(Buffer.from(magicBytes), file.slice(0, magicBytes.length)) === 0;
326
}
327
async function loadScreenshotStyles(stylePath) {
328
if (!stylePath)
329
return;
330
const stylePaths = Array.isArray(stylePath) ? stylePath : [stylePath];
331
const styles = await Promise.all(stylePaths.map(async (stylePath2) => {
332
const text = await import_fs.default.promises.readFile(stylePath2, "utf8");
333
return text.trim();
334
}));
335
return styles.join("\n").trim() || void 0;
336
}
337
// Annotate the CommonJS export names for ESM import in node:
338
0 && (module.exports = {
339
toHaveScreenshot,
340
toHaveScreenshotStepTitle,
341
toMatchSnapshot
342
});
343

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button