@rettangoli/vt 1.0.0-rc4 → 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 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` for flat sections, `items[].title` for grouped sections)
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 components_basic
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 components_basic --item pages/home
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 components_basic
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: components_basic
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 (`title` for flat sections and group `items[].title`) allow only letters, numbers, `-`, `_`.
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: ...`), with legacy string/block forms still supported.
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
- - First screenshot is `-01`.
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-rc6
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-rc6 rtgl vt screenshot
147
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc6 rtgl vt report
148
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc6 rtgl vt accept
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.0-rc4",
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
- const firstScreenshotStart = nowMs();
363
- const firstScreenshotPath = await wrappedScreenshot(page, task.baseName);
364
- initialScreenshotMs = nowMs() - firstScreenshotStart;
365
- console.log(`Screenshot saved: ${firstScreenshotPath}`);
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,7 +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="/public/theme.css">
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">
7
8
  <script>
8
9
  window.rtglIcons = {
9
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>`,
@@ -38,7 +39,8 @@
38
39
  `,
39
40
  }
40
41
  </script>
41
- <script src="https://cdn.jsdelivr.net/npm/rettangoli-ui@0.1.0-rc2/dist/rettangoli-iife-ui.min.js"></script>
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-rc15/dist/rettangoli-iife-ui.min.js"></script>
42
44
  <script src="/public/main.js"></script>
43
45
  </head>
44
46
  <body class="dark">
@@ -46,4 +48,4 @@
46
48
  {{ content }}
47
49
  </div>
48
50
  </body>
49
- </html>
51
+ </html>
@@ -4,7 +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="/public/theme.css">
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">
8
9
  <script>
9
10
  window.addEventListener('DOMContentLoaded', () => {
10
11
  if (location.hash) {
@@ -15,7 +16,8 @@
15
16
  }
16
17
  });
17
18
  </script>
18
- <script src="https://cdn.jsdelivr.net/npm/rettangoli-ui@0.1.0-rc2/dist/rettangoli-iife-ui.min.js"></script>
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-rc15/dist/rettangoli-iife-ui.min.js"></script>
19
21
 
20
22
  <style>
21
23
  pre {
@@ -45,14 +47,14 @@
45
47
  </head>
46
48
 
47
49
  <body class="dark">
48
- <rtgl-view d="h" w="100vw" h="100vh">
50
+ <rtgl-view d="h" w="f" h="100vh">
49
51
 
50
52
  <rtgl-view h="f" sm-hidden>
51
53
  <rtgl-sidebar items="{{ sidebarItems }}">
52
54
  </rtgl-sidebar>
53
55
  </rtgl-view>
54
56
 
55
- <rtgl-view id="content" h="100vh" w="1fg" p="lg" g="lg" style="flex-wrap: nowrap;" sv ah="c">
57
+ <rtgl-view id="content" h="100vh" w="1fg" p="lg" g="lg" style="flex-wrap: nowrap; min-width: 0;" sv ah="c">
56
58
  <rtgl-view w="f" g="xl">
57
59
  <rtgl-text s="h2">{{ currentSection.title }} </rtgl-text>
58
60
  {% for file in files %}
@@ -100,8 +102,8 @@
100
102
  <rtgl-view h="33vh"></rtgl-view>
101
103
  </rtgl-view>
102
104
  </rtgl-view>
103
- <rtgl-view lg-hidden>
104
- <rtgl-page-outline id="page-outline" target-id="content"></rtgl-page-outline>
105
+ <rtgl-view lg-hidden style="flex: 0 0 auto;">
106
+ <rtgl-page-outline id="page-outline" target-id="content" scroll-container-id="content"></rtgl-page-outline>
105
107
  </rtgl-view>
106
108
  </rtgl-view>
107
109
  </body>
@@ -4,8 +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="/public/theme.css">
8
- <script src="https://cdn.jsdelivr.net/npm/rettangoli-ui@0.1.0-rc2/dist/rettangoli-iife-ui.min.js"></script>
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
+ <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-rc15/dist/rettangoli-iife-ui.min.js"></script>
9
11
  <style>
10
12
  code {
11
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 String(sectionLike.title || "").toLowerCase();
252
+ return deriveSectionPageKey(sectionLike);
252
253
  }
253
254
 
254
255
  /**
@@ -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 string or object.");
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 string or object.");
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
+ }
@@ -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).trim().toLowerCase())
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(String(item.title).toLowerCase(), normalizePathValue(item.files));
63
+ groupFolderMap.set(deriveSectionPageKey(item), normalizePathValue(item.files));
63
64
  }
64
65
  continue;
65
66
  }
66
67
  if (section.files) {
67
- groupFolderMap.set(String(section.title).toLowerCase(), normalizePathValue(section.files));
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 assertValidSectionPageKey(value, path) {
137
- assert(
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
- SECTION_PAGE_KEY_PATTERN.test(value),
143
- `"${path}" must contain only letters, numbers, "-" or "_", and cannot include spaces.`,
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
- assertValidSectionPageKey(item.title, `${itemPath}.title`);
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
- assertValidSectionPageKey(section.title, `${sectionPath}.title`);
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.title));
186
+ section.items.forEach((item) => keys.push(deriveSectionPageKey(item)));
191
187
  return;
192
188
  }
193
189
  if (section.files) {
194
- keys.push(section.title);
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 with one key or a string.`);
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
  }