@rettangoli/vt 1.0.1 → 1.0.3
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 +5 -4
- package/package.json +1 -1
- package/src/cli/templates/index.html +2 -2
- package/src/common.js +16 -12
- package/src/createSteps.js +46 -15
- package/src/report/report-render.js +3 -3
- package/src/section-page-key.js +10 -2
- package/src/validation.js +13 -6
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ Step action reference:
|
|
|
126
126
|
|
|
127
127
|
- `docs/step-actions.md`
|
|
128
128
|
- canonical format is structured action objects (`- action: ...`); legacy one-line string steps are not supported.
|
|
129
|
+
- `action: select` accepts exactly one of `testId` or `selector` for interaction targeting.
|
|
129
130
|
- `assert` supports `js` deep-equal checks for object/array values.
|
|
130
131
|
|
|
131
132
|
Screenshot naming:
|
|
@@ -141,15 +142,15 @@ Screenshot naming:
|
|
|
141
142
|
A pre-built Docker image with `rtgl` and Playwright browsers is available:
|
|
142
143
|
|
|
143
144
|
```bash
|
|
144
|
-
docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.
|
|
145
|
+
docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10
|
|
145
146
|
```
|
|
146
147
|
|
|
147
148
|
Run commands against a local project:
|
|
148
149
|
|
|
149
150
|
```bash
|
|
150
|
-
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.
|
|
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.
|
|
151
|
+
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt screenshot
|
|
152
|
+
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt report
|
|
153
|
+
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt accept
|
|
153
154
|
```
|
|
154
155
|
|
|
155
156
|
Note:
|
package/package.json
CHANGED
|
@@ -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
|
});
|
package/src/createSteps.js
CHANGED
|
@@ -180,6 +180,27 @@ function requireStructuredString(stepObject, key, actionName) {
|
|
|
180
180
|
return value;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
function resolveStructuredSelectTarget(stepObject, actionName) {
|
|
184
|
+
const hasTestId = Object.prototype.hasOwnProperty.call(stepObject, "testId");
|
|
185
|
+
const hasSelector = Object.prototype.hasOwnProperty.call(stepObject, "selector");
|
|
186
|
+
|
|
187
|
+
if (hasTestId === hasSelector) {
|
|
188
|
+
throw new Error(`Structured action "${actionName}" requires exactly one of \`testId\` or \`selector\`.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (hasTestId) {
|
|
192
|
+
return {
|
|
193
|
+
type: "testId",
|
|
194
|
+
value: requireStructuredString(stepObject, "testId", actionName),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
type: "selector",
|
|
200
|
+
value: requireStructuredString(stepObject, "selector", actionName),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
183
204
|
function requireStructuredNumber(stepObject, key, actionName) {
|
|
184
205
|
const value = stepObject[key];
|
|
185
206
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -224,13 +245,13 @@ function normalizeStructuredActionStep(stepObject) {
|
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
if (action === "select") {
|
|
227
|
-
assertStructuredKeys(stepObject, new Set(["action", "testId", "steps"]), action);
|
|
228
|
-
const
|
|
248
|
+
assertStructuredKeys(stepObject, new Set(["action", "testId", "selector", "steps"]), action);
|
|
249
|
+
const target = resolveStructuredSelectTarget(stepObject, action);
|
|
229
250
|
if (!Array.isArray(stepObject.steps)) {
|
|
230
251
|
throw new Error('Structured action "select" requires array `steps`.');
|
|
231
252
|
}
|
|
232
253
|
const nestedSteps = stepObject.steps.map((nestedStep) => normalizeStepValue(nestedStep));
|
|
233
|
-
return { kind: "block", command: "select", args: [
|
|
254
|
+
return { kind: "block", command: "select", args: [`${target.type}=${target.value}`], nestedSteps };
|
|
234
255
|
}
|
|
235
256
|
|
|
236
257
|
if (action === "click" || action === "dblclick" || action === "hover" || action === "rclick") {
|
|
@@ -828,22 +849,32 @@ async function assertStructured(page, assertionConfig, selectedElement) {
|
|
|
828
849
|
}
|
|
829
850
|
|
|
830
851
|
async function select(page, args) {
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
)
|
|
840
|
-
|
|
852
|
+
const { named, positional } = parseNamedArgs(args);
|
|
853
|
+
const testId = typeof named.testId === "string" && named.testId.length > 0
|
|
854
|
+
? named.testId
|
|
855
|
+
: positional[0];
|
|
856
|
+
const selector = typeof named.selector === "string" && named.selector.length > 0
|
|
857
|
+
? named.selector
|
|
858
|
+
: undefined;
|
|
859
|
+
|
|
860
|
+
if ((testId ? 1 : 0) + (selector ? 1 : 0) !== 1) {
|
|
861
|
+
throw new Error("`select` requires exactly one target: `testId` or `selector`.");
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const hostElementLocator = selector
|
|
865
|
+
? page.locator(selector)
|
|
866
|
+
: page.getByTestId(testId);
|
|
867
|
+
|
|
868
|
+
const interactiveElementLocator = hostElementLocator
|
|
869
|
+
.locator('input, textarea, button, select, a')
|
|
870
|
+
.first();
|
|
871
|
+
|
|
841
872
|
const count = await interactiveElementLocator.count();
|
|
842
|
-
|
|
873
|
+
|
|
843
874
|
if (count > 0) {
|
|
844
875
|
return interactiveElementLocator;
|
|
845
876
|
}
|
|
846
|
-
|
|
877
|
+
|
|
847
878
|
return hostElementLocator;
|
|
848
879
|
}
|
|
849
880
|
|
|
@@ -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
|
@@ -136,7 +136,7 @@ function assertDerivableSectionPageKey(sectionLike, path) {
|
|
|
136
136
|
const pageKey = deriveSectionPageKey(sectionLike);
|
|
137
137
|
assert(
|
|
138
138
|
pageKey.length > 0,
|
|
139
|
-
`"${path}" must
|
|
139
|
+
`"${path}" must produce a page key from title or files.`,
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -169,14 +169,14 @@ function validateSection(section, index) {
|
|
|
169
169
|
|
|
170
170
|
assert(typeof item.title === "string" && item.title.trim().length > 0, `"${itemPath}.title" is required.`);
|
|
171
171
|
assert(typeof item.files === "string" && item.files.trim().length > 0, `"${itemPath}.files" is required.`);
|
|
172
|
-
assertDerivableSectionPageKey(item,
|
|
172
|
+
assertDerivableSectionPageKey(item, itemPath);
|
|
173
173
|
});
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
validateOptionalString(section.files, `${sectionPath}.files`);
|
|
178
178
|
assert(typeof section.files === "string" && section.files.trim().length > 0, `"${sectionPath}.files" is required.`);
|
|
179
|
-
assertDerivableSectionPageKey(section,
|
|
179
|
+
assertDerivableSectionPageKey(section, sectionPath);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function collectSectionPageKeys(vtConfig) {
|
|
@@ -273,11 +273,18 @@ function validateStructuredActionStep(step, stepPath) {
|
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
if (action === "select") {
|
|
276
|
-
assertNoUnknownStepKeys(step, stepPath, new Set(["action", "testId", "steps"]));
|
|
276
|
+
assertNoUnknownStepKeys(step, stepPath, new Set(["action", "testId", "selector", "steps"]));
|
|
277
277
|
validateOptionalString(step.testId, `${stepPath}.testId`);
|
|
278
|
+
validateOptionalString(step.selector, `${stepPath}.selector`);
|
|
278
279
|
assert(
|
|
279
|
-
|
|
280
|
-
|
|
280
|
+
(
|
|
281
|
+
typeof step.testId === "string"
|
|
282
|
+
&& step.testId.trim().length > 0
|
|
283
|
+
) !== (
|
|
284
|
+
typeof step.selector === "string"
|
|
285
|
+
&& step.selector.trim().length > 0
|
|
286
|
+
),
|
|
287
|
+
`"${stepPath}" for action=select requires exactly one of "testId" or "selector".`,
|
|
281
288
|
);
|
|
282
289
|
assert(Array.isArray(step.steps), `"${stepPath}.steps" must be an array for action=select.`);
|
|
283
290
|
step.steps.forEach((nestedStep, nestedIndex) => {
|