styleproof 3.1.1 → 3.1.2

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,42 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.1.2] - 2026-06-28
11
+
12
+ ### Added
13
+
14
+ - Documented the sound way to skip StyleProof work: skip the **entire** CI
15
+ workflow for docs-only / non-rendered paths with native path filters, but do
16
+ not skip individual surfaces based on changed-file guesses.
17
+
18
+ ### Fixed
19
+
20
+ - **Live-region detection now respects the surface's `ignore` list.** A region
21
+ the caller ignored was still scanned for `aria-live`/`role` and surfaced (and
22
+ persisted into the committed map) as a live-state candidate, unlike every other
23
+ capture pass which honours the merged `ignore`. Ignored regions are now excluded
24
+ from `liveCandidates` too.
25
+ - **The scaffolded CI workflow no longer blocks PRs that only add new surfaces.**
26
+ `styleproof init` generated a workflow that failed on any non-zero diff exit
27
+ code, so a new-surface-only diff (exit `3`) held the check red and the PR
28
+ receipt claimed "Visual changes detected" — contradicting the documented
29
+ contract that new surfaces never block. The gate now fails only on reviewable
30
+ diffs (exit `1`) and errors (exit `2`), matching `action.yml`, and the receipt
31
+ reports new surfaces explicitly.
32
+ - **A truncated forced-state layer is no longer silently certified as
33
+ identical.** When a surface had more interactive elements than
34
+ `maxInteractive`, capture truncated the `:hover/:focus/:active` layer but left
35
+ `statesSkipped` unset, so the uncaptured states read as "identical" against a
36
+ fully-captured side — the exact false-certification the flag exists to prevent.
37
+ Truncation now sets `statesSkipped`, and the diff reports the layer as "not
38
+ fully captured" on the affected side.
39
+ - **Release workflow no longer passes an empty `body_path` to the GitHub Release
40
+ step.** Previously a release whose CHANGELOG version section was absent (e.g. a
41
+ hotfix tag with no entry) passed `body_path: ''`, which `softprops/action-gh-release`
42
+ treats as a file to read and fails with ENOENT instead of auto-generating notes.
43
+ The step is now split into two mutually-exclusive steps: one with
44
+ `body_path: notes.md`, one with `generate_release_notes: true`.
45
+
10
46
  ## [3.1.1] - 2026-06-27
11
47
 
12
48
  ### Changed
