|
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
|
|