@rettangoli/vt 1.0.2 → 1.0.4

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
@@ -21,6 +21,7 @@ Behavior split:
21
21
  - `--concurrency <number>`
22
22
  - `--timeout <ms>`
23
23
  - `--wait-event <name>`
24
+ - `--isolation <fast|strict>`
24
25
  - `--folder <path>` (repeatable)
25
26
  - `--group <section-key>` (repeatable)
26
27
  - `--item <spec-path>` (repeatable)
@@ -69,7 +70,7 @@ rtgl vt report --group components-basic
69
70
  rtgl vt report --item components/forms/login
70
71
  ```
71
72
 
72
- Everything else in capture is internal and intentionally not user-configurable.
73
+ Other capture tuning remains internal and intentionally not user-configurable.
73
74
 
74
75
  ## Config
75
76
 
@@ -85,6 +86,7 @@ vt:
85
86
  concurrency: 4
86
87
  timeout: 30000
87
88
  waitEvent: vt:ready
89
+ isolationMode: strict
88
90
  viewport:
89
91
  id: desktop
90
92
  width: 1280
@@ -99,6 +101,8 @@ Notes:
99
101
  - `vt.sections` is required.
100
102
  - `vt.service` is optional. When set, VT starts the command before capture, waits for `vt.url`, then stops it after capture.
101
103
  - when `vt.service` is omitted and `vt.url` is set, VT expects that URL to already be running.
104
+ - `vt.isolationMode` is optional: `fast` or `strict`. Default is `fast`.
105
+ - `strict` uses a fresh browser context per captured spec and is recommended for real app routes or IndexedDB-backed state.
102
106
  - Section page keys are derived as `kebab-case(title)` for flat sections and group `items[].title`.
103
107
  - Derived section page keys must be unique case-insensitively.
104
108
  - `vt.viewport` supports object or array; each viewport requires `id`, `width`, `height`.
@@ -142,15 +146,15 @@ Screenshot naming:
142
146
  A pre-built Docker image with `rtgl` and Playwright browsers is available:
143
147
 
144
148
  ```bash
145
- docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27
149
+ docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12
146
150
  ```
147
151
 
148
152
  Run commands against a local project:
149
153
 
150
154
  ```bash
151
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27 rtgl vt screenshot
152
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27 rtgl vt report
153
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27 rtgl vt accept
155
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12 rtgl vt screenshot
156
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12 rtgl vt report
157
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12 rtgl vt accept
154
158
  ```
155
159
 
156
160
  Note:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/vt",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Rettangoli Visual Testing",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,6 +11,7 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
11
11
  concurrency: cliConcurrency,
12
12
  timeout: cliTimeout,
13
13
  waitEvent: cliWaitEvent,
14
+ isolation: cliIsolation,
14
15
  folder: cliFolder,
15
16
  group: cliGroup,
16
17
  item: cliItem,
@@ -20,6 +21,7 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
20
21
 
21
22
  const waitEvent = cliWaitEvent ?? configData.waitEvent;
22
23
  const timeout = cliTimeout ?? configData.timeout ?? 30000;
24
+ const isolationMode = cliIsolation ?? configData.isolationMode ?? "fast";
23
25
  const selectors = normalizeSelectors({
24
26
  folder: cliFolder,
25
27
  group: cliGroup,
@@ -52,7 +54,7 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
52
54
  screenshotWaitTime: 0,
53
55
  waitStrategy: waitEvent ? "event" : "load",
54
56
  workerCount: cliConcurrency ?? configData.concurrency ?? undefined, // adaptive worker planning
55
- isolationMode: "fast",
57
+ isolationMode,
56
58
  navigationTimeout: timeout,
57
59
  readyTimeout: timeout,
58
60
  screenshotTimeout: timeout,
@@ -84,6 +86,11 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
84
86
  throw new Error(`Invalid waitEvent: expected a non-empty string, got ${typeof resolvedOptions.waitEvent}.`);
85
87
  }
86
88
  }
89
+ if (!["fast", "strict"].includes(resolvedOptions.isolationMode)) {
90
+ throw new Error(
91
+ `Invalid isolation: expected "fast" or "strict", got "${resolvedOptions.isolationMode}".`,
92
+ );
93
+ }
87
94
  if (resolvedOptions.configUrl !== undefined && resolvedOptions.configUrl !== null) {
88
95
  if (typeof resolvedOptions.configUrl !== "string" || resolvedOptions.configUrl.trim().length === 0) {
89
96
  throw new Error(`Invalid url: expected a non-empty string, got ${typeof resolvedOptions.configUrl}.`);
@@ -60,8 +60,8 @@
60
60
  {% for file in files %}
61
61
  <rtgl-view w="f">
62
62
  <a style="display: contents; text-decoration: none; color: inherit;"
63
- href="#{{ file.frontMatter.title | slug }}">
64
- <rtgl-text id="{{ file.frontMatter.title | slug }}" s="h3">{{ file.frontMatter.title | default: file.path
63
+ href="#{{ file.anchorId }}">
64
+ <rtgl-text id="{{ file.anchorId }}" s="h3">{{ file.frontMatter.title | default: file.path
65
65
  }}</rtgl-text>
66
66
  </a>
67
67
 
package/src/common.js CHANGED
@@ -15,7 +15,7 @@ import path from "path";
15
15
  import { validateFiniteNumber, validateFrontMatter } from "./validation.js";
16
16
  import { createCaptureTasks } from "./capture/spec-loader.js";
17
17
  import { runCaptureScheduler } from "./capture/capture-scheduler.js";
18
- import { deriveSectionPageKey } from "./section-page-key.js";
18
+ import { deriveAnchorId, deriveSectionPageKey } from "./section-page-key.js";
19
19
 
20
20
  const removeExtension = (filePath) => filePath.replace(/\.[^/.]+$/, "");
21
21
 
@@ -44,10 +44,9 @@ async function readYaml(filePath) {
44
44
  }
45
45
  }
46
46
 
47
- // Add custom filter to convert string to lowercase and replace spaces with hyphens
48
- engine.registerFilter("slug", (value) => {
49
- if (typeof value !== "string") return "";
50
- return value.toLowerCase().replace(/\s+/g, "-");
47
+ // Add custom filter to derive slug-safe ids for URLs and anchors.
48
+ engine.registerFilter("slug", (value, fallbackValue) => {
49
+ return deriveAnchorId(value, fallbackValue);
51
50
  });
52
51
 
53
52
  // Add custom filter to remove file extension
@@ -416,13 +415,18 @@ function generateOverview(data, templatePath, outputPath, configData) {
416
415
  try {
417
416
  renderedContent = engine.parseAndRenderSync(templateContent, {
418
417
  ...configData,
419
- files: data.filter((file) => {
420
- const filePath = path.normalize(file.path);
421
- const sectionPath = path.normalize(section.files);
422
- // Check if file is in the folder or any subfolder
423
- const fileDir = path.dirname(filePath);
424
- return fileDir === sectionPath || fileDir.startsWith(sectionPath + path.sep);
425
- }),
418
+ files: data
419
+ .filter((file) => {
420
+ const filePath = path.normalize(file.path);
421
+ const sectionPath = path.normalize(section.files);
422
+ // Check if file is in the folder or any subfolder
423
+ const fileDir = path.dirname(filePath);
424
+ return fileDir === sectionPath || fileDir.startsWith(sectionPath + path.sep);
425
+ })
426
+ .map((file) => ({
427
+ ...file,
428
+ anchorId: deriveAnchorId(file.frontMatter?.title, removeExtension(file.path)),
429
+ })),
426
430
  currentSection: section,
427
431
  sidebarItems: encodeURIComponent(JSON.stringify(sidebarItems)),
428
432
  });
@@ -1,11 +1,11 @@
1
1
  import fs from "fs";
2
2
  import { Liquid } from "liquidjs";
3
+ import { deriveAnchorId } from "../section-page-key.js";
3
4
 
4
5
  const engine = new Liquid();
5
6
 
6
- engine.registerFilter("slug", (value) => {
7
- if (typeof value !== "string") return "";
8
- return value.toLowerCase().replace(/\s+/g, "-");
7
+ engine.registerFilter("slug", (value, fallbackValue) => {
8
+ return deriveAnchorId(value, fallbackValue);
9
9
  });
10
10
 
11
11
  export async function renderHtmlReport({ results, templatePath, outputPath }) {
@@ -5,10 +5,18 @@ function normalizeString(value) {
5
5
  return value.trim();
6
6
  }
7
7
 
8
- export function deriveSectionPageKey(sectionLike) {
9
- return normalizeString(sectionLike?.title)
8
+ export function derivePageKey(value) {
9
+ return normalizeString(value)
10
10
  .toLowerCase()
11
11
  .replace(/[^a-z0-9]+/g, "-")
12
12
  .replace(/-+/g, "-")
13
13
  .replace(/^-+|-+$/g, "");
14
14
  }
15
+
16
+ export function deriveSectionPageKey(sectionLike) {
17
+ return derivePageKey(sectionLike?.title) || derivePageKey(sectionLike?.files);
18
+ }
19
+
20
+ export function deriveAnchorId(value, fallbackValue = "") {
21
+ return derivePageKey(value) || derivePageKey(fallbackValue);
22
+ }
package/src/validation.js CHANGED
@@ -112,7 +112,6 @@ const LEGACY_CAPTURE_FIELDS = {
112
112
  waitSelector: true,
113
113
  waitStrategy: true,
114
114
  workerCount: true,
115
- isolationMode: true,
116
115
  navigationTimeout: true,
117
116
  readyTimeout: true,
118
117
  screenshotTimeout: true,
@@ -136,7 +135,7 @@ function assertDerivableSectionPageKey(sectionLike, path) {
136
135
  const pageKey = deriveSectionPageKey(sectionLike);
137
136
  assert(
138
137
  pageKey.length > 0,
139
- `"${path}" must contain at least one letter or number.`,
138
+ `"${path}" must produce a page key from title or files.`,
140
139
  );
141
140
  }
142
141
 
@@ -169,14 +168,14 @@ function validateSection(section, index) {
169
168
 
170
169
  assert(typeof item.title === "string" && item.title.trim().length > 0, `"${itemPath}.title" is required.`);
171
170
  assert(typeof item.files === "string" && item.files.trim().length > 0, `"${itemPath}.files" is required.`);
172
- assertDerivableSectionPageKey(item, `${itemPath}.title`);
171
+ assertDerivableSectionPageKey(item, itemPath);
173
172
  });
174
173
  return;
175
174
  }
176
175
 
177
176
  validateOptionalString(section.files, `${sectionPath}.files`);
178
177
  assert(typeof section.files === "string" && section.files.trim().length > 0, `"${sectionPath}.files" is required.`);
179
- assertDerivableSectionPageKey(section, `${sectionPath}.title`);
178
+ assertDerivableSectionPageKey(section, sectionPath);
180
179
  }
181
180
 
182
181
  function collectSectionPageKeys(vtConfig) {
@@ -557,6 +556,7 @@ export function validateVtConfig(vtConfig, sourcePath = "rettangoli.config.yaml"
557
556
  validateOptionalNumber(vtConfig.concurrency, "vt.concurrency", { integer: true, min: 1 });
558
557
  validateOptionalNumber(vtConfig.timeout, "vt.timeout", { integer: true, min: 1 });
559
558
  validateOptionalString(vtConfig.waitEvent, "vt.waitEvent");
559
+ validateOptionalEnum(vtConfig.isolationMode, "vt.isolationMode", ["fast", "strict"]);
560
560
  normalizeViewportField(vtConfig.viewport, "vt.viewport");
561
561
  validateOptionalNumber(vtConfig.colorThreshold, "vt.colorThreshold", { min: 0, max: 1 });
562
562
  validateOptionalNumber(vtConfig.diffThreshold, "vt.diffThreshold", { min: 0, max: 100 });