@rettangoli/vt 0.0.12 → 0.0.13
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/package.json +5 -4
- package/src/cli/report.js +105 -19
- package/src/common.js +12 -0
- package/src/createSteps.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/vt",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "Rettangoli Visual Testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"commander": "^13.1.0",
|
|
16
16
|
"js-yaml": "^4.1.0",
|
|
17
17
|
"liquidjs": "^10.21.0",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"pixelmatch": "^7.1.0",
|
|
19
|
+
"playwright": "1.57.0",
|
|
20
|
+
"sharp": "^0.34.5",
|
|
20
21
|
"shiki": "^3.3.0"
|
|
21
22
|
}
|
|
22
|
-
}
|
|
23
|
+
}
|
package/src/cli/report.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "path";
|
|
|
3
3
|
import crypto from "crypto";
|
|
4
4
|
import { Liquid } from "liquidjs";
|
|
5
5
|
import { cp } from "node:fs/promises";
|
|
6
|
+
import pixelmatch from "pixelmatch";
|
|
7
|
+
import sharp from "sharp";
|
|
6
8
|
|
|
7
9
|
const libraryTemplatesPath = new URL('./templates', import.meta.url).pathname;
|
|
8
10
|
|
|
@@ -70,35 +72,80 @@ async function calculateImageHash(imagePath) {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
async function
|
|
75
|
+
async function compareImagesMd5(artifactPath, goldPath) {
|
|
74
76
|
try {
|
|
75
77
|
const artifactHash = await calculateImageHash(artifactPath);
|
|
76
78
|
const goldHash = await calculateImageHash(goldPath);
|
|
77
79
|
|
|
78
80
|
if (artifactHash === null || goldHash === null) {
|
|
79
|
-
return {
|
|
80
|
-
equal: false,
|
|
81
|
-
error: true,
|
|
82
|
-
};
|
|
81
|
+
return { equal: false, error: true };
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
equal,
|
|
89
|
-
error: false,
|
|
90
|
-
};
|
|
84
|
+
return { equal: artifactHash === goldHash, error: false };
|
|
91
85
|
} catch (error) {
|
|
92
86
|
console.error("Error comparing images:", error);
|
|
93
|
-
return {
|
|
94
|
-
equal: false,
|
|
95
|
-
error: true,
|
|
96
|
-
diffBounds: null,
|
|
97
|
-
diffClusters: null
|
|
98
|
-
};
|
|
87
|
+
return { equal: false, error: true };
|
|
99
88
|
}
|
|
100
89
|
}
|
|
101
90
|
|
|
91
|
+
async function compareImagesPixelmatch(artifactPath, goldPath, diffPath, options = {}) {
|
|
92
|
+
const { colorThreshold = 0.1, diffThreshold = 0.3 } = options;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Load images and convert to raw RGBA using sharp
|
|
96
|
+
const [artifactData, goldData] = await Promise.all([
|
|
97
|
+
sharp(artifactPath).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
|
|
98
|
+
sharp(goldPath).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const { width, height } = artifactData.info;
|
|
102
|
+
const goldWidth = goldData.info.width;
|
|
103
|
+
const goldHeight = goldData.info.height;
|
|
104
|
+
const totalPixels = width * height;
|
|
105
|
+
|
|
106
|
+
// If dimensions don't match, images are different
|
|
107
|
+
if (width !== goldWidth || height !== goldHeight) {
|
|
108
|
+
return { equal: false, error: false, diffPixels: -1, totalPixels, similarity: 0 };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Create diff image buffer
|
|
112
|
+
const diffBuffer = Buffer.alloc(totalPixels * 4);
|
|
113
|
+
|
|
114
|
+
const diffPixels = pixelmatch(
|
|
115
|
+
artifactData.data,
|
|
116
|
+
goldData.data,
|
|
117
|
+
diffBuffer,
|
|
118
|
+
width,
|
|
119
|
+
height,
|
|
120
|
+
{ threshold: colorThreshold }
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const diffPercent = (diffPixels / totalPixels) * 100;
|
|
124
|
+
const similarity = (100 - diffPercent).toFixed(2);
|
|
125
|
+
const equal = diffPercent < diffThreshold;
|
|
126
|
+
|
|
127
|
+
// Save diff image if not equal
|
|
128
|
+
if (!equal && diffPath) {
|
|
129
|
+
fs.mkdirSync(path.dirname(diffPath), { recursive: true });
|
|
130
|
+
await sharp(diffBuffer, { raw: { width, height, channels: 4 } })
|
|
131
|
+
.png()
|
|
132
|
+
.toFile(diffPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { equal, error: false, diffPixels, totalPixels, similarity };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("Error comparing images with pixelmatch:", error);
|
|
138
|
+
return { equal: false, error: true };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function compareImages(artifactPath, goldPath, method = 'pixelmatch', diffPath = null, options = {}) {
|
|
143
|
+
if (method === 'md5') {
|
|
144
|
+
return compareImagesMd5(artifactPath, goldPath);
|
|
145
|
+
}
|
|
146
|
+
return compareImagesPixelmatch(artifactPath, goldPath, diffPath, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
102
149
|
async function generateReport({ results, templatePath, outputPath }) {
|
|
103
150
|
try {
|
|
104
151
|
// Read the template file
|
|
@@ -119,20 +166,36 @@ async function generateReport({ results, templatePath, outputPath }) {
|
|
|
119
166
|
}
|
|
120
167
|
|
|
121
168
|
async function main(options = {}) {
|
|
122
|
-
const {
|
|
169
|
+
const {
|
|
170
|
+
vtPath = "./vt",
|
|
171
|
+
compareMethod = 'pixelmatch',
|
|
172
|
+
colorThreshold = 0.1,
|
|
173
|
+
diffThreshold = 0.3,
|
|
174
|
+
} = options;
|
|
123
175
|
|
|
124
176
|
const siteOutputPath = path.join(".rettangoli", "vt", "_site");
|
|
125
177
|
const candidateDir = path.join(siteOutputPath, "candidate");
|
|
178
|
+
const diffDir = path.join(siteOutputPath, "diff");
|
|
126
179
|
const originalReferenceDir = path.join(vtPath, "reference");
|
|
127
180
|
const siteReferenceDir = path.join(siteOutputPath, "reference");
|
|
128
181
|
const templatePath = path.join(libraryTemplatesPath, "report.html");
|
|
129
182
|
const outputPath = path.join(siteOutputPath, "report.html");
|
|
130
183
|
|
|
184
|
+
console.log(`Comparison method: ${compareMethod}`);
|
|
185
|
+
if (compareMethod === 'pixelmatch') {
|
|
186
|
+
console.log(` color threshold: ${colorThreshold}, diff threshold: ${diffThreshold}%`);
|
|
187
|
+
}
|
|
188
|
+
|
|
131
189
|
if (!fs.existsSync(originalReferenceDir)) {
|
|
132
190
|
console.log("Reference directory does not exist, creating it...");
|
|
133
191
|
fs.mkdirSync(originalReferenceDir, { recursive: true });
|
|
134
192
|
}
|
|
135
193
|
|
|
194
|
+
// Create diff directory for diffs
|
|
195
|
+
if (compareMethod === 'pixelmatch' && !fs.existsSync(diffDir)) {
|
|
196
|
+
fs.mkdirSync(diffDir, { recursive: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
136
199
|
// Copy reference directory to _site for web access
|
|
137
200
|
if (fs.existsSync(originalReferenceDir)) {
|
|
138
201
|
console.log("Copying reference directory to _site...");
|
|
@@ -168,6 +231,7 @@ async function main(options = {}) {
|
|
|
168
231
|
const candidatePath = path.join(candidateDir, relativePath);
|
|
169
232
|
const referencePath = path.join(originalReferenceDir, relativePath);
|
|
170
233
|
const siteReferencePath = path.join(siteReferenceDir, relativePath);
|
|
234
|
+
const diffPath = path.join(diffDir, relativePath.replace('.webp', '-diff.png'));
|
|
171
235
|
|
|
172
236
|
const candidateExists = fs.existsSync(candidatePath);
|
|
173
237
|
const referenceExists = fs.existsSync(referencePath);
|
|
@@ -177,12 +241,28 @@ async function main(options = {}) {
|
|
|
177
241
|
|
|
178
242
|
let equal = true;
|
|
179
243
|
let error = false;
|
|
244
|
+
let similarity = null;
|
|
245
|
+
let diffPixels = null;
|
|
180
246
|
|
|
181
247
|
// Compare images if both exist
|
|
182
248
|
if (candidateExists && referenceExists) {
|
|
183
|
-
|
|
249
|
+
// Ensure diff directory exists
|
|
250
|
+
const diffDirPath = path.dirname(diffPath);
|
|
251
|
+
if (!fs.existsSync(diffDirPath)) {
|
|
252
|
+
fs.mkdirSync(diffDirPath, { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const comparison = await compareImages(
|
|
256
|
+
candidatePath,
|
|
257
|
+
referencePath,
|
|
258
|
+
compareMethod,
|
|
259
|
+
diffPath,
|
|
260
|
+
{ colorThreshold, diffThreshold }
|
|
261
|
+
);
|
|
184
262
|
equal = comparison.equal;
|
|
185
263
|
error = comparison.error;
|
|
264
|
+
similarity = comparison.similarity;
|
|
265
|
+
diffPixels = comparison.diffPixels;
|
|
186
266
|
} else {
|
|
187
267
|
equal = false; // If one file is missing, they're not equal
|
|
188
268
|
}
|
|
@@ -193,6 +273,8 @@ async function main(options = {}) {
|
|
|
193
273
|
referencePath: referenceExists ? siteReferencePath : null, // Use site reference path for HTML report
|
|
194
274
|
path: relativePath,
|
|
195
275
|
equal: candidateExists && referenceExists ? equal : false,
|
|
276
|
+
similarity,
|
|
277
|
+
diffPixels,
|
|
196
278
|
onlyInCandidate: candidateExists && !referenceExists,
|
|
197
279
|
onlyInReference: !candidateExists && referenceExists,
|
|
198
280
|
});
|
|
@@ -213,6 +295,8 @@ async function main(options = {}) {
|
|
|
213
295
|
? path.relative(siteOutputPath, result.referencePath)
|
|
214
296
|
: null,
|
|
215
297
|
equal: result.equal,
|
|
298
|
+
similarity: result.similarity,
|
|
299
|
+
diffPixels: result.diffPixels,
|
|
216
300
|
onlyInCandidate: result.onlyInCandidate,
|
|
217
301
|
onlyInReference: result.onlyInReference,
|
|
218
302
|
};
|
|
@@ -223,6 +307,8 @@ async function main(options = {}) {
|
|
|
223
307
|
candidatePath: item.candidatePath,
|
|
224
308
|
referencePath: item.referencePath,
|
|
225
309
|
equal: item.equal,
|
|
310
|
+
similarity: item.similarity ? `${item.similarity}%` : null,
|
|
311
|
+
diffPixels: item.diffPixels,
|
|
226
312
|
};
|
|
227
313
|
console.log(JSON.stringify(logData, null, 2));
|
|
228
314
|
});
|
package/src/common.js
CHANGED
|
@@ -306,6 +306,18 @@ async function takeScreenshots(
|
|
|
306
306
|
|
|
307
307
|
console.log(`Navigating to ${fileUrl}`);
|
|
308
308
|
await page.goto(fileUrl, { waitUntil: "networkidle" });
|
|
309
|
+
|
|
310
|
+
// Normalize font rendering for consistent screenshots
|
|
311
|
+
await page.addStyleTag({
|
|
312
|
+
content: `
|
|
313
|
+
* {
|
|
314
|
+
-webkit-font-smoothing: antialiased !important;
|
|
315
|
+
-moz-osx-font-smoothing: grayscale !important;
|
|
316
|
+
text-rendering: geometricPrecision !important;
|
|
317
|
+
}
|
|
318
|
+
`
|
|
319
|
+
});
|
|
320
|
+
|
|
309
321
|
if (waitTime > 0) {
|
|
310
322
|
await page.waitForTimeout(waitTime);
|
|
311
323
|
}
|
package/src/createSteps.js
CHANGED
|
@@ -22,6 +22,16 @@ async function customEvent(page, args) {
|
|
|
22
22
|
|
|
23
23
|
async function goto(page, args) {
|
|
24
24
|
await page.goto(args[0], { waitUntil: "networkidle" });
|
|
25
|
+
// Normalize font rendering for consistent screenshots
|
|
26
|
+
await page.addStyleTag({
|
|
27
|
+
content: `
|
|
28
|
+
* {
|
|
29
|
+
-webkit-font-smoothing: antialiased !important;
|
|
30
|
+
-moz-osx-font-smoothing: grayscale !important;
|
|
31
|
+
text-rendering: geometricPrecision !important;
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
});
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
async function keypress(page, args) {
|