@rettangoli/vt 0.0.14 → 1.0.0-rc12

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.
@@ -0,0 +1,76 @@
1
+ import path from "path";
2
+
3
+ export function extractParts(filePath) {
4
+ const dir = path.dirname(filePath);
5
+ const filename = path.basename(filePath, ".webp");
6
+ const lastHyphenIndex = filename.lastIndexOf("-");
7
+
8
+ if (lastHyphenIndex > -1) {
9
+ const suffix = filename.substring(lastHyphenIndex + 1);
10
+ if (/^\d+$/.test(suffix)) {
11
+ const number = parseInt(suffix, 10);
12
+ const name = path.join(dir, filename.substring(0, lastHyphenIndex));
13
+ return { name, number };
14
+ }
15
+ }
16
+
17
+ return { name: path.join(dir, filename), number: -1 };
18
+ }
19
+
20
+ export function sortPaths(a, b) {
21
+ const partsA = extractParts(a);
22
+ const partsB = extractParts(b);
23
+
24
+ if (partsA.name < partsB.name) return -1;
25
+ if (partsA.name > partsB.name) return 1;
26
+
27
+ return partsA.number - partsB.number;
28
+ }
29
+
30
+ export function buildAllRelativePaths(candidateRelativePaths, referenceRelativePaths) {
31
+ const allPaths = [
32
+ ...new Set([...candidateRelativePaths, ...referenceRelativePaths]),
33
+ ];
34
+ allPaths.sort(sortPaths);
35
+ return allPaths;
36
+ }
37
+
38
+ export function toMismatchingItems(results, siteOutputPath) {
39
+ return results
40
+ .filter(
41
+ (result) =>
42
+ !result.equal || result.onlyInCandidate || result.onlyInReference,
43
+ )
44
+ .map((result) => {
45
+ return {
46
+ candidatePath: result.candidatePath
47
+ ? path.relative(siteOutputPath, result.candidatePath)
48
+ : null,
49
+ referencePath: result.referencePath
50
+ ? path.relative(siteOutputPath, result.referencePath)
51
+ : null,
52
+ equal: result.equal,
53
+ similarity: result.similarity,
54
+ diffPixels: result.diffPixels,
55
+ onlyInCandidate: result.onlyInCandidate,
56
+ onlyInReference: result.onlyInReference,
57
+ };
58
+ });
59
+ }
60
+
61
+ export function buildJsonReport({ total, mismatchingItems, timestamp = new Date().toISOString() }) {
62
+ return {
63
+ timestamp,
64
+ total,
65
+ mismatched: mismatchingItems.length,
66
+ items: mismatchingItems.map((item) => ({
67
+ path: item.candidatePath || item.referencePath,
68
+ candidatePath: item.candidatePath,
69
+ referencePath: item.referencePath,
70
+ equal: item.equal,
71
+ similarity: item.similarity,
72
+ onlyInCandidate: item.onlyInCandidate,
73
+ onlyInReference: item.onlyInReference,
74
+ })),
75
+ };
76
+ }
@@ -0,0 +1,22 @@
1
+ import fs from "fs";
2
+ import { Liquid } from "liquidjs";
3
+
4
+ const engine = new Liquid();
5
+
6
+ engine.registerFilter("slug", (value) => {
7
+ if (typeof value !== "string") return "";
8
+ return value.toLowerCase().replace(/\s+/g, "-");
9
+ });
10
+
11
+ export async function renderHtmlReport({ results, templatePath, outputPath }) {
12
+ try {
13
+ const templateContent = fs.readFileSync(templatePath, "utf8");
14
+ const renderedHtml = await engine.parseAndRender(templateContent, {
15
+ files: results,
16
+ });
17
+ fs.writeFileSync(outputPath, renderedHtml);
18
+ console.log(`Report generated successfully at ${outputPath}`);
19
+ } catch (error) {
20
+ throw new Error(`Failed to generate HTML report: ${error.message}`, { cause: error });
21
+ }
22
+ }
@@ -0,0 +1,14 @@
1
+ function normalizeString(value) {
2
+ if (typeof value !== "string") {
3
+ return "";
4
+ }
5
+ return value.trim();
6
+ }
7
+
8
+ export function deriveSectionPageKey(sectionLike) {
9
+ return normalizeString(sectionLike?.title)
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, "-")
12
+ .replace(/-+/g, "-")
13
+ .replace(/^-+|-+$/g, "");
14
+ }
@@ -0,0 +1,140 @@
1
+ import path from "path";
2
+ import { stripViewportSuffix } from "./viewport.js";
3
+ import { deriveSectionPageKey } from "./section-page-key.js";
4
+
5
+ function toList(value) {
6
+ if (value === undefined || value === null) return [];
7
+ if (Array.isArray(value)) return value;
8
+ return [value];
9
+ }
10
+
11
+ function normalizePathValue(value) {
12
+ return String(value)
13
+ .trim()
14
+ .replace(/\\/g, "/")
15
+ .replace(/^\.?\//, "")
16
+ .replace(/\/+$/, "");
17
+ }
18
+
19
+ function normalizeItemKey(value) {
20
+ const normalized = normalizePathValue(value);
21
+ return normalized.replace(/\.(html?|ya?ml|md)$/i, "");
22
+ }
23
+
24
+ export function normalizeSelectors(raw = {}) {
25
+ const folders = toList(raw.folder)
26
+ .map(normalizePathValue)
27
+ .filter((item) => item.length > 0);
28
+ const groups = toList(raw.group)
29
+ .map((item) => deriveSectionPageKey({ title: String(item) }))
30
+ .filter((item) => item.length > 0);
31
+ const items = toList(raw.item)
32
+ .map(normalizeItemKey)
33
+ .filter((item) => item.length > 0);
34
+
35
+ return {
36
+ folders: [...new Set(folders)],
37
+ groups: [...new Set(groups)],
38
+ items: [...new Set(items)],
39
+ };
40
+ }
41
+
42
+ export function hasSelectors(selectors) {
43
+ return (
44
+ selectors.folders.length > 0
45
+ || selectors.groups.length > 0
46
+ || selectors.items.length > 0
47
+ );
48
+ }
49
+
50
+ function matchesFolderPrefix(filePath, folderPrefix) {
51
+ return filePath === folderPrefix || filePath.startsWith(`${folderPrefix}/`);
52
+ }
53
+
54
+ function resolveGroupFolders(configSections = [], groupSelectors = []) {
55
+ if (groupSelectors.length === 0) {
56
+ return [];
57
+ }
58
+
59
+ const groupFolderMap = new Map();
60
+ for (const section of configSections) {
61
+ if (section.type === "groupLabel" && Array.isArray(section.items)) {
62
+ for (const item of section.items) {
63
+ groupFolderMap.set(deriveSectionPageKey(item), normalizePathValue(item.files));
64
+ }
65
+ continue;
66
+ }
67
+ if (section.files) {
68
+ groupFolderMap.set(deriveSectionPageKey(section), normalizePathValue(section.files));
69
+ }
70
+ }
71
+
72
+ const missing = [];
73
+ const folders = [];
74
+ for (const selector of groupSelectors) {
75
+ const folder = groupFolderMap.get(selector);
76
+ if (!folder) {
77
+ missing.push(selector);
78
+ continue;
79
+ }
80
+ folders.push(folder);
81
+ }
82
+ if (missing.length > 0) {
83
+ throw new Error(
84
+ `Unknown group selector(s): ${missing.join(", ")}.`,
85
+ );
86
+ }
87
+ return [...new Set(folders)];
88
+ }
89
+
90
+ function toGeneratedFileItemKey(filePath) {
91
+ const normalized = normalizePathValue(filePath);
92
+ const ext = path.extname(normalized);
93
+ return normalized.slice(0, normalized.length - ext.length);
94
+ }
95
+
96
+ export function filterGeneratedFilesBySelectors(generatedFiles, selectors, configSections = []) {
97
+ if (!hasSelectors(selectors)) {
98
+ return generatedFiles;
99
+ }
100
+
101
+ const folderSelectors = [
102
+ ...selectors.folders,
103
+ ...resolveGroupFolders(configSections, selectors.groups),
104
+ ];
105
+
106
+ return generatedFiles.filter((file) => {
107
+ const normalizedPath = normalizePathValue(file.path);
108
+ const itemKey = toGeneratedFileItemKey(normalizedPath);
109
+
110
+ if (selectors.items.includes(itemKey)) {
111
+ return true;
112
+ }
113
+ return folderSelectors.some((folder) => matchesFolderPrefix(normalizedPath, folder));
114
+ });
115
+ }
116
+
117
+ function toScreenshotItemKey(relativeScreenshotPath) {
118
+ const normalized = normalizePathValue(relativeScreenshotPath).replace(/\.webp$/i, "");
119
+ const withoutOrdinal = normalized.replace(/-\d{1,3}$/i, "");
120
+ return stripViewportSuffix(withoutOrdinal);
121
+ }
122
+
123
+ export function filterRelativeScreenshotPathsBySelectors(relativePaths, selectors, configSections = []) {
124
+ if (!hasSelectors(selectors)) {
125
+ return relativePaths;
126
+ }
127
+
128
+ const folderSelectors = [
129
+ ...selectors.folders,
130
+ ...resolveGroupFolders(configSections, selectors.groups),
131
+ ];
132
+
133
+ return relativePaths.filter((relativePath) => {
134
+ const itemKey = toScreenshotItemKey(relativePath);
135
+ if (selectors.items.includes(itemKey)) {
136
+ return true;
137
+ }
138
+ return folderSelectors.some((folder) => matchesFolderPrefix(itemKey, folder));
139
+ });
140
+ }
@@ -0,0 +1,37 @@
1
+ export const STEP_COMMANDS = Object.freeze([
2
+ "blur",
3
+ "check",
4
+ "clear",
5
+ "click",
6
+ "customEvent",
7
+ "dblclick",
8
+ "focus",
9
+ "goto",
10
+ "hover",
11
+ "keypress",
12
+ "mouseDown",
13
+ "mouseUp",
14
+ "move",
15
+ "rclick",
16
+ "rightMouseDown",
17
+ "rightMouseUp",
18
+ "setViewport",
19
+ "screenshot",
20
+ "scroll",
21
+ "select",
22
+ "selectOption",
23
+ "uncheck",
24
+ "upload",
25
+ "wait",
26
+ "waitFor",
27
+ "write",
28
+ ]);
29
+
30
+ export const STRUCTURED_STEP_KEYS = Object.freeze([
31
+ "action",
32
+ "assert",
33
+ ]);
34
+
35
+ export const BLOCK_COMMANDS = Object.freeze([
36
+ "select",
37
+ ]);