@crouton-kit/capture 1.3.8 → 1.3.10
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/bin/capture +29841 -8539
- package/package.json +6 -3
- package/src/capture.ts +86 -173
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -205
- package/src/cdp/bridge/protocol.ts +93 -6
- package/src/cdp/bridge/server.ts +118 -53
- package/src/cdp/bridge/spawn.ts +53 -6
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +20 -14
- package/src/cdp/commands/cdp.ts +259 -90
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +76 -20
- package/src/cdp/commands/measure/check.ts +100 -26
- package/src/cdp/commands/measure/diff.ts +109 -21
- package/src/cdp/commands/measure/explain.ts +121 -19
- package/src/cdp/commands/measure/index.ts +31 -39
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +41 -13
- package/src/cdp/commands/measure/map-layers.ts +49 -15
- package/src/cdp/commands/measure/map-scroll.ts +85 -15
- package/src/cdp/commands/measure/snap.ts +405 -25
- package/src/cdp/commands/measure/sweep.ts +217 -20
- package/src/cdp/commands/motion/index.ts +18 -17
- package/src/cdp/commands/motion/jank.ts +119 -9
- package/src/cdp/commands/motion/mask.ts +81 -12
- package/src/cdp/commands/motion/rec.ts +520 -19
- package/src/cdp/commands/motion/response.ts +65 -15
- package/src/cdp/commands/motion/timeline.ts +139 -14
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +74 -28
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1478 -105
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +15 -10
- package/src/cdp/types.ts +22 -7
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +6 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +396 -2
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +452 -200
- package/src/session/commands.ts +1162 -397
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +279 -51
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -79
- package/src/tab-session.ts +0 -91
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `measure map ax` query — the AX-tree ↔ layout map, a pure read over the
|
|
3
|
+
* snap substrate's `ax.json` + `geometry.json` (D1's measurement half of
|
|
4
|
+
* the old `a11y` split). Reports, as facts only:
|
|
5
|
+
*
|
|
6
|
+
* - every non-ignored AX node: role, name, curated states, `backendNodeId`
|
|
7
|
+
* (the cross-artifact join key, I-3), and its top-viewport rect;
|
|
8
|
+
* - every ignored AX node with its ignored-reasons;
|
|
9
|
+
* - DOM elements with rendered boxes (from `geometry.json`) but no
|
|
10
|
+
* non-ignored AX node — the "unmapped boxes";
|
|
11
|
+
* - AX nodes whose rect is offscreen, clipped by the viewport edge, or
|
|
12
|
+
* zero-size.
|
|
13
|
+
*
|
|
14
|
+
* A missing, failed, or malformed consumed artifact (or an explicit
|
|
15
|
+
* `available:false` collector report) renders the I-5 unavailability fact —
|
|
16
|
+
* never an empty tree. No score, coverage grade, or gate (I-8). Captured AX
|
|
17
|
+
* names and other browser evidence are preserved; page-derived strings cross
|
|
18
|
+
* the renderer's structural escaping/control-neutralization boundary (I-9).
|
|
19
|
+
*/
|
|
20
|
+
import type { AxNodeRecord, AxRectUnavailableReason } from './collectors/ax.js';
|
|
21
|
+
import {
|
|
22
|
+
annotateUnstableFacts,
|
|
23
|
+
ArtifactResolutionError,
|
|
24
|
+
readAx,
|
|
25
|
+
readGeometry,
|
|
26
|
+
readMeta,
|
|
27
|
+
type Rect,
|
|
28
|
+
type SnapRef,
|
|
29
|
+
type UnstableCaveat,
|
|
30
|
+
type UnstableRegion,
|
|
31
|
+
} from '../../output/artifact.js';
|
|
32
|
+
import {
|
|
33
|
+
data,
|
|
34
|
+
fact,
|
|
35
|
+
formatCoordinate,
|
|
36
|
+
line,
|
|
37
|
+
lineList,
|
|
38
|
+
text,
|
|
39
|
+
type FactLine,
|
|
40
|
+
type RenderableResult,
|
|
41
|
+
} from '../../output/render.js';
|
|
42
|
+
|
|
43
|
+
/** Collector rect shape (`src/cdp/coordinates.ts`) as written to `ax.json`/`geometry.json`. */
|
|
44
|
+
interface CollectorRect {
|
|
45
|
+
readonly x: number;
|
|
46
|
+
readonly y: number;
|
|
47
|
+
readonly width: number;
|
|
48
|
+
readonly height: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ValidatedAxNode = Omit<AxNodeRecord, 'rectUnavailable' | 'rectUnavailableReason'> & (
|
|
52
|
+
| { readonly rectUnavailable?: never; readonly rectUnavailableReason?: never }
|
|
53
|
+
| { readonly rectUnavailable: true; readonly rectUnavailableReason: AxRectUnavailableReason }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
interface ValidatedMeta {
|
|
57
|
+
readonly settled: boolean;
|
|
58
|
+
readonly viewport: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface GeometryElement {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly selector?: string;
|
|
64
|
+
readonly backendNodeId: number | null;
|
|
65
|
+
readonly identityUnresolved?: true;
|
|
66
|
+
readonly rect: CollectorRect;
|
|
67
|
+
readonly visibility: { readonly visible: boolean };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ValidatedAxReportBase {
|
|
71
|
+
readonly nodes: readonly ValidatedAxNode[];
|
|
72
|
+
readonly truncated?: number;
|
|
73
|
+
readonly rectLookupsTruncated?: number;
|
|
74
|
+
readonly coverage: { readonly scope: 'top-document' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type ValidatedAxReport = ValidatedAxReportBase & (
|
|
78
|
+
| { readonly available: true; readonly unavailableReason?: never }
|
|
79
|
+
| { readonly available: false; readonly unavailableReason: string }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
interface ValidatedGeometryBase {
|
|
83
|
+
readonly elements: readonly GeometryElement[];
|
|
84
|
+
readonly elementsTruncated: number;
|
|
85
|
+
readonly elementsTruncatedUnknown?: true;
|
|
86
|
+
readonly unstableRegions?: readonly UnstableRegion[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type ValidatedGeometry = ValidatedGeometryBase & (
|
|
90
|
+
| { readonly available: true; readonly unavailableReason?: never }
|
|
91
|
+
| { readonly available: false; readonly unavailableReason: string }
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
type RectPlacement = 'offscreen' | 'clipped' | 'zero-size';
|
|
95
|
+
|
|
96
|
+
class ArtifactShapeError extends Error {
|
|
97
|
+
constructor(readonly location: string, expected: string) {
|
|
98
|
+
super(`${location} ${expected}`);
|
|
99
|
+
this.name = 'ArtifactShapeError';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fieldPath(parent: string, field: string): string {
|
|
104
|
+
return parent.length === 0 ? field : `${parent}.${field}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function failShape(filename: string, field: string, expected: string): never {
|
|
108
|
+
throw new ArtifactShapeError(`${filename}.${field}`, expected);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function expectRecord(value: unknown, filename: string, field: string): Record<string, unknown> {
|
|
112
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
113
|
+
failShape(filename, field, 'must be an object');
|
|
114
|
+
}
|
|
115
|
+
return value as Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function required(record: Record<string, unknown>, key: string, filename: string, parent = ''): unknown {
|
|
119
|
+
const field = fieldPath(parent, key);
|
|
120
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) failShape(filename, field, 'is required');
|
|
121
|
+
return record[key];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function expectBoolean(value: unknown, filename: string, field: string): boolean {
|
|
125
|
+
if (typeof value !== 'boolean') failShape(filename, field, 'must be a boolean');
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function expectString(value: unknown, filename: string, field: string): string {
|
|
130
|
+
if (typeof value !== 'string') failShape(filename, field, 'must be a string');
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function expectArray(value: unknown, filename: string, field: string): unknown[] {
|
|
135
|
+
if (!Array.isArray(value)) failShape(filename, field, 'must be an array');
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function expectFiniteNumber(value: unknown, filename: string, field: string): number {
|
|
140
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) failShape(filename, field, 'must be a finite number');
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function expectNonNegativeInteger(value: unknown, filename: string, field: string): number {
|
|
145
|
+
const number = expectFiniteNumber(value, filename, field);
|
|
146
|
+
if (!Number.isSafeInteger(number) || number < 0) failShape(filename, field, 'must be a nonnegative safe integer');
|
|
147
|
+
return number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function expectPositiveInteger(value: unknown, filename: string, field: string): number {
|
|
151
|
+
const number = expectFiniteNumber(value, filename, field);
|
|
152
|
+
if (!Number.isSafeInteger(number) || number <= 0) failShape(filename, field, 'must be a positive safe integer');
|
|
153
|
+
return number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function expectTrueIfPresent(record: Record<string, unknown>, key: string, filename: string, parent: string): true | undefined {
|
|
157
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) return undefined;
|
|
158
|
+
if (record[key] !== true) failShape(filename, fieldPath(parent, key), 'must be true when present');
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function expectOptionalString(record: Record<string, unknown>, key: string, filename: string, parent: string): string | undefined {
|
|
163
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) return undefined;
|
|
164
|
+
return expectString(record[key], filename, fieldPath(parent, key));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateRect(value: unknown, filename: string, field: string, dimensions: 'width-height' | 'w-h'): void {
|
|
168
|
+
const rect = expectRecord(value, filename, field);
|
|
169
|
+
expectFiniteNumber(required(rect, 'x', filename, field), filename, fieldPath(field, 'x'));
|
|
170
|
+
expectFiniteNumber(required(rect, 'y', filename, field), filename, fieldPath(field, 'y'));
|
|
171
|
+
const widthKey = dimensions === 'width-height' ? 'width' : 'w';
|
|
172
|
+
const heightKey = dimensions === 'width-height' ? 'height' : 'h';
|
|
173
|
+
const width = expectFiniteNumber(required(rect, widthKey, filename, field), filename, fieldPath(field, widthKey));
|
|
174
|
+
const height = expectFiniteNumber(required(rect, heightKey, filename, field), filename, fieldPath(field, heightKey));
|
|
175
|
+
if (width < 0) failShape(filename, fieldPath(field, widthKey), 'must be nonnegative');
|
|
176
|
+
if (height < 0) failShape(filename, fieldPath(field, heightKey), 'must be nonnegative');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function validateMeta(value: unknown): ValidatedMeta {
|
|
180
|
+
const filename = 'meta.json';
|
|
181
|
+
const meta = expectRecord(value, filename, '$');
|
|
182
|
+
const settled = expectBoolean(required(meta, 'settled', filename), filename, 'settled');
|
|
183
|
+
const viewportValue = required(meta, 'viewport', filename);
|
|
184
|
+
if (viewportValue !== null) {
|
|
185
|
+
if (typeof viewportValue !== 'string') failShape(filename, 'viewport', 'must be a string or null');
|
|
186
|
+
const match = /^([1-9]\d*)x([1-9]\d*)$/.exec(viewportValue);
|
|
187
|
+
if (
|
|
188
|
+
!match
|
|
189
|
+
|| !Number.isSafeInteger(Number(match[1]))
|
|
190
|
+
|| !Number.isSafeInteger(Number(match[2]))
|
|
191
|
+
) {
|
|
192
|
+
failShape(filename, 'viewport', 'must be null or an exact positive-safe <width>x<height> value');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { settled, viewport: viewportValue as string | null };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateAxNode(value: unknown, index: number): ValidatedAxNode {
|
|
199
|
+
const filename = 'ax.json';
|
|
200
|
+
const parent = `nodes[${index}]`;
|
|
201
|
+
const node = expectRecord(value, filename, parent);
|
|
202
|
+
expectString(required(node, 'id', filename, parent), filename, fieldPath(parent, 'id'));
|
|
203
|
+
|
|
204
|
+
const role = required(node, 'role', filename, parent);
|
|
205
|
+
if (
|
|
206
|
+
role !== null
|
|
207
|
+
&& typeof role !== 'string'
|
|
208
|
+
&& typeof role !== 'boolean'
|
|
209
|
+
&& (typeof role !== 'number' || !Number.isFinite(role))
|
|
210
|
+
) {
|
|
211
|
+
failShape(filename, fieldPath(parent, 'role'), 'must be a string, finite number, boolean, or null');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
expectOptionalString(node, 'axName', filename, parent);
|
|
215
|
+
expectBoolean(required(node, 'ignored', filename, parent), filename, fieldPath(parent, 'ignored'));
|
|
216
|
+
|
|
217
|
+
const ignoredReasonsField = fieldPath(parent, 'ignoredReasons');
|
|
218
|
+
const ignoredReasons = expectArray(required(node, 'ignoredReasons', filename, parent), filename, ignoredReasonsField);
|
|
219
|
+
ignoredReasons.forEach((reason, reasonIndex) => expectString(reason, filename, `${ignoredReasonsField}[${reasonIndex}]`));
|
|
220
|
+
|
|
221
|
+
if (Object.prototype.hasOwnProperty.call(node, 'backendNodeId')) {
|
|
222
|
+
expectPositiveInteger(node.backendNodeId, filename, fieldPath(parent, 'backendNodeId'));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const statesField = fieldPath(parent, 'states');
|
|
226
|
+
const states = expectRecord(required(node, 'states', filename, parent), filename, statesField);
|
|
227
|
+
for (const [key, state] of Object.entries(states)) {
|
|
228
|
+
if (
|
|
229
|
+
state !== null
|
|
230
|
+
&& typeof state !== 'string'
|
|
231
|
+
&& typeof state !== 'boolean'
|
|
232
|
+
&& (typeof state !== 'number' || !Number.isFinite(state))
|
|
233
|
+
) {
|
|
234
|
+
failShape(filename, `${statesField}[${JSON.stringify(key)}]`, 'must be a string, finite number, boolean, or null');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const hasRect = Object.prototype.hasOwnProperty.call(node, 'rect');
|
|
239
|
+
if (hasRect) validateRect(node.rect, filename, fieldPath(parent, 'rect'), 'width-height');
|
|
240
|
+
|
|
241
|
+
const rectUnavailable = expectTrueIfPresent(node, 'rectUnavailable', filename, parent);
|
|
242
|
+
const hasRectUnavailableReason = Object.prototype.hasOwnProperty.call(node, 'rectUnavailableReason');
|
|
243
|
+
if (rectUnavailable) {
|
|
244
|
+
if (hasRect) failShape(filename, fieldPath(parent, 'rect'), 'must be absent when rectUnavailable is true');
|
|
245
|
+
const reason = expectString(required(node, 'rectUnavailableReason', filename, parent), filename, fieldPath(parent, 'rectUnavailableReason'));
|
|
246
|
+
if (reason !== 'box-model-read-threw' && reason !== 'box-model-no-content') {
|
|
247
|
+
failShape(filename, fieldPath(parent, 'rectUnavailableReason'), 'must name a supported rect-read failure');
|
|
248
|
+
}
|
|
249
|
+
} else if (hasRectUnavailableReason) {
|
|
250
|
+
failShape(filename, fieldPath(parent, 'rectUnavailable'), 'must be true when rectUnavailableReason is present');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return node as unknown as ValidatedAxNode;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function validateAx(value: unknown): ValidatedAxReport {
|
|
257
|
+
const filename = 'ax.json';
|
|
258
|
+
const report = expectRecord(value, filename, '$');
|
|
259
|
+
const nodes = expectArray(required(report, 'nodes', filename), filename, 'nodes').map(validateAxNode);
|
|
260
|
+
const available = expectBoolean(required(report, 'available', filename), filename, 'available');
|
|
261
|
+
|
|
262
|
+
const coverage = expectRecord(required(report, 'coverage', filename), filename, 'coverage');
|
|
263
|
+
const scope = expectString(required(coverage, 'scope', filename, 'coverage'), filename, 'coverage.scope');
|
|
264
|
+
if (scope !== 'top-document') failShape(filename, 'coverage.scope', 'must equal "top-document"');
|
|
265
|
+
|
|
266
|
+
let truncated: number | undefined;
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(report, 'truncated')) {
|
|
268
|
+
truncated = expectPositiveInteger(report.truncated, filename, 'truncated');
|
|
269
|
+
}
|
|
270
|
+
let rectLookupsTruncated: number | undefined;
|
|
271
|
+
if (Object.prototype.hasOwnProperty.call(report, 'rectLookupsTruncated')) {
|
|
272
|
+
rectLookupsTruncated = expectPositiveInteger(report.rectLookupsTruncated, filename, 'rectLookupsTruncated');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const base: ValidatedAxReportBase = {
|
|
276
|
+
nodes,
|
|
277
|
+
coverage: { scope: 'top-document' },
|
|
278
|
+
...(truncated === undefined ? {} : { truncated }),
|
|
279
|
+
...(rectLookupsTruncated === undefined ? {} : { rectLookupsTruncated }),
|
|
280
|
+
};
|
|
281
|
+
if (!available) {
|
|
282
|
+
if (nodes.length !== 0) failShape(filename, 'nodes', 'must be empty when available is false');
|
|
283
|
+
if (truncated !== undefined) failShape(filename, 'truncated', 'must be absent when available is false');
|
|
284
|
+
if (rectLookupsTruncated !== undefined) failShape(filename, 'rectLookupsTruncated', 'must be absent when available is false');
|
|
285
|
+
const reason = expectString(required(report, 'unavailableReason', filename), filename, 'unavailableReason');
|
|
286
|
+
if (reason !== 'axtree-unavailable' && reason !== 'axtree-returned-no-nodes') {
|
|
287
|
+
failShape(filename, 'unavailableReason', 'must name a supported AX collection failure');
|
|
288
|
+
}
|
|
289
|
+
return { ...base, available: false, unavailableReason: reason };
|
|
290
|
+
}
|
|
291
|
+
if (Object.prototype.hasOwnProperty.call(report, 'unavailableReason')) {
|
|
292
|
+
failShape(filename, 'unavailableReason', 'must be absent when available is true');
|
|
293
|
+
}
|
|
294
|
+
return { ...base, available: true };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function validateGeometryElement(value: unknown, index: number): GeometryElement {
|
|
298
|
+
const filename = 'geometry.json';
|
|
299
|
+
const parent = `elements[${index}]`;
|
|
300
|
+
const element = expectRecord(value, filename, parent);
|
|
301
|
+
expectString(required(element, 'id', filename, parent), filename, fieldPath(parent, 'id'));
|
|
302
|
+
expectOptionalString(element, 'selector', filename, parent);
|
|
303
|
+
|
|
304
|
+
const backendField = fieldPath(parent, 'backendNodeId');
|
|
305
|
+
const backendNodeId = required(element, 'backendNodeId', filename, parent);
|
|
306
|
+
const identityUnresolved = expectTrueIfPresent(element, 'identityUnresolved', filename, parent);
|
|
307
|
+
if (backendNodeId === null) {
|
|
308
|
+
if (identityUnresolved !== true) failShape(filename, fieldPath(parent, 'identityUnresolved'), 'must be true when backendNodeId is null');
|
|
309
|
+
} else {
|
|
310
|
+
expectPositiveInteger(backendNodeId, filename, backendField);
|
|
311
|
+
if (identityUnresolved === true) failShape(filename, fieldPath(parent, 'identityUnresolved'), 'must be absent when backendNodeId is resolved');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
validateRect(required(element, 'rect', filename, parent), filename, fieldPath(parent, 'rect'), 'width-height');
|
|
315
|
+
const visibilityField = fieldPath(parent, 'visibility');
|
|
316
|
+
const visibility = expectRecord(required(element, 'visibility', filename, parent), filename, visibilityField);
|
|
317
|
+
expectBoolean(required(visibility, 'visible', filename, visibilityField), filename, fieldPath(visibilityField, 'visible'));
|
|
318
|
+
|
|
319
|
+
return element as unknown as GeometryElement;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function validateUnstableRegion(value: unknown, index: number): UnstableRegion {
|
|
323
|
+
const filename = 'geometry.json';
|
|
324
|
+
const parent = `unstableRegions[${index}]`;
|
|
325
|
+
const region = expectRecord(value, filename, parent);
|
|
326
|
+
expectString(required(region, 'id', filename, parent), filename, fieldPath(parent, 'id'));
|
|
327
|
+
expectOptionalString(region, 'selector', filename, parent);
|
|
328
|
+
expectOptionalString(region, 'reason', filename, parent);
|
|
329
|
+
if (Object.prototype.hasOwnProperty.call(region, 'rect')) {
|
|
330
|
+
validateRect(region.rect, filename, fieldPath(parent, 'rect'), 'w-h');
|
|
331
|
+
}
|
|
332
|
+
if (Object.prototype.hasOwnProperty.call(region, 'elementIds')) {
|
|
333
|
+
const elementIdsField = fieldPath(parent, 'elementIds');
|
|
334
|
+
const elementIds = expectArray(region.elementIds, filename, elementIdsField);
|
|
335
|
+
elementIds.forEach((elementId, elementIndex) => expectString(elementId, filename, `${elementIdsField}[${elementIndex}]`));
|
|
336
|
+
}
|
|
337
|
+
return region as unknown as UnstableRegion;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function validateGeometry(value: unknown): ValidatedGeometry {
|
|
341
|
+
const filename = 'geometry.json';
|
|
342
|
+
const geometry = expectRecord(value, filename, '$');
|
|
343
|
+
const elements = expectArray(required(geometry, 'elements', filename), filename, 'elements').map(validateGeometryElement);
|
|
344
|
+
const elementsTruncated = expectNonNegativeInteger(
|
|
345
|
+
required(geometry, 'elementsTruncated', filename),
|
|
346
|
+
filename,
|
|
347
|
+
'elementsTruncated',
|
|
348
|
+
);
|
|
349
|
+
const elementsTruncatedUnknown = expectTrueIfPresent(geometry, 'elementsTruncatedUnknown', filename, '');
|
|
350
|
+
const available = expectBoolean(required(geometry, 'available', filename), filename, 'available');
|
|
351
|
+
|
|
352
|
+
let unstableRegions: readonly UnstableRegion[] | undefined;
|
|
353
|
+
if (Object.prototype.hasOwnProperty.call(geometry, 'unstableRegions')) {
|
|
354
|
+
unstableRegions = expectArray(geometry.unstableRegions, filename, 'unstableRegions').map(validateUnstableRegion);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (elementsTruncatedUnknown && elementsTruncated !== 0) {
|
|
358
|
+
failShape(filename, 'elementsTruncated', 'must be the zero placeholder when elementsTruncatedUnknown is true');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const base: ValidatedGeometryBase = {
|
|
362
|
+
elements,
|
|
363
|
+
elementsTruncated,
|
|
364
|
+
...(elementsTruncatedUnknown === undefined ? {} : { elementsTruncatedUnknown }),
|
|
365
|
+
...(unstableRegions === undefined ? {} : { unstableRegions }),
|
|
366
|
+
};
|
|
367
|
+
if (!available) {
|
|
368
|
+
if (elements.length !== 0) failShape(filename, 'elements', 'must be empty when available is false');
|
|
369
|
+
if (elementsTruncated !== 0) failShape(filename, 'elementsTruncated', 'must be zero when available is false');
|
|
370
|
+
if (elementsTruncatedUnknown) failShape(filename, 'elementsTruncatedUnknown', 'must be absent when available is false');
|
|
371
|
+
const reason = expectString(required(geometry, 'unavailableReason', filename), filename, 'unavailableReason');
|
|
372
|
+
if (
|
|
373
|
+
reason !== 'walk-evaluate-threw'
|
|
374
|
+
&& reason !== 'walk-evaluate-returned-no-object'
|
|
375
|
+
&& reason !== 'walk-facts-unavailable'
|
|
376
|
+
&& reason !== 'walk-meta-unavailable'
|
|
377
|
+
) {
|
|
378
|
+
failShape(filename, 'unavailableReason', 'must name a supported geometry collection failure');
|
|
379
|
+
}
|
|
380
|
+
return { ...base, available: false, unavailableReason: reason };
|
|
381
|
+
}
|
|
382
|
+
if (Object.prototype.hasOwnProperty.call(geometry, 'unavailableReason')) {
|
|
383
|
+
failShape(filename, 'unavailableReason', 'must be absent when available is true');
|
|
384
|
+
}
|
|
385
|
+
return { ...base, available: true };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function artifactFailureDetail(filename: string, error: unknown): string {
|
|
389
|
+
if (error instanceof ArtifactShapeError) return error.message;
|
|
390
|
+
if (error instanceof ArtifactResolutionError) return `${filename}.$ could not be read (${error.message})`;
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function toRect(rect: CollectorRect | undefined): Rect | undefined {
|
|
395
|
+
if (rect === undefined) return undefined;
|
|
396
|
+
return { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function parseViewport(viewport: string | null): { w: number; h: number } | undefined {
|
|
400
|
+
if (viewport === null) return undefined;
|
|
401
|
+
const [width, height] = viewport.split('x');
|
|
402
|
+
return { w: Number(width), h: Number(height) };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Viewport placement of a top-viewport rect. `undefined` = fully in-viewport
|
|
406
|
+
* (no placement fact to report). Offscreen/clipped need a viewport size;
|
|
407
|
+
* zero-size does not. */
|
|
408
|
+
function classifyRect(rect: Rect, viewport: { w: number; h: number } | undefined): RectPlacement | undefined {
|
|
409
|
+
if (rect.w === 0 || rect.h === 0) return 'zero-size';
|
|
410
|
+
if (!viewport) return undefined;
|
|
411
|
+
if (rect.x + rect.w <= 0 || rect.y + rect.h <= 0 || rect.x >= viewport.w || rect.y >= viewport.h) return 'offscreen';
|
|
412
|
+
if (rect.x < 0 || rect.y < 0 || rect.x + rect.w > viewport.w || rect.y + rect.h > viewport.h) return 'clipped';
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function caveatSuffix(caveats: readonly UnstableCaveat[]): FactLine | undefined {
|
|
417
|
+
if (caveats.length === 0) return undefined;
|
|
418
|
+
return line(
|
|
419
|
+
text` · nondeterminism caveat: `,
|
|
420
|
+
data(caveats.map((c) => c.selector ?? c.regionId).join(', ')),
|
|
421
|
+
text` is an unstable captured region`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function roleString(role: unknown): string {
|
|
426
|
+
return role === null ? '(none)' : String(role);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function identityParts(node: ValidatedAxNode): FactLine[] {
|
|
430
|
+
const parts: FactLine[] = [text`role=`, data(roleString(node.role))];
|
|
431
|
+
if (node.axName !== undefined) parts.push(line(text` name=`, data(node.axName)));
|
|
432
|
+
parts.push(line(text` backend-node-id=`, data(node.backendNodeId === undefined ? '(unresolved)' : node.backendNodeId)));
|
|
433
|
+
return parts;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function rectParts(node: ValidatedAxNode): FactLine[] {
|
|
437
|
+
const rect = toRect(node.rect);
|
|
438
|
+
if (rect) return [line(text` rect `, formatCoordinate(rect))];
|
|
439
|
+
if (node.rectUnavailable) return [fact` rect unavailable (${node.rectUnavailableReason})`];
|
|
440
|
+
return [text` rect not captured`];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function statesParts(node: ValidatedAxNode): FactLine[] {
|
|
444
|
+
const entries = Object.entries(node.states);
|
|
445
|
+
if (entries.length === 0) return [];
|
|
446
|
+
return [line(text` states `, data(entries.map(([k, v]) => `${k}=${String(v)}`).join(',')))];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function nodeLine(node: ValidatedAxNode, caveats: readonly UnstableCaveat[]): FactLine {
|
|
450
|
+
const parts: FactLine[] = [fact`ax node ${node.id}: `, ...identityParts(node), ...rectParts(node), ...statesParts(node)];
|
|
451
|
+
const caveat = caveatSuffix(caveats);
|
|
452
|
+
if (caveat) parts.push(caveat);
|
|
453
|
+
return line(...parts);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function ignoredLine(node: ValidatedAxNode, caveats: readonly UnstableCaveat[]): FactLine {
|
|
457
|
+
const reasons = node.ignoredReasons.length ? node.ignoredReasons.join(',') : '(none recorded)';
|
|
458
|
+
const parts: FactLine[] = [
|
|
459
|
+
fact`ignored ax node ${node.id}: `,
|
|
460
|
+
text`role=`, data(roleString(node.role)),
|
|
461
|
+
line(text` ignored-reasons=`, data(reasons)),
|
|
462
|
+
];
|
|
463
|
+
if (node.backendNodeId !== undefined) parts.push(line(text` backend-node-id=`, data(node.backendNodeId)));
|
|
464
|
+
const caveat = caveatSuffix(caveats);
|
|
465
|
+
if (caveat) parts.push(caveat);
|
|
466
|
+
return line(...parts);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function unmappedBoxLine(element: GeometryElement, caveats: readonly UnstableCaveat[]): FactLine {
|
|
470
|
+
const parts: FactLine[] = [
|
|
471
|
+
text`unmapped box: selector=`, data(element.selector ?? element.id),
|
|
472
|
+
line(text` backend-node-id=`, data(element.backendNodeId ?? '(unresolved)')),
|
|
473
|
+
];
|
|
474
|
+
const rect = toRect(element.rect);
|
|
475
|
+
if (rect) parts.push(line(text` rect `, formatCoordinate(rect)));
|
|
476
|
+
const caveat = caveatSuffix(caveats);
|
|
477
|
+
if (caveat) parts.push(caveat);
|
|
478
|
+
return line(...parts);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function placementLine(node: ValidatedAxNode, placement: RectPlacement, caveats: readonly UnstableCaveat[]): FactLine {
|
|
482
|
+
const parts: FactLine[] = [fact`ax node ${node.id} rect ${placement}: `, ...identityParts(node), ...rectParts(node)];
|
|
483
|
+
const caveat = caveatSuffix(caveats);
|
|
484
|
+
if (caveat) parts.push(caveat);
|
|
485
|
+
return line(...parts);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function attestationFor(ref: SnapRef, meta: ValidatedMeta): RenderableResult['attestation'] {
|
|
489
|
+
return {
|
|
490
|
+
kind: 'snapshot',
|
|
491
|
+
id: ref.id,
|
|
492
|
+
path: ref.dir,
|
|
493
|
+
note: meta.settled === false
|
|
494
|
+
? text`Snapshot was captured with unsettled regions; only facts intersecting those regions carry nondeterminism caveats.`
|
|
495
|
+
: text`Snapshot was settled before its AX facts were captured.`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function unavailableResult(ref: SnapRef, meta: ValidatedMeta, detail: string): RenderableResult {
|
|
500
|
+
return {
|
|
501
|
+
tag: 'ax-map',
|
|
502
|
+
attestation: attestationFor(ref, meta),
|
|
503
|
+
attrs: { available: false, settled: meta.settled },
|
|
504
|
+
summary: line(
|
|
505
|
+
text`AX facts are unavailable for this snapshot: `,
|
|
506
|
+
data(detail),
|
|
507
|
+
text`. No AX↔layout map can be computed — this is a collection gap, not an empty accessibility tree.`,
|
|
508
|
+
),
|
|
509
|
+
followUp: fact`Re-capture the substrate with \`capture measure snap <url>\`, then run \`capture measure map ax <snap>\`.`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function artifactBoundaryFailureResult(detail: string): RenderableResult {
|
|
514
|
+
return {
|
|
515
|
+
tag: 'ax-map',
|
|
516
|
+
attrs: { available: false },
|
|
517
|
+
summary: line(
|
|
518
|
+
text`AX facts are unavailable for this snapshot because a consumed artifact is malformed or unreadable: `,
|
|
519
|
+
data(detail),
|
|
520
|
+
text`. This is a collection gap, not an empty accessibility tree; no AX↔layout facts are attested.`,
|
|
521
|
+
),
|
|
522
|
+
followUp: fact`Re-capture the substrate with \`capture measure snap <url>\`, then run \`capture measure map ax <snap>\`.`,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Builds the read-only `measure map ax` report from an existing snapshot. */
|
|
527
|
+
export function buildMeasureMapAxResult(ref: SnapRef): RenderableResult {
|
|
528
|
+
let meta: ValidatedMeta;
|
|
529
|
+
try {
|
|
530
|
+
meta = validateMeta(readMeta<unknown>(ref));
|
|
531
|
+
} catch (error) {
|
|
532
|
+
return artifactBoundaryFailureResult(artifactFailureDetail('meta.json', error));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let report: ValidatedAxReport;
|
|
536
|
+
try {
|
|
537
|
+
report = validateAx(readAx<unknown>(ref));
|
|
538
|
+
} catch (error) {
|
|
539
|
+
return artifactBoundaryFailureResult(artifactFailureDetail('ax.json', error));
|
|
540
|
+
}
|
|
541
|
+
if (!report.available) {
|
|
542
|
+
return unavailableResult(ref, meta, `the ax collector reported available:false (${report.unavailableReason})`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let geometry: ValidatedGeometry;
|
|
546
|
+
try {
|
|
547
|
+
geometry = validateGeometry(readGeometry<unknown>(ref));
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return artifactBoundaryFailureResult(artifactFailureDetail('geometry.json', error));
|
|
550
|
+
}
|
|
551
|
+
if (!geometry.available) {
|
|
552
|
+
return unavailableResult(ref, meta, `the geometry collector reported available:false (${geometry.unavailableReason})`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const regions = geometry.unstableRegions === undefined ? [] : geometry.unstableRegions;
|
|
556
|
+
const geometryByBackendNodeId = new Map<number, GeometryElement>();
|
|
557
|
+
let identityUnresolvedCount = 0;
|
|
558
|
+
for (const element of geometry.elements) {
|
|
559
|
+
if (typeof element.backendNodeId === 'number') geometryByBackendNodeId.set(element.backendNodeId, element);
|
|
560
|
+
else identityUnresolvedCount++;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const nodes = report.nodes;
|
|
564
|
+
const nonIgnored = nodes.filter((n) => !n.ignored);
|
|
565
|
+
const ignored = nodes.filter((n) => n.ignored);
|
|
566
|
+
|
|
567
|
+
const elementIdFor = (node: AxNodeRecord): string | undefined => {
|
|
568
|
+
if (typeof node.backendNodeId !== 'number') return undefined;
|
|
569
|
+
return geometryByBackendNodeId.get(node.backendNodeId)?.id ?? String(node.backendNodeId);
|
|
570
|
+
};
|
|
571
|
+
const annotateNodes = (items: readonly ValidatedAxNode[]) => annotateUnstableFacts(
|
|
572
|
+
items.map((node) => ({ elementId: elementIdFor(node), rect: toRect(node.rect) })),
|
|
573
|
+
regions,
|
|
574
|
+
);
|
|
575
|
+
const annotatedNonIgnored = annotateNodes(nonIgnored);
|
|
576
|
+
const annotatedIgnored = annotateNodes(ignored);
|
|
577
|
+
|
|
578
|
+
const mappedBackendNodeIds = new Set<number>();
|
|
579
|
+
for (const node of nonIgnored) {
|
|
580
|
+
if (typeof node.backendNodeId === 'number') mappedBackendNodeIds.add(node.backendNodeId);
|
|
581
|
+
}
|
|
582
|
+
const unmappedBoxes = geometry.elements.filter((element) => {
|
|
583
|
+
if (typeof element.backendNodeId !== 'number') return false;
|
|
584
|
+
if (mappedBackendNodeIds.has(element.backendNodeId)) return false;
|
|
585
|
+
const rect = element.rect;
|
|
586
|
+
return element.visibility.visible && rect.width > 0 && rect.height > 0;
|
|
587
|
+
});
|
|
588
|
+
const annotatedUnmapped = annotateUnstableFacts(
|
|
589
|
+
unmappedBoxes.map((element) => ({ elementId: element.id, rect: toRect(element.rect) })),
|
|
590
|
+
regions,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const viewport = parseViewport(meta.viewport);
|
|
594
|
+
const placed: Array<{ node: ValidatedAxNode; placement: RectPlacement; caveats: readonly UnstableCaveat[] }> = [];
|
|
595
|
+
annotatedNonIgnored.forEach((entry, index) => {
|
|
596
|
+
const node = nonIgnored[index]!;
|
|
597
|
+
const rect = toRect(node.rect);
|
|
598
|
+
if (!rect) return;
|
|
599
|
+
const placement = classifyRect(rect, viewport);
|
|
600
|
+
if (placement) placed.push({ node, placement, caveats: entry.caveats });
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const sections: FactLine[] = [];
|
|
604
|
+
sections.push(line(
|
|
605
|
+
text`AX coverage scope: `,
|
|
606
|
+
data(report.coverage.scope),
|
|
607
|
+
text` (iframe AX nodes are outside Accessibility.getFullAXTree's scope).`,
|
|
608
|
+
));
|
|
609
|
+
if (report.truncated !== undefined) sections.push(fact`AX tree recording cap reached: ${report.truncated} node(s) beyond the cap are not in ax.json.`);
|
|
610
|
+
if (report.rectLookupsTruncated !== undefined) sections.push(fact`Rect lookup cap reached: ${report.rectLookupsTruncated} eligible node(s) have no rect because the lookup was skipped, not because they are unrendered.`);
|
|
611
|
+
if (geometry.elementsTruncatedUnknown) sections.push(text`Geometry element cap count is unavailable: geometry.json records elementsTruncatedUnknown=true.`);
|
|
612
|
+
else if (geometry.elementsTruncated > 0) sections.push(fact`Geometry element recording cap reached: ${geometry.elementsTruncated} element(s) beyond the cap are not in geometry.json.`);
|
|
613
|
+
if (!viewport) sections.push(text`Offscreen/clipped classification is unavailable: the snapshot records no parseable viewport size. Zero-size rects are still classified.`);
|
|
614
|
+
if (identityUnresolvedCount > 0) sections.push(fact`${identityUnresolvedCount} geometry element(s) carry no resolved backendNodeId and could not be checked for AX coverage.`);
|
|
615
|
+
if (annotatedNonIgnored.length) sections.push(lineList(annotatedNonIgnored.map((entry, index) => nodeLine(nonIgnored[index]!, entry.caveats))));
|
|
616
|
+
if (annotatedIgnored.length) sections.push(lineList(annotatedIgnored.map((entry, index) => ignoredLine(ignored[index]!, entry.caveats))));
|
|
617
|
+
if (annotatedUnmapped.length) sections.push(lineList(annotatedUnmapped.map((entry, index) => unmappedBoxLine(unmappedBoxes[index]!, entry.caveats))));
|
|
618
|
+
if (placed.length) sections.push(lineList(placed.map(({ node, placement, caveats }) => placementLine(node, placement, caveats))));
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
tag: 'ax-map',
|
|
622
|
+
attestation: attestationFor(ref, meta),
|
|
623
|
+
attrs: {
|
|
624
|
+
nodes: nonIgnored.length,
|
|
625
|
+
ignored: ignored.length,
|
|
626
|
+
'unmapped-boxes': unmappedBoxes.length,
|
|
627
|
+
settled: meta.settled,
|
|
628
|
+
},
|
|
629
|
+
summary: fact`AX↔layout facts: ${nonIgnored.length} non-ignored AX node(s), ${ignored.length} ignored AX node(s), ${unmappedBoxes.length} DOM element(s) with rendered boxes but no non-ignored AX node, and ${placed.length} AX node(s) with offscreen, clipped, or zero-size rects.`,
|
|
630
|
+
sections,
|
|
631
|
+
followUp: fact`Read another snapshot map with \`capture measure map focus ${ref.id}\` or \`capture measure map layers ${ref.id}\`.`,
|
|
632
|
+
};
|
|
633
|
+
}
|