@rettangoli/vt 0.0.6 → 0.0.8

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.6",
3
+ "version": "0.0.8",
4
4
  "description": "Rettangoli Visual Testing",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -15,7 +15,6 @@
15
15
  "commander": "^13.1.0",
16
16
  "js-yaml": "^4.1.0",
17
17
  "liquidjs": "^10.21.0",
18
- "looks-same": "^9.0.0",
19
18
  "playwright": "^1.52.0",
20
19
  "sharp": "^0.33.0",
21
20
  "shiki": "^3.3.0"
package/src/cli/report.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import looksSame from "looks-same";
3
+ import crypto from "crypto";
4
4
  import sharp from "sharp";
5
5
  import { Liquid } from "liquidjs";
6
6
  import { cp } from "node:fs/promises";
@@ -60,25 +60,38 @@ function sortPaths(a, b) {
60
60
  return partsA.number - partsB.number;
61
61
  }
62
62
 
63
+ async function calculateImageHash(imagePath) {
64
+ try {
65
+ const imageBuffer = fs.readFileSync(imagePath);
66
+ const hash = crypto.createHash('md5').update(imageBuffer).digest('hex');
67
+ return hash;
68
+ } catch (error) {
69
+ console.error(`Error calculating hash for ${imagePath}:`, error);
70
+ return null;
71
+ }
72
+ }
73
+
63
74
  async function compareImages(artifactPath, goldPath) {
64
75
  try {
65
- const {equal, diffBounds, diffClusters} = await looksSame(artifactPath, goldPath, {
66
- strict: true,
67
- ignoreAntialiasing: true,
68
- ignoreCaret: true,
69
- shouldCluster: true,
70
- clustersSize: 10
71
- });
76
+ const artifactHash = await calculateImageHash(artifactPath);
77
+ const goldHash = await calculateImageHash(goldPath);
78
+
79
+ if (artifactHash === null || goldHash === null) {
80
+ return {
81
+ equal: false,
82
+ error: true,
83
+ };
84
+ }
85
+
86
+ const equal = artifactHash === goldHash;
72
87
 
73
88
  return {
74
89
  equal,
75
90
  error: false,
76
- diffBounds,
77
- diffClusters
78
91
  };
79
92
  } catch (error) {
80
93
  console.error("Error comparing images:", error);
81
- return {
94
+ return {
82
95
  equal: false,
83
96
  error: true,
84
97
  diffBounds: null,
@@ -165,16 +178,12 @@ async function main(options = {}) {
165
178
 
166
179
  let equal = true;
167
180
  let error = false;
168
- let diffBounds = null;
169
- let diffClusters = null;
170
181
 
171
182
  // Compare images if both exist
172
183
  if (candidateExists && referenceExists) {
173
184
  const comparison = await compareImages(candidatePath, referencePath);
174
185
  equal = comparison.equal;
175
186
  error = comparison.error;
176
- diffBounds = comparison.diffBounds;
177
- diffClusters = comparison.diffClusters;
178
187
  } else {
179
188
  equal = false; // If one file is missing, they're not equal
180
189
  }
@@ -185,8 +194,6 @@ async function main(options = {}) {
185
194
  referencePath: referenceExists ? siteReferencePath : null, // Use site reference path for HTML report
186
195
  path: relativePath,
187
196
  equal: candidateExists && referenceExists ? equal : false,
188
- diffBounds,
189
- diffClusters,
190
197
  onlyInCandidate: candidateExists && !referenceExists,
191
198
  onlyInReference: !candidateExists && referenceExists,
192
199
  });
@@ -207,8 +214,6 @@ async function main(options = {}) {
207
214
  ? path.relative(siteOutputPath, result.referencePath)
208
215
  : null,
209
216
  equal: result.equal,
210
- diffBounds: result.diffBounds,
211
- diffClusters: result.diffClusters,
212
217
  onlyInCandidate: result.onlyInCandidate,
213
218
  onlyInReference: result.onlyInReference,
214
219
  };
@@ -219,8 +224,6 @@ async function main(options = {}) {
219
224
  candidatePath: item.candidatePath,
220
225
  referencePath: item.referencePath,
221
226
  equal: item.equal,
222
- diffBounds: item.diffBounds,
223
- diffClusters: item.diffClusters
224
227
  };
225
228
  console.log(JSON.stringify(logData, null, 2));
226
229
  });
package/src/common.js CHANGED
@@ -15,6 +15,7 @@ import { chromium } from "playwright";
15
15
  import { codeToHtml } from "shiki";
16
16
  import sharp from "sharp";
17
17
  import path from "path";
18
+ import { createSteps } from "./createSteps.js";
18
19
 
19
20
  const removeExtension = (filePath) => filePath.replace(/\.[^/.]+$/, "");
20
21
 
@@ -301,53 +302,14 @@ async function takeScreenshots(
301
302
  const initialScreenshotPath = await takeAndSaveScreenshot(page, baseName);
302
303
  console.log(`Initial screenshot saved: ${initialScreenshotPath}`);
303
304
 
305
+ const stepContext = {
306
+ baseName,
307
+ takeAndSaveScreenshot,
308
+ };
309
+ const stepsExecutor = createSteps(page, stepContext);
310
+
304
311
  for (const step of file.frontMatter?.steps || []) {
305
- const [command, ...args] = step.split(" ");
306
- switch (command) {
307
- case "move":
308
- await page.mouse.move(Number(args[0]), Number(args[1]));
309
- break;
310
- case "click":
311
- await page.mouse.click(Number(args[0]), Number(args[1]), { button: "left" });
312
- break;
313
- case "rclick":
314
- await page.mouse.click(Number(args[0]), Number(args[1]), { button: "right" });
315
- break;
316
- case "mouseDown":
317
- await page.mouse.down();
318
- break;
319
- case "mouseUp":
320
- await page.mouse.up();
321
- break;
322
- case "keypress":
323
- await page.keyboard.press(args[0]);
324
- break;
325
- case "wait":
326
- await page.waitForTimeout(Number(args[0]));
327
- break;
328
- case "screenshot":
329
- screenshotIndex++;
330
- const screenshotPath = await takeAndSaveScreenshot(page, `${baseName}-${screenshotIndex}`);
331
- console.log(`Screenshot saved: ${screenshotPath}`);
332
- break;
333
- case "customEvent":
334
- //Use to dispatch custom event for the test evironment to listen to
335
- //customEvent <eventName> <...parameters>
336
- //To listen to the event use
337
- //window.addEventListener(<eventName>,(event)=>{
338
- // console.log("The params that you pass through: ",event.detail)
339
- //})
340
- const [eventName, ...params] = args;
341
- const payload = {};
342
- params.forEach(param => {
343
- const [key, value] = param.split('=');
344
- payload[key] = value;
345
- });
346
- await page.evaluate(({eventName,payload}) => {
347
- window.dispatchEvent(new CustomEvent(eventName, { detail: payload }));
348
- },{eventName,payload});
349
- break;
350
- }
312
+ await stepsExecutor.executeStep(step);
351
313
  }
352
314
  completed++;
353
315
  console.log(`Finished processing ${file.path} (${completed}/${total})`);
@@ -0,0 +1,148 @@
1
+ async function click(page, args, context, selectedElement) {
2
+ if (selectedElement) {
3
+ await selectedElement.click();
4
+ } else if (args.length >= 2) {
5
+ await page.mouse.click(Number(args[0]), Number(args[1]), { button: "left" });
6
+ } else {
7
+ console.warn('`click` command needs a `select` block or coordinates.');
8
+ }
9
+ }
10
+
11
+ async function customEvent(page, args) {
12
+ const [eventName, ...params] = args;
13
+ const payload = {};
14
+ params.forEach(param => {
15
+ const [key, value] = param.split('=');
16
+ payload[key] = value;
17
+ });
18
+ await page.evaluate(({ eventName, payload }) => {
19
+ window.dispatchEvent(new CustomEvent(eventName, { detail: payload }));
20
+ }, { eventName, payload });
21
+ }
22
+
23
+ async function goto(page, args) {
24
+ await page.goto(args[0], { waitUntil: "networkidle" });
25
+ }
26
+
27
+ async function keypress(page, args) {
28
+ await page.keyboard.press(args[0]);
29
+ }
30
+
31
+ async function mouseDown(page) {
32
+ await page.mouse.down();
33
+ }
34
+
35
+ async function mouseUp(page) {
36
+ await page.mouse.up();
37
+ }
38
+
39
+ async function rMouseDown(page){
40
+ await page.mouse.down({ button: 'right' });
41
+ }
42
+
43
+ async function rMouseUp(page){
44
+ await page.mouse.up({ button: 'right' });
45
+ }
46
+
47
+ async function move(page, args) {
48
+ await page.mouse.move(Number(args[0]), Number(args[1]));
49
+ }
50
+
51
+ async function rclick(page, args, context, selectedElement) {
52
+ if (selectedElement) {
53
+ await selectedElement.click({ button: 'right' });
54
+ } else if (args.length >= 2) {
55
+ await page.mouse.click(Number(args[0]), Number(args[1]), { button: "right" });
56
+ } else {
57
+ console.warn('`rclick` command needs a `select` block or coordinates.');
58
+ }
59
+ }
60
+
61
+ async function wait(page, args) {
62
+ await page.waitForTimeout(Number(args[0]));
63
+ }
64
+
65
+ async function write(page, args, context, selectedElement) {
66
+ if (selectedElement) {
67
+ const textToWrite = args.join(' ');
68
+ await selectedElement.fill(textToWrite);
69
+ } else {
70
+ console.warn('`write` command called without a `select` block.');
71
+ }
72
+ }
73
+
74
+ async function select(page, args) {
75
+ const testId = args[0];
76
+ const hostElementLocator = page.getByTestId(testId);
77
+
78
+ const interactiveElementLocator = hostElementLocator.locator(
79
+ 'input, textarea, button, select, a'
80
+ ).first();
81
+
82
+ const count = await interactiveElementLocator.count();
83
+
84
+ if (count > 0) {
85
+ return interactiveElementLocator;
86
+ }
87
+
88
+ return hostElementLocator;
89
+ }
90
+
91
+ export function createSteps(page, context) {
92
+ let screenshotIndex = 0;
93
+
94
+ async function screenshot() {
95
+ screenshotIndex++;
96
+ const screenshotPath = await context.takeAndSaveScreenshot(page, `${context.baseName}-${screenshotIndex}`);
97
+ console.log(`Screenshot saved: ${screenshotPath}`);
98
+ }
99
+
100
+ const actionHandlers = {
101
+ click,
102
+ customEvent,
103
+ goto,
104
+ keypress,
105
+ mouseDown,
106
+ mouseUp,
107
+ move,
108
+ rclick,
109
+ rMouseDown,
110
+ rMouseUp,
111
+ screenshot,
112
+ select,
113
+ wait,
114
+ write,
115
+ };
116
+
117
+ async function executeSingleStep(stepString, selectedElement) {
118
+ const [command, ...args] = stepString.split(" ");
119
+ const actionFn = actionHandlers[command];
120
+ if (actionFn) {
121
+ await actionFn(page, args, context, selectedElement);
122
+ } else {
123
+ console.warn(`Unknown step command: "${command}"`);
124
+ }
125
+ }
126
+
127
+ return {
128
+ async executeStep(step) {
129
+ if (typeof step === 'string') {
130
+ await executeSingleStep(step, null);
131
+ } else if (typeof step === 'object' && step !== null) {
132
+ const blockCommandString = Object.keys(step)[0];
133
+ const nestedStepStrings = step[blockCommandString];
134
+ const [command, ...args] = blockCommandString.split(" ");
135
+
136
+ const blockFn = actionHandlers[command];
137
+ if (blockFn) {
138
+ const selectedElement = await blockFn(page, args, context, null);
139
+ for (const nestedStep of nestedStepStrings) {
140
+ await executeSingleStep(nestedStep, selectedElement);
141
+ }
142
+ } else {
143
+ console.warn(`Unsupported block command: "${command}".`);
144
+ }
145
+ }
146
+ }
147
+ };
148
+ }