styleproof 4.3.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +10 -6
- package/bin/styleproof-init.mjs +43 -24
- package/bin/styleproof-map.mjs +3 -1
- package/dist/runner.d.ts +12 -0
- package/dist/runner.js +21 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [4.4.1] - 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`styleproof-init` now scaffolds the least-work v4 map loop.** Pull-request
|
|
15
|
+
CI restores exact base/head bundles from `styleproof-maps`; a compatible base
|
|
16
|
+
hit plus head miss captures only the head, while a base miss rebuilds the
|
|
17
|
+
pair. Every fallback capture is bound to its explicit commit SHA and
|
|
18
|
+
published, so the same cold work is not repeated on later pull requests.
|
|
19
|
+
Temporary CI maps live under `runner.temp`, outside the checkout, and the
|
|
20
|
+
fallback installs Chromium before capture. The generated Action pin is `v4`.
|
|
21
|
+
- **The generated pre-push hook is restore-first too.** Re-pushing an already
|
|
22
|
+
published commit restores its exact bundle and skips the browser; a real miss
|
|
23
|
+
captures once and requires a clean SHA-keyed upload before the push proceeds.
|
|
24
|
+
|
|
25
|
+
## [4.4.0] - 2026-07-13
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Spec-driven captures now run in parallel across Playwright workers.** The
|
|
30
|
+
capture describe declares `parallel` mode itself, so surface×width tests fan
|
|
31
|
+
out over the consumer's `workers` even when the project pins
|
|
32
|
+
`fullyParallel: false` for its behaviour suite. Every generated test was
|
|
33
|
+
already independent (own page, own map/HAR files, own self-check; ledger and
|
|
34
|
+
manifest tests mkdir and tolerate any order), so the maps are byte-identical —
|
|
35
|
+
a real consumer's 150-capture run dropped from 24.5 to 6.0 minutes at 4
|
|
36
|
+
workers on the same box. A spec file whose OWN sibling tests read the captured
|
|
37
|
+
maps in file order can opt out with `parallel: false`. Found while
|
|
38
|
+
dogfooding against a large single-route consumer app.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **An absolute `STYLEMAP_DIR` (or `--dir`) is respected as-is.** It was joined
|
|
43
|
+
under `baseDir`, so `STYLEMAP_DIR=/abs/path styleproof-map` stranded the maps
|
|
44
|
+
at `.styleproof/maps/abs/path` where no consumer looks. Relative dirs nest
|
|
45
|
+
under `baseDir` unchanged. Also found dogfooding against a consumer app.
|
|
46
|
+
|
|
10
47
|
## [4.3.0] - 2026-07-13
|
|
11
48
|
|
|
12
49
|
### Changed
|
package/README.md
CHANGED
|
@@ -104,8 +104,8 @@ Either way the generated spec runs as-is. It also wires everything around it so
|
|
|
104
104
|
- widths you never set — **omit `widths`** and StyleProof sweeps your app's real `@media` breakpoints automatically;
|
|
105
105
|
- determinism you never set up — network settle, frozen clock, animation freeze, and framework-noise filtering are all on by default (see [Deterministic by default](#deterministic-by-default));
|
|
106
106
|
- `.gitignore` entries for `.styleproof/`, `test-results/`, and `playwright-report/`;
|
|
107
|
-
- a **cache-first CI workflow** that restores reusable maps from the `styleproof-maps` branch
|
|
108
|
-
- a **pre-push hook** (`.husky/` if present, else `.githooks/`) that
|
|
107
|
+
- a **cache-first CI workflow** that restores reusable maps from the `styleproof-maps` branch, captures only a missing head when the base is compatible, and publishes every cold fallback so later runs stay browserless;
|
|
108
|
+
- a **pre-push hook** (`.husky/` if present, else `.githooks/`) that restores an already-published commit or captures and publishes it once — CI's hot path stays report-only, repeated pushes do no browser work, and maps never get committed to the PR branch;
|
|
109
109
|
- the **approval workflow** (`styleproof-approve.yml`) that turns the `StyleProof` status green when a reviewer ticks **Approve all changes** — so the review gate is complete, not half-wired (it activates once the init PR merges, since GitHub runs `issue_comment` workflows only from your default branch).
|
|
110
110
|
|
|
111
111
|
### 2. Capture, then diff
|
|
@@ -133,9 +133,12 @@ automatically: in GitHub Actions it uses the PR base/head SHAs; locally it check
|
|
|
133
133
|
pre-push hook `styleproof-init` installs runs `styleproof-map` on every push
|
|
134
134
|
that can affect render (skip one with `STYLEPROOF_SKIP_CAPTURE=1 git push`).
|
|
135
135
|
On the PR, CI first restores the base/head bundles and only generates the
|
|
136
|
-
report — no build, no browser. If
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
report — no build, no browser. If only the head bundle is missing, CI keeps the
|
|
137
|
+
compatible restored base and captures/publishes only the head. If the base
|
|
138
|
+
bundle is missing or incompatible, CI recaptures and publishes both sides in
|
|
139
|
+
the same pinned environment before reporting. Cold fallback work therefore
|
|
140
|
+
becomes reusable instead of recurring on the next PR. Correctness wins over a
|
|
141
|
+
stale cache, but the hot path is report-only.
|
|
139
142
|
|
|
140
143
|
> **Same-environment note.** Computed styles depend on the browser build and installed fonts, so maps are only comparable when captured in the same runtime environment. StyleProof records a compatibility key to select the right cached bundle and refuses to compare maps captured under different browser/platform settings; CI then recaptures both sides instead of producing a bogus report. Each capture also records the **real browser build** (`browser().version()`) in its manifest — the npm `@playwright/test` version is only a proxy, and the actual Chromium binary can change while it holds constant (a `playwright install` re-download, a different `PLAYWRIGHT_BROWSERS_PATH`, a CI image bump). When both sides carry it, a differing build refuses to compare (exit 2, both builds named) instead of walling the PR with false diffs. This guard needs a `styleproof-manifest.json` on **both** sides. Since **v4** a two-directory `styleproof-diff`/`styleproof-report` where a side ships maps but no manifest **refuses to compare**: it exits `2` (usage/capture error) naming the bare side(s), because the environment can't be verified and captures from different browser builds or platforms would diff as false changes. Re-capture with current StyleProof — `styleproof-map`, or `styleproof-capture` for a one-shot design diff (both write a manifest); maps without one are unsupported. (A dir with _no maps at all_ is "no baseline yet", not a bare bundle — that stays the first-adoption review path, exit `3`.) **Installed fonts are your responsibility:** they are noisy across machines (user-installed families, OS updates, and no cheap cross-platform enumeration), so StyleProof does not fingerprint them — capture both sides on the same fonts, which is what CI's pinned image already gives you.
|
|
141
144
|
|
|
@@ -986,6 +989,7 @@ It's **asynchronous by design**: approval is a checkbox tick handled by a separa
|
|
|
986
989
|
| `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
990
|
| `freezeClock` | `true` | Pin `Date.now()`/`new Date()` so time-derived styling can't drift; timers keep running so settling still works. |
|
|
988
991
|
| `clockTime` | `2025-01-01T00:00:00Z` | The frozen instant. |
|
|
992
|
+
| `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
993
|
| `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
994
|
| `screenshots` | `true` | Save full-page screenshots for the report's before/after crops. |
|
|
991
995
|
| `baseDir` | `__stylemaps__` | Output root directory. |
|
|
@@ -1011,7 +1015,7 @@ Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<styl
|
|
|
1011
1015
|
|
|
1012
1016
|
**CLIs** (every flag accepts `--flag value` and `--flag=value`; `--help` lists all):
|
|
1013
1017
|
|
|
1014
|
-
- `styleproof-init` — scaffold the gate: the capture spec (inventory guard on; Next.js repos get route discovery + the coverage guard, others a crawl-by-default spec), a dedicated `playwright.styleproof.config.ts` (production-build `webServer`, parallel capture), `.gitignore` cache entries, the
|
|
1018
|
+
- `styleproof-init` — scaffold the gate: the capture spec (inventory guard on; Next.js repos get route discovery + the coverage guard, others a crawl-by-default spec), a dedicated `playwright.styleproof.config.ts` (production-build `webServer`, parallel capture), `.gitignore` cache entries, the restore-first report workflow, the approval workflow, and the restore-or-publish pre-push hook. One command. The CI hot path restores exact-SHA maps and runs no browser; a compatible base hit plus head miss captures only the head; a base miss captures the pair; every fallback is published for reuse. Generated commands follow the repo's lockfile (`bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, or npm by default), respect pnpm/Corepack version pins, and detect Vite/Next production preview commands instead of assuming every repo has `start`.
|
|
1015
1019
|
- `styleproof-map` — capture the current commit's computed-style map through Playwright. By default it writes `.styleproof/maps/current`, keeps screenshots for reports, writes a manifest, and uploads to `styleproof-maps` outside CI when the working tree was clean and a git remote exists. Pass `--crawl-base-url` plus repeated `--crawl-route` to run `styleproof-variants` before capture, `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, or `--no-screenshots` for custom flows.
|
|
1016
1020
|
- `styleproof-diff` — the certify gate. With no args, it restores cached maps for the current commit and inferred base (`GITHUB_BASE_REF`, `branch.<name>.gh-merge-base`, `gh pr view`, then main/master fallbacks); `styleproof-diff main` / `styleproof-diff master` pins the base; `styleproof-diff <beforeDir> <afterDir>` keeps the manual two-directory form for CI fallback captures. Exits `0` certified (identical); `1` on a reviewable diff — computed-style/DOM/state differences, and equally an unacknowledged inventory removal, an unacknowledged failing data endpoint under an armed `dataResidue: 'gate'`, an incomplete coverage registry, or an unproven-determinism capture; `2` on a usage/capture error (including a **manifest-less side** — since **v4**, a two-directory compare where a side ships maps but no `styleproof-manifest.json` is refused loudly, naming the bare side(s), because the same-environment guard can't be enforced without one; re-capture with current StyleProof; **and** a **missing map** — a bundle that claims to exist yet holds zero captures, i.e. a `styleproof-manifest.json` present with no maps, on either side, or a head capture that produced nothing; refused loudly rather than mislabelled as all-new — **and** the no-args case where the cached base map can't be restored at all: no map-store remote, no cached bundle, nothing to compare. A "nothing was compared" outcome always exits `2`, never a soft `0` that would read as certified; the error names the two ways forward — run in CI where the base is restorable, or use the two-directory form); `3` when only new surfaces are present — surfaces captured only on the **head** side (a surface present only on the **base** side is a **removed** surface, a reviewable change: exit `1`) — (no baseline for _those_ surfaces to diff against — new surfaces against an existing baseline, or a base dir with no maps at all (and hence no manifest), meaning no baseline was ever captured: the first-adoption review path; approval policy decides whether to gate). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`. The human output **groups the same way the report does**: surfaces that changed identically collapse into one finding (with the per-surface count on its header), longhands fold into shorthands, and size/position-derived longhands fold behind a `(+N derived longhands)` count — so one real change reads as one entry, not dozens of raw lines. A change that rode the shared frame every view draws (a persistent nav/header/footer) is promoted to a "🧱 Global chrome change" callout up top. `--json` stays the complete, unchanged machine contract — every surface and every raw longhand — regardless of the human grouping.
|
|
1017
1021
|
- `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports cached maps for the current commit against the inferred base; `styleproof-report main` / `styleproof-report master` pins the base; `styleproof-report <beforeDir> <afterDir> --out <dir>` keeps the manual two-directory form. Add `--include-content` for the opt-in, advisory content section (see above).
|
package/bin/styleproof-init.mjs
CHANGED
|
@@ -366,13 +366,13 @@ export default defineConfig({
|
|
|
366
366
|
const CI_PATH = '.github/workflows/styleproof.yml';
|
|
367
367
|
const CI_WORKFLOW = `name: StyleProof
|
|
368
368
|
|
|
369
|
-
# Cache-first
|
|
369
|
+
# Cache-first v4 flow:
|
|
370
370
|
# - run \`styleproof-map\` locally after committing to build/upload this commit's map
|
|
371
371
|
# outside CI when possible;
|
|
372
372
|
# - CI restores base/head maps from the styleproof-maps branch and generates the
|
|
373
373
|
# report without a browser;
|
|
374
|
-
# - on
|
|
375
|
-
#
|
|
374
|
+
# - on a head-only miss, CI captures/publishes only the head; on a base miss it
|
|
375
|
+
# recaptures/publishes the pair in one pinned environment.
|
|
376
376
|
on:
|
|
377
377
|
pull_request:
|
|
378
378
|
types: [opened, synchronize, reopened, closed]
|
|
@@ -400,13 +400,16 @@ ${PM.setup}
|
|
|
400
400
|
run: |
|
|
401
401
|
BASE_SHA="\${{ github.event.pull_request.base.sha }}"
|
|
402
402
|
HEAD_SHA="\${{ github.event.pull_request.head.sha }}"
|
|
403
|
-
|
|
403
|
+
MAP_ROOT="\${{ runner.temp }}/styleproof-maps"
|
|
404
|
+
rm -rf "$MAP_ROOT"
|
|
404
405
|
set +e
|
|
405
|
-
${PM.exec(`styleproof-map --restore --sha "$BASE_SHA" --dir base --base-dir
|
|
406
|
+
${PM.exec(`styleproof-map --restore --sha "$BASE_SHA" --dir base --base-dir "$MAP_ROOT" --spec ${specPath}`)}
|
|
406
407
|
base_code=$?
|
|
407
|
-
${PM.exec(`styleproof-map --restore --sha "$HEAD_SHA" --dir head --base-dir
|
|
408
|
+
${PM.exec(`styleproof-map --restore --sha "$HEAD_SHA" --dir head --base-dir "$MAP_ROOT" --spec ${specPath}`)}
|
|
408
409
|
head_code=$?
|
|
409
410
|
set -e
|
|
411
|
+
echo "base-hit=$([ "$base_code" -eq 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
412
|
+
echo "head-hit=$([ "$head_code" -eq 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
410
413
|
if [ "$base_code" -eq 0 ] && [ "$head_code" -eq 0 ]; then
|
|
411
414
|
echo "capture-needed=false" >> "$GITHUB_OUTPUT"
|
|
412
415
|
else
|
|
@@ -418,27 +421,40 @@ ${PM.setup}
|
|
|
418
421
|
run: |
|
|
419
422
|
BASE_SHA="\${{ github.event.pull_request.base.sha }}"
|
|
420
423
|
HEAD_SHA="\${{ github.event.pull_request.head.sha }}"
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
424
|
+
MAP_ROOT="\${{ runner.temp }}/styleproof-maps"
|
|
425
|
+
|
|
426
|
+
if [ "\${{ steps.maps.outputs.base-hit }}" != 'true' ]; then
|
|
427
|
+
# Without a compatible base bundle, rebuild and publish the pair in
|
|
428
|
+
# one pinned environment. This is the expensive cold path.
|
|
429
|
+
rm -rf "$MAP_ROOT"
|
|
430
|
+
git checkout "$BASE_SHA"
|
|
431
|
+
${PM.install}
|
|
432
|
+
${PM.exec('playwright install --with-deps chromium')}
|
|
433
|
+
if [ -f "${specPath}" ]; then
|
|
434
|
+
${PM.exec(`styleproof-map --spec ${specPath} --dir base --base-dir "$MAP_ROOT" --keep-har --sha "$BASE_SHA" --upload`)}
|
|
435
|
+
else
|
|
436
|
+
mkdir -p "$MAP_ROOT/base"
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
git checkout "$HEAD_SHA"
|
|
440
|
+
${PM.install}
|
|
441
|
+
${PM.exec('playwright install --with-deps chromium')}
|
|
427
442
|
else
|
|
428
|
-
|
|
443
|
+
# A compatible base hit proves the current head environment. Keep
|
|
444
|
+
# that restored base and capture only the missing head.
|
|
445
|
+
rm -rf "$MAP_ROOT/head"
|
|
446
|
+
${PM.exec('playwright install --with-deps chromium')}
|
|
429
447
|
fi
|
|
430
448
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if find __stylemaps__/base -name '*.har' -print -quit | grep -q .; then
|
|
434
|
-
STYLEPROOF_REPLAY_FROM=__stylemaps__/base ${PM.exec(`styleproof-map --spec ${specPath} --dir head --base-dir __stylemaps__ --no-upload`)}
|
|
449
|
+
if find "$MAP_ROOT/base" -name '*.har' -print -quit | grep -q .; then
|
|
450
|
+
STYLEPROOF_REPLAY_FROM="$MAP_ROOT/base" ${PM.exec(`styleproof-map --spec ${specPath} --dir head --base-dir "$MAP_ROOT" --sha "$HEAD_SHA" --upload`)}
|
|
435
451
|
else
|
|
436
|
-
${PM.exec(`styleproof-map --spec ${specPath} --dir head --base-dir
|
|
452
|
+
${PM.exec(`styleproof-map --spec ${specPath} --dir head --base-dir "$MAP_ROOT" --sha "$HEAD_SHA" --upload`)}
|
|
437
453
|
fi
|
|
438
|
-
- uses: BenSheridanEdwards/StyleProof@
|
|
454
|
+
- uses: BenSheridanEdwards/StyleProof@v4
|
|
439
455
|
with:
|
|
440
|
-
baseline-dir:
|
|
441
|
-
fresh-dir:
|
|
456
|
+
baseline-dir: \${{ runner.temp }}/styleproof-maps/base
|
|
457
|
+
fresh-dir: \${{ runner.temp }}/styleproof-maps/head
|
|
442
458
|
require-approval: true
|
|
443
459
|
|
|
444
460
|
prune:
|
|
@@ -600,7 +616,10 @@ const HOOK = `#!/bin/sh
|
|
|
600
616
|
# Skip (e.g. a docs-only push): STYLEPROOF_SKIP_CAPTURE=1 git push
|
|
601
617
|
set -e
|
|
602
618
|
[ "\${STYLEPROOF_SKIP_CAPTURE:-}" = "1" ] && exit 0
|
|
603
|
-
$
|
|
619
|
+
head_sha="$(git rev-parse HEAD)"
|
|
620
|
+
if ! ${PM.exec(`styleproof-map --restore --sha "$head_sha" --dir current --base-dir .styleproof/maps --spec ${specPath}`)}; then
|
|
621
|
+
${PM.exec(`styleproof-map --spec ${specPath} --sha "$head_sha" --upload`)}
|
|
622
|
+
fi
|
|
604
623
|
${PM.exec('styleproof-diff')} || true # advisory: show drift before CI does
|
|
605
624
|
`;
|
|
606
625
|
const hookDir = fs.existsSync('.husky') ? '.husky' : '.githooks';
|
|
@@ -627,8 +646,8 @@ if (touched.length) {
|
|
|
627
646
|
console.log('\nHow the gate works — it runs on your first PR with no extra steps:');
|
|
628
647
|
console.log(' 1. Commit and open a PR. CI captures the base and head surfaces in one pinned');
|
|
629
648
|
console.log(' environment and posts the StyleProof report — no local step required.');
|
|
630
|
-
console.log(' 2. The pre-push hook
|
|
631
|
-
console.log('
|
|
649
|
+
console.log(' 2. The pre-push hook restores an existing exact-SHA map or captures once and');
|
|
650
|
+
console.log(' publishes it to styleproof-maps; CI restores by SHA and generates');
|
|
632
651
|
console.log(' the report without a browser. Maps never get committed to the PR branch.');
|
|
633
652
|
console.log(' Skip a push that cannot affect render: STYLEPROOF_SKIP_CAPTURE=1 git push');
|
|
634
653
|
console.log(' 3. Merge this PR. The approval workflow only runs from your default branch, so');
|
package/bin/styleproof-map.mjs
CHANGED
|
@@ -253,7 +253,9 @@ function runVariantCrawl(env) {
|
|
|
253
253
|
if (status !== 0) process.exit(status);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
|
|
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 =
|
|
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 =
|
|
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
|
+
"version": "4.4.1",
|
|
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",
|