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 +38 -1
- package/README.md +18 -0
- package/bin/styleproof-init.mjs +13 -6
- package/dist/capture.d.ts +5 -4
- package/dist/capture.js +9 -3
- package/dist/diff.js +5 -4
- package/package.json +1 -1
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.
|
|
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.
|
package/bin/styleproof-init.mjs
CHANGED
|
@@ -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 ]
|
|
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
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
:
|
|
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
|
-
|
|
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
|
|
80
|
-
* capture
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
105
|
-
// loudly rather than letting {} vs {} read as
|
|
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 ? '
|
|
117
|
-
after: b.statesSkipped ? '
|
|
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.
|
|
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",
|