@variance-authority/presentation 0.1.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 +32 -0
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/alignment.d.ts +9 -0
- package/dist/alignment.js +100 -0
- package/dist/alignment.js.map +1 -0
- package/dist/analyze.d.ts +20 -0
- package/dist/analyze.js +132 -0
- package/dist/analyze.js.map +1 -0
- package/dist/browser-agent-entry.d.ts +2 -0
- package/dist/browser-agent-entry.js +8 -0
- package/dist/browser-agent-entry.js.map +1 -0
- package/dist/browser-agent.bundle.js +3587 -0
- package/dist/browser-agent.d.ts +37 -0
- package/dist/browser-agent.js +130 -0
- package/dist/browser-agent.js.map +1 -0
- package/dist/bundle.d.ts +3 -0
- package/dist/bundle.js +20 -0
- package/dist/bundle.js.map +1 -0
- package/dist/compare.d.ts +4 -0
- package/dist/compare.js +49 -0
- package/dist/compare.js.map +1 -0
- package/dist/findings.d.ts +5 -0
- package/dist/findings.js +285 -0
- package/dist/findings.js.map +1 -0
- package/dist/focus.d.ts +17 -0
- package/dist/focus.js +84 -0
- package/dist/focus.js.map +1 -0
- package/dist/graph.d.ts +11 -0
- package/dist/graph.js +145 -0
- package/dist/graph.js.map +1 -0
- package/dist/hierarchy.d.ts +4 -0
- package/dist/hierarchy.js +171 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +24 -0
- package/dist/math.js +124 -0
- package/dist/math.js.map +1 -0
- package/dist/measure.d.ts +15 -0
- package/dist/measure.js +216 -0
- package/dist/measure.js.map +1 -0
- package/dist/model.d.ts +346 -0
- package/dist/model.js +2 -0
- package/dist/model.js.map +1 -0
- package/dist/paint.d.ts +12 -0
- package/dist/paint.js +192 -0
- package/dist/paint.js.map +1 -0
- package/dist/patterns.d.ts +10 -0
- package/dist/patterns.js +148 -0
- package/dist/patterns.js.map +1 -0
- package/dist/playwright.d.ts +27 -0
- package/dist/playwright.js +108 -0
- package/dist/playwright.js.map +1 -0
- package/dist/report.d.ts +17 -0
- package/dist/report.js +126 -0
- package/dist/report.js.map +1 -0
- package/dist/spacing.d.ts +4 -0
- package/dist/spacing.js +112 -0
- package/dist/spacing.js.map +1 -0
- package/dist/telemetry.d.ts +4 -0
- package/dist/telemetry.js +73 -0
- package/dist/telemetry.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +62 -0
- package/skills/variance-presentation/SKILL.md +463 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @variance-authority/presentation
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5b2a28f: Carry the caller-established font identities onto the presentation report.
|
|
8
|
+
|
|
9
|
+
`sensePresentation` accepted a `fonts` option, threaded it into the capture it
|
|
10
|
+
builds, then analyzed that capture and dropped it — so through the live door the
|
|
11
|
+
option had no observable consequence: three different font lists produced a
|
|
12
|
+
byte-identical report. The report now carries them, from the capture, so the
|
|
13
|
+
pure entry point answers the same way.
|
|
14
|
+
|
|
15
|
+
Fonts are a render input of the more consequential kind — a substitution moves
|
|
16
|
+
every metric on the page without changing a byte of code — so the identities
|
|
17
|
+
participate in `digest` and a reading taken under different ones is a different
|
|
18
|
+
reading. `contentDigest` is untouched: content identity is what has to survive
|
|
19
|
+
presentation moving. Absent rather than empty when none were established, and
|
|
20
|
+
sorted, because the order a caller listed them in is not part of their identity.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [1d402d1]
|
|
25
|
+
- Updated dependencies [f800342]
|
|
26
|
+
- Updated dependencies [26ae9ed]
|
|
27
|
+
- Updated dependencies [e8fee66]
|
|
28
|
+
- Updated dependencies [5c34e6d]
|
|
29
|
+
- @variance-authority/report@0.1.0
|
|
30
|
+
- @variance-authority/dom@0.1.0
|
|
31
|
+
- @variance-authority/react@0.1.0
|
|
32
|
+
- @variance-authority/core@0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Machine Garden
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
|
|
2
|
+
|
|
3
|
+
# @variance-authority/presentation
|
|
4
|
+
|
|
5
|
+
> Sense spacing, alignment, prominence and repetition inside one live interface, as evidence an agent can read.
|
|
6
|
+
|
|
7
|
+
Sense one rendered subject and return a machine-readable presentation graph. The
|
|
8
|
+
report keeps semantic anchors attached to concrete element references, measures
|
|
9
|
+
geometry and presentation relationships, and exposes deterministic collapse (the
|
|
10
|
+
gap that should separate repeated instances, or the size difference that should
|
|
11
|
+
separate a heading from body text, disappearing) and drift (one instance's
|
|
12
|
+
position, baseline, or presentation diverging from the dominant pattern) —
|
|
13
|
+
without recommending a layout or assigning a UI quality score.
|
|
14
|
+
|
|
15
|
+
It reads a `RawCapture` — a normalized, serializable snapshot of one rendered DOM
|
|
16
|
+
subject, produced by a collector such as this package's Playwright entry point.
|
|
17
|
+
The layout-derived evidence arrives only when that capture's profile — a
|
|
18
|
+
declaration of what its collector could observe (ARIA, style, layout, pixels) —
|
|
19
|
+
includes computed style and layout.
|
|
20
|
+
|
|
21
|
+
This is a sensing and support surface, not a visual-regression assertion. It does
|
|
22
|
+
not create a baseline, approve a change, or produce a pass/fail verdict; pair it
|
|
23
|
+
with `@variance-authority/report` (or another comparison layer) when a run needs
|
|
24
|
+
one.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install --save-dev @variance-authority/presentation @playwright/test
|
|
28
|
+
npx playwright install chromium
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Sense once, then choose the structural owner
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import type { Page } from '@playwright/test';
|
|
35
|
+
import { focusPresentation } from '@variance-authority/presentation';
|
|
36
|
+
import { sensePresentation } from '@variance-authority/presentation/playwright';
|
|
37
|
+
|
|
38
|
+
declare const page: Page;
|
|
39
|
+
|
|
40
|
+
const report = await sensePresentation(page, page.getByRole('main'));
|
|
41
|
+
const pattern = report.patterns?.find((candidate) => candidate.instances.length >= 3);
|
|
42
|
+
if (pattern === undefined) throw new Error('the subject has no repeated presentation pattern');
|
|
43
|
+
|
|
44
|
+
const ownerReading = focusPresentation(report, pattern.parent, {
|
|
45
|
+
paint: ['repetition', 'findings'],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(ownerReading.owner, ownerReading.findings, ownerReading.nested);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Playwright supplies the live layout and its ARIA snapshot. The browser agent
|
|
52
|
+
can paint from the same report it returns, but acquisition is normally left
|
|
53
|
+
unpainted. Its default settlement — the waits, for fonts, images, and
|
|
54
|
+
animations, that hold a page still before it is measured — covers images inside
|
|
55
|
+
the subject and its React portals, not unrelated images elsewhere in the
|
|
56
|
+
document.
|
|
57
|
+
`focusPresentation` reads one owner from that report without touching
|
|
58
|
+
the page again. Its default `owner` depth includes the owner and its immediate
|
|
59
|
+
children; evidence owned by nested boxes is counted in `nested` rather than
|
|
60
|
+
folded into the current reading. Use `depth: 'subtree'` only when the product
|
|
61
|
+
question deliberately treats the complete composition as one subject.
|
|
62
|
+
|
|
63
|
+
Focus options are `depth`, `paint`, and `findings`. `depth` defaults to `owner`;
|
|
64
|
+
`paint` limits retained layers; `findings` limits the reading to report-local
|
|
65
|
+
finding ids owned at the selected depth.
|
|
66
|
+
|
|
67
|
+
Every finding — a detected rule violation such as `SEPARATION_COLLISION`, tying
|
|
68
|
+
specific graph nodes to a measurement — has a stable report-local `id` and the
|
|
69
|
+
graph-node `owner` whose relationship produced it. Pass `findings: [id]` to
|
|
70
|
+
isolate one question. Paint that focused evidence without another acquisition:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import type { Page } from '@playwright/test';
|
|
74
|
+
import { focusPresentation, type PresentationReport } from '@variance-authority/presentation';
|
|
75
|
+
import { paintPresentationFocus } from '@variance-authority/presentation/playwright';
|
|
76
|
+
|
|
77
|
+
declare const page: Page;
|
|
78
|
+
declare const report: PresentationReport;
|
|
79
|
+
|
|
80
|
+
const finding = report.findings?.[0];
|
|
81
|
+
if (finding !== undefined) {
|
|
82
|
+
const isolated = focusPresentation(report, finding.owner, {
|
|
83
|
+
findings: [finding.id],
|
|
84
|
+
paint: ['findings'],
|
|
85
|
+
});
|
|
86
|
+
await paintPresentationFocus(page, isolated);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Call `clearPresentationPaint(page)` to remove the overlay. No ARIA, or a partial
|
|
91
|
+
ARIA tree with no parent or children, remains an observed value rather than an
|
|
92
|
+
acquisition error.
|
|
93
|
+
|
|
94
|
+
`subjectId` and `title` identify the sensed boundary in the returned report.
|
|
95
|
+
`fonts` records the browser fonts whose identities the caller has established;
|
|
96
|
+
the report carries them back, and a substitution changes its digest while
|
|
97
|
+
content identity — a digest of semantic classes, names, text, state, and the
|
|
98
|
+
browser ARIA reading, independent of layout — holds.
|
|
99
|
+
`suspense` controls settlement of React boundaries before sensing. `paint` is
|
|
100
|
+
either `true` for every diagnostic layer (`semantic`, `spacing`, `axes`,
|
|
101
|
+
`baselines`, `surfaces`, `prominence`, `repetition`, `findings`) or a list of
|
|
102
|
+
named layers; omitting it leaves the page unpainted.
|
|
103
|
+
|
|
104
|
+
`stabilize` replaces the default collection recipe by intervention id. Pass an
|
|
105
|
+
empty list only for a caller-owned static document such as an MHTML archive whose
|
|
106
|
+
page clock cannot advance; a live page normally needs the default recipe.
|
|
107
|
+
|
|
108
|
+
Use the pure entry point below when another collector already supplies a
|
|
109
|
+
`RawCapture`.
|
|
110
|
+
|
|
111
|
+
## Inspect a visual flow across nested boxes
|
|
112
|
+
|
|
113
|
+
Use this when peers — a brand mark and navigation controls, say — live in
|
|
114
|
+
different DOM wrappers but should be read as one visual flow: select their graph
|
|
115
|
+
node ids explicitly. A node id encodes a path from a capture root, so
|
|
116
|
+
`r0:0/1/2` is capture root `0`, its child `1`, and that child's child `2`; get
|
|
117
|
+
real ids from `report.graph.nodes` rather than hardcoding them as this example
|
|
118
|
+
does:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import type { Page } from '@playwright/test';
|
|
122
|
+
import {
|
|
123
|
+
inspectPresentationAlignment,
|
|
124
|
+
type PresentationReport,
|
|
125
|
+
} from '@variance-authority/presentation';
|
|
126
|
+
import { paintPresentationAlignment } from '@variance-authority/presentation/playwright';
|
|
127
|
+
|
|
128
|
+
declare const page: Page;
|
|
129
|
+
declare const report: PresentationReport;
|
|
130
|
+
|
|
131
|
+
const reading = inspectPresentationAlignment(
|
|
132
|
+
report,
|
|
133
|
+
'r0:0',
|
|
134
|
+
['r0:0/0/0', 'r0:0/1/0', 'r0:0/1/1', 'r0:0/1/2'],
|
|
135
|
+
'vertical-center',
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
console.log(reading.coordinatePx, reading.spreadPx, reading.members);
|
|
139
|
+
await paintPresentationAlignment(page, reading);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The owner must contain every selected member, and at least two distinct members
|
|
143
|
+
are required. Members must sit at one structural level — a wrapper and its own
|
|
144
|
+
nested control sharing a semantic role do not count as two peers. The result
|
|
145
|
+
reports coordinates and deviations; it does not add a finding. Its `paint`
|
|
146
|
+
marks the median axis and every selected member with its signed deviation;
|
|
147
|
+
painting reuses the report and does not acquire the page.
|
|
148
|
+
|
|
149
|
+
## Inspect spacing at a composition owner
|
|
150
|
+
|
|
151
|
+
Select consecutive immediate children at the owner that arranges them — they
|
|
152
|
+
don't need matching semantic shape to belong to one composition:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import {
|
|
156
|
+
inspectPresentationSpacing,
|
|
157
|
+
type PresentationReport,
|
|
158
|
+
} from '@variance-authority/presentation';
|
|
159
|
+
import type { Page } from '@playwright/test';
|
|
160
|
+
import { paintPresentationSpacing } from '@variance-authority/presentation/playwright';
|
|
161
|
+
|
|
162
|
+
declare const browserPage: Page;
|
|
163
|
+
declare const report: PresentationReport;
|
|
164
|
+
|
|
165
|
+
const page = report.graph.nodes.find((node) => node.parent === undefined)!;
|
|
166
|
+
const reading = inspectPresentationSpacing(report, page.id, page.children, 'vertical');
|
|
167
|
+
|
|
168
|
+
console.log(reading.distance, reading.boundary, reading.separations);
|
|
169
|
+
await paintPresentationSpacing(browserPage, reading);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The reading preserves every adjacent distance, boundary strength and spacing
|
|
173
|
+
cluster, plus min/median/max summaries. Members must be consecutive immediate
|
|
174
|
+
children in the owner's structural order. The result is evidence — distances and
|
|
175
|
+
clusters — not a preferred gap or an automatic finding.
|
|
176
|
+
|
|
177
|
+
## Declare relationship roles instead of trusting tokens
|
|
178
|
+
|
|
179
|
+
Declare which measured separations in an existing report stand for which
|
|
180
|
+
product-defined role, independent of the design-system token that implemented
|
|
181
|
+
the spacing:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import {
|
|
185
|
+
inspectPresentationHierarchy,
|
|
186
|
+
type PresentationHierarchyContract,
|
|
187
|
+
type PresentationReport,
|
|
188
|
+
} from '@variance-authority/presentation';
|
|
189
|
+
import type { Page } from '@playwright/test';
|
|
190
|
+
import { paintPresentationHierarchy } from '@variance-authority/presentation/playwright';
|
|
191
|
+
|
|
192
|
+
declare const page: Page;
|
|
193
|
+
declare const report: PresentationReport;
|
|
194
|
+
|
|
195
|
+
const contract: PresentationHierarchyContract = {
|
|
196
|
+
id: 'demand-record',
|
|
197
|
+
owner: 'r0:0',
|
|
198
|
+
axis: 'vertical',
|
|
199
|
+
levels: [
|
|
200
|
+
{ role: 'owner-boundary', relations: [{ from: 'r0:0/0', to: 'r0:0/1' }] },
|
|
201
|
+
{ role: 'leading-to-body', relations: [{ from: 'r0:0/1/0', to: 'r0:0/1/1' }] },
|
|
202
|
+
{ role: 'body-peer', relations: [{ from: 'r0:0/1/1', to: 'r0:0/1/2' }] },
|
|
203
|
+
{ role: 'content-internal', relations: [{ from: 'r0:0/1/1/0', to: 'r0:0/1/1/1' }] },
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
const hierarchy = inspectPresentationHierarchy(report, contract);
|
|
207
|
+
|
|
208
|
+
console.log(hierarchy.levels, hierarchy.collisions, hierarchy.findings);
|
|
209
|
+
await paintPresentationHierarchy(page, hierarchy);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Roles are ordered outside-in and may not repeat. Every relationship must be a
|
|
213
|
+
measured separation inside the declared owner, and one pair cannot hold two
|
|
214
|
+
roles. Adjacent roles occupying the same spacing cluster or calibrated
|
|
215
|
+
distribution produce `SPACING_HIERARCHY_COLLISION` — a finding that two declared
|
|
216
|
+
roles are not actually distinguishable in the rendered spacing; the result does
|
|
217
|
+
not choose a replacement value. Paint labels each declared role.
|
|
218
|
+
|
|
219
|
+
## Analyze one capture
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import type { AccessibilitySnapshot, RawCapture } from '@variance-authority/core';
|
|
223
|
+
import { analyzePresentation } from '@variance-authority/presentation';
|
|
224
|
+
|
|
225
|
+
declare const capture: RawCapture;
|
|
226
|
+
declare const browserAccessibility: AccessibilitySnapshot;
|
|
227
|
+
|
|
228
|
+
const report = analyzePresentation(capture, {
|
|
229
|
+
accessibility: browserAccessibility,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log(report.telemetry, report.patterns, report.findings);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`telemetry` records content volume, dimensions, utilization, and density as
|
|
236
|
+
neutral context. It never creates a finding independently. `patterns` describe
|
|
237
|
+
repeated semantic shapes and their dominant presentation signatures. `findings`
|
|
238
|
+
contain measurements before prose: the involved nodes, pattern, coordinates,
|
|
239
|
+
ratios, distances, or cluster identities.
|
|
240
|
+
|
|
241
|
+
The browser accessibility snapshot is retained as an independent semantic
|
|
242
|
+
reading. The `accessibility` option omits that reading only when the browser did
|
|
243
|
+
not observe it. An empty root or a root with no parent or children is an observed
|
|
244
|
+
value and remains present without repair.
|
|
245
|
+
|
|
246
|
+
A capture whose profile cannot observe layout still produces content telemetry
|
|
247
|
+
and semantic anchors. Its layout-derived `patterns`, `findings`, clusters, and
|
|
248
|
+
paint instructions are absent rather than empty. A layout-capable capture that
|
|
249
|
+
omits an element rect is refused.
|
|
250
|
+
|
|
251
|
+
## Re-sense after an edit
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import type { PresentationReport } from '@variance-authority/presentation';
|
|
255
|
+
import { comparePresentation } from '@variance-authority/presentation';
|
|
256
|
+
|
|
257
|
+
declare const before: PresentationReport;
|
|
258
|
+
declare const after: PresentationReport;
|
|
259
|
+
|
|
260
|
+
const change = comparePresentation(before, after);
|
|
261
|
+
console.log(change.findings, change.information);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`change.findings` and `change.information` are reported separately, never
|
|
265
|
+
folded into a global score or verdict. `information` holds the content identity
|
|
266
|
+
plus the element, character, and repeated-object counts. A matching content
|
|
267
|
+
identity proves content held while presentation moved; it is derived before
|
|
268
|
+
layout analysis, so the same check works for a capture whose layout is
|
|
269
|
+
unobserved. Re-sensing can therefore show fewer findings resolved alongside an
|
|
270
|
+
unchanged content identity, and no stored baseline is required.
|
|
271
|
+
|
|
272
|
+
## Carry presentation impact into a run report
|
|
273
|
+
|
|
274
|
+
Use `presentationSignal` when the same consequence must survive the browser
|
|
275
|
+
session and travel with a general regression report:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import type { ObservationRecord } from '@variance-authority/report';
|
|
279
|
+
import {
|
|
280
|
+
presentationSignal,
|
|
281
|
+
type PresentationHierarchyReading,
|
|
282
|
+
type PresentationReport,
|
|
283
|
+
} from '@variance-authority/presentation';
|
|
284
|
+
|
|
285
|
+
declare const before: PresentationReport;
|
|
286
|
+
declare const after: PresentationReport;
|
|
287
|
+
declare const beforeHierarchy: PresentationHierarchyReading;
|
|
288
|
+
declare const afterHierarchy: PresentationHierarchyReading;
|
|
289
|
+
|
|
290
|
+
const presentation = presentationSignal(before, after, {
|
|
291
|
+
beforeHierarchy: [beforeHierarchy],
|
|
292
|
+
afterHierarchy: [afterHierarchy],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const observation: ObservationRecord = {
|
|
296
|
+
subject: 'underwriting:demands',
|
|
297
|
+
verdict: 'changed',
|
|
298
|
+
because: 'the candidate differs from its baseline',
|
|
299
|
+
changedPixels: 320,
|
|
300
|
+
regions: [],
|
|
301
|
+
signals: { presentation },
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
console.log(observation.signals?.presentation);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The function combines automatic findings with the supplied product-owned
|
|
308
|
+
hierarchy readings, matching relationship identities across the two reports and
|
|
309
|
+
recording introduced, resolved, and measurement-changing effects.
|
|
310
|
+
`beforeHierarchy` and `afterHierarchy` are optional; omit both when automatic
|
|
311
|
+
findings are the complete evidence for the subject. Missing reports or layout
|
|
312
|
+
findings produce `incomparable`, never an empty clean list. The content
|
|
313
|
+
identity and information counts travel alongside the effects.
|
|
314
|
+
|
|
315
|
+
The stored signal reports consequence only, not severity: renderer impact —
|
|
316
|
+
whether a changed CSS property can only repaint or must also reflow the page —
|
|
317
|
+
still answers how far a change can reach, and project policy still decides
|
|
318
|
+
whether any finding blocks a run. A collector that participates in the general
|
|
319
|
+
regression pipeline returns this value as its optional `presentation` field.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PresentationAlignmentKind, PresentationAlignmentReading, PresentationReport } from './model.js';
|
|
2
|
+
/**
|
|
3
|
+
* Measure an explicitly chosen visual flow, including peers nested in different boxes.
|
|
4
|
+
*
|
|
5
|
+
* Selection remains with the product-aware caller. The reading supplies coordinates
|
|
6
|
+
* and deviations without converting their spread into a design verdict.
|
|
7
|
+
*/
|
|
8
|
+
export declare function inspectPresentationAlignment(report: PresentationReport, ownerId: string, members: readonly string[], kind: PresentationAlignmentKind): PresentationAlignmentReading;
|
|
9
|
+
//# sourceMappingURL=alignment.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { codeUnitCompare, median, round } from './math.js';
|
|
2
|
+
/**
|
|
3
|
+
* Measure an explicitly chosen visual flow, including peers nested in different boxes.
|
|
4
|
+
*
|
|
5
|
+
* Selection remains with the product-aware caller. The reading supplies coordinates
|
|
6
|
+
* and deviations without converting their spread into a design verdict.
|
|
7
|
+
*/
|
|
8
|
+
export function inspectPresentationAlignment(report, ownerId, members, kind) {
|
|
9
|
+
const byId = new Map(report.graph.nodes.map((node) => [node.id, node]));
|
|
10
|
+
const owner = byId.get(ownerId);
|
|
11
|
+
if (owner === undefined)
|
|
12
|
+
throw new Error(`presentation owner ${ownerId} is not in report ${report.digest}`);
|
|
13
|
+
const uniqueMembers = [...new Set(members)].sort(codeUnitCompare);
|
|
14
|
+
if (uniqueMembers.length < 2)
|
|
15
|
+
throw new Error('presentation alignment needs at least two distinct members');
|
|
16
|
+
const descendants = new Set(descendantIds(owner, byId));
|
|
17
|
+
const selected = uniqueMembers.map((id) => {
|
|
18
|
+
const node = byId.get(id);
|
|
19
|
+
if (node === undefined)
|
|
20
|
+
throw new Error(`presentation alignment member ${id} is not in report ${report.digest}`);
|
|
21
|
+
if (!descendants.has(id))
|
|
22
|
+
throw new Error(`presentation alignment member ${id} is not inside owner ${ownerId}`);
|
|
23
|
+
return node;
|
|
24
|
+
});
|
|
25
|
+
const values = selected.map((node) => coordinate(node, kind));
|
|
26
|
+
const center = median(values);
|
|
27
|
+
const membersWithCoordinates = selected.map((node) => {
|
|
28
|
+
const coordinatePx = coordinate(node, kind);
|
|
29
|
+
return { node, coordinatePx: round(coordinatePx, 2), deviationPx: round(coordinatePx - center, 2) };
|
|
30
|
+
});
|
|
31
|
+
const horizontal = ['top', 'bottom', 'vertical-center'].includes(kind);
|
|
32
|
+
const readingId = `alignment:${kind}:${uniqueMembers.join('+')}`;
|
|
33
|
+
return {
|
|
34
|
+
formatVersion: 1,
|
|
35
|
+
report: report.digest,
|
|
36
|
+
owner,
|
|
37
|
+
kind,
|
|
38
|
+
coordinatePx: round(center, 2),
|
|
39
|
+
spreadPx: round(Math.max(...values) - Math.min(...values), 2),
|
|
40
|
+
members: membersWithCoordinates,
|
|
41
|
+
paint: [
|
|
42
|
+
{
|
|
43
|
+
id: readingId,
|
|
44
|
+
owner: ownerId,
|
|
45
|
+
nodes: uniqueMembers,
|
|
46
|
+
layer: 'axes',
|
|
47
|
+
shape: 'line',
|
|
48
|
+
color: '#70ff70',
|
|
49
|
+
label: `${kind} ${round(center, 2)}px`,
|
|
50
|
+
line: horizontal
|
|
51
|
+
? {
|
|
52
|
+
x1: Math.min(...selected.map((node) => node.rect.x)),
|
|
53
|
+
y1: round(center, 2),
|
|
54
|
+
x2: Math.max(...selected.map((node) => node.rect.x + node.rect.width)),
|
|
55
|
+
y2: round(center, 2),
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
x1: round(center, 2),
|
|
59
|
+
y1: Math.min(...selected.map((node) => node.rect.y)),
|
|
60
|
+
x2: round(center, 2),
|
|
61
|
+
y2: Math.max(...selected.map((node) => node.rect.y + node.rect.height)),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
...membersWithCoordinates.map(({ node, coordinatePx, deviationPx }) => ({
|
|
65
|
+
id: `${readingId}:${node.id}`,
|
|
66
|
+
owner: ownerId,
|
|
67
|
+
nodes: [node.id],
|
|
68
|
+
layer: 'axes',
|
|
69
|
+
shape: 'rect',
|
|
70
|
+
color: '#70ff70',
|
|
71
|
+
label: `${coordinatePx}px (${deviationPx >= 0 ? '+' : ''}${deviationPx}px)`,
|
|
72
|
+
rect: node.rect,
|
|
73
|
+
})),
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function coordinate(node, kind) {
|
|
78
|
+
if (kind === 'left')
|
|
79
|
+
return node.rect.x;
|
|
80
|
+
if (kind === 'right')
|
|
81
|
+
return node.rect.x + node.rect.width;
|
|
82
|
+
if (kind === 'horizontal-center')
|
|
83
|
+
return node.rect.x + node.rect.width / 2;
|
|
84
|
+
if (kind === 'top')
|
|
85
|
+
return node.rect.y;
|
|
86
|
+
if (kind === 'bottom')
|
|
87
|
+
return node.rect.y + node.rect.height;
|
|
88
|
+
return node.rect.y + node.rect.height / 2;
|
|
89
|
+
}
|
|
90
|
+
function descendantIds(owner, byId) {
|
|
91
|
+
const result = [];
|
|
92
|
+
for (const id of owner.children) {
|
|
93
|
+
const child = byId.get(id);
|
|
94
|
+
if (child === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
result.push(id, ...descendantIds(child, byId));
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=alignment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alignment.js","sourceRoot":"","sources":["../src/alignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAQ3D;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAC1C,MAA0B,EAC1B,OAAe,EACf,OAA0B,EAC1B,IAA+B;IAE/B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,OAAO,qBAAqB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5G,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAClE,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAC5G,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1B,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,EAAE,qBAAqB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QACjH,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,EAAE,wBAAwB,OAAO,EAAE,CAAC,CAAC;QAChH,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAE,CAAC;IAC/B,MAAM,sBAAsB,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACnD,MAAM,YAAY,GAAG,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,YAAY,GAAG,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC;IACtG,CAAC,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,aAAa,IAAI,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACjE,OAAO;QACL,aAAa,EAAE,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK;QACL,IAAI;QACJ,YAAY,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9B,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;QAC7D,OAAO,EAAE,sBAAsB;QAC/B,KAAK,EAAE;YACL;gBACE,EAAE,EAAE,SAAS;gBACb,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,aAAa;gBACpB,KAAK,EAAE,MAAM;gBACb,KAAK,EAAE,MAAM;gBACb,KAAK,EAAE,SAAS;gBAChB,KAAK,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI;gBACtC,IAAI,EAAE,UAAU;oBACd,CAAC,CAAC;wBACE,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;wBACpD,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;wBACpB,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;wBACtE,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;qBACrB;oBACH,CAAC,CAAC;wBACE,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;wBACpB,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;wBACpD,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;wBACpB,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;qBACxE;aACN;YACD,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;gBACtE,EAAE,EAAE,GAAG,SAAS,IAAI,IAAI,CAAC,EAAE,EAAE;gBAC7B,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChB,KAAK,EAAE,MAAe;gBACtB,KAAK,EAAE,MAAe;gBACtB,KAAK,EAAE,SAAS;gBAChB,KAAK,EAAE,GAAG,YAAY,OAAO,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,WAAW,KAAK;gBAC3E,IAAI,EAAE,IAAI,CAAC,IAAI;aAChB,CAAC,CAAC;SACJ;KACF,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,IAAsB,EAAE,IAA+B;IACzE,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACxC,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;IAC3D,IAAI,IAAI,KAAK,mBAAmB;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IAC3E,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAC7D,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,aAAa,CAAC,KAAuB,EAAE,IAA2C;IACzF,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { codeUnitCompare, median, round } from './math.js';\nimport type {\n PresentationAlignmentKind,\n PresentationAlignmentReading,\n PresentationNode,\n PresentationReport,\n} from './model.js';\n\n/**\n * Measure an explicitly chosen visual flow, including peers nested in different boxes.\n *\n * Selection remains with the product-aware caller. The reading supplies coordinates\n * and deviations without converting their spread into a design verdict.\n */\nexport function inspectPresentationAlignment(\n report: PresentationReport,\n ownerId: string,\n members: readonly string[],\n kind: PresentationAlignmentKind,\n): PresentationAlignmentReading {\n const byId = new Map(report.graph.nodes.map((node) => [node.id, node]));\n const owner = byId.get(ownerId);\n if (owner === undefined) throw new Error(`presentation owner ${ownerId} is not in report ${report.digest}`);\n const uniqueMembers = [...new Set(members)].sort(codeUnitCompare);\n if (uniqueMembers.length < 2) throw new Error('presentation alignment needs at least two distinct members');\n const descendants = new Set(descendantIds(owner, byId));\n const selected = uniqueMembers.map((id) => {\n const node = byId.get(id);\n if (node === undefined) throw new Error(`presentation alignment member ${id} is not in report ${report.digest}`);\n if (!descendants.has(id)) throw new Error(`presentation alignment member ${id} is not inside owner ${ownerId}`);\n return node;\n });\n const values = selected.map((node) => coordinate(node, kind));\n const center = median(values)!;\n const membersWithCoordinates = selected.map((node) => {\n const coordinatePx = coordinate(node, kind);\n return { node, coordinatePx: round(coordinatePx, 2), deviationPx: round(coordinatePx - center, 2) };\n });\n const horizontal = ['top', 'bottom', 'vertical-center'].includes(kind);\n const readingId = `alignment:${kind}:${uniqueMembers.join('+')}`;\n return {\n formatVersion: 1,\n report: report.digest,\n owner,\n kind,\n coordinatePx: round(center, 2),\n spreadPx: round(Math.max(...values) - Math.min(...values), 2),\n members: membersWithCoordinates,\n paint: [\n {\n id: readingId,\n owner: ownerId,\n nodes: uniqueMembers,\n layer: 'axes',\n shape: 'line',\n color: '#70ff70',\n label: `${kind} ${round(center, 2)}px`,\n line: horizontal\n ? {\n x1: Math.min(...selected.map((node) => node.rect.x)),\n y1: round(center, 2),\n x2: Math.max(...selected.map((node) => node.rect.x + node.rect.width)),\n y2: round(center, 2),\n }\n : {\n x1: round(center, 2),\n y1: Math.min(...selected.map((node) => node.rect.y)),\n x2: round(center, 2),\n y2: Math.max(...selected.map((node) => node.rect.y + node.rect.height)),\n },\n },\n ...membersWithCoordinates.map(({ node, coordinatePx, deviationPx }) => ({\n id: `${readingId}:${node.id}`,\n owner: ownerId,\n nodes: [node.id],\n layer: 'axes' as const,\n shape: 'rect' as const,\n color: '#70ff70',\n label: `${coordinatePx}px (${deviationPx >= 0 ? '+' : ''}${deviationPx}px)`,\n rect: node.rect,\n })),\n ],\n };\n}\n\nfunction coordinate(node: PresentationNode, kind: PresentationAlignmentKind): number {\n if (kind === 'left') return node.rect.x;\n if (kind === 'right') return node.rect.x + node.rect.width;\n if (kind === 'horizontal-center') return node.rect.x + node.rect.width / 2;\n if (kind === 'top') return node.rect.y;\n if (kind === 'bottom') return node.rect.y + node.rect.height;\n return node.rect.y + node.rect.height / 2;\n}\n\nfunction descendantIds(owner: PresentationNode, byId: ReadonlyMap<string, PresentationNode>): string[] {\n const result: string[] = [];\n for (const id of owner.children) {\n const child = byId.get(id);\n if (child === undefined) continue;\n result.push(id, ...descendantIds(child, byId));\n }\n return result;\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AccessibilitySnapshot, type RawCapture } from '@variance-authority/core';
|
|
2
|
+
import type { PresentationReport } from './model.js';
|
|
3
|
+
export interface AnalyzePresentationOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Browser-computed ARIA evidence observed beside this capture.
|
|
6
|
+
*
|
|
7
|
+
* Omit only when it was not observed. `roots: ['']` and partial roots are
|
|
8
|
+
* readings and remain byte-for-byte present in the report.
|
|
9
|
+
*/
|
|
10
|
+
readonly accessibility?: AccessibilitySnapshot;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Derive machine-readable presentation relationships from one browser capture.
|
|
14
|
+
*
|
|
15
|
+
* Telemetry is descriptive and never creates a finding by itself. Findings are
|
|
16
|
+
* limited to measured relationships; this function does not recommend a layout,
|
|
17
|
+
* remove information, or produce an overall quality score.
|
|
18
|
+
*/
|
|
19
|
+
export declare function analyzePresentation(capture: RawCapture, options?: AnalyzePresentationOptions): PresentationReport;
|
|
20
|
+
//# sourceMappingURL=analyze.d.ts.map
|
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { digestValue, } from '@variance-authority/core';
|
|
2
|
+
import { detectFindings } from './findings.js';
|
|
3
|
+
import { buildGraph } from './graph.js';
|
|
4
|
+
import { measureGraph } from './measure.js';
|
|
5
|
+
import { paintInstructions } from './paint.js';
|
|
6
|
+
import { inferPatterns } from './patterns.js';
|
|
7
|
+
import { telemetryOf } from './telemetry.js';
|
|
8
|
+
/**
|
|
9
|
+
* Derive machine-readable presentation relationships from one browser capture.
|
|
10
|
+
*
|
|
11
|
+
* Telemetry is descriptive and never creates a finding by itself. Findings are
|
|
12
|
+
* limited to measured relationships; this function does not recommend a layout,
|
|
13
|
+
* remove information, or produce an overall quality score.
|
|
14
|
+
*/
|
|
15
|
+
export function analyzePresentation(capture, options = {}) {
|
|
16
|
+
const anchors = semanticAnchors(capture);
|
|
17
|
+
const contentDigest = digestValue(contentEvidence(capture));
|
|
18
|
+
if (!capture.profile.layout) {
|
|
19
|
+
const body = {
|
|
20
|
+
formatVersion: 1,
|
|
21
|
+
contentDigest,
|
|
22
|
+
subject: capture.subject,
|
|
23
|
+
...fontsOf(capture),
|
|
24
|
+
semantic: {
|
|
25
|
+
anchors,
|
|
26
|
+
...(options.accessibility === undefined ? {} : { browserAccessibility: options.accessibility }),
|
|
27
|
+
},
|
|
28
|
+
telemetry: telemetryOf(capture, [], undefined),
|
|
29
|
+
graph: { nodes: [], relations: [] },
|
|
30
|
+
};
|
|
31
|
+
return { ...body, digest: digestValue(body) };
|
|
32
|
+
}
|
|
33
|
+
assertLayout(capture);
|
|
34
|
+
const graph = buildGraph(capture);
|
|
35
|
+
const measured = measureGraph(graph);
|
|
36
|
+
const inferred = inferPatterns(graph, measured);
|
|
37
|
+
const findings = detectFindings(graph, measured, inferred.patterns);
|
|
38
|
+
const relations = [...measured.relations, ...inferred.peerRelations];
|
|
39
|
+
const paint = paintInstructions({
|
|
40
|
+
nodes: measured.nodes,
|
|
41
|
+
relations,
|
|
42
|
+
axes: measured.axes,
|
|
43
|
+
baselines: measured.baselines,
|
|
44
|
+
prominence: measured.prominence,
|
|
45
|
+
surfaces: measured.surfaces,
|
|
46
|
+
patterns: inferred.patterns,
|
|
47
|
+
findings,
|
|
48
|
+
});
|
|
49
|
+
const body = {
|
|
50
|
+
formatVersion: 1,
|
|
51
|
+
contentDigest,
|
|
52
|
+
subject: capture.subject,
|
|
53
|
+
...fontsOf(capture),
|
|
54
|
+
semantic: {
|
|
55
|
+
anchors,
|
|
56
|
+
...(options.accessibility === undefined ? {} : { browserAccessibility: options.accessibility }),
|
|
57
|
+
},
|
|
58
|
+
telemetry: telemetryOf(capture, measured.nodes, inferred.patterns),
|
|
59
|
+
graph: { nodes: measured.nodes, relations },
|
|
60
|
+
spacing: measured.spacing,
|
|
61
|
+
axes: measured.axes,
|
|
62
|
+
baselines: measured.baselines,
|
|
63
|
+
prominence: measured.prominence,
|
|
64
|
+
surfaces: measured.surfaces,
|
|
65
|
+
patterns: inferred.patterns,
|
|
66
|
+
findings,
|
|
67
|
+
paint,
|
|
68
|
+
};
|
|
69
|
+
return { ...body, digest: digestValue(body) };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Carry the caller-established font identities onto the report.
|
|
73
|
+
*
|
|
74
|
+
* The live entry accepts a `fonts` option and threads it into the capture, and
|
|
75
|
+
* the capture is then analyzed and dropped — so through that door the option
|
|
76
|
+
* had no observable consequence at all. It has one here. Fonts are a render
|
|
77
|
+
* input of the more consequential kind: a substitution moves every metric on
|
|
78
|
+
* the page without changing a byte of code, so a reading taken under different
|
|
79
|
+
* pinned identities is a different reading and the digest says so.
|
|
80
|
+
*
|
|
81
|
+
* Sorted, because the order a caller happened to list them in is not part of
|
|
82
|
+
* their identity — the same position `normalize` takes on the environment key.
|
|
83
|
+
* Absent rather than empty when none were established, matching the capture,
|
|
84
|
+
* which says the same thing a second way with its `unverified-fonts` caveat.
|
|
85
|
+
* The content digest is deliberately untouched: font identity is presentation,
|
|
86
|
+
* and content identity is what has to survive presentation moving.
|
|
87
|
+
*/
|
|
88
|
+
function fontsOf(capture) {
|
|
89
|
+
const fonts = capture.environment.fonts;
|
|
90
|
+
return fonts.length === 0 ? {} : { fonts: [...fonts].sort() };
|
|
91
|
+
}
|
|
92
|
+
function contentEvidence(capture) {
|
|
93
|
+
const read = (node) => ({
|
|
94
|
+
tag: node.tag,
|
|
95
|
+
...(node.aria === undefined ? {} : { aria: node.aria }),
|
|
96
|
+
...(node.text === undefined ? {} : { text: node.text }),
|
|
97
|
+
children: node.children.map(read),
|
|
98
|
+
...(node.shadowChildren === undefined ? {} : { shadowChildren: node.shadowChildren.map(read) }),
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
root: read(capture.root),
|
|
102
|
+
...(capture.portals === undefined ? {} : { portals: capture.portals.map(read) }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function semanticAnchors(capture) {
|
|
106
|
+
const anchors = [];
|
|
107
|
+
[capture.root, ...(capture.portals ?? [])].forEach((root, rootIndex) => walk(root, rootIndex, '0'));
|
|
108
|
+
return anchors;
|
|
109
|
+
function walk(node, root, path) {
|
|
110
|
+
if (node.tag !== '#text' &&
|
|
111
|
+
node.aria !== undefined &&
|
|
112
|
+
(node.aria.role !== null || node.aria.name !== null)) {
|
|
113
|
+
const role = node.aria?.role ?? node.tag;
|
|
114
|
+
const name = node.aria?.name ?? '';
|
|
115
|
+
anchors.push(`r${root}:${path} ${role}${name.length > 0 ? ` "${name}"` : ''}`);
|
|
116
|
+
}
|
|
117
|
+
node.children.forEach((child, index) => walk(child, root, `${path}/${index}`));
|
|
118
|
+
node.shadowChildren?.forEach((child, index) => walk(child, root, `${path}/shadow:${index}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function assertLayout(capture) {
|
|
122
|
+
[capture.root, ...(capture.portals ?? [])].forEach((root, rootIndex) => walk(root, `r${rootIndex}:0`));
|
|
123
|
+
function walk(node, path) {
|
|
124
|
+
if (node.tag !== '#text' && node.rect === undefined) {
|
|
125
|
+
throw new Error(`the ${capture.profile.id} capture claims layout is observable but ${path} has no rect; ` +
|
|
126
|
+
'an absent measurement cannot be analyzed as an empty box');
|
|
127
|
+
}
|
|
128
|
+
node.children.forEach((child, index) => walk(child, `${path}/${index}`));
|
|
129
|
+
node.shadowChildren?.forEach((child, index) => walk(child, `${path}/shadow:${index}`));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=analyze.js.map
|