@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 +1 -2
- package/src/cli/report.js +24 -21
- package/src/common.js +8 -46
- package/src/createSteps.js +148 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/vt",
|
|
3
|
-
"version": "0.0.
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
+
}
|