comparadise-utils 1.15.1 → 1.16.0
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.
- package/dist/index.d.ts +12 -0
- package/dist/index.js +199 -13
- package/{types → dist}/match-screenshot.d.ts +4 -5
- package/dist/match-screenshot.js +109 -70
- package/package.json +6 -6
- package/dist/files.js +0 -17
- package/dist/images.js +0 -102
- package/dist/screenshots.js +0 -118
- package/types/files.d.ts +0 -7
- package/types/images.d.ts +0 -11
- package/types/index.d.ts +0 -10
- package/types/index.js +0 -1
- package/types/screenshots.d.ts +0 -19
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MatchScreenshotArgs } from './match-screenshot.js';
|
|
2
|
+
|
|
3
|
+
declare function setupVisualTests(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.PluginConfigOptions;
|
|
4
|
+
declare global {
|
|
5
|
+
namespace Cypress {
|
|
6
|
+
interface Chainable {
|
|
7
|
+
matchScreenshot(args?: MatchScreenshotArgs): Chainable;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { setupVisualTests };
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,203 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 comparadise_utils_exports = {};
|
|
32
|
+
__export(comparadise_utils_exports, {
|
|
33
|
+
setupVisualTests: () => setupVisualTests
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(comparadise_utils_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
|
+
threshold: 0.3
|
|
47
|
+
};
|
|
48
|
+
var createImageResizer = (width, height) => (source) => {
|
|
49
|
+
const resized = new import_pngjs.PNG({ width, height, fill: true });
|
|
50
|
+
import_pngjs.PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
|
|
51
|
+
return resized;
|
|
52
|
+
};
|
|
53
|
+
var fillSizeDifference = (width, height) => (image) => {
|
|
54
|
+
const inArea = (x, y) => y > height || x > width;
|
|
55
|
+
for (let y = 0; y < image.height; y++) {
|
|
56
|
+
for (let x = 0; x < image.width; x++) {
|
|
57
|
+
if (inArea(x, y)) {
|
|
58
|
+
const idx = image.width * y + x << 2;
|
|
59
|
+
image.data[idx] = 0;
|
|
60
|
+
image.data[idx + 1] = 0;
|
|
61
|
+
image.data[idx + 2] = 0;
|
|
62
|
+
image.data[idx + 3] = 64;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return image;
|
|
67
|
+
};
|
|
68
|
+
function alignImagesToSameSize(firstImage, secondImage) {
|
|
69
|
+
const firstImageWidth = firstImage.width;
|
|
70
|
+
const firstImageHeight = firstImage.height;
|
|
71
|
+
const secondImageWidth = secondImage.width;
|
|
72
|
+
const secondImageHeight = secondImage.height;
|
|
73
|
+
const resizeToSameSize = createImageResizer(
|
|
74
|
+
Math.max(firstImageWidth, secondImageWidth),
|
|
75
|
+
Math.max(firstImageHeight, secondImageHeight)
|
|
76
|
+
);
|
|
77
|
+
const resizedFirst = resizeToSameSize(firstImage);
|
|
78
|
+
const resizedSecond = resizeToSameSize(secondImage);
|
|
79
|
+
return [
|
|
80
|
+
fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst),
|
|
81
|
+
fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond)
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
function getDiffPixels(basePath, actualPath) {
|
|
85
|
+
const rawBase = import_pngjs.PNG.sync.read(fs.readFileSync(basePath));
|
|
86
|
+
const rawActual = import_pngjs.PNG.sync.read(fs.readFileSync(actualPath));
|
|
87
|
+
const hasSizeMismatch = rawBase.height !== rawActual.height || rawBase.width !== rawActual.width;
|
|
88
|
+
const [base, actual] = hasSizeMismatch ? alignImagesToSameSize(rawBase, rawActual) : [rawBase, rawActual];
|
|
89
|
+
const diff = new import_pngjs.PNG({ width: base.width, height: base.height });
|
|
90
|
+
const diffPixels = (0, import_pixelmatch.default)(
|
|
91
|
+
actual.data,
|
|
92
|
+
base.data,
|
|
93
|
+
diff.data,
|
|
94
|
+
diff.width,
|
|
95
|
+
diff.height,
|
|
96
|
+
PIXELMATCH_OPTIONS
|
|
97
|
+
);
|
|
98
|
+
return { diffPixels, diff };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// files.ts
|
|
102
|
+
function createImageFileName(path, type) {
|
|
103
|
+
let newPath = path;
|
|
104
|
+
if (path.startsWith("/")) {
|
|
105
|
+
newPath = `.${path}`;
|
|
106
|
+
}
|
|
107
|
+
return `${newPath}/${type}.png`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// screenshots.ts
|
|
111
|
+
function baseExists(path) {
|
|
112
|
+
const fileName = createImageFileName(path, "base");
|
|
113
|
+
const exists = fs2.existsSync(fileName);
|
|
114
|
+
if (!exists) {
|
|
115
|
+
console.log(
|
|
116
|
+
"Base image does not exist. This means a new one will be created. If your base should exist, something went wrong."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return exists;
|
|
120
|
+
}
|
|
121
|
+
function compareScreenshots(screenshotFolder) {
|
|
122
|
+
const basePath = createImageFileName(screenshotFolder, "base");
|
|
123
|
+
const actualPath = createImageFileName(screenshotFolder, "new");
|
|
124
|
+
const { diffPixels, diff } = getDiffPixels(basePath, actualPath);
|
|
125
|
+
if (diffPixels) {
|
|
126
|
+
fs2.writeFile(
|
|
127
|
+
createImageFileName(screenshotFolder, "diff"),
|
|
128
|
+
import_pngjs2.PNG.sync.write(diff),
|
|
129
|
+
(err) => {
|
|
130
|
+
if (err) {
|
|
131
|
+
console.error("\u274C Diff exists but unable to create diff.png", err);
|
|
13
132
|
}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
fs2.unlink(actualPath, (err) => {
|
|
137
|
+
if (err) {
|
|
138
|
+
console.error("\u274C No diff but unable to delete actualPath}", err);
|
|
139
|
+
}
|
|
14
140
|
});
|
|
15
|
-
|
|
141
|
+
}
|
|
142
|
+
return diffPixels;
|
|
143
|
+
}
|
|
144
|
+
var trimPostfix = (path) => {
|
|
145
|
+
const imgExt = ".png";
|
|
146
|
+
const attemptPostfixRegex = new RegExp(`\\s\\(attempt \\d+\\)${imgExt}$`);
|
|
147
|
+
if (attemptPostfixRegex.test(path)) {
|
|
148
|
+
return path.replace(attemptPostfixRegex, imgExt);
|
|
149
|
+
}
|
|
150
|
+
return path;
|
|
151
|
+
};
|
|
152
|
+
var getNewPath = (path) => {
|
|
153
|
+
let newPath;
|
|
154
|
+
if (path.includes("___")) {
|
|
155
|
+
newPath = path.slice(path.lastIndexOf("___") + 3);
|
|
156
|
+
if (newPath.startsWith("/")) {
|
|
157
|
+
newPath = `.${newPath}`;
|
|
158
|
+
}
|
|
159
|
+
console.log(newPath);
|
|
160
|
+
} else {
|
|
161
|
+
newPath = path;
|
|
162
|
+
}
|
|
163
|
+
return trimPostfix(newPath);
|
|
164
|
+
};
|
|
165
|
+
function onAfterScreenshot(details) {
|
|
166
|
+
console.log("\u{1F9F8} Screenshot was saved to:", details.path);
|
|
167
|
+
if (!details.path.match("cypress")) {
|
|
168
|
+
return Promise.resolve({});
|
|
169
|
+
}
|
|
170
|
+
const newPath = getNewPath(details.path);
|
|
171
|
+
const newPathDir = newPath.substring(0, newPath.lastIndexOf("/"));
|
|
172
|
+
try {
|
|
173
|
+
fs2.mkdirSync(newPathDir, { recursive: true });
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error("\u274C Error creating new screenshot folder:", newPathDir, err);
|
|
176
|
+
}
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
fs2.rename(details.path, newPath, (err) => {
|
|
179
|
+
if (err) {
|
|
180
|
+
reject(err);
|
|
181
|
+
}
|
|
182
|
+
resolve({ path: newPath });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// index.ts
|
|
188
|
+
function setupVisualTests(on, config) {
|
|
189
|
+
on("after:screenshot", onAfterScreenshot);
|
|
190
|
+
on("task", {
|
|
191
|
+
baseExists,
|
|
192
|
+
compareScreenshots,
|
|
193
|
+
log: (message) => {
|
|
194
|
+
console.log(message);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return config;
|
|
16
199
|
}
|
|
17
|
-
|
|
200
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
201
|
+
0 && (module.exports = {
|
|
202
|
+
setupVisualTests
|
|
203
|
+
});
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
/// <reference types="cypress" />
|
|
3
|
-
export type MatchScreenshotArgs = {
|
|
1
|
+
type MatchScreenshotArgs = {
|
|
4
2
|
rawName?: string;
|
|
5
3
|
options?: Partial<Cypress.ScreenshotOptions>;
|
|
6
4
|
};
|
|
7
|
-
|
|
5
|
+
declare function matchScreenshot(subject: Cypress.JQueryWithSelector | Window | Document | void, args?: MatchScreenshotArgs): void;
|
|
8
6
|
interface ExtendedCurrentRunnable extends Mocha.Runnable {
|
|
9
7
|
currentRunnable?: {
|
|
10
8
|
order?: unknown;
|
|
@@ -19,4 +17,5 @@ declare global {
|
|
|
19
17
|
}
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
|
-
|
|
20
|
+
|
|
21
|
+
export { type MatchScreenshotArgs, matchScreenshot };
|
package/dist/match-screenshot.js
CHANGED
|
@@ -1,81 +1,120 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
matchScreenshot: () => matchScreenshot
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(match_screenshot_exports);
|
|
26
|
+
var PREFIX_DIFFERENTIATOR = "___";
|
|
27
|
+
var SUFFIX_TEST_IDENTIFIER = ".spec.ts";
|
|
28
|
+
var SCREENSHOTS_FOLDER_NAME = "screenshots";
|
|
7
29
|
function forceFont() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
30
|
+
const iframe = window.parent.document.querySelector("iframe");
|
|
31
|
+
const contentDocument = iframe && iframe.contentDocument;
|
|
32
|
+
if (contentDocument) {
|
|
33
|
+
const style = contentDocument.createElement("style");
|
|
34
|
+
style.type = "text/css";
|
|
35
|
+
style.appendChild(
|
|
36
|
+
contentDocument.createTextNode("* { font-family: Arial !important; }")
|
|
37
|
+
);
|
|
38
|
+
contentDocument.head.appendChild(style);
|
|
39
|
+
return style;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
18
42
|
}
|
|
19
43
|
function getTestFolderPathFromScripts(rawName) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
const relativeTestPath = Cypress.spec.relative;
|
|
45
|
+
if (!relativeTestPath) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"\u274C Could not find matching script in the Cypress DOM to infer the test folder path"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const currentTestNumber = Cypress.mocha.getRunner().currentRunnable?.order;
|
|
51
|
+
if (!rawName && typeof currentTestNumber === "number" && currentTestNumber > 1) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"\u274C The rawName argument was not provided to matchScreenshot and is required for test files containing multiple tests!"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const testName = relativeTestPath.substring(
|
|
57
|
+
relativeTestPath.lastIndexOf("/") + 1,
|
|
58
|
+
relativeTestPath.lastIndexOf(SUFFIX_TEST_IDENTIFIER)
|
|
59
|
+
);
|
|
60
|
+
const name = rawName || testName;
|
|
61
|
+
const screenshotsFolder = `${SCREENSHOTS_FOLDER_NAME}/${relativeTestPath.substring(
|
|
62
|
+
0,
|
|
63
|
+
relativeTestPath.lastIndexOf(testName)
|
|
64
|
+
)}${name}`;
|
|
65
|
+
return {
|
|
66
|
+
name,
|
|
67
|
+
screenshotsFolder
|
|
68
|
+
};
|
|
37
69
|
}
|
|
38
70
|
function verifyImages() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.then(images => {
|
|
45
|
-
if (images) {
|
|
46
|
-
cy.wrap(images).each($img => {
|
|
47
|
-
cy.wrap($img).should('exist').and('have.prop', 'naturalWidth');
|
|
48
|
-
});
|
|
49
|
-
}
|
|
71
|
+
if (Cypress.$("img:visible").length > 0) {
|
|
72
|
+
cy.document().its("body").find("img").filter(":visible").then((images) => {
|
|
73
|
+
if (images) {
|
|
74
|
+
cy.wrap(images).each(($img) => {
|
|
75
|
+
cy.wrap($img).should("exist").and("have.prop", "naturalWidth");
|
|
50
76
|
});
|
|
51
|
-
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
52
80
|
}
|
|
53
81
|
function matchScreenshot(subject, args) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
const { rawName, options = {} } = args || {};
|
|
83
|
+
forceFont();
|
|
84
|
+
verifyImages();
|
|
85
|
+
const { name, screenshotsFolder } = getTestFolderPathFromScripts(rawName);
|
|
86
|
+
cy.task("baseExists", screenshotsFolder).then((hasBase) => {
|
|
87
|
+
const target = subject ? cy.wrap(subject) : cy;
|
|
88
|
+
target.screenshot(
|
|
89
|
+
`${PREFIX_DIFFERENTIATOR}${screenshotsFolder}/new`,
|
|
90
|
+
options
|
|
91
|
+
);
|
|
92
|
+
if (!hasBase) {
|
|
93
|
+
cy.task(
|
|
94
|
+
"log",
|
|
95
|
+
`\u2705 A new base image was created for ${name}. Create this as a new base image via Comparadise!`
|
|
96
|
+
);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
cy.task("compareScreenshots", screenshotsFolder).then((diffPixels) => {
|
|
100
|
+
if (diffPixels === 0) {
|
|
101
|
+
cy.log(`\u2705 Actual image of ${name} was the same as base`);
|
|
102
|
+
} else {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`\u274C Actual image of ${name} differed by ${diffPixels} pixels.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
78
108
|
});
|
|
109
|
+
return null;
|
|
110
|
+
});
|
|
79
111
|
}
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
Cypress.Commands.add(
|
|
113
|
+
"matchScreenshot",
|
|
114
|
+
{ prevSubject: ["optional", "element", "window", "document"] },
|
|
115
|
+
matchScreenshot
|
|
116
|
+
);
|
|
117
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
118
|
+
0 && (module.exports = {
|
|
119
|
+
matchScreenshot
|
|
120
|
+
});
|
package/package.json
CHANGED
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/pixelmatch": "5.2.6",
|
|
25
|
-
"@types/pngjs": "6.0.
|
|
26
|
-
"cypress": "13.8.
|
|
25
|
+
"@types/pngjs": "6.0.5",
|
|
26
|
+
"cypress": "13.8.1",
|
|
27
|
+
"tsup": "8.0.2"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
30
|
"cypress": ">=12"
|
|
@@ -32,13 +33,12 @@
|
|
|
32
33
|
"dist",
|
|
33
34
|
"types"
|
|
34
35
|
],
|
|
35
|
-
"version": "1.
|
|
36
|
+
"version": "1.16.0",
|
|
36
37
|
"scripts": {
|
|
37
|
-
"build": "
|
|
38
|
+
"build": "tsup",
|
|
38
39
|
"bump-version": "pnpm version $NEW_VERSION",
|
|
39
40
|
"lint": "eslint . --max-warnings=0",
|
|
40
|
-
"postbuild": "echo \"require('./dist/match-screenshot');\" > commands.js
|
|
41
|
-
"prebuild": "rm -rf dist types",
|
|
41
|
+
"postbuild": "echo \"require('./dist/match-screenshot');\" > commands.js",
|
|
42
42
|
"test": "exit 0"
|
|
43
43
|
}
|
|
44
44
|
}
|
package/dist/files.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createImageFileName = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Creates a file path that is relative to the root
|
|
6
|
-
* @param {string} type - Value to append to fileName
|
|
7
|
-
* @param {string} path - (optional) FileName which corresponds to a module Id
|
|
8
|
-
* returns a string of just the prefix or the fileName prepended by the prefix
|
|
9
|
-
*/
|
|
10
|
-
function createImageFileName(path, type) {
|
|
11
|
-
let newPath = path;
|
|
12
|
-
if (path.startsWith('/')) {
|
|
13
|
-
newPath = `.${path}`;
|
|
14
|
-
}
|
|
15
|
-
return `${newPath}/${type}.png`;
|
|
16
|
-
}
|
|
17
|
-
exports.createImageFileName = createImageFileName;
|
package/dist/images.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
-
};
|
|
28
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.getDiffPixels = void 0;
|
|
30
|
-
const fs = __importStar(require("fs"));
|
|
31
|
-
const pngjs_1 = require("pngjs");
|
|
32
|
-
const pixelmatch_1 = __importDefault(require("pixelmatch"));
|
|
33
|
-
const PIXELMATCH_OPTIONS = {
|
|
34
|
-
threshold: 0.3
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Helper function to create reusable image resizer
|
|
38
|
-
*/
|
|
39
|
-
const createImageResizer = (width, height) => (source) => {
|
|
40
|
-
const resized = new pngjs_1.PNG({ width, height, fill: true });
|
|
41
|
-
pngjs_1.PNG.bitblt(source, resized, 0, 0, source.width, source.height, 0, 0);
|
|
42
|
-
return resized;
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Fills new area added after resize with transparent black color.
|
|
46
|
-
* I like idea of checker board pattern, but it seems to be too complicated
|
|
47
|
-
* to implement considering how low-level pngjs API is.
|
|
48
|
-
*/
|
|
49
|
-
const fillSizeDifference = (width, height) => (image) => {
|
|
50
|
-
const inArea = (x, y) => y > height || x > width;
|
|
51
|
-
for (let y = 0; y < image.height; y++) {
|
|
52
|
-
for (let x = 0; x < image.width; x++) {
|
|
53
|
-
if (inArea(x, y)) {
|
|
54
|
-
const idx = (image.width * y + x) << 2;
|
|
55
|
-
image.data[idx] = 0;
|
|
56
|
-
image.data[idx + 1] = 0;
|
|
57
|
-
image.data[idx + 2] = 0;
|
|
58
|
-
image.data[idx + 3] = 64;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return image;
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Aligns images sizes to biggest common value
|
|
66
|
-
* and fills new pixels with transparent pixels
|
|
67
|
-
*/
|
|
68
|
-
function alignImagesToSameSize(firstImage, secondImage) {
|
|
69
|
-
// Keep original sizes to fill extended area later
|
|
70
|
-
const firstImageWidth = firstImage.width;
|
|
71
|
-
const firstImageHeight = firstImage.height;
|
|
72
|
-
const secondImageWidth = secondImage.width;
|
|
73
|
-
const secondImageHeight = secondImage.height;
|
|
74
|
-
// Calculate biggest common values
|
|
75
|
-
const resizeToSameSize = createImageResizer(Math.max(firstImageWidth, secondImageWidth), Math.max(firstImageHeight, secondImageHeight));
|
|
76
|
-
// Resize both images
|
|
77
|
-
const resizedFirst = resizeToSameSize(firstImage);
|
|
78
|
-
const resizedSecond = resizeToSameSize(secondImage);
|
|
79
|
-
// Fill resized area with black transparent pixels
|
|
80
|
-
return [
|
|
81
|
-
fillSizeDifference(firstImageWidth, firstImageHeight)(resizedFirst),
|
|
82
|
-
fillSizeDifference(secondImageWidth, secondImageHeight)(resizedSecond)
|
|
83
|
-
];
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Compares a base and new image and returns the pixel difference
|
|
87
|
-
* and diff PNG for writing the diff image to.
|
|
88
|
-
* @param {string} basePath - Full file path to base image
|
|
89
|
-
* @param {string} actualPath - Full file path to new image
|
|
90
|
-
*/
|
|
91
|
-
function getDiffPixels(basePath, actualPath) {
|
|
92
|
-
const rawBase = pngjs_1.PNG.sync.read(fs.readFileSync(basePath));
|
|
93
|
-
const rawActual = pngjs_1.PNG.sync.read(fs.readFileSync(actualPath));
|
|
94
|
-
const hasSizeMismatch = rawBase.height !== rawActual.height || rawBase.width !== rawActual.width;
|
|
95
|
-
const [base, actual] = hasSizeMismatch
|
|
96
|
-
? alignImagesToSameSize(rawBase, rawActual)
|
|
97
|
-
: [rawBase, rawActual];
|
|
98
|
-
const diff = new pngjs_1.PNG({ width: base.width, height: base.height });
|
|
99
|
-
const diffPixels = (0, pixelmatch_1.default)(actual.data, base.data, diff.data, diff.width, diff.height, PIXELMATCH_OPTIONS);
|
|
100
|
-
return { diffPixels, diff };
|
|
101
|
-
}
|
|
102
|
-
exports.getDiffPixels = getDiffPixels;
|
package/dist/screenshots.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.onAfterScreenshot = exports.compareScreenshots = exports.baseExists = void 0;
|
|
27
|
-
const fs = __importStar(require("fs"));
|
|
28
|
-
const pngjs_1 = require("pngjs");
|
|
29
|
-
const images_1 = require("./images");
|
|
30
|
-
const files_1 = require("./files");
|
|
31
|
-
/**
|
|
32
|
-
* Checks if a base image exists
|
|
33
|
-
* @param path - Folder path where you can find the base.png image
|
|
34
|
-
* @returns true if path/base.png exists, false if not.
|
|
35
|
-
*/
|
|
36
|
-
function baseExists(path) {
|
|
37
|
-
const fileName = (0, files_1.createImageFileName)(path, 'base');
|
|
38
|
-
const exists = fs.existsSync(fileName);
|
|
39
|
-
if (!exists) {
|
|
40
|
-
console.log('Base image does not exist. This means a new one will be created. If your base should exist, something went wrong.');
|
|
41
|
-
}
|
|
42
|
-
return exists;
|
|
43
|
-
}
|
|
44
|
-
exports.baseExists = baseExists;
|
|
45
|
-
/**
|
|
46
|
-
* Runs a visual regression test.
|
|
47
|
-
* @param screenshotFolder - Full screenshots folder where the base/new/diff
|
|
48
|
-
* images will be compared and written to.
|
|
49
|
-
*/
|
|
50
|
-
function compareScreenshots(screenshotFolder) {
|
|
51
|
-
const basePath = (0, files_1.createImageFileName)(screenshotFolder, 'base');
|
|
52
|
-
const actualPath = (0, files_1.createImageFileName)(screenshotFolder, 'new');
|
|
53
|
-
const { diffPixels, diff } = (0, images_1.getDiffPixels)(basePath, actualPath);
|
|
54
|
-
if (diffPixels) {
|
|
55
|
-
// Create diff.png next to base and new for review
|
|
56
|
-
fs.writeFile((0, files_1.createImageFileName)(screenshotFolder, 'diff'), pngjs_1.PNG.sync.write(diff), err => {
|
|
57
|
-
if (err) {
|
|
58
|
-
console.error('❌ Diff exists but unable to create diff.png', err);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
// Delete created new.png. Not needed if there's no diff
|
|
64
|
-
fs.unlink(actualPath, err => {
|
|
65
|
-
if (err) {
|
|
66
|
-
console.error('❌ No diff but unable to delete actualPath}', err);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
return diffPixels;
|
|
71
|
-
}
|
|
72
|
-
exports.compareScreenshots = compareScreenshots;
|
|
73
|
-
const trimPostfix = (path) => {
|
|
74
|
-
const imgExt = '.png';
|
|
75
|
-
const attemptPostfixRegex = new RegExp(`\\s\\(attempt \\d+\\)${imgExt}$`);
|
|
76
|
-
if (attemptPostfixRegex.test(path)) {
|
|
77
|
-
return path.replace(attemptPostfixRegex, imgExt);
|
|
78
|
-
}
|
|
79
|
-
return path;
|
|
80
|
-
};
|
|
81
|
-
const getNewPath = (path) => {
|
|
82
|
-
let newPath = path.slice(path.lastIndexOf('___') + 3);
|
|
83
|
-
console.log(newPath);
|
|
84
|
-
if (newPath.startsWith('/')) {
|
|
85
|
-
newPath = `.${newPath}`;
|
|
86
|
-
}
|
|
87
|
-
return trimPostfix(newPath);
|
|
88
|
-
};
|
|
89
|
-
/**
|
|
90
|
-
* Renames all root cypress screenshots to where the test was actually run.
|
|
91
|
-
* Should NOT be used standalone. Works with the matchScreenshot task.
|
|
92
|
-
* @param {Cypress.ScreenshotDetails} details
|
|
93
|
-
*/
|
|
94
|
-
function onAfterScreenshot(details) {
|
|
95
|
-
console.log('🧸 Screenshot was saved to:', details.path);
|
|
96
|
-
if (!details.path.match('cypress')) {
|
|
97
|
-
return Promise.resolve({});
|
|
98
|
-
}
|
|
99
|
-
const newPath = getNewPath(details.path);
|
|
100
|
-
const newPathDir = newPath.substring(0, newPath.lastIndexOf('/'));
|
|
101
|
-
try {
|
|
102
|
-
fs.mkdirSync(newPathDir, { recursive: true });
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
console.error('❌ Error creating new screenshot folder:', newPathDir, err);
|
|
106
|
-
}
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
fs.rename(details.path, newPath, err => {
|
|
109
|
-
if (err) {
|
|
110
|
-
reject(err);
|
|
111
|
-
}
|
|
112
|
-
// because we renamed/moved the image, resolve with the new path
|
|
113
|
-
// so it is accurate in the test results
|
|
114
|
-
resolve({ path: newPath });
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
exports.onAfterScreenshot = onAfterScreenshot;
|
package/types/files.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a file path that is relative to the root
|
|
3
|
-
* @param {string} type - Value to append to fileName
|
|
4
|
-
* @param {string} path - (optional) FileName which corresponds to a module Id
|
|
5
|
-
* returns a string of just the prefix or the fileName prepended by the prefix
|
|
6
|
-
*/
|
|
7
|
-
export declare function createImageFileName(path: string, type: string): string;
|
package/types/images.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { PNG } from 'pngjs';
|
|
2
|
-
/**
|
|
3
|
-
* Compares a base and new image and returns the pixel difference
|
|
4
|
-
* and diff PNG for writing the diff image to.
|
|
5
|
-
* @param {string} basePath - Full file path to base image
|
|
6
|
-
* @param {string} actualPath - Full file path to new image
|
|
7
|
-
*/
|
|
8
|
-
export declare function getDiffPixels(basePath: string, actualPath: string): {
|
|
9
|
-
diffPixels: number;
|
|
10
|
-
diff: PNG;
|
|
11
|
-
};
|
package/types/index.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/// <reference types="cypress" />
|
|
2
|
-
import { MatchScreenshotArgs } from './match-screenshot';
|
|
3
|
-
export declare function setupVisualTests(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions): Cypress.PluginConfigOptions;
|
|
4
|
-
declare global {
|
|
5
|
-
namespace Cypress {
|
|
6
|
-
interface Chainable {
|
|
7
|
-
matchScreenshot(args?: MatchScreenshotArgs): Chainable;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
}
|
package/types/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/types/screenshots.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/// <reference types="cypress" />
|
|
2
|
-
/**
|
|
3
|
-
* Checks if a base image exists
|
|
4
|
-
* @param path - Folder path where you can find the base.png image
|
|
5
|
-
* @returns true if path/base.png exists, false if not.
|
|
6
|
-
*/
|
|
7
|
-
export declare function baseExists(path: string): boolean;
|
|
8
|
-
/**
|
|
9
|
-
* Runs a visual regression test.
|
|
10
|
-
* @param screenshotFolder - Full screenshots folder where the base/new/diff
|
|
11
|
-
* images will be compared and written to.
|
|
12
|
-
*/
|
|
13
|
-
export declare function compareScreenshots(screenshotFolder: string): number;
|
|
14
|
-
/**
|
|
15
|
-
* Renames all root cypress screenshots to where the test was actually run.
|
|
16
|
-
* Should NOT be used standalone. Works with the matchScreenshot task.
|
|
17
|
-
* @param {Cypress.ScreenshotDetails} details
|
|
18
|
-
*/
|
|
19
|
-
export declare function onAfterScreenshot(details: Cypress.ScreenshotDetails): Promise<Cypress.AfterScreenshotReturnObject>;
|