@variance-authority/storybook-collector 0.13.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,346 @@
1
1
  # @variance-authority/storybook-collector
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1a0b845: A subject is read with nothing hovered, nothing focused, motion reduced and `Date` fixed
8
+
9
+ Every layout and raster baseline made before this release is compared under a
10
+ moved recipe once; a semantic baseline, read in jsdom, does not move. The
11
+ default recipes gained tricks, so the stabilization digest every reading records
12
+ has changed, and each subject names that recipe as the cause of whatever it
13
+ reports. Review the changes and adopt them with `variance accept --all`, as for
14
+ any recipe change.
15
+
16
+ The standard collection recipe now also holds:
17
+
18
+ - `no-hover`: the pointer is moved to `(-1, -1)`, off the page, once per page
19
+ before it loads. On the `variance` fixture's page it is moved again before
20
+ every observation, a frame each, and not put back: a menu a script opened on
21
+ hover is closed for your test's next step.
22
+ - `no-focus`: whatever holds focus is blurred before the subject is read,
23
+ including an element inside a shadow root. On a page your test or agent goes
24
+ on using, focus goes back to that element once the reading and any screenshot
25
+ of it are taken, so a test types where it left off. The blur still ran the
26
+ page's blur handlers, and what they did, such as a validate-on-blur error,
27
+ stays.
28
+ - `reduced-motion`: the browser reports `prefers-reduced-motion: reduce`, which
29
+ stops the script animations of a page that honours it, and the reading
30
+ declares the same preference among the conditions its `@media` rules are
31
+ resolved against. A `prefers-reduced-motion` condition you declare through
32
+ `createHarness`'s `features` wins, and the browser is asked for that value;
33
+ a declared `no-preference` is not recorded as `reduced-motion`. Rules under
34
+ `@media (prefers-reduced-motion: no-preference)` are not read by default;
35
+ leave `reduced-motion` out to read them.
36
+ - `freeze-clock`: `Date` reads one instant. A script added before the page's
37
+ own replaces `new Date()`, `Date()` and `Date.now()`, and gives the same
38
+ instant to `Intl.DateTimeFormat`'s `format()` and `formatToParts()` called
39
+ without a date. Timers, animation frames and `performance.now()` are the
40
+ browser's own, so a page that settles on a timer still settles. No Playwright
41
+ clock is installed, so `page.clock` stays yours, and a clock your test
42
+ installs replaces the fixed `Date`; the reading still records `freeze-clock`,
43
+ so leave it out of the list when your test owns the clock. `Temporal.Now` and
44
+ `Date` inside a worker are not fixed. The default instant is in the past, and
45
+ the browser's cookie store keeps real time: a cookie your page's script sets
46
+ with an expiry counted from `Date.now()` is already expired and is not
47
+ stored, so a dismissed consent banner can come back after the next
48
+ navigation. Set `varianceNow` or `now` near today, or leave `freeze-clock`
49
+ out.
50
+
51
+ The render and layout recipes gain `no-focus` alone, because a painted document
52
+ still honours a collected `autofocus`. The other three have nothing to act on
53
+ there: the date the page printed is already text in the document, and its
54
+ `@media` rules were resolved when it was read. A `<script>` inside a subject
55
+ root is carried into the render document and runs there under the real clock.
56
+ Every document is painted under the render recipe, so a reading taken without
57
+ `no-focus` is still painted blurred.
58
+
59
+ The instant defaults to `DEFAULT_NOW`, `2024-06-15T12:00:00.000Z`, exported
60
+ from `@variance-authority/core/format`. `routeCollector` and
61
+ `storybookCollector` take a `now` option to choose another. It must be an ISO
62
+ 8601 instant with its zone; anything else is refused before a browser opens.
63
+ An instant other than the default is spelled in the clock's id,
64
+ `freeze-clock@<instant>`, so it is part of the environment key, and a baseline taken at one instant is compared
65
+ with a run at another with the moved instant named as the cause.
66
+
67
+ To read a subject hovered, focused, animated or at the real time, pass your own
68
+ `stabilize` list without that trick: `DEFAULT_STABILIZE`, the ids of the
69
+ default recipe, filtered, as in
70
+ `DEFAULT_STABILIZE.filter((id) => id !== 'freeze-clock')`. It is exported from
71
+ `@variance-authority/core/format`, `@variance-authority/route-collector`,
72
+ `@variance-authority/storybook-collector` and
73
+ `@variance-authority/playwright-test`. `storybookCollector` and `observe` in
74
+ `@variance-authority/playwright-test` now take `stabilize`, as `routeCollector`
75
+ already did.
76
+
77
+ A trick needs a browser driver to apply it, and is recorded only where one did.
78
+ The collectors apply all four. So does the `variance` fixture in
79
+ `@variance-authority/playwright-test`: it sets `reduced-motion` and
80
+ `freeze-clock` on the `page` it hands your test, before the test body runs, and
81
+ takes the pointer off the page and releases focus before every observation,
82
+ giving focus back after it. Two new fixture options, `varianceStabilize` and
83
+ `varianceNow`, choose what it sets and the instant, and are set with
84
+ `test.use`; `varianceDriven` holds the ids it set. A fixed `Date` stops code
85
+ that times itself with `Date.now()`: a debounce or a throttle, such as lodash's
86
+ `debounce`, sees no time pass and never fires, so a test that types into a
87
+ debounced search box can stop getting results after this upgrade. A loop that
88
+ waits for `Date.now()` to pass a mark hangs the page, an animation timed by
89
+ `Date`, such as jQuery's `animate`, never calls its `complete`, and a client
90
+ that checks a token's `nbf` or `iat` against `Date.now()` can refuse a token
91
+ issued today. None of these failures names the clock. Leave
92
+ `freeze-clock` out for such a test, and for one that sets `page.clock` itself:
93
+
94
+ ```ts
95
+ import { DEFAULT_STABILIZE } from '@variance-authority/playwright-test';
96
+
97
+ test.use({ varianceStabilize: DEFAULT_STABILIZE.filter((id) => id !== 'freeze-clock') });
98
+ ```
99
+
100
+ On a page the fixture never handed your test,
101
+ such as one opened from your own browser context, call the new
102
+ `driveBeforeTest(page)` before `page.goto()` to set the same two; `observe` and
103
+ `createVariance` there apply `no-hover` and `no-focus`.
104
+ `@variance-authority/vitest-browser` reads from inside your page and has no
105
+ driver; `@variance-authority/presentation` reads through the Playwright `Page`
106
+ you hand it, a test's or an agent's live browser, and does not drive it. Both
107
+ apply `no-focus` only, giving focus back once the reading is taken. A trick left out is absent from the
108
+ reading's ids and digest, as a trick a tier cannot reach already is.
109
+ `stabilizeForObservation` in `@variance-authority/dom` takes `driven`, the ids
110
+ your driver applied, and returns `refocus()`, which gives focus back to the
111
+ element the reading released. `stabilizeAsRequested` takes the recipe by id, as
112
+ a page agent receives it. `createHarness` in `@variance-authority/playwright`
113
+ takes a `stabilize` recipe to drive its page before the first navigation, and
114
+ reports the ids it applied as `driven`. `createObservedHarness` opens one with
115
+ `observeNetwork` started before its first navigation, as both collectors do.
116
+
117
+ `createPlaywrightRenderer` no longer couples `no-focus` to `waitForFonts`:
118
+ `waitForFonts: false` turns off the waits alone.
119
+
120
+ `@variance-authority/core/format` exports `DEFAULT_STABILIZE`, `DEFAULT_NOW`,
121
+ `driveBeforeLoad` and `driveBeforeRead`, which apply the driver tricks to a
122
+ Playwright `Page`, `forDriver`, which drops the ones a host did not apply,
123
+ `featuresOf`, which declares the motion preference a recipe drove, and
124
+ `recipeToDrive`, which resolves a `stabilize` list and checks `now` before a
125
+ browser opens.
126
+ - d432339: A page reads its clock in UTC and formats for en-US, not for the host
127
+
128
+ The harness the route and Storybook collectors open, and the pages the renderer
129
+ paints, took their timezone and locale from the machine. A date the page
130
+ formatted read one way on a laptop in Sydney and another on a runner in
131
+ Virginia, from one build, and the two compared against one baseline as
132
+ `changed`.
133
+
134
+ Both are pinned: `timezoneId` to `UTC` and `locale` to `en-US`. Declare either
135
+ in `variance.config.json` (`"timezoneId": "Europe/Berlin"`, `"locale":
136
+ "de-DE"`) and the collectors and the renderer pin that instead; an unknown
137
+ timezone or a malformed language tag is refused by its field.
138
+ `createHarness` and `createPlaywrightRenderer` take the same two options.
139
+ `@variance-authority/core/format` exports their type, `PageRegion`, and
140
+ `@variance-authority/playwright` exports `pinnedRegion`, which fills in `UTC`
141
+ and `en-US` for a field left out.
142
+
143
+ Both are part of the environment key, and the renderer's pair is part of
144
+ `RenderIdentity.rasterization`. A capture read in another timezone or language
145
+ is another environment, and an image painted in one is compared with a
146
+ baseline painted in another, with `rasterization` named as the cause of
147
+ whatever moved.
148
+
149
+ `@variance-authority/playwright-test` paints each test in the timezone and
150
+ locale its page runs in: whatever the Playwright project or `test.use` sets.
151
+ When neither sets a `timezoneId`, `varianceFixtures` opens the page in `UTC`
152
+ rather than the host's, so a laptop and a runner in UTC read it under one key;
153
+ Playwright Test already opens it in `en-US` when nothing sets a `locale`. That
154
+ reaches every test of a suite extended with `varianceFixtures`, including tests
155
+ that never call `variance`, so a test that asserts a date formatted in your
156
+ machine's timezone reads it in `UTC` after this upgrade; set `timezoneId` in
157
+ `use` to keep your own. A page
158
+ you hand to `createVariance` is read and painted in whatever its own context
159
+ opened it in, which is the host's timezone when that context set none: open the
160
+ context with `timezoneId: 'UTC'` to match. `varianceRenderer` is now a test fixture, chosen
161
+ by the page, and `varianceRenderers` is the worker's set of renderers, one per
162
+ timezone and locale; a project that overrode `varianceRenderer` at worker scope
163
+ overrides it at test scope instead.
164
+
165
+ **Every image painted by the Playwright renderer is compared under a moved
166
+ recipe once after this upgrade**, because its recipe now carries the timezone
167
+ and locale. Each subject names `rasterization` as the cause of what it reports,
168
+ and one whose pixels moved is `changed`:
169
+
170
+ - With `variance run`: the run exits `1` for each `changed` subject. Review
171
+ them in `variance report --format html` and adopt them with
172
+ `variance accept --all`. A rerun reports `unchanged`.
173
+ - With `@variance-authority/playwright-test`: the suite fails each `changed`
174
+ test, and `--update-snapshots=changed` adopts them.
175
+ - With `@variance-authority/vitest-browser`: `vitest --update` adopts them, as
176
+ `variance accept --all` does.
177
+
178
+ `@variance-authority/vitest-browser` reads no `variance.config.json` and takes no
179
+ timezone or locale, so it paints in `UTC` and `en-US` whatever its tab ran in,
180
+ and neither it nor `@variance-authority/unit-test` writes a timezone or locale
181
+ to the environment key.
182
+ - 3e6a370: A run's cases travel in its record
183
+
184
+ The case index, the cases a run replaced and the run that replaced them are
185
+ sections of `coverage.bin`, no longer a `coverage.bin.cases.bin` beside it. The
186
+ one file is landed, layered, seeded, repinned, sharded and shared by the same
187
+ rules and under the same lock as the coverage it was recorded with, so the two
188
+ always answer for the same runs. A share, a seed or a fetch carries the index
189
+ and drops the replaced cases and the run that names them, which belong to the
190
+ machine that ran.
191
+
192
+ A record that carries cases is format 10, and a reader that knows only coverage
193
+ refuses it rather than misreading it. A record without cases keeps format 9. A
194
+ `coverage.bin.cases.bin` left from an earlier run is not read; the next run
195
+ writes its cases into the record.
196
+
197
+ The `executionFile` option is removed from `withTestSelection`, the Jest and
198
+ rstest seams, `startRecording`, the Playwright reporter and the Storybook
199
+ collector, along with the JSON it could write. `decodeExecutionIndex`,
200
+ `readExecutionIndex`, `variance covering --against`, `variance review` and
201
+ `distill --execution` read the index out of a record; JSON stays readable as
202
+ the spelling a foreign tool supplies. `landCaseIndexes` is replaced by
203
+ `landCases`, which returns the sections for the record you write, and
204
+ `lastCaseRunOf` reads the run they name. `caseLayerFiles` and
205
+ `executionIndexBytes`, which named and read the file beside the record, are
206
+ removed. `CaseSections`, `caseSectionsAt`, `caseSectionsOf`, `caseIndexOf`,
207
+ `recordedCases`, `withCaseSections`, `keepsCases` and `sharedRecord` read and
208
+ write the sections.
209
+ - 754d04c: The port a build is served on is no part of a render
210
+
211
+ The route and Storybook collectors serve a build on a free port, and that port
212
+ reached three identities. An asset the page loaded was keyed by its absolute
213
+ URL, a `url()` in a computed style was the absolute URL the engine resolved,
214
+ and the document digest read both. One build served twice therefore read as two
215
+ environment keys and two documents: each run missed the render cache, painted
216
+ again, and read its document as changed against its own baseline.
217
+
218
+ An asset the page's own origin serves is keyed by its path, and the page's
219
+ origin is dropped from a `url()` before it is hashed. An asset another origin
220
+ serves keeps its absolute URL. A capture carries the page's `baseUrl` so core
221
+ knows which origin is the page's, and the renderer resolves a path-keyed asset
222
+ against the document's `baseUrl`. `documentDigest` reads the page's own base
223
+ and resources as paths, so one build served on two ports addresses one render
224
+ and the render cache hits; the document keeps them absolute for the renderer,
225
+ and a resource another origin serves stays absolute in the digest too.
226
+
227
+ A `@variance-authority/unit-test` capture keys its assets the same way, so a
228
+ `@variance-authority/vitest-browser` observation, which captures through it on
229
+ the port the runner had free, keeps the port out of its environment key.
230
+ `readCapture` reads back a capture whose asset is keyed by path, resolving the
231
+ key against the document's `baseUrl` as the renderer does; `assetUrl` in
232
+ `@variance-authority/core/format` is that one resolution.
233
+
234
+ **The document of every page served over `http` moves once after this
235
+ upgrade.** `documentDigest` now reads a document's base and resources without
236
+ the page's origin, so the digest of every document served over `http` or
237
+ `https` changes: every route, every story, every Vitest browser page, and every
238
+ Playwright page opened by URL. A moved document misses the render cache once,
239
+ and its image is still compared with the baseline: a page whose pixels did not
240
+ move reads `unchanged`, and one whose pixels did reads `changed`, which
241
+ `variance accept --all` and Playwright's `--update-snapshots=changed` adopt.
242
+ `RULESET_VERSION` moves from `r1` to `r2`. The ruleset is part of a snapshot's
243
+ environment key, not of the render recipe, so no image names it as a moved
244
+ identity; a snapshot diffed against one recorded under `r1` names the ruleset as its
245
+ environment root.
246
+
247
+ ### Patch Changes
248
+
249
+ - 8dbc834: The components a module declares are read from its code. A declaration written
250
+ in a block comment, a JSDoc example or a template literal no longer names a
251
+ component, so a commented-out `function Retired()` no longer makes its file
252
+ the one that declares `Retired`. A name is declared by a statement of the
253
+ module itself: a function, class or binding inside a function body belongs to
254
+ that function and no longer counts. An `export async function Page()`, an
255
+ abstract class, a generator, a second name in `const A = 1, B = 2`, and a name
256
+ that starts with a non-ASCII capital or carries a `$` are now declared too.
257
+
258
+ `variance run`, `variance collect`, the Storybook collector and the route
259
+ collector resolve a component to its `file:line` from the same reading, through
260
+ `indexDeclarations`, which `sense` now exports. What that changes in the index
261
+ they build from `source.dirs`:
262
+
263
+ - The line is the first line of the declaring statement. A decorated class
264
+ sits at its first decorator, or at its `export` when the decorator is written
265
+ above the `export`, and a second name in a multi-line `const` at the line of
266
+ the `const`.
267
+ - `.d.ts` files, and test, spec and story files your own `exclude` lets
268
+ through, declare nothing.
269
+ - A file the parser cannot read declares nothing, where the text scan found
270
+ the components in it. That covers a Flow-annotated `.js` file, and a `.vue`,
271
+ `.svelte` or `.mdx` file you add to a collector's `extensions`, which is
272
+ parsed as TSX and fails. The index built from `source.dirs` or a collector's
273
+ `source` does not say so: the file's components are missing, and nothing
274
+ names the file. Only `sense`'s source index, for a file it records, names the
275
+ parse error in that file's record.
276
+ - Building the index needs `sense`'s native addon, which ships for macOS on
277
+ arm64, Linux on x64 and arm64 with glibc, and Windows on x64. On any other
278
+ machine, including an Intel Mac, an Alpine image and Windows on arm64, a
279
+ configured `source.dirs` fails at its first file and names why the addon did
280
+ not load, where the text scan ran anywhere. The route collector depends on
281
+ `sense` for it.
282
+
283
+ `indexSource` is removed from `@variance-authority/core/attribute`. Build a
284
+ `SourceIndex` with `indexDeclarations` from `@variance-authority/sense`, or
285
+ write one as plain data: a map from component name to `{ file, line, via }`.
286
+ `SourceRef['via']` loses `'declared'`, which nothing produced, and
287
+ `readCapture` in `@variance-authority/unit-test` refuses a capture whose source
288
+ index carries it.
289
+
290
+ The source index format moves to version 18, so an index written before this
291
+ release is rebuilt once instead of keeping the names it read from comments.
292
+ - 1eed290: A form filled in after mount is painted and compared filled in
293
+
294
+ Text a play function or a test typed into an `<input>` or `<textarea>`, the
295
+ option it chose in a `<select>` and the box it ticked now reach the render
296
+ document and the snapshot. Before, the document painted the form as the server
297
+ sent it and the snapshot did not move for typed text or a chosen option. A
298
+ password field's value is carried as one `•` per character, and a file input's
299
+ value is not carried. A form nobody filled in reads exactly as before.
300
+
301
+ This is part of `RULESET_VERSION` `r2`. A subject whose form was filled in
302
+ after the server sent it has a new render document, so after this upgrade its
303
+ image shows the form filled in and reads `changed` against a baseline painted
304
+ empty; `variance accept --all` adopts it. A Playwright test painted its form
305
+ filled in before this change, so it does not move there.
306
+ - ef496ad: READMEs name what the program does
307
+
308
+ The package READMEs, and the `@variance-authority/vantage` and
309
+ `@variance-authority/playwright-test` descriptions, no longer write a test, a
310
+ record or a run as something that says, asks or knows. Each sentence names what
311
+ the program does: a command prints, a record holds a field, a test runs or
312
+ covers. Where a value is filled in from configuration or a default rather than
313
+ recorded, the README says so. Four renamed headings change their anchors:
314
+ `help`'s "Where the declaration is undocumented", `playwright`'s "Where a
315
+ component is declared, read from the engine", `storybook`'s "What a pass sends
316
+ the preview" and `sense`'s "Correct what a file's text declares it imports".
317
+ - 4799688: The checkout says what the probes meant: no build writes module records
318
+
319
+ A probe names the file it was placed on and the digest of that text,
320
+ `path@digest`. When a run is folded, each module the journals name is cut again
321
+ from the file in the checkout. A file whose text still matches is read on its
322
+ own regions; a file that has moved on since the build reads as one whose reach
323
+ is not known, which selects every test that entered it; a file that is gone
324
+ counts the same way. Nothing is written beside the build, and nothing under the
325
+ cache's `test-selection/<key>/<label>/` is read.
326
+
327
+ Breaking:
328
+
329
+ - `testSelectionProbes()` takes no `cacheRoot`. Its `label` names the journey
330
+ head and nothing else.
331
+ - `recordExecution()` takes no `label` and no `heads`.
332
+ - The Jest journeys options (`withJourneyCoverage`) take no `heads`.
333
+ - `jestStore` is removed from `@variance-authority/sense/jest`.
334
+ - `instrumentModule(code, file)` takes no options: call it first, on the file as
335
+ it is on disk.
336
+ - A journal names a module by `path@digest`. A driver that writes its own
337
+ journal builds the id with `moduleId(path, code)` from
338
+ `@variance-authority/sense/journal`.
339
+
340
+ ## 0.14.0
341
+
342
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
343
+
3
344
  ## 0.13.0
