"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var toMatchSnapshot_exports = {}; __export(toMatchSnapshot_exports, { toHaveScreenshot: () => toHaveScreenshot, toHaveScreenshotStepTitle: () => toHaveScreenshotStepTitle, toMatchSnapshot: () => toMatchSnapshot }); module.exports = __toCommonJS(toMatchSnapshot_exports); var import_fs = __toESM(require("fs")); var import_path = __toESM(require("path")); var import_utils = require("playwright-core/lib/utils"); var import_utils2 = require("playwright-core/lib/utils"); var import_utilsBundle = require("playwright-core/lib/utilsBundle"); var import_util = require("../util"); var import_matcherHint = require("./matcherHint"); var import_globals = require("../common/globals"); const NonConfigProperties = [ "clip", "fullPage", "mask", "maskColor", "omitBackground", "timeout" ]; class SnapshotHelper { constructor(testInfo, matcherName, locator, anonymousSnapshotExtension, configOptions, nameOrOptions, optOptions) { let name; if (Array.isArray(nameOrOptions) || typeof nameOrOptions === "string") { name = nameOrOptions; this.options = { ...optOptions }; } else { const { name: nameFromOptions, ...options } = nameOrOptions; this.options = options; name = nameFromOptions; } this.name = Array.isArray(name) ? name.join(import_path.default.sep) : name || ""; const resolvedPaths = testInfo._resolveSnapshotPaths(matcherName === "toHaveScreenshot" ? "screenshot" : "snapshot", name, "updateSnapshotIndex", anonymousSnapshotExtension); this.expectedPath = resolvedPaths.absoluteSnapshotPath; this.attachmentBaseName = resolvedPaths.relativeOutputPath; const outputBasePath = testInfo._getOutputPath(resolvedPaths.relativeOutputPath); this.legacyExpectedPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-expected"); this.previousPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-previous"); this.actualPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-actual"); this.diffPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-diff"); const filteredConfigOptions = { ...configOptions }; for (const prop of NonConfigProperties) delete filteredConfigOptions[prop]; this.options = { ...filteredConfigOptions, ...this.options }; if (this.options._comparator) { this.options.comparator = this.options._comparator; delete this.options._comparator; } if (this.options.maxDiffPixels !== void 0 && this.options.maxDiffPixels < 0) throw new Error("`maxDiffPixels` option value must be non-negative integer"); if (this.options.maxDiffPixelRatio !== void 0 && (this.options.maxDiffPixelRatio < 0 || this.options.maxDiffPixelRatio > 1)) throw new Error("`maxDiffPixelRatio` option value must be between 0 and 1"); this.matcherName = matcherName; this.locator = locator; this.updateSnapshots = testInfo.config.updateSnapshots; this.mimeType = import_utilsBundle.mime.getType(import_path.default.basename(this.expectedPath)) ?? "application/octet-stream"; this.comparator = (0, import_utils.getComparator)(this.mimeType); this.testInfo = testInfo; this.kind = this.mimeType.startsWith("image/") ? "Screenshot" : "Snapshot"; } createMatcherResult(message, pass, log) { const unfiltered = { name: this.matcherName, expected: this.expectedPath, actual: this.actualPath, diff: this.diffPath, pass, message: () => message, log }; return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== void 0)); } handleMissingNegated() { const isWriteMissingMode = this.updateSnapshots !== "none"; const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? `, matchers using ".not" won't write them automatically.` : "."}`; return this.createMatcherResult(message, true); } handleDifferentNegated() { return this.createMatcherResult("", false); } handleMatchingNegated() { const message = [ import_utils2.colors.red(`${this.kind} comparison failed:`), "", indent("Expected result should be different from the actual one.", " ") ].join("\n"); return this.createMatcherResult(message, true); } handleMissing(actual, step) { const isWriteMissingMode = this.updateSnapshots !== "none"; if (isWriteMissingMode) writeFileSync(this.expectedPath, actual); step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-expected"), contentType: this.mimeType, path: this.expectedPath }); writeFileSync(this.actualPath, actual); step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-actual"), contentType: this.mimeType, path: this.actualPath }); const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ", writing actual." : "."}`; if (this.updateSnapshots === "all" || this.updateSnapshots === "changed") { console.log(message); return this.createMatcherResult(message, true); } if (this.updateSnapshots === "missing") { this.testInfo._hasNonRetriableError = true; this.testInfo._failWithError(new Error(message)); return this.createMatcherResult("", true); } return this.createMatcherResult(message, false); } handleDifferent(actual, expected, previous, diff, header, diffError, log, step) { const output = [`${header}${indent(diffError, " ")}`]; if (this.name) { output.push(""); output.push(` Snapshot: ${this.name}`); } if (expected !== void 0) { writeFileSync(this.legacyExpectedPath, expected); step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-expected"), contentType: this.mimeType, path: this.expectedPath }); } if (previous !== void 0) { writeFileSync(this.previousPath, previous); step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-previous"), contentType: this.mimeType, path: this.previousPath }); } if (actual !== void 0) { writeFileSync(this.actualPath, actual); step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-actual"), contentType: this.mimeType, path: this.actualPath }); } if (diff !== void 0) { writeFileSync(this.diffPath, diff); step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-diff"), contentType: this.mimeType, path: this.diffPath }); } if (log?.length) output.push((0, import_util.callLogText)(log)); else output.push(""); return this.createMatcherResult(output.join("\n"), false, log); } handleMatching() { return this.createMatcherResult("", true); } } function toMatchSnapshot(received, nameOrOptions = {}, optOptions = {}) { const testInfo = (0, import_globals.currentTestInfo)(); if (!testInfo) throw new Error(`toMatchSnapshot() must be called during the test`); if (received instanceof Promise) throw new Error("An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it."); if (testInfo._projectInternal.ignoreSnapshots) return { pass: !this.isNot, message: () => "", name: "toMatchSnapshot", expected: nameOrOptions }; const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {}; const helper = new SnapshotHelper( testInfo, "toMatchSnapshot", void 0, "." + determineFileExtension(received), configOptions, nameOrOptions, optOptions ); if (this.isNot) { if (!import_fs.default.existsSync(helper.expectedPath)) return helper.handleMissingNegated(); const isDifferent = !!helper.comparator(received, import_fs.default.readFileSync(helper.expectedPath), helper.options); return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated(); } if (!import_fs.default.existsSync(helper.expectedPath)) return helper.handleMissing(received, this._stepInfo); const expected = import_fs.default.readFileSync(helper.expectedPath); if (helper.updateSnapshots === "all") { if (!(0, import_utils.compareBuffersOrStrings)(received, expected)) return helper.handleMatching(); writeFileSync(helper.expectedPath, received); console.log(helper.expectedPath + " is not the same, writing actual."); return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true); } if (helper.updateSnapshots === "changed") { const result2 = helper.comparator(received, expected, helper.options); if (!result2) return helper.handleMatching(); writeFileSync(helper.expectedPath, received); console.log(helper.expectedPath + " does not match, writing actual."); return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true); } const result = helper.comparator(received, expected, helper.options); if (!result) return helper.handleMatching(); const receiver = (0, import_utils.isString)(received) ? "string" : "Buffer"; const header = (0, import_matcherHint.matcherHint)(this, void 0, "toMatchSnapshot", receiver, void 0, void 0, void 0); return helper.handleDifferent(received, expected, void 0, result.diff, header, result.errorMessage, void 0, this._stepInfo); } function toHaveScreenshotStepTitle(nameOrOptions = {}, optOptions = {}) { let name; if (typeof nameOrOptions === "object" && !Array.isArray(nameOrOptions)) name = nameOrOptions.name; else name = nameOrOptions; return Array.isArray(name) ? name.join(import_path.default.sep) : name || ""; } async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions = {}) { const testInfo = (0, import_globals.currentTestInfo)(); if (!testInfo) throw new Error(`toHaveScreenshot() must be called during the test`); if (testInfo._projectInternal.ignoreSnapshots) return { pass: !this.isNot, message: () => "", name: "toHaveScreenshot", expected: nameOrOptions }; (0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot"); const [page, locator] = pageOrLocator.constructor.name === "Page" ? [pageOrLocator, void 0] : [pageOrLocator.page(), pageOrLocator]; const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {}; const helper = new SnapshotHelper(testInfo, "toHaveScreenshot", locator, void 0, configOptions, nameOrOptions, optOptions); if (!helper.expectedPath.toLowerCase().endsWith(".png")) throw new Error(`Screenshot name "${import_path.default.basename(helper.expectedPath)}" must have '.png' extension`); (0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot"); const style = await loadScreenshotStyles(helper.options.stylePath); const timeout = helper.options.timeout ?? this.timeout; const expectScreenshotOptions = { locator, animations: helper.options.animations ?? "disabled", caret: helper.options.caret ?? "hide", clip: helper.options.clip, fullPage: helper.options.fullPage, mask: helper.options.mask, maskColor: helper.options.maskColor, omitBackground: helper.options.omitBackground, scale: helper.options.scale ?? "css", style, isNot: !!this.isNot, timeout, comparator: helper.options.comparator, maxDiffPixels: helper.options.maxDiffPixels, maxDiffPixelRatio: helper.options.maxDiffPixelRatio, threshold: helper.options.threshold }; const hasSnapshot = import_fs.default.existsSync(helper.expectedPath); if (this.isNot) { if (!hasSnapshot) return helper.handleMissingNegated(); expectScreenshotOptions.expected = await import_fs.default.promises.readFile(helper.expectedPath); const isDifferent = !(await page._expectScreenshot(expectScreenshotOptions)).errorMessage; return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated(); } if (helper.updateSnapshots === "none" && !hasSnapshot) return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.expectedPath}.`, false); const receiver = locator ? "locator" : "page"; if (!hasSnapshot) { const { actual: actual2, previous: previous2, diff: diff2, errorMessage: errorMessage2, log: log2, timedOut: timedOut2 } = await page._expectScreenshot(expectScreenshotOptions); if (errorMessage2) { const header2 = (0, import_matcherHint.matcherHint)(this, locator, "toHaveScreenshot", receiver, void 0, void 0, timedOut2 ? timeout : void 0); return helper.handleDifferent(actual2, void 0, previous2, diff2, header2, errorMessage2, log2, this._stepInfo); } return helper.handleMissing(actual2, this._stepInfo); } const expected = await import_fs.default.promises.readFile(helper.expectedPath); expectScreenshotOptions.expected = helper.updateSnapshots === "all" ? void 0 : expected; const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions); const writeFiles = () => { writeFileSync(helper.expectedPath, actual); writeFileSync(helper.actualPath, actual); console.log(helper.expectedPath + " is re-generated, writing actual."); return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true); }; if (!errorMessage) { if (helper.updateSnapshots === "all" && actual && (0, import_utils.compareBuffersOrStrings)(actual, expected)) { console.log(helper.expectedPath + " is re-generated, writing actual."); return writeFiles(); } return helper.handleMatching(); } if (helper.updateSnapshots === "changed" || helper.updateSnapshots === "all") return writeFiles(); const header = (0, import_matcherHint.matcherHint)(this, void 0, "toHaveScreenshot", receiver, void 0, void 0, timedOut ? timeout : void 0); return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo); } function writeFileSync(aPath, content) { import_fs.default.mkdirSync(import_path.default.dirname(aPath), { recursive: true }); import_fs.default.writeFileSync(aPath, content); } function indent(lines, tab) { return lines.replace(/^(?=.+$)/gm, tab); } function determineFileExtension(file) { if (typeof file === "string") return "txt"; if (compareMagicBytes(file, [137, 80, 78, 71, 13, 10, 26, 10])) return "png"; if (compareMagicBytes(file, [255, 216, 255])) return "jpg"; return "dat"; } function compareMagicBytes(file, magicBytes) { return Buffer.compare(Buffer.from(magicBytes), file.slice(0, magicBytes.length)) === 0; } async function loadScreenshotStyles(stylePath) { if (!stylePath) return; const stylePaths = Array.isArray(stylePath) ? stylePath : [stylePath]; const styles = await Promise.all(stylePaths.map(async (stylePath2) => { const text = await import_fs.default.promises.readFile(stylePath2, "utf8"); return text.trim(); })); return styles.join("\n").trim() || void 0; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { toHaveScreenshot, toHaveScreenshotStepTitle, toMatchSnapshot });