styleproof 4.3.0 → 4.4.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
@@ -7,6 +7,28 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.4.0] - 2026-07-13
11
+
12
+ ### Changed
13
+
14
+ - **Spec-driven captures now run in parallel across Playwright workers.** The
15
+ capture describe declares `parallel` mode itself, so surface×width tests fan
16
+ out over the consumer's `workers` even when the project pins
17
+ `fullyParallel: false` for its behaviour suite. Every generated test was
18
+ already independent (own page, own map/HAR files, own self-check; ledger and
19
+ manifest tests mkdir and tolerate any order), so the maps are byte-identical —
20
+ a real consumer's 150-capture run dropped from 24.5 to 6.0 minutes at 4
21
+ workers on the same box. A spec file whose OWN sibling tests read the captured
22
+ maps in file order can opt out with `parallel: false`. Found while
23
+ dogfooding against a large single-route consumer app.
24
+
25
+ ### Fixed
26
+
27
+ - **An absolute `STYLEMAP_DIR` (or `--dir`) is respected as-is.** It was joined
28
+ under `baseDir`, so `STYLEMAP_DIR=/abs/path styleproof-map` stranded the maps
29
+ at `.styleproof/maps/abs/path` where no consumer looks. Relative dirs nest
30
+ under `baseDir` unchanged. Also found dogfooding against a consumer app.
31
+
10
32
  ## [4.3.0] - 2026-07-13
11
33
 
12
34
  ### Changed