@@ -922,7 +958,8 @@ number)`), so each viewport band can capture at its own height. Default remains
922
958
  - `styleproof-diff` CLI: certifies a refactor (exit 0) or names the exact element,
923
959
  property, and state that drifted (exit 1).
924
960
 
925
- [Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.1...HEAD
961
+ [Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.2...HEAD
962
+ [3.1.2]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.1...v3.1.2
926
963
  [3.1.1]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.0...v3.1.1
927
964
  [3.1.0]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.0.2...v3.1.0
928
965
  [3.0.2]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.0.1...v3.0.2
package/README.md CHANGED
@@ -234,6 +234,24 @@ Then copy [`example/styleproof-approve.yml`](https://github.com/BenSheridanEdwar
234
234
 
235
235
  **Prefer to capture in CI instead of committing maps?** For a repo with many outside contributors on different machines, StyleProof can capture **both** base and head in CI and diff them there — no committed maps. See **[Forks and Dependabot](#forks-and-dependabot)** for that flow (it's also the fork-safe split). The committed-map flow above is the default because it's far faster and simpler for a single team.
236
236
 
237
+ **Want to skip work safely?** Skip the **whole** StyleProof workflow only for
238
+ changes that cannot affect rendered output, such as docs-only edits, using your
239
+ CI provider's native path filters. Do not skip individual surfaces from a
240
+ StyleProof run based on a changed-file guess: shared CSS, tokens, resets,
241
+ themes, layout primitives, and runtime styling can repaint any surface, and a
242
+ missed surface would certify green without being measured. If you want faster
243
+ feedback, order the highest-signal surfaces first in your spec, but still let
244
+ the full sweep finish before treating the gate as passed.
245
+
246
+ ```yaml
247
+ on:
248
+ pull_request:
249
+ paths-ignore:
250
+ - '**/*.md'
251
+ - 'docs/**'
252
+ - '.github/ISSUE_TEMPLATE/**'
253
+ ```
254
+
237
255
  ## Forks and Dependabot
238
256
 
239
257
  If you **capture in CI** rather than committing maps (the alternative noted at the end of the Quickstart — a better fit when many outside contributors push from different machines), the simplest setup runs the whole gate in one `pull_request` job that captures base + head and diffs them. That job needs a **write** token to push the report branch, post the comment, and set the `StyleProof` status. That's fine for same-repo PRs, but **fork and Dependabot PRs run with a read-only `GITHUB_TOKEN`** (GitHub's security default for untrusted PRs). So the job can't post the status — and a required `StyleProof` check then sits `pending` forever, blocking the PR even though a dependency or fork change usually touches no UI at all.
@@ -381,8 +381,10 @@ ${PM.setup}
381
381
  echo "exit-code=\${code}" >> "$GITHUB_OUTPUT"
382
382
  if [ "$code" -eq 0 ]; then
383
383
  echo "result=clean" >> "$GITHUB_OUTPUT"
384
- elif [ "$code" -eq 1 ] || [ "$code" -eq 3 ]; then
384
+ elif [ "$code" -eq 1 ]; then
385
385
  echo "result=changed" >> "$GITHUB_OUTPUT"
386
+ elif [ "$code" -eq 3 ]; then
387
+ echo "result=new" >> "$GITHUB_OUTPUT"
386
388
  else
387
389
  echo "result=error" >> "$GITHUB_OUTPUT"
388
390
  fi
@@ -397,10 +399,13 @@ ${PM.setup}
397
399
  script: |
398
400
  const issue_number = context.payload.pull_request.number;
399
401
  const marker = '<!-- styleproof-report -->';
400
- const clean = '\${{ steps.diff.outputs.result }}' === 'clean';
401
- const body = clean
402
- ? marker + '\\n## 🗺️ StyleProof report\\n\\n✓ No visual changes detected.'
403
- : marker + '\\n## 🗺️ StyleProof report\\n\\nVisual changes detected. See the StyleProof check logs for the computed-style diff.';
402
+ const result = '\${{ steps.diff.outputs.result }}';
403
+ const bodies = {
404
+ clean: '✓ No visual changes detected.',
405
+ new: '🆕 New surface(s) detected no committed baseline to diff against yet, so nothing to approve. They will be certified once their map is committed.',
406
+ changed: 'Visual changes detected. See the StyleProof check logs for the computed-style diff.',
407
+ };
408
+ const body = marker + '\\n## 🗺️ StyleProof report\\n\\n' + (bodies[result] || bodies.changed);
404
409
  const { data: comments } = await github.rest.issues.listComments({
405
410
  ...context.repo,
406
411
  issue_number,
@@ -413,7 +418,9 @@ ${PM.setup}
413
418
  await github.rest.issues.createComment({ ...context.repo, issue_number, body });
414
419
  }
415
420
  - name: Fail on StyleProof diff
416
- if: steps.diff.outputs.exit-code != '0'
421
+ # Exit 1 = reviewable diffs (gate red); 2 = error. Exit 3 (new surface,
422
+ # no baseline) never blocks — it has nothing to approve yet.
423
+ if: steps.diff.outputs.exit-code != '0' && steps.diff.outputs.exit-code != '3'
417
424
  shell: bash
418
425
  run: exit \${{ steps.diff.outputs.exit-code }}
419
426
  `;