4
345
 
5
346
  Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
package/README.md CHANGED
@@ -34,7 +34,7 @@ This is the Storybook adapter, not the `variance` binary. The binary lives in
34
34
  | Module format | ESM only. Every `@variance-authority/*` package sets `"type": "module"`; `require()` will not load them. |
35
35
  | Browser | Chromium, installed through Playwright. The binaries do not arrive with an `npm install`. |
36
36
  | Storybook | Exercised against Storybook 10 with `@storybook/react-vite`. Index versions `3`, `4` and `5` are read, and the story root is looked for at `#storybook-root` (Storybook 7 and later) and then `#root` (before it). Storybook 8.3 and later emit `storyFinished`, which this package waits for; it falls back to `storyRendered` when the event is absent. |
37
- | Builder | Collection reads `index.json` and drives the preview channel, so it does not know or care whether Vite or Webpack 5 built the Storybook. Two optional precision features are Vite plugins with no Webpack equivalent, and are named as such where they appear below: `@variance-authority/jsx-source` and the `tests` option. |
37
+ | Builder | Collection reads `index.json` and drives the preview channel, so it works the same whether Vite or Webpack 5 built the Storybook. Two optional precision features are Vite plugins with no Webpack equivalent, and are named as such where they appear below: `@variance-authority/jsx-source` and the `tests` option. |
38
38
  | React | Required for component attribution, the `wiring` band and the Suspense wait. The reading is of the expando `react-dom` writes on host nodes — `__reactFiber$` on React 17 and later, `__reactInternalInstance$` on React 16 — so there is no React version to match and none is imported. The suite runs against React 19. |
