@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/vt",
3
- "version": "0.0.12",
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
- "playwright": "^1.52.0",
19
- "sharp": "^0.33.0",
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 compareImages(artifactPath, goldPath) {
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
- const equal = artifactHash === goldHash;
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 { vtPath = "./vt" } = options;
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
- const comparison = await compareImages(candidatePath, referencePath);
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
  }
@@ -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) {