"""Constrained JavaScript builders for live DOM operations."""
from __future__ import annotations
import json
def _literal(value: object) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def _wrapped(body: str) -> str:
return (
"(() => { try { " + body
+ " } catch (error) { return JSON.stringify({ok:false,error:{"
+ "code:'page_script_failed',message:String(error && "
+ "error.message || error)}}); } })()"
)
def _wrappedAsync(body: str) -> str:
return (
"(async () => { 'use strict'; try { " + body
+ " } catch (error) { return JSON.stringify({ok:false,error:{"
+ "code:'javascript_error',message:String(error && "
+ "error.message || error)}}); } })()"
)
def runJavaScript(script: str, *, statement_body: bool = False) -> str:
"""Build an async evaluator for caller-supplied page JavaScript."""
if statement_body:
operation = f"(async () => {{\n{script}\n}})()"
else:
operation = f"(\n{script}\n)"
body = f"""
const result = await {operation};
const resultType = result === null ? 'null' : typeof result;
let serializable = result;
if (resultType === 'undefined') serializable = null;
else if (resultType === 'bigint') serializable = String(result);
else if (resultType === 'symbol' || resultType === 'function') {{
serializable = String(result);
}} else if (resultType === 'number' && !Number.isFinite(result)) {{
serializable = String(result);
}}
return JSON.stringify({{ok:true,value:{{
result:serializable,
result_type:resultType
}}}});
"""
return _wrappedAsync(body)
COMMON_HELPERS = r"""
const clean = value => String(value || '').replace(/\s+/g, ' ').trim();
const labelled = element => {
const direct = element.getAttribute('aria-label');
if (direct) return clean(direct);
const ids = (element.getAttribute('aria-labelledby') || '')
.split(/\s+/).filter(Boolean);
const names = ids.map(id => document.getElementById(id))
.filter(Boolean).map(item => clean(item.textContent)).filter(Boolean);
if (names.length) return names.join(' ');
if (element.labels && element.labels.length) {
return clean(Array.from(element.labels)
.map(item => item.textContent).join(' '));
}
return clean(element.getAttribute('alt') ||
element.getAttribute('title') ||
element.getAttribute('placeholder') || element.textContent);
};
const pathFor = element => {
if (element.id) return '#' + CSS.escape(element.id);
const parts = [];
let current = element;
while (current && current.nodeType === Node.ELEMENT_NODE) {
let part = current.localName;
if (current.id) {
parts.unshift('#' + CSS.escape(current.id));
break;
}
const parent = current.parentElement;
if (parent) {
const peers = Array.from(parent.children)
.filter(item => item.localName === current.localName);
if (peers.length > 1) {
part += ':nth-of-type(' + (peers.indexOf(current) + 1) + ')';
}
}
parts.unshift(part);
current = parent;
}
return parts.join(' > ');
};
const safeHref = element => {
if (!element.href) return '';
try {
const url = new URL(element.href, document.baseURI);
url.username = '';
url.password = '';
return url.href;
} catch (_) { return ''; }
};
const describe = element => ({
selector: pathFor(element),
tag_name: element.localName || '',
role: element.getAttribute('role') || null,
input_type: element instanceof HTMLInputElement ? element.type : null,
accessible_name: labelled(element),
href: safeHref(element)
});
"""
def snapshot(max_elements: int, max_text_chars: int) -> str:
"""Build a compact visibility and actionable-element snapshot script."""
body = COMMON_HELPERS + rf"""
const visible = element => {{
if (element instanceof HTMLInputElement && element.type === 'file') {{
return true;
}}
const style = getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden' &&
style.visibility !== 'collapse' && element.getClientRects().length > 0;
}};
const roles = new Set(['button','link','textbox','checkbox','radio',
'combobox','listbox','option','menuitem','switch','tab','slider']);
const candidates = Array.from(document.querySelectorAll(
'a[href],button,input,textarea,select,[role],[contenteditable]'));
const elements = [];
for (const element of candidates) {{
if (elements.length >= {max_elements}) break;
const role = element.getAttribute('role');
if (!visible(element) || (role && !roles.has(role) &&
!element.matches('a[href],button,input,textarea,select,' +
'[contenteditable]'))) continue;
const item = describe(element);
item.value = element instanceof HTMLInputElement &&
element.type === 'password' ? null :
('value' in element ? String(element.value) : null);
item.disabled = Boolean(element.disabled ||
element.getAttribute('aria-disabled') === 'true');
item.readonly = Boolean(element.readOnly ||
element.getAttribute('aria-readonly') === 'true');
item.checked = 'checked' in element ? Boolean(element.checked) : null;
item.selected = 'selected' in element ? Boolean(element.selected) : null;
item.accept = element instanceof HTMLInputElement &&
element.type === 'file' ? element.accept : null;
item.multiple = element instanceof HTMLInputElement &&
element.type === 'file' ? element.multiple : null;
elements.push(item);
}}
const rawText = clean(document.body ? document.body.innerText : '');
const truncated = rawText.length > {max_text_chars};
const text = rawText.slice(0, {max_text_chars});
return JSON.stringify({{ok:true,value:{{
title: document.title,
url: location.href,
text,
text_truncated: truncated,
elements
}}}});
"""
return _wrapped(body)
def describeElement(selector: str) -> str:
"""Build a script that returns current identity fields for a selector."""
body = COMMON_HELPERS + f"""
let element;
try {{ element = document.querySelector({_literal(selector)}); }}
catch (_) {{ return JSON.stringify({{ok:false,error:{{
code:'invalid_selector',message:'The stored selector is invalid.'
}}}}); }}
if (!element) return JSON.stringify({{ok:false,error:{{
code:'element_not_found',message:'The selector no longer resolves.'
}}}});
return JSON.stringify({{ok:true,value:describe(element)}});
"""
return _wrapped(body)
def click(selector: str) -> str:
"""Build a stale-safe DOM click script for a validated selector."""
body = f"""
const element = document.querySelector({_literal(selector)});
if (!element) return JSON.stringify({{ok:false,error:{{
code:'element_not_found',message:'The selector no longer resolves.'
}}}});
if (element.disabled || element.getAttribute('aria-disabled') === 'true') {{
return JSON.stringify({{ok:false,error:{{
code:'element_not_interactable',message:'The element is disabled.'
}}}});
}}
element.scrollIntoView({{block:'nearest',inline:'nearest'}});
if (typeof element.focus === 'function') element.focus();
element.click();
return JSON.stringify({{ok:true,value:{{synthetic:true,url:location.href}}}});
"""
return _wrapped(body)
def fill(selector: str, value: str, submit: bool) -> str:
"""Build a native-setter text replacement script."""
body = f"""
const element = document.querySelector({_literal(selector)});
if (!element) return JSON.stringify({{ok:false,error:{{
code:'element_not_found',message:'The selector no longer resolves.'
}}}});
const value = {_literal(value)};
element.focus();
if (element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement) {{
if (element.disabled || element.readOnly) return JSON.stringify({{
ok:false,error:{{code:'element_not_interactable',
message:'The element is disabled or readonly.'}}
}});
const proto = element instanceof HTMLInputElement ?
HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(element, value);
element.dispatchEvent(new Event('input', {{bubbles:true}}));
element.dispatchEvent(new Event('change', {{bubbles:true}}));
}} else if (element.isContentEditable) {{
const selection = getSelection();
const range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('insertText', false, value);
element.dispatchEvent(new InputEvent('input', {{
bubbles:true,inputType:'insertText',data:value
}}));
}} else {{
return JSON.stringify({{ok:false,error:{{
code:'unsupported_element',message:'Element is not text-editable.'
}}}});
}}
if ({_literal(submit)}) {{
const form = element.form || element.closest('form');
if (form) form.requestSubmit();
}}
return JSON.stringify({{ok:true,value:{{synthetic:true,
submitted:{_literal(submit)}}}}});
"""
return _wrapped(body)
def appendCharacter(selector: str, character: str) -> str:
"""Build one bounded synthetic typing step."""
body = f"""
const element = document.querySelector({_literal(selector)});
if (!element) return JSON.stringify({{ok:false,error:{{
code:'element_not_found',message:'The selector no longer resolves.'
}}}});
const character = {_literal(character)};
if (!(element instanceof HTMLInputElement) &&
!(element instanceof HTMLTextAreaElement) && !element.isContentEditable) {{
return JSON.stringify({{ok:false,error:{{
code:'unsupported_element',message:'Element is not text-editable.'
}}}});
}}
element.focus();
element.dispatchEvent(new KeyboardEvent('keydown', {{
key:character,bubbles:true
}}));
if (element.isContentEditable) {{
document.execCommand('insertText', false, character);
}} else {{
const proto = element instanceof HTMLInputElement ?
HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(element, element.value + character);
}}
element.dispatchEvent(new InputEvent('input', {{
bubbles:true,inputType:'insertText',data:character
}}));
element.dispatchEvent(new KeyboardEvent('keyup', {{
key:character,bubbles:true
}}));
return JSON.stringify({{ok:true,value:{{synthetic:true}}}});
"""
return _wrapped(body)
def pressKey(selector: str | None, key: str) -> str:
"""Build one allowlisted synthetic key action."""
target = (
f"document.querySelector({_literal(selector)})"
if selector is not None
else "document.activeElement"
)
body = f"""
const element = {target};
if (!element) return JSON.stringify({{ok:false,error:{{
code:'element_not_found',message:'No target element is available.'
}}}});
const key = {_literal(key)};
element.focus();
element.dispatchEvent(new KeyboardEvent('keydown', {{key,bubbles:true}}));
let effect = 'event';
if (key === 'Enter' && element.form) {{
element.form.requestSubmit(); effect = 'form_submitted';
}} else if (key === 'Tab') {{
const all = Array.from(document.querySelectorAll(
'a[href],button,input,textarea,select,[tabindex],[contenteditable]'))
.filter(item => !item.disabled && item.tabIndex >= 0 &&
item.getClientRects().length > 0);
const index = all.indexOf(element);
if (all.length) all[(index + 1) % all.length].focus();
effect = 'focus_moved';
}} else if ((key === 'Backspace' || key === 'Delete') &&
(element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement)) {{
const start = element.selectionStart ?? element.value.length;
const end = element.selectionEnd ?? start;
let next = element.value;
if (start !== end) next = next.slice(0, start) + next.slice(end);
else if (key === 'Backspace' && start > 0)
next = next.slice(0, start - 1) + next.slice(end);
else if (key === 'Delete')
next = next.slice(0, start) + next.slice(start + 1);
const proto = element instanceof HTMLInputElement ?
HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
Object.getOwnPropertyDescriptor(proto, 'value').set.call(element, next);
element.dispatchEvent(new InputEvent('input', {{bubbles:true,
inputType:key === 'Backspace' ? 'deleteContentBackward' :
'deleteContentForward'}}));
effect = 'value_changed';
}}
element.dispatchEvent(new KeyboardEvent('keyup', {{key,bubbles:true}}));
return JSON.stringify({{ok:true,value:{{synthetic:true,effect}}}});
"""
return _wrapped(body)
def navigate(url: str) -> str:
"""Build an absolute URL assignment script."""
return _wrapped(
f"window.location.href = {_literal(url)}; "
"return JSON.stringify({ok:true,value:{started:true}});"
)
def pageState() -> str:
"""Build a script returning URL, title, and document readiness."""
return _wrapped(
"return JSON.stringify({ok:true,value:{url:location.href,"
"title:document.title,ready_state:document.readyState}});"
)
def waitCondition(
condition: str,
value: str,
state: str,
) -> str:
"""Build one injection-safe observable-condition poll."""
if condition == "text":
expression = (
"Boolean(document.body && "
f"document.body.innerText.includes({_literal(value)}))"
)
elif condition == "url_contains":
expression = f"location.href.includes({_literal(value)})"
else:
body = f"""
let present;
try {{ present = document.querySelector({_literal(value)}) !== null; }}
catch (_) {{ return JSON.stringify({{ok:false,error:{{
code:'invalid_selector',message:'The CSS selector is invalid.'
}}}}); }}
return JSON.stringify({{ok:true,value:{{matched:
present === {_literal(state == 'present')},present,url:location.href}}}});
"""
return _wrapped(body)
return _wrapped(
f"const present = {expression}; "
f"return JSON.stringify({{ok:true,value:{{matched:"
f"present === {_literal(state == 'present')},present,"
"url:location.href}});"
)
def uploadInitialize(
selector: str,
key: str,
metadata: list[dict[str, object]],
) -> str:
"""Build upload target validation and staging initialization."""
body = f"""
const input = document.querySelector({_literal(selector)});
if (!(input instanceof HTMLInputElement) || input.type !== 'file') {{
return JSON.stringify({{ok:false,error:{{code:'upload_target_invalid',
message:'Target is not a file input.'}}}});
}}
if (input.disabled) return JSON.stringify({{ok:false,error:{{
code:'upload_target_invalid',message:'The file input is disabled.'
}}}});
const metadata = {_literal(metadata)};
if (!input.multiple && metadata.length > 1) {{
return JSON.stringify({{ok:false,error:{{code:'upload_target_invalid',
message:'The file input does not allow multiple files.'}}}});
}}
window[{_literal(key)}] = {{selector:{_literal(selector)}, metadata,
chunks:metadata.map(() => []), encoded:metadata.map(() => 0)}};
return JSON.stringify({{ok:true,value:{{multiple:input.multiple,
accept:input.accept}}}});
"""
return _wrapped(body)
def uploadAppend(key: str, file_index: int, chunk: str) -> str:
"""Build one bounded base64 upload staging append."""
body = f"""
const stage = window[{_literal(key)}];
if (!stage) return JSON.stringify({{ok:false,error:{{
code:'upload_transfer_failed',message:'Upload staging was lost.'
}}}});
const index = {file_index};
const chunk = {_literal(chunk)};
stage.chunks[index].push(chunk);
stage.encoded[index] += chunk.length;
return JSON.stringify({{ok:true,value:{{
chunk_index:stage.chunks[index].length - 1,
encoded_length:stage.encoded[index]
}}}});
"""
return _wrapped(body)
def uploadAssign(key: str) -> str:
"""Build File construction, assignment, verification, and cleanup."""
body = f"""
const key = {_literal(key)};
const stage = window[key];
if (!stage) return JSON.stringify({{ok:false,error:{{
code:'upload_assignment_failed',message:'Upload staging was lost.'
}}}});
try {{
const input = document.querySelector(stage.selector);
if (!(input instanceof HTMLInputElement) || input.type !== 'file') {{
return JSON.stringify({{ok:false,error:{{
code:'upload_target_invalid',message:'File input was replaced.'
}}}});
}}
const files = stage.metadata.map((meta, index) => {{
const parts = stage.chunks[index].map(encoded => {{
const binary = atob(encoded);
const bytes = new Uint8Array(binary.length);
for (let offset = 0; offset < binary.length; ++offset) {{
bytes[offset] = binary.charCodeAt(offset);
}}
return bytes;
}});
return new File(parts, meta.name, {{type:meta.type,
lastModified:meta.last_modified}});
}});
const transfer = new DataTransfer();
for (const file of files) transfer.items.add(file);
input.files = transfer.files;
input.dispatchEvent(new Event('input', {{bubbles:true}}));
input.dispatchEvent(new Event('change', {{bubbles:true}}));
const actual = Array.from(input.files).map(file => ({{
name:file.name,size:file.size,type:file.type
}}));
const valid = actual.length === stage.metadata.length &&
actual.every((item, index) => item.name ===
stage.metadata[index].name && item.size ===
stage.metadata[index].size && item.type ===
stage.metadata[index].type);
if (!valid) return JSON.stringify({{ok:false,error:{{
code:'upload_assignment_failed',
message:'File input verification failed.'
}}}});
return JSON.stringify({{ok:true,value:{{files:actual,
input_dispatched:true,change_dispatched:true}}}});
}} finally {{ delete window[key]; }}
"""
return _wrapped(body)
def uploadCleanup(key: str) -> str:
"""Build best-effort deletion of upload staging state."""
return _wrapped(
f"delete window[{_literal(key)}]; "
"return JSON.stringify({ok:true,value:{cleaned:true}});"
)