@rettangoli/vt 1.0.0-rc13 → 1.0.0-rc15

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/README.md CHANGED
@@ -141,15 +141,15 @@ Screenshot naming:
141
141
  A pre-built Docker image with `rtgl` and Playwright browsers is available:
142
142
 
143
143
  ```bash
144
- docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13
144
+ docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27
145
145
  ```
146
146
 
147
147
  Run commands against a local project:
148
148
 
149
149
  ```bash
150
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13 rtgl vt screenshot
151
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13 rtgl vt report
152
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc13 rtgl vt accept
150
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27 rtgl vt screenshot
151
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27 rtgl vt report
152
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27 rtgl vt accept
153
153
  ```
154
154
 
155
155
  Note:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/vt",
3
- "version": "1.0.0-rc13",
3
+ "version": "1.0.0-rc15",
4
4
  "description": "Rettangoli Visual Testing",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/cli/report.js CHANGED
@@ -2,10 +2,11 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import crypto from "crypto";
4
4
  import { cp } from "node:fs/promises";
5
+ import { load as loadYaml } from "js-yaml";
5
6
  import pixelmatch from "pixelmatch";
6
7
  import sharp from "sharp";
7
- import { readYaml } from "../common.js";
8
- import { validateVtConfig } from "../validation.js";
8
+ import { extractFrontMatter, readYaml } from "../common.js";
9
+ import { validateFiniteNumber, validateVtConfig } from "../validation.js";
9
10
  import { resolveReportOptions } from "./report-options.js";
