@fusion.dev/helix-cli 0.205.2 → 0.207.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/README.md +8 -3
- package/bin.mjs +111 -22
- package/package.json +9 -6
- package/visual/README.md +3 -1
- package/visual/checks.mjs +7 -2
- package/visual/driver.mjs +37 -19
package/README.md
CHANGED
|
@@ -10,8 +10,10 @@ npm install --save-dev @fusion.dev/helix-cli
|
|
|
10
10
|
|
|
11
11
|
The command is a native binary, and npm installs the build for your machine
|
|
12
12
|
beside this package: macOS and Linux on Arm and x86-64, and Windows on x86-64.
|
|
13
|
-
An install that skips optional dependencies skips it too
|
|
14
|
-
|
|
13
|
+
An install that skips optional dependencies skips it too. `helix` then runs a
|
|
14
|
+
`helix` on your `PATH` whose `helix --version --json` names this package, and
|
|
15
|
+
otherwise names the package that is missing and each `helix` it did not run,
|
|
16
|
+
such as the Helix text editor. To start a new project with Helix, use
|
|
15
17
|
`npm create @fusion.dev/helix-app`.
|
|
16
18
|
|
|
17
19
|
```sh
|
|
@@ -24,10 +26,11 @@ helix mcp init --client claude
|
|
|
24
26
|
|---|---|
|
|
25
27
|
| `component` | Checks the components you build on Helix's base classes: registration under your prefix, imports, slots, parts, states, hooks, words, reflection, attribute names, lifecycle, and listeners. Reads the folders named by `tiers` in `helix.json`. `--only <rule>` runs some of them |
|
|
26
28
|
| `theme` | `init` a starter intent, `build` it to CSS, `audit` every contrast pair, `check` in CI |
|
|
27
|
-
| `new` | Scaffolds a project from a Helix template |
|
|
29
|
+
| `new` | Scaffolds a project from a Helix template: `helix new my-app --template react-vite`. The templates install with the CLI, in `@fusion.dev/helix-templates`, so `npx @fusion.dev/helix-cli new` works from an empty directory. Omit `--template` to list them |
|
|
28
30
|
| `diagnostics` | Receives the runs `initDiagnostics()` sends from your app and prints each new finding. `--once <url>` opens a page headless instead, diagnoses it, and exits. `--out <path>` writes the CI file, `--port` picks the port, 7417 by default, and `--config <path>` names the settings served to the page |
|
|
29
31
|
| `scan` | Reads your project's stylesheets, templates, and modules, and prints each finding with its file and line. `--build <dir>` weighs each Helix family in your bundle, `--out <path>` writes the run to the CI file, `--config <path>` names the settings, `--file <path>` reports one file's findings, and `--stdin <language>` checks a snippet |
|
|
30
32
|
| `api` | Looks up the installed Helix packages: `find` a family, a `family`'s API, an `example` with its imports, the `imports` for a list of tags, and `tokens` |
|
|
33
|
+
| `manifest` | `diff` compares two custom elements manifests and prints each difference. Exits 1 on any difference |
|
|
31
34
|
| `visual` | Pictures each page in a real browser and compares it with a baseline kept in git: `test`, `update` the baselines, open the `report`, and `compare` two PNG files. Reads `visual.config.ts`, and needs Playwright installed. [The guide](https://github.com/project-fusion/fusion/blob/main/helix/cli/npm/visual/README.md) has the config and the GitHub Action |
|
|
32
35
|
| `mcp` | Serves the same lookups, the scan, and the theme derivation to an agent over stdio, as an MCP server. `mcp init --client <client>` writes the configuration for Claude Code, Cursor, VS Code, Gemini CLI, opencode, Roo Code, or Junie |
|
|
33
36
|
|
|
@@ -40,6 +43,8 @@ Color is already off when stdout is not a terminal and when `NO_COLOR` is set.
|
|
|
40
43
|
rather than compiled into the binary, so one build serves every release that
|
|
41
44
|
changes no Rust. A binary built from a checkout of this repository has no such
|
|
42
45
|
package beside it and says `0.0.0-dev`, which is what that build is.
|
|
46
|
+
`helix --version --json` prints the same release as
|
|
47
|
+
`{ "name": "helix", "package": "@fusion.dev/helix-cli", "version": … }`.
|
|
43
48
|
|
|
44
49
|
[Documentation](https://fusion.dev/helix)
|
|
45
50
|
|
package/bin.mjs
CHANGED
|
@@ -13,15 +13,17 @@
|
|
|
13
13
|
* builds one if it can, and says exactly what is missing if it cannot.
|
|
14
14
|
*
|
|
15
15
|
* **Resolution order, and each step is there for a reason.** `HELIX_BIN` first,
|
|
16
|
-
* because a caller that knows better should not have to argue. Then
|
|
17
|
-
* target directory, release before debug, honoring
|
|
18
|
-
* shared target is common and guessing `./target`
|
|
19
|
-
* binary npm installed beside this, which is what a
|
|
20
|
-
* target, so a checkout runs what it built rather than
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* because a caller that knows better should not have to argue. Then, in a
|
|
17
|
+
* checkout, the cargo target directory, release before debug, honoring
|
|
18
|
+
* `CARGO_TARGET_DIR` because a shared target is common and guessing `./target`
|
|
19
|
+
* would miss it. Then the binary npm installed beside this, which is what a
|
|
20
|
+
* consumer has: after the target, so a checkout runs what it built rather than
|
|
21
|
+
* a binary a release once placed. A consumer's target is never read: a `helix`
|
|
22
|
+
* there is another program or an old build of ours, and the installed one is
|
|
23
|
+
* the release they asked for. Then whatever is on `PATH` that says it is this
|
|
24
|
+
* command line, named out loud in a checkout, because a binary from somewhere
|
|
25
|
+
* else is about to answer questions about this tree. Only then a build, and
|
|
26
|
+
* only in this repository, the one place there is a crate to build.
|
|
25
27
|
*
|
|
26
28
|
* **A build is a fallback and not the plan.** It happens once, prints that it
|
|
27
29
|
* is happening, and a CI pipeline that has already run `cargo build` never
|
|
@@ -97,7 +99,7 @@ const hasCargo = () => spawnSync('cargo', ['--version'], { stdio: 'ignore' }).st
|
|
|
97
99
|
const SELF = realpathSync(fileURLToPath(import.meta.url))
|
|
98
100
|
|
|
99
101
|
/**
|
|
100
|
-
*
|
|
102
|
+
* Each `helix` on `PATH`, in order, resolved through its symlinks.
|
|
101
103
|
*
|
|
102
104
|
* **Walked rather than spawned, because the spawn is the bug.** Probing with
|
|
103
105
|
* `spawnSync('helix', ['--version'])` asks `PATH` to answer, and under
|
|
@@ -112,8 +114,9 @@ const SELF = realpathSync(fileURLToPath(import.meta.url))
|
|
|
112
114
|
* trap is set for a fresh clone, which is what CI and a new contributor both
|
|
113
115
|
* are.
|
|
114
116
|
*/
|
|
115
|
-
|
|
117
|
+
function* onPath() {
|
|
116
118
|
const extensions = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : ['']
|
|
119
|
+
const seen = new Set()
|
|
117
120
|
for (const directory of (process.env.PATH ?? '').split(delimiter)) {
|
|
118
121
|
if (directory === '') {
|
|
119
122
|
continue
|
|
@@ -136,13 +139,47 @@ const onPath = () => {
|
|
|
136
139
|
says nothing about a real one later in it, and returning here would
|
|
137
140
|
make an installed binary invisible whenever a checkout was in front of
|
|
138
141
|
it. */
|
|
139
|
-
if (real === SELF) {
|
|
142
|
+
if (real === SELF || seen.has(real)) {
|
|
140
143
|
continue
|
|
141
144
|
}
|
|
142
|
-
|
|
145
|
+
seen.add(real)
|
|
146
|
+
yield real
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
|
-
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Set while this shim runs a `PATH` entry with `--version --json`, so a copy
|
|
153
|
+
* of this shim run that way uses only the binary it has installed and never
|
|
154
|
+
* searches `PATH` itself: two copies with no binary between them would
|
|
155
|
+
* otherwise run each other without end.
|
|
156
|
+
*/
|
|
157
|
+
const PROBE = 'HELIX_SHIM_PROBE'
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether `binary` is this command line, read from what it prints for
|
|
161
|
+
* `--version --json`.
|
|
162
|
+
*
|
|
163
|
+
* **The name proves nothing.** On Arch Linux `helix` is the Helix text editor,
|
|
164
|
+
* and an old `cargo install` of this command line has none of the commands a
|
|
165
|
+
* project runs now, such as `scan`. This command line prints its package's
|
|
166
|
+
* name for `--version --json`. The editor exits with an error on `--json`, and
|
|
167
|
+
* an older build prints its plain version line. Measured 10 Oct 2026 on an
|
|
168
|
+
* Apple silicon laptop: 4 ms for this command line, and 2 ms for an old build.
|
|
169
|
+
* The timeout ends a program that never exits.
|
|
170
|
+
*/
|
|
171
|
+
const ours = (binary) => {
|
|
172
|
+
const asked = spawnSync(binary, ['--version', '--json'], {
|
|
173
|
+
encoding: 'utf8',
|
|
174
|
+
env: { ...process.env, [PROBE]: '1' },
|
|
175
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
176
|
+
timeout: 2_000,
|
|
177
|
+
})
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(asked.stdout ?? '').package === '@fusion.dev/helix-cli'
|
|
180
|
+
} catch {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
/**
|
|
@@ -157,6 +194,27 @@ const DRIVER_ENV = {
|
|
|
157
194
|
HELIX_VISUAL_RUNTIME: process.env.HELIX_VISUAL_RUNTIME ?? process.execPath,
|
|
158
195
|
}
|
|
159
196
|
|
|
197
|
+
/**
|
|
198
|
+
* What `helix new` reads: the `@fusion.dev/helix-templates` this package
|
|
199
|
+
* depends on, at this package's version, named in `HELIX_TEMPLATES` where the
|
|
200
|
+
* caller has not named another.
|
|
201
|
+
*
|
|
202
|
+
* **Resolved here, because node resolves a dependency the way it was
|
|
203
|
+
* installed.** The binary walks up from where it runs for a `node_modules`
|
|
204
|
+
* holding the templates, and `npx @fusion.dev/helix-cli new` runs from a
|
|
205
|
+
* directory with none: npm installed the command and its dependencies in its
|
|
206
|
+
* own cache. Under pnpm, or beside another version of the templates in a
|
|
207
|
+
* project, that walk misses this package's copy as well.
|
|
208
|
+
*/
|
|
209
|
+
try {
|
|
210
|
+
DRIVER_ENV.HELIX_TEMPLATES ??= dirname(
|
|
211
|
+
createRequire(import.meta.url).resolve('@fusion.dev/helix-templates/templates.json'),
|
|
212
|
+
)
|
|
213
|
+
} catch {
|
|
214
|
+
/* Not installed, so the binary walks up for the templates and names the
|
|
215
|
+
package when nothing has it. */
|
|
216
|
+
}
|
|
217
|
+
|
|
160
218
|
const run = (binary) => {
|
|
161
219
|
const { status, error } = spawnSync(binary, process.argv.slice(2), {
|
|
162
220
|
env: DRIVER_ENV,
|
|
@@ -183,8 +241,14 @@ const run = (binary) => {
|
|
|
183
241
|
features per selection, so alternating `-p fusion-helix-cli` with `--workspace`
|
|
184
242
|
recompiled on every switch, about a second each, where asking for the same
|
|
185
243
|
selection twice is 0.14s. `HELIX_BIN` still outranks everything, and a checkout
|
|
186
|
-
with no cargo falls through to whatever is already built
|
|
187
|
-
|
|
244
|
+
with no cargo falls through to whatever is already built, as a probe from
|
|
245
|
+
another copy of this shim does: it waits two seconds for an answer. */
|
|
246
|
+
if (
|
|
247
|
+
process.env.HELIX_BIN === undefined &&
|
|
248
|
+
process.env[PROBE] === undefined &&
|
|
249
|
+
checkout &&
|
|
250
|
+
hasCargo()
|
|
251
|
+
) {
|
|
188
252
|
const fresh = spawnSync('cargo', ['build', '--workspace', '--quiet'], {
|
|
189
253
|
cwd: WORKSPACE,
|
|
190
254
|
stdio: 'inherit',
|
|
@@ -199,14 +263,24 @@ if (process.env.HELIX_BIN === undefined && checkout && hasCargo()) {
|
|
|
199
263
|
}
|
|
200
264
|
}
|
|
201
265
|
|
|
202
|
-
const found = process.env.HELIX_BIN ?? built() ?? packaged()
|
|
266
|
+
const found = process.env.HELIX_BIN ?? (checkout ? built() : undefined) ?? packaged()
|
|
203
267
|
if (found && existsSync(found)) {
|
|
204
268
|
run(found)
|
|
205
269
|
}
|
|
206
270
|
|
|
207
|
-
/* `PATH`, which is what an installed binary looks like
|
|
208
|
-
|
|
209
|
-
|
|
271
|
+
/* `PATH`, which is what an installed binary looks like, walked rather than
|
|
272
|
+
searched by a spawn: see `onPath`, where a spawn of `helix` recursed. Each
|
|
273
|
+
one found runs only once `ours` says it is this command line, and the
|
|
274
|
+
others are named below, where the run gives up. */
|
|
275
|
+
const skipped = []
|
|
276
|
+
let installed
|
|
277
|
+
for (const candidate of process.env[PROBE] === undefined ? onPath() : []) {
|
|
278
|
+
if (ours(candidate)) {
|
|
279
|
+
installed = candidate
|
|
280
|
+
break
|
|
281
|
+
}
|
|
282
|
+
skipped.push(candidate)
|
|
283
|
+
}
|
|
210
284
|
if (installed) {
|
|
211
285
|
/* **In a checkout, say which binary this is before running it.** Getting here
|
|
212
286
|
means no `target/`, no platform package and no cargo, so what runs is
|
|
@@ -231,6 +305,21 @@ if (installed) {
|
|
|
231
305
|
run(installed)
|
|
232
306
|
}
|
|
233
307
|
|
|
308
|
+
/** The `helix` binaries on `PATH` that are some other program, named. */
|
|
309
|
+
const foreign = () => {
|
|
310
|
+
if (skipped.length === 0) {
|
|
311
|
+
return ''
|
|
312
|
+
}
|
|
313
|
+
const one = skipped.length === 1
|
|
314
|
+
return (
|
|
315
|
+
`\n\n Not run, from PATH: ${skipped.map((path) => `\`${path}\``).join(', ')}.\n` +
|
|
316
|
+
` ${one ? 'Its output' : 'The output of each'} for \`--version --json\` does not name\n` +
|
|
317
|
+
` \`@fusion.dev/helix-cli\`, so ${one ? 'it is' : 'each is'} another program named helix, such\n` +
|
|
318
|
+
' as the Helix text editor, or a build of this command line from before that\n' +
|
|
319
|
+
' output.'
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
234
323
|
/* Installed from the registry, with no platform package for this machine. A
|
|
235
324
|
build would be `cargo build` in somebody else's project, for a crate that is
|
|
236
325
|
not in it. */
|
|
@@ -241,7 +330,7 @@ if (!checkout) {
|
|
|
241
330
|
' `@fusion.dev/helix-cli`, and it is not installed. Either this machine is not\n' +
|
|
242
331
|
' one there is a build for, or the install skipped optional dependencies\n' +
|
|
243
332
|
' (`--omit=optional`, `--no-optional`), in which case install again without it.\n' +
|
|
244
|
-
|
|
333
|
+
` \`HELIX_BIN\` names a binary to run instead.${foreign()}`,
|
|
245
334
|
)
|
|
246
335
|
process.exit(69)
|
|
247
336
|
}
|
|
@@ -252,7 +341,7 @@ if (!hasCargo()) {
|
|
|
252
341
|
` Looked at: $HELIX_BIN, ${join(target, 'release', NAME)},\n` +
|
|
253
342
|
` ${join(target, 'debug', NAME)}, and \`${NAME}\` on PATH.\n\n` +
|
|
254
343
|
' Either install the binary, point `HELIX_BIN` at one, or install a Rust\n' +
|
|
255
|
-
|
|
344
|
+
` toolchain so this can build it.${foreign()}`,
|
|
256
345
|
)
|
|
257
346
|
process.exit(69)
|
|
258
347
|
}
|
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.207.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
"test": "bun test"
|
|
30
30
|
},
|
|
31
31
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@fusion.dev/helix-templates": "0.207.0"
|
|
34
|
+
},
|
|
32
35
|
"homepage": "https://fusion.dev",
|
|
33
36
|
"keywords": [
|
|
34
37
|
"helix",
|
|
@@ -41,11 +44,11 @@
|
|
|
41
44
|
"visual-regression"
|
|
42
45
|
],
|
|
43
46
|
"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.
|
|
47
|
+
"@fusion.dev/helix-cli-darwin-arm64": "0.207.0",
|
|
48
|
+
"@fusion.dev/helix-cli-darwin-x64": "0.207.0",
|
|
49
|
+
"@fusion.dev/helix-cli-linux-arm64": "0.207.0",
|
|
50
|
+
"@fusion.dev/helix-cli-linux-x64": "0.207.0",
|
|
51
|
+
"@fusion.dev/helix-cli-win32-x64": "0.207.0"
|
|
49
52
|
},
|
|
50
53
|
"peerDependencies": {
|
|
51
54
|
"@anthropic-ai/sdk": ">=0.132",
|
package/visual/README.md
CHANGED
|
@@ -159,7 +159,9 @@ export default defineConfig({
|
|
|
159
159
|
ground it is painted on, in each color scheme the shots picture. The ground
|
|
160
160
|
is composited through every translucent layer, and a ground that is a
|
|
161
161
|
picture or a video is not judged. Text hidden from the accessibility tree is
|
|
162
|
-
a glyph for the eye, and answers to the 3:1 of a graphic.
|
|
162
|
+
a glyph for the eye, and answers to the 3:1 of a graphic. The run record
|
|
163
|
+
lists every ink and ground pair it measured, passing or not, under
|
|
164
|
+
`painted`, so a project can say which of its colors its pages put together. `rules` names the colors of lines that
|
|
163
165
|
separate, which answer to no target.
|
|
164
166
|
- Each check takes `ignore`, selectors it leaves alone with everything inside
|
|
165
167
|
them, and an exception's `checks` excuses one entry from the checks it names.
|
package/visual/checks.mjs
CHANGED
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Reads the page for the checks `options` turns on: `clipped`, `overlap` and
|
|
15
|
-
* `contrast`, each with its own settings, or absent.
|
|
15
|
+
* `contrast`, each with its own settings, or absent. Returns the findings,
|
|
16
|
+
* and every ink and ground pair contrast measured, passing or not, as
|
|
17
|
+
* `<scheme> <ink> on <ground>` in hex, so a run can say which pairs its pages
|
|
18
|
+
* paint.
|
|
16
19
|
*/
|
|
17
20
|
export const readPage = (page, options) =>
|
|
18
21
|
page.evaluate((options) => {
|
|
19
22
|
const findings = []
|
|
23
|
+
const pairs = new Set()
|
|
20
24
|
|
|
21
25
|
/* **Every element, shadow roots included**, because the boxes a component
|
|
22
26
|
lays out for itself are inside one, and the light tree alone holds only
|
|
@@ -696,6 +700,7 @@ export const readPage = (page, options) =>
|
|
|
696
700
|
const wanted =
|
|
697
701
|
!text || drawn || size >= large || (bold && size >= largeBold) ? component : target
|
|
698
702
|
const flat = ink.a === 1 ? ink : over(ink, ground.color)
|
|
703
|
+
pairs.add(`${scheme} ${hex(flat)} on ${hex(ground.color)}`)
|
|
699
704
|
const measured = ratio(flat, ground.color)
|
|
700
705
|
if (measured < wanted) {
|
|
701
706
|
const line = `${describe(painter)} is ${hex(flat)} on ${hex(ground.color)} in ${scheme}, ${measured.toFixed(2)}:1 against ${wanted}:1: "${sample(element.textContent ?? '')}"`
|
|
@@ -712,7 +717,7 @@ export const readPage = (page, options) =>
|
|
|
712
717
|
window.scrollTo(pageAt[0], pageAt[1])
|
|
713
718
|
}
|
|
714
719
|
|
|
715
|
-
return findings
|
|
720
|
+
return { findings, pairs: [...pairs] }
|
|
716
721
|
}, options)
|
|
717
722
|
|
|
718
723
|
/**
|
package/visual/driver.mjs
CHANGED
|
@@ -230,19 +230,34 @@ const respond = (request, response) => {
|
|
|
230
230
|
*
|
|
231
231
|
* **Bounded, because `whenDefined` on a tag nothing registers never settles.**
|
|
232
232
|
* Unbounded, a page naming an element the bundle does not define hangs the
|
|
233
|
-
* run with no output. This gives up after
|
|
233
|
+
* run with no output. This gives up after {@link UPGRADES} seconds and names
|
|
234
|
+
* the tags.
|
|
234
235
|
*/
|
|
235
236
|
const upgrades = (page, prefix) =>
|
|
236
|
-
page.evaluate(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Promise.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
237
|
+
page.evaluate(
|
|
238
|
+
async ({ prefix, seconds }) => {
|
|
239
|
+
const pending = [
|
|
240
|
+
...new Set([...document.querySelectorAll(':not(:defined)')].map((one) => one.localName)),
|
|
241
|
+
].filter((tag) => tag.startsWith(prefix))
|
|
242
|
+
await Promise.race([
|
|
243
|
+
Promise.all(pending.map((tag) => customElements.whenDefined(tag))),
|
|
244
|
+
new Promise((done) => setTimeout(done, seconds * 1000)),
|
|
245
|
+
])
|
|
246
|
+
return pending.filter((tag) => customElements.get(tag) === undefined)
|
|
247
|
+
},
|
|
248
|
+
{ prefix: prefix ?? '', seconds: UPGRADES },
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* **How long a page has to define its elements after it loads: twenty
|
|
253
|
+
* seconds**, a third of what one shot may take. The bound only stops a run
|
|
254
|
+
* hanging on a tag nothing registers, and a page that registers late is slow,
|
|
255
|
+
* which a picture does not judge. Five seconds failed the site's theme builder
|
|
256
|
+
* on two CI runs in a row on October 10, 2026: its first style pass over
|
|
257
|
+
* 15,638 elements and Helix's chunk took 1.7 seconds on an M-series Mac, and a
|
|
258
|
+
* shared runner takes several times that.
|
|
259
|
+
*/
|
|
260
|
+
const UPGRADES = 20
|
|
246
261
|
|
|
247
262
|
/**
|
|
248
263
|
* Waits for the page to hold still: three frames, the fonts, each element's
|
|
@@ -397,6 +412,8 @@ const take = async (lane, item, { elements, readyProperty }, progress) => {
|
|
|
397
412
|
let error = null
|
|
398
413
|
let problems = []
|
|
399
414
|
let findings = []
|
|
415
|
+
/* Every ink and ground pair contrast measured on the page. */
|
|
416
|
+
let painted = []
|
|
400
417
|
let aria = null
|
|
401
418
|
/* The color schemes contrast has been read in. */
|
|
402
419
|
const read = new Set()
|
|
@@ -422,7 +439,7 @@ const take = async (lane, item, { elements, readyProperty }, progress) => {
|
|
|
422
439
|
const missing = await upgrades(page, elements)
|
|
423
440
|
if (missing.length > 0) {
|
|
424
441
|
throw new Error(
|
|
425
|
-
`nothing defines ${missing.map((tag) => `<${tag}>`).join(', ')}, after
|
|
442
|
+
`nothing defines ${missing.map((tag) => `<${tag}>`).join(', ')}, after ${UPGRADES} seconds of waiting`,
|
|
426
443
|
)
|
|
427
444
|
}
|
|
428
445
|
} else if (shot.harness !== null) {
|
|
@@ -488,13 +505,13 @@ const take = async (lane, item, { elements, readyProperty }, progress) => {
|
|
|
488
505
|
if ((index === 0 && (item.checks?.clipped || item.checks?.overlap)) || contrast) {
|
|
489
506
|
progress.stage = 'reading the page'
|
|
490
507
|
read.add(shot.scheme)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
)
|
|
508
|
+
const reading = await readPage(page, {
|
|
509
|
+
clipped: index === 0 ? (item.checks.clipped ?? null) : null,
|
|
510
|
+
contrast: contrast ? { ...item.checks.contrast, scheme: shot.scheme } : null,
|
|
511
|
+
overlap: index === 0 ? (item.checks.overlap ?? null) : null,
|
|
512
|
+
})
|
|
513
|
+
findings = findings.concat(reading.findings)
|
|
514
|
+
painted = painted.concat(reading.pairs)
|
|
498
515
|
}
|
|
499
516
|
if (index === 0 && item.aria) {
|
|
500
517
|
progress.stage = 'taking its accessibility snapshot'
|
|
@@ -526,6 +543,7 @@ const take = async (lane, item, { elements, readyProperty }, progress) => {
|
|
|
526
543
|
findings,
|
|
527
544
|
key: item.key,
|
|
528
545
|
op: 'item',
|
|
546
|
+
painted,
|
|
529
547
|
problems,
|
|
530
548
|
seconds,
|
|
531
549
|
})
|