comparadise-utils 1.43.2 → 1.43.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ import { t as MatchScreenshotArgs } from "./match-screenshot-Crt-i9Rf.mjs";
2
+
3
+ //#region create-base-image.d.ts
4
+ declare function createBaseImage(subject: Cypress.JQueryWithSelector | Window | Document | void, args?: MatchScreenshotArgs): void;
5
+ interface ExtendedCurrentRunnable extends Mocha.Runnable {
6
+ currentRunnable?: {
7
+ order?: unknown;
8
+ };
9
+ }
10
+ declare global {
11
+ namespace Cypress {
12
+ interface Cypress {
13
+ mocha: {
14
+ getRunner: () => ExtendedCurrentRunnable;
15
+ };
16
+ }
17
+ }
18
+ }
19
+ //#endregion
20
+ export { createBaseImage };
@@ -0,0 +1,18 @@
1
+ import { forceFont, getTestFolderPathFromScripts, verifyImages } from "./match-screenshot.mjs";
2
+ //#region create-base-image.ts
3
+ function createBaseImage(subject, args) {
4
+ const { rawName, options } = args || {};
5
+ forceFont();
6
+ verifyImages();
7
+ const { name, screenshotsFolder } = getTestFolderPathFromScripts(rawName);
8
+ (subject ? cy.wrap(subject) : cy).screenshot(`___${screenshotsFolder}/base`, options);
9
+ cy.task("log", `✅ A new base image was created for ${name}.`);
10
+ }
11
+ Cypress.Commands.add("createBaseImage", { prevSubject: [
12
+ "optional",
13
+ "element",
14
+ "window",
15
+ "document"
16
+ ] }, createBaseImage);
17
+ //#endregion
18
+ export { createBaseImage };
@@ -0,0 +1,14 @@
1
+ import { t as MatchScreenshotArgs } from "./match-screenshot-Crt-i9Rf.mjs";
2
+
3
+ //#region index.d.ts
4
+ declare function setupVisualTests(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.PluginConfigOptions;
5
+ declare global {
6
+ namespace Cypress {
7
+ interface Chainable {
8
+ matchScreenshot(args?: MatchScreenshotArgs): Chainable;
9
+ createBaseImage(args?: MatchScreenshotArgs): Chainable;
10
+ }
11
+ }
12
+ }
13
+ //#endregion
14
+ export { setupVisualTests };
package/dist/index.mjs ADDED
@@ -0,0 +1,171 @@
1
+ import * as fs from "fs";
2
+ import { PNG } from "pngjs";
3
+ import pixelmatch from "pixelmatch";
4
+ //#region images.ts
5
+ const PIXELMATCH_OPTIONS = {
6
+ alpha: .3,
7
+ threshold: .5,
8
+ includeAA: false
9
+ };
10
+ /**
11
+ * Helper function to create reusable image resizer
12
+ */
13
+ const createImageResizer = (width, height) => (source) => {
14
+ const resized = new PNG({
15
+ width,
16
+ height,
17
+ fill: true
18
+ });
19
+ PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
20
+ return resized;
21
+ };
22
+ /**
23
+ * Fills new area added after resize with transparent black color.
24
+ * I like idea of checker board pattern, but it seems to be too complicated
25
+ * to implement considering how low-level pngjs API is.
26
+ */
27
+ const fillSizeDifference = (width, height) => (image) => {
28
+ const inArea = (x, y) => y > height || x > width;
29
+ for (let y = 0; y < image.height; y++) for (let x = 0; x < image.width; x++) if (inArea(x, y)) {
30
+ const idx = image.width * y + x << 2;
31
+ image.data[idx] = 0;
32
+ image.data[idx + 1] = 0;
33
+ image.data[idx + 2] = 0;
34
+ image.data[idx + 3] = 64;
35
+ }
36
+ return image;
37
+ };
38
+ /**
39
+ * Aligns images sizes to biggest common value
40
+ * and fills new pixels with transparent pixels
41
+ */
42
+ function alignImagesToSameSize(firstImage, secondImage) {
43
+ const firstImageWidth = firstImage.width;
44
+ const firstImageHeight = firstImage.height;
45
+ const secondImageWidth = secondImage.width;
46
+ const secondImageHeight = secondImage.height;
47
+ const resizeToSameSize = createImageResizer(Math.max(firstImageWidth, secondImageWidth), Math.max(firstImageHeight, secondImageHeight));
48
+ const resizedFirst = resizeToSameSize(firstImage);
49
+ const resizedSecond = resizeToSameSize(secondImage);
50
+ return [fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst), fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond)];
51
+ }
52
+ /**
53
+ * Compares a base and new image and returns the pixel difference
54
+ * and diff PNG for writing the diff image to.
55
+ * @param {string} basePath - Full file path to base image
56
+ * @param {string} actualPath - Full file path to new image
57
+ * @param pixelMatchOptions - (Optional) options to calculate pixel differences between two images
58
+ */
59
+ function getDiffPixels(basePath, actualPath, pixelMatchOptions = PIXELMATCH_OPTIONS) {
60
+ const rawBase = PNG.sync.read(fs.readFileSync(basePath));
61
+ const rawActual = PNG.sync.read(fs.readFileSync(actualPath));
62
+ const [base, actual] = rawBase.height !== rawActual.height || rawBase.width !== rawActual.width ? alignImagesToSameSize(rawBase, rawActual) : [rawBase, rawActual];
63
+ if (!base || !actual) throw new Error();
64
+ const diff = new PNG({
65
+ width: base.width,
66
+ height: base.height
67
+ });
68
+ return {
69
+ diffPixels: pixelmatch(actual.data, base.data, diff.data, diff.width, diff.height, pixelMatchOptions),
70
+ diff
71
+ };
72
+ }
73
+ //#endregion
74
+ //#region files.ts
75
+ /**
76
+ * Creates a file path that is relative to the root
77
+ * @param {string} type - Value to append to fileName
78
+ * @param {string} path - (optional) FileName which corresponds to a module Id
79
+ * returns a string of just the prefix or the fileName prepended by the prefix
80
+ */
81
+ function createImageFileName(path, type) {
82
+ let newPath = path;
83
+ if (path.startsWith("/")) newPath = `.${path}`;
84
+ return `${newPath}/${type}.png`;
85
+ }
86
+ //#endregion
87
+ //#region screenshots.ts
88
+ /**
89
+ * Checks if a base image exists
90
+ * @param path - Folder path where you can find the base.png image
91
+ * @returns true if path/base.png exists, false if not.
92
+ */
93
+ function baseExists(path) {
94
+ const fileName = createImageFileName(path, "base");
95
+ console.log("Checking if base image exists at:", fileName);
96
+ const exists = fs.existsSync(fileName);
97
+ if (!exists) console.log("Base image does not exist. This means a new one will be created. If your base should exist, something went wrong.");
98
+ return exists;
99
+ }
100
+ /**
101
+ * Runs a visual regression test.
102
+ * @param args -
103
+ * Contains Full screenshots folder where the base/new/diff images will be compared and written to
104
+ * Optionally contains specified Pixelmatch config settings.
105
+ */
106
+ function compareScreenshots(args) {
107
+ const { screenshotsFolder, pixelMatchOptions } = args;
108
+ const basePath = createImageFileName(screenshotsFolder, "base");
109
+ const actualPath = createImageFileName(screenshotsFolder, "new");
110
+ const { diffPixels, diff } = getDiffPixels(basePath, actualPath, pixelMatchOptions);
111
+ if (diffPixels) fs.writeFile(createImageFileName(screenshotsFolder, "diff"), new Uint8Array(PNG.sync.write(diff)), (err) => {
112
+ if (err) console.error("❌ Diff exists but unable to create diff.png", err);
113
+ });
114
+ else fs.unlink(actualPath, (err) => {
115
+ if (err) console.error("❌ No diff but unable to delete actualPath}", err);
116
+ });
117
+ return diffPixels;
118
+ }
119
+ const trimPostfix = (path) => {
120
+ const imgExt = ".png";
121
+ const attemptPostfixRegex = new RegExp(`\\s\\(attempt \\d+\\)${imgExt}$`);
122
+ if (attemptPostfixRegex.test(path)) return path.replace(attemptPostfixRegex, imgExt);
123
+ return path;
124
+ };
125
+ const getNewPath = (path) => {
126
+ let newPath;
127
+ if (path.includes("___")) {
128
+ newPath = path.slice(path.lastIndexOf("___") + 3);
129
+ if (newPath.startsWith("/")) newPath = `.${newPath}`;
130
+ console.log(newPath);
131
+ } else newPath = path;
132
+ return trimPostfix(newPath);
133
+ };
134
+ /**
135
+ * Renames all root cypress screenshots to where the test was actually run.
136
+ * Should NOT be used standalone. Works with the matchScreenshot task.
137
+ * @param {Cypress.ScreenshotDetails} details
138
+ */
139
+ function onAfterScreenshot(details) {
140
+ console.log("🧸 Screenshot was saved to:", details.path);
141
+ if (!details.path.match("cypress")) return Promise.resolve({});
142
+ const newPath = getNewPath(details.path);
143
+ const newPathDir = newPath.substring(0, newPath.lastIndexOf("/"));
144
+ try {
145
+ fs.mkdirSync(newPathDir, { recursive: true });
146
+ } catch (err) {
147
+ console.error("❌ Error creating new screenshot folder:", newPathDir, err);
148
+ }
149
+ return new Promise((resolve, reject) => {
150
+ fs.rename(details.path, newPath, (err) => {
151
+ if (err) reject(err);
152
+ resolve({ path: newPath });
153
+ });
154
+ });
155
+ }
156
+ //#endregion
157
+ //#region index.ts
158
+ function setupVisualTests(on, config) {
159
+ on("after:screenshot", onAfterScreenshot);
160
+ on("task", {
161
+ baseExists,
162
+ compareScreenshots,
163
+ log: (message) => {
164
+ console.log(message);
165
+ return null;
166
+ }
167
+ });
168
+ return config;
169
+ }
170
+ //#endregion
171
+ export { setupVisualTests };
@@ -0,0 +1,37 @@
1
+ import { PNG } from "pngjs";
2
+ import pixelmatch from "pixelmatch";
3
+
4
+ //#region images.d.ts
5
+ type PixelMatchOptions = Parameters<typeof pixelmatch>[5];
6
+ //#endregion
7
+ //#region match-screenshot.d.ts
8
+ declare const PREFIX_DIFFERENTIATOR = "___";
9
+ declare function forceFont(): false | HTMLStyleElement;
10
+ declare function getTestFolderPathFromScripts(rawName?: string): {
11
+ name: string;
12
+ screenshotsFolder: string;
13
+ };
14
+ declare function verifyImages(): void;
15
+ type MatchScreenshotArgs = {
16
+ rawName?: string;
17
+ options?: Partial<Cypress.ScreenshotOptions> & {
18
+ pixelMatchOptions?: PixelMatchOptions;
19
+ };
20
+ };
21
+ declare function matchScreenshot(subject: Cypress.JQueryWithSelector | Window | Document | void, args?: MatchScreenshotArgs): void;
22
+ interface ExtendedCurrentRunnable extends Mocha.Runnable {
23
+ currentRunnable?: {
24
+ order?: unknown;
25
+ };
26
+ }
27
+ declare global {
28
+ namespace Cypress {
29
+ interface Cypress {
30
+ mocha: {
31
+ getRunner: () => ExtendedCurrentRunnable;
32
+ };
33
+ }
34
+ }
35
+ }
36
+ //#endregion
37
+ export { matchScreenshot as a, getTestFolderPathFromScripts as i, PREFIX_DIFFERENTIATOR as n, verifyImages as o, forceFont as r, MatchScreenshotArgs as t };
@@ -0,0 +1,2 @@
1
+ import { a as matchScreenshot, i as getTestFolderPathFromScripts, n as PREFIX_DIFFERENTIATOR, o as verifyImages, r as forceFont, t as MatchScreenshotArgs } from "./match-screenshot-Crt-i9Rf.mjs";
2
+ export { MatchScreenshotArgs, PREFIX_DIFFERENTIATOR, forceFont, getTestFolderPathFromScripts, matchScreenshot, verifyImages };
@@ -0,0 +1,66 @@
1
+ //#region match-screenshot.ts
2
+ const PREFIX_DIFFERENTIATOR = "___";
3
+ const SUFFIX_TEST_IDENTIFIER = ".spec.ts";
4
+ const SCREENSHOTS_FOLDER_NAME = "screenshots";
5
+ function forceFont() {
6
+ const iframe = window.parent.document.querySelector("iframe");
7
+ const contentDocument = iframe && iframe.contentDocument;
8
+ if (contentDocument) {
9
+ const style = contentDocument.createElement("style");
10
+ style.type = "text/css";
11
+ style.appendChild(contentDocument.createTextNode("* { font-family: Arial !important; }"));
12
+ contentDocument.head.appendChild(style);
13
+ return style;
14
+ }
15
+ return false;
16
+ }
17
+ function getTestFolderPathFromScripts(rawName) {
18
+ const relativeTestPath = Cypress.spec.relative;
19
+ if (!relativeTestPath) throw new Error("❌ Could not find matching script in the Cypress DOM to infer the test folder path");
20
+ const currentTestNumber = Cypress.mocha.getRunner().currentRunnable?.order;
21
+ if (!rawName && typeof currentTestNumber === "number" && currentTestNumber > 1) throw new Error("❌ The rawName argument was not provided to matchScreenshot and is required for test files containing multiple tests!");
22
+ const testName = relativeTestPath.substring(relativeTestPath.lastIndexOf("/") + 1, relativeTestPath.lastIndexOf(SUFFIX_TEST_IDENTIFIER));
23
+ const name = rawName || testName;
24
+ return {
25
+ name,
26
+ screenshotsFolder: `${SCREENSHOTS_FOLDER_NAME}/${relativeTestPath.substring(0, relativeTestPath.lastIndexOf(testName))}${name}`
27
+ };
28
+ }
29
+ function verifyImages() {
30
+ if (Cypress.$("img:visible").length > 0) cy.document().its("body").find("img").filter(":visible").then((images) => {
31
+ if (images) cy.wrap(images).each(($img) => {
32
+ cy.wrap($img).should("exist").and("have.prop", "naturalWidth");
33
+ });
34
+ });
35
+ }
36
+ function matchScreenshot(subject, args) {
37
+ const { rawName, options } = args || {};
38
+ forceFont();
39
+ verifyImages();
40
+ const { name, screenshotsFolder } = getTestFolderPathFromScripts(rawName);
41
+ cy.task("baseExists", screenshotsFolder).then((hasBase) => {
42
+ (subject ? cy.wrap(subject) : cy).screenshot(`___${screenshotsFolder}/new`, options);
43
+ if (!hasBase) {
44
+ cy.task("log", `✅ A new base image was created for ${name}. Create this as a new base image via Comparadise!`);
45
+ return null;
46
+ }
47
+ const compareScreenshotsArg = {
48
+ screenshotsFolder,
49
+ pixelMatchOptions: options?.pixelMatchOptions
50
+ };
51
+ cy.task("compareScreenshots", compareScreenshotsArg).then((diffPixels) => {
52
+ if (diffPixels === 0) cy.log(`✅ Actual image of ${name} was the same as base`);
53
+ else throw new Error(`❌ Actual image of ${name} differed by ${diffPixels} pixels.`);
54
+ return null;
55
+ });
56
+ return null;
57
+ });
58
+ }
59
+ Cypress.Commands.add("matchScreenshot", { prevSubject: [
60
+ "optional",
61
+ "element",
62
+ "window",
63
+ "document"
64
+ ] }, matchScreenshot);
65
+ //#endregion
66
+ export { PREFIX_DIFFERENTIATOR, forceFont, getTestFolderPathFromScripts, matchScreenshot, verifyImages };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "comparadise-utils",
3
- "main": "dist/index.js",
4
- "types": "dist/index.d.ts",
3
+ "main": "dist/index.mjs",
4
+ "types": "dist/index.d.mts",
5
5
  "license": "Apache-2.0",