package/dist/capture.d.ts CHANGED
@@ -76,10 +76,11 @@ export type StyleMap = {
76
76
  elements: Record<string, ElementEntry>;
77
77
  states: Record<string, Record<string, Record<string, Props>>>;
78
78
  /**
79
- * True when the forced :hover/:focus/:active layer was skipped for this
80
- * capture because of CDP/page interactive-element count skew. Persisted so a
81
- * diff against a fully-captured side flags that the state layer wasn't
82
- * certified, instead of silently reading as "identical".
79
+ * True when the forced :hover/:focus/:active layer was not fully captured for
80
+ * this capture either CDP/page interactive-element count skew, or because the
81
+ * interactive-element count exceeded `maxInteractive` and capture was truncated.
82
+ * Persisted so a diff against a fully-captured side flags that the state layer
83
+ * wasn't certified, instead of silently reading as "identical".
83
84
  */
84
85
  statesSkipped?: boolean;
85
86
  /**
package/dist/capture.js CHANGED
@@ -375,7 +375,8 @@ async function captureForcedStates(page, ignore, maxInteractive, skipPaths = [])
375
375
  const skipSel = ignore.length ? ignore.map((s) => `${s}, ${s} *`).join(', ') : '';
376
376
  const marked = (await page.evaluate(markInteractiveElements, { selector: INTERACTIVE, skipSel, attr: STATE_ID_ATTR })).filter((m) => !(skipPaths.length && isUnder(m.path, skipPaths)));
377
377
  const limit = Math.min(marked.length, maxInteractive);
378
- if (marked.length > maxInteractive) {
378
+ const truncated = marked.length > maxInteractive;
379
+ if (truncated) {
379
380
  // eslint-disable-next-line no-console
380
381
  console.warn(`styleproof: ${marked.length} interactive elements exceeds maxInteractive=${maxInteractive}; ` +
381
382
  `forced-state capture truncated to the first ${maxInteractive}. Raise maxInteractive to cover them all.`);
@@ -405,7 +406,10 @@ async function captureForcedStates(page, ignore, maxInteractive, skipPaths = [])
405
406
  await page.evaluate(clearInteractiveMarks, STATE_ID_ATTR).catch(() => undefined);
406
407
  await client.detach();
407
408
  }
408
- return { states, skipped: false };
409
+ // When truncated, the elements past the limit have no forced-state delta —
410
+ // flag the layer so the diff reports it as uncertified instead of letting the
411
+ // missing states read as "identical" against a fully-captured side.
412
+ return { states, skipped: truncated };
409
413
  }
410
414
  /** Element paths that differ between two captures (added, removed, or restyled). */
411
415
  function changedElementPaths(a, b) {
@@ -630,7 +634,9 @@ export async function captureStyleMap(page, options = {}) {
630
634
  // Detect semantic live-state candidates automatically, but don't exclude them
631
635
  // merely for being live regions. Stable status/alert/log UI is product UI and
632
636
  // should still be captured; this metadata only improves reports and diagnostics.
633
- const liveCandidates = await page.evaluate(detectLiveCandidates, { ignore: FRAMEWORK_IGNORE });
637
+ // Honour the caller's `ignore` too: a region the user excluded shouldn't be
638
+ // surfaced (or persisted) as a live candidate, matching every other capture pass.
639
+ const liveCandidates = await page.evaluate(detectLiveCandidates, { ignore });
634
640
  // Motion longhands (transition/animation) are read separately so declared
635
641
  // motion is verified even though every other value is a frozen end state.
636
642
  // Read them on the SETTLED DOM, not before it: capturing pre-settle missed any
package/dist/diff.js CHANGED
@@ -101,8 +101,9 @@ export function diffStyleMaps(a, b) {
101
101
  }
102
102
  }
103
103
  // If the forced-state layer was skipped on exactly one side (CDP skew during
104
- // that capture), the :hover/:focus/:active layer was not compared — flag it
105
- // loudly rather than letting {} vs {} read as "identical".
104
+ // capture, or truncation past maxInteractive), the :hover/:focus/:active layer
105
+ // was not fully compared — flag it loudly rather than letting {} vs {} read as
106
+ // "identical".
106
107
  if (!!a.statesSkipped !== !!b.statesSkipped) {
107
108
  findings.push({
108
109
  kind: 'state',
@@ -113,8 +114,8 @@ export function diffStyleMaps(a, b) {
113
114
  props: [
114
115
  {
115
116
  prop: 'forced :hover/:focus/:active layer',
116
- before: a.statesSkipped ? 'skipped (CDP skew)' : 'captured',
117
- after: b.statesSkipped ? 'skipped (CDP skew)' : 'captured',
117
+ before: a.statesSkipped ? 'not fully captured' : 'captured',
118
+ after: b.statesSkipped ? 'not fully captured' : 'captured',
118
119
  },
119
120
  ],
120
121
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "styleproof",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
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",