@crouton-kit/capture 1.3.7 → 1.3.9
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 +32786 -9989
- package/package.json +9 -3
- package/src/capture.ts +86 -163
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -116
- package/src/cdp/bridge/protocol.ts +187 -1
- package/src/cdp/bridge/server.ts +211 -84
- package/src/cdp/bridge/spawn.ts +110 -1
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +35 -4
- package/src/cdp/commands/cdp.ts +266 -69
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +87 -0
- package/src/cdp/commands/measure/check.ts +109 -0
- package/src/cdp/commands/measure/diff.ts +120 -0
- package/src/cdp/commands/measure/explain.ts +133 -0
- package/src/cdp/commands/measure/index.ts +106 -0
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +52 -0
- package/src/cdp/commands/measure/map-layers.ts +59 -0
- package/src/cdp/commands/measure/map-scroll.ts +94 -0
- package/src/cdp/commands/measure/snap.ts +416 -0
- package/src/cdp/commands/measure/sweep.ts +227 -0
- package/src/cdp/commands/motion/index.ts +61 -0
- package/src/cdp/commands/motion/jank.ts +132 -0
- package/src/cdp/commands/motion/mask.ts +92 -0
- package/src/cdp/commands/motion/rec.ts +534 -0
- package/src/cdp/commands/motion/response.ts +76 -0
- package/src/cdp/commands/motion/timeline.ts +152 -0
- 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/coordinates.ts +158 -0
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +76 -26
- package/src/cdp/domains.ts +75 -0
- 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 +1894 -0
- 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/source-map.ts +309 -0
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +87 -0
- package/src/cdp/types.ts +87 -6
- 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 +526 -0
- package/src/output/diff.ts +111 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +987 -0
- package/src/output/selector.ts +185 -0
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +481 -0
- package/src/session/commands.ts +1237 -368
- 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 +302 -34
- 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 -77
- package/src/tab-session.ts +0 -91
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
annotateUnstableFacts,
|
|
3
|
+
readFocus,
|
|
4
|
+
readGeometry,
|
|
5
|
+
readMeta,
|
|
6
|
+
unstableRegionsFor,
|
|
7
|
+
type Rect,
|
|
8
|
+
type SnapRef,
|
|
9
|
+
} from '../../output/artifact.js';
|
|
10
|
+
import {
|
|
11
|
+
data,
|
|
12
|
+
fact,
|
|
13
|
+
formatCoordinate,
|
|
14
|
+
line,
|
|
15
|
+
lineList,
|
|
16
|
+
text,
|
|
17
|
+
type FactLine,
|
|
18
|
+
type RenderableResult,
|
|
19
|
+
} from '../../output/render.js';
|
|
20
|
+
|
|
21
|
+
interface FocusRect {
|
|
22
|
+
readonly x: number;
|
|
23
|
+
readonly y: number;
|
|
24
|
+
readonly width?: number;
|
|
25
|
+
readonly height?: number;
|
|
26
|
+
readonly w?: number;
|
|
27
|
+
readonly h?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface FocusStop {
|
|
31
|
+
readonly step?: number;
|
|
32
|
+
readonly id?: string | null;
|
|
33
|
+
readonly backendNodeId?: number | null;
|
|
34
|
+
readonly selector?: string | null;
|
|
35
|
+
readonly role?: string | null;
|
|
36
|
+
readonly name?: string | null;
|
|
37
|
+
readonly rect?: FocusRect | null;
|
|
38
|
+
readonly scrollBefore?: { readonly x?: number; readonly y?: number };
|
|
39
|
+
readonly scrollAfter?: { readonly x?: number; readonly y?: number };
|
|
40
|
+
readonly scrollJump?: boolean;
|
|
41
|
+
readonly focusVisibleStyle?: { readonly outline?: string | null; readonly boxShadow?: string | null } | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface UnreachedFocusable {
|
|
45
|
+
readonly id?: string;
|
|
46
|
+
readonly backendNodeId?: number | null;
|
|
47
|
+
readonly selector?: string | null;
|
|
48
|
+
readonly rect?: FocusRect | null;
|
|
49
|
+
readonly visible?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface FocusArtifact {
|
|
53
|
+
readonly available?: boolean;
|
|
54
|
+
readonly unavailableReason?: string;
|
|
55
|
+
readonly forward?: readonly FocusStop[];
|
|
56
|
+
readonly forwardTruncated?: boolean;
|
|
57
|
+
readonly reverse?: readonly FocusStop[];
|
|
58
|
+
readonly reverseTruncated?: boolean;
|
|
59
|
+
readonly unreachedFocusable?: readonly UnreachedFocusable[];
|
|
60
|
+
readonly clickableUnfocusable?: readonly UnreachedFocusable[];
|
|
61
|
+
readonly candidateCount?: number;
|
|
62
|
+
readonly scope?: { readonly root?: string; readonly shadowDom?: string; readonly iframesPresent?: number; readonly shadowHostsPresent?: number };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SnapshotMeta {
|
|
66
|
+
readonly settled?: boolean;
|
|
67
|
+
readonly settleMs?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface FocusFact {
|
|
71
|
+
readonly elementId?: string;
|
|
72
|
+
readonly rect?: Rect;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function rectOf(rect: FocusRect | null | undefined): Rect | undefined {
|
|
76
|
+
if (!rect || !Number.isFinite(rect.x) || !Number.isFinite(rect.y)) return undefined;
|
|
77
|
+
const w = rect.w ?? rect.width;
|
|
78
|
+
const h = rect.h ?? rect.height;
|
|
79
|
+
if (!Number.isFinite(w) || !Number.isFinite(h)) return undefined;
|
|
80
|
+
return { x: rect.x, y: rect.y, w, h };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function identity(stop: FocusStop | UnreachedFocusable, geometryIdsByBackendNode: ReadonlyMap<number, string>): string | undefined {
|
|
84
|
+
if (typeof stop.backendNodeId === 'number') return geometryIdsByBackendNode.get(stop.backendNodeId) ?? String(stop.backendNodeId);
|
|
85
|
+
return typeof stop.id === 'string' ? stop.id : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function caveatSuffix(caveats: ReturnType<typeof annotateUnstableFacts<FocusFact>>[number]['caveats']): FactLine | undefined {
|
|
89
|
+
if (caveats.length === 0) return undefined;
|
|
90
|
+
return line(
|
|
91
|
+
text` · nondeterminism caveat: `,
|
|
92
|
+
data(caveats.map((c) => c.selector ?? c.regionId).join(', ')),
|
|
93
|
+
text` is an unstable captured region`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopLine(direction: 'forward' | 'reverse', stop: FocusStop, caveats: ReturnType<typeof annotateUnstableFacts<FocusFact>>[number]['caveats']): FactLine {
|
|
98
|
+
const parts: FactLine[] = [
|
|
99
|
+
fact`${direction} step ${stop.step ?? 0}: `,
|
|
100
|
+
text`selector=`, data(stop.selector ?? '(unresolved)'),
|
|
101
|
+
];
|
|
102
|
+
if (stop.role !== null && stop.role !== undefined) parts.push(line(text` role=`, data(stop.role)));
|
|
103
|
+
if (stop.name !== null && stop.name !== undefined) parts.push(line(text` name=`, data(stop.name)));
|
|
104
|
+
const rect = rectOf(stop.rect);
|
|
105
|
+
if (rect) parts.push(line(text` rect `, formatCoordinate(rect)));
|
|
106
|
+
if (stop.scrollBefore && stop.scrollAfter) {
|
|
107
|
+
parts.push(fact` scroll x=${stop.scrollBefore.x ?? 0},y=${stop.scrollBefore.y ?? 0} → x=${stop.scrollAfter.x ?? 0},y=${stop.scrollAfter.y ?? 0}`);
|
|
108
|
+
}
|
|
109
|
+
if (stop.scrollJump) parts.push(text` scroll-jump=true`);
|
|
110
|
+
if (stop.focusVisibleStyle) {
|
|
111
|
+
parts.push(line(text` focus-visible outline=`, data(stop.focusVisibleStyle.outline ?? '(unavailable)')));
|
|
112
|
+
if (stop.focusVisibleStyle.boxShadow) parts.push(line(text` box-shadow=`, data(stop.focusVisibleStyle.boxShadow)));
|
|
113
|
+
}
|
|
114
|
+
const caveat = caveatSuffix(caveats);
|
|
115
|
+
if (caveat) parts.push(caveat);
|
|
116
|
+
return line(...parts);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function unreachedLine(item: UnreachedFocusable, caveats: ReturnType<typeof annotateUnstableFacts<FocusFact>>[number]['caveats']): FactLine {
|
|
120
|
+
const parts: FactLine[] = [text`unreached focusable: selector=`, data(item.selector ?? '(unresolved)')];
|
|
121
|
+
const rect = rectOf(item.rect);
|
|
122
|
+
if (rect) parts.push(line(text` rect `, formatCoordinate(rect)));
|
|
123
|
+
if (item.visible !== undefined) parts.push(fact` visible=${String(item.visible)}`);
|
|
124
|
+
const caveat = caveatSuffix(caveats);
|
|
125
|
+
if (caveat) parts.push(caveat);
|
|
126
|
+
return line(...parts);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Builds the read-only `measure map focus` report from an existing snapshot. */
|
|
130
|
+
export function mapFocus(ref: SnapRef): RenderableResult {
|
|
131
|
+
const focus = readFocus<FocusArtifact>(ref);
|
|
132
|
+
const meta = readMeta<SnapshotMeta>(ref);
|
|
133
|
+
const regions = unstableRegionsFor(ref);
|
|
134
|
+
const geometry = readGeometry<{ elements?: Array<{ id?: string; backendNodeId?: number }> }>(ref);
|
|
135
|
+
const geometryIdsByBackendNode = new Map<number, string>();
|
|
136
|
+
for (const element of geometry.elements ?? []) {
|
|
137
|
+
if (typeof element.backendNodeId === 'number' && typeof element.id === 'string') {
|
|
138
|
+
geometryIdsByBackendNode.set(element.backendNodeId, element.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const forward = focus.forward ?? [];
|
|
142
|
+
const reverse = focus.reverse ?? [];
|
|
143
|
+
const unreached = focus.unreachedFocusable ?? [];
|
|
144
|
+
|
|
145
|
+
const annotate = <T extends FocusStop | UnreachedFocusable>(items: readonly T[]) => annotateUnstableFacts<FocusFact>(
|
|
146
|
+
items.map((item) => ({ elementId: identity(item, geometryIdsByBackendNode), rect: rectOf(item.rect) })),
|
|
147
|
+
regions,
|
|
148
|
+
);
|
|
149
|
+
const annotatedForward = annotate(forward);
|
|
150
|
+
const annotatedReverse = annotate(reverse);
|
|
151
|
+
const annotatedUnreached = annotate(unreached);
|
|
152
|
+
|
|
153
|
+
const sections: FactLine[] = [];
|
|
154
|
+
if (focus.available === false) {
|
|
155
|
+
sections.push(fact`Focus traversal was unavailable during snapshot capture: ${focus.unavailableReason ?? 'capture did not return a completed traversal'}.`);
|
|
156
|
+
} else {
|
|
157
|
+
sections.push(lineList(annotatedForward.map((entry, index) => stopLine('forward', forward[index]!, entry.caveats))));
|
|
158
|
+
sections.push(lineList(annotatedReverse.map((entry, index) => stopLine('reverse', reverse[index]!, entry.caveats))));
|
|
159
|
+
if (focus.forwardTruncated || focus.reverseTruncated) {
|
|
160
|
+
sections.push(fact`Traversal recording cap reached: forward=${String(Boolean(focus.forwardTruncated))}, reverse=${String(Boolean(focus.reverseTruncated))}.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (annotatedUnreached.length) sections.push(lineList(annotatedUnreached.map((entry, index) => unreachedLine(unreached[index]!, entry.caveats))));
|
|
164
|
+
if (focus.scope) {
|
|
165
|
+
sections.push(fact`Traversal scope: root=${focus.scope.root ?? 'unavailable'}, shadow-dom=${focus.scope.shadowDom ?? 'unavailable'}, iframes-present=${focus.scope.iframesPresent ?? 0}, shadow-hosts-present=${focus.scope.shadowHostsPresent ?? 0}.`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
tag: 'focus-map',
|
|
170
|
+
attestation: {
|
|
171
|
+
kind: 'snapshot',
|
|
172
|
+
id: ref.id,
|
|
173
|
+
path: ref.dir,
|
|
174
|
+
note: meta.settled === false
|
|
175
|
+
? text`Snapshot was captured with unsettled regions; only facts intersecting those regions carry nondeterminism caveats.`
|
|
176
|
+
: fact`Snapshot settled${meta.settleMs === undefined ? '' : ` after ${meta.settleMs}ms`}.`,
|
|
177
|
+
},
|
|
178
|
+
attrs: {
|
|
179
|
+
forward: forward.length,
|
|
180
|
+
reverse: reverse.length,
|
|
181
|
+
'unreached-focusable': unreached.length,
|
|
182
|
+
settled: meta.settled ?? false,
|
|
183
|
+
},
|
|
184
|
+
summary: fact`Keyboard traversal facts: ${forward.length} forward stop(s), ${reverse.length} reverse stop(s), and ${unreached.length} unreached focusable element(s) from ${focus.candidateCount ?? 0} captured candidate(s).`,
|
|
185
|
+
sections,
|
|
186
|
+
followUp: fact`Read another snapshot map with \`capture measure map scroll ${ref.id}\` or \`capture measure map layers ${ref.id}\`.`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { LayerRecord, LayersReport } from './collectors/layers.js';
|
|
2
|
+
import type { SnapMeta } from '../../session/artifacts.js';
|
|
3
|
+
import {
|
|
4
|
+
annotateUnstableFacts,
|
|
5
|
+
readGeometry,
|
|
6
|
+
readLayers,
|
|
7
|
+
readMeta,
|
|
8
|
+
unstableRegionsFor,
|
|
9
|
+
type Rect,
|
|
10
|
+
type SnapRef,
|
|
11
|
+
type UnstableCaveat,
|
|
12
|
+
} from '../../output/artifact.js';
|
|
13
|
+
import {
|
|
14
|
+
data,
|
|
15
|
+
fact,
|
|
16
|
+
formatCoordinate,
|
|
17
|
+
formatProvenance,
|
|
18
|
+
line,
|
|
19
|
+
lineList,
|
|
20
|
+
text,
|
|
21
|
+
type FactLine,
|
|
22
|
+
type RenderableResult,
|
|
23
|
+
} from '../../output/render.js';
|
|
24
|
+
|
|
25
|
+
interface LayerFact {
|
|
26
|
+
readonly layer: LayerRecord;
|
|
27
|
+
readonly rect: Rect;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sourceFor(layer: LayerRecord): string | undefined {
|
|
31
|
+
const provenance = layer.styleProvenance;
|
|
32
|
+
if (!provenance) return undefined;
|
|
33
|
+
if (provenance.authored) {
|
|
34
|
+
return `${provenance.authored.file}:${provenance.authored.line}:${provenance.authored.column}`;
|
|
35
|
+
}
|
|
36
|
+
if (provenance.generated) {
|
|
37
|
+
return `${provenance.generated.sourceURL}:${provenance.generated.line}:${provenance.generated.column}`;
|
|
38
|
+
}
|
|
39
|
+
return provenance.sourceStyleSheetUrl;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function caveatLine(caveats: readonly UnstableCaveat[]): FactLine | undefined {
|
|
43
|
+
if (!caveats.length) return undefined;
|
|
44
|
+
return lineList(caveats.map((caveat) => fact`nondeterminism caveat: unstable region ${caveat.regionId}${caveat.selector ? ` (${caveat.selector})` : ''}${caveat.reason ? ` — ${caveat.reason}` : ''}`));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function uniqueCaveats(caveats: readonly UnstableCaveat[]): UnstableCaveat[] {
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
return caveats.filter((caveat) => {
|
|
50
|
+
const key = `${caveat.regionId}\u0000${caveat.selector ?? ''}\u0000${caveat.reason ?? ''}`;
|
|
51
|
+
if (seen.has(key)) return false;
|
|
52
|
+
seen.add(key);
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatLayer(layer: LayerRecord, membershipAvailable: boolean, caveats: readonly UnstableCaveat[]): FactLine {
|
|
58
|
+
const details: FactLine[] = [
|
|
59
|
+
line(
|
|
60
|
+
text`Layer `,
|
|
61
|
+
data(layer.id),
|
|
62
|
+
text` — `,
|
|
63
|
+
formatCoordinate({ x: layer.bounds.x, y: layer.bounds.y, w: layer.bounds.width, h: layer.bounds.height }),
|
|
64
|
+
text`; LayerTree delivery index `,
|
|
65
|
+
data(layer.layerPaintOrder),
|
|
66
|
+
text`; draws content `,
|
|
67
|
+
data(layer.drawsContentUnavailable ? 'unavailable' : String(layer.drawsContent)),
|
|
68
|
+
text`; paint count `,
|
|
69
|
+
data(layer.paintCountUnavailable ? 'unavailable' : layer.paintCount),
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
if (layer.backendNodeId !== null) details.push(fact`owner backend node ${layer.backendNodeId}${layer.selector ? ` (${layer.selector})` : ''}`);
|
|
73
|
+
if (layer.parentLayerId) details.push(fact`parent layer ${layer.parentLayerId}`);
|
|
74
|
+
if (layer.compositingReasonsUnavailable) details.push(text`Compositing reasons were unavailable for this layer.`);
|
|
75
|
+
else if (layer.compositingReasons.length) details.push(fact`Compositing reasons: ${layer.compositingReasons.join(', ')}`);
|
|
76
|
+
else details.push(text`Compositing reasons: none reported.`);
|
|
77
|
+
if (!membershipAvailable) details.push(text`Node membership was unavailable for this layer.`);
|
|
78
|
+
else if (layer.membersTruncated) details.push(fact`Node membership: ${layer.memberCount} painted node(s); first ${layer.memberBackendNodeIds.length} backend id(s): ${layer.memberBackendNodeIds.join(', ')}; ${layer.membersTruncated} additional id(s) not listed.`);
|
|
79
|
+
else details.push(fact`Node membership: ${layer.memberCount} painted node(s); backend id(s): ${layer.memberBackendNodeIds.join(', ') || '(none)'}.`);
|
|
80
|
+
if (layer.styleProvenance) {
|
|
81
|
+
const provenance = layer.styleProvenance;
|
|
82
|
+
details.push(line(
|
|
83
|
+
text`Layer-affecting property `,
|
|
84
|
+
data(provenance.property),
|
|
85
|
+
text`: `,
|
|
86
|
+
formatProvenance({
|
|
87
|
+
selector: provenance.selector ?? '(no selector)',
|
|
88
|
+
source: sourceFor(layer),
|
|
89
|
+
specificity: provenance.specificity ?? undefined,
|
|
90
|
+
extra: provenance.value === null ? text`computed value unavailable` : fact`computed value ${provenance.value}`,
|
|
91
|
+
}),
|
|
92
|
+
));
|
|
93
|
+
if (provenance.winnerApproximate) details.push(fact`Winning-declaration ordering is approximate${provenance.winnerApproximateReason ? `: ${provenance.winnerApproximateReason}` : ''}.`);
|
|
94
|
+
if (provenance.sourceResolutionUnavailable) details.push(fact`Source provenance resolution was unavailable${provenance.sourceResolutionUnavailableReason ? `: ${provenance.sourceResolutionUnavailableReason}` : ''}.`);
|
|
95
|
+
} else if (layer.styleProvenanceUnavailable) {
|
|
96
|
+
details.push(text`Layer-affecting declaration provenance was unavailable for this layer.`);
|
|
97
|
+
} else {
|
|
98
|
+
details.push(text`No author-declared layer-affecting property was recorded for this layer.`);
|
|
99
|
+
}
|
|
100
|
+
const caveat = caveatLine(caveats);
|
|
101
|
+
if (caveat) details.push(caveat);
|
|
102
|
+
return lineList(details);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function paintOrderLine(report: LayersReport, caveats: readonly UnstableCaveat[]): FactLine {
|
|
106
|
+
if (!report.paintOrder.available) return fact`DOMSnapshot paint order unavailable${report.paintOrder.reason ? `: ${report.paintOrder.reason}` : ''}.`;
|
|
107
|
+
const ids = report.paintOrder.backendNodeIds;
|
|
108
|
+
const suffix = report.paintOrder.truncated ? `; ${report.paintOrder.truncated} additional backend node id(s) not listed` : '';
|
|
109
|
+
const base = fact`DOMSnapshot paint order (back-to-front): ${ids.join(', ') || '(no painted backend nodes recorded)'}${suffix}.`;
|
|
110
|
+
const caveat = caveatLine(caveats);
|
|
111
|
+
return caveat ? lineList([base, caveat]) : base;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function buildMeasureMapLayersResult(ref: SnapRef): RenderableResult {
|
|
115
|
+
const report = readLayers<LayersReport>(ref);
|
|
116
|
+
const meta = readMeta<SnapMeta>(ref);
|
|
117
|
+
const regions = unstableRegionsFor(ref);
|
|
118
|
+
const geometry = readGeometry<{ elements?: Array<{ id?: string; backendNodeId?: number; rect?: Rect }> }>(ref);
|
|
119
|
+
const geometryByBackendNodeId = new Map<number, { id?: string; rect?: Rect }>();
|
|
120
|
+
for (const element of geometry.elements ?? []) {
|
|
121
|
+
if (typeof element.backendNodeId === 'number') geometryByBackendNodeId.set(element.backendNodeId, element);
|
|
122
|
+
}
|
|
123
|
+
const caveatsForBackendNodeIds = (ids: readonly number[]): UnstableCaveat[] => uniqueCaveats(
|
|
124
|
+
annotateUnstableFacts(ids.map((id) => {
|
|
125
|
+
const element = geometryByBackendNodeId.get(id);
|
|
126
|
+
return { elementId: element?.id, rect: element?.rect };
|
|
127
|
+
}), regions).flatMap(({ caveats }) => caveats),
|
|
128
|
+
);
|
|
129
|
+
const layerFacts: LayerFact[] = report.layers.map((layer) => ({
|
|
130
|
+
layer,
|
|
131
|
+
rect: { x: layer.bounds.x, y: layer.bounds.y, w: layer.bounds.width, h: layer.bounds.height },
|
|
132
|
+
}));
|
|
133
|
+
const annotated = annotateUnstableFacts(layerFacts, regions).map(({ fact: layerFact, caveats }) => ({
|
|
134
|
+
layerFact,
|
|
135
|
+
caveats: uniqueCaveats([...caveats, ...caveatsForBackendNodeIds(layerFact.layer.memberBackendNodeIds)]),
|
|
136
|
+
}));
|
|
137
|
+
const paintOrderCaveats = caveatsForBackendNodeIds(report.paintOrder.backendNodeIds);
|
|
138
|
+
const layerTreeStatus = report.layerTree.available ? 'available' : 'unavailable';
|
|
139
|
+
const paintOrderStatus = report.paintOrder.available ? 'available' : 'unavailable';
|
|
140
|
+
const membershipStatus = report.membership.available ? 'available' : 'unavailable';
|
|
141
|
+
const availability = report.layerTree.available
|
|
142
|
+
? fact`LayerTree facts available for ${report.layers.length} layer(s). DOMSnapshot paint order is ${paintOrderStatus}.`
|
|
143
|
+
: fact`LayerTree facts unavailable${report.layerTree.reason ? `: ${report.layerTree.reason}` : ''}. DOMSnapshot paint order is ${paintOrderStatus}.`;
|
|
144
|
+
const sections: FactLine[] = [availability, paintOrderLine(report, paintOrderCaveats)];
|
|
145
|
+
if (report.layersTruncated) sections.push(fact`${report.layersTruncated} compositor layer(s) were not listed because the snapshot collector capped the layer inventory.`);
|
|
146
|
+
if (report.membership.available) {
|
|
147
|
+
sections.push(fact`Per-node layer membership available; ${report.membership.unassignedCount} painted node(s) had no layer assignment.`);
|
|
148
|
+
} else {
|
|
149
|
+
sections.push(fact`Per-node layer membership unavailable${report.membership.reason ? `: ${report.membership.reason}` : ''}.`);
|
|
150
|
+
}
|
|
151
|
+
if (!report.styleSheetHeaders.available) {
|
|
152
|
+
sections.push(fact`Stylesheet-header provenance availability: unavailable${report.styleSheetHeaders.reason ? `: ${report.styleSheetHeaders.reason}` : ''}.`);
|
|
153
|
+
}
|
|
154
|
+
sections.push(...annotated.map(({ layerFact, caveats }) => formatLayer(layerFact.layer, report.membership.available, caveats)));
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
tag: 'layer-map',
|
|
158
|
+
attestation: {
|
|
159
|
+
kind: 'snapshot',
|
|
160
|
+
id: ref.id,
|
|
161
|
+
path: ref.dir,
|
|
162
|
+
note: meta.settled
|
|
163
|
+
? text`Snapshot was settled before its layer facts were captured.`
|
|
164
|
+
: fact`Snapshot was captured unsettled; layer facts carry per-region nondeterminism caveats where their bounds overlap marked unstable regions.`,
|
|
165
|
+
},
|
|
166
|
+
attrs: {
|
|
167
|
+
layers: report.layers.length,
|
|
168
|
+
'layer-tree': layerTreeStatus,
|
|
169
|
+
'paint-order': paintOrderStatus,
|
|
170
|
+
membership: membershipStatus,
|
|
171
|
+
settled: meta.settled,
|
|
172
|
+
},
|
|
173
|
+
summary: text`Paint/compositor layer facts, DOMSnapshot paint order, node membership, and available layer-style provenance are reported from layers.json.`,
|
|
174
|
+
sections,
|
|
175
|
+
followUp: fact`Inspect an element's cascade and stacking facts with \`capture measure explain ${ref.id} --selector <selector>\`.`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readGeometry,
|
|
3
|
+
readMeta,
|
|
4
|
+
readScroll,
|
|
5
|
+
unstableRegionsFor,
|
|
6
|
+
annotateUnstableFacts,
|
|
7
|
+
type Rect,
|
|
8
|
+
type SnapRef,
|
|
9
|
+
type UnstableCaveat,
|
|
10
|
+
type UnstableRegion,
|
|
11
|
+
} from '../../output/artifact.js';
|
|
12
|
+
import { data, fact, line, lineList, text, type FactLine, type RenderableResult } from '../../output/render.js';
|
|
13
|
+
|
|
14
|
+
interface RectLike {
|
|
15
|
+
readonly x?: unknown;
|
|
16
|
+
readonly y?: unknown;
|
|
17
|
+
readonly width?: unknown;
|
|
18
|
+
readonly height?: unknown;
|
|
19
|
+
readonly w?: unknown;
|
|
20
|
+
readonly h?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ScrollChild {
|
|
24
|
+
readonly id?: unknown;
|
|
25
|
+
readonly backendNodeId?: unknown;
|
|
26
|
+
readonly selector?: unknown;
|
|
27
|
+
readonly position?: unknown;
|
|
28
|
+
readonly scrollSnapAlign?: unknown;
|
|
29
|
+
readonly rect?: RectLike | null;
|
|
30
|
+
readonly nestedAncestryUnavailable?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ScrollSample {
|
|
34
|
+
readonly offsetTop?: unknown;
|
|
35
|
+
readonly visibleChildren?: readonly ScrollChild[];
|
|
36
|
+
readonly visibleChildrenTruncated?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ScrollContainer extends ScrollChild {
|
|
40
|
+
readonly isRoot?: unknown;
|
|
41
|
+
readonly scrollWidth?: unknown;
|
|
42
|
+
readonly scrollHeight?: unknown;
|
|
43
|
+
readonly clientWidth?: unknown;
|
|
44
|
+
readonly clientHeight?: unknown;
|
|
45
|
+
readonly scrollTop?: unknown;
|
|
46
|
+
readonly scrollLeft?: unknown;
|
|
47
|
+
readonly maxScrollTop?: unknown;
|
|
48
|
+
readonly maxScrollLeft?: unknown;
|
|
49
|
+
readonly overflowX?: unknown;
|
|
50
|
+
readonly overflowY?: unknown;
|
|
51
|
+
readonly scrollbarGutter?: unknown;
|
|
52
|
+
readonly scrollSnapType?: unknown;
|
|
53
|
+
readonly snapDescendants?: readonly ScrollChild[];
|
|
54
|
+
readonly snapDescendantsTruncated?: unknown;
|
|
55
|
+
readonly stickyFixedDescendants?: readonly ScrollChild[];
|
|
56
|
+
readonly stickyFixedDescendantsTruncated?: unknown;
|
|
57
|
+
readonly samples?: readonly ScrollSample[];
|
|
58
|
+
readonly nestedAncestry?: readonly unknown[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ScrollArtifact {
|
|
62
|
+
readonly available?: unknown;
|
|
63
|
+
readonly reason?: unknown;
|
|
64
|
+
readonly containers?: readonly ScrollContainer[];
|
|
65
|
+
readonly scrollContainersTotal?: unknown;
|
|
66
|
+
readonly scrollContainersTruncated?: unknown;
|
|
67
|
+
readonly scrollContainersCountUnavailable?: unknown;
|
|
68
|
+
readonly documentScrollHeight?: unknown;
|
|
69
|
+
readonly documentScrollWidth?: unknown;
|
|
70
|
+
readonly visualViewport?: unknown;
|
|
71
|
+
readonly visualViewportUnavailable?: unknown;
|
|
72
|
+
readonly layoutViewport?: unknown;
|
|
73
|
+
readonly layoutViewportUnavailable?: unknown;
|
|
74
|
+
readonly scope?: { readonly root?: unknown; readonly shadowDom?: unknown; readonly iframesPresent?: unknown; readonly shadowHostsPresent?: unknown };
|
|
75
|
+
readonly scopeCountsUnavailable?: unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SnapMeta {
|
|
79
|
+
readonly settled?: unknown;
|
|
80
|
+
readonly viewport?: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface GeometryElement {
|
|
84
|
+
readonly id?: unknown;
|
|
85
|
+
readonly backendNodeId?: unknown;
|
|
86
|
+
readonly rect?: RectLike | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CaveatContext {
|
|
90
|
+
readonly regions: readonly UnstableRegion[];
|
|
91
|
+
readonly geometryByBackendNodeId: ReadonlyMap<number, { readonly id?: string; readonly rect?: Rect }>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function numeric(value: unknown): number | undefined {
|
|
95
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function numberText(value: unknown): FactLine {
|
|
99
|
+
const n = numeric(value);
|
|
100
|
+
return n === undefined ? text`unavailable` : data(n);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function string(value: unknown, fallback = '(unavailable)'): string {
|
|
104
|
+
return typeof value === 'string' ? value : fallback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function rect(value: RectLike | null | undefined): Rect | undefined {
|
|
108
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
109
|
+
const x = numeric(value.x);
|
|
110
|
+
const y = numeric(value.y);
|
|
111
|
+
const w = numeric(value.w ?? value.width);
|
|
112
|
+
const h = numeric(value.h ?? value.height);
|
|
113
|
+
if (x === undefined || y === undefined || w === undefined || h === undefined) return undefined;
|
|
114
|
+
return { x, y, w, h };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatRect(value: RectLike | null | undefined): FactLine {
|
|
118
|
+
const measured = rect(value);
|
|
119
|
+
return measured
|
|
120
|
+
? fact`x=${measured.x} y=${measured.y} w=${measured.w} h=${measured.h}`
|
|
121
|
+
: text`rect unavailable`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatDimension(width: unknown, height: unknown): FactLine {
|
|
125
|
+
return line(numberText(width), text`×`, numberText(height));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function viewportFacts(label: string, value: unknown, unavailable: unknown): FactLine {
|
|
129
|
+
if (unavailable === true) return fact`${label}: viewport metrics unavailable in Page.getLayoutMetrics.`;
|
|
130
|
+
if (!value || typeof value !== 'object') return fact`${label}: unavailable`;
|
|
131
|
+
const viewport = value as Record<string, unknown>;
|
|
132
|
+
return line(
|
|
133
|
+
data(label),
|
|
134
|
+
text`: client `,
|
|
135
|
+
formatDimension(viewport.clientWidth, viewport.clientHeight),
|
|
136
|
+
text` at page x=`,
|
|
137
|
+
numberText(viewport.pageX),
|
|
138
|
+
text` y=`,
|
|
139
|
+
numberText(viewport.pageY),
|
|
140
|
+
text` scale=`,
|
|
141
|
+
numberText(viewport.scale),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function backendNodeId(value: unknown): number | undefined {
|
|
146
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function explicitElementId(value: unknown): string | undefined {
|
|
150
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Keeps only producer-supplied region identities. Settle's descriptive
|
|
155
|
+
* `tag#id.classes` labels cannot safely be reconciled with geometry paths:
|
|
156
|
+
* ids may contain punctuation and geometry intentionally caps class lists.
|
|
157
|
+
* A selector-only legacy region therefore remains unjoined rather than
|
|
158
|
+
* attributing its caveat to a potentially different measured element.
|
|
159
|
+
*/
|
|
160
|
+
function resolveRegions(regions: readonly UnstableRegion[]): readonly UnstableRegion[] {
|
|
161
|
+
return regions;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function uniqueCaveats(caveats: readonly UnstableCaveat[]): UnstableCaveat[] {
|
|
165
|
+
const seen = new Set<string>();
|
|
166
|
+
return caveats.filter((caveat) => {
|
|
167
|
+
const key = `${caveat.regionId}\u0000${caveat.selector ?? ''}\u0000${caveat.reason ?? ''}`;
|
|
168
|
+
if (seen.has(key)) return false;
|
|
169
|
+
seen.add(key);
|
|
170
|
+
return true;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function caveatsFor(target: ScrollChild, context: CaveatContext): readonly UnstableCaveat[] {
|
|
175
|
+
const backend = backendNodeId(target.backendNodeId);
|
|
176
|
+
const geometry = backend === undefined ? undefined : context.geometryByBackendNodeId.get(backend);
|
|
177
|
+
const ownRect = rect(target.rect) ?? geometry?.rect;
|
|
178
|
+
const elementIds = [
|
|
179
|
+
explicitElementId(target.id),
|
|
180
|
+
geometry?.id,
|
|
181
|
+
backend === undefined ? undefined : String(backend),
|
|
182
|
+
].filter((id): id is string => id !== undefined);
|
|
183
|
+
|
|
184
|
+
const facts = elementIds.length
|
|
185
|
+
? elementIds.map((elementId) => ({ elementId, rect: ownRect }))
|
|
186
|
+
: [{ rect: ownRect }];
|
|
187
|
+
return uniqueCaveats(annotateUnstableFacts(facts, context.regions).flatMap((entry) => entry.caveats));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function caveatLines(caveats: readonly UnstableCaveat[]): FactLine[] {
|
|
191
|
+
return caveats.map((caveat) => fact` nondeterminism caveat: unstable region ${caveat.regionId}${caveat.selector ? ` (${caveat.selector})` : ''}${caveat.reason ? ` — ${caveat.reason}` : ''}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function availabilityLines(label: string, children: readonly ScrollChild[] | undefined, truncated: unknown, context: CaveatContext): FactLine[] {
|
|
195
|
+
if (children === undefined) return [fact` ${label} facts unavailable in scroll.json.`];
|
|
196
|
+
if (children.length === 0) {
|
|
197
|
+
const none = label === 'snap point' ? 'none recorded' : 'none recorded';
|
|
198
|
+
return [fact` ${label}: ${none}.`];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const rows: FactLine[] = [];
|
|
202
|
+
for (const child of children) {
|
|
203
|
+
const detail = child.position ?? child.scrollSnapAlign ?? '(unavailable)';
|
|
204
|
+
rows.push(line(text` `, data(label), text` `, data(string(child.selector)), text` — `, data(string(detail)), text` · `, formatRect(child.rect)));
|
|
205
|
+
rows.push(...caveatLines(caveatsFor(child, context)));
|
|
206
|
+
}
|
|
207
|
+
if (truncated === true) rows.push(fact` ${label} enumeration reached its capture cap.`);
|
|
208
|
+
return rows;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sampleLines(samples: readonly ScrollSample[] | undefined, context: CaveatContext): FactLine[] {
|
|
212
|
+
if (samples === undefined) return [text` reachable-content samples unavailable in scroll.json.`];
|
|
213
|
+
if (samples.length === 0) return [text` reachable-content samples: none recorded.`];
|
|
214
|
+
|
|
215
|
+
const rows: FactLine[] = [];
|
|
216
|
+
for (const sample of samples) {
|
|
217
|
+
rows.push(line(text` sample offset y=`, numberText(sample.offsetTop), text`: `));
|
|
218
|
+
const children = sample.visibleChildren;
|
|
219
|
+
if (children === undefined) {
|
|
220
|
+
rows.push(text` visible children unavailable in scroll.json.`);
|
|
221
|
+
} else if (children.length === 0) {
|
|
222
|
+
rows.push(text` no visible child records at this sampled offset.`);
|
|
223
|
+
} else {
|
|
224
|
+
for (const child of children) {
|
|
225
|
+
rows.push(line(text` visible child `, data(string(child.selector)), text` · `, formatRect(child.rect)));
|
|
226
|
+
rows.push(...caveatLines(caveatsFor(child, context)).map((caveat) => line(text` `, caveat)));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (sample.visibleChildrenTruncated === true) rows.push(text` visible-child enumeration reached its capture cap.`);
|
|
230
|
+
}
|
|
231
|
+
return rows;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function scopeCount(value: unknown, unavailable: unknown, topologyAvailable: boolean): FactLine {
|
|
235
|
+
return !topologyAvailable || unavailable === true || numeric(value) === undefined ? text`unavailable` : numberText(value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function buildCaveatContext(ref: SnapRef): CaveatContext {
|
|
239
|
+
const geometry = readGeometry<{ elements?: readonly GeometryElement[] }>(ref);
|
|
240
|
+
const elements = geometry.elements ?? [];
|
|
241
|
+
const regions = resolveRegions(unstableRegionsFor(ref));
|
|
242
|
+
const geometryByBackendNodeId = new Map<number, { id?: string; rect?: Rect }>();
|
|
243
|
+
for (const element of elements) {
|
|
244
|
+
const backend = backendNodeId(element.backendNodeId);
|
|
245
|
+
if (backend !== undefined) geometryByBackendNodeId.set(backend, { id: explicitElementId(element.id), rect: rect(element.rect) });
|
|
246
|
+
}
|
|
247
|
+
return { regions, geometryByBackendNodeId };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Builds the read-only scroll topology report for a resolved snapshot. */
|
|
251
|
+
export function measureMapScroll(ref: SnapRef): RenderableResult {
|
|
252
|
+
const scroll = readScroll<ScrollArtifact>(ref);
|
|
253
|
+
const meta = readMeta<SnapMeta>(ref);
|
|
254
|
+
const topologyAvailable = scroll.available !== false;
|
|
255
|
+
const containersAvailable = topologyAvailable && Array.isArray(scroll.containers);
|
|
256
|
+
const containers = containersAvailable ? scroll.containers : [];
|
|
257
|
+
const caveatContext = buildCaveatContext(ref);
|
|
258
|
+
|
|
259
|
+
const sections: FactLine[] = [];
|
|
260
|
+
if (scroll.available === false) {
|
|
261
|
+
sections.push(fact`scroll.json reports that topology collection was unavailable: ${string(scroll.reason)}.`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const container of containers) {
|
|
265
|
+
const selector = container.isRoot === true ? '(document)' : string(container.selector);
|
|
266
|
+
const ancestry = container.nestedAncestryUnavailable === true
|
|
267
|
+
? 'unavailable'
|
|
268
|
+
: container.nestedAncestry === undefined
|
|
269
|
+
? 'unavailable'
|
|
270
|
+
: container.nestedAncestry.map((item) => string(item)).join(' → ') || '(top-level)';
|
|
271
|
+
const containerCaveats = caveatsFor(container, caveatContext);
|
|
272
|
+
const rows: FactLine[] = [
|
|
273
|
+
line(fact`${selector} — `, formatRect(container.rect), text` · range `, formatDimension(container.scrollWidth, container.scrollHeight), text` · client `, formatDimension(container.clientWidth, container.clientHeight)),
|
|
274
|
+
...caveatLines(containerCaveats),
|
|
275
|
+
line(text` offsets current x=`, numberText(container.scrollLeft), text` y=`, numberText(container.scrollTop), text`; max x=`, numberText(container.maxScrollLeft), text` y=`, numberText(container.maxScrollTop), fact` · overflow ${string(container.overflowX)}/${string(container.overflowY)} · gutter ${string(container.scrollbarGutter)} · snap type ${string(container.scrollSnapType)}`),
|
|
276
|
+
fact` nested ancestry: ${ancestry}`,
|
|
277
|
+
...availabilityLines('sticky/fixed', container.stickyFixedDescendants, container.stickyFixedDescendantsTruncated, caveatContext),
|
|
278
|
+
...availabilityLines('snap point', container.snapDescendants, container.snapDescendantsTruncated, caveatContext),
|
|
279
|
+
...sampleLines(container.samples, caveatContext),
|
|
280
|
+
...(container.nestedAncestryUnavailable === true ? [text` nested-ancestry measurement unavailable for this container.`] : []),
|
|
281
|
+
];
|
|
282
|
+
sections.push(lineList(rows));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const settled = meta.settled === true;
|
|
286
|
+
const totalAvailable = topologyAvailable && scroll.scrollContainersCountUnavailable !== true && numeric(scroll.scrollContainersTotal) !== undefined;
|
|
287
|
+
const measuredCount = totalAvailable ? numberText(scroll.scrollContainersTotal) : text`unavailable`;
|
|
288
|
+
const recordedCount = containersAvailable ? data(containers.length) : text`unavailable`;
|
|
289
|
+
const documentWidth = topologyAvailable ? scroll.documentScrollWidth : undefined;
|
|
290
|
+
const documentHeight = topologyAvailable ? scroll.documentScrollHeight : undefined;
|
|
291
|
+
return {
|
|
292
|
+
tag: 'scroll-map',
|
|
293
|
+
attestation: {
|
|
294
|
+
kind: 'snapshot',
|
|
295
|
+
id: ref.id,
|
|
296
|
+
path: ref.dir,
|
|
297
|
+
note: settled ? text`Snapshot settled before topology collection.` : text`Snapshot was captured without settledness; facts intersecting marked unstable regions carry nondeterminism caveats.`,
|
|
298
|
+
},
|
|
299
|
+
attrs: {
|
|
300
|
+
containers: containersAvailable ? containers.length : undefined,
|
|
301
|
+
'scroll-containers': totalAvailable ? numeric(scroll.scrollContainersTotal) : undefined,
|
|
302
|
+
settled,
|
|
303
|
+
viewport: typeof meta.viewport === 'string' ? meta.viewport : undefined,
|
|
304
|
+
},
|
|
305
|
+
summary: line(measuredCount, text` measured scroll container(s) (`, recordedCount, text` recorded); document extent `, formatDimension(documentWidth, documentHeight), text`.`),
|
|
306
|
+
sections: [
|
|
307
|
+
viewportFacts('visual viewport', scroll.visualViewport, scroll.visualViewportUnavailable),
|
|
308
|
+
viewportFacts('layout viewport', scroll.layoutViewport, scroll.layoutViewportUnavailable),
|
|
309
|
+
line(text`scope: root `, data(string(scroll.scope?.root)), text`; shadow DOM `, data(string(scroll.scope?.shadowDom)), text`; iframes `, scopeCount(scroll.scope?.iframesPresent, scroll.scopeCountsUnavailable, topologyAvailable), text`; shadow hosts `, scopeCount(scroll.scope?.shadowHostsPresent, scroll.scopeCountsUnavailable, topologyAvailable), text`.`),
|
|
310
|
+
...(!containersAvailable ? [text`Scroll container records were unavailable in scroll.json; no container rows are rendered below.`] : []),
|
|
311
|
+
...(!totalAvailable && containersAvailable ? [text`Scroll-container total/truncation counts were unavailable in scroll.json; recorded container rows are still rendered below.`] : []),
|
|
312
|
+
...(scroll.scrollContainersTruncated === true ? [text`Scroll-container enumeration reached its capture cap; recorded containers are a prefix of the measured topology.`] : []),
|
|
313
|
+
...sections,
|
|
314
|
+
],
|
|
315
|
+
followUp: line(fact`Read focus or layer facts from ${ref.id} with `, text`\`capture measure map focus `, data(ref.id), text`\` or \`capture measure map layers `, data(ref.id), text`\`.`),
|
|
316
|
+
};
|
|
317
|
+
}
|