@rettangoli/vt 1.0.0-rc5 → 1.0.2
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 +17 -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 +48 -26
- package/src/section-page-key.js +14 -0
- package/src/selector-filter.js +4 -3
- package/src/validation.js +24 -29
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,22 @@ 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.
|
|
129
|
+
- `action: select` accepts exactly one of `testId` or `selector` for interaction targeting.
|
|
127
130
|
- `assert` supports `js` deep-equal checks for object/array values.
|
|
128
131
|
|
|
129
132
|
Screenshot naming:
|
|
130
133
|
|
|
131
|
-
-
|
|
134
|
+
- By default, VT takes an immediate first screenshot before running `steps`.
|
|
135
|
+
- Set `skipInitialScreenshot: true` in frontmatter to skip that immediate first screenshot.
|
|
136
|
+
- First captured screenshot is `-01`.
|
|
132
137
|
- Then `-02`, `-03`, up to `-99`.
|
|
133
138
|
- When viewport id is configured, filenames include `--<viewportId>` before ordinal (for example `pages/home--mobile-01.webp`).
|
|
134
139
|
|
|
@@ -137,15 +142,15 @@ Screenshot naming:
|
|
|
137
142
|
A pre-built Docker image with `rtgl` and Playwright browsers is available:
|
|
138
143
|
|
|
139
144
|
```bash
|
|
140
|
-
docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-
|
|
145
|
+
docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc27
|
|
141
146
|
```
|
|
142
147
|
|
|
143
148
|
Run commands against a local project:
|
|
144
149
|
|
|
145
150
|
```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-
|
|
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
|
|
149
154
|
```
|
|
150
155
|
|
|
151
156
|
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.2",
|
|
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`.");
|
|
@@ -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") {
|
|
@@ -411,12 +432,8 @@ function normalizeLegacyBlockStep(stepObject) {
|
|
|
411
432
|
}
|
|
412
433
|
|
|
413
434
|
function normalizeStepValue(step) {
|
|
414
|
-
if (typeof step === "string") {
|
|
415
|
-
const { command, args } = parseStepCommand(step);
|
|
416
|
-
return { kind: "command", command, args };
|
|
417
|
-
}
|
|
418
435
|
if (!isPlainObject(step)) {
|
|
419
|
-
throw new Error("Invalid step: expected
|
|
436
|
+
throw new Error("Invalid step: expected an object.");
|
|
420
437
|
}
|
|
421
438
|
if (Object.prototype.hasOwnProperty.call(step, "action")) {
|
|
422
439
|
return normalizeStructuredActionStep(step);
|
|
@@ -832,22 +849,32 @@ async function assertStructured(page, assertionConfig, selectedElement) {
|
|
|
832
849
|
}
|
|
833
850
|
|
|
834
851
|
async function select(page, args) {
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
)
|
|
844
|
-
|
|
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
|
+
|
|
845
872
|
const count = await interactiveElementLocator.count();
|
|
846
|
-
|
|
873
|
+
|
|
847
874
|
if (count > 0) {
|
|
848
875
|
return interactiveElementLocator;
|
|
849
876
|
}
|
|
850
|
-
|
|
877
|
+
|
|
851
878
|
return hostElementLocator;
|
|
852
879
|
}
|
|
853
880
|
|
|
@@ -890,11 +917,6 @@ export function createSteps(page, context) {
|
|
|
890
917
|
if (!command) {
|
|
891
918
|
return;
|
|
892
919
|
}
|
|
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
920
|
const actionFn = actionHandlers[command];
|
|
899
921
|
if (actionFn) {
|
|
900
922
|
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;
|
|
@@ -277,19 +273,22 @@ function validateStructuredActionStep(step, stepPath) {
|
|
|
277
273
|
}
|
|
278
274
|
|
|
279
275
|
if (action === "select") {
|
|
280
|
-
assertNoUnknownStepKeys(step, stepPath, new Set(["action", "testId", "steps"]));
|
|
276
|
+
assertNoUnknownStepKeys(step, stepPath, new Set(["action", "testId", "selector", "steps"]));
|
|
281
277
|
validateOptionalString(step.testId, `${stepPath}.testId`);
|
|
278
|
+
validateOptionalString(step.selector, `${stepPath}.selector`);
|
|
282
279
|
assert(
|
|
283
|
-
|
|
284
|
-
|
|
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".`,
|
|
285
288
|
);
|
|
286
289
|
assert(Array.isArray(step.steps), `"${stepPath}.steps" must be an array for action=select.`);
|
|
287
290
|
step.steps.forEach((nestedStep, nestedIndex) => {
|
|
288
291
|
const nestedPath = `${stepPath}.steps[${nestedIndex}]`;
|
|
289
|
-
if (typeof nestedStep === "string") {
|
|
290
|
-
assert(nestedStep.trim().length > 0, `"${nestedPath}" cannot be empty.`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
292
|
validateStepObject(nestedStep, nestedPath);
|
|
294
293
|
});
|
|
295
294
|
return;
|
|
@@ -434,7 +433,7 @@ function validateStructuredActionStep(step, stepPath) {
|
|
|
434
433
|
}
|
|
435
434
|
|
|
436
435
|
function validateStepObject(step, stepPath) {
|
|
437
|
-
assert(isPlainObject(step), `"${stepPath}" must be an object
|
|
436
|
+
assert(isPlainObject(step), `"${stepPath}" must be an object.`);
|
|
438
437
|
|
|
439
438
|
if (Object.prototype.hasOwnProperty.call(step, "action")) {
|
|
440
439
|
validateStructuredActionStep(step, stepPath);
|
|
@@ -459,10 +458,6 @@ function validateStepObject(step, stepPath) {
|
|
|
459
458
|
assert(Array.isArray(nestedSteps), `"${stepPath}.${stepKey}" must be an array of step values.`);
|
|
460
459
|
nestedSteps.forEach((nestedStep, nestedIndex) => {
|
|
461
460
|
const nestedPath = `${stepPath}.${stepKey}[${nestedIndex}]`;
|
|
462
|
-
if (typeof nestedStep === "string") {
|
|
463
|
-
assert(nestedStep.trim().length > 0, `"${nestedPath}" cannot be empty.`);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
461
|
validateStepObject(nestedStep, nestedPath);
|
|
467
462
|
});
|
|
468
463
|
}
|
|
@@ -606,6 +601,10 @@ export function validateFrontMatter(frontMatter, specPath) {
|
|
|
606
601
|
["networkidle", "load", "event", "selector"],
|
|
607
602
|
);
|
|
608
603
|
validateOptionalBoolean(frontMatter.skipScreenshot, `${specPath}: frontMatter.skipScreenshot`);
|
|
604
|
+
validateOptionalBoolean(
|
|
605
|
+
frontMatter.skipInitialScreenshot,
|
|
606
|
+
`${specPath}: frontMatter.skipInitialScreenshot`,
|
|
607
|
+
);
|
|
609
608
|
|
|
610
609
|
if (frontMatter.waitStrategy === "event") {
|
|
611
610
|
assert(
|
|
@@ -633,10 +632,6 @@ export function validateFrontMatter(frontMatter, specPath) {
|
|
|
633
632
|
assert(Array.isArray(frontMatter.steps), `"${specPath}: frontMatter.steps" must be an array.`);
|
|
634
633
|
frontMatter.steps.forEach((step, index) => {
|
|
635
634
|
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
635
|
validateStepObject(step, stepPath);
|
|
641
636
|
});
|
|
642
637
|
}
|