@rettangoli/vt 0.0.3 → 0.0.5

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.3",
3
+ "version": "0.0.5",
4
4
  "description": "Rettangoli Visual Testing",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/cli/report.js CHANGED
@@ -32,6 +32,34 @@ function getAllFiles(dir, fileList = []) {
32
32
  return fileList;
33
33
  }
34
34
 
35
+ function extractParts(p) {
36
+ const dir = path.dirname(p);
37
+ const filename = path.basename(p, '.webp');
38
+ const lastHyphenIndex = filename.lastIndexOf('-');
39
+
40
+ if (lastHyphenIndex > -1) {
41
+ const suffix = filename.substring(lastHyphenIndex + 1);
42
+ const number = parseInt(suffix);
43
+
44
+ if (!isNaN(number) && String(number) === suffix) {
45
+ const name = path.join(dir, filename.substring(0, lastHyphenIndex));
46
+ return { name, number };
47
+ }
48
+ }
49
+ // -1 is for the first file (as it will result in the first index when sorting)
50
+ return { name: path.join(dir, filename), number: -1 };
51
+ }
52
+
53
+ function sortPaths(a, b) {
54
+ const partsA = extractParts(a);
55
+ const partsB = extractParts(b);
56
+
57
+ if (partsA.name < partsB.name) return -1;
58
+ if (partsA.name > partsB.name) return 1;
59
+
60
+ return partsA.number - partsB.number;
61
+ }
62
+
35
63
  async function compareImages(artifactPath, goldPath) {
36
64
  try {
37
65
  const {equal, diffBounds, diffClusters} = await looksSame(artifactPath, goldPath, {
@@ -122,6 +150,8 @@ async function main(options = {}) {
122
150
  ...new Set([...candidateRelativePaths, ...referenceRelativePaths]),
123
151
  ];
124
152
 
153
+ allPaths.sort(sortPaths);
154
+
125
155
  for (const relativePath of allPaths) {
126
156
  const candidatePath = path.join(candidateDir, relativePath);
127
157
  const referencePath = path.join(originalReferenceDir, relativePath);
@@ -206,6 +236,10 @@ async function main(options = {}) {
206
236
  templatePath,
207
237
  outputPath,
208
238
  });
239
+ if(mismatchingItems.length > 0){
240
+ console.error("Error: there are more than 0 mismatching item.")
241
+ process.exit(1);
242
+ }
209
243
  } catch (error) {
210
244
  console.error("Error reading directories:", error);
211
245
  }
package/src/common.js CHANGED
@@ -16,12 +16,14 @@ import { codeToHtml } from "shiki";
16
16
  import sharp from "sharp";
17
17
  import path from "path";
18
18
 
19
+ const removeExtension = (filePath) => filePath.replace(/\.[^/.]+$/, "");
20
+
19
21
  const convertToHtmlExtension = (filePath) => {
20
22
  if (filePath.endsWith(".html")) {
21
23
  return filePath;
22
24
  }
23
25
  // Remove existing extension and add .html
24
- const baseName = filePath.replace(/\.[^/.]+$/, "");
26
+ const baseName = removeExtension(filePath);
25
27
  return baseName + ".html";
26
28
  };
27
29
 
@@ -50,7 +52,7 @@ engine.registerFilter("slug", (value) => {
50
52
  // Add custom filter to remove file extension
51
53
  engine.registerFilter("remove_ext", (value) => {
52
54
  if (typeof value !== "string") return "";
53
- return value.replace(/\.[^/.]+$/, "");
55
+ return removeExtension(value);
54
56
  });
55
57
 
56
58
  /**
@@ -136,10 +138,10 @@ async function generateHtml(specsDir, templatePath, outputDir, templateConfig) {
136
138
  const relativePath = path.relative(specsDir, filePath);
137
139
 
138
140
  let templateToUse = defaultTemplateContent;
139
- const resolvedTemplatePath = frontMatterObj.template ?
141
+ const resolvedTemplatePath = frontMatterObj?.template ?
140
142
  join(templateConfig.vtPath, "templates", frontMatterObj.template) :
141
143
  templateConfig.defaultTemplate;
142
- templateToUse = readFileSync(resolvedTemplatePath, "utf8");
144
+ templateToUse = readFileSync(resolvedTemplatePath, "utf8");
143
145
 
144
146
  // Render template
145
147
  const renderedContent = engine.parseAndRenderSync(templateToUse, {
@@ -214,7 +216,7 @@ function startWebServer(artifactsDir, staticDir, port) {
214
216
  server.listen(port, () => {
215
217
  console.log(`Server started at http://localhost:${port}`);
216
218
  });
217
-
219
+
218
220
  return server;
219
221
  }
220
222
 
@@ -255,9 +257,23 @@ async function takeScreenshots(
255
257
  console.log("Launching browser to take screenshots...");
256
258
  const browser = await chromium.launch();
257
259
 
260
+ const takeAndSaveScreenshot = async (page, basePath, suffix = '') => {
261
+ const finalPath = suffix ? `${basePath}-${suffix}` : basePath;
262
+ const tempPngPath = join(screenshotsDir, `${finalPath}.png`);
263
+ const screenshotPath = join(screenshotsDir, `${finalPath}.webp`);
264
+ ensureDirectoryExists(dirname(screenshotPath));
265
+
266
+ await page.screenshot({ path: tempPngPath, fullPage: true});
267
+ await sharp(tempPngPath).webp({ quality: 85 }).toFile(screenshotPath);
268
+
269
+ if (existsSync(tempPngPath)) {
270
+ unlinkSync(tempPngPath);
271
+ }
272
+ return screenshotPath;
273
+ };
274
+
258
275
  try {
259
- // Process files in parallel with limited concurrency
260
- const files = [...generatedFiles]; // Create a copy to work with
276
+ const files = [...generatedFiles];
261
277
  const total = files.length;
262
278
  let completed = 0;
263
279
 
@@ -268,95 +284,69 @@ async function takeScreenshots(
268
284
  // Create a new context and page for each file (for parallelism)
269
285
  const context = await browser.newContext();
270
286
  const page = await context.newPage();
287
+ let screenshotIndex = 0;
271
288
 
272
289
  try {
273
290
  // Construct URL from file path (add /candidate prefix since server serves from parent)
274
291
  const fileUrl = convertToHtmlExtension(
275
292
  `${serverUrl}/candidate/${file.path.replace(/\\/g, '/')}`
276
293
  );
277
- console.log(`Taking screenshot of ${fileUrl}`);
278
-
279
- // Navigate to the page
294
+ console.log(`Navigating to ${fileUrl}`);
280
295
  await page.goto(fileUrl, { waitUntil: "networkidle" });
281
-
282
- // Create screenshot output path (remove extension and add .webp)
283
- const baseName = file.path.replace(/\.[^/.]+$/, "");
284
- const tempPngPath = join(screenshotsDir, `${baseName}.png`);
285
- const screenshotPath = join(screenshotsDir, `${baseName}.webp`);
286
- ensureDirectoryExists(dirname(screenshotPath));
287
-
288
296
  if (waitTime > 0) {
289
- await new Promise((resolve) => setTimeout(resolve, waitTime));
297
+ await page.waitForTimeout(waitTime);
290
298
  }
299
+ const baseName = removeExtension(file.path);
291
300
 
292
- // Take screenshot as PNG first (Playwright doesn't support WebP)
293
- await page.screenshot({
294
- path: tempPngPath,
295
- fullPage: true
296
- });
297
-
298
- // Convert PNG to WebP using Sharp
299
- await sharp(tempPngPath)
300
- .webp({ quality: 85 })
301
- .toFile(screenshotPath);
302
-
303
- // Remove temporary PNG file
304
- if (existsSync(tempPngPath)) {
305
- unlinkSync(tempPngPath);
306
- }
301
+ const initialScreenshotPath = await takeAndSaveScreenshot(page, baseName);
302
+ console.log(`Initial screenshot saved: ${initialScreenshotPath}`);
307
303
 
308
- // example instructions:
309
- for (const instruction of file.frontMatter?.instructions || []) {
310
- const [command, ...args] = instruction.split(" ");
304
+ for (const step of file.frontMatter?.steps || []) {
305
+ const [command, ...args] = step.split(" ");
311
306
  switch (command) {
312
- case "lc":
313
- const x = Number(args[0]);
314
- const y = Number(args[1]);
315
- await page.mouse.click(x, y, {
316
- button: "left",
317
- });
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 "keypress":
317
+ await page.keyboard.press(args[0]);
318
+ break;
319
+ case "wait":
320
+ await page.waitForTimeout(Number(args[0]));
321
+ break;
322
+ case "screenshot":
323
+ screenshotIndex++;
324
+ const screenshotPath = await takeAndSaveScreenshot(page, `${baseName}-${screenshotIndex}`);
325
+ console.log(`Screenshot saved: ${screenshotPath}`);
318
326
  break;
319
- case "ss":
320
- const baseName = file.path.replace(/\.[^/.]+$/, "");
321
- const tempAdditionalPngPath = join(
322
- screenshotsDir,
323
- `${baseName}-${args[0]}.png`
324
- );
325
- const additonalScreenshotPath = join(
326
- screenshotsDir,
327
- `${baseName}-${args[0]}.webp`
328
- );
329
- console.log(`Taking additional screenshot at ${additonalScreenshotPath}`);
330
-
331
- // Take screenshot as PNG first
332
- await page.screenshot({
333
- path: tempAdditionalPngPath,
334
- fullPage: true
327
+ case "customEvent":
328
+ //Use to dispatch custom event for the test evironment to listen to
329
+ //customEvent <eventName> <...parameters>
330
+ //To listen to the event use
331
+ //window.addEventListener(<eventName>,(event)=>{
332
+ // console.log("The params that you pass through: ",event.detail)
333
+ //})
334
+ const [eventName, ...params] = args;
335
+ const payload = {};
336
+ params.forEach(param => {
337
+ const [key, value] = param.split('=');
338
+ payload[key] = value;
335
339
  });
336
-
337
- // Convert PNG to WebP using Sharp
338
- await sharp(tempAdditionalPngPath)
339
- .webp({ quality: 85 })
340
- .toFile(additonalScreenshotPath);
341
-
342
- // Remove temporary PNG file
343
- if (existsSync(tempAdditionalPngPath)) {
344
- unlinkSync(tempAdditionalPngPath);
345
- }
346
-
347
- console.log(
348
- `Additional screenshot taken at ${additonalScreenshotPath}`
349
- );
340
+ await page.evaluate(({eventName,payload}) => {
341
+ window.dispatchEvent(new CustomEvent(eventName, { detail: payload }));
342
+ },{eventName,payload});
350
343
  break;
351
344
  }
352
345
  }
353
-
354
346
  completed++;
355
- console.log(
356
- `Screenshot saved: ${screenshotPath} (${completed}/${total})`
357
- );
347
+ console.log(`Finished processing ${file.path} (${completed}/${total})`);
358
348
  } catch (error) {
359
- console.error(`Error taking screenshot for ${file.path}:`, error);
349
+ console.error(`Error processing instructions for ${file.path}:`, error);
360
350
  } finally {
361
351
  // Close the context when done
362
352
  await context.close();
@@ -467,4 +457,4 @@ export {
467
457
  takeScreenshots,
468
458
  generateOverview,
469
459
  readYaml,
470
- };
460
+ };