@nexface/tools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/controller.d.ts +30 -0
- package/dist/browser/controller.js +783 -0
- package/dist/browser/controller.js.map +1 -0
- package/dist/browser/dom.d.ts +43 -0
- package/dist/browser/dom.js +438 -0
- package/dist/browser/dom.js.map +1 -0
- package/dist/browser/feedback.d.ts +4 -0
- package/dist/browser/feedback.js +5 -0
- package/dist/browser/feedback.js.map +1 -0
- package/dist/browser/locator.d.ts +19 -0
- package/dist/browser/locator.js +277 -0
- package/dist/browser/locator.js.map +1 -0
- package/dist/browser/prompt.d.ts +5 -0
- package/dist/browser/prompt.generated.d.ts +1 -0
- package/dist/browser/prompt.generated.js +50 -0
- package/dist/browser/prompt.generated.js.map +1 -0
- package/dist/browser/prompt.js +9 -0
- package/dist/browser/prompt.js.map +1 -0
- package/dist/browser/snapshot.d.ts +7 -0
- package/dist/browser/snapshot.js +424 -0
- package/dist/browser/snapshot.js.map +1 -0
- package/dist/browser/toolset.d.ts +35 -0
- package/dist/browser/toolset.js +383 -0
- package/dist/browser/toolset.js.map +1 -0
- package/dist/browser/types.d.ts +129 -0
- package/dist/browser/types.js +2 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/browser/worker-runner.d.ts +11 -0
- package/dist/browser/worker-runner.js +400 -0
- package/dist/browser/worker-runner.js.map +1 -0
- package/dist/definition.d.ts +16 -0
- package/dist/definition.js +72 -0
- package/dist/definition.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal-react.d.ts +12 -0
- package/dist/internal-react.js +23 -0
- package/dist/internal-react.js.map +1 -0
- package/dist/internal-types.d.ts +11 -0
- package/dist/internal-types.js +2 -0
- package/dist/internal-types.js.map +1 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.js +5 -0
- package/dist/internal.js.map +1 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +258 -0
- package/dist/react.js.map +1 -0
- package/dist/runtime.d.ts +93 -0
- package/dist/runtime.js +492 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +46 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/webmcp.d.ts +2 -0
- package/dist/webmcp.js +73 -0
- package/dist/webmcp.js.map +1 -0
- package/package.json +26 -0
- package/src/browser/controller.ts +984 -0
- package/src/browser/dom.ts +507 -0
- package/src/browser/feedback.ts +5 -0
- package/src/browser/locator.ts +383 -0
- package/src/browser/prompt.generated.ts +50 -0
- package/src/browser/prompt.ts +10 -0
- package/src/browser/snapshot.ts +542 -0
- package/src/browser/toolset.ts +522 -0
- package/src/browser/types.ts +172 -0
- package/src/browser/worker-runner.ts +468 -0
- package/src/definition.ts +115 -0
- package/src/index.ts +35 -0
- package/src/internal-react.tsx +62 -0
- package/src/internal-types.ts +12 -0
- package/src/internal.ts +36 -0
- package/src/react.tsx +397 -0
- package/src/runtime.ts +722 -0
- package/src/schema.ts +52 -0
- package/src/types.ts +123 -0
- package/src/webmcp.ts +102 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import {
|
|
2
|
+
composedChildren,
|
|
3
|
+
composedParent,
|
|
4
|
+
computeNameInfo,
|
|
5
|
+
computeRole,
|
|
6
|
+
computeState,
|
|
7
|
+
createIdentity,
|
|
8
|
+
createScrollableIdentity,
|
|
9
|
+
findNearestHeadingText,
|
|
10
|
+
getElementScrollability,
|
|
11
|
+
isActionableRole,
|
|
12
|
+
isContextRole,
|
|
13
|
+
isElementInViewport,
|
|
14
|
+
isElementRendered,
|
|
15
|
+
normalizeText,
|
|
16
|
+
walkComposedElements,
|
|
17
|
+
} from "./dom.js";
|
|
18
|
+
import type { ElementScrollability, NameSource } from "./dom.js";
|
|
19
|
+
import type {
|
|
20
|
+
PageSnapshot,
|
|
21
|
+
SemanticState,
|
|
22
|
+
SnapshotBundle,
|
|
23
|
+
SnapshotLimits,
|
|
24
|
+
SnapshotRegistry,
|
|
25
|
+
SnapshotScope,
|
|
26
|
+
} from "./types.js";
|
|
27
|
+
|
|
28
|
+
interface RenderNode {
|
|
29
|
+
element?: Element;
|
|
30
|
+
role: string;
|
|
31
|
+
name: string;
|
|
32
|
+
normalizedName: string;
|
|
33
|
+
nameSource: NameSource;
|
|
34
|
+
state: SemanticState;
|
|
35
|
+
actionable: boolean;
|
|
36
|
+
scrollable?: ElementScrollability;
|
|
37
|
+
children: RenderNode[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface WalkContext {
|
|
41
|
+
scope: SnapshotScope;
|
|
42
|
+
activeModal?: Element;
|
|
43
|
+
allowedPortalRoots: Set<Element>;
|
|
44
|
+
styles: WeakMap<Element, CSSStyleDeclaration>;
|
|
45
|
+
rects: WeakMap<Element, DOMRect>;
|
|
46
|
+
visitedNodes: number;
|
|
47
|
+
maxVisitedNodes: number;
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
execution?: SnapshotExecutionContext;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface RenderContext {
|
|
53
|
+
refs: SnapshotRegistry["refs"];
|
|
54
|
+
maxChars: number;
|
|
55
|
+
usedChars: number;
|
|
56
|
+
lines: string[];
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
renderedNodes: number;
|
|
59
|
+
execution?: SnapshotExecutionContext;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SnapshotExecutionContext {
|
|
63
|
+
deadline: number;
|
|
64
|
+
signal: AbortSignal;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const DEFAULT_LIMITS: SnapshotLimits = {
|
|
68
|
+
viewportMaxChars: 18_000,
|
|
69
|
+
viewportMaxVisitedNodes: 12_000,
|
|
70
|
+
fullMaxChars: 50_000,
|
|
71
|
+
fullMaxVisitedNodes: 30_000,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const STRUCTURAL_ROLES = new Set([
|
|
75
|
+
"alert",
|
|
76
|
+
"alertdialog",
|
|
77
|
+
"dialog",
|
|
78
|
+
"form",
|
|
79
|
+
"heading",
|
|
80
|
+
"iframe",
|
|
81
|
+
"list",
|
|
82
|
+
"listbox",
|
|
83
|
+
"menu",
|
|
84
|
+
"navigation",
|
|
85
|
+
"progressbar",
|
|
86
|
+
"region",
|
|
87
|
+
"row",
|
|
88
|
+
"rowgroup",
|
|
89
|
+
"status",
|
|
90
|
+
"table",
|
|
91
|
+
"tooltip",
|
|
92
|
+
"tree",
|
|
93
|
+
"listitem",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const PORTAL_ROLES = new Set([
|
|
97
|
+
"alert",
|
|
98
|
+
"listbox",
|
|
99
|
+
"menu",
|
|
100
|
+
"progressbar",
|
|
101
|
+
"status",
|
|
102
|
+
"tooltip",
|
|
103
|
+
"tree",
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const REDUNDANT_WRAPPER_ROLES = new Set(["group", "listitem"]);
|
|
107
|
+
|
|
108
|
+
function checkExecution(context?: SnapshotExecutionContext): void {
|
|
109
|
+
if (!context) return;
|
|
110
|
+
context.signal.throwIfAborted();
|
|
111
|
+
if (performance.now() >= context.deadline) {
|
|
112
|
+
throw new DOMException(
|
|
113
|
+
"Snapshot collection exceeded the executeCode deadline.",
|
|
114
|
+
"TimeoutError",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function nextAnimationFrame(
|
|
120
|
+
context?: SnapshotExecutionContext,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
checkExecution(context);
|
|
123
|
+
if (!context) {
|
|
124
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
125
|
+
}
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
let frame = 0;
|
|
129
|
+
const remaining = Math.max(0, context.deadline - performance.now());
|
|
130
|
+
const cleanup = (): void => {
|
|
131
|
+
window.clearTimeout(timer);
|
|
132
|
+
context.signal.removeEventListener("abort", onAbort);
|
|
133
|
+
};
|
|
134
|
+
const finish = (error?: Error): void => {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
cleanup();
|
|
138
|
+
if (error) reject(error);
|
|
139
|
+
else resolve();
|
|
140
|
+
};
|
|
141
|
+
const onAbort = (): void => finish(
|
|
142
|
+
context.signal.reason instanceof Error
|
|
143
|
+
? context.signal.reason
|
|
144
|
+
: new DOMException("Execution cancelled.", "AbortError"),
|
|
145
|
+
);
|
|
146
|
+
const timer = window.setTimeout(() => finish(new DOMException(
|
|
147
|
+
"Snapshot collection exceeded the executeCode deadline.",
|
|
148
|
+
"TimeoutError",
|
|
149
|
+
)), remaining);
|
|
150
|
+
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
151
|
+
frame = requestAnimationFrame(() => finish());
|
|
152
|
+
if (settled && typeof cancelAnimationFrame === "function") {
|
|
153
|
+
cancelAnimationFrame(frame);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function cachedStyle(
|
|
159
|
+
element: Element,
|
|
160
|
+
cache: WeakMap<Element, CSSStyleDeclaration>,
|
|
161
|
+
): CSSStyleDeclaration {
|
|
162
|
+
const existing = cache.get(element);
|
|
163
|
+
if (existing) return existing;
|
|
164
|
+
const style = getComputedStyle(element);
|
|
165
|
+
cache.set(element, style);
|
|
166
|
+
return style;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function cachedRect(
|
|
170
|
+
element: Element,
|
|
171
|
+
cache: WeakMap<Element, DOMRect>,
|
|
172
|
+
): DOMRect {
|
|
173
|
+
const existing = cache.get(element);
|
|
174
|
+
if (existing) return existing;
|
|
175
|
+
const rect = element.getBoundingClientRect();
|
|
176
|
+
cache.set(element, rect);
|
|
177
|
+
return rect;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isTextInScope(node: Text, context: WalkContext): boolean {
|
|
181
|
+
if (!normalizeText(node.data)) return false;
|
|
182
|
+
const parent = node.parentElement;
|
|
183
|
+
if (!parent) return false;
|
|
184
|
+
const style = cachedStyle(parent, context.styles);
|
|
185
|
+
if (!isElementRendered(parent, style)) return false;
|
|
186
|
+
if (context.scope === "full") return true;
|
|
187
|
+
const parentRect = cachedRect(parent, context.rects);
|
|
188
|
+
if (!isElementInViewport(parent, parentRect)) return false;
|
|
189
|
+
const range = document.createRange();
|
|
190
|
+
range.selectNodeContents(node);
|
|
191
|
+
const rect = range.getBoundingClientRect();
|
|
192
|
+
range.detach();
|
|
193
|
+
return (
|
|
194
|
+
rect.width > 0 &&
|
|
195
|
+
rect.height > 0 &&
|
|
196
|
+
rect.right > 0 &&
|
|
197
|
+
rect.bottom > 0 &&
|
|
198
|
+
rect.left < window.innerWidth &&
|
|
199
|
+
rect.top < window.innerHeight
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isElementInScope(element: Element, context: WalkContext): boolean {
|
|
204
|
+
const style = cachedStyle(element, context.styles);
|
|
205
|
+
if (!isElementRendered(element, style)) return false;
|
|
206
|
+
if (context.scope === "full") {
|
|
207
|
+
const rect = cachedRect(element, context.rects);
|
|
208
|
+
return rect.width > 0 && rect.height > 0;
|
|
209
|
+
}
|
|
210
|
+
return isElementInViewport(element, cachedRect(element, context.rects));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isDescendantOrSelf(element: Element, ancestor: Element): boolean {
|
|
214
|
+
if (element === ancestor) return true;
|
|
215
|
+
let current: Element | null = element;
|
|
216
|
+
while (current) {
|
|
217
|
+
if (current === ancestor) return true;
|
|
218
|
+
current = composedParent(current);
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isAllowedByModal(element: Element, context: WalkContext): boolean {
|
|
224
|
+
if (!context.activeModal) return true;
|
|
225
|
+
if (isDescendantOrSelf(element, context.activeModal)) return true;
|
|
226
|
+
for (const root of context.allowedPortalRoots) {
|
|
227
|
+
if (isDescendantOrSelf(element, root)) return true;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeNodeChildren(node: RenderNode): void {
|
|
233
|
+
const merged: RenderNode[] = [];
|
|
234
|
+
for (const child of node.children) {
|
|
235
|
+
const previous = merged.at(-1);
|
|
236
|
+
if (
|
|
237
|
+
child.role === "text" &&
|
|
238
|
+
previous?.role === "text" &&
|
|
239
|
+
!child.element &&
|
|
240
|
+
!previous.element
|
|
241
|
+
) {
|
|
242
|
+
previous.name = normalizeText(`${previous.name} ${child.name}`);
|
|
243
|
+
previous.normalizedName = previous.name;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
merged.push(child);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const directText = normalizeText(
|
|
250
|
+
merged
|
|
251
|
+
.filter((child) => child.role === "text")
|
|
252
|
+
.map((child) => child.name)
|
|
253
|
+
.join(" "),
|
|
254
|
+
);
|
|
255
|
+
node.children =
|
|
256
|
+
node.normalizedName &&
|
|
257
|
+
directText &&
|
|
258
|
+
node.normalizedName === directText
|
|
259
|
+
? merged.filter((child) => child.role !== "text")
|
|
260
|
+
: merged;
|
|
261
|
+
|
|
262
|
+
const onlyChild = node.children.length === 1 ? node.children[0] : undefined;
|
|
263
|
+
if (
|
|
264
|
+
REDUNDANT_WRAPPER_ROLES.has(node.role) &&
|
|
265
|
+
node.nameSource === "content" &&
|
|
266
|
+
!node.actionable &&
|
|
267
|
+
Object.keys(node.state).length === 0 &&
|
|
268
|
+
onlyChild &&
|
|
269
|
+
onlyChild.role !== "text" &&
|
|
270
|
+
node.normalizedName === onlyChild.normalizedName
|
|
271
|
+
) {
|
|
272
|
+
node.name = "";
|
|
273
|
+
node.normalizedName = "";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function visitNode(node: Node, context: WalkContext): RenderNode[] {
|
|
278
|
+
context.visitedNodes += 1;
|
|
279
|
+
if (context.visitedNodes % 128 === 0) checkExecution(context.execution);
|
|
280
|
+
if (context.visitedNodes > context.maxVisitedNodes) {
|
|
281
|
+
context.truncated = true;
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (node instanceof Text) {
|
|
286
|
+
if (!isTextInScope(node, context)) return [];
|
|
287
|
+
const parent = node.parentElement;
|
|
288
|
+
if (parent && !isAllowedByModal(parent, context)) return [];
|
|
289
|
+
return [{
|
|
290
|
+
role: "text",
|
|
291
|
+
name: normalizeText(node.data),
|
|
292
|
+
normalizedName: normalizeText(node.data),
|
|
293
|
+
nameSource: "content",
|
|
294
|
+
state: {},
|
|
295
|
+
actionable: false,
|
|
296
|
+
children: [],
|
|
297
|
+
}];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!(node instanceof Element)) return [];
|
|
301
|
+
const tag = node.tagName.toLowerCase();
|
|
302
|
+
if (["script", "style", "template", "meta", "link", "noscript"].includes(tag)) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const style = cachedStyle(node, context.styles);
|
|
307
|
+
if (!isElementRendered(node, style)) return [];
|
|
308
|
+
|
|
309
|
+
const children = composedChildren(node).flatMap((child) =>
|
|
310
|
+
visitNode(child, context),
|
|
311
|
+
);
|
|
312
|
+
const inScope = isElementInScope(node, context);
|
|
313
|
+
const allowed = isAllowedByModal(node, context);
|
|
314
|
+
const role = computeRole(node);
|
|
315
|
+
const state = computeState(node);
|
|
316
|
+
const inferredActionable = isActionableRole(role);
|
|
317
|
+
const focusOnlyActionable =
|
|
318
|
+
role === "actionable" &&
|
|
319
|
+
node instanceof HTMLElement &&
|
|
320
|
+
node.tabIndex >= 0 &&
|
|
321
|
+
node.onclick === null &&
|
|
322
|
+
!node.hasAttribute("onclick") &&
|
|
323
|
+
!node.isContentEditable;
|
|
324
|
+
const scrollable = inScope && (!inferredActionable || focusOnlyActionable)
|
|
325
|
+
? getElementScrollability(node, style)
|
|
326
|
+
: undefined;
|
|
327
|
+
const actionable = inferredActionable && !scrollable;
|
|
328
|
+
const hasState = Object.keys(state).length > 0;
|
|
329
|
+
const keep =
|
|
330
|
+
allowed &&
|
|
331
|
+
((inScope && (actionable || scrollable || STRUCTURAL_ROLES.has(role) || hasState)) ||
|
|
332
|
+
(children.length > 0 && (isContextRole(role) || role === "table" || role === "list")));
|
|
333
|
+
|
|
334
|
+
if (!keep) return children;
|
|
335
|
+
|
|
336
|
+
const computedName = computeNameInfo(node);
|
|
337
|
+
const scrollableName =
|
|
338
|
+
scrollable && computedName.source === "content"
|
|
339
|
+
? findNearestHeadingText(node)
|
|
340
|
+
: computedName.value || (scrollable ? findNearestHeadingText(node) : "");
|
|
341
|
+
const renderNode: RenderNode = {
|
|
342
|
+
element: node,
|
|
343
|
+
role: scrollable && (!role || role === "actionable")
|
|
344
|
+
? "scrollable"
|
|
345
|
+
: role || "group",
|
|
346
|
+
name: scrollableName,
|
|
347
|
+
normalizedName: normalizeText(scrollableName),
|
|
348
|
+
nameSource: computedName.source,
|
|
349
|
+
state,
|
|
350
|
+
actionable,
|
|
351
|
+
...(scrollable ? { scrollable } : {}),
|
|
352
|
+
children,
|
|
353
|
+
};
|
|
354
|
+
normalizeNodeChildren(renderNode);
|
|
355
|
+
return [renderNode];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function isVisibleModal(element: Element): boolean {
|
|
359
|
+
const role = computeRole(element);
|
|
360
|
+
const ariaModal =
|
|
361
|
+
role === "dialog" && element.getAttribute("aria-modal") === "true";
|
|
362
|
+
const alertDialog = role === "alertdialog";
|
|
363
|
+
let nativeModal = false;
|
|
364
|
+
if (element instanceof HTMLDialogElement && element.open) {
|
|
365
|
+
try {
|
|
366
|
+
nativeModal = element.matches(":modal");
|
|
367
|
+
} catch {
|
|
368
|
+
nativeModal = element.open;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return (ariaModal || alertDialog || nativeModal) && isElementInViewport(element);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function findActiveModal(
|
|
375
|
+
execution?: SnapshotExecutionContext,
|
|
376
|
+
): Element | undefined {
|
|
377
|
+
let activeModal: Element | undefined;
|
|
378
|
+
let visited = 0;
|
|
379
|
+
for (const element of walkComposedElements()) {
|
|
380
|
+
visited += 1;
|
|
381
|
+
if (visited % 128 === 0) checkExecution(execution);
|
|
382
|
+
if (isVisibleModal(element)) activeModal = element;
|
|
383
|
+
}
|
|
384
|
+
return activeModal;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function findPortalRoots(
|
|
388
|
+
activeModal: Element,
|
|
389
|
+
execution?: SnapshotExecutionContext,
|
|
390
|
+
): Set<Element> {
|
|
391
|
+
const roots = new Set<Element>();
|
|
392
|
+
let visited = 0;
|
|
393
|
+
for (const element of walkComposedElements()) {
|
|
394
|
+
visited += 1;
|
|
395
|
+
if (visited % 128 === 0) checkExecution(execution);
|
|
396
|
+
if (isDescendantOrSelf(element, activeModal)) continue;
|
|
397
|
+
const role = computeRole(element);
|
|
398
|
+
if (
|
|
399
|
+
(PORTAL_ROLES.has(role) || element.hasAttribute("aria-live")) &&
|
|
400
|
+
isElementInViewport(element)
|
|
401
|
+
) {
|
|
402
|
+
roots.add(element);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return roots;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function serializeState(state: SemanticState): string {
|
|
409
|
+
const entries = Object.entries(state);
|
|
410
|
+
if (entries.length === 0) return "";
|
|
411
|
+
return ` [${entries
|
|
412
|
+
.map(([key, value]) =>
|
|
413
|
+
typeof value === "boolean"
|
|
414
|
+
? value
|
|
415
|
+
? key
|
|
416
|
+
: `${key}=false`
|
|
417
|
+
: `${key}=${JSON.stringify(value)}`,
|
|
418
|
+
)
|
|
419
|
+
.join(" ")}]`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function serializeScrollability(value: ElementScrollability | undefined): string {
|
|
423
|
+
if (!value) return "";
|
|
424
|
+
const axis = value.x && value.y ? "xy" : value.y ? "y" : "x";
|
|
425
|
+
const metrics = value.y
|
|
426
|
+
? `scrollTop=${value.scrollTop} scrollHeight=${value.scrollHeight} clientHeight=${value.clientHeight}`
|
|
427
|
+
: `scrollLeft=${value.scrollLeft} scrollWidth=${value.scrollWidth} clientWidth=${value.clientWidth}`;
|
|
428
|
+
return ` [axis=${axis} ${metrics}]`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderTree(nodes: RenderNode[], context: RenderContext): void {
|
|
432
|
+
const renderNode = (node: RenderNode, depth: number): void => {
|
|
433
|
+
if (context.truncated) return;
|
|
434
|
+
context.renderedNodes += 1;
|
|
435
|
+
if (context.renderedNodes % 128 === 0) {
|
|
436
|
+
checkExecution(context.execution);
|
|
437
|
+
}
|
|
438
|
+
const indent = " ".repeat(Math.min(depth, 6));
|
|
439
|
+
const name = node.name ? ` ${JSON.stringify(normalizeText(node.name))}` : "";
|
|
440
|
+
const state = serializeState(node.state);
|
|
441
|
+
const scrollability = serializeScrollability(node.scrollable);
|
|
442
|
+
const nextRef = node.actionable || node.scrollable
|
|
443
|
+
? `@${context.refs.size + 1}`
|
|
444
|
+
: undefined;
|
|
445
|
+
const refText = nextRef ? ` [ref=${nextRef}]` : "";
|
|
446
|
+
const prefix = node.role === "text" ? `${indent}- text:` : `${indent}- ${node.role}`;
|
|
447
|
+
const line =
|
|
448
|
+
node.role === "text"
|
|
449
|
+
? `${prefix} ${JSON.stringify(normalizeText(node.name))}`
|
|
450
|
+
: `${prefix}${name}${state}${scrollability}${refText}`;
|
|
451
|
+
|
|
452
|
+
const added = line.length + (context.lines.length > 0 ? 1 : 0);
|
|
453
|
+
if (context.usedChars + added > context.maxChars) {
|
|
454
|
+
context.truncated = true;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
context.lines.push(line);
|
|
459
|
+
context.usedChars += added;
|
|
460
|
+
if (nextRef && node.element) {
|
|
461
|
+
context.refs.set(nextRef, {
|
|
462
|
+
element: node.element,
|
|
463
|
+
identity: node.scrollable
|
|
464
|
+
? createScrollableIdentity(node.element)
|
|
465
|
+
: createIdentity(node.element),
|
|
466
|
+
kind: node.scrollable ? "scrollable" : "element",
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
for (const child of node.children) renderNode(child, depth + 1);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
for (const node of nodes) renderNode(node, 0);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function createSnapshot(
|
|
477
|
+
scope: SnapshotScope = "viewport",
|
|
478
|
+
limits: Partial<SnapshotLimits> = {},
|
|
479
|
+
execution?: SnapshotExecutionContext,
|
|
480
|
+
): Promise<SnapshotBundle> {
|
|
481
|
+
await nextAnimationFrame(execution);
|
|
482
|
+
checkExecution(execution);
|
|
483
|
+
const resolvedLimits = { ...DEFAULT_LIMITS, ...limits };
|
|
484
|
+
const activeModal = findActiveModal(execution);
|
|
485
|
+
const walkContext: WalkContext = {
|
|
486
|
+
scope,
|
|
487
|
+
...(activeModal ? { activeModal } : {}),
|
|
488
|
+
allowedPortalRoots: activeModal
|
|
489
|
+
? findPortalRoots(activeModal, execution)
|
|
490
|
+
: new Set(),
|
|
491
|
+
styles: new WeakMap(),
|
|
492
|
+
rects: new WeakMap(),
|
|
493
|
+
visitedNodes: 0,
|
|
494
|
+
maxVisitedNodes:
|
|
495
|
+
scope === "viewport"
|
|
496
|
+
? resolvedLimits.viewportMaxVisitedNodes
|
|
497
|
+
: resolvedLimits.fullMaxVisitedNodes,
|
|
498
|
+
truncated: false,
|
|
499
|
+
...(execution ? { execution } : {}),
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const tree = composedChildren(document.body).flatMap((node) =>
|
|
503
|
+
visitNode(node, walkContext),
|
|
504
|
+
);
|
|
505
|
+
const snapshotId = `s-${crypto.randomUUID()}`;
|
|
506
|
+
const registry: SnapshotRegistry = {
|
|
507
|
+
snapshotId,
|
|
508
|
+
refs: new Map(),
|
|
509
|
+
};
|
|
510
|
+
const renderContext: RenderContext = {
|
|
511
|
+
refs: registry.refs,
|
|
512
|
+
maxChars:
|
|
513
|
+
scope === "viewport"
|
|
514
|
+
? resolvedLimits.viewportMaxChars
|
|
515
|
+
: resolvedLimits.fullMaxChars,
|
|
516
|
+
usedChars: 0,
|
|
517
|
+
lines: [],
|
|
518
|
+
truncated: false,
|
|
519
|
+
renderedNodes: 0,
|
|
520
|
+
...(execution ? { execution } : {}),
|
|
521
|
+
};
|
|
522
|
+
renderTree(tree, renderContext);
|
|
523
|
+
checkExecution(execution);
|
|
524
|
+
|
|
525
|
+
const snapshot: PageSnapshot = {
|
|
526
|
+
snapshotId,
|
|
527
|
+
scope,
|
|
528
|
+
url: location.href,
|
|
529
|
+
title: document.title,
|
|
530
|
+
content: renderContext.lines.join("\n"),
|
|
531
|
+
truncated: walkContext.truncated || renderContext.truncated,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return { snapshot, registry };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export async function getSnapshot(
|
|
538
|
+
scope: SnapshotScope = "viewport",
|
|
539
|
+
limits: Partial<SnapshotLimits> = {},
|
|
540
|
+
): Promise<PageSnapshot> {
|
|
541
|
+
return (await createSnapshot(scope, limits)).snapshot;
|
|
542
|
+
}
|