39
39
 
40
40
  A Storybook whose renderer is not React still collects: documents, pixels,
@@ -64,7 +64,7 @@ the build.
64
64
 
65
65
  ### 2. Write the collector module
66
66
 
67
- The collector is the module that tells the run how to find and read your
67
+ The collector is the module that defines how the run finds and reads your
68
68
  stories. Create it anywhere in your project and default-export the factory.
69
69
 
70
70
  ```js
@@ -146,8 +146,9 @@ full form is:
146
146
  npx variance accept [--config <path>] <subject>... | --all | --shape <fingerprint>[,...] [--message-file <path> [--message <text>]]
147
147
  ```
148
148
 
149
- Keep `--all` for the first run and for deliberate re-baselines; it cannot tell a
150
- story nobody looked at from one that changed. After setup, name subject ids:
149
+ Keep `--all` for the first run and for deliberate re-baselines; it does not
150
+ separate a story nobody looked at from one that changed. After setup, name
151
+ subject ids:
151
152
 
152
153
  ```bash
153
154
  npx variance accept --config variance.config.json story:checkout--empty story:checkout--one-item
@@ -207,6 +208,8 @@ Everything below is passed to `storybookCollector(...)` in the module from step
207
208
  | `source` | Reports should resolve component names to `file:line`. | Omitted; component names remain available. An empty or mistyped scan is refused. |
208
209
  | `baseUrl` | Storybook is already running, e.g. `http://localhost:6006`. | Omitted; the directory containing `subjects.index` is served on loopback for the run. |