package/README.md CHANGED
@@ -986,6 +986,7 @@ It's **asynchronous by design**: approval is a checkbox tick handled by a separa
986
986
  | `dataResidue` | `'gate'` | Name data-boundary (`replayUrl`) requests that **fail** during capture (network error / 4xx/5xx — the fallback branch got captured). Always warned + recorded; `'gate'` (the default) also blocks the diff on an unacknowledged one, `'warn'` is the opt-out that records + warns without gating. See [Data residue](#data-residue-a-failed-data-request-is-named-not-swallowed). |
987
987
  | `freezeClock` | `true` | Pin `Date.now()`/`new Date()` so time-derived styling can't drift; timers keep running so settling still works. |
988
988
  | `clockTime` | `2025-01-01T00:00:00Z` | The frozen instant. |
989
+ | `parallel` | `true` | Run the generated capture tests across Playwright workers, even when the project config pins `fullyParallel: false` — every capture test is independent, so a multi-surface spec speeds up ~workers×. Set `false` only for a spec file whose own sibling tests read the captured maps in file order. |
989
990
  | `selfCheck` | on while recording | Capture each surface twice and fail on any difference — proves the capture is deterministic. Off on the replay run; `STYLEPROOF_SELFCHECK=1` forces both. |
990
991
  | `screenshots` | `true` | Save full-page screenshots for the report's before/after crops. |
991
992
  | `baseDir` | `__stylemaps__` | Output root directory. |
@@ -253,7 +253,9 @@ function runVariantCrawl(env) {
253
253
  if (status !== 0) process.exit(status);
254
254
  }
255
255
 
256
- const targetDir = path.join(baseDir, dir);
256
+ // An ABSOLUTE STYLEMAP_DIR/--dir is respected as-is; a relative one nests under
257
+ // baseDir (.styleproof/maps by default) — mirrors the runner's resolveOutputDir.
258
+ const targetDir = path.isAbsolute(dir) ? dir : path.join(baseDir, dir);
257
259
  // Sample the tree state the capture is ABOUT to render, so the manifest can bind the
258
260
  // map to it. A capture runs for minutes; if the source is edited or HEAD moves in that
259
261
  // window, the map renders one state but would otherwise be stamped clean@post-HEAD and
package/dist/runner.d.ts CHANGED
@@ -150,6 +150,14 @@ export type DefineOptions = {
150
150
  * `selfCheck` explicitly to override.
151
151
  */
152
152
  selfCheck?: boolean;
153
+ /**
154
+ * Run the generated capture tests in PARALLEL across Playwright workers
155
+ * (default true). Every capture test is independent, so parallel is safe and
156
+ * ~workers× faster on a multi-surface spec — even when the project config
157
+ * pins `fullyParallel: false`. Set false ONLY for a spec file whose OTHER
158
+ * tests read the captured maps in file order (an in-file assertion suite).
159
+ */
160
+ parallel?: boolean;
153
161
  /**
154
162
  * Opt-in content layer (default OFF). Record each element's own rendered text
155
163
  * so the report's optional content section can surface copy changes (run
@@ -239,6 +247,10 @@ export declare function passLiveStreams(page: Page, url: string): Promise<void>;
239
247
  * forces it on either way.
240
248
  */
241
249
  export declare function defaultSelfCheck(replayFrom: string | undefined, env?: string | undefined): boolean;
250
+ /** Resolve a capture output dir: an ABSOLUTE `dir` is respected as-is (a user's
251
+ * `STYLEMAP_DIR=/abs/path` must not be buried under `baseDir`); a relative one
252
+ * nests under `baseDir` as before. */
253
+ export declare function resolveOutputDir(baseDir: string, dir: string): string;
242
254
  /**
243
255
  * Output base dir: explicit `baseDir` wins, then `STYLEPROOF_BASEDIR`, then the
244
256
  * default. Lets CLIs and CI redirect capture into cache/fallback dirs without
package/dist/runner.js CHANGED
@@ -304,7 +304,7 @@ async function pinInputs(page, harName, s) {
304
304
  }
305
305
  }
306
306
  else {
307
- await page.routeFromHAR(path.join(s.baseDir, s.dir, harName), {
307
+ await page.routeFromHAR(path.join(resolveOutputDir(s.baseDir, s.dir), harName), {
308
308
  url: s.replayUrl,
309
309
  update: true,
310
310
  updateContent: 'embed', // single portable file, no sidecar resources
@@ -443,7 +443,7 @@ async function capturePopupCandidate(page, surface, width, height, s, options, c
443
443
  return;
444
444
  }
445
445
  }
446
- const stem = path.join(s.baseDir, s.dir, `${surface.key}-${popupId}@${width}`);
446
+ const stem = path.join(resolveOutputDir(s.baseDir, s.dir), `${surface.key}-${popupId}@${width}`);
447
447
  saveStyleMap(`${stem}.json.gz`, map);
448
448
  if (s.screenshots)
449
449
  await page.screenshot({ path: `${stem}.png`, fullPage: true, animations: 'disabled' });
@@ -495,7 +495,7 @@ async function captureSurface(page, surface, width, s) {
495
495
  // Attach data-residue AFTER the self-check re-run so both runs' failures are folded
496
496
  // (deduped in the watcher). Warn always; the recorded residue is what the gate reads.
497
497
  attachDataResidue(map, residue.residue());
498
- const stem = path.join(s.baseDir, s.dir, `${surface.key}@${width}`);
498
+ const stem = path.join(resolveOutputDir(s.baseDir, s.dir), `${surface.key}@${width}`);
499
499
  saveStyleMap(`${stem}.json.gz`, map);
500
500
  if (s.screenshots) {
501
501
  // captureStyleMap froze animations/transitions, so this is the same settled
@@ -533,6 +533,12 @@ function attachDataResidue(map, residue) {
533
533
  export function defaultSelfCheck(replayFrom, env = process.env.STYLEPROOF_SELFCHECK) {
534
534
  return env === '1' || !replayFrom;
535
535
  }
536
+ /** Resolve a capture output dir: an ABSOLUTE `dir` is respected as-is (a user's
537
+ * `STYLEMAP_DIR=/abs/path` must not be buried under `baseDir`); a relative one
538
+ * nests under `baseDir` as before. */
539
+ export function resolveOutputDir(baseDir, dir) {
540
+ return path.isAbsolute(dir) ? dir : path.join(baseDir, dir);
541
+ }
536
542
  /**
537
543
  * Output base dir: explicit `baseDir` wins, then `STYLEPROOF_BASEDIR`, then the
538
544
  * default. Lets CLIs and CI redirect capture into cache/fallback dirs without
@@ -602,7 +608,7 @@ function resolveSettings(c) {
602
608
  */
603
609
  function writeCoverageLedgerTest(settings, dir, expected, exclude, captureSurfaces) {
604
610
  test('styleproof coverage ledger', () => {
605
- const outDir = path.join(settings.baseDir, dir);
611
+ const outDir = resolveOutputDir(settings.baseDir, dir);
606
612
  fs.mkdirSync(outDir, { recursive: true });
607
613
  // Determinism basis: self-check ON proves it (a drift would have failed the capture);
608
614
  // else a replay run is deterministic by construction; else it's unproven.
@@ -648,7 +654,7 @@ function writeCoverageLedgerTest(settings, dir, expected, exclude, captureSurfac
648
654
  function writeBrowserBuildTest(settings, dir) {
649
655
  test('styleproof browser build', ({ page }) => {
650
656
  const version = page.context().browser()?.version();
651
- const outDir = path.join(settings.baseDir, dir);
657
+ const outDir = resolveOutputDir(settings.baseDir, dir);
652
658
  writeBrowserBuildSidecar(outDir, version);
653
659
  writeCaptureManifest({ dir: outDir, screenshots: settings.screenshots });
654
660
  });
@@ -681,6 +687,16 @@ export function defineStyleMapCapture(options) {
681
687
  return;
682
688
  const settings = resolveSettings(options);
683
689
  test.describe('styleproof capture', () => {
690
+ // Every generated test is independent — its own page, its own map/HAR files,
691
+ // its own self-check; the ledger/manifest tests mkdir and tolerate any order.
692
+ // Declare the block PARALLEL so captures fan out across the consumer's
693
+ // Playwright workers even when the project pins `fullyParallel: false` for
694
+ // its behaviour suite (150 serial surface×width captures is a ~25-minute CI
695
+ // step; 4 workers make it ~4x faster with byte-identical maps).
696
+ // `parallel: false` keeps file order for specs whose own sibling tests read
697
+ // the captured maps.
698
+ if (options.parallel !== false)
699
+ test.describe.configure({ mode: 'parallel' });
684
700
  writeCoverageLedgerTest(settings, dir, expected ?? null, exclude, captureSurfaces);
685
701
  writeBrowserBuildTest(settings, dir);
686
702
  for (const surface of captureSurfaces) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "styleproof",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "Catch every CSS change before it ships — review PRs and certify refactors by the browser's computed styles, not pixels. Works with any styling system.",
5
5
  "keywords": [
6
6
  "playwright",