10
11
  import {
11
12
  buildAllRelativePaths,
@@ -17,6 +18,7 @@ import {
17
18
  filterRelativeScreenshotPathsBySelectors,
18
19
  hasSelectors,
19
20
  } from "../selector-filter.js";
21
+ import { stripViewportSuffix } from "../viewport.js";
20
22
 
21
23
  const libraryTemplatesPath = new URL("./templates", import.meta.url).pathname;
22
24
 
@@ -35,6 +37,64 @@ function getAllFiles(dir, fileList = []) {
35
37
  return fileList;
36
38
  }
37
39
 
40
+ function normalizePathForLookup(filePath) {
41
+ return String(filePath)
42
+ .replace(/\\/g, "/")
43
+ .replace(/^\.?\//, "");
44
+ }
45
+
46
+ function toSpecItemKey(relativeSpecPath) {
47
+ const normalized = normalizePathForLookup(relativeSpecPath);
48
+ const ext = path.extname(normalized);
49
+ return normalized.slice(0, normalized.length - ext.length);
50
+ }
51
+
52
+ function toScreenshotItemKey(relativeScreenshotPath) {
53
+ const normalized = normalizePathForLookup(relativeScreenshotPath).replace(/\.webp$/i, "");
54
+ const withoutOrdinal = normalized.replace(/-\d{1,3}$/i, "");
55
+ return stripViewportSuffix(withoutOrdinal);
56
+ }
57
+
58
+ function loadFrontMatterDiffThresholdOverrides(specsDir) {
59
+ const overrides = new Map();
60
+ if (!fs.existsSync(specsDir)) {
61
+ return overrides;
62
+ }
63
+
64
+ const specFiles = getAllFiles(specsDir);
65
+ for (const specFilePath of specFiles) {
66
+ const fileContent = fs.readFileSync(specFilePath, "utf8");
67
+ const { frontMatter } = extractFrontMatter(fileContent);
68
+ if (!frontMatter) {
69
+ continue;
70
+ }
71
+
72
+ const relativePath = path.relative(specsDir, specFilePath);
73
+ const frontMatterData = loadYaml(frontMatter);
74
+ if (
75
+ frontMatterData === null
76
+ || frontMatterData === undefined
77
+ || typeof frontMatterData !== "object"
78
+ || Array.isArray(frontMatterData)
79
+ ) {
80
+ continue;
81
+ }
82
+
83
+ if (frontMatterData.diffThreshold === undefined || frontMatterData.diffThreshold === null) {
84
+ continue;
85
+ }
86
+
87
+ validateFiniteNumber(
88
+ frontMatterData.diffThreshold,
89
+ `${relativePath}: frontMatter.diffThreshold`,
90
+ { min: 0, max: 100 },
91
+ );
92
+ overrides.set(toSpecItemKey(relativePath), frontMatterData.diffThreshold);
93
+ }
94
+
95
+ return overrides;
96
+ }
97
+
38
98
  async function calculateImageHash(imagePath) {
39
99
  const imageBuffer = fs.readFileSync(imagePath);
40
100
  const hash = crypto.createHash("md5").update(imageBuffer).digest("hex");
@@ -137,10 +197,19 @@ async function main(options = {}) {
137
197
  const templatePath = path.join(libraryTemplatesPath, "report.html");
138
198
  const outputPath = path.join(siteOutputPath, "report.html");
139
199
  const jsonReportPath = path.join(".rettangoli", "vt", "report.json");
200
+ const specsDir = path.join(vtPath, "specs");
201
+
202
+ let diffThresholdOverridesBySpec = new Map();
203
+ if (compareMethod === "pixelmatch") {
204
+ diffThresholdOverridesBySpec = loadFrontMatterDiffThresholdOverrides(specsDir);
205
+ }
140
206
 
141
207
  console.log(`Comparison method: ${compareMethod}`);
142
208
  if (compareMethod === "pixelmatch") {
143
209
  console.log(` color threshold: ${colorThreshold}, diff threshold: ${diffThreshold}%`);
210
+ if (diffThresholdOverridesBySpec.size > 0) {
211
+ console.log(` frontmatter diff threshold overrides: ${diffThresholdOverridesBySpec.size}`);
212
+ }
144
213
  }
145
214
 
146
215
  if (!fs.existsSync(originalReferenceDir)) {
@@ -204,6 +273,9 @@ async function main(options = {}) {
204
273
  let error = false;
205
274
  let similarity = null;
206
275
  let diffPixels = null;
276
+ const itemKey = toScreenshotItemKey(relativePath);
277
+ const itemDiffThreshold = diffThresholdOverridesBySpec.get(itemKey);
278
+ const effectiveDiffThreshold = itemDiffThreshold ?? diffThreshold;
207
279
 
208
280
  if (candidateExists && referenceExists) {
209
281
  const diffDirPath = path.dirname(diffPath);
@@ -216,7 +288,7 @@ async function main(options = {}) {
216
288
  referencePath,
217
289
  compareMethod,
218
290
  diffPath,
219
- { colorThreshold, diffThreshold },
291
+ { colorThreshold, diffThreshold: effectiveDiffThreshold },
220
292
  );
221
293
  if (comparison.error) {
222
294
  comparisonErrors.push(
@@ -3,8 +3,8 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/base.css">
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/theme-rtgl-slate.css">
6
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/themes/base.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/themes/theme-rtgl-slate.css">
8
8
  <script>
9
9
  window.rtglIcons = {
10
10
  text: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4 12H20M4 8H20M4 16H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
@@ -40,7 +40,7 @@
40
40
  }
41
41
  </script>
42
42
  <script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js"></script>
43
- <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/rettangoli-iife-ui.min.js"></script>
43
+ <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/rettangoli-iife-ui.min.js"></script>
44
44
  <script src="/public/main.js"></script>
45
45
  </head>
46
46
  <body class="dark">
@@ -4,8 +4,8 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/base.css">
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/theme-rtgl-slate.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/themes/base.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/themes/theme-rtgl-slate.css">
9
9
  <script>
10
10
  window.addEventListener('DOMContentLoaded', () => {
11
11
  if (location.hash) {
@@ -17,7 +17,7 @@
17
17
  });
18
18
  </script>
19
19
  <script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js"></script>
20
- <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/rettangoli-iife-ui.min.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/rettangoli-iife-ui.min.js"></script>
21
21
 
22
22
  <style>
23
23
  pre {
@@ -4,10 +4,10 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/base.css">
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/themes/theme-rtgl-slate.css">
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/themes/base.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/themes/theme-rtgl-slate.css">
9
9
  <script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js"></script>
10
- <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc6/dist/rettangoli-iife-ui.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc15/dist/rettangoli-iife-ui.min.js"></script>
11
11
  <style>
12
12
  code {
13
13
  white-space: pre-wrap;