209
210
  | `headless` | You need to watch collection while debugging. | `true`. |
211
+ | `stabilize` | A story is about its hover, focus, motion or the current date, or your preview has its own determinism strategy. | The standard collection recipe, which also takes the pointer off the page, releases focus, asks for reduced motion and fixes `Date`; `[]` records an untouched story. A list replaces the recipe: start from the exported `DEFAULT_STABILIZE` and filter out what you want read, as in `DEFAULT_STABILIZE.filter((id) => id !== 'no-hover')`. |
212
+ | `now` | Your stories print a date and you want one other than the default. | `2024-06-15T12:00:00.000Z`. An ISO 8601 instant with its zone; anything else is refused before a browser opens. |
210
213
  | `network` | Asset bytes at stable URLs must participate in render identity. | `true`; set `false` only when asset URLs are already content-addressed. |
211
214
  | `hashAssets` | Your asset URLs already contain their own content hash. | `true`. Read only while `network` is on, so GIF freezing and blanking survive it — this, not `network: false`, is the setting for a content-addressed build. |
212
215
  | `wiring` | Your preview's renderer is not React. | `true`. Reads props, context, hook cells and keys into a band of its own; turning it off changes no stored digest. |
@@ -356,7 +359,7 @@ export default {
356
359
  framework: { name: '@storybook/react-vite', options: {} },
357
360
  viteFinal: async (config) => ({
358
361
  ...config,
359
- plugins: [...(config.plugins ?? []), testSelectionProbes({ label: 'storybook' })],
362
+ plugins: [...(config.plugins ?? []), testSelectionProbes()],
360
363
  }),
361
364
  };
