Skip to main content

glass/browser/session/
frame.rs

1//! Frame tree discovery and selection.
2//!
3//! Lists all frames in the active page target's frame tree and provides
4//! explicit frame selection for subsequent operations.
5
6use super::*;
7
8impl BrowserSession {
9    /// List all frames in the active target's frame tree.
10    ///
11    /// Discovers iframe/out-of-process frames and marks the currently active
12    /// frame. Call this before `select_frame` to discover available frame IDs.
13    pub async fn list_frames(&self) -> BrowserResult<Vec<FrameInfo>> {
14        let (target_id, target_session, active) = {
15            let topology = self.topology.lock().await;
16            (
17                topology.active_target_id.clone(),
18                topology.active_target_session_id.clone(),
19                topology.active_frame_id.clone(),
20            )
21        };
22        if target_id.is_none() {
23            return Err(TopologyError::new(
24                TopologyErrorKind::NoTargetSelected,
25                "no active target is selected; call listTargets to discover available pages",
26            )
27            .into());
28        }
29        let target_session = target_session.ok_or_else(|| {
30            TopologyError::new(
31                TopologyErrorKind::NoPageSession,
32                "active target has no CDP session; the session may need to be re-established",
33            )
34        })?;
35        let raw = self
36            .cdp
37            .send_to_session(&target_session, "Page.getFrameTree", None)
38            .await?;
39        let mut frames = Vec::new();
40        collect_frames(&raw["frameTree"], None, active.as_deref(), &mut frames)?;
41        let (attached_sessions, frame_parents) = {
42            let topology = self.topology.lock().await;
43            (
44                topology.frame_sessions.clone(),
45                topology.frame_parents.clone(),
46            )
47        };
48        let mut discovered_frame_sessions = Vec::new();
49        let mut stale_frame_sessions = HashSet::new();
50        let mut queried_sessions = HashSet::new();
51        for (attached_frame_id, session_id) in &attached_sessions {
52            if !queried_sessions.insert(session_id.clone()) {
53                continue;
54            }
55            let oopif_tree = match self
56                .cdp
57                .send_to_session(session_id, "Page.getFrameTree", None)
58                .await
59            {
60                Ok(tree) => tree,
61                Err(error) if error.code == -32_001 => {
62                    stale_frame_sessions.insert(session_id.clone());
63                    continue;
64                }
65                Err(error) => return Err(error.into()),
66            };
67            let start = frames.len();
68            collect_frames(
69                &oopif_tree["frameTree"],
70                frame_parents.get(attached_frame_id).map(String::as_str),
71                active.as_deref(),
72                &mut frames,
73            )?;
74            for frame in &frames[start..] {
75                discovered_frame_sessions.push((frame.id.clone(), session_id.clone()));
76            }
77            for frame in &mut frames[start..] {
78                frame.out_of_process = true;
79            }
80        }
81        let mut topology = self.topology.lock().await;
82        topology
83            .frame_sessions
84            .retain(|_, session| !stale_frame_sessions.contains(session));
85        for (frame_id, session_id) in discovered_frame_sessions {
86            topology.frame_sessions.insert(frame_id, session_id);
87        }
88        topology.frames = frames.clone();
89        Ok(frames)
90    }
91
92    /// Select a frame as the active context for subsequent operations.
93    ///
94    /// The frame must exist in the current target's frame tree. Selecting an
95    /// iframe creates an isolated execution context for JavaScript evaluation.
96    /// Invalidates the observation cache.
97    pub async fn select_frame(&self, frame_id: &str) -> BrowserResult<FrameInfo> {
98        validate_topology_id(frame_id)?;
99        let frame = self
100            .list_frames()
101            .await?
102            .into_iter()
103            .find(|frame| frame.id == frame_id)
104            .ok_or_else(|| {
105                TopologyError::new(
106                    TopologyErrorKind::NoSuchFrame,
107                    format!("frame {frame_id} was not found; call listFrames to discover available frames"),
108                )
109            })?;
110        let session_id = {
111            let topology = self.topology.lock().await;
112            topology
113                .frame_sessions
114                .get(frame_id)
115                .cloned()
116                .or_else(|| topology.active_target_session_id.clone())
117                .ok_or_else(|| {
118                    TopologyError::new(
119                        TopologyErrorKind::NoPageSession,
120                        "active target has no CDP session; the session may need to be re-established",
121                    )
122                })?
123        };
124        let context_id = if frame.parent_id.is_none() {
125            None
126        } else {
127            let world = self
128                .cdp
129                .send_to_session(
130                    &session_id,
131                    "Page.createIsolatedWorld",
132                    Some(serde_json::json!({"frameId": frame_id, "worldName":"glass"})),
133                )
134                .await?;
135            Some(
136                world["executionContextId"]
137                    .as_i64()
138                    .ok_or("Page.createIsolatedWorld returned no executionContextId")?,
139            )
140        };
141        self.cdp.set_active_route(
142            Some(session_id.clone()),
143            Some(frame_id.to_string()),
144            context_id,
145        );
146        {
147            let mut topology = self.topology.lock().await;
148            topology.active_frame_id = Some(frame_id.to_string());
149            topology.active_session_id = Some(session_id);
150        }
151        self.invalidate_observation();
152        Ok(FrameInfo {
153            active: true,
154            ..frame
155        })
156    }
157}