mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
Label standalone toolbar actions, match WebView accessibility values, and fence workspace return checks by pathname. Reuse Android labelled navigation and support adversarial journeys without native screenshot baselines. Record passing iOS file parity and the unresolved terminal file-link gate on both platforms.
587 lines
20 KiB
JavaScript
587 lines
20 KiB
JavaScript
import { WebSocket } from 'ws'
|
|
import { evaluateHostedWebViewCdp } from './hosted-webview-cdp-evaluation.mjs'
|
|
import {
|
|
assertNoHostedMobileWebCdpTarget,
|
|
CDP_TARGET_LIMIT,
|
|
HOSTED_DOCUMENT_TEXT_LIMIT,
|
|
isHostedMobileWebUrl,
|
|
probeHostedWebView,
|
|
readCdpTargets
|
|
} from './hosted-webview-cdp-target-discovery.mjs'
|
|
|
|
const CDP_MESSAGE_MAX_BYTES = 2 * 1024 * 1024
|
|
const HOSTED_CONTROL_ACTIVATION_ATTRIBUTE = 'data-orca-cdp-activation'
|
|
const HOSTED_CONNECTION_OBSERVATION_PROPERTY = '__orcaE2eConnectionObservation'
|
|
let hostedControlActivationSequence = 0
|
|
|
|
export { assertNoHostedMobileWebCdpTarget, isHostedMobileWebUrl }
|
|
|
|
export function selectVisibleHostedWebView(
|
|
probes,
|
|
expectedText,
|
|
expectedHrefIncludes,
|
|
requireInteractiveControls = true,
|
|
expectedPathname
|
|
) {
|
|
const eligible = probes.filter(
|
|
(probe) =>
|
|
isHostedMobileWebUrl(probe.href) &&
|
|
(!expectedHrefIncludes || probe.href.includes(expectedHrefIncludes)) &&
|
|
(!expectedPathname || new URL(probe.href).pathname === expectedPathname) &&
|
|
probe.visibility === 'visible' &&
|
|
probe.bridgeListening &&
|
|
probe.bodyText.includes(expectedText) &&
|
|
(!requireInteractiveControls || probe.buttonCount > 0)
|
|
)
|
|
return eligible.find((probe) => probe.focused) ?? eligible[0] ?? null
|
|
}
|
|
|
|
export async function waitForVisibleHostedWebView({
|
|
discoveryUrl,
|
|
expectedText,
|
|
expectedHrefIncludes,
|
|
expectedPathname,
|
|
requireInteractiveControls = true,
|
|
timeoutMs,
|
|
fetchImpl = fetch,
|
|
WebSocketCtor = WebSocket
|
|
}) {
|
|
const deadline = Date.now() + timeoutMs
|
|
let lastSummary = 'No inspectable WebKit targets'
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const targets = await readCdpTargets(discoveryUrl, fetchImpl)
|
|
const probes = (
|
|
await Promise.all(
|
|
targets
|
|
.slice(-CDP_TARGET_LIMIT)
|
|
.map((target) => probeHostedWebView(target, WebSocketCtor))
|
|
)
|
|
).filter(Boolean)
|
|
lastSummary = JSON.stringify(
|
|
probes.map(({ href, visibility, focused, bridgeListening, bodyText, buttonCount }) => ({
|
|
href,
|
|
visibility,
|
|
focused,
|
|
bridgeListening,
|
|
buttonCount,
|
|
textLength: bodyText.length,
|
|
textPreview: bodyText.slice(0, 240)
|
|
}))
|
|
)
|
|
const selected = selectVisibleHostedWebView(
|
|
probes,
|
|
expectedText,
|
|
expectedHrefIncludes,
|
|
requireInteractiveControls,
|
|
expectedPathname
|
|
)
|
|
if (selected) {
|
|
return selected
|
|
}
|
|
} catch (error) {
|
|
lastSummary = error instanceof Error ? error.message : String(error)
|
|
}
|
|
await delay(500)
|
|
}
|
|
throw new Error(
|
|
`Timed out waiting for the visible hosted mobile document. Last targets: ${lastSummary}`
|
|
)
|
|
}
|
|
|
|
export async function verifyHostedWebViewNetworkIsolation({
|
|
document,
|
|
probeId,
|
|
settleDelayMs = 250,
|
|
WebSocketCtor = WebSocket
|
|
}) {
|
|
if (!document?.webSocketDebuggerUrl) {
|
|
throw new Error('Hosted WebView inspector target is unavailable')
|
|
}
|
|
if (typeof probeId !== 'string' || probeId.length === 0) {
|
|
throw new Error('Hosted WebView network probe token is unavailable')
|
|
}
|
|
const started = await evaluateHostedDocumentWithRetry(
|
|
document,
|
|
`(() => {
|
|
const run = globalThis.__orcaRunSecurityProbe;
|
|
if (typeof run !== 'function') return '';
|
|
run();
|
|
return ${JSON.stringify(probeId)};
|
|
})()`,
|
|
WebSocketCtor
|
|
)
|
|
if (started !== probeId) {
|
|
throw new Error('Hosted WebView network probe could not start')
|
|
}
|
|
await delay(settleDelayMs)
|
|
const completed = await evaluateHostedDocumentWithRetry(
|
|
document,
|
|
`String(globalThis.__orcaDebugNetworkProbeCompletion ?? '')`,
|
|
WebSocketCtor
|
|
)
|
|
if (completed !== probeId) {
|
|
throw new Error('Hosted WebView network probe did not complete')
|
|
}
|
|
return {
|
|
fetch: 'attempted',
|
|
xhr: 'attempted',
|
|
webSocket: 'attempted',
|
|
image: 'attempted'
|
|
}
|
|
}
|
|
|
|
export async function verifyHostedWebViewNavigationIsolation({
|
|
document,
|
|
discoveryUrl,
|
|
probeId,
|
|
settleDelayMs = 500,
|
|
WebSocketCtor = WebSocket,
|
|
fetchImpl = fetch
|
|
}) {
|
|
if (!document?.webSocketDebuggerUrl) {
|
|
throw new Error('Hosted WebView inspector target is unavailable')
|
|
}
|
|
if (typeof probeId !== 'string' || probeId.length === 0) {
|
|
throw new Error('Hosted WebView navigation probe token is unavailable')
|
|
}
|
|
await delay(settleDelayMs)
|
|
const value = await evaluateHostedDocumentWithRetry(
|
|
document,
|
|
`String(globalThis.__orcaDebugNavigationProbeCompletion ?? '')`,
|
|
WebSocketCtor
|
|
)
|
|
let result
|
|
try {
|
|
result = JSON.parse(value)
|
|
} catch {
|
|
throw new Error('Hosted WebView navigation probe did not complete')
|
|
}
|
|
const popupBlocked =
|
|
result.popupBlocked === true ||
|
|
(discoveryUrl ? await verifyNoPopupInspectorTarget(document, discoveryUrl, fetchImpl) : false)
|
|
const expected = {
|
|
token: probeId,
|
|
documentRetained: true,
|
|
popupBlocked: true,
|
|
serviceWorkerBlocked: true,
|
|
redirectFrameAttempted: true,
|
|
downloadAttempted: true,
|
|
externalSchemeAttempted: true
|
|
}
|
|
const verifiedResult = { ...result, popupBlocked }
|
|
if (JSON.stringify(verifiedResult) !== JSON.stringify(expected)) {
|
|
throw new Error(`Hosted WebView navigation isolation failed: ${JSON.stringify(result)}`)
|
|
}
|
|
const { token: _, ...evidence } = verifiedResult
|
|
return evidence
|
|
}
|
|
|
|
async function verifyNoPopupInspectorTarget(document, discoveryUrl, fetchImpl) {
|
|
const targets = await readCdpTargets(discoveryUrl, fetchImpl)
|
|
const pages = targets.filter((target) => target.type === 'page')
|
|
return pages.length === 1 && pages[0]?.id === document.targetId
|
|
}
|
|
|
|
export async function readHostedWebViewState(document, WebSocketCtor = WebSocket) {
|
|
const expression = `JSON.stringify({
|
|
href: String(location.href).slice(0, 2048),
|
|
bodyText: String(document.body?.innerText ?? '').slice(0, ${HOSTED_DOCUMENT_TEXT_LIMIT}),
|
|
labels: Array.from(document.querySelectorAll('[aria-label]')).filter((element) => {
|
|
const style = getComputedStyle(element);
|
|
const rect = element.getBoundingClientRect();
|
|
return style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
rect.width > 0 &&
|
|
rect.height > 0 &&
|
|
rect.bottom > 0 &&
|
|
rect.right > 0 &&
|
|
rect.top < innerHeight &&
|
|
rect.left < innerWidth;
|
|
}).slice(0, 128)
|
|
.map((element) => String(element.getAttribute('aria-label') ?? '').slice(0, 240)),
|
|
placeholders: Array.from(document.querySelectorAll('input[placeholder],textarea[placeholder]'))
|
|
.slice(0, 32)
|
|
.map((element) => String(element.getAttribute('placeholder') ?? '').slice(0, 240))
|
|
})`
|
|
const value = await evaluateHostedDocumentWithRetry(document, expression, WebSocketCtor)
|
|
const parsed = JSON.parse(value)
|
|
if (
|
|
typeof parsed?.href !== 'string' ||
|
|
typeof parsed.bodyText !== 'string' ||
|
|
!Array.isArray(parsed.labels) ||
|
|
!parsed.labels.every((label) => typeof label === 'string') ||
|
|
!Array.isArray(parsed.placeholders) ||
|
|
!parsed.placeholders.every((placeholder) => typeof placeholder === 'string')
|
|
) {
|
|
throw new Error('Hosted WebView state probe returned an invalid value')
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
export async function readHostedWebViewTextPoint(
|
|
document,
|
|
text,
|
|
WebSocketCtor = WebSocket,
|
|
options = {}
|
|
) {
|
|
const horizontalPosition = options.horizontalPosition ?? 0.5
|
|
if (!Number.isFinite(horizontalPosition) || horizontalPosition < 0 || horizontalPosition > 1) {
|
|
throw new Error('Hosted WebView text point position is invalid')
|
|
}
|
|
const expression = `(() => {
|
|
const expected = ${JSON.stringify(text)};
|
|
const comparableExpected = ${
|
|
options.ignoreCase === true
|
|
} ? expected.toLocaleLowerCase() : expected;
|
|
const isVisible = (candidate) => {
|
|
const style = getComputedStyle(candidate);
|
|
const rect = candidate.getBoundingClientRect();
|
|
return style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
rect.width > 0 &&
|
|
rect.height > 0 &&
|
|
rect.bottom > 0 &&
|
|
rect.right > 0 &&
|
|
rect.top < innerHeight &&
|
|
rect.left < innerWidth;
|
|
};
|
|
const matches = Array.from(document.querySelectorAll('body *')).filter((candidate) =>
|
|
candidate.children.length === 0 && (${options.reveal === true} || isVisible(candidate)) && (
|
|
${
|
|
options.ignoreCase === true
|
|
? "String(candidate.textContent ?? '').trim().toLocaleLowerCase()"
|
|
: "String(candidate.textContent ?? '').trim()"
|
|
}
|
|
) === comparableExpected
|
|
);
|
|
const element = matches[${options.occurrence ?? 0}];
|
|
if (!element) return '';
|
|
if (${options.reveal === true}) {
|
|
element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
}
|
|
const rect = element.getBoundingClientRect();
|
|
const screenWidth = Number(screen.width);
|
|
const screenHeight = Number(screen.height);
|
|
const viewportTop = Math.max(0, screenHeight - Number(innerHeight));
|
|
return JSON.stringify({
|
|
x: (rect.left + rect.width * ${horizontalPosition}) / screenWidth,
|
|
y: (viewportTop + rect.top + rect.height / 2) / screenHeight
|
|
});
|
|
})()`
|
|
const value = await evaluateHostedDocumentWithRetry(document, expression, WebSocketCtor)
|
|
let point
|
|
try {
|
|
point = JSON.parse(value)
|
|
} catch {
|
|
throw new Error(`Hosted WebView control was not found: ${text}`)
|
|
}
|
|
if (
|
|
!point ||
|
|
!Number.isFinite(point.x) ||
|
|
!Number.isFinite(point.y) ||
|
|
point.x < 0 ||
|
|
point.x > 1 ||
|
|
point.y < 0 ||
|
|
point.y > 1
|
|
) {
|
|
throw new Error(`Hosted WebView returned an invalid text point: ${text}`)
|
|
}
|
|
return point
|
|
}
|
|
|
|
export function terminateHostedWebViewProcess(document, WebSocketCtor = WebSocket) {
|
|
if (!document?.webSocketDebuggerUrl) {
|
|
return Promise.reject(new Error('Hosted WebView inspector target is unavailable'))
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const socket = new WebSocketCtor(document.webSocketDebuggerUrl, {
|
|
maxPayload: CDP_MESSAGE_MAX_BYTES
|
|
})
|
|
const timer = setTimeout(
|
|
() => finish(new Error('Hosted WebView process termination timed out')),
|
|
5_000
|
|
)
|
|
let settled = false
|
|
|
|
const finish = (error) => {
|
|
if (settled) {
|
|
return
|
|
}
|
|
settled = true
|
|
clearTimeout(timer)
|
|
socket.close()
|
|
if (error) {
|
|
reject(error)
|
|
} else {
|
|
resolve()
|
|
}
|
|
}
|
|
|
|
socket.once('open', () => {
|
|
socket.send(JSON.stringify({ id: 1, method: 'Page.crash' }))
|
|
})
|
|
socket.on('message', (data) => {
|
|
try {
|
|
if (data.length > CDP_MESSAGE_MAX_BYTES) {
|
|
finish(new Error('WebKit CDP response exceeded its size limit'))
|
|
return
|
|
}
|
|
const message = JSON.parse(String(data))
|
|
if (
|
|
message.method === 'Inspector.detached' &&
|
|
message.params?.reason === 'Render process gone.'
|
|
) {
|
|
finish()
|
|
} else if (message.id === 1 && message.error) {
|
|
finish(new Error(message.error.message ?? 'WebKit process termination failed'))
|
|
}
|
|
} catch (error) {
|
|
finish(error)
|
|
}
|
|
})
|
|
socket.once('error', (error) => finish(error))
|
|
socket.once('close', () => {
|
|
if (!settled) {
|
|
finish(new Error('WebKit process closed without termination evidence'))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
export async function startHostedWebViewConnectionObservation(
|
|
document,
|
|
{ expectedText, expectedHrefIncludes },
|
|
WebSocketCtor = WebSocket
|
|
) {
|
|
const expression = `(() => {
|
|
const key = ${JSON.stringify(HOSTED_CONNECTION_OBSERVATION_PROPERTY)};
|
|
const observation = globalThis[key] ??= { entries: [], listening: false };
|
|
observation.entries.length = 0;
|
|
observation.expectedText = ${JSON.stringify(expectedText)};
|
|
observation.expectedHrefIncludes = ${JSON.stringify(expectedHrefIncludes)};
|
|
if (!observation.listening) {
|
|
addEventListener('message', (event) => {
|
|
try {
|
|
const message = typeof event.data === 'string' ? JSON.parse(event.data) : null;
|
|
if (message?.type !== 'connection') return;
|
|
const state = message.state;
|
|
if (!['connecting', 'connected', 'offline', 'recovering'].includes(state)) return;
|
|
requestAnimationFrame(() => {
|
|
const current = globalThis[key];
|
|
if (!current) return;
|
|
const href = String(location.href).slice(0, 2048);
|
|
current.entries.push({
|
|
state,
|
|
href,
|
|
retainedExpectedText: String(document.body?.innerText ?? '')
|
|
.includes(current.expectedText),
|
|
retainedExpectedRoute: href.includes(current.expectedHrefIncludes)
|
|
});
|
|
if (current.entries.length > 32) current.entries.shift();
|
|
});
|
|
} catch {}
|
|
});
|
|
observation.listening = true;
|
|
}
|
|
return JSON.stringify({ started: true });
|
|
})()`
|
|
const value = await evaluateHostedDocumentWithRetry(document, expression, WebSocketCtor)
|
|
if (JSON.parse(value)?.started !== true) {
|
|
throw new Error('Hosted WebView connection observation did not start')
|
|
}
|
|
}
|
|
|
|
export async function waitForHostedWebViewConnectionSequence(
|
|
document,
|
|
expectedStates,
|
|
timeoutMs,
|
|
transport = WebSocket
|
|
) {
|
|
const WebSocketCtor =
|
|
typeof transport === 'function' ? transport : (transport.WebSocketCtor ?? WebSocket)
|
|
const reacquireDocument =
|
|
typeof transport === 'function' ? undefined : transport.reacquireDocument
|
|
const deadline = Date.now() + timeoutMs
|
|
let entries = []
|
|
let activeDocument = document
|
|
const observedEntryCounts = new Map()
|
|
const expression = `JSON.stringify(globalThis[${JSON.stringify(
|
|
HOSTED_CONNECTION_OBSERVATION_PROPERTY
|
|
)}]?.entries ?? [])`
|
|
while (Date.now() < deadline) {
|
|
let currentEntries
|
|
try {
|
|
const value = await evaluateHostedDocumentWithRetry(activeDocument, expression, WebSocketCtor)
|
|
currentEntries = JSON.parse(value)
|
|
} catch (error) {
|
|
if (!reacquireDocument) {
|
|
throw error
|
|
}
|
|
activeDocument = await reacquireDocument(Math.max(1, deadline - Date.now()))
|
|
continue
|
|
}
|
|
const documentKey = activeDocument.webSocketDebuggerUrl
|
|
const previousCount = observedEntryCounts.get(documentKey) ?? 0
|
|
const nextOffset = currentEntries.length < previousCount ? 0 : previousCount
|
|
entries.push(...currentEntries.slice(nextOffset))
|
|
observedEntryCounts.set(documentKey, currentEntries.length)
|
|
if (hasOrderedConnectionStates(entries, expectedStates)) {
|
|
return entries
|
|
}
|
|
await delay(250)
|
|
}
|
|
throw new Error(
|
|
`Hosted WebView did not observe connection sequence ${expectedStates.join(
|
|
' → '
|
|
)}. Last states: ${entries.map((entry) => entry.state).join(', ')}`
|
|
)
|
|
}
|
|
|
|
function hasOrderedConnectionStates(entries, expectedStates) {
|
|
let expectedIndex = 0
|
|
for (const entry of entries) {
|
|
if (entry?.state === expectedStates[expectedIndex]) {
|
|
expectedIndex += 1
|
|
}
|
|
}
|
|
return expectedIndex === expectedStates.length
|
|
}
|
|
|
|
export async function evaluateHostedDocumentWithRetry(
|
|
document,
|
|
expression,
|
|
WebSocketCtor = WebSocket
|
|
) {
|
|
let lastError = null
|
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
try {
|
|
return await evaluateHostedDocument(document, expression, WebSocketCtor)
|
|
} catch (error) {
|
|
lastError = error
|
|
await delay(250)
|
|
}
|
|
}
|
|
throw lastError
|
|
}
|
|
|
|
export async function activateHostedWebViewControl(document, target, WebSocketCtor = WebSocket) {
|
|
const activationToken = `activation-${Date.now()}-${hostedControlActivationSequence++}`
|
|
const locateExpression = `(() => {
|
|
const expected = ${JSON.stringify(target.value)};
|
|
const comparableExpected = ${
|
|
target.ignoreCase === true
|
|
} ? expected.toLocaleLowerCase() : expected;
|
|
const token = ${JSON.stringify(activationToken)};
|
|
const isVisible = (candidate) => {
|
|
const style = getComputedStyle(candidate);
|
|
const rect = candidate.getBoundingClientRect();
|
|
return style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
rect.width > 0 &&
|
|
rect.height > 0 &&
|
|
rect.bottom > 0 &&
|
|
rect.right > 0 &&
|
|
rect.top < innerHeight &&
|
|
rect.left < innerWidth;
|
|
};
|
|
const candidates = Array.from(document.querySelectorAll(${
|
|
target.kind === 'label' ? "'[aria-label]'" : "'body *'"
|
|
}));
|
|
const matches = candidates.filter((candidate) => (${
|
|
target.reveal === true
|
|
} || isVisible(candidate)) && ${
|
|
target.kind === 'label'
|
|
? "candidate.getAttribute('aria-label') === expected"
|
|
: `candidate.children.length === 0 && (
|
|
${
|
|
target.ignoreCase === true
|
|
? "String(candidate.textContent ?? '').trim().toLocaleLowerCase()"
|
|
: "String(candidate.textContent ?? '').trim()"
|
|
}
|
|
) === comparableExpected`
|
|
});
|
|
const interactive = ${
|
|
target.kind === 'label'
|
|
? 'matches'
|
|
: 'Array.from(new Set(matches.map((candidate) => candidate.closest(\'button,[role="button"],a,[tabindex]\')).filter(Boolean)))'
|
|
};
|
|
const controls = interactive.length > 0 ? interactive : matches;
|
|
const element = controls[${target.occurrence ?? 0}];
|
|
if (!(element instanceof HTMLElement)) return JSON.stringify({ found: false });
|
|
if (${target.reveal === true}) element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
element.setAttribute(${JSON.stringify(HOSTED_CONTROL_ACTIVATION_ATTRIBUTE)}, token);
|
|
return JSON.stringify({ found: true });
|
|
})()`
|
|
const locatedValue = await evaluateHostedDocumentWithRetry(
|
|
document,
|
|
locateExpression,
|
|
WebSocketCtor
|
|
)
|
|
const located = JSON.parse(locatedValue)
|
|
if (located?.found !== true) {
|
|
throw new Error(`Hosted WebView control was not found: ${target.value}`)
|
|
}
|
|
|
|
const activateExpression = `(() => {
|
|
const token = ${JSON.stringify(activationToken)};
|
|
const ledger = globalThis.__orcaCdpActivationLedger ??= new Set();
|
|
if (ledger.has(token)) return JSON.stringify({ activated: true, duplicate: true });
|
|
const element = Array.from(document.querySelectorAll('[${HOSTED_CONTROL_ACTIVATION_ATTRIBUTE}]'))
|
|
.find((candidate) => candidate.getAttribute(${JSON.stringify(
|
|
HOSTED_CONTROL_ACTIVATION_ATTRIBUTE
|
|
)}) === token);
|
|
if (!(element instanceof HTMLElement)) return JSON.stringify({ activated: false });
|
|
ledger.add(token);
|
|
element.click();
|
|
return JSON.stringify({ activated: true });
|
|
})()`
|
|
const value = await evaluateHostedDocumentWithRetry(document, activateExpression, WebSocketCtor)
|
|
const parsed = JSON.parse(value)
|
|
if (parsed?.activated !== true) {
|
|
throw new Error(`Hosted WebView control was not found: ${target.value}`)
|
|
}
|
|
}
|
|
|
|
export async function setHostedWebViewInput(document, target, WebSocketCtor = WebSocket) {
|
|
const expression = `(() => {
|
|
const input = Array.from(document.querySelectorAll('input[placeholder],textarea[placeholder]'))
|
|
.find((candidate) => candidate.getAttribute('placeholder') === ${JSON.stringify(
|
|
target.placeholder
|
|
)});
|
|
if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) {
|
|
return JSON.stringify({ updated: false });
|
|
}
|
|
const prototype = input instanceof HTMLTextAreaElement
|
|
? HTMLTextAreaElement.prototype
|
|
: HTMLInputElement.prototype;
|
|
const setter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
|
|
if (!setter) return JSON.stringify({ updated: false });
|
|
setter.call(input, ${JSON.stringify(target.value)});
|
|
input.dispatchEvent(new InputEvent('input', {
|
|
bubbles: true,
|
|
composed: true,
|
|
inputType: 'insertText',
|
|
data: null
|
|
}));
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return JSON.stringify({ updated: true });
|
|
})()`
|
|
const value = await evaluateHostedDocumentWithRetry(document, expression, WebSocketCtor)
|
|
const parsed = JSON.parse(value)
|
|
if (parsed?.updated !== true) {
|
|
throw new Error(`Hosted WebView input was not found: ${target.placeholder}`)
|
|
}
|
|
}
|
|
|
|
function evaluateHostedDocument(document, expression, WebSocketCtor) {
|
|
if (!document?.webSocketDebuggerUrl) {
|
|
throw new Error('Hosted WebView inspector target is unavailable')
|
|
}
|
|
return evaluateHostedWebViewCdp(document.webSocketDebuggerUrl, expression, WebSocketCtor)
|
|
}
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|