362
365
  ```
@@ -367,19 +370,21 @@ import { storybookCollector } from '@variance-authority/storybook-collector';
367
370
 
368
371
  export default storybookCollector({
369
372
  source: { dirs: ['src'] },
370
- tests: { label: 'storybook' },
373
+ tests: true,
371
374
  });
372
375
  ```
373
376
 
374
- `tests` accepts `true` or an options object: `label` must match the one the
375
- preview's `testSelectionProbes()` was given, and `cacheRoot` and `coverageFile`
376
- override the repository-keyed cache paths for the block records and the coverage
377
- index. `suite` names the suite this run is, when the root `variance.config.json`
377
+ `tests` accepts `true` or an options object: `cacheRoot` and `coverageFile`
378
+ override the repository-keyed cache paths for the coverage layers and the
379
+ coverage index. The preview's probes report each module's file and a digest of
380
+ the text they were placed on, and the run cuts that file again from your
381
+ checkout, so the preview's build writes nothing the run has to find. `suite` names the suite this run is, when the root `variance.config.json`
378
382
  declares its suites under `suites`; it is required once any suite is declared,
379
- and cannot be combined with `coverageFile`. `root` is the directory Storybook ran in, the cwd by default: story paths
380
- and a relative `coverageFile` or `executionFile` are read from it, and every
381
- path the run records is relative to the repository that contains it.
382
- Without a collector in the page, the run says so on stderr and records nothing.
383
+ and cannot be combined with `coverageFile`. `root` is the directory Storybook
384
+ ran in, the cwd by default: story paths and a relative `coverageFile` are read
385
+ from it, and every path the run records is relative to the repository that
386
+ contains it. Without a collector in the page, the run prints that on stderr and
387
+ records nothing.
383
388
 
384
389
  `mode` is the probe recipe, and it has to be the one the preview was built
385
390
  under: a snapshot names the recipe its ordinals were cut by, and a merge
@@ -387,17 +392,17 @@ discards a layer cut by another one, so a preview probing under `entries` and a
387
392
  run folding under `presence` would each wipe the other. `preconditions` names
388
393
  files whose contents are a precondition of every story's observation — a
389
394
  `preview.js` that decides what renders belongs there, because a change to it
390
- invalidates every reading and no crossing will ever say so. `heads` and
391
- `commit` stamp the snapshot with the branch tips this recording stands on and
392
- the commit it was taken at, which is what lets a later run tell a stale layer
393
- from a current one.
395
+ invalidates every reading and no crossing will ever record it. `commit`
396
+ stamps the snapshot with the commit it was taken at, the checkout's `HEAD` by
397
+ default, which is what lets a later run separate a stale layer from a current
398
+ one.
394
399
 
395
400
  A story is its own owner in the recorded index — Storybook is an execution
396
401
  surface this tool drives one subject at a time — and a story that did not render
397
402
  still contributes its crossings while never justifying a later skip.
398
403
 
399
404
  Every run also writes the execution index: one row per story per region,
400
- beside the coverage index, or wherever `executionFile` points. That is the file
405
+ inside the coverage index. That is the file
401
406
  `variance covering` and `@variance-authority/distill` read to answer *which
402
407
  stories walk this branch*.
403
408
 
@@ -431,8 +436,9 @@ a diff touches.
431
436
  URL already identifies the bytes.
432
437
  - **The browser is missing:** run `npx playwright install chromium`, then
433
438
  `npx variance doctor` again.
434
- - **A rerun reports `incomparable`:** the approved image was stored under a
435
- different identity than this machine produces. `npx variance doctor` lists the
439
+ - **A rerun reports every story `changed`, a moved identity first:** the
440
+ approved image was stored under a different identity than this machine
441
+ produces. `npx variance doctor` lists the
436
442
  identities the baseline root stores.
437
443
 
438
444
  ## Boundaries
@@ -1,5 +1,5 @@
1
1
  import type { CallSiteResolver, SourceIndex } from '@variance-authority/core/attribute';
2
- import type { Diagnostic, RenderDocument, SemanticSnapshot, SubjectRef, Viewport } from '@variance-authority/core/format';
2
+ import type { Diagnostic, PageRegion, RenderDocument, SemanticSnapshot, SubjectRef, Viewport } from '@variance-authority/core/format';
3
3
  /**
4
4
  * The contract a collector satisfies, restated here rather than imported.
5
5
  *
@@ -10,8 +10,11 @@ import type { Diagnostic, RenderDocument, SemanticSnapshot, SubjectRef, Viewport
10
10
  * collector in its own tests, so a drift between them fails a build rather than
11
11
  * a run.
12
12
  */
