styleproof 3.11.0 → 3.12.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 +23 -0
- package/dist/inventory.d.ts +14 -0
- package/dist/inventory.js +48 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.12.0] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Inventory guard: target-based keying.** Source-of-truth step 4 (keying honesty).
|
|
15
|
+
Tabs / menu items / nav buttons now key by the most stable identity the element
|
|
16
|
+
exposes — a developer-authored `data-testid`, else a non-generated `id` /
|
|
17
|
+
`aria-controls` — falling back to the label slug only when there's none. Previously
|
|
18
|
+
they always keyed by `slug(accessible-name)`, so a wobble in the **label** (a live
|
|
19
|
+
count badge like "COMMAND 5" → "COMMAND 3", a re-word) faked a `removed` + `added`
|
|
20
|
+
pair. Links were already immune (they key by href); this extends the same
|
|
21
|
+
target-based principle to the rest.
|
|
22
|
+
|
|
23
|
+
Framework-generated ids (React `useId` `:r0:`, Headless UI `headlessui-…`, Radix,
|
|
24
|
+
hashes) are rejected so keying on them can't _add_ churn. The design keeps the
|
|
25
|
+
guard's safe failure direction: the label-slug fallback turns a wobble into a
|
|
26
|
+
**surfaced** removed+added (a red you see and acknowledge), never a hidden real
|
|
27
|
+
removal. Give a nav item a `data-testid` to key it immune to its own text.
|
|
28
|
+
|
|
29
|
+
Note: an id-bearing affordance's key changes from `<role>:<slug>` to
|
|
30
|
+
`<role>:#<id>`, so existing `styleproof.inventory.json` acknowledgements for such
|
|
31
|
+
items need re-keying once (the guard names the new key).
|
|
32
|
+
|
|
10
33
|
## [3.11.0] - 2026-07-06
|
|
11
34
|
|
|
12
35
|
### Added
|
package/dist/inventory.d.ts
CHANGED
|
@@ -35,9 +35,23 @@ export type RawAffordance = {
|
|
|
35
35
|
name: string;
|
|
36
36
|
/** pathname+search for a same-origin `<a href>`; null otherwise. Resolved in-page. */
|
|
37
37
|
internalPath: string | null;
|
|
38
|
+
/** `data-testid` — developer-authored, trusted as a stable identity when present. */
|
|
39
|
+
testId: string | null;
|
|
40
|
+
/** `id` — used as a stable identity only when it doesn't look framework-generated. */
|
|
41
|
+
domId: string | null;
|
|
42
|
+
/** `aria-controls` (a tab's panel) — a stable identity when not framework-generated. */
|
|
43
|
+
controls: string | null;
|
|
38
44
|
};
|
|
39
45
|
/** In-page: collect visible navigable affordances. No classification. */
|
|
40
46
|
export declare function collectNavAffordances(): RawAffordance[];
|
|
47
|
+
/**
|
|
48
|
+
* Whether an `id` / `aria-controls` value is a STABLE identity worth keying on, vs a
|
|
49
|
+
* framework-generated one (React useId `:r0:`, Headless UI `headlessui-tabs-tab-3`,
|
|
50
|
+
* Radix `radix-:r1:`, Emotion hashes) that wobbles across renders/builds. Keying by a
|
|
51
|
+
* generated id would ADD churn, so those fall back to the label slug. (`data-testid` is
|
|
52
|
+
* developer-authored by definition, so it's trusted without this check.)
|
|
53
|
+
*/
|
|
54
|
+
export declare function isStableId(id: string | null | undefined): id is string;
|
|
41
55
|
/** Pure: turn raw affordances into keyed, deduped, sorted navigable items. */
|
|
42
56
|
export declare function classifyInventory(raw: RawAffordance[]): NavigableItem[];
|
|
43
57
|
/** Harvest a page's inventory: thin in-page collect + pure classify. */
|
package/dist/inventory.js
CHANGED
|
@@ -75,6 +75,9 @@ export function collectNavAffordances() {
|
|
|
75
75
|
role: (el.getAttribute('role') || '').toLowerCase(),
|
|
76
76
|
name: nameOf(el),
|
|
77
77
|
internalPath: el.tagName === 'A' ? internalPath(el) : null,
|
|
78
|
+
testId: el.getAttribute('data-testid'),
|
|
79
|
+
domId: el.getAttribute('id'),
|
|
80
|
+
controls: el.getAttribute('aria-controls'),
|
|
78
81
|
}));
|
|
79
82
|
}
|
|
80
83
|
const slug = (s) => s
|
|
@@ -82,6 +85,48 @@ const slug = (s) => s
|
|
|
82
85
|
.replace(/[^a-z0-9]+/g, '-')
|
|
83
86
|
.replace(/^-+|-+$/g, '')
|
|
84
87
|
.slice(0, 60);
|
|
88
|
+
/**
|
|
89
|
+
* Whether an `id` / `aria-controls` value is a STABLE identity worth keying on, vs a
|
|
90
|
+
* framework-generated one (React useId `:r0:`, Headless UI `headlessui-tabs-tab-3`,
|
|
91
|
+
* Radix `radix-:r1:`, Emotion hashes) that wobbles across renders/builds. Keying by a
|
|
92
|
+
* generated id would ADD churn, so those fall back to the label slug. (`data-testid` is
|
|
93
|
+
* developer-authored by definition, so it's trusted without this check.)
|
|
94
|
+
*/
|
|
95
|
+
export function isStableId(id) {
|
|
96
|
+
if (!id)
|
|
97
|
+
return false;
|
|
98
|
+
const v = id.trim();
|
|
99
|
+
if (!v || v.length > 80)
|
|
100
|
+
return false;
|
|
101
|
+
if (v.includes(':'))
|
|
102
|
+
return false; // React useId / Radix scoped ids
|
|
103
|
+
if (/^(headlessui|radix|mui|chakra|reach|react-aria|floating-ui|downshift)-/i.test(v))
|
|
104
|
+
return false;
|
|
105
|
+
if (/^[0-9a-f]{8,}$/i.test(v))
|
|
106
|
+
return false; // hash-like
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
// A tab/menuitem/button's stable identity, if it exposes one: a data-testid (trusted),
|
|
110
|
+
// else a non-generated id or aria-controls. Preferred over the label so a count badge
|
|
111
|
+
// or re-label in the text doesn't move the key. (Links already key by href, the most
|
|
112
|
+
// stable target of all.) When absent, we fall back to the label slug — the wobble is a
|
|
113
|
+
// false removed+added, which the guard SURFACES; it never hides a real removal.
|
|
114
|
+
function stableIdOf(c) {
|
|
115
|
+
if (c.testId && c.testId.trim())
|
|
116
|
+
return c.testId.trim();
|
|
117
|
+
if (isStableId(c.domId))
|
|
118
|
+
return c.domId.trim();
|
|
119
|
+
if (isStableId(c.controls))
|
|
120
|
+
return c.controls.trim();
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
// `<role>:#<stable-id>` when the affordance exposes a stable identity, else
|
|
124
|
+
// `<role>:<slug(name)>`. The `#` marker keeps id-keys from colliding with slug-keys
|
|
125
|
+
// (a slug never contains `#`).
|
|
126
|
+
function affordanceKey(role, c) {
|
|
127
|
+
const id = stableIdOf(c);
|
|
128
|
+
return id ? `${role}:#${id}` : `${role}:${slug(c.name)}`;
|
|
129
|
+
}
|
|
85
130
|
/** Pure: turn raw affordances into keyed, deduped, sorted navigable items. */
|
|
86
131
|
export function classifyInventory(raw) {
|
|
87
132
|
const items = new Map();
|
|
@@ -94,13 +139,13 @@ export function classifyInventory(raw) {
|
|
|
94
139
|
add(`route:${c.internalPath}`, 'link', c.name || c.internalPath, c.internalPath);
|
|
95
140
|
}
|
|
96
141
|
else if (c.name && c.role === 'tab') {
|
|
97
|
-
add(
|
|
142
|
+
add(affordanceKey('tab', c), 'tab', c.name);
|
|
98
143
|
}
|
|
99
144
|
else if (c.name && c.role.startsWith('menuitem')) {
|
|
100
|
-
add(
|
|
145
|
+
add(affordanceKey('menuitem', c), 'menuitem', c.name);
|
|
101
146
|
}
|
|
102
147
|
else if (c.name && c.tag === 'button') {
|
|
103
|
-
add(
|
|
148
|
+
add(affordanceKey('nav-button', c), 'nav-button', c.name);
|
|
104
149
|
}
|
|
105
150
|
}
|
|
106
151
|
return Array.from(items.values()).sort((a, b) => a.key.localeCompare(b.key));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "styleproof",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.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",
|