@fusion.dev/helix-cli 0.199.0 → 0.200.0
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/package.json +6 -6
- package/visual/README.md +5 -2
- package/visual/driver.mjs +41 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-cli",
|
|
3
3
|
"description": "The Helix command line, for a workspace that reaches it through npm, and the visual tests it runs.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.200.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"visual-regression"
|
|
42
42
|
],
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@fusion.dev/helix-cli-darwin-arm64": "0.
|
|
45
|
-
"@fusion.dev/helix-cli-darwin-x64": "0.
|
|
46
|
-
"@fusion.dev/helix-cli-linux-arm64": "0.
|
|
47
|
-
"@fusion.dev/helix-cli-linux-x64": "0.
|
|
48
|
-
"@fusion.dev/helix-cli-win32-x64": "0.
|
|
44
|
+
"@fusion.dev/helix-cli-darwin-arm64": "0.200.0",
|
|
45
|
+
"@fusion.dev/helix-cli-darwin-x64": "0.200.0",
|
|
46
|
+
"@fusion.dev/helix-cli-linux-arm64": "0.200.0",
|
|
47
|
+
"@fusion.dev/helix-cli-linux-x64": "0.200.0",
|
|
48
|
+
"@fusion.dev/helix-cli-win32-x64": "0.200.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@anthropic-ai/sdk": ">=0.132",
|
package/visual/README.md
CHANGED
|
@@ -242,8 +242,11 @@ picture is committed.
|
|
|
242
242
|
the page's boxes and scroll offsets to stay the same for three frames. It
|
|
243
243
|
emulates reduced motion and takes the shot with animations disabled. A font
|
|
244
244
|
that fails to load fails the entry.
|
|
245
|
-
- **One page load for
|
|
246
|
-
scheme
|
|
245
|
+
- **One page load for each width an entry is pictured at.** Shots at the same
|
|
246
|
+
width change the color scheme and the harness style on the same page, in the
|
|
247
|
+
order the config lists them. A shot at a new width loads the page again, so
|
|
248
|
+
nothing a page keeps from being resized reaches its picture. List the shots
|
|
249
|
+
of one width together.
|
|
247
250
|
- **A difference is taken again from a fresh page load.** A shot that matches
|
|
248
251
|
on the second take passes, and the run names it.
|
|
249
252
|
- **Changes are grouped by what they happened to.** Each region of a shot
|
package/visual/driver.mjs
CHANGED
|
@@ -22,11 +22,19 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Any request that fails answers `error`, with the reason.
|
|
24
24
|
*
|
|
25
|
-
* **One navigation
|
|
26
|
-
*
|
|
27
|
-
* the harness style and settles again. Measured on October 8, 2026
|
|
28
|
-
* pictures of 51 examples, that took 242.6 ms a picture against 287.3
|
|
29
|
-
* navigation each, and every picture was pixel for pixel the same.
|
|
25
|
+
* **One navigation for each width an entry is pictured at.** The first shot
|
|
26
|
+
* loads the page; a later one at the same width switches the emulated scheme,
|
|
27
|
+
* rewrites the harness style and settles again. Measured on October 8, 2026
|
|
28
|
+
* over 153 pictures of 51 examples, that took 242.6 ms a picture against 287.3
|
|
29
|
+
* ms for a navigation each, and every picture was pixel for pixel the same.
|
|
30
|
+
*
|
|
31
|
+
* **A new width loads the page again**, served styled for that shot. A page
|
|
32
|
+
* resized in place can keep what a fresh one never has, and the second take,
|
|
33
|
+
* which a baseline is written from, is always a fresh load: on Linux a
|
|
34
|
+
* dialog's text resized from 640 to 390 kept grayscale antialiasing where a
|
|
35
|
+
* fresh load at 390 drew it in color, so the shot needed a second take in
|
|
36
|
+
* every run. A change of scheme stays in place, where it tests a component
|
|
37
|
+
* that has to follow the page's colors.
|
|
30
38
|
*
|
|
31
39
|
* **Node and Playwright, decided on October 8, 2026.** Chromium's work is about
|
|
32
40
|
* 94% of the time of a picture, so a capture written in Rust over the browser
|
|
@@ -109,6 +117,8 @@ const state = {
|
|
|
109
117
|
* the document is named in the query and the path is the entry's.
|
|
110
118
|
*/
|
|
111
119
|
documents: new Map(),
|
|
120
|
+
/** Each entry's harness rules by shot, so a later shot loads styled for itself. */
|
|
121
|
+
harnesses: new Map(),
|
|
112
122
|
lanes: [],
|
|
113
123
|
/** Selectors painted over in every picture. */
|
|
114
124
|
mask: [],
|
|
@@ -173,18 +183,29 @@ const lookup = (path, document) => {
|
|
|
173
183
|
const respond = (request, response) => {
|
|
174
184
|
let path
|
|
175
185
|
let document
|
|
186
|
+
let shot
|
|
176
187
|
try {
|
|
177
188
|
const url = new URL(request.url ?? '/', 'http://visual')
|
|
178
189
|
path = decodeURIComponent(url.pathname)
|
|
179
190
|
document = url.searchParams.get('visual')
|
|
191
|
+
shot = url.searchParams.get('shot')
|
|
180
192
|
} catch {
|
|
181
193
|
response.writeHead(400).end()
|
|
182
194
|
return
|
|
183
195
|
}
|
|
184
196
|
const found = lookup(path, document)
|
|
185
197
|
if (found.html !== undefined) {
|
|
198
|
+
/* Styled for the shot it is loaded for, where that is not the first. */
|
|
199
|
+
const rules = shot === null ? undefined : state.harnesses.get(document)?.[Number(shot)]
|
|
186
200
|
response.writeHead(200, { 'content-type': 'text/html' })
|
|
187
|
-
response.end(
|
|
201
|
+
response.end(
|
|
202
|
+
typeof rules === 'string'
|
|
203
|
+
? found.html.replace(
|
|
204
|
+
new RegExp(`(<style id="${STYLE_ID}">)[^<]*(</style>)`),
|
|
205
|
+
(_, open, close) => `${open}${rules}${close}`,
|
|
206
|
+
)
|
|
207
|
+
: found.html,
|
|
208
|
+
)
|
|
188
209
|
return
|
|
189
210
|
}
|
|
190
211
|
let body
|
|
@@ -381,17 +402,23 @@ const take = async (lane, item, { elements, readyProperty }, progress) => {
|
|
|
381
402
|
const read = new Set()
|
|
382
403
|
lane.errors.length = 0
|
|
383
404
|
lane.messages.length = 0
|
|
405
|
+
/* The width the page was last loaded at. */
|
|
406
|
+
let loaded
|
|
384
407
|
try {
|
|
385
408
|
for (const [index, shot] of item.shots.entries()) {
|
|
386
|
-
|
|
409
|
+
const fresh = index === 0 || shot.width !== loaded
|
|
410
|
+
progress.stage = fresh ? 'loading the page' : `settling for ${shot.name}`
|
|
387
411
|
await page.setViewportSize({ height: shot.height, width: shot.width })
|
|
388
412
|
await page.emulateMedia({ colorScheme: shot.scheme })
|
|
389
|
-
if (
|
|
413
|
+
if (fresh) {
|
|
390
414
|
/* An entry on a server has its own address. An HTML entry is served
|
|
391
|
-
here, its document named in the query
|
|
415
|
+
here, its document named in the query, and the shot it is styled
|
|
416
|
+
for where that is not the first. */
|
|
392
417
|
const address =
|
|
393
|
-
item.url ??
|
|
418
|
+
item.url ??
|
|
419
|
+
`${state.origin}${item.path}?visual=${encodeURIComponent(item.key)}${index > 0 ? `&shot=${index}` : ''}`
|
|
394
420
|
await page.goto(address, { waitUntil: 'load' })
|
|
421
|
+
loaded = shot.width
|
|
395
422
|
const missing = await upgrades(page, elements)
|
|
396
423
|
if (missing.length > 0) {
|
|
397
424
|
throw new Error(
|
|
@@ -599,6 +626,10 @@ const handlers = {
|
|
|
599
626
|
async capture({ elements, items, readyProperty }) {
|
|
600
627
|
for (const item of items.filter((one) => one.path !== undefined)) {
|
|
601
628
|
state.documents.set(item.key, item.html)
|
|
629
|
+
state.harnesses.set(
|
|
630
|
+
item.key,
|
|
631
|
+
item.shots.map((shot) => shot.harness),
|
|
632
|
+
)
|
|
602
633
|
state.pages.set(item.path, { dir: item.dir, files: item.files ?? {} })
|
|
603
634
|
}
|
|
604
635
|
/* **Worker `k` takes items `k`, `k + n`, and so on**, rather than the next
|