13
- /** The subset of a run's configuration this collector reads. */
14
- export interface CollectorConfig {
13
+ /**
14
+ * The subset of a run's configuration this collector reads. A region field it
15
+ * leaves out is pinned to `UTC` or `en-US`, never the host's.
16
+ */
17
+ export interface CollectorConfig extends PageRegion {
15
18
  readonly viewport: Viewport;
16
19
  readonly fonts?: readonly string[];
17
20
  /**
@@ -102,9 +105,9 @@ export type Collected = {
102
105
  * Stabilization tricks applied to the page before this subject was read.
103
106
  *
104
107
  * Reported so a run can say what it did to somebody else's page. The
105
- * digest of the same list is in the environment key, which is what makes a
106
- * differently-stabilized baseline `incomparable`; this is the half a
107
- * person reads.
108
+ * digest of the same list is in the environment key, which is what names a
109
+ * differently-stabilized baseline as the cause of a change; this is the
110
+ * half a person reads.
108
111
  */
109
112
  readonly stabilization?: readonly string[];
110
113
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"","sourcesContent":["import type { CallSiteResolver, SourceIndex } from '@variance-authority/core/attribute';\nimport type {\n Diagnostic,\n RenderDocument,\n SemanticSnapshot,\n SubjectRef,\n Viewport,\n} from '@variance-authority/core/format';\n\n/**\n * The contract a collector satisfies, restated here rather than imported.\n *\n * The CLI declares the same shapes and this package does not depend on it — a\n * surface package pulling in the binary would put a config parser, a docket and\n * six commands into an adopter's dependency tree to describe four values\n * (ADR-0024). What keeps the two copies honest is that the CLI imports *this*\n * collector in its own tests, so a drift between them fails a build rather than\n * a run.\n */\n\n/** The subset of a run's configuration this collector reads. */\nexport interface CollectorConfig {\n readonly viewport: Viewport;\n readonly fonts?: readonly string[];\n /**\n * Subtrees this project excludes, from `config.ignore` (spec 0024).\n *\n * Only the two fields a page needs: a rule id to record the mark under, and a\n * selector to find it with. Everything else about a rule — its reason, its\n * expiry, the bands it narrows to — is decided after collection, where a clock\n * and the whole run's diffs are available and a browser is not.\n */\n readonly ignore?: readonly {\n readonly id: string;\n readonly select?: string;\n /**\n * Subjects the rule names. Read here to decide what to send, and never sent.\n *\n * A page can resolve a selector and cannot know which subject it is one of,\n * so scoping has to happen on this side of the boundary. Without it a rule\n * written for one route marks its element in every subject that renders the\n * same shared layout, and a real regression inside that element is absorbed\n * everywhere — an ignore silencing more than it says, which is the one thing\n * the mechanism must not do.\n */\n readonly subjects?: readonly string[];\n\n /** Tags the subject must carry. Read here to decide what to send, never sent. */\n readonly tags?: readonly string[];\n }[];\n\n /**\n * Images to serve as nothing, from `config.blank`.\n *\n * Handed to the driver rather than to the page, because it is enforced on the\n * wire — before a byte is decoded, and therefore before the layout it would\n * have participated in exists. Not scoped by subject, and it cannot be: a\n * request carries no idea which subject will end up using it, which is the\n * same reason the asset map is per page rather than per subject.\n */\n readonly blank?: readonly {\n readonly id: string;\n readonly url?: string;\n readonly minPixels?: number;\n readonly maxPixels?: number;\n }[];\n\n /**\n * The subjects section of the config, as the operator wrote it.\n *\n * `index` is optional because it belongs to `subjects.kind: \"storybook\"`, and\n * a config naming any other kind still reaches this collector — the kind is\n * chosen in the config and the collector is chosen in the config, and nothing\n * makes the operator pick a matching pair. Typed as always present, the first\n * thing to read it hands `undefined` to `node:path` and the adopter gets a\n * TypeError about an argument instead of a sentence naming the wrong word.\n */\n readonly subjects: { readonly index?: string };\n}\n\nexport interface PlannedSubject {\n readonly subject: SubjectRef;\n readonly viewport?: Viewport;\n\n /**\n * What the subject declares itself to be, from the artifact that produced it.\n *\n * Storybook's built index carries `tags` and does not carry a story's\n * `parameters`, so a tag is the one per-story declaration the index can plan\n * from — and it is the right one anyway: what a subject *is* belongs in its\n * own name, next to it, rather than in a central file repeating every id.\n * How a story is *read* — excluded, resized, read at several widths — comes\n * from its parameters, which `withParameters` lays over this plan from the\n * running preview.\n *\n * Selection lives here; definition lives in the config. A tag is a word a\n * story wears, and what that word *means* is the operator's to write down\n * once, where a typo can be refused by name.\n */\n readonly tags?: readonly string[];\n}\n\nexport interface Plan {\n readonly subjects: readonly PlannedSubject[];\n readonly notObserved: readonly unknown[];\n readonly warnings: readonly string[];\n}\n\nexport type Collected =\n | {\n readonly ok: true;\n readonly document: RenderDocument;\n readonly snapshot?: SemanticSnapshot;\n readonly source?: SourceIndex;\n\n /**\n * Stabilization tricks applied to the page before this subject was read.\n *\n * Reported so a run can say what it did to somebody else's page. The\n * digest of the same list is in the environment key, which is what makes a\n * differently-stabilized baseline `incomparable`; this is the half a\n * person reads.\n */\n readonly stabilization?: readonly string[];\n\n /**\n * What the adapter noticed about the *way* this subject was produced.\n *\n * Not about the picture — the document and the snapshot carry that — but\n * about the reading: a story Storybook finished by reporting it failed, a\n * preview that had to be reloaded to show it, a render that never\n * finished. Each of those leaves a real image that a run can compare, and\n * each of them changes what a person should conclude from a green verdict,\n * so they travel with the subject rather than being dropped at the seam.\n */\n readonly diagnostics?: readonly Diagnostic[];\n }\n | { readonly ok: false; readonly because: string };\n\nexport interface CollectorContext {\n readonly config: CollectorConfig;\n /** Computed by the run from the story index. Returned unchanged. */\n readonly plan?: Plan;\n}\n\nexport interface Collector {\n plan(): Promise<Plan>;\n collect(subject: PlannedSubject): Promise<Collected>;\n\n /**\n * The frames a snapshot carries, spent on demand.\n *\n * Present when the collector drives a browser, because resolving a frame means\n * fetching the module it names and the page is the only place that request is\n * already correct. Absent otherwise, and absence is not a degradation: it means\n * either that nothing captured frames, or that the run has no live page to\n * fetch through — and both are a report without call sites rather than a\n * report that is wrong.\n *\n * Handed to `locateSites` with the few nodes a region or a finding names. The\n * resolver is per collector rather than per subject: the cache is what makes\n * this bounded, and a suite's subjects share their modules.\n */\n readonly callSites?: CallSiteResolver;\n\n /**\n * Collect this subject again, in a world nothing else has touched.\n *\n * The saving this whole tool is built on is that the world is *not* rebuilt\n * between subjects (ADR-0009): one browser, one page, one Storybook, for the\n * length of a run. What it buys is the possibility that story B renders\n * differently because story A ran first — and a comparison cannot tell that\n * apart from a regression, because both arrive as \"the pixels moved\".\n *\n * One clean collection settles it. If the difference is gone with nothing else\n * in the world, the baseline was right and the session moved this subject, and\n * the run reports `order-dependent` rather than asking anybody to accept it.\n *\n * Optional in the contract the CLI declares, because a collector holding a\n * single page open has no clean world to offer and the run says so rather than\n * reading silence as \"nothing leaked\". This collector implements it: a\n * Storybook preview can be opened twice, and the second one costs a browser\n * launch — which is why the run only spends it on subjects it already called\n * `changed`, up to `alone.limit`.\n */\n collectAlone?(subject: PlannedSubject): Promise<Collected>;\n\n /**\n * Another standing world from the same recipe and the same server, for a\n * second worker. Optional in the contract the CLI declares; absent, a run\n * configured with `workers` collects in one world and says so.\n */\n openWorker?(): Promise<Collector>;\n\n /**\n * `costs` is what the run timed for each story, first collection to\n * decision, by subject id; the execution record carries it as each story's\n * duration.\n */\n close(costs?: ReadonlyMap<string, number>): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"","sourcesContent":["import type { CallSiteResolver, SourceIndex } from '@variance-authority/core/attribute';\nimport type {\n Diagnostic,\n PageRegion,\n RenderDocument,\n SemanticSnapshot,\n SubjectRef,\n Viewport,\n} from '@variance-authority/core/format';\n\n/**\n * The contract a collector satisfies, restated here rather than imported.\n *\n * The CLI declares the same shapes and this package does not depend on it — a\n * surface package pulling in the binary would put a config parser, a docket and\n * six commands into an adopter's dependency tree to describe four values\n * (ADR-0024). What keeps the two copies honest is that the CLI imports *this*\n * collector in its own tests, so a drift between them fails a build rather than\n * a run.\n */\n\n/**\n * The subset of a run's configuration this collector reads. A region field it\n * leaves out is pinned to `UTC` or `en-US`, never the host's.\n */\nexport interface CollectorConfig extends PageRegion {\n readonly viewport: Viewport;\n readonly fonts?: readonly string[];\n /**\n * Subtrees this project excludes, from `config.ignore` (spec 0024).\n *\n * Only the two fields a page needs: a rule id to record the mark under, and a\n * selector to find it with. Everything else about a rule — its reason, its\n * expiry, the bands it narrows to — is decided after collection, where a clock\n * and the whole run's diffs are available and a browser is not.\n */\n readonly ignore?: readonly {\n readonly id: string;\n readonly select?: string;\n /**\n * Subjects the rule names. Read here to decide what to send, and never sent.\n *\n * A page can resolve a selector and cannot know which subject it is one of,\n * so scoping has to happen on this side of the boundary. Without it a rule\n * written for one route marks its element in every subject that renders the\n * same shared layout, and a real regression inside that element is absorbed\n * everywhere — an ignore silencing more than it says, which is the one thing\n * the mechanism must not do.\n */\n readonly subjects?: readonly string[];\n\n /** Tags the subject must carry. Read here to decide what to send, never sent. */\n readonly tags?: readonly string[];\n }[];\n\n /**\n * Images to serve as nothing, from `config.blank`.\n *\n * Handed to the driver rather than to the page, because it is enforced on the\n * wire — before a byte is decoded, and therefore before the layout it would\n * have participated in exists. Not scoped by subject, and it cannot be: a\n * request carries no idea which subject will end up using it, which is the\n * same reason the asset map is per page rather than per subject.\n */\n readonly blank?: readonly {\n readonly id: string;\n readonly url?: string;\n readonly minPixels?: number;\n readonly maxPixels?: number;\n }[];\n\n /**\n * The subjects section of the config, as the operator wrote it.\n *\n * `index` is optional because it belongs to `subjects.kind: \"storybook\"`, and\n * a config naming any other kind still reaches this collector — the kind is\n * chosen in the config and the collector is chosen in the config, and nothing\n * makes the operator pick a matching pair. Typed as always present, the first\n * thing to read it hands `undefined` to `node:path` and the adopter gets a\n * TypeError about an argument instead of a sentence naming the wrong word.\n */\n readonly subjects: { readonly index?: string };\n}\n\nexport interface PlannedSubject {\n readonly subject: SubjectRef;\n readonly viewport?: Viewport;\n\n /**\n * What the subject declares itself to be, from the artifact that produced it.\n *\n * Storybook's built index carries `tags` and does not carry a story's\n * `parameters`, so a tag is the one per-story declaration the index can plan\n * from — and it is the right one anyway: what a subject *is* belongs in its\n * own name, next to it, rather than in a central file repeating every id.\n * How a story is *read* — excluded, resized, read at several widths — comes\n * from its parameters, which `withParameters` lays over this plan from the\n * running preview.\n *\n * Selection lives here; definition lives in the config. A tag is a word a\n * story wears, and what that word *means* is the operator's to write down\n * once, where a typo can be refused by name.\n */\n readonly tags?: readonly string[];\n}\n\nexport interface Plan {\n readonly subjects: readonly PlannedSubject[];\n readonly notObserved: readonly unknown[];\n readonly warnings: readonly string[];\n}\n\nexport type Collected =\n | {\n readonly ok: true;\n readonly document: RenderDocument;\n readonly snapshot?: SemanticSnapshot;\n readonly source?: SourceIndex;\n\n /**\n * Stabilization tricks applied to the page before this subject was read.\n *\n * Reported so a run can say what it did to somebody else's page. The\n * digest of the same list is in the environment key, which is what names a\n * differently-stabilized baseline as the cause of a change; this is the\n * half a person reads.\n */\n readonly stabilization?: readonly string[];\n\n /**\n * What the adapter noticed about the *way* this subject was produced.\n *\n * Not about the picture — the document and the snapshot carry that — but\n * about the reading: a story Storybook finished by reporting it failed, a\n * preview that had to be reloaded to show it, a render that never\n * finished. Each of those leaves a real image that a run can compare, and\n * each of them changes what a person should conclude from a green verdict,\n * so they travel with the subject rather than being dropped at the seam.\n */\n readonly diagnostics?: readonly Diagnostic[];\n }\n | { readonly ok: false; readonly because: string };\n\nexport interface CollectorContext {\n readonly config: CollectorConfig;\n /** Computed by the run from the story index. Returned unchanged. */\n readonly plan?: Plan;\n}\n\nexport interface Collector {\n plan(): Promise<Plan>;\n collect(subject: PlannedSubject): Promise<Collected>;\n\n /**\n * The frames a snapshot carries, spent on demand.\n *\n * Present when the collector drives a browser, because resolving a frame means\n * fetching the module it names and the page is the only place that request is\n * already correct. Absent otherwise, and absence is not a degradation: it means\n * either that nothing captured frames, or that the run has no live page to\n * fetch through — and both are a report without call sites rather than a\n * report that is wrong.\n *\n * Handed to `locateSites` with the few nodes a region or a finding names. The\n * resolver is per collector rather than per subject: the cache is what makes\n * this bounded, and a suite's subjects share their modules.\n */\n readonly callSites?: CallSiteResolver;\n\n /**\n * Collect this subject again, in a world nothing else has touched.\n *\n * The saving this whole tool is built on is that the world is *not* rebuilt\n * between subjects (ADR-0009): one browser, one page, one Storybook, for the\n * length of a run. What it buys is the possibility that story B renders\n * differently because story A ran first — and a comparison cannot tell that\n * apart from a regression, because both arrive as \"the pixels moved\".\n *\n * One clean collection settles it. If the difference is gone with nothing else\n * in the world, the baseline was right and the session moved this subject, and\n * the run reports `order-dependent` rather than asking anybody to accept it.\n *\n * Optional in the contract the CLI declares, because a collector holding a\n * single page open has no clean world to offer and the run says so rather than\n * reading silence as \"nothing leaked\". This collector implements it: a\n * Storybook preview can be opened twice, and the second one costs a browser\n * launch — which is why the run only spends it on subjects it already called\n * `changed`, up to `alone.limit`.\n */\n collectAlone?(subject: PlannedSubject): Promise<Collected>;\n\n /**\n * Another standing world from the same recipe and the same server, for a\n * second worker. Optional in the contract the CLI declares; absent, a run\n * configured with `workers` collects in one world and says so.\n */\n openWorker?(): Promise<Collector>;\n\n /**\n * `costs` is what the run timed for each story, first collection to\n * decision, by subject id; the execution record carries it as each story's\n * duration.\n */\n close(costs?: ReadonlyMap<string, number>): Promise<void>;\n}\n"]}
@@ -21,23 +21,12 @@ export interface StoryExecutionOptions {
21
21
  * read from here, because it is relative to the directory Storybook ran in.
22
22
  */
23
23
  readonly root?: string;
24
- /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */
25
- readonly label?: string;
26
- /** Where the build wrote its block records. Defaults to the repository's cache. */
24
+ /** Where the coverage layers live. Defaults to the repository's cache. */
27
25
  readonly cacheRoot?: string;
28
26
  /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */
29
27
  readonly coverageFile?: string;
30
28
  /** The suite this run is, as the root `variance.config.json` declares it under `suites`. */
31
29
  readonly suite?: string;
32
- /**
33
- * Where the execution index — which individual story entered which region —
34
- * goes. Defaults beside the snapshot, as the Vitest seam's does.
35
- *
36
- * A story is the case this costs nothing to name: the driver shows one at a
37
- * time, so the per-case grain a unit runner needs a custom runner for is
38
- * already here.
39
- */
40
- readonly executionFile?: string;
41
30
  /**
42
31
  * The probe recipe the preview was built with, matching
43
32
  * `testSelectionProbes()`'s `mode`. `presence` when absent, as it is there.
@@ -55,14 +44,6 @@ export interface StoryExecutionOptions {
55
44
  * the ones no story declares and nothing enters.
56
45
  */
57
46
  readonly preconditions?: readonly string[];
58
- /**
59
- * Other builds this same run drove, by the label each instrumented under.
60
- *
61
- * A preview and the application behind it are two builds of overlapping
62
- * source, and a story that reaches both is one observation. Their stores
63
- * join this recording rather than getting one of their own.
64
- */
65
- readonly heads?: readonly string[];
66
47
  /**
67
48
  * Where this recording stands. Defaults to the checkout's `HEAD`, which is
68
49
  * the answer in every case except a caller that already knows better.
package/dist/execution.js CHANGED
@@ -94,7 +94,6 @@ export async function createStoryRecorder(index, options) {
94
94
  record = await recordExecution({
95
95
  root,
96
96
  subjects: zip(observed, timedOnce(observed.map((one) => one.owner), costs)),
97
- ...(options.label === undefined ? {} : { label: options.label }),
98
97
  ...(options.cacheRoot === undefined ? {} : { cacheRoot: options.cacheRoot }),
99
98
  // Against `root`, as the runner seams resolve them, never the checkout.
100
99
  ...(options.coverageFile === undefined
@@ -105,16 +104,10 @@ export async function createStoryRecorder(index, options) {
105
104
  ...(options.preconditions === undefined
106
105
  ? {}
107
106
  : { preconditions: options.preconditions }),
108
- ...(options.heads === undefined || options.heads.length === 0
109
- ? {}
110
- : { heads: options.heads }),
111
107
  ...(options.commit === undefined ? {} : { commit: options.commit }),
112
108
  ...(cases.length === 0
113
109
  ? {}
114
110
  : { cases: zip(cases.map((one) => one.observed), timedOnce(cases.map((one) => one.subject), costs)) }),
115
- ...(options.executionFile === undefined
116
- ? {}
117
- : { executionFile: resolve(ran, options.executionFile) }),
118
111
  });
119
112
  }
120
113
  catch (error) {