fix(relay): DOM-dispatch hover/dblclick/drag; deeper iframe snapshot; key-events typing (#37)

Follow-up to #36 — make the whole interaction surface reach cross-origin OOPIFs
and stop coordinate events drifting onto the user's foreground tab over the relay.

- hover/dblclick/drag now DOM-dispatch over the relay or into an iframe (like
  click already did): a coordinate Input event isn't confined to the target tab
  on a busy real Chrome and can't map an OOPIF element's box to a top-viewport
  point. drag does an HTML5 DnD in the element's frame; cross-frame drag errors
  loudly instead of drifting.
- snapshot recurses iframes to MAX_IFRAME_DEPTH (3) instead of one level, so refs
  inside nested payment/checkout widgets get a frame_id and resolve into the
  right frame.
- relay tab adoption merges several Target.getTargets snapshots — a single flaky
  relay snapshot was dropping live tabs (a driven tab vanished after restart).
- `type --key-events` (alias --keys) sends real per-character keyDown/keyUp
  instead of Input.insertText, so autocomplete/combobox widgets that ignore the
  insertText input event fire (Google address postal lookup; commits Angular
  reactive forms so Save enables).
- SKILL: hard "snapshot-first, never default to screenshot+coordinates" rule;
  snapshot -i pierces cross-origin iframes since v1.5.12; cross-origin iframe
  driving guidance (#37).
This commit is contained in:
leeguooooo
2026-06-17 00:58:12 +09:00
parent 70ab38d35f
commit 9f24e66033
10 changed files with 420 additions and 42 deletions
+27 -5
View File
@@ -330,6 +330,13 @@ impl RoleNameTracker {
}
}
/// Max iframe nesting depth `take_snapshot` expands. Embedded payment/checkout
/// widgets nest a few frames deep (e.g. AdSense → payments.google.com → an inner
/// form frame); expanding past the first level is what gives those inner refs a
/// `frame_id` so clicks resolve into the right frame (issue #36). Capped to keep
/// a pathological frame tree from blowing up the snapshot.
const MAX_IFRAME_DEPTH: usize = 3;
pub async fn take_snapshot(
client: &CdpClient,
session_id: &str,
@@ -337,6 +344,19 @@ pub async fn take_snapshot(
ref_map: &mut RefMap,
frame_id: Option<&str>,
iframe_sessions: &HashMap<String, String>,
) -> Result<String, String> {
take_snapshot_at_depth(client, session_id, options, ref_map, frame_id, iframe_sessions, 0).await
}
#[allow(clippy::too_many_arguments)]
async fn take_snapshot_at_depth(
client: &CdpClient,
session_id: &str,
options: &SnapshotOptions,
ref_map: &mut RefMap,
frame_id: Option<&str>,
iframe_sessions: &HashMap<String, String>,
depth: usize,
) -> Result<String, String> {
client
.send_command_no_params("DOM.enable", Some(session_id))
@@ -606,10 +626,11 @@ pub async fn take_snapshot(
}
// Recurse into child iframes: for each Iframe node with a backend_node_id,
// resolve the child frame ID and take a snapshot of its content.
// We only recurse from the main frame (frame_id == None) to avoid
// unbounded depth; nested iframes within iframes are not expanded.
if frame_id.is_none() {
// resolve the child frame ID and snapshot its content. Recurse to
// MAX_IFRAME_DEPTH (not just the main frame) so refs inside nested
// payment/checkout widgets get a `frame_id` and clicks resolve into the right
// frame (issue #36); the cap bounds a pathological frame tree.
if depth < MAX_IFRAME_DEPTH {
let mut iframe_snapshots: Vec<(String, String)> = Vec::new(); // (ref_id, child_snapshot)
for node in tree_nodes.iter() {
if node.role != "Iframe" || !node.has_ref {
@@ -622,13 +643,14 @@ pub async fn take_snapshot(
if let Ok(child_fid) = resolve_iframe_frame_id(client, session_id, bid).await {
// Snapshot the child frame; errors are silently ignored
// (e.g. cross-origin iframes)
if let Ok(child_text) = Box::pin(take_snapshot(
if let Ok(child_text) = Box::pin(take_snapshot_at_depth(
client,
session_id,
options,
ref_map,
Some(&child_fid),
iframe_sessions,
depth + 1,
))
.await
{