toMatchSnapshot.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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_matcherHint = require("./matcherHint");
  43. var import_globals = require("../common/globals");
  44. const NonConfigProperties = [
  45. "clip",
  46. "fullPage",
  47. "mask",
  48. "maskColor",
  49. "omitBackground",
  50. "timeout"
  51. ];
  52. class SnapshotHelper {
  53. constructor(testInfo, matcherName, locator, anonymousSnapshotExtension, configOptions, nameOrOptions, optOptions) {
  54. let name;
  55. if (Array.isArray(nameOrOptions) || typeof nameOrOptions === "string") {
  56. name = nameOrOptions;
  57. this.options = { ...optOptions };
  58. } else {
  59. const { name: nameFromOptions, ...options } = nameOrOptions;
  60. this.options = options;
  61. name = nameFromOptions;
  62. }
  63. this.name = Array.isArray(name) ? name.join(import_path.default.sep) : name || "";
  64. const resolvedPaths = testInfo._resolveSnapshotPaths(matcherName === "toHaveScreenshot" ? "screenshot" : "snapshot", name, "updateSnapshotIndex", anonymousSnapshotExtension);
  65. this.expectedPath = resolvedPaths.absoluteSnapshotPath;
  66. this.attachmentBaseName = resolvedPaths.relativeOutputPath;
  67. const outputBasePath = testInfo._getOutputPath(resolvedPaths.relativeOutputPath);
  68. this.legacyExpectedPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-expected");
  69. this.previousPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-previous");
  70. this.actualPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-actual");
  71. this.diffPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-diff");
  72. const filteredConfigOptions = { ...configOptions };
  73. for (const prop of NonConfigProperties)
  74. delete filteredConfigOptions[prop];
  75. this.options = {
  76. ...filteredConfigOptions,
  77. ...this.options
  78. };
  79. if (this.options._comparator) {
  80. this.options.comparator = this.options._comparator;
  81. delete this.options._comparator;
  82. }
  83. if (this.options.maxDiffPixels !== void 0 && this.options.maxDiffPixels < 0)
  84. throw new Error("`maxDiffPixels` option value must be non-negative integer");
  85. if (this.options.maxDiffPixelRatio !== void 0 && (this.options.maxDiffPixelRatio < 0 || this.options.maxDiffPixelRatio > 1))
  86. throw new Error("`maxDiffPixelRatio` option value must be between 0 and 1");
  87. this.matcherName = matcherName;
  88. this.locator = locator;
  89. this.updateSnapshots = testInfo.config.updateSnapshots;
  90. this.mimeType = import_utilsBundle.mime.getType(import_path.default.basename(this.expectedPath)) ?? "application/octet-stream";
  91. this.comparator = (0, import_utils.getComparator)(this.mimeType);
  92. this.testInfo = testInfo;
  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_util.callLogText)(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. testInfo,
  185. "toMatchSnapshot",
  186. void 0,
  187. "." + determineFileExtension(received),
  188. configOptions,
  189. nameOrOptions,
  190. optOptions
  191. );
  192. if (this.isNot) {
  193. if (!import_fs.default.existsSync(helper.expectedPath))
  194. return helper.handleMissingNegated();
  195. const isDifferent = !!helper.comparator(received, import_fs.default.readFileSync(helper.expectedPath), helper.options);
  196. return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
  197. }
  198. if (!import_fs.default.existsSync(helper.expectedPath))
  199. return helper.handleMissing(received, this._stepInfo);
  200. const expected = import_fs.default.readFileSync(helper.expectedPath);
  201. if (helper.updateSnapshots === "all") {
  202. if (!(0, import_utils.compareBuffersOrStrings)(received, expected))
  203. return helper.handleMatching();
  204. writeFileSync(helper.expectedPath, received);
  205. console.log(helper.expectedPath + " is not the same, writing actual.");
  206. return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
  207. }
  208. if (helper.updateSnapshots === "changed") {
  209. const result2 = helper.comparator(received, expected, helper.options);
  210. if (!result2)
  211. return helper.handleMatching();
  212. writeFileSync(helper.expectedPath, received);
  213. console.log(helper.expectedPath + " does not match, writing actual.");
  214. return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
  215. }
  216. const result = helper.comparator(received, expected, helper.options);
  217. if (!result)
  218. return helper.handleMatching();
  219. const receiver = (0, import_utils.isString)(received) ? "string" : "Buffer";
  220. const header = (0, import_matcherHint.matcherHint)(this, void 0, "toMatchSnapshot", receiver, void 0, void 0, void 0);
  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(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. const receiver = locator ? "locator" : "page";
  275. if (!hasSnapshot) {
  276. const { actual: actual2, previous: previous2, diff: diff2, errorMessage: errorMessage2, log: log2, timedOut: timedOut2 } = await page._expectScreenshot(expectScreenshotOptions);
  277. if (errorMessage2) {
  278. const header2 = (0, import_matcherHint.matcherHint)(this, locator, "toHaveScreenshot", receiver, void 0, void 0, timedOut2 ? timeout : void 0);
  279. return helper.handleDifferent(actual2, void 0, previous2, diff2, header2, errorMessage2, log2, this._stepInfo);
  280. }
  281. return helper.handleMissing(actual2, this._stepInfo);
  282. }
  283. const expected = await import_fs.default.promises.readFile(helper.expectedPath);
  284. expectScreenshotOptions.expected = helper.updateSnapshots === "all" ? void 0 : expected;
  285. const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions);
  286. const writeFiles = () => {
  287. writeFileSync(helper.expectedPath, actual);
  288. writeFileSync(helper.actualPath, actual);
  289. console.log(helper.expectedPath + " is re-generated, writing actual.");
  290. return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
  291. };
  292. if (!errorMessage) {
  293. if (helper.updateSnapshots === "all" && actual && (0, import_utils.compareBuffersOrStrings)(actual, expected)) {
  294. console.log(helper.expectedPath + " is re-generated, writing actual.");
  295. return writeFiles();
  296. }
  297. return helper.handleMatching();
  298. }
  299. if (helper.updateSnapshots === "changed" || helper.updateSnapshots === "all")
  300. return writeFiles();
  301. const header = (0, import_matcherHint.matcherHint)(this, void 0, "toHaveScreenshot", receiver, void 0, void 0, timedOut ? timeout : void 0);
  302. return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo);
  303. }
  304. function writeFileSync(aPath, content) {
  305. import_fs.default.mkdirSync(import_path.default.dirname(aPath), { recursive: true });
  306. import_fs.default.writeFileSync(aPath, content);
  307. }
  308. function indent(lines, tab) {
  309. return lines.replace(/^(?=.+$)/gm, tab);
  310. }
  311. function determineFileExtension(file) {
  312. if (typeof file === "string")
  313. return "txt";
  314. if (compareMagicBytes(file, [137, 80, 78, 71, 13, 10, 26, 10]))
  315. return "png";
  316. if (compareMagicBytes(file, [255, 216, 255]))
  317. return "jpg";
  318. return "dat";
  319. }
  320. function compareMagicBytes(file, magicBytes) {
  321. return Buffer.compare(Buffer.from(magicBytes), file.slice(0, magicBytes.length)) === 0;
  322. }
  323. async function loadScreenshotStyles(stylePath) {
  324. if (!stylePath)
  325. return;
  326. const stylePaths = Array.isArray(stylePath) ? stylePath : [stylePath];
  327. const styles = await Promise.all(stylePaths.map(async (stylePath2) => {
  328. const text = await import_fs.default.promises.readFile(stylePath2, "utf8");
  329. return text.trim();
  330. }));
  331. return styles.join("\n").trim() || void 0;
  332. }
  333. // Annotate the CommonJS export names for ESM import in node:
  334. 0 && (module.exports = {
  335. toHaveScreenshot,
  336. toHaveScreenshotStepTitle,
  337. toMatchSnapshot
  338. });