@rettangoli/vt 1.0.0-rc5 → 1.0.1
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 +16 -12
- package/bun.lock +125 -0
- package/package.json +6 -1
- package/src/capture/playwright-runner.js +6 -4
- package/src/cli/report.js +75 -3
- package/src/cli/templates/default.html +3 -3
- package/src/cli/templates/index.html +3 -3
- package/src/cli/templates/report.html +3 -3
- package/src/common.js +2 -1
- package/src/createSteps.js +2 -11
- package/src/section-page-key.js +14 -0
- package/src/selector-filter.js +4 -3
- package/src/validation.js +14 -26
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Behavior split:
|
|
|
39
39
|
Use selectors to run only part of VT in both `screenshot` and `report`:
|
|
40
40
|
|
|
41
41
|
- `folder`: matches specs by folder prefix under `vt/specs` (example: `components/forms`)
|
|
42
|
-
- `group`: matches section page key from `vt.sections` (`title`
|
|
42
|
+
- `group`: matches derived section page key from `vt.sections` titles (`kebab-case(title)`)
|
|
43
43
|
- `item`: matches a single spec path relative to `vt/specs` (with or without extension)
|
|
44
44
|
|
|
45
45
|
Selector rules:
|
|
@@ -54,18 +54,18 @@ Examples:
|
|
|
54
54
|
rtgl vt screenshot --folder components/forms
|
|
55
55
|
|
|
56
56
|
# Only one section/group key from vt.sections
|
|
57
|
-
rtgl vt screenshot --group
|
|
57
|
+
rtgl vt screenshot --group components-basic
|
|
58
58
|
|
|
59
59
|
# Only one spec item (extension optional)
|
|
60
60
|
rtgl vt screenshot --item components/forms/login
|
|
61
61
|
rtgl vt screenshot --item components/forms/login.html
|
|
62
62
|
|
|
63
63
|
# Combine selectors (union)
|
|
64
|
-
rtgl vt screenshot --group
|
|
64
|
+
rtgl vt screenshot --group components-basic --item pages/home
|
|
65
65
|
|
|
66
66
|
# Same selectors for report
|
|
67
67
|
rtgl vt report --folder components/forms
|
|
68
|
-
rtgl vt report --group
|
|
68
|
+
rtgl vt report --group components-basic
|
|
69
69
|
rtgl vt report --item components/forms/login
|
|
70
70
|
```
|
|
71
71
|
|
|
@@ -90,7 +90,7 @@ vt:
|
|
|
90
90
|
width: 1280
|
|
91
91
|
height: 720
|
|
92
92
|
sections:
|
|
93
|
-
- title:
|
|
93
|
+
- title: Components Basic
|
|
94
94
|
files: components
|
|
95
95
|
```
|
|
96
96
|
|
|
@@ -99,7 +99,8 @@ Notes:
|
|
|
99
99
|
- `vt.sections` is required.
|
|
100
100
|
- `vt.service` is optional. When set, VT starts the command before capture, waits for `vt.url`, then stops it after capture.
|
|
101
101
|
- when `vt.service` is omitted and `vt.url` is set, VT expects that URL to already be running.
|
|
102
|
-
- Section page keys (
|
|
102
|
+
- Section page keys are derived as `kebab-case(title)` for flat sections and group `items[].title`.
|
|
103
|
+
- Derived section page keys must be unique case-insensitively.
|
|
103
104
|
- `vt.viewport` supports object or array; each viewport requires `id`, `width`, `height`.
|
|
104
105
|
- `vt.capture` is internal and must be omitted.
|
|
105
106
|
- Viewport contract details: `docs/viewport-contract.md`.
|
|
@@ -117,18 +118,21 @@ Supported frontmatter keys per spec file:
|
|
|
117
118
|
- `waitStrategy` (`networkidle` | `load` | `event` | `selector`)
|
|
118
119
|
- `viewport` (object or array of viewport objects)
|
|
119
120
|
- `skipScreenshot`
|
|
121
|
+
- `skipInitialScreenshot`
|
|
120
122
|
- `specs`
|
|
121
123
|
- `steps`
|
|
122
124
|
|
|
123
125
|
Step action reference:
|
|
124
126
|
|
|
125
127
|
- `docs/step-actions.md`
|
|
126
|
-
- canonical format is structured action objects (`- action: ...`)
|
|
128
|
+
- canonical format is structured action objects (`- action: ...`); legacy one-line string steps are not supported.
|
|
127
129
|
- `assert` supports `js` deep-equal checks for object/array values.
|
|
128
130
|
|
|
129
131
|
Screenshot naming:
|
|
130
132
|
|
|
131
|
-
-
|
|
133
|
+
- By default, VT takes an immediate first screenshot before running `steps`.
|
|
134
|
+
- Set `skipInitialScreenshot: true` in frontmatter to skip that immediate first screenshot.
|
|
135
|
+
- First captured screenshot is `-01`.
|
|
132
136
|
- Then `-02`, `-03`, up to `-99`.
|
|
133
137
|
- When viewport id is configured, filenames include `--<viewportId>` before ordinal (for example `pages/home--mobile-01.webp`).
|
|
134
138
|
|
|
@@ -137,15 +141,15 @@ Screenshot naming:
|
|
|
137
141
|
A pre-built Docker image with `rtgl` and Playwright browsers is available:
|
|
138
142
|
|
|
139
143
|
```bash
|
|
140
|
-
docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-
|
|
144
|
+
docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27
|
|
141
145
|
```
|
|
142
146
|
|
|
143
147
|
Run commands against a local project:
|
|
144
148
|
|
|
145
149
|
```bash
|
|
146
|
-
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-
|
|
147
|
-
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-
|
|
148
|
-
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-
|
|
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
|
|
149
153
|
```
|
|
150
154
|
|
|
151
155
|
Note:
|
package/bun.lock
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "rviz",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"commander": "^13.1.0",
|
|
8
|
+
"js-yaml": "^4.1.0",
|
|
9
|
+
"liquidjs": "^10.21.0",
|
|
10
|
+
"pixelmatch": "^7.1.0",
|
|
11
|
+
"playwright": "^1.52.0",
|
|
12
|
+
"shiki": "^3.3.0"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"packages": {
|
|
17
|
+
"@shikijs/core": ["@shikijs/core@3.3.0", "", { "dependencies": { "@shikijs/types": "3.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-CovkFL2WVaHk6PCrwv6ctlmD4SS1qtIfN8yEyDXDYWh4ONvomdM9MaFw20qHuqJOcb8/xrkqoWQRJ//X10phOQ=="],
|
|
18
|
+
|
|
19
|
+
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.3.0", "", { "dependencies": { "@shikijs/types": "3.3.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.2.0" } }, "sha512-XlhnFGv0glq7pfsoN0KyBCz9FJU678LZdQ2LqlIdAj6JKsg5xpYKay3DkazXWExp3DTJJK9rMOuGzU2911pg7Q=="],
|
|
20
|
+
|
|
21
|
+
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.3.0", "", { "dependencies": { "@shikijs/types": "3.3.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A=="],
|
|
22
|
+
|
|
23
|
+
"@shikijs/langs": ["@shikijs/langs@3.3.0", "", { "dependencies": { "@shikijs/types": "3.3.0" } }, "sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g=="],
|
|
24
|
+
|
|
25
|
+
"@shikijs/themes": ["@shikijs/themes@3.3.0", "", { "dependencies": { "@shikijs/types": "3.3.0" } }, "sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg=="],
|
|
26
|
+
|
|
27
|
+
"@shikijs/types": ["@shikijs/types@3.3.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q=="],
|
|
28
|
+
|
|
29
|
+
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
|
30
|
+
|
|
31
|
+
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
|
32
|
+
|
|
33
|
+
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
|
34
|
+
|
|
35
|
+
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
|
36
|
+
|
|
37
|
+
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
|
38
|
+
|
|
39
|
+
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
|
40
|
+
|
|
41
|
+
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
|
42
|
+
|
|
43
|
+
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
|
44
|
+
|
|
45
|
+
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
|
46
|
+
|
|
47
|
+
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
|
48
|
+
|
|
49
|
+
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
|
50
|
+
|
|
51
|
+
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
|
52
|
+
|
|
53
|
+
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
|
54
|
+
|
|
55
|
+
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
|
56
|
+
|
|
57
|
+
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
|
58
|
+
|
|
59
|
+
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
|
60
|
+
|
|
61
|
+
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
|
62
|
+
|
|
63
|
+
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
|
64
|
+
|
|
65
|
+
"liquidjs": ["liquidjs@10.21.0", "", { "dependencies": { "commander": "^10.0.0" }, "bin": { "liquidjs": "bin/liquid.js", "liquid": "bin/liquid.js" } }, "sha512-DouqxNU2jfoZzb1LinVjOc/f6ssitGIxiDJT+kEKyYqPSSSd+WmGOAhtWbVm1/n75svu4aQ+FyQ3ctd3wh1bbw=="],
|
|
66
|
+
|
|
67
|
+
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
|
|
68
|
+
|
|
69
|
+
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
|
70
|
+
|
|
71
|
+
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
|
72
|
+
|
|
73
|
+
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
|
74
|
+
|
|
75
|
+
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
|
76
|
+
|
|
77
|
+
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
|
78
|
+
|
|
79
|
+
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
|
80
|
+
|
|
81
|
+
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
|
|
82
|
+
|
|
83
|
+
"pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="],
|
|
84
|
+
|
|
85
|
+
"playwright": ["playwright@1.52.0", "", { "dependencies": { "playwright-core": "1.52.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw=="],
|
|
86
|
+
|
|
87
|
+
"playwright-core": ["playwright-core@1.52.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg=="],
|
|
88
|
+
|
|
89
|
+
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
|
|
90
|
+
|
|
91
|
+
"property-information": ["property-information@7.0.0", "", {}, "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg=="],
|
|
92
|
+
|
|
93
|
+
"regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],
|
|
94
|
+
|
|
95
|
+
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
|
96
|
+
|
|
97
|
+
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
|
98
|
+
|
|
99
|
+
"shiki": ["shiki@3.3.0", "", { "dependencies": { "@shikijs/core": "3.3.0", "@shikijs/engine-javascript": "3.3.0", "@shikijs/engine-oniguruma": "3.3.0", "@shikijs/langs": "3.3.0", "@shikijs/themes": "3.3.0", "@shikijs/types": "3.3.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-j0Z1tG5vlOFGW8JVj0Cpuatzvshes7VJy5ncDmmMaYcmnGW0Js1N81TOW98ivTFNZfKRn9uwEg/aIm638o368g=="],
|
|
100
|
+
|
|
101
|
+
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
|
102
|
+
|
|
103
|
+
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
|
104
|
+
|
|
105
|
+
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
|
106
|
+
|
|
107
|
+
"unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
|
|
108
|
+
|
|
109
|
+
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
|
110
|
+
|
|
111
|
+
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
|
112
|
+
|
|
113
|
+
"unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
|
114
|
+
|
|
115
|
+
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
|
|
116
|
+
|
|
117
|
+
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
|
118
|
+
|
|
119
|
+
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
|
120
|
+
|
|
121
|
+
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
|
122
|
+
|
|
123
|
+
"liquidjs/commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="]
|
|
124
|
+
}
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/vt",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Rettangoli Visual Testing",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/yuusoft-org/rettangoli",
|
|
9
|
+
"directory": "packages/rettangoli-vt"
|
|
10
|
+
},
|
|
6
11
|
"main": "./src/index.js",
|
|
7
12
|
"exports": {
|
|
8
13
|
".": "./src/index.js",
|
|
@@ -359,10 +359,12 @@ export class PlaywrightRunner {
|
|
|
359
359
|
}
|
|
360
360
|
settleMs = nowMs() - settleStart;
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
362
|
+
if (!task.frontMatter?.skipInitialScreenshot) {
|
|
363
|
+
const firstScreenshotStart = nowMs();
|
|
364
|
+
const firstScreenshotPath = await wrappedScreenshot(page, task.baseName);
|
|
365
|
+
initialScreenshotMs = nowMs() - firstScreenshotStart;
|
|
366
|
+
console.log(`Screenshot saved: ${firstScreenshotPath}`);
|
|
367
|
+
}
|
|
366
368
|
|
|
367
369
|
const stepsStart = nowMs();
|
|
368
370
|
const stepsExecutor = createSteps(page, {
|
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-
|
|
7
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-
|
|
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-
|
|
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-
|
|
8
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-
|
|
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-
|
|
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-
|
|
8
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-
|
|
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-
|
|
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;
|
package/src/common.js
CHANGED
|
@@ -15,6 +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
19
|
|
|
19
20
|
const removeExtension = (filePath) => filePath.replace(/\.[^/.]+$/, "");
|
|
20
21
|
|
|
@@ -248,7 +249,7 @@ function getContentType(filePath) {
|
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
function toSectionPageKey(sectionLike) {
|
|
251
|
-
return
|
|
252
|
+
return deriveSectionPageKey(sectionLike);
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
/**
|
package/src/createSteps.js
CHANGED
|
@@ -160,7 +160,7 @@ function assertStructuredKeys(stepObject, allowedKeys, actionName) {
|
|
|
160
160
|
|
|
161
161
|
function requireStepAction(stepObject) {
|
|
162
162
|
if (!isPlainObject(stepObject)) {
|
|
163
|
-
throw new Error("Invalid step: expected
|
|
163
|
+
throw new Error("Invalid step: expected an object.");
|
|
164
164
|
}
|
|
165
165
|
if (typeof stepObject.action !== "string" || stepObject.action.trim().length === 0) {
|
|
166
166
|
throw new Error("Structured step requires non-empty string `action`.");
|
|
@@ -411,12 +411,8 @@ function normalizeLegacyBlockStep(stepObject) {
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
function normalizeStepValue(step) {
|
|
414
|
-
if (typeof step === "string") {
|
|
415
|
-
const { command, args } = parseStepCommand(step);
|
|
416
|
-
return { kind: "command", command, args };
|
|
417
|
-
}
|
|
418
414
|
if (!isPlainObject(step)) {
|
|
419
|
-
throw new Error("Invalid step: expected
|
|
415
|
+
throw new Error("Invalid step: expected an object.");
|
|
420
416
|
}
|
|
421
417
|
if (Object.prototype.hasOwnProperty.call(step, "action")) {
|
|
422
418
|
return normalizeStructuredActionStep(step);
|
|
@@ -890,11 +886,6 @@ export function createSteps(page, context) {
|
|
|
890
886
|
if (!command) {
|
|
891
887
|
return;
|
|
892
888
|
}
|
|
893
|
-
if (command === "assert") {
|
|
894
|
-
throw new Error(
|
|
895
|
-
"Inline `assert` step strings are no longer supported. Use structured syntax: `- assert: { type: ..., ... }`.",
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
889
|
const actionFn = actionHandlers[command];
|
|
899
890
|
if (actionFn) {
|
|
900
891
|
await actionFn(page, args, context, selectedElement);
|
|
@@ -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
|
+
}
|
package/src/selector-filter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { stripViewportSuffix } from "./viewport.js";
|
|
3
|
+
import { deriveSectionPageKey } from "./section-page-key.js";
|
|
3
4
|
|
|
4
5
|
function toList(value) {
|
|
5
6
|
if (value === undefined || value === null) return [];
|
|
@@ -25,7 +26,7 @@ export function normalizeSelectors(raw = {}) {
|
|
|
25
26
|
.map(normalizePathValue)
|
|
26
27
|
.filter((item) => item.length > 0);
|
|
27
28
|
const groups = toList(raw.group)
|
|
28
|
-
.map((item) => String(item)
|
|
29
|
+
.map((item) => deriveSectionPageKey({ title: String(item) }))
|
|
29
30
|
.filter((item) => item.length > 0);
|
|
30
31
|
const items = toList(raw.item)
|
|
31
32
|
.map(normalizeItemKey)
|
|
@@ -59,12 +60,12 @@ function resolveGroupFolders(configSections = [], groupSelectors = []) {
|
|
|
59
60
|
for (const section of configSections) {
|
|
60
61
|
if (section.type === "groupLabel" && Array.isArray(section.items)) {
|
|
61
62
|
for (const item of section.items) {
|
|
62
|
-
groupFolderMap.set(
|
|
63
|
+
groupFolderMap.set(deriveSectionPageKey(item), normalizePathValue(item.files));
|
|
63
64
|
}
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
66
67
|
if (section.files) {
|
|
67
|
-
groupFolderMap.set(
|
|
68
|
+
groupFolderMap.set(deriveSectionPageKey(section), normalizePathValue(section.files));
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
package/src/validation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeViewportField } from "./viewport.js";
|
|
2
|
+
import { deriveSectionPageKey } from "./section-page-key.js";
|
|
2
3
|
|
|
3
4
|
function isPlainObject(value) {
|
|
4
5
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -121,8 +122,6 @@ const LEGACY_CAPTURE_FIELDS = {
|
|
|
121
122
|
headless: true,
|
|
122
123
|
};
|
|
123
124
|
|
|
124
|
-
const SECTION_PAGE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
125
|
-
|
|
126
125
|
function assertNoLegacyCaptureFields(vtConfig, sourcePath) {
|
|
127
126
|
for (const legacyField of Object.keys(LEGACY_CAPTURE_FIELDS)) {
|
|
128
127
|
if (Object.prototype.hasOwnProperty.call(vtConfig, legacyField)) {
|
|
@@ -133,14 +132,11 @@ function assertNoLegacyCaptureFields(vtConfig, sourcePath) {
|
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
134
|
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
typeof value === "string" && value.trim().length > 0,
|
|
139
|
-
`"${path}" is required.`,
|
|
140
|
-
);
|
|
135
|
+
function assertDerivableSectionPageKey(sectionLike, path) {
|
|
136
|
+
const pageKey = deriveSectionPageKey(sectionLike);
|
|
141
137
|
assert(
|
|
142
|
-
|
|
143
|
-
`"${path}" must contain
|
|
138
|
+
pageKey.length > 0,
|
|
139
|
+
`"${path}" must contain at least one letter or number.`,
|
|
144
140
|
);
|
|
145
141
|
}
|
|
146
142
|
|
|
@@ -173,25 +169,25 @@ function validateSection(section, index) {
|
|
|
173
169
|
|
|
174
170
|
assert(typeof item.title === "string" && item.title.trim().length > 0, `"${itemPath}.title" is required.`);
|
|
175
171
|
assert(typeof item.files === "string" && item.files.trim().length > 0, `"${itemPath}.files" is required.`);
|
|
176
|
-
|
|
172
|
+
assertDerivableSectionPageKey(item, `${itemPath}.title`);
|
|
177
173
|
});
|
|
178
174
|
return;
|
|
179
175
|
}
|
|
180
176
|
|
|
181
177
|
validateOptionalString(section.files, `${sectionPath}.files`);
|
|
182
178
|
assert(typeof section.files === "string" && section.files.trim().length > 0, `"${sectionPath}.files" is required.`);
|
|
183
|
-
|
|
179
|
+
assertDerivableSectionPageKey(section, `${sectionPath}.title`);
|
|
184
180
|
}
|
|
185
181
|
|
|
186
182
|
function collectSectionPageKeys(vtConfig) {
|
|
187
183
|
const keys = [];
|
|
188
184
|
vtConfig.sections.forEach((section) => {
|
|
189
185
|
if (section.type === "groupLabel" && Array.isArray(section.items)) {
|
|
190
|
-
section.items.forEach((item) => keys.push(item
|
|
186
|
+
section.items.forEach((item) => keys.push(deriveSectionPageKey(item)));
|
|
191
187
|
return;
|
|
192
188
|
}
|
|
193
189
|
if (section.files) {
|
|
194
|
-
keys.push(section
|
|
190
|
+
keys.push(deriveSectionPageKey(section));
|
|
195
191
|
}
|
|
196
192
|
});
|
|
197
193
|
return keys;
|
|
@@ -286,10 +282,6 @@ function validateStructuredActionStep(step, stepPath) {
|
|
|
286
282
|
assert(Array.isArray(step.steps), `"${stepPath}.steps" must be an array for action=select.`);
|
|
287
283
|
step.steps.forEach((nestedStep, nestedIndex) => {
|
|
288
284
|
const nestedPath = `${stepPath}.steps[${nestedIndex}]`;
|
|
289
|
-
if (typeof nestedStep === "string") {
|
|
290
|
-
assert(nestedStep.trim().length > 0, `"${nestedPath}" cannot be empty.`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
285
|
validateStepObject(nestedStep, nestedPath);
|
|
294
286
|
});
|
|
295
287
|
return;
|
|
@@ -434,7 +426,7 @@ function validateStructuredActionStep(step, stepPath) {
|
|
|
434
426
|
}
|
|
435
427
|
|
|
436
428
|
function validateStepObject(step, stepPath) {
|
|
437
|
-
assert(isPlainObject(step), `"${stepPath}" must be an object
|
|
429
|
+
assert(isPlainObject(step), `"${stepPath}" must be an object.`);
|
|
438
430
|
|
|
439
431
|
if (Object.prototype.hasOwnProperty.call(step, "action")) {
|
|
440
432
|
validateStructuredActionStep(step, stepPath);
|
|
@@ -459,10 +451,6 @@ function validateStepObject(step, stepPath) {
|
|
|
459
451
|
assert(Array.isArray(nestedSteps), `"${stepPath}.${stepKey}" must be an array of step values.`);
|
|
460
452
|
nestedSteps.forEach((nestedStep, nestedIndex) => {
|
|
461
453
|
const nestedPath = `${stepPath}.${stepKey}[${nestedIndex}]`;
|
|
462
|
-
if (typeof nestedStep === "string") {
|
|
463
|
-
assert(nestedStep.trim().length > 0, `"${nestedPath}" cannot be empty.`);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
454
|
validateStepObject(nestedStep, nestedPath);
|
|
467
455
|
});
|
|
468
456
|
}
|
|
@@ -606,6 +594,10 @@ export function validateFrontMatter(frontMatter, specPath) {
|
|
|
606
594
|
["networkidle", "load", "event", "selector"],
|
|
607
595
|
);
|
|
608
596
|
validateOptionalBoolean(frontMatter.skipScreenshot, `${specPath}: frontMatter.skipScreenshot`);
|
|
597
|
+
validateOptionalBoolean(
|
|
598
|
+
frontMatter.skipInitialScreenshot,
|
|
599
|
+
`${specPath}: frontMatter.skipInitialScreenshot`,
|
|
600
|
+
);
|
|
609
601
|
|
|
610
602
|
if (frontMatter.waitStrategy === "event") {
|
|
611
603
|
assert(
|
|
@@ -633,10 +625,6 @@ export function validateFrontMatter(frontMatter, specPath) {
|
|
|
633
625
|
assert(Array.isArray(frontMatter.steps), `"${specPath}: frontMatter.steps" must be an array.`);
|
|
634
626
|
frontMatter.steps.forEach((step, index) => {
|
|
635
627
|
const stepPath = `${specPath}: frontMatter.steps[${index}]`;
|
|
636
|
-
if (typeof step === "string") {
|
|
637
|
-
assert(step.trim().length > 0, `"${stepPath}" cannot be empty.`);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
628
|
validateStepObject(step, stepPath);
|
|
641
629
|
});
|
|
642
630
|
}
|