feat: support cross-origin iframe snapshots and interactions via Target.setAutoAttach (#949)

* feat: support cross-origin iframe snapshots and interactions via Target.setAutoAttach (#925)

Enable Target.setAutoAttach with flatten: true on page sessions so Chrome
auto-creates dedicated CDP sessions for cross-origin iframe targets.

- Add DrainedEvents struct, iframe_sessions map, attach/detach event handling
- Extract resolve_ax_session (shared) and resolve_frame_session helpers
- resolve_element_object_id returns (object_id, effective_session) tuple
- resolve_element_center returns (x, y, effective_session) tuple
- Input dispatch (click/hover/tap) uses effective session for correct coordinates
- Thread iframe_sessions through element.rs, interaction.rs, screenshot.rs
- Clear iframe sessions on navigate, tab switch, tab new, tab close
- Ignore Target.setAutoAttach failure for non-Chrome backends (Lightpanda)
- Unit tests for session resolution logic

* fix

* fix

* fix

---------

Co-authored-by: hyunjinee <leehj0110@kakao.com>
This commit is contained in:
jin.2
2026-03-20 16:37:33 -05:00
committed by GitHub
co-authored by hyunjinee
parent 39e54113e6
commit 59b6e5a034
6 changed files with 919 additions and 168 deletions
+66 -7
View File
@@ -6,7 +6,7 @@ use super::cdp::client::CdpClient;
use super::cdp::types::{
AXNode, AXProperty, AXValue, EvaluateParams, EvaluateResult, GetFullAXTreeResult,
};
use super::element::RefMap;
use super::element::{resolve_ax_session, RefMap};
const INTERACTIVE_ROLES: &[&str] = &[
"button",
@@ -139,6 +139,7 @@ pub async fn take_snapshot(
options: &SnapshotOptions,
ref_map: &mut RefMap,
frame_id: Option<&str>,
iframe_sessions: &HashMap<String, String>,
) -> Result<String, String> {
client
.send_command_no_params("DOM.enable", Some(session_id))
@@ -202,13 +203,24 @@ pub async fn take_snapshot(
None
};
let ax_params = if let Some(fid) = frame_id {
serde_json::json!({ "frameId": fid })
} else {
serde_json::json!({})
};
let (ax_params, effective_session_id) =
resolve_ax_session(frame_id, session_id, iframe_sessions);
// Ensure domains are enabled on the iframe session (defensive fallback
// in case the attach-time enable in execute_command was missed).
if effective_session_id != session_id {
let _ = client
.send_command_no_params("DOM.enable", Some(effective_session_id))
.await;
let _ = client
.send_command_no_params("Accessibility.enable", Some(effective_session_id))
.await;
}
let ax_tree: GetFullAXTreeResult = client
.send_command_typed("Accessibility.getFullAXTree", &ax_params, Some(session_id))
.send_command_typed(
"Accessibility.getFullAXTree",
&ax_params,
Some(effective_session_id),
)
.await?;
let (tree_nodes, root_indices) = build_tree(&ax_tree.nodes);
@@ -350,6 +362,7 @@ pub async fn take_snapshot(
options,
ref_map,
Some(&child_fid),
iframe_sessions,
))
.await
{
@@ -1161,4 +1174,50 @@ mod tests {
assert_eq!(set.len(), 1);
assert!(set.contains("ok"));
}
// -----------------------------------------------------------------------
// resolve_ax_session tests (Issue #925 regression guard)
// Cross-origin iframes must use a dedicated session without frameId.
// Same-origin iframes must use the parent session with frameId.
// -----------------------------------------------------------------------
#[test]
fn test_cross_origin_iframe_uses_dedicated_session() {
let parent_session = "parent-session";
let iframe_frame_id = "cross-origin-iframe-frame";
let iframe_session = "cross-origin-iframe-session";
let mut iframe_sessions = HashMap::new();
iframe_sessions.insert(iframe_frame_id.to_string(), iframe_session.to_string());
let (params, session) =
resolve_ax_session(Some(iframe_frame_id), parent_session, &iframe_sessions);
assert_eq!(session, iframe_session);
assert_eq!(params, serde_json::json!({}));
}
#[test]
fn test_same_origin_iframe_uses_parent_session_with_frame_id() {
let parent_session = "parent-session";
let iframe_frame_id = "same-origin-iframe-frame";
let iframe_sessions = HashMap::new();
let (params, session) =
resolve_ax_session(Some(iframe_frame_id), parent_session, &iframe_sessions);
assert_eq!(session, parent_session);
assert_eq!(params, serde_json::json!({ "frameId": iframe_frame_id }));
}
#[test]
fn test_main_frame_uses_parent_session() {
let parent_session = "parent-session";
let iframe_sessions = HashMap::new();
let (params, session) = resolve_ax_session(None, parent_session, &iframe_sessions);
assert_eq!(session, parent_session);
assert_eq!(params, serde_json::json!({}));
}
}