6
6
  "files": [
7
7
  "dist",
@@ -21,15 +21,14 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/pngjs": "6.0.5",
24
- "cypress": "15.12.0",
25
- "tsup": "8.5.1"
24
+ "tsdown": "0.21.10"
26
25
  },
27
26
  "peerDependencies": {
28
27
  "cypress": ">=12"
29
28
  },
30
- "version": "1.43.2",
29
+ "version": "1.43.3",
31
30
  "scripts": {
32
- "build": "tsup",
31
+ "build": "tsdown index.ts match-screenshot.ts create-base-image.ts --deps.neverBundle shared",
33
32
  "postbuild": "echo \"require('./dist/match-screenshot');\nrequire('./dist/create-base-image');\" > commands.js",
34
33
  "test": "bun test"
35
34
  }
@@ -1,20 +0,0 @@
1
- import { MatchScreenshotArgs } from './match-screenshot.js';
2
- import 'pixelmatch';
3
-
4
- declare function createBaseImage(subject: Cypress.JQueryWithSelector | Window | Document | void, args?: MatchScreenshotArgs): void;
5
- interface ExtendedCurrentRunnable extends Mocha.Runnable {
6
- currentRunnable?: {
7
- order?: unknown;
8
- };
9
- }
10
- declare global {
11
- namespace Cypress {
12
- interface Cypress {
13
- mocha: {
14
- getRunner: () => ExtendedCurrentRunnable;
15
- };
16
- }
17
- }
18
- }
19
-
20
- export { createBaseImage };
@@ -1,146 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // create-base-image.ts
21
- var create_base_image_exports = {};
22
- __export(create_base_image_exports, {
23
- createBaseImage: () => createBaseImage
24
- });
25
- module.exports = __toCommonJS(create_base_image_exports);
26
-
27
- // match-screenshot.ts
28
- var PREFIX_DIFFERENTIATOR = "___";
29
- var SUFFIX_TEST_IDENTIFIER = ".spec.ts";
30
- var SCREENSHOTS_FOLDER_NAME = "screenshots";
31
- function forceFont() {
32
- const iframe = window.parent.document.querySelector("iframe");
33
- const contentDocument = iframe && iframe.contentDocument;
34
- if (contentDocument) {
35
- const style = contentDocument.createElement("style");
36
- style.type = "text/css";
37
- style.appendChild(
38
- contentDocument.createTextNode("* { font-family: Arial !important; }")
39
- );
40
- contentDocument.head.appendChild(style);
41
- return style;
42
- }
43
- return false;
44
- }
45
- function getTestFolderPathFromScripts(rawName) {
46
- const relativeTestPath = Cypress.spec.relative;
47
- if (!relativeTestPath) {
48
- throw new Error(
49
- "\u274C Could not find matching script in the Cypress DOM to infer the test folder path"
50
- );
51
- }
52
- const currentTestNumber = Cypress.mocha.getRunner().currentRunnable?.order;
53
- if (!rawName && typeof currentTestNumber === "number" && currentTestNumber > 1) {
54
- throw new Error(
55
- "\u274C The rawName argument was not provided to matchScreenshot and is required for test files containing multiple tests!"
56
- );
57
- }
58
- const testName = relativeTestPath.substring(
59
- relativeTestPath.lastIndexOf("/") + 1,
60
- relativeTestPath.lastIndexOf(SUFFIX_TEST_IDENTIFIER)
61
- );
62
- const name = rawName || testName;
63
- const screenshotsFolder = `${SCREENSHOTS_FOLDER_NAME}/${relativeTestPath.substring(
64
- 0,
65
- relativeTestPath.lastIndexOf(testName)
66
- )}${name}`;
67
- return {
68
- name,
69
- screenshotsFolder
70
- };
71
- }
72
- function verifyImages() {
73
- if (Cypress.$("img:visible").length > 0) {
74
- cy.document().its("body").find("img").filter(":visible").then((images) => {
75
- if (images) {
76
- cy.wrap(images).each(($img) => {
77
- cy.wrap($img).should("exist").and("have.prop", "naturalWidth");
78
- });
79
- }
80
- });
81
- }
82
- }
83
- function matchScreenshot(subject, args) {
84
- const { rawName, options } = args || {};
85
- forceFont();
86
- verifyImages();
87
- const { name, screenshotsFolder } = getTestFolderPathFromScripts(rawName);
88
- cy.task("baseExists", screenshotsFolder).then((hasBase) => {
89
- const target = subject ? cy.wrap(subject) : cy;
90
- target.screenshot(
91
- `${PREFIX_DIFFERENTIATOR}${screenshotsFolder}/new`,
92
- options
93
- );
94
- if (!hasBase) {
95
- cy.task(
96
- "log",
97
- `\u2705 A new base image was created for ${name}. Create this as a new base image via Comparadise!`
98
- );
99
- return null;
100
- }
101
- const pixelMatchOptions = options?.pixelMatchOptions;
102
- const compareScreenshotsArg = {
103
- screenshotsFolder,
104
- pixelMatchOptions
105
- };
106
- cy.task("compareScreenshots", compareScreenshotsArg).then((diffPixels) => {
107
- if (diffPixels === 0) {
108
- cy.log(`\u2705 Actual image of ${name} was the same as base`);
109
- } else {
110
- throw new Error(
111
- `\u274C Actual image of ${name} differed by ${diffPixels} pixels.`
112
- );
113
- }
114
- return null;
115
- });
116
- return null;
117
- });
118
- }
119
- Cypress.Commands.add(
120
- "matchScreenshot",
121
- { prevSubject: ["optional", "element", "window", "document"] },
122
- matchScreenshot
123
- );
124
-
125
- // create-base-image.ts
126
- function createBaseImage(subject, args) {
127
- const { rawName, options } = args || {};
128
- forceFont();
129
- verifyImages();
130
- const { name, screenshotsFolder } = getTestFolderPathFromScripts(rawName);
131
- const target = subject ? cy.wrap(subject) : cy;
132
- target.screenshot(
133
- `${PREFIX_DIFFERENTIATOR}${screenshotsFolder}/base`,
134
- options
135
- );
136
- cy.task("log", `\u2705 A new base image was created for ${name}.`);
137
- }
138
- Cypress.Commands.add(
139
- "createBaseImage",
140
- { prevSubject: ["optional", "element", "window", "document"] },
141
- createBaseImage
142
- );
143
- // Annotate the CommonJS export names for ESM import in node:
144
- 0 && (module.exports = {
145
- createBaseImage
146
- });
package/dist/index.d.ts DELETED
@@ -1,14 +0,0 @@
1
- import { MatchScreenshotArgs } from './match-screenshot.js';
2
- import 'pixelmatch';
3
-
4
- declare function setupVisualTests(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.PluginConfigOptions;
5
- declare global {
6
- namespace Cypress {
7
- interface Chainable {
8
- matchScreenshot(args?: MatchScreenshotArgs): Chainable;
9
- createBaseImage(args?: MatchScreenshotArgs): Chainable;
10
- }
11
- }
12
- }
13
-
14
- export { setupVisualTests };
package/dist/index.js DELETED
@@ -1,217 +0,0 @@
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
-
30
- // index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- setupVisualTests: () => setupVisualTests
34
- });
35
- module.exports = __toCommonJS(index_exports);
36
-
37
- // screenshots.ts
38
- var fs2 = __toESM(require("fs"));
39
- var import_pngjs2 = require("pngjs");
40
-
41
- // images.ts
42
- var fs = __toESM(require("fs"));
43
- var import_pngjs = require("pngjs");
44
- var import_pixelmatch = __toESM(require("pixelmatch"));
45
- var PIXELMATCH_OPTIONS = {
46
- alpha: 0.3,
47
- // defaults to 0.1
48
- threshold: 0.5,
49
- // defaults to 0.1
50
- includeAA: false
51
- // defaults to true
52
- };
53
- var createImageResizer = (width, height) => (source) => {
54
- const resized = new import_pngjs.PNG({ width, height, fill: true });
55
- import_pngjs.PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
56
- return resized;
57
- };
58
- var fillSizeDifference = (width, height) => (image) => {
59
- const inArea = (x, y) => y > height || x > width;
60
- for (let y = 0; y < image.height; y++) {
61
- for (let x = 0; x < image.width; x++) {
62
- if (inArea(x, y)) {
63
- const idx = image.width * y + x << 2;
64
- image.data[idx] = 0;
65
- image.data[idx + 1] = 0;
66
- image.data[idx + 2] = 0;
67
- image.data[idx + 3] = 64;
68
- }
69
- }
70
- }
71
- return image;
72
- };
73
- function alignImagesToSameSize(firstImage, secondImage) {
74
- const firstImageWidth = firstImage.width;
75
- const firstImageHeight = firstImage.height;
76
- const secondImageWidth = secondImage.width;
77
- const secondImageHeight = secondImage.height;
78
- const resizeToSameSize = createImageResizer(
79
- Math.max(firstImageWidth, secondImageWidth),
80
- Math.max(firstImageHeight, secondImageHeight)
81
- );
82
- const resizedFirst = resizeToSameSize(firstImage);
83
- const resizedSecond = resizeToSameSize(secondImage);
84
- return [
85
- fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst),
86
- fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond)
87
- ];
88
- }
89
- function getDiffPixels(basePath, actualPath, pixelMatchOptions = PIXELMATCH_OPTIONS) {
90
- const rawBase = import_pngjs.PNG.sync.read(fs.readFileSync(basePath));
91
- const rawActual = import_pngjs.PNG.sync.read(fs.readFileSync(actualPath));
92
- const hasSizeMismatch = rawBase.height !== rawActual.height || rawBase.width !== rawActual.width;
93
- const [base, actual] = hasSizeMismatch ? alignImagesToSameSize(rawBase, rawActual) : [rawBase, rawActual];
94
- if (!base || !actual) {
95
- throw new Error();
96
- }
97
- const diff = new import_pngjs.PNG({ width: base.width, height: base.height });
98
- const diffPixels = (0, import_pixelmatch.default)(
99
- actual.data,
100
- base.data,
101
- diff.data,
102
- diff.width,
103
- diff.height,
104
- pixelMatchOptions
105
- );
106
- return { diffPixels, diff };
107
- }
108
-
109
- // files.ts
110
- function createImageFileName(path, type) {
111
- let newPath = path;
112
- if (path.startsWith("/")) {
113
- newPath = `.${path}`;
114
- }
115
- return `${newPath}/${type}.png`;
116
- }
117
-
118
- // screenshots.ts
119
- function baseExists(path) {
120
- const fileName = createImageFileName(path, "base");
121
- console.log("Checking if base image exists at:", fileName);
122
- const exists = fs2.existsSync(fileName);
123
- if (!exists) {
124
- console.log(
125
- "Base image does not exist. This means a new one will be created. If your base should exist, something went wrong."
126
- );
127
- }
128
- return exists;
129
- }
130
- function compareScreenshots(args) {
131
- const { screenshotsFolder, pixelMatchOptions } = args;
132
- const basePath = createImageFileName(screenshotsFolder, "base");
133
- const actualPath = createImageFileName(screenshotsFolder, "new");
134
- const { diffPixels, diff } = getDiffPixels(
135
- basePath,
136
- actualPath,
137
- pixelMatchOptions
138
- );
139
- if (diffPixels) {
140
- fs2.writeFile(
141
- createImageFileName(screenshotsFolder, "diff"),
142
- new Uint8Array(import_pngjs2.PNG.sync.write(diff)),
143
- (err) => {
144
- if (err) {
145
- console.error("\u274C Diff exists but unable to create diff.png", err);
146
- }
147
- }
148
- );
149
- } else {
150
- fs2.unlink(actualPath, (err) => {
151
- if (err) {
152
- console.error("\u274C No diff but unable to delete actualPath}", err);
153
- }
154
- });
155
- }
156
- return diffPixels;
157
- }
158
- var trimPostfix = (path) => {
159
- const imgExt = ".png";
160
- const attemptPostfixRegex = new RegExp(`\\s\\(attempt \\d+\\)${imgExt}$`);
161
- if (attemptPostfixRegex.test(path)) {
162
- return path.replace(attemptPostfixRegex, imgExt);
163
- }
164
- return path;
165
- };
166
- var getNewPath = (path) => {
167
- let newPath;
168
- if (path.includes("___")) {
169
- newPath = path.slice(path.lastIndexOf("___") + 3);
170
- if (newPath.startsWith("/")) {
171
- newPath = `.${newPath}`;
172
- }
173
- console.log(newPath);
174
- } else {
175
- newPath = path;
176
- }
177
- return trimPostfix(newPath);
178
- };
179
- function onAfterScreenshot(details) {
180
- console.log("\u{1F9F8} Screenshot was saved to:", details.path);
181
- if (!details.path.match("cypress")) {
182
- return Promise.resolve({});
183
- }
184
- const newPath = getNewPath(details.path);
185
- const newPathDir = newPath.substring(0, newPath.lastIndexOf("/"));
186
- try {
187
- fs2.mkdirSync(newPathDir, { recursive: true });
188
- } catch (err) {
189
- console.error("\u274C Error creating new screenshot folder:", newPathDir, err);
190
- }
191
- return new Promise((resolve, reject) => {
192
- fs2.rename(details.path, newPath, (err) => {
193
- if (err) {
194
- reject(err);
195
- }
196
- resolve({ path: newPath });
197
- });
198
- });
199
- }
200
-
201
- // index.ts
202
- function setupVisualTests(on, config) {
203
- on("after:screenshot", onAfterScreenshot);
204
- on("task", {
205
- baseExists,
206
- compareScreenshots,
207
- log: (message) => {
208
- console.log(message);
209
- return null;
210
- }
211
- });
212
- return config;
213
- }
214
- // Annotate the CommonJS export names for ESM import in node:
215
- 0 && (module.exports = {
216
- setupVisualTests
217
- });
@@ -1,34 +0,0 @@
1
- import pixelmatch from 'pixelmatch';
2
-
3
- type PixelMatchOptions = Parameters<typeof pixelmatch>[5];
4
-
5
- declare const PREFIX_DIFFERENTIATOR = "___";
6
- declare function forceFont(): false | HTMLStyleElement;
7
- declare function getTestFolderPathFromScripts(rawName?: string): {
8
- name: string;
9
- screenshotsFolder: string;
10
- };
11
- declare function verifyImages(): void;
12
- type MatchScreenshotArgs = {
13
- rawName?: string;
14
- options?: Partial<Cypress.ScreenshotOptions> & {
15
- pixelMatchOptions?: PixelMatchOptions;
16
- };
17
- };
18
- declare function matchScreenshot(subject: Cypress.JQueryWithSelector | Window | Document | void, args?: MatchScreenshotArgs): void;
19
- interface ExtendedCurrentRunnable extends Mocha.Runnable {
20
- currentRunnable?: {
21
- order?: unknown;
22
- };
23
- }
24
- declare global {
25
- namespace Cypress {
26
- interface Cypress {
27
- mocha: {
28
- getRunner: () => ExtendedCurrentRunnable;
29
- };
30
- }
31
- }
32
- }
33
-
34
- export { type MatchScreenshotArgs, PREFIX_DIFFERENTIATOR, forceFont, getTestFolderPathFromScripts, matchScreenshot, verifyImages };
@@ -1,133 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // match-screenshot.ts
21
- var match_screenshot_exports = {};
22
- __export(match_screenshot_exports, {
23
- PREFIX_DIFFERENTIATOR: () => PREFIX_DIFFERENTIATOR,
24
- forceFont: () => forceFont,
25
- getTestFolderPathFromScripts: () => getTestFolderPathFromScripts,
26
- matchScreenshot: () => matchScreenshot,
27
- verifyImages: () => verifyImages
28
- });
29
- module.exports = __toCommonJS(match_screenshot_exports);
30
- var PREFIX_DIFFERENTIATOR = "___";
31
- var SUFFIX_TEST_IDENTIFIER = ".spec.ts";
32
- var SCREENSHOTS_FOLDER_NAME = "screenshots";
33
- function forceFont() {
34
- const iframe = window.parent.document.querySelector("iframe");
35
- const contentDocument = iframe && iframe.contentDocument;
36
- if (contentDocument) {
37
- const style = contentDocument.createElement("style");
38
- style.type = "text/css";
39
- style.appendChild(
40
- contentDocument.createTextNode("* { font-family: Arial !important; }")
41
- );
42
- contentDocument.head.appendChild(style);
43
- return style;
44
- }
45
- return false;
46
- }
47
- function getTestFolderPathFromScripts(rawName) {
48
- const relativeTestPath = Cypress.spec.relative;
49
- if (!relativeTestPath) {
50
- throw new Error(
51
- "\u274C Could not find matching script in the Cypress DOM to infer the test folder path"
52
- );
53
- }
54
- const currentTestNumber = Cypress.mocha.getRunner().currentRunnable?.order;
55
- if (!rawName && typeof currentTestNumber === "number" && currentTestNumber > 1) {
56
- throw new Error(
57
- "\u274C The rawName argument was not provided to matchScreenshot and is required for test files containing multiple tests!"
58
- );
59
- }
60
- const testName = relativeTestPath.substring(
61
- relativeTestPath.lastIndexOf("/") + 1,
62
- relativeTestPath.lastIndexOf(SUFFIX_TEST_IDENTIFIER)
63
- );
64
- const name = rawName || testName;
65
- const screenshotsFolder = `${SCREENSHOTS_FOLDER_NAME}/${relativeTestPath.substring(
66
- 0,
67
- relativeTestPath.lastIndexOf(testName)
68
- )}${name}`;
69
- return {
70
- name,
71
- screenshotsFolder
72
- };
73
- }
74
- function verifyImages() {
75
- if (Cypress.$("img:visible").length > 0) {
76
- cy.document().its("body").find("img").filter(":visible").then((images) => {
77
- if (images) {
78
- cy.wrap(images).each(($img) => {
79
- cy.wrap($img).should("exist").and("have.prop", "naturalWidth");
80
- });
81
- }
82
- });
83
- }
84
- }
85
- function matchScreenshot(subject, args) {
86
- const { rawName, options } = args || {};
87
- forceFont();
88
- verifyImages();
89
- const { name, screenshotsFolder } = getTestFolderPathFromScripts(rawName);
90
- cy.task("baseExists", screenshotsFolder).then((hasBase) => {
91
- const target = subject ? cy.wrap(subject) : cy;
92
- target.screenshot(
93
- `${PREFIX_DIFFERENTIATOR}${screenshotsFolder}/new`,
94
- options
95
- );
96
- if (!hasBase) {
97
- cy.task(
98
- "log",
99
- `\u2705 A new base image was created for ${name}. Create this as a new base image via Comparadise!`
100
- );
101
- return null;
102
- }
103
- const pixelMatchOptions = options?.pixelMatchOptions;
104
- const compareScreenshotsArg = {
105
- screenshotsFolder,
106
- pixelMatchOptions
107
- };
108
- cy.task("compareScreenshots", compareScreenshotsArg).then((diffPixels) => {
109
- if (diffPixels === 0) {
110
- cy.log(`\u2705 Actual image of ${name} was the same as base`);
111
- } else {
112
- throw new Error(
113
- `\u274C Actual image of ${name} differed by ${diffPixels} pixels.`
114
- );
115
- }
116
- return null;
117
- });
118
- return null;
119
- });
120
- }
121
- Cypress.Commands.add(
122
- "matchScreenshot",
123
- { prevSubject: ["optional", "element", "window", "document"] },
124
- matchScreenshot
125
- );
126
- // Annotate the CommonJS export names for ESM import in node:
127
- 0 && (module.exports = {
128
- PREFIX_DIFFERENTIATOR,
129
- forceFont,
130
- getTestFolderPathFromScripts,
131
- matchScreenshot,
132
- verifyImages
133
- });