Files
orca/src/main/browser/snapshot-cursor-interactive-elements.ts
T
Neil c8fe5fc8c1 refactor(browser): split browser and browser-IPC modules under the max-lines budget (#14697)
The five oversized src/main/browser modules and src/main/ipc/browser.ts each
carried a file-level `eslint-disable max-lines` and ran 377-654 counted lines
against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so
this removes all six suppressions and prunes their entries (341 -> 335).

Pure move, no behavior change. cdp-ws-proxy is decomposed into collaborating
objects rather than free functions because its state is genuinely
per-connection: every collaborator is a private readonly instance field built
in the constructor with live closures over `this`, so per-connection state
stays per-connection. Likewise the screencast pacer's isClosed/isStopping and
snapshot capture's getSeq are live thunks, not values captured at wiring time,
so guards inside already-armed timers still observe a later stop().

browser-guest-ui.ts is renamed to browser-guest-shortcut-forwarding.ts: after
the split it exports exactly one function, setupGuestShortcutForwarding, so the
old name no longer described its contents.

Also restores a single `webContents.debugger` read in the screencast path. The
extraction had left three reads where the original had one; the accessor is
stable today, so this is not a behavior fix but it removes a latent divergence.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no new runtime import cycles, and the IPC channel set
diffed identical before/after with all 23 handlers still trust-gated.
2026-08-15 17:59:29 -07:00

103 lines
3.7 KiB
TypeScript

import type { SnapshotEntry } from './snapshot-ax-tree-walk'
import type { CdpCommandSender } from './snapshot-engine'
// Why: finds DOM elements that are visually interactive (cursor:pointer, onclick,
// tabindex, contenteditable) but lack standard ARIA roles. These are common in
// modern SPAs where styled <div>s act as buttons. Returns them as a JS array of
// remote object references that we can resolve to backendNodeIds via CDP.
export async function findCursorInteractiveElements(
sendCommand: CdpCommandSender,
existingEntries: SnapshotEntry[]
): Promise<SnapshotEntry[]> {
const existingNodeIds = new Set(existingEntries.map((e) => e.backendDOMNodeId))
const results: SnapshotEntry[] = []
try {
// Single evaluate call that finds interactive elements and returns their info
// along with a way to reference them by index
const { result } = (await sendCommand('Runtime.evaluate', {
expression: `(() => {
const SKIP_ROLES = new Set(['button','link','textbox','checkbox','radio','tab',
'menuitem','option','switch','slider','combobox','searchbox','spinbutton','treeitem',
'menuitemcheckbox','menuitemradio']);
const SKIP_TAGS = new Set(['input','button','select','textarea','a']);
const seen = new Set();
const found = [];
const matchedElements = [];
function check(el) {
if (seen.has(el)) return;
seen.add(el);
const tag = el.tagName.toLowerCase();
if (SKIP_TAGS.has(tag)) return;
const role = el.getAttribute('role');
if (role && SKIP_ROLES.has(role)) return;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const text = (el.ariaLabel || el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 80);
if (!text) return;
found.push({ text, tag });
matchedElements.push(el);
if (found.length >= 50) return;
}
document.querySelectorAll('[onclick], [tabindex]:not([tabindex="-1"]), [contenteditable="true"]').forEach(el => {
if (found.length < 50) check(el);
});
document.querySelectorAll('div, span, li, td, img, svg, label').forEach(el => {
if (found.length >= 50) return;
try {
if (window.getComputedStyle(el).cursor === 'pointer') check(el);
} catch {}
});
window.__orcaCursorInteractive = matchedElements;
return JSON.stringify(found);
})()`,
returnByValue: true
})) as { result: { value: string } }
const elements = JSON.parse(result.value) as { text: string; tag: string }[]
for (let i = 0; i < elements.length; i++) {
try {
const { result: objResult } = (await sendCommand('Runtime.evaluate', {
expression: `window.__orcaCursorInteractive[${i}]`
})) as { result: { objectId?: string } }
if (!objResult.objectId) {
continue
}
const { node } = (await sendCommand('DOM.describeNode', {
objectId: objResult.objectId
})) as { node: { backendNodeId: number } }
if (existingNodeIds.has(node.backendNodeId)) {
continue
}
results.push({
ref: '',
role: 'clickable',
name: elements[i].text,
backendDOMNodeId: node.backendNodeId,
depth: 0
})
} catch {
continue
}
}
// Clean up
await sendCommand('Runtime.evaluate', {
expression: 'delete window.__orcaCursorInteractive',
returnByValue: true
})
} catch {
// DOM query failed — not critical, just return empty
}
return results
}