main
js 1,343 lines 45.4 KB
Raw
1 (() => {
2 const DOM_HELPER_CHANNEL = "a0.browser.dom_helper";
3 const DOM_HELPER_KEY = "__spaceBrowserDomHelper__";
4 const VERSION = "1";
5 const REQUEST_TIMEOUT_MS = 800;
6
7 if (globalThis[DOM_HELPER_KEY]?.version === VERSION) {
8 return;
9 }
10
11 const INTERACTIVE_ROLES = new Set([
12 "button",
13 "checkbox",
14 "combobox",
15 "link",
16 "menuitem",
17 "menuitemcheckbox",
18 "menuitemradio",
19 "option",
20 "radio",
21 "searchbox",
22 "slider",
23 "spinbutton",
24 "switch",
25 "tab",
26 "textbox"
27 ]);
28 const STRUCTURAL_ROLES = new Set([
29 "alertdialog",
30 "article",
31 "banner",
32 "complementary",
33 "contentinfo",
34 "dialog",
35 "document",
36 "form",
37 "group",
38 "main",
39 "navigation",
40 "none",
41 "presentation",
42 "region"
43 ]);
44 const INTERACTIVE_EVENT_NAMES = new Set([
45 "auxclick",
46 "change",
47 "click",
48 "contextmenu",
49 "dblclick",
50 "input",
51 "keydown",
52 "keypress",
53 "keyup",
54 "mousedown",
55 "mouseup",
56 "pointerdown",
57 "pointerup",
58 "submit",
59 "touchend",
60 "touchstart"
61 ]);
62 const INTERACTIVE_EVENT_PROPERTIES = [...INTERACTIVE_EVENT_NAMES]
63 .map((eventName) => `on${eventName}`);
64 const SKIP_TAGS = new Set([
65 "HEAD",
66 "LINK",
67 "META",
68 "NOSCRIPT",
69 "SCRIPT",
70 "STYLE",
71 "TEMPLATE"
72 ]);
73 const VOID_TAGS = new Set([
74 "area",
75 "base",
76 "br",
77 "col",
78 "embed",
79 "hr",
80 "img",
81 "input",
82 "link",
83 "meta",
84 "param",
85 "source",
86 "track",
87 "wbr"
88 ]);
89
90 const childFramesById = new Map();
91 const elementsByNodeId = new Map();
92 const nodeIdsByElement = new WeakMap();
93 const pendingRequests = new Map();
94 const helperFrameId = typeof globalThis.crypto?.randomUUID === "function"
95 ? globalThis.crypto.randomUUID()
96 : `a0-browser-frame-${Date.now()}-${Math.random().toString(16).slice(2)}`;
97 let nextNodeId = 1;
98 let nextRequestId = 1;
99
100 function patchOpenShadowDom() {
101 const original = globalThis.Element?.prototype?.attachShadow;
102 if (!original || original.__a0BrowserDomHelperOpenShadowPatch) {
103 return;
104 }
105 const patched = function attachShadow(options) {
106 return original.call(this, { ...(options || {}), mode: "open" });
107 };
108 patched.__a0BrowserDomHelperOpenShadowPatch = true;
109 globalThis.Element.prototype.attachShadow = patched;
110 }
111
112 patchOpenShadowDom();
113
114 function createNamedError(name, message, details = {}) {
115 const error = new Error(message);
116 error.name = name;
117 Object.assign(error, details);
118 return error;
119 }
120
121 function normalizeText(value) {
122 return String(value ?? "").replace(/\s+/gu, " ").trim();
123 }
124
125 function normalizeAttributeText(value) {
126 return normalizeText(value).slice(0, 160);
127 }
128
129 function truncateText(value, maxLength = 120) {
130 const normalizedValue = normalizeText(value);
131 if (normalizedValue.length <= maxLength) {
132 return normalizedValue;
133 }
134 return `${normalizedValue.slice(0, Math.max(0, maxLength - 1)).trimEnd()}...`;
135 }
136
137 function escapeHtmlText(value) {
138 return String(value ?? "")
139 .replace(/&/gu, "&amp;")
140 .replace(/</gu, "&lt;")
141 .replace(/>/gu, "&gt;");
142 }
143
144 function escapeHtmlAttribute(value) {
145 return escapeHtmlText(value)
146 .replace(/"/gu, "&quot;")
147 .replace(/'/gu, "&#39;");
148 }
149
150 function isElementNode(value) {
151 return Boolean(value && value.nodeType === 1);
152 }
153
154 function isTextNode(value) {
155 return Boolean(value && value.nodeType === 3);
156 }
157
158 function getTagName(element) {
159 return String(element?.tagName || "").toUpperCase();
160 }
161
162 function getAttributeNamesSafe(element) {
163 try {
164 if (typeof element?.getAttributeNames === "function") {
165 return element.getAttributeNames();
166 }
167 return [...(element?.attributes || [])]
168 .map((attribute) => String(attribute?.name || "").trim())
169 .filter(Boolean);
170 } catch {
171 return [];
172 }
173 }
174
175 function getComputedStyleSafe(element) {
176 try {
177 return globalThis.getComputedStyle?.(element) || null;
178 } catch {
179 return null;
180 }
181 }
182
183 function isStyleDeclarationHidden(styleValue) {
184 const normalizedStyleValue = String(styleValue || "")
185 .toLowerCase()
186 .replace(/\s+/gu, "");
187 if (!normalizedStyleValue) {
188 return false;
189 }
190 return /(?:^|;)display:none(?:;|$)/u.test(normalizedStyleValue)
191 || /(?:^|;)visibility:hidden(?:;|$)/u.test(normalizedStyleValue)
192 || /(?:^|;)visibility:collapse(?:;|$)/u.test(normalizedStyleValue)
193 || /(?:^|;)content-visibility:hidden(?:;|$)/u.test(normalizedStyleValue)
194 || /(?:^|;)opacity:0(?:\.0+)?(?:;|$)/u.test(normalizedStyleValue);
195 }
196
197 function isComputedStyleHidden(computedStyle) {
198 if (!computedStyle) {
199 return false;
200 }
201 const display = normalizeText(computedStyle.display).toLowerCase();
202 const visibility = normalizeText(computedStyle.visibility).toLowerCase();
203 const contentVisibility = normalizeText(computedStyle.contentVisibility).toLowerCase();
204 const opacity = Number(computedStyle.opacity || 1);
205 return display === "none"
206 || visibility === "hidden"
207 || visibility === "collapse"
208 || contentVisibility === "hidden"
209 || opacity <= 0;
210 }
211
212 function isEffectivelyHiddenByAncestor(element) {
213 let current = element;
214 while (isElementNode(current)) {
215 if (current.hidden || current.getAttribute?.("aria-hidden") === "true") {
216 return true;
217 }
218 if (isStyleDeclarationHidden(current.getAttribute?.("style"))) {
219 return true;
220 }
221 if (isComputedStyleHidden(getComputedStyleSafe(current))) {
222 return true;
223 }
224 current = current.parentElement;
225 }
226 return false;
227 }
228
229 function isHiddenElement(element) {
230 if (!isElementNode(element)) {
231 return true;
232 }
233 const tagName = getTagName(element);
234 if (SKIP_TAGS.has(tagName)) {
235 return true;
236 }
237 if (element.hidden || element.getAttribute?.("aria-hidden") === "true") {
238 return true;
239 }
240 if (tagName === "INPUT" && String(element.getAttribute?.("type") || "").toLowerCase() === "hidden") {
241 return true;
242 }
243 if (isStyleDeclarationHidden(element.getAttribute?.("style"))) {
244 return true;
245 }
246 if (isComputedStyleHidden(getComputedStyleSafe(element))) {
247 return true;
248 }
249 return isEffectivelyHiddenByAncestor(element.parentElement);
250 }
251
252 function normalizeInteractiveEventName(value) {
253 return String(value || "")
254 .trim()
255 .toLowerCase()
256 .split(/[.:]/u, 1)[0];
257 }
258
259 function isGlobalOrDelegatedEventBinding(value) {
260 const parts = String(value || "")
261 .trim()
262 .toLowerCase()
263 .split(/[.:]/u)
264 .map((part) => part.trim())
265 .filter(Boolean);
266 return parts.includes("window")
267 || parts.includes("document")
268 || parts.includes("outside")
269 || parts.includes("away");
270 }
271
272 function isInteractiveEventName(value) {
273 return INTERACTIVE_EVENT_NAMES.has(normalizeInteractiveEventName(value));
274 }
275
276 function isInteractiveEventAttributeName(attributeName) {
277 const normalizedName = String(attributeName || "").trim().toLowerCase();
278 if (!normalizedName) {
279 return false;
280 }
281 if (normalizedName.startsWith("@")) {
282 return !isGlobalOrDelegatedEventBinding(normalizedName.slice(1))
283 && isInteractiveEventName(normalizedName.slice(1));
284 }
285 if (normalizedName.startsWith("x-on:") || normalizedName.startsWith("v-on:")) {
286 return !isGlobalOrDelegatedEventBinding(normalizedName.slice(5))
287 && isInteractiveEventName(normalizedName.slice(5));
288 }
289 if (normalizedName.startsWith("ng-")) {
290 return isInteractiveEventName(normalizedName.slice(3));
291 }
292 if (normalizedName.startsWith("on") && normalizedName.length > 2) {
293 return isInteractiveEventName(normalizedName.slice(2));
294 }
295 return false;
296 }
297
298 function hasInteractiveEventHandlerAttribute(element) {
299 return getAttributeNamesSafe(element).some((attributeName) => {
300 return isInteractiveEventAttributeName(attributeName);
301 });
302 }
303
304 function hasInteractiveEventHandlerProperty(element) {
305 return INTERACTIVE_EVENT_PROPERTIES.some((propertyName) => {
306 return typeof element?.[propertyName] === "function";
307 });
308 }
309
310 function hasInteractiveEventHandler(element) {
311 return hasInteractiveEventHandlerAttribute(element) || hasInteractiveEventHandlerProperty(element);
312 }
313
314 function isActionableElement(element) {
315 if (!isElementNode(element) || isHiddenElement(element)) {
316 return false;
317 }
318 const tagName = getTagName(element);
319 if (tagName === "A" && element.hasAttribute?.("href")) {
320 return true;
321 }
322 if (tagName === "IMG") {
323 return true;
324 }
325 if (["BUTTON", "INPUT", "SELECT", "TEXTAREA", "SUMMARY"].includes(tagName)) {
326 return true;
327 }
328 if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
329 return true;
330 }
331 const role = String(element.getAttribute?.("role") || "").trim().toLowerCase();
332 if (INTERACTIVE_ROLES.has(role)) {
333 return true;
334 }
335 if (STRUCTURAL_ROLES.has(role)) {
336 return false;
337 }
338 if (hasInteractiveEventHandlerAttribute(element)) {
339 return true;
340 }
341 return hasInteractiveEventHandlerProperty(element) && Boolean(normalizeText(element.textContent || ""));
342 }
343
344 function normalizeFrameChain(frameChain) {
345 const rawFrameChain = Array.isArray(frameChain)
346 ? frameChain
347 : typeof frameChain === "string"
348 ? frameChain.split(">")
349 : [];
350 return rawFrameChain
351 .map((entry) => String(entry || "").trim())
352 .filter(Boolean);
353 }
354
355 function encodeFrameChain(frameChain) {
356 return normalizeFrameChain(frameChain).join(">");
357 }
358
359 function ensureNodeId(element) {
360 if (nodeIdsByElement.has(element)) {
361 return nodeIdsByElement.get(element);
362 }
363 const nodeId = String(nextNodeId++);
364 nodeIdsByElement.set(element, nodeId);
365 elementsByNodeId.set(nodeId, element);
366 return nodeId;
367 }
368
369 function normalizeSnapshotMode(value) {
370 return String(value || "").trim().toLowerCase() || "dom";
371 }
372
373 function isContentSnapshotMode(payload = {}) {
374 return normalizeSnapshotMode(payload?.snapshotMode) === "content";
375 }
376
377 function normalizeSelectorList(payload = {}) {
378 const rawSelectors = typeof payload === "string"
379 ? [payload]
380 : Array.isArray(payload?.selectors)
381 ? payload.selectors
382 : typeof payload?.selectors === "string"
383 ? [payload.selectors]
384 : Array.isArray(payload?.selector)
385 ? payload.selector
386 : typeof payload?.selector === "string"
387 ? [payload.selector]
388 : [];
389 return rawSelectors.map((selector) => String(selector || "").trim()).filter(Boolean);
390 }
391
392 function getReferenceValueMetadata(element) {
393 const tagName = getTagName(element);
394 if (tagName === "INPUT") {
395 const inputType = String(element.getAttribute?.("type") || element.type || "text").toLowerCase();
396 if (inputType === "password") {
397 return "";
398 }
399 if (["checkbox", "radio"].includes(inputType)) {
400 return element.checked ? "checked" : "unchecked";
401 }
402 return truncateText(element.value || element.getAttribute?.("value") || "", 96);
403 }
404 if (tagName === "TEXTAREA") {
405 return truncateText(element.value || "", 96);
406 }
407 if (tagName === "SELECT") {
408 return [...(element.selectedOptions || [])]
409 .map((option) => truncateText(option.textContent || option.label || option.value || "", 48))
410 .filter(Boolean)
411 .join(" | ");
412 }
413 if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
414 return truncateText(element.textContent || "", 96);
415 }
416 return "";
417 }
418
419 function collectElementStateMetadata(element) {
420 if (!isElementNode(element)) {
421 return {
422 descriptorTags: [],
423 semanticTags: [],
424 stateTags: []
425 };
426 }
427 const tagName = getTagName(element);
428 const computedStyle = getComputedStyleSafe(element);
429 const ariaDisabled = String(element.getAttribute?.("aria-disabled") || "").trim().toLowerCase() === "true";
430 const ariaChecked = String(element.getAttribute?.("aria-checked") || "").trim().toLowerCase() === "true";
431 const ariaExpanded = String(element.getAttribute?.("aria-expanded") || "").trim().toLowerCase() === "true";
432 const ariaInvalid = String(element.getAttribute?.("aria-invalid") || "").trim().toLowerCase() === "true";
433 const ariaPressed = String(element.getAttribute?.("aria-pressed") || "").trim().toLowerCase() === "true";
434 const ariaReadonly = String(element.getAttribute?.("aria-readonly") || "").trim().toLowerCase() === "true";
435 const ariaRequired = String(element.getAttribute?.("aria-required") || "").trim().toLowerCase() === "true";
436 const ariaSelected = String(element.getAttribute?.("aria-selected") || "").trim().toLowerCase() === "true";
437 const disabled = Boolean(element.disabled || ariaDisabled || element.closest?.("[inert]"));
438 const checked = Boolean(element.checked || ariaChecked);
439 const selected = tagName === "OPTION" ? Boolean(element.selected) : ariaSelected;
440 const invalid = Boolean(ariaInvalid || element.matches?.(":invalid"));
441 const readonly = Boolean(element.readOnly || ariaReadonly);
442 const required = Boolean(element.required || ariaRequired);
443 const blocked = !disabled && normalizeText(computedStyle?.pointerEvents || "").toLowerCase() === "none";
444 const stateTags = [
445 disabled ? "disabled" : "",
446 blocked ? "blocked" : "",
447 checked ? "checked" : "",
448 selected && tagName !== "SELECT" ? "selected" : "",
449 invalid ? "invalid" : "",
450 ariaExpanded ? "expanded" : "",
451 ariaPressed ? "pressed" : ""
452 ].filter(Boolean);
453 return {
454 blocked,
455 checked,
456 descriptorTags: stateTags.slice(),
457 disabled,
458 expanded: ariaExpanded,
459 invalid,
460 pointerEventsNone: blocked,
461 pressed: ariaPressed,
462 readonly,
463 required,
464 selected,
465 semanticTags: [],
466 stateTags,
467 visible: !isHiddenElement(element)
468 };
469 }
470
471 function serializeAttributes(element, frameChain) {
472 const serializedAttributes = [];
473 const helperManagedAttributes = new Set([
474 "data-space-browser-node-id",
475 "data-space-browser-frame-id",
476 "data-space-browser-frame-chain",
477 "data-space-browser-state-tags",
478 "data-space-browser-semantic-tags",
479 "data-space-browser-descriptor-tags",
480 "data-space-browser-live-value",
481 "data-space-browser-selected-text"
482 ]);
483 try {
484 [...(element?.attributes || [])].forEach((attribute) => {
485 const name = String(attribute?.name || "").trim();
486 if (!name || helperManagedAttributes.has(name)) {
487 return;
488 }
489 serializedAttributes.push(` ${name}="${escapeHtmlAttribute(attribute?.value || "")}"`);
490 });
491 } catch {
492 // Attribute reads are best effort across unusual DOM nodes.
493 }
494
495 if (isActionableElement(element)) {
496 const stateMetadata = collectElementStateMetadata(element);
497 const liveValue = getReferenceValueMetadata(element);
498 serializedAttributes.push(` data-space-browser-node-id="${escapeHtmlAttribute(ensureNodeId(element))}"`);
499 serializedAttributes.push(` data-space-browser-frame-id="${escapeHtmlAttribute(helperFrameId)}"`);
500 serializedAttributes.push(` data-space-browser-frame-chain="${escapeHtmlAttribute(encodeFrameChain(frameChain))}"`);
501 if (stateMetadata.stateTags.length) {
502 serializedAttributes.push(` data-space-browser-state-tags="${escapeHtmlAttribute(stateMetadata.stateTags.join(" "))}"`);
503 }
504 if (stateMetadata.semanticTags.length) {
505 serializedAttributes.push(` data-space-browser-semantic-tags="${escapeHtmlAttribute(stateMetadata.semanticTags.join(" "))}"`);
506 }
507 if (stateMetadata.descriptorTags.length) {
508 serializedAttributes.push(` data-space-browser-descriptor-tags="${escapeHtmlAttribute(stateMetadata.descriptorTags.join(" "))}"`);
509 }
510 if (liveValue) {
511 serializedAttributes.push(` data-space-browser-live-value="${escapeHtmlAttribute(liveValue)}"`);
512 if (getTagName(element) === "SELECT") {
513 serializedAttributes.push(` data-space-browser-selected-text="${escapeHtmlAttribute(liveValue)}"`);
514 }
515 }
516 }
517 return serializedAttributes.join("");
518 }
519
520 function isFrameLikeElement(element) {
521 return ["IFRAME", "FRAME", "OBJECT", "EMBED"].includes(getTagName(element));
522 }
523
524 function createRequestId() {
525 nextRequestId += 1;
526 return `a0-browser-dom-${Date.now()}-${nextRequestId}-${Math.random().toString(16).slice(2)}`;
527 }
528
529 function resolveElementWindow(element) {
530 try {
531 return element?.contentWindow || null;
532 } catch {
533 return null;
534 }
535 }
536
537 function requestChildFrameOperation(targetWindow, type, payload = {}, frameElement = null) {
538 if (!targetWindow || typeof targetWindow.postMessage !== "function") {
539 throw createNamedError(
540 "BrowserDomHelperFrameUnavailableError",
541 "Embedded frame window is unavailable.",
542 {
543 code: "browser_dom_helper_frame_window_unavailable",
544 details: { frameElementTag: getTagName(frameElement).toLowerCase() }
545 }
546 );
547 }
548 return new Promise((resolve, reject) => {
549 const requestId = createRequestId();
550 const timer = globalThis.setTimeout(() => {
551 pendingRequests.delete(requestId);
552 reject(createNamedError(
553 "BrowserDomHelperFrameTimeoutError",
554 `Embedded frame request "${type}" timed out.`,
555 { code: "browser_dom_helper_frame_timeout", details: { type } }
556 ));
557 }, REQUEST_TIMEOUT_MS);
558 pendingRequests.set(requestId, { reject, resolve, timer, type });
559 try {
560 targetWindow.postMessage({
561 channel: DOM_HELPER_CHANNEL,
562 payload,
563 requestId,
564 type
565 }, "*");
566 } catch (error) {
567 globalThis.clearTimeout(timer);
568 pendingRequests.delete(requestId);
569 reject(createNamedError(
570 "BrowserDomHelperFrameRequestError",
571 `Embedded frame request "${type}" could not be posted.`,
572 { cause: error, code: "browser_dom_helper_frame_postmessage_failed", details: { type } }
573 ));
574 }
575 });
576 }
577
578 function registerChildFrame(frameId, targetWindow) {
579 const normalizedFrameId = String(frameId || "").trim();
580 if (normalizedFrameId && targetWindow && typeof targetWindow.postMessage === "function") {
581 childFramesById.set(normalizedFrameId, targetWindow);
582 }
583 }
584
585 function extractDocumentBodyHtml(html) {
586 const normalizedHtml = String(html || "").trim();
587 if (!normalizedHtml) {
588 return "";
589 }
590 try {
591 if (typeof DOMParser === "function") {
592 const parsedDocument = new DOMParser().parseFromString(normalizedHtml, "text/html");
593 return String(parsedDocument.body?.innerHTML || normalizedHtml).trim();
594 }
595 } catch {
596 // Fall through to the raw snapshot.
597 }
598 return normalizedHtml.replace(/<!doctype[\s\S]*?>/iu, "").trim();
599 }
600
601 async function captureFrameElement(frameElement, frameChain, payload = {}) {
602 const childWindow = resolveElementWindow(frameElement);
603 const childPayload = {
604 snapshotMode: normalizeSnapshotMode(payload?.snapshotMode),
605 parentFrameChain: frameChain
606 };
607 if (!childWindow) {
608 return {
609 frameChain: [],
610 frameId: "",
611 html: escapeHtmlText("Embedded frame snapshot unavailable."),
612 message: "Embedded frame snapshot unavailable.",
613 ok: false,
614 status: "window_unavailable",
615 title: "",
616 url: String(frameElement?.getAttribute?.("src") || "").trim()
617 };
618 }
619
620 try {
621 const snapshot = await requestChildFrameOperation(childWindow, "capture_document", childPayload, frameElement);
622 registerChildFrame(snapshot?.frameId, childWindow);
623 return snapshot;
624 } catch (error) {
625 try {
626 const frameDocument = frameElement?.contentDocument;
627 if (frameDocument) {
628 const currentFrameChain = normalizeFrameChain(frameChain);
629 return {
630 frameChain: currentFrameChain,
631 frameId: helperFrameId,
632 html: await serializeDocumentNode(frameDocument, currentFrameChain, childPayload),
633 ok: true,
634 title: String(frameDocument.title || "").trim(),
635 url: String(childWindow.location?.href || frameElement?.src || "").trim()
636 };
637 }
638 } catch {
639 // Cross-origin frames stay best effort and report the postMessage failure below.
640 }
641 return {
642 frameChain: normalizeFrameChain(frameChain),
643 frameId: "",
644 html: escapeHtmlText(String(error?.message || "Embedded frame snapshot unavailable.")),
645 message: String(error?.message || "Embedded frame snapshot unavailable."),
646 ok: false,
647 status: String(error?.code || "capture_failed"),
648 title: String(frameElement?.getAttribute?.("title") || "").trim(),
649 url: String(frameElement?.getAttribute?.("src") || frameElement?.src || "").trim()
650 };
651 }
652 }
653
654 function renderFrameDocument(snapshot, frameElement, payload = {}) {
655 const normalizedSnapshot = snapshot && typeof snapshot === "object" ? snapshot : {};
656 const content = String(
657 normalizedSnapshot.html
658 || escapeHtmlText(normalizedSnapshot.message || "Embedded frame snapshot unavailable.")
659 );
660 if (isContentSnapshotMode(payload)) {
661 return normalizedSnapshot.ok === false ? "" : extractDocumentBodyHtml(content);
662 }
663 return `<space-browser-frame-document`
664 + ` data-space-browser-frame-id="${escapeHtmlAttribute(normalizedSnapshot.frameId || "")}"`
665 + ` data-space-browser-frame-chain="${escapeHtmlAttribute(encodeFrameChain(normalizedSnapshot.frameChain))}"`
666 + ` data-space-browser-status="${escapeHtmlAttribute(normalizedSnapshot.ok === false ? normalizedSnapshot.status || "error" : "ok")}"`
667 + ` data-space-browser-frame-url="${escapeHtmlAttribute(normalizedSnapshot.url || frameElement?.src || "")}"`
668 + ` data-space-browser-frame-title="${escapeHtmlAttribute(normalizedSnapshot.title || frameElement?.getAttribute?.("title") || "")}">`
669 + content
670 + `</space-browser-frame-document>`;
671 }
672
673 async function serializeChildNodes(parentNode, frameChain, payload = {}) {
674 const parts = [];
675 for (const childNode of Array.from(parentNode?.childNodes || [])) {
676 parts.push(await serializeNode(childNode, frameChain, payload));
677 }
678 return parts.join("");
679 }
680
681 async function serializeElementNode(element, frameChain, payload = {}) {
682 const tagName = String(element?.tagName || "").toLowerCase();
683 if (!tagName) {
684 return "";
685 }
686 if (isContentSnapshotMode(payload) && isHiddenElement(element)) {
687 return "";
688 }
689 const openTag = `<${tagName}${serializeAttributes(element, frameChain)}>`;
690 const lightDom = await serializeChildNodes(element, frameChain, payload);
691 let shadowDom = "";
692 try {
693 const shadowRoot = element?.shadowRoot;
694 if (shadowRoot) {
695 const shadowInnerHtml = await serializeChildNodes(shadowRoot, frameChain, payload);
696 shadowDom = isContentSnapshotMode(payload)
697 ? shadowInnerHtml
698 : `<space-browser-shadow-root>${shadowInnerHtml}</space-browser-shadow-root>`;
699 }
700 } catch {
701 shadowDom = "";
702 }
703 let frameDom = "";
704 if (isFrameLikeElement(element)) {
705 frameDom = renderFrameDocument(
706 await captureFrameElement(element, frameChain, payload),
707 element,
708 payload
709 );
710 if (isContentSnapshotMode(payload)) {
711 return frameDom;
712 }
713 }
714 if (VOID_TAGS.has(tagName)) {
715 return `${openTag}${shadowDom}${frameDom}`;
716 }
717 return `${openTag}${lightDom}${shadowDom}${frameDom}</${tagName}>`;
718 }
719
720 async function serializeNode(node, frameChain, payload = {}) {
721 if (!node || typeof node.nodeType !== "number") {
722 return "";
723 }
724 if (node.nodeType === 9) {
725 return serializeDocumentNode(node, frameChain, payload);
726 }
727 if (node.nodeType === 11) {
728 return serializeChildNodes(node, frameChain, payload);
729 }
730 if (node.nodeType === 1) {
731 return serializeElementNode(node, frameChain, payload);
732 }
733 if (node.nodeType === 3) {
734 return escapeHtmlText(node.textContent || "");
735 }
736 if (node.nodeType === 8 && !isContentSnapshotMode(payload)) {
737 return `<!--${escapeHtmlText(node.data || "")}-->`;
738 }
739 return "";
740 }
741
742 async function serializeDocumentNode(doc, frameChain, payload = {}) {
743 return serializeChildNodes(doc, frameChain, payload);
744 }
745
746 async function serializeSelectorTargets(doc, selectors, frameChain, payload = {}) {
747 const targets = {};
748 for (const selector of selectors) {
749 let elements = [];
750 try {
751 elements = [...(doc?.querySelectorAll?.(selector) || [])];
752 } catch (error) {
753 throw createNamedError(
754 "BrowserDomHelperSelectorError",
755 `Browser DOM helper could not resolve selector "${selector}".`,
756 { cause: error, code: "browser_dom_helper_selector_error", details: { selector } }
757 );
758 }
759 const parts = [];
760 for (const element of elements) {
761 parts.push(await serializeNode(element, frameChain, payload));
762 }
763 targets[selector] = parts.join("");
764 }
765 return targets;
766 }
767
768 async function captureDocument(payload = {}) {
769 childFramesById.clear();
770 const currentFrameChain = normalizeFrameChain(payload?.parentFrameChain).concat(helperFrameId);
771 const selectors = normalizeSelectorList(payload);
772 const documentSnapshot = {
773 frameChain: currentFrameChain,
774 frameId: helperFrameId,
775 ok: true,
776 title: String(globalThis.document?.title || "").trim(),
777 url: String(globalThis.location?.href || "").trim()
778 };
779 if (selectors.length) {
780 return {
781 ...documentSnapshot,
782 targets: await serializeSelectorTargets(globalThis.document, selectors, currentFrameChain, payload)
783 };
784 }
785 return {
786 ...documentSnapshot,
787 html: await serializeDocumentNode(globalThis.document, currentFrameChain, payload)
788 };
789 }
790
791 function getElementByNodeId(nodeId, actionLabel) {
792 const normalizedNodeId = String(nodeId || "").trim();
793 if (!normalizedNodeId) {
794 throw createNamedError(
795 "BrowserDomHelperReferenceError",
796 `Browser DOM helper ${actionLabel} requires a node id.`,
797 { code: "browser_dom_helper_node_required", details: { action: actionLabel } }
798 );
799 }
800 const element = elementsByNodeId.get(normalizedNodeId);
801 if (!element) {
802 throw createNamedError(
803 "BrowserDomHelperReferenceError",
804 `Browser DOM helper could not find node "${normalizedNodeId}".`,
805 { code: "browser_dom_helper_node_not_found", details: { action: actionLabel, nodeId: normalizedNodeId } }
806 );
807 }
808 if (element.isConnected === false) {
809 throw createNamedError(
810 "BrowserDomHelperReferenceError",
811 `Browser DOM helper node "${normalizedNodeId}" is no longer connected.`,
812 { code: "browser_dom_helper_node_disconnected", details: { action: actionLabel, nodeId: normalizedNodeId } }
813 );
814 }
815 return element;
816 }
817
818 function serializeElementSnapshot(element) {
819 if (!isElementNode(element)) {
820 return "";
821 }
822 try {
823 if (typeof element.outerHTML === "string" && element.outerHTML) {
824 return element.outerHTML;
825 }
826 } catch {
827 // Fall through.
828 }
829 return "";
830 }
831
832 function scrollElementIntoView(element) {
833 try {
834 element.scrollIntoView?.({ behavior: "auto", block: "center", inline: "center" });
835 return true;
836 } catch {
837 return false;
838 }
839 }
840
841 function focusElement(element) {
842 try {
843 element.focus?.({ preventScroll: true });
844 return true;
845 } catch {
846 try {
847 element.focus?.();
848 return true;
849 } catch {
850 return false;
851 }
852 }
853 }
854
855 function dispatchDomEvent(target, eventName, EventType = "Event", options = {}) {
856 const EventConstructor = typeof globalThis[EventType] === "function"
857 ? globalThis[EventType]
858 : globalThis.Event;
859 const event = new EventConstructor(eventName, {
860 bubbles: true,
861 cancelable: true,
862 composed: true,
863 ...options
864 });
865 target.dispatchEvent(event);
866 return event;
867 }
868
869 function dispatchKeyboardEvent(target, eventName, options = {}) {
870 const KeyboardEventConstructor = typeof globalThis.KeyboardEvent === "function"
871 ? globalThis.KeyboardEvent
872 : globalThis.Event;
873 const event = new KeyboardEventConstructor(eventName, {
874 bubbles: true,
875 cancelable: true,
876 composed: true,
877 code: "Enter",
878 key: "Enter",
879 keyCode: 13,
880 which: 13,
881 ...options
882 });
883 target.dispatchEvent(event);
884 return event;
885 }
886
887 function setNativeValue(element, nextValue) {
888 const tagName = getTagName(element);
889 const normalizedValue = String(nextValue ?? "");
890 if (tagName === "INPUT") {
891 const descriptor = Object.getOwnPropertyDescriptor(globalThis.HTMLInputElement?.prototype || {}, "value");
892 if (typeof descriptor?.set === "function") {
893 descriptor.set.call(element, normalizedValue);
894 } else {
895 element.value = normalizedValue;
896 }
897 return normalizedValue;
898 }
899 if (tagName === "TEXTAREA") {
900 const descriptor = Object.getOwnPropertyDescriptor(globalThis.HTMLTextAreaElement?.prototype || {}, "value");
901 if (typeof descriptor?.set === "function") {
902 descriptor.set.call(element, normalizedValue);
903 } else {
904 element.value = normalizedValue;
905 }
906 return normalizedValue;
907 }
908 if (tagName === "SELECT") {
909 element.value = normalizedValue;
910 return element.value;
911 }
912 if (String(element.getAttribute?.("contenteditable") || "").toLowerCase() === "true") {
913 element.textContent = normalizedValue;
914 return normalizedValue;
915 }
916 throw createNamedError(
917 "BrowserDomHelperActionError",
918 `Browser DOM helper cannot type into <${getTagName(element).toLowerCase()}>.`,
919 { code: "browser_dom_helper_type_unsupported" }
920 );
921 }
922
923 function delayMs(timeoutMs) {
924 return new Promise((resolve) => {
925 globalThis.setTimeout(resolve, Math.max(0, Number(timeoutMs) || 0));
926 });
927 }
928
929 function describeActiveElement(element) {
930 if (!isElementNode(element)) {
931 return "";
932 }
933 return [
934 getTagName(element).toLowerCase(),
935 normalizeAttributeText(element.getAttribute?.("id")) ? `#${normalizeAttributeText(element.getAttribute?.("id"))}` : "",
936 normalizeAttributeText(element.getAttribute?.("name")) ? `name=${normalizeAttributeText(element.getAttribute?.("name"))}` : ""
937 ].filter(Boolean).join(" ");
938 }
939
940 function getActionObservationRoot(element) {
941 return element?.closest?.("form, fieldset, dialog, [role='dialog'], [role='alert'], [role='status'], [aria-live], article, section, main, li, tr, td, th")
942 || element?.parentElement
943 || element
944 || globalThis.document?.body
945 || globalThis.document?.documentElement
946 || null;
947 }
948
949 function captureActionEffectSnapshot(element) {
950 const observationRoot = getActionObservationRoot(element);
951 return {
952 activeElement: describeActiveElement(globalThis.document?.activeElement),
953 observationRoot,
954 observationText: truncateText(normalizeText(observationRoot?.textContent || ""), 2000),
955 targetDom: truncateText(serializeElementSnapshot(element), 2000),
956 targetState: collectElementStateMetadata(element),
957 value: getReferenceValueMetadata(element)
958 };
959 }
960
961 async function withObservedActionWindow(observationRoot, action, { quietMs = 40, timeoutMs = 180 } = {}) {
962 const target = observationRoot?.ownerDocument?.body
963 || observationRoot?.ownerDocument?.documentElement
964 || globalThis.document?.body
965 || globalThis.document?.documentElement;
966 if (!target || typeof globalThis.MutationObserver !== "function") {
967 const result = await action();
968 await delayMs(timeoutMs);
969 return { observedMutations: { attributeNames: [], mutationCount: 0 }, result };
970 }
971 const attributeNames = new Set();
972 let lastMutationAt = 0;
973 let mutationCount = 0;
974 const observer = new globalThis.MutationObserver((mutations) => {
975 mutationCount += mutations.length;
976 lastMutationAt = Date.now();
977 mutations.forEach((mutation) => {
978 if (mutation.type === "attributes" && mutation.attributeName) {
979 attributeNames.add(String(mutation.attributeName));
980 }
981 });
982 });
983 try {
984 observer.observe(target, {
985 attributes: true,
986 characterData: true,
987 childList: true,
988 subtree: true
989 });
990 const result = await action();
991 const startedAt = Date.now();
992 while (Date.now() - startedAt < timeoutMs) {
993 await delayMs(20);
994 if (mutationCount > 0 && Date.now() - lastMutationAt >= quietMs) {
995 break;
996 }
997 }
998 return { observedMutations: { attributeNames: [...attributeNames], mutationCount }, result };
999 } finally {
1000 observer.disconnect();
1001 }
1002 }
1003
1004 function buildActionEffectResult(beforeSnapshot, afterSnapshot, observedMutations, extra = {}) {
1005 const focusChanged = beforeSnapshot.activeElement !== afterSnapshot.activeElement;
1006 const nearbyTextChanged = beforeSnapshot.observationText !== afterSnapshot.observationText;
1007 const valueChanged = beforeSnapshot.value !== afterSnapshot.value;
1008 const checkedChanged = beforeSnapshot.targetState.checked !== afterSnapshot.targetState.checked;
1009 const selectedChanged = beforeSnapshot.targetState.selected !== afterSnapshot.targetState.selected;
1010 const expandedChanged = beforeSnapshot.targetState.expanded !== afterSnapshot.targetState.expanded;
1011 const pressedChanged = beforeSnapshot.targetState.pressed !== afterSnapshot.targetState.pressed;
1012 const targetDomChanged = beforeSnapshot.targetDom !== afterSnapshot.targetDom;
1013 const domChanged = Boolean(observedMutations.mutationCount) || targetDomChanged || nearbyTextChanged;
1014 const status = {
1015 checkedChanged,
1016 domChanged,
1017 expandedChanged,
1018 focusChanged,
1019 nearbyTextChanged,
1020 pressedChanged,
1021 reacted: false,
1022 selectedChanged,
1023 targetChanged: targetDomChanged || valueChanged || checkedChanged || selectedChanged || expandedChanged || pressedChanged,
1024 targetDomChanged,
1025 valueChanged
1026 };
1027 status.reacted = Object.entries(status).some(([key, value]) => key !== "reacted" && value === true);
1028 status.noObservedEffect = !status.reacted;
1029 return {
1030 ...extra,
1031 effect: {
1032 mutationAttributes: observedMutations.attributeNames.slice(0, 8),
1033 mutationCount: observedMutations.mutationCount
1034 },
1035 status
1036 };
1037 }
1038
1039 function collectActionResult(element) {
1040 const state = collectElementStateMetadata(element);
1041 return {
1042 connected: element.isConnected !== false,
1043 descriptorTags: state.descriptorTags.slice(),
1044 dom: serializeElementSnapshot(element),
1045 frameId: helperFrameId,
1046 nodeId: ensureNodeId(element),
1047 semanticTags: state.semanticTags.slice(),
1048 state,
1049 tagName: getTagName(element)
1050 };
1051 }
1052
1053 function detailLocalNode(payload = {}) {
1054 return collectActionResult(getElementByNodeId(payload?.nodeId, "detail"));
1055 }
1056
1057 async function clickLocalNode(payload = {}) {
1058 const element = getElementByNodeId(payload?.nodeId, "click");
1059 const beforeSnapshot = captureActionEffectSnapshot(element);
1060 scrollElementIntoView(element);
1061 focusElement(element);
1062 if (beforeSnapshot.targetState.disabled) {
1063 throw createNamedError(
1064 "BrowserDomHelperActionError",
1065 `Browser DOM helper node "${payload?.nodeId}" is disabled.`,
1066 { code: "browser_dom_helper_click_disabled" }
1067 );
1068 }
1069 const { observedMutations } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
1070 if (typeof element.click === "function") {
1071 element.click();
1072 } else {
1073 dispatchDomEvent(element, "click", "MouseEvent", { button: 0 });
1074 }
1075 });
1076 return {
1077 ...collectActionResult(element),
1078 ...buildActionEffectResult(beforeSnapshot, captureActionEffectSnapshot(element), observedMutations)
1079 };
1080 }
1081
1082 async function typeLocalNode(payload = {}) {
1083 const element = getElementByNodeId(payload?.nodeId, "type");
1084 const beforeSnapshot = captureActionEffectSnapshot(element);
1085 const { observedMutations, result } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
1086 scrollElementIntoView(element);
1087 focusElement(element);
1088 const appliedValue = setNativeValue(element, payload?.value ?? "");
1089 dispatchDomEvent(element, "beforeinput", "InputEvent", {
1090 data: String(payload?.value ?? ""),
1091 inputType: "insertText"
1092 });
1093 dispatchDomEvent(element, "input", "InputEvent", {
1094 data: String(payload?.value ?? ""),
1095 inputType: "insertText"
1096 });
1097 dispatchDomEvent(element, "change");
1098 return appliedValue;
1099 });
1100 return {
1101 ...collectActionResult(element),
1102 ...buildActionEffectResult(beforeSnapshot, captureActionEffectSnapshot(element), observedMutations),
1103 value: result
1104 };
1105 }
1106
1107 async function submitLocalNode(payload = {}) {
1108 const element = getElementByNodeId(payload?.nodeId, "submit");
1109 const tagName = getTagName(element);
1110 const beforeSnapshot = captureActionEffectSnapshot(element);
1111 const { observedMutations } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
1112 scrollElementIntoView(element);
1113 focusElement(element);
1114 if (tagName === "FORM" && typeof element.requestSubmit === "function") {
1115 element.requestSubmit();
1116 } else if (element.form && typeof element.form.requestSubmit === "function") {
1117 element.form.requestSubmit(["BUTTON", "INPUT"].includes(tagName) ? element : undefined);
1118 } else if (element.form) {
1119 const submitEvent = dispatchDomEvent(element.form, "submit");
1120 if (!submitEvent.defaultPrevented) {
1121 element.form.submit?.();
1122 }
1123 } else if (typeof element.click === "function") {
1124 element.click();
1125 } else {
1126 throw createNamedError(
1127 "BrowserDomHelperActionError",
1128 `Browser DOM helper cannot submit node "${payload?.nodeId}".`,
1129 { code: "browser_dom_helper_submit_unsupported" }
1130 );
1131 }
1132 });
1133 return {
1134 ...collectActionResult(element),
1135 ...buildActionEffectResult(beforeSnapshot, captureActionEffectSnapshot(element), observedMutations)
1136 };
1137 }
1138
1139 async function pressEnterLocalNode(payload = {}) {
1140 const element = getElementByNodeId(payload?.nodeId, "type_submit");
1141 const beforeSnapshot = captureActionEffectSnapshot(element);
1142 const { observedMutations } = await withObservedActionWindow(beforeSnapshot.observationRoot, async () => {
1143 scrollElementIntoView(element);
1144 focusElement(element);
1145 dispatchKeyboardEvent(element, "keydown");
1146 dispatchKeyboardEvent(element, "keypress");
1147 dispatchKeyboardEvent(element, "keyup");
1148 if (getTagName(element) === "INPUT" && element.form && typeof element.form.requestSubmit === "function") {
1149 element.form.requestSubmit();
1150 }
1151 });
1152 return {
1153 ...collectActionResult(element),
1154 ...buildActionEffectResult(beforeSnapshot, captureActionEffectSnapshot(element), observedMutations)
1155 };
1156 }
1157
1158 async function typeSubmitLocalNode(payload = {}) {
1159 const typed = await typeLocalNode(payload);
1160 const submitted = await pressEnterLocalNode(payload);
1161 return {
1162 ...submitted,
1163 effect: {
1164 mutationAttributes: [...new Set([...(typed?.effect?.mutationAttributes || []), ...(submitted?.effect?.mutationAttributes || [])])],
1165 mutationCount: Number(typed?.effect?.mutationCount || 0) + Number(submitted?.effect?.mutationCount || 0)
1166 },
1167 status: {
1168 ...(typed?.status || {}),
1169 ...(submitted?.status || {}),
1170 reacted: Boolean(typed?.status?.reacted || submitted?.status?.reacted),
1171 noObservedEffect: !Boolean(typed?.status?.reacted || submitted?.status?.reacted)
1172 },
1173 value: typed.value
1174 };
1175 }
1176
1177 function scrollLocalNode(payload = {}) {
1178 const element = getElementByNodeId(payload?.nodeId, "scroll");
1179 const beforeSnapshot = captureActionEffectSnapshot(element);
1180 scrollElementIntoView(element);
1181 focusElement(element);
1182 const scrollEffect = buildActionEffectResult(beforeSnapshot, captureActionEffectSnapshot(element), {
1183 attributeNames: [],
1184 mutationCount: 0
1185 });
1186 return {
1187 ...collectActionResult(element),
1188 ...scrollEffect,
1189 status: {
1190 ...scrollEffect.status,
1191 reacted: true,
1192 noObservedEffect: false
1193 }
1194 };
1195 }
1196
1197 async function invokeLocalOperation(type, payload = {}) {
1198 if (type === "capture_document") {
1199 return captureDocument(payload);
1200 }
1201 if (type === "detail_node") {
1202 return detailLocalNode(payload);
1203 }
1204 if (type === "click_node") {
1205 return clickLocalNode(payload);
1206 }
1207 if (type === "type_node") {
1208 return typeLocalNode(payload);
1209 }
1210 if (type === "submit_node") {
1211 return submitLocalNode(payload);
1212 }
1213 if (type === "type_submit_node") {
1214 return typeSubmitLocalNode(payload);
1215 }
1216 if (type === "scroll_node") {
1217 return scrollLocalNode(payload);
1218 }
1219 throw createNamedError(
1220 "BrowserDomHelperActionError",
1221 `Browser DOM helper does not support "${type}".`,
1222 { code: "browser_dom_helper_action_unsupported", details: { type } }
1223 );
1224 }
1225
1226 async function routeOperation(type, payload = {}) {
1227 const frameChain = normalizeFrameChain(payload?.frameChain);
1228 if (!frameChain.length) {
1229 return invokeLocalOperation(type, payload);
1230 }
1231 if (frameChain[0] !== helperFrameId) {
1232 throw createNamedError(
1233 "BrowserDomHelperFrameRouteError",
1234 `Browser DOM helper cannot route frame chain "${encodeFrameChain(frameChain)}" from "${helperFrameId}".`,
1235 { code: "browser_dom_helper_frame_chain_mismatch", details: { frameChain } }
1236 );
1237 }
1238 if (frameChain.length === 1) {
1239 return invokeLocalOperation(type, payload);
1240 }
1241 const nextFrameId = frameChain[1];
1242 const childWindow = childFramesById.get(nextFrameId);
1243 if (!childWindow) {
1244 throw createNamedError(
1245 "BrowserDomHelperFrameRouteError",
1246 `Browser DOM helper does not know child frame "${nextFrameId}".`,
1247 { code: "browser_dom_helper_child_frame_missing", details: { frameChain } }
1248 );
1249 }
1250 return requestChildFrameOperation(childWindow, type, {
1251 ...payload,
1252 frameChain: frameChain.slice(1)
1253 });
1254 }
1255
1256 globalThis.addEventListener("message", (event) => {
1257 const rawMessage = event?.data;
1258 if (!rawMessage || rawMessage.channel !== DOM_HELPER_CHANNEL || typeof rawMessage.type !== "string") {
1259 return;
1260 }
1261 const requestId = typeof rawMessage.requestId === "string" ? rawMessage.requestId : "";
1262 if (!requestId) {
1263 return;
1264 }
1265 if (rawMessage.type.endsWith("_result")) {
1266 const pendingRequest = pendingRequests.get(requestId);
1267 if (!pendingRequest) {
1268 return;
1269 }
1270 pendingRequests.delete(requestId);
1271 if (pendingRequest.timer != null) {
1272 globalThis.clearTimeout(pendingRequest.timer);
1273 }
1274 if (rawMessage.ok === false) {
1275 pendingRequest.reject(createNamedError(
1276 "BrowserDomHelperRemoteError",
1277 String(rawMessage?.payload?.message || `Embedded frame request "${pendingRequest.type}" failed.`),
1278 {
1279 code: rawMessage?.payload?.code ?? "browser_dom_helper_remote_error",
1280 details: rawMessage?.payload?.details || {},
1281 payload: rawMessage.payload
1282 }
1283 ));
1284 return;
1285 }
1286 pendingRequest.resolve(rawMessage.payload);
1287 return;
1288 }
1289 if (typeof event?.source?.postMessage !== "function") {
1290 return;
1291 }
1292 Promise.resolve(routeOperation(rawMessage.type, rawMessage.payload || {}))
1293 .then((payload) => {
1294 event.source.postMessage({
1295 channel: DOM_HELPER_CHANNEL,
1296 ok: true,
1297 payload,
1298 requestId,
1299 type: `${rawMessage.type}_result`
1300 }, "*");
1301 })
1302 .catch((error) => {
1303 console.error(`[a0-browser/dom-helper] Request "${rawMessage.type}" failed.`, error);
1304 event.source.postMessage({
1305 channel: DOM_HELPER_CHANNEL,
1306 ok: false,
1307 payload: {
1308 code: error?.code ?? "browser_dom_helper_error",
1309 details: error?.details || {},
1310 message: String(error?.message || `Embedded frame request "${rawMessage.type}" failed.`)
1311 },
1312 requestId,
1313 type: `${rawMessage.type}_result`
1314 }, "*");
1315 });
1316 });
1317
1318 globalThis[DOM_HELPER_KEY] = {
1319 captureDocument(payload) {
1320 return captureDocument(payload);
1321 },
1322 clickNode(frameChain, nodeId) {
1323 return routeOperation("click_node", { frameChain, nodeId });
1324 },
1325 detailNode(frameChain, nodeId) {
1326 return routeOperation("detail_node", { frameChain, nodeId });
1327 },
1328 frameId: helperFrameId,
1329 scrollNode(frameChain, nodeId) {
1330 return routeOperation("scroll_node", { frameChain, nodeId });
1331 },
1332 submitNode(frameChain, nodeId) {
1333 return routeOperation("submit_node", { frameChain, nodeId });
1334 },
1335 typeNode(frameChain, nodeId, value) {
1336 return routeOperation("type_node", { frameChain, nodeId, value });
1337 },
1338 typeSubmitNode(frameChain, nodeId, value) {
1339 return routeOperation("type_submit_node", { frameChain, nodeId, value });
1340 },
1341 version: VERSION
1342 };
1343 })();