@crouton-kit/capture 1.3.8 → 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 +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,498 @@
|
|
|
1
|
+
import {
|
|
2
|
+
annotateUnstableFacts,
|
|
3
|
+
artifactExists,
|
|
4
|
+
readAx,
|
|
5
|
+
readFocus,
|
|
6
|
+
readForms,
|
|
7
|
+
readGeometry,
|
|
8
|
+
readMeta,
|
|
9
|
+
readQueries,
|
|
10
|
+
readScroll,
|
|
11
|
+
readStates,
|
|
12
|
+
readStyles,
|
|
13
|
+
readText,
|
|
14
|
+
unstableRegionsFor,
|
|
15
|
+
type AnnotatedFact,
|
|
16
|
+
type Rect,
|
|
17
|
+
type SnapRef,
|
|
18
|
+
} from '../../output/artifact.js';
|
|
19
|
+
import { parseSelectorInput, resolveSelectorInput, type ElementRecord, type SelectorInputKind } from '../../output/selector.js';
|
|
20
|
+
import { fact, line, text, type FactLine } from '../../output/render.js';
|
|
21
|
+
|
|
22
|
+
export interface ExplainDetailOptions {
|
|
23
|
+
readonly size?: boolean;
|
|
24
|
+
readonly text?: boolean;
|
|
25
|
+
readonly form?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ArtifactElement extends ElementRecord {
|
|
29
|
+
readonly backendNodeId?: number | null;
|
|
30
|
+
readonly geometryId?: string;
|
|
31
|
+
readonly rect?: RectLike | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface RectLike {
|
|
35
|
+
readonly x: number;
|
|
36
|
+
readonly y: number;
|
|
37
|
+
readonly width?: number;
|
|
38
|
+
readonly height?: number;
|
|
39
|
+
readonly w?: number;
|
|
40
|
+
readonly h?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface GeometryElement extends ArtifactElement {
|
|
44
|
+
readonly tag?: string;
|
|
45
|
+
readonly domPath?: string;
|
|
46
|
+
readonly zIndex?: string;
|
|
47
|
+
readonly boxModel?: Record<string, unknown> | null;
|
|
48
|
+
readonly stackingContext?: { readonly creates?: boolean; readonly reasons?: readonly string[] };
|
|
49
|
+
readonly clipping?: { readonly clippedBy?: string; readonly clippedFraction?: number } | null;
|
|
50
|
+
readonly visibility?: { readonly visible?: boolean; readonly opacity?: number };
|
|
51
|
+
readonly layout?: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface StyleElement extends ArtifactElement {
|
|
55
|
+
readonly computed?: Record<string, string | null>;
|
|
56
|
+
readonly winningDeclarations?: readonly WinningDeclaration[];
|
|
57
|
+
readonly provenanceUnavailable?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface WinningDeclaration {
|
|
61
|
+
readonly property?: string;
|
|
62
|
+
readonly value?: string | null;
|
|
63
|
+
readonly declaredValue?: string;
|
|
64
|
+
readonly selector?: string | null;
|
|
65
|
+
readonly specificity?: string | null;
|
|
66
|
+
readonly important?: boolean;
|
|
67
|
+
readonly authored?: { readonly file?: string; readonly line?: number; readonly column?: number };
|
|
68
|
+
readonly generated?: { readonly sourceURL?: string; readonly line?: number; readonly column?: number };
|
|
69
|
+
readonly sourceStyleSheetUrl?: string;
|
|
70
|
+
readonly mediaQuery?: string;
|
|
71
|
+
readonly containerQuery?: string;
|
|
72
|
+
readonly winnerApproximate?: boolean;
|
|
73
|
+
readonly winnerApproximateReason?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ExplainFact {
|
|
77
|
+
readonly line: FactLine;
|
|
78
|
+
readonly elementId?: string;
|
|
79
|
+
readonly rect?: Rect;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ExplainSection {
|
|
83
|
+
readonly kind: 'element' | 'cascade' | 'stacking-clipping' | 'focus-scroll' | 'queries-states' | 'size' | 'text' | 'form';
|
|
84
|
+
readonly facts: readonly AnnotatedFact<ExplainFact>[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ExplainSuccess {
|
|
88
|
+
readonly kind: 'explanation';
|
|
89
|
+
readonly ref: SnapRef;
|
|
90
|
+
readonly requestedSelector: string;
|
|
91
|
+
readonly element: GeometryElement;
|
|
92
|
+
readonly matchCount: number;
|
|
93
|
+
readonly meta: { readonly settled?: boolean; readonly settleMs?: number };
|
|
94
|
+
readonly sections: readonly ExplainSection[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MissingSelectorRecovery {
|
|
98
|
+
readonly recordCount: number;
|
|
99
|
+
readonly kind: SelectorInputKind;
|
|
100
|
+
readonly candidates: readonly string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ExplainMissingSelector {
|
|
104
|
+
readonly kind: 'missing-selector';
|
|
105
|
+
readonly ref: SnapRef;
|
|
106
|
+
readonly selector: string;
|
|
107
|
+
readonly meta: { readonly settled?: boolean; readonly settleMs?: number };
|
|
108
|
+
readonly available: MissingSelectorRecovery;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type ExplainSnapshotResult = ExplainSuccess | ExplainMissingSelector;
|
|
112
|
+
|
|
113
|
+
type RawSection = { kind: ExplainSection['kind']; facts: ExplainFact[] };
|
|
114
|
+
|
|
115
|
+
function rectOf(rect: RectLike | null | undefined): Rect | undefined {
|
|
116
|
+
if (!rect || !Number.isFinite(rect.x) || !Number.isFinite(rect.y)) return undefined;
|
|
117
|
+
const w = rect.w ?? rect.width;
|
|
118
|
+
const h = rect.h ?? rect.height;
|
|
119
|
+
if (!Number.isFinite(w) || !Number.isFinite(h)) return undefined;
|
|
120
|
+
return { x: rect.x, y: rect.y, w: w!, h: h! };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function factFor(element: GeometryElement, rendered: FactLine): ExplainFact {
|
|
124
|
+
return { line: rendered, elementId: element.id, rect: rectOf(element.rect) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function joinToGeometry<T extends ArtifactElement>(records: readonly T[], geometry: GeometryElement): T[] {
|
|
128
|
+
if (typeof geometry.backendNodeId === 'number') {
|
|
129
|
+
const byBackend = records.filter((record) => record.backendNodeId === geometry.backendNodeId);
|
|
130
|
+
if (byBackend.length) return byBackend;
|
|
131
|
+
}
|
|
132
|
+
return records.filter((record) => record.id === geometry.id || record.geometryId === geometry.id);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function enrichGeometry(
|
|
136
|
+
geometry: readonly GeometryElement[],
|
|
137
|
+
axNodes: readonly ArtifactElement[],
|
|
138
|
+
textElements: readonly ArtifactElement[],
|
|
139
|
+
): ElementRecord[] {
|
|
140
|
+
return geometry.map((element) => {
|
|
141
|
+
const ax = joinToGeometry(axNodes, element)[0];
|
|
142
|
+
const textRecord = joinToGeometry(textElements, element)[0];
|
|
143
|
+
return {
|
|
144
|
+
...element,
|
|
145
|
+
axId: ax?.axId,
|
|
146
|
+
axName: ax?.axName,
|
|
147
|
+
text: element.text ?? textRecord?.text,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readOptional<T>(ref: SnapRef, filename: string, reader: (snap: SnapRef) => T, fallback: T): T {
|
|
153
|
+
return artifactExists(ref, filename) ? reader(ref) : fallback;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const RECOVERY_CANDIDATE_LIMIT = 8;
|
|
157
|
+
|
|
158
|
+
function editDistance(a: string, b: string): number {
|
|
159
|
+
const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
160
|
+
for (let i = 1; i <= a.length; i++) {
|
|
161
|
+
let diagonal = previous[0]!;
|
|
162
|
+
previous[0] = i;
|
|
163
|
+
for (let j = 1; j <= b.length; j++) {
|
|
164
|
+
const above = previous[j]!;
|
|
165
|
+
previous[j] = Math.min(previous[j]! + 1, previous[j - 1]! + 1, diagonal + Number(a[i - 1] !== b[j - 1]));
|
|
166
|
+
diagonal = above;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return previous[b.length]!;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectorIdentifierDistance(requested: string, candidate: string): number {
|
|
173
|
+
const identifiers = (value: string) => value.toLowerCase().match(/[a-z][a-z0-9_-]*/g)?.filter((token) => token.length > 1) ?? [];
|
|
174
|
+
const requestedTokens = identifiers(requested);
|
|
175
|
+
const candidateTokens = identifiers(candidate);
|
|
176
|
+
if (!requestedTokens.length || !candidateTokens.length) return Number.POSITIVE_INFINITY;
|
|
177
|
+
return requestedTokens.reduce((total, token) => total + Math.min(...candidateTokens.map((other) => editDistance(token, other) / Math.max(token.length, other.length))), 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function recoveryForms(elements: readonly ElementRecord[], requested: string): MissingSelectorRecovery {
|
|
181
|
+
const input = parseSelectorInput(requested);
|
|
182
|
+
const formFor = (element: ElementRecord): string | undefined => {
|
|
183
|
+
switch (input.kind) {
|
|
184
|
+
case 'css': return element.selector;
|
|
185
|
+
case 'backend': return typeof element.backendNodeId === 'number' ? `backend:${element.backendNodeId}` : undefined;
|
|
186
|
+
case 'axid': return element.axId ? `axid:${element.axId}` : undefined;
|
|
187
|
+
case 'ax': return element.axName ? `ax:${element.axName}` : undefined;
|
|
188
|
+
case 'text': return element.text ? `text:${element.text}` : undefined;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const valueOf = (candidate: string) => input.kind === 'css' ? candidate : candidate.slice(candidate.indexOf(':') + 1);
|
|
192
|
+
const candidates = [...new Set(elements.map(formFor).filter((candidate): candidate is string => Boolean(candidate)))];
|
|
193
|
+
candidates.sort((a, b) => {
|
|
194
|
+
const aValue = valueOf(a).toLowerCase(), bValue = valueOf(b).toLowerCase();
|
|
195
|
+
const aIdentifierDistance = selectorIdentifierDistance(input.value, aValue), bIdentifierDistance = selectorIdentifierDistance(input.value, bValue);
|
|
196
|
+
const aDistance = editDistance(input.value.toLowerCase(), aValue), bDistance = editDistance(input.value.toLowerCase(), bValue);
|
|
197
|
+
return aIdentifierDistance - bIdentifierDistance || aDistance - bDistance || a.length - b.length || a.localeCompare(b);
|
|
198
|
+
});
|
|
199
|
+
return { recordCount: elements.length, kind: input.kind, candidates: candidates.slice(0, RECOVERY_CANDIDATE_LIMIT) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sourceDescription(declaration: WinningDeclaration): string | undefined {
|
|
203
|
+
if (declaration.authored?.file) {
|
|
204
|
+
return `${declaration.authored.file}:${declaration.authored.line ?? 0}:${declaration.authored.column ?? 0} (authored)`;
|
|
205
|
+
}
|
|
206
|
+
if (declaration.generated?.sourceURL) {
|
|
207
|
+
return `${declaration.generated.sourceURL}:${declaration.generated.line ?? 0}:${declaration.generated.column ?? 0} (generated)`;
|
|
208
|
+
}
|
|
209
|
+
if (declaration.sourceStyleSheetUrl) return `${declaration.sourceStyleSheetUrl} (generated source)`;
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function ancestorElements(all: readonly GeometryElement[], target: GeometryElement): GeometryElement[] {
|
|
214
|
+
if (!target.domPath) return [];
|
|
215
|
+
return all
|
|
216
|
+
.filter((candidate) => candidate.id !== target.id && candidate.domPath && target.domPath!.startsWith(`${candidate.domPath}/`))
|
|
217
|
+
.sort((a, b) => (a.domPath?.split('/').length ?? 0) - (b.domPath?.split('/').length ?? 0));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function elementSection(target: GeometryElement, matchCount: number): RawSection {
|
|
221
|
+
const rect = rectOf(target.rect);
|
|
222
|
+
const facts: ExplainFact[] = [];
|
|
223
|
+
if (rect) {
|
|
224
|
+
facts.push(factFor(target, fact`Element ${target.selector ?? target.id}: rect x=${rect.x} y=${rect.y} w=${rect.w} h=${rect.h}; visible=${String(target.visibility?.visible ?? 'unknown')}; opacity=${target.visibility?.opacity ?? 'unknown'}.`));
|
|
225
|
+
} else {
|
|
226
|
+
facts.push(factFor(target, fact`Element ${target.selector ?? target.id}: no resolved geometry rect was recorded.`));
|
|
227
|
+
}
|
|
228
|
+
if (matchCount > 1) facts.push(factFor(target, fact`Selector matched ${matchCount} geometry records; this explanation uses the first record in snapshot order.`));
|
|
229
|
+
return { kind: 'element', facts };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function cascadeSection(target: GeometryElement, styles: readonly StyleElement[]): RawSection {
|
|
233
|
+
const facts: ExplainFact[] = [];
|
|
234
|
+
const matches = joinToGeometry(styles, target);
|
|
235
|
+
if (!matches.length) {
|
|
236
|
+
facts.push(factFor(target, text`No style record joined to this geometry record by backend node id or geometry element id.`));
|
|
237
|
+
return { kind: 'cascade', facts };
|
|
238
|
+
}
|
|
239
|
+
for (const style of matches) {
|
|
240
|
+
if (style.provenanceUnavailable) facts.push(factFor(target, text`Winning-declaration provenance was unavailable for this style record.`));
|
|
241
|
+
for (const declaration of style.winningDeclarations ?? []) {
|
|
242
|
+
const property = declaration.property ?? '(unknown property)';
|
|
243
|
+
const value = declaration.value ?? '(no computed value)';
|
|
244
|
+
const selector = declaration.selector ?? '(no author declaration)';
|
|
245
|
+
const specificity = declaration.specificity ?? (declaration.selector === 'inline' ? 'inline' : 'none');
|
|
246
|
+
const source = sourceDescription(declaration);
|
|
247
|
+
const important = declaration.important ? ' !important' : '';
|
|
248
|
+
facts.push(factFor(target, source
|
|
249
|
+
? fact`Style winner ${property}=${value}${important}: selector ${selector}; specificity ${specificity}; source ${source}.`
|
|
250
|
+
: fact`Style winner ${property}=${value}${important}: selector ${selector}; specificity ${specificity}; selector-only provenance.`));
|
|
251
|
+
if (declaration.winnerApproximate) {
|
|
252
|
+
facts.push(factFor(target, fact`Style winner ${property} has approximate cascade ordering: ${declaration.winnerApproximateReason ?? 'simplified cascade model'}.`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!facts.length) facts.push(factFor(target, text`The joined style record contains no recorded winning declarations.`));
|
|
257
|
+
return { kind: 'cascade', facts };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stackingClippingSection(target: GeometryElement, geometry: readonly GeometryElement[]): RawSection {
|
|
261
|
+
const facts: ExplainFact[] = [];
|
|
262
|
+
const ancestors = ancestorElements(geometry, target);
|
|
263
|
+
const stacking = ancestors.filter((element) => element.stackingContext?.creates);
|
|
264
|
+
const root = ancestors[0];
|
|
265
|
+
if (root) facts.push(factFor(root, fact`Stacking climb root: ${root.selector ?? root.tag ?? root.id}; z-index=${root.zIndex ?? 'auto'}.`));
|
|
266
|
+
for (const ancestor of stacking) {
|
|
267
|
+
facts.push(factFor(ancestor, fact`Stacking ancestor ${ancestor.selector ?? ancestor.id}: z-index=${ancestor.zIndex ?? 'auto'}; creates context from ${(ancestor.stackingContext?.reasons ?? []).join(', ') || 'recorded context trigger'}.`));
|
|
268
|
+
}
|
|
269
|
+
facts.push(factFor(target, fact`Target stacking record: z-index=${target.zIndex ?? 'auto'}; creates-context=${String(Boolean(target.stackingContext?.creates))}; reasons=${(target.stackingContext?.reasons ?? []).join(', ') || 'none recorded'}.`));
|
|
270
|
+
|
|
271
|
+
const clippingAncestors = ancestors.filter((element) => {
|
|
272
|
+
const layout = element.layout ?? {};
|
|
273
|
+
return [layout.overflowX, layout.overflowY].some((value) => typeof value === 'string' && /hidden|auto|scroll|clip/.test(value));
|
|
274
|
+
});
|
|
275
|
+
for (const ancestor of clippingAncestors) {
|
|
276
|
+
const layout = ancestor.layout ?? {};
|
|
277
|
+
const active = target.clipping?.clippedBy === ancestor.selector;
|
|
278
|
+
facts.push(factFor(ancestor, fact`Clipping ancestor ${ancestor.selector ?? ancestor.id}: overflow-x=${String(layout.overflowX ?? 'unknown')}; overflow-y=${String(layout.overflowY ?? 'unknown')}; clips-target=${String(active)}${active && target.clipping?.clippedFraction !== undefined ? `; visible-fraction=${target.clipping.clippedFraction}` : ''}.`));
|
|
279
|
+
}
|
|
280
|
+
if (target.clipping?.clippedBy && !clippingAncestors.some((ancestor) => ancestor.selector === target.clipping?.clippedBy)) {
|
|
281
|
+
facts.push(factFor(target, fact`Recorded clipping source ${target.clipping.clippedBy}; visible-fraction=${target.clipping.clippedFraction ?? 'unknown'}.`));
|
|
282
|
+
} else if (!target.clipping) {
|
|
283
|
+
facts.push(factFor(target, text`No clipping source was recorded for the target.`));
|
|
284
|
+
}
|
|
285
|
+
return { kind: 'stacking-clipping', facts };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sameTarget(record: ArtifactElement, target: GeometryElement): boolean {
|
|
289
|
+
if (typeof target.backendNodeId === 'number' && record.backendNodeId === target.backendNodeId) return true;
|
|
290
|
+
return record.id === target.id || record.geometryId === target.id;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function focusScrollSection(target: GeometryElement, focus: Record<string, unknown>, scroll: Record<string, unknown>): RawSection {
|
|
294
|
+
const facts: ExplainFact[] = [];
|
|
295
|
+
const focusStops = [...(Array.isArray(focus.forward) ? focus.forward : []), ...(Array.isArray(focus.reverse) ? focus.reverse : [])]
|
|
296
|
+
.filter((entry): entry is ArtifactElement & Record<string, unknown> => Boolean(entry && typeof entry === 'object'))
|
|
297
|
+
.filter((entry) => sameTarget(entry, target));
|
|
298
|
+
if (focus.available === false) {
|
|
299
|
+
facts.push(factFor(target, fact`Focus traversal context was unavailable: ${String(focus.unavailableReason ?? 'unavailable reason not recorded')}.`));
|
|
300
|
+
} else if (focusStops.length) {
|
|
301
|
+
for (const stop of focusStops) {
|
|
302
|
+
facts.push(factFor(target, fact`Focus context: step=${String(stop.step ?? 'unknown')}; selector=${String(stop.selector ?? target.selector ?? target.id)}; scroll-jump=${String(stop.scrollJump ?? false)}; focus-visible-outline=${String((stop.focusVisibleStyle as Record<string, unknown> | undefined)?.outline ?? 'none recorded')}.`));
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
facts.push(factFor(target, fact`Focus context: target was not present in the ${Number(focus.candidateCount ?? 0)}-candidate captured traversal.`));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const containers = (Array.isArray(scroll.containers) ? scroll.containers : [])
|
|
309
|
+
.filter((entry): entry is ArtifactElement & Record<string, unknown> => Boolean(entry && typeof entry === 'object'));
|
|
310
|
+
const related = containers.filter((container) => {
|
|
311
|
+
if (sameTarget(container, target)) return true;
|
|
312
|
+
const descendants = [
|
|
313
|
+
...(Array.isArray(container.snapDescendants) ? container.snapDescendants : []),
|
|
314
|
+
...(Array.isArray(container.stickyFixedDescendants) ? container.stickyFixedDescendants : []),
|
|
315
|
+
...(Array.isArray(container.samples) ? container.samples.flatMap((sample) => sample && typeof sample === 'object' && Array.isArray((sample as Record<string, unknown>).visibleChildren) ? (sample as Record<string, unknown>).visibleChildren as unknown[] : []) : []),
|
|
316
|
+
];
|
|
317
|
+
return descendants.some((entry) => Boolean(entry && typeof entry === 'object' && sameTarget(entry as ArtifactElement, target)));
|
|
318
|
+
});
|
|
319
|
+
if (scroll.available === false) {
|
|
320
|
+
facts.push(factFor(target, fact`Scroll topology context was unavailable: ${String(scroll.reason ?? 'unavailable reason not recorded')}.`));
|
|
321
|
+
} else if (related.length) {
|
|
322
|
+
for (const container of related) {
|
|
323
|
+
facts.push(factFor(target, fact`Scroll context: container=${String(container.selector ?? '(root)')}; offsets left=${Number(container.scrollLeft ?? 0)} top=${Number(container.scrollTop ?? 0)}; maxima left=${Number(container.maxScrollLeft ?? 0)} top=${Number(container.maxScrollTop ?? 0)}; nested ancestry=${Array.isArray(container.nestedAncestry) ? container.nestedAncestry.join(' > ') || 'none' : 'not recorded'}.`));
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
facts.push(factFor(target, fact`Scroll context: no target relation was recorded among ${containers.length} captured container record(s).`));
|
|
327
|
+
}
|
|
328
|
+
return { kind: 'focus-scroll', facts };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function queriesStatesSection(target: GeometryElement, queries: Record<string, unknown>, states: Record<string, unknown> | undefined, declarations: readonly StyleElement[]): RawSection {
|
|
332
|
+
const facts: ExplainFact[] = [];
|
|
333
|
+
const queryContexts = joinToGeometry(declarations, target).flatMap((style) => style.winningDeclarations ?? []).flatMap((declaration) => [
|
|
334
|
+
declaration.mediaQuery ? `media ${declaration.mediaQuery}` : undefined,
|
|
335
|
+
declaration.containerQuery ? `container ${declaration.containerQuery}` : undefined,
|
|
336
|
+
].filter((value): value is string => Boolean(value)));
|
|
337
|
+
const selector = target.selector;
|
|
338
|
+
const activeMedia = (Array.isArray(queries.mediaQueries) ? queries.mediaQueries : []).filter((entry) => {
|
|
339
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
340
|
+
const query = entry as Record<string, unknown>;
|
|
341
|
+
return query.matched === true && (!selector || !Array.isArray(query.affectedSelectors) || query.affectedSelectors.includes(selector));
|
|
342
|
+
}) as Array<Record<string, unknown>>;
|
|
343
|
+
const activeContainers = (Array.isArray(queries.containerQueries) ? queries.containerQueries : []).filter((entry) => {
|
|
344
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
345
|
+
const query = entry as Record<string, unknown>;
|
|
346
|
+
return query.matched === true && (!selector || !Array.isArray(query.affectedSelectors) || query.affectedSelectors.includes(selector));
|
|
347
|
+
}) as Array<Record<string, unknown>>;
|
|
348
|
+
if (queries.available === false) {
|
|
349
|
+
facts.push(factFor(target, fact`Active query context was unavailable: ${String(queries.unavailableReason ?? 'unavailable reason not recorded')}.`));
|
|
350
|
+
} else {
|
|
351
|
+
const environment = queries.environment && typeof queries.environment === 'object' ? queries.environment as Record<string, unknown> : {};
|
|
352
|
+
facts.push(factFor(target, fact`Query environment: width=${Number(environment.width ?? 0)}; height=${Number(environment.height ?? 0)}; dpr=${Number(environment.dpr ?? 0)}; color-scheme=${String(environment.colorScheme ?? 'unknown')}; pointer=${String(environment.pointer ?? 'unknown')}; hover=${String(environment.hover ?? 'unknown')}; reduced-motion=${String(environment.reducedMotion ?? 'unknown')}; forced-colors=${String(environment.forcedColors ?? 'unknown')}.`));
|
|
353
|
+
for (const query of activeMedia) facts.push(factFor(target, fact`Active media query ${String(query.query ?? '(condition unavailable)')}.`));
|
|
354
|
+
for (const query of activeContainers) facts.push(factFor(target, fact`Active container query ${String(query.query ?? '(condition unavailable)')}; container=${String(query.containerSelector ?? '(unresolved)')}; size=${JSON.stringify(query.containerSize ?? null)}.`));
|
|
355
|
+
for (const context of [...new Set(queryContexts)]) facts.push(factFor(target, fact`Winning declaration query context: ${context}.`));
|
|
356
|
+
if (!activeMedia.length && !activeContainers.length && !queryContexts.length) facts.push(factFor(target, text`No active query context was recorded for the target's winning declarations.`));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (states) {
|
|
360
|
+
const requested = Array.isArray(states.requested) ? states.requested.map(String) : [];
|
|
361
|
+
const records = (Array.isArray(states.elements) ? states.elements : [])
|
|
362
|
+
.filter((entry): entry is ArtifactElement & Record<string, unknown> => Boolean(entry && typeof entry === 'object'))
|
|
363
|
+
.filter((entry) => sameTarget(entry, target));
|
|
364
|
+
facts.push(factFor(target, fact`State matrix context: requested=${requested.join(', ') || 'none'}; matching records=${records.length}.`));
|
|
365
|
+
for (const record of records) {
|
|
366
|
+
const changes = record.style && typeof record.style === 'object' && Array.isArray((record.style as Record<string, unknown>).changed)
|
|
367
|
+
? ((record.style as Record<string, unknown>).changed as unknown[]).map(String).join(', ')
|
|
368
|
+
: 'none recorded';
|
|
369
|
+
facts.push(factFor(target, fact`State ${String(record.state ?? 'unknown')}: supported=${String(record.supported ?? false)}; style changes=${changes}; geometry-changed=${String((record.geometry as Record<string, unknown> | undefined)?.changed ?? false)}; hittest-changed=${String((record.hittest as Record<string, unknown> | undefined)?.changed ?? false)}.`));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return { kind: 'queries-states', facts };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function sizeSection(target: GeometryElement): RawSection {
|
|
376
|
+
const facts: ExplainFact[] = [];
|
|
377
|
+
const layout = target.layout ?? {};
|
|
378
|
+
const box = target.boxModel ?? {};
|
|
379
|
+
facts.push(factFor(target, fact`Box model: measured width=${String(box.width ?? 'unavailable')}; height=${String(box.height ?? 'unavailable')}; content=${JSON.stringify(box.content ?? null)}; padding=${JSON.stringify(box.padding ?? null)}; border=${JSON.stringify(box.border ?? null)}; margin=${JSON.stringify(box.margin ?? null)}.`));
|
|
380
|
+
facts.push(factFor(target, fact`Layout: display=${String(layout.display ?? 'unknown')}; position=${String(layout.position ?? 'unknown')}; box-sizing=${String(layout.boxSizing ?? 'unknown')}; client=${Number(layout.clientWidth ?? 0)}×${Number(layout.clientHeight ?? 0)}; scroll=${Number(layout.scrollWidth ?? 0)}×${Number(layout.scrollHeight ?? 0)}; overflow contribution x=${String(layout.contributesOverflowX ?? false)} y=${String(layout.contributesOverflowY ?? false)}.`));
|
|
381
|
+
facts.push(factFor(target, fact`Constraints: min-width=${String(layout.minWidth ?? 'unknown')}; max-width=${String(layout.maxWidth ?? 'unknown')}; min-height=${String(layout.minHeight ?? 'unknown')}; max-height=${String(layout.maxHeight ?? 'unknown')}; aspect-ratio=${String(layout.aspectRatio ?? 'unknown')}.`));
|
|
382
|
+
if (layout.flex && typeof layout.flex === 'object') {
|
|
383
|
+
const flex = layout.flex as Record<string, unknown>;
|
|
384
|
+
facts.push(factFor(target, fact`Flex allocation: grow=${Number(flex.grow ?? 0)}; shrink=${Number(flex.shrink ?? 0)}; basis=${String(flex.basis ?? 'unknown')}; align-self=${String(flex.alignSelf ?? 'unknown')}; order=${Number(flex.order ?? 0)}; container=${JSON.stringify(flex.container ?? {})}.`));
|
|
385
|
+
} else facts.push(factFor(target, text`Flex allocation: target is not recorded as a flex item.`));
|
|
386
|
+
if (layout.grid && typeof layout.grid === 'object') {
|
|
387
|
+
const grid = layout.grid as Record<string, unknown>;
|
|
388
|
+
facts.push(factFor(target, fact`Grid allocation: column=${String(grid.columnStart ?? 'auto')} / ${String(grid.columnEnd ?? 'auto')}; row=${String(grid.rowStart ?? 'auto')} / ${String(grid.rowEnd ?? 'auto')}; container=${JSON.stringify(grid.container ?? {})}.`));
|
|
389
|
+
} else facts.push(factFor(target, text`Grid allocation: target is not recorded as a grid item.`));
|
|
390
|
+
return { kind: 'size', facts };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function textSection(target: GeometryElement, report: Record<string, unknown>): RawSection {
|
|
394
|
+
const facts: ExplainFact[] = [];
|
|
395
|
+
const records = (Array.isArray(report.elements) ? report.elements : [])
|
|
396
|
+
.filter((entry): entry is ArtifactElement & Record<string, unknown> => Boolean(entry && typeof entry === 'object'));
|
|
397
|
+
const matches = joinToGeometry(records, target);
|
|
398
|
+
if (!matches.length) {
|
|
399
|
+
facts.push(factFor(target, text`No text-layout record joined to this geometry record by backend node id or geometry element id.`));
|
|
400
|
+
return { kind: 'text', facts };
|
|
401
|
+
}
|
|
402
|
+
for (const record of matches) {
|
|
403
|
+
const font = record.font && typeof record.font === 'object' ? record.font as Record<string, unknown> : {};
|
|
404
|
+
facts.push(factFor(target, fact`Text metrics: chars=${Number(record.textLength ?? (typeof record.text === 'string' ? record.text.length : 0))}; lines=${Number(record.lineCount ?? (Array.isArray(record.lines) ? record.lines.length : 0))}; truncated=${String(record.truncated ?? false)}; truncation-style=${String(record.truncationStyle ?? 'none')}; scroll-width=${Number(record.scrollWidth ?? 0)}; client-width=${Number(record.clientWidth ?? 0)}.`));
|
|
405
|
+
facts.push(factFor(target, fact`Font metrics: family=${String(font.family ?? 'unknown')}; size=${String(font.size ?? 'unknown')}; weight=${String(font.weight ?? 'unknown')}; line-height=${String(font.lineHeight ?? 'unknown')}; writing-mode=${String(record.writingMode ?? 'unknown')}; direction=${String(record.direction ?? 'unknown')}; fallback-used=${String(record.fallbackUsed ?? 'unknown')}.`));
|
|
406
|
+
for (const lineRecord of Array.isArray(record.lines) ? record.lines : []) {
|
|
407
|
+
if (!lineRecord || typeof lineRecord !== 'object') continue;
|
|
408
|
+
const row = lineRecord as Record<string, unknown>;
|
|
409
|
+
const rect = row.rect && typeof row.rect === 'object' ? row.rect as Record<string, unknown> : {};
|
|
410
|
+
facts.push(factFor(target, fact`Text line ${Number(row.index ?? 0)}: rect x=${Number(rect.x ?? 0)} y=${Number(rect.y ?? 0)} w=${Number(rect.width ?? rect.w ?? 0)} h=${Number(rect.height ?? rect.h ?? 0)}; baseline=${String(row.baseline ?? 'unavailable')}; baseline-approximate=${String(row.baselineApproximate ?? false)}; wrap-after-char=${row.wrapAfterCharUnavailable ? 'unavailable' : String(row.wrapAfterChar ?? 'none')}.`));
|
|
411
|
+
}
|
|
412
|
+
const platformFonts = Array.isArray(record.platformFonts) ? record.platformFonts : [];
|
|
413
|
+
facts.push(factFor(target, fact`Platform fonts: available=${String(record.platformFontsAvailable ?? false)}; families=${platformFonts.map((entry) => entry && typeof entry === 'object' ? String((entry as Record<string, unknown>).familyName ?? 'unknown') : 'unknown').join(', ') || 'none recorded'}.`));
|
|
414
|
+
}
|
|
415
|
+
return { kind: 'text', facts };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function formSection(target: GeometryElement, report: Record<string, unknown>): RawSection {
|
|
419
|
+
const facts: ExplainFact[] = [];
|
|
420
|
+
const records = (Array.isArray(report.controls) ? report.controls : [])
|
|
421
|
+
.filter((entry): entry is ArtifactElement & Record<string, unknown> => Boolean(entry && typeof entry === 'object'));
|
|
422
|
+
const matches = joinToGeometry(records, target);
|
|
423
|
+
if (!matches.length) {
|
|
424
|
+
facts.push(factFor(target, text`No form-control record joined to this geometry record by backend node id or geometry element id.`));
|
|
425
|
+
return { kind: 'form', facts };
|
|
426
|
+
}
|
|
427
|
+
for (const control of matches) {
|
|
428
|
+
const rect = rectOf(control.rect as RectLike | undefined);
|
|
429
|
+
const dimensions = control.dimensions && typeof control.dimensions === 'object' ? control.dimensions as Record<string, unknown> : {};
|
|
430
|
+
const scroll = control.scroll && typeof control.scroll === 'object' ? control.scroll as Record<string, unknown> : {};
|
|
431
|
+
const autofill = control.autofill && typeof control.autofill === 'object' ? control.autofill as Record<string, unknown> : {};
|
|
432
|
+
facts.push(factFor(target, fact`Form control: type=${String(control.type ?? 'unknown')}; rect=${rect ? `x=${rect.x} y=${rect.y} w=${rect.w} h=${rect.h}` : 'unavailable'}; client=${Number(dimensions.clientWidth ?? 0)}×${Number(dimensions.clientHeight ?? 0)}; scroll-size=${Number(dimensions.scrollWidth ?? 0)}×${Number(dimensions.scrollHeight ?? 0)}.`));
|
|
433
|
+
const visibleSubstring = control.visibleSubstring && typeof control.visibleSubstring === 'object' ? control.visibleSubstring as Record<string, unknown> : undefined;
|
|
434
|
+
facts.push(factFor(target, fact`Form value: length=${Number(control.valueLength ?? 0)}; value=${JSON.stringify(control.value ?? '')}${control.capped ? ' (recorded value capped at the shared artifact-string limit)' : ''}; visible-substring=${visibleSubstring ? `${JSON.stringify(String(visibleSubstring.text ?? ''))} [${Number(visibleSubstring.start ?? 0)},${Number(visibleSubstring.end ?? 0)})` : 'none recorded'}.`));
|
|
435
|
+
facts.push(factFor(target, fact`Selection/caret: start=${String(control.selectionStart ?? 'none')}; end=${String(control.selectionEnd ?? 'none')}; caret=${JSON.stringify(control.caretRect ?? null)}; selection-rects=${Array.isArray(control.selectionRects) ? control.selectionRects.length : 0}; internal-scroll left=${Number(scroll.left ?? 0)} top=${Number(scroll.top ?? 0)}.`));
|
|
436
|
+
facts.push(factFor(target, fact`Autofill/native parts: autofilled=${String(autofill.isAutofilled ?? false)}; native-part-dimensions=${JSON.stringify(control.nativePartDimensions ?? {})}.`));
|
|
437
|
+
if (control.validity && typeof control.validity === 'object') {
|
|
438
|
+
const validity = control.validity as Record<string, unknown>;
|
|
439
|
+
facts.push(factFor(target, fact`Validity state: valid=${String(validity.valid ?? 'unknown')}; value-missing=${String(validity.valueMissing ?? false)}; type-mismatch=${String(validity.typeMismatch ?? false)}; pattern-mismatch=${String(validity.patternMismatch ?? false)}; custom-error=${String(validity.customError ?? false)}; message=${JSON.stringify(validity.message ?? '')}.`));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { kind: 'form', facts };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Pure read over one resolved snapshot: no browser driving and no artifact writes. */
|
|
446
|
+
export function explainSnapshot(ref: SnapRef, selector: string, detailOpts: ExplainDetailOptions = {}): ExplainSnapshotResult {
|
|
447
|
+
const geometryReport = readGeometry<{ elements?: GeometryElement[] }>(ref);
|
|
448
|
+
const stylesReport = readStyles<{ elements?: StyleElement[] }>(ref);
|
|
449
|
+
const meta = readMeta<{ settled?: boolean; settleMs?: number }>(ref);
|
|
450
|
+
const geometry = geometryReport.elements ?? [];
|
|
451
|
+
const styles = stylesReport.elements ?? [];
|
|
452
|
+
|
|
453
|
+
const selectorKind = selector.startsWith('ax:') || selector.startsWith('axid:') ? 'ax' : selector.startsWith('text:') ? 'text' : 'base';
|
|
454
|
+
let axNodes: ArtifactElement[] = [];
|
|
455
|
+
let textElements: ArtifactElement[] = [];
|
|
456
|
+
if (selectorKind === 'ax' && artifactExists(ref, 'ax.json')) axNodes = readAx<{ nodes?: ArtifactElement[] }>(ref).nodes ?? [];
|
|
457
|
+
if ((selectorKind === 'text' || detailOpts.text) && artifactExists(ref, 'text.json')) textElements = readText<{ elements?: ArtifactElement[] }>(ref).elements ?? [];
|
|
458
|
+
|
|
459
|
+
let selectable = enrichGeometry(geometry, axNodes, textElements);
|
|
460
|
+
let matches = resolveSelectorInput(selectable, selector);
|
|
461
|
+
if (!matches.length) {
|
|
462
|
+
if (!axNodes.length && artifactExists(ref, 'ax.json')) axNodes = readAx<{ nodes?: ArtifactElement[] }>(ref).nodes ?? [];
|
|
463
|
+
if (!textElements.length && artifactExists(ref, 'text.json')) textElements = readText<{ elements?: ArtifactElement[] }>(ref).elements ?? [];
|
|
464
|
+
selectable = enrichGeometry(geometry, axNodes, textElements);
|
|
465
|
+
matches = resolveSelectorInput(selectable, selector);
|
|
466
|
+
}
|
|
467
|
+
if (!matches.length) {
|
|
468
|
+
return { kind: 'missing-selector', ref, selector, meta, available: recoveryForms(selectable, selector) };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const selectedId = matches[0]!.id;
|
|
472
|
+
const target = geometry.find((element) => element.id === selectedId)!;
|
|
473
|
+
const focus = readOptional(ref, 'focus.json', readFocus<Record<string, unknown>>, { available: false, unavailableReason: 'focus.json not present', forward: [], reverse: [], candidateCount: 0 });
|
|
474
|
+
const scroll = readOptional(ref, 'scroll.json', readScroll<Record<string, unknown>>, { available: false, reason: 'scroll.json not present', containers: [] });
|
|
475
|
+
const queries = readOptional(ref, 'queries.json', readQueries<Record<string, unknown>>, { available: false, unavailableReason: 'queries.json not present', environment: {}, mediaQueries: [], containerQueries: [] });
|
|
476
|
+
const states = artifactExists(ref, 'states.json') ? readStates<Record<string, unknown>>(ref) : undefined;
|
|
477
|
+
|
|
478
|
+
const rawSections: RawSection[] = [
|
|
479
|
+
elementSection(target, matches.length),
|
|
480
|
+
cascadeSection(target, styles),
|
|
481
|
+
stackingClippingSection(target, geometry),
|
|
482
|
+
focusScrollSection(target, focus, scroll),
|
|
483
|
+
queriesStatesSection(target, queries, states, styles),
|
|
484
|
+
];
|
|
485
|
+
if (detailOpts.size) rawSections.push(sizeSection(target));
|
|
486
|
+
if (detailOpts.text) rawSections.push(textSection(target, readText<Record<string, unknown>>(ref)));
|
|
487
|
+
if (detailOpts.form) rawSections.push(formSection(target, readForms<Record<string, unknown>>(ref)));
|
|
488
|
+
|
|
489
|
+
const regions = unstableRegionsFor(ref);
|
|
490
|
+
const sections = rawSections
|
|
491
|
+
.filter((section) => section.facts.length > 0)
|
|
492
|
+
.map((section): ExplainSection => ({
|
|
493
|
+
kind: section.kind,
|
|
494
|
+
facts: annotateUnstableFacts(section.facts, regions),
|
|
495
|
+
}));
|
|
496
|
+
|
|
497
|
+
return { kind: 'explanation', ref, requestedSelector: selector, element: target, matchCount: matches.length, meta, sections };
|
|
498
|
+
}
|