@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 +9 -5
- package/package.json +1 -1
- package/src/cli/generate-options.js +8 -1
- package/src/cli/templates/index.html +2 -2
- package/src/common.js +16 -12
- package/src/report/report-render.js +3 -3
- package/src/section-page-key.js +10 -2
- package/src/validation.js +4 -4
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
|
-
|
|
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.
|
|
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.
|
|
152
|
-
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.
|
|
153
|
-
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.
|
|
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
|
@@ -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
|
|
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.
|
|
64
|
-
<rtgl-text id="{{ file.
|
|
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
|
|
48
|
-
engine.registerFilter("slug", (value) => {
|
|
49
|
-
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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 }) {
|
package/src/section-page-key.js
CHANGED
|
@@ -5,10 +5,18 @@ function normalizeString(value) {
|
|
|
5
5
|
return value.trim();
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function
|
|
9
|
-
return normalizeString(
|
|
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
|
|
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,
|
|
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,
|
|
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 });
|