Files
2025-08-28 19:35:28 -07:00

339 lines
16 KiB
JavaScript

"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
});