viewpoint_core/page/frame/
tree.rs

1//! Frame tree traversal operations.
2
3use std::sync::Arc;
4
5use tracing::{debug, instrument};
6use viewpoint_cdp::CdpConnection;
7use viewpoint_cdp::protocol::runtime::ExecutionContextId;
8
9use super::Frame;
10use crate::error::PageError;
11
12impl Frame {
13    /// Get child frames of this frame.
14    ///
15    /// Returns a list of frames that are direct children of this frame.
16    ///
17    /// # Errors
18    ///
19    /// Returns an error if querying the frame tree fails.
20    #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
21    pub async fn child_frames(&self) -> Result<Vec<Frame>, PageError> {
22        if self.is_detached() {
23            return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
24        }
25
26        // Get the frame tree from CDP
27        let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
28            .connection
29            .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
30            .await?;
31
32        // Find this frame in the tree and return its children
33        let children = find_child_frames(
34            &result.frame_tree,
35            &self.id,
36            &self.connection,
37            &self.session_id,
38        );
39
40        Ok(children)
41    }
42
43    /// Get the parent frame.
44    ///
45    /// Returns `None` if this is the main frame.
46    ///
47    /// # Errors
48    ///
49    /// Returns an error if querying the frame tree fails.
50    #[instrument(level = "debug", skip(self), fields(frame_id = %self.id))]
51    pub async fn parent_frame(&self) -> Result<Option<Frame>, PageError> {
52        if self.is_detached() {
53            return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
54        }
55
56        // Main frame has no parent
57        if self.is_main() {
58            return Ok(None);
59        }
60
61        // Get the frame tree from CDP
62        let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
63            .connection
64            .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
65            .await?;
66
67        // Find the parent frame
68        let parent = find_parent_frame(
69            &result.frame_tree,
70            &self.id,
71            &self.connection,
72            &self.session_id,
73        );
74
75        Ok(parent)
76    }
77
78    /// Get or create an isolated world execution context for this frame.
79    ///
80    /// Isolated worlds are separate JavaScript execution contexts that do not
81    /// share global scope with the main world or other isolated worlds.
82    /// They are useful for injecting scripts that should not interfere with
83    /// page scripts.
84    ///
85    /// The world name is used to identify the isolated world. If an isolated
86    /// world with the same name already exists for this frame, its context ID
87    /// is returned. Otherwise, a new isolated world is created.
88    ///
89    /// # Arguments
90    ///
91    /// * `world_name` - A name for the isolated world (e.g., "viewpoint-isolated")
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if:
96    /// - The frame is detached
97    /// - Creating the isolated world fails
98    #[instrument(level = "debug", skip(self), fields(frame_id = %self.id, world_name = %world_name))]
99    pub(crate) async fn get_or_create_isolated_world(
100        &self,
101        world_name: &str,
102    ) -> Result<ExecutionContextId, PageError> {
103        if self.is_detached() {
104            return Err(PageError::EvaluationFailed("Frame is detached".to_string()));
105        }
106
107        // Check if we already have this isolated world cached
108        {
109            let data = self.data.read();
110            if let Some(&context_id) = data.execution_contexts.get(world_name) {
111                debug!(
112                    context_id = context_id,
113                    "Using cached isolated world context"
114                );
115                return Ok(context_id);
116            }
117        }
118
119        // Create a new isolated world
120        debug!("Creating new isolated world");
121        let result: viewpoint_cdp::protocol::page::CreateIsolatedWorldResult = self
122            .connection
123            .send_command(
124                "Page.createIsolatedWorld",
125                Some(viewpoint_cdp::protocol::page::CreateIsolatedWorldParams {
126                    frame_id: self.id.clone(),
127                    world_name: Some(world_name.to_string()),
128                    grant_univeral_access: Some(true),
129                }),
130                Some(&self.session_id),
131            )
132            .await?;
133
134        let context_id = result.execution_context_id;
135        debug!(context_id = context_id, "Created isolated world");
136
137        // Cache the context ID
138        self.set_execution_context(world_name.to_string(), context_id);
139
140        Ok(context_id)
141    }
142}
143
144/// Recursively find child frames of a given frame ID.
145pub(super) fn find_child_frames(
146    tree: &viewpoint_cdp::protocol::page::FrameTree,
147    parent_id: &str,
148    connection: &Arc<CdpConnection>,
149    session_id: &str,
150) -> Vec<Frame> {
151    let mut children = Vec::new();
152
153    // Check if this is the parent we're looking for
154    if tree.frame.id == parent_id {
155        // Return all direct children
156        if let Some(ref child_frames) = tree.child_frames {
157            for child in child_frames {
158                children.push(Frame::new(
159                    connection.clone(),
160                    session_id.to_string(),
161                    child.frame.id.clone(),
162                    Some(parent_id.to_string()),
163                    child.frame.loader_id.clone(),
164                    child.frame.url.clone(),
165                    child.frame.name.clone().unwrap_or_default(),
166                ));
167            }
168        }
169    } else {
170        // Recurse into children to find the parent
171        if let Some(ref child_frames) = tree.child_frames {
172            for child in child_frames {
173                let found = find_child_frames(child, parent_id, connection, session_id);
174                children.extend(found);
175            }
176        }
177    }
178
179    children
180}
181
182/// Recursively find the parent frame of a given frame ID.
183pub(super) fn find_parent_frame(
184    tree: &viewpoint_cdp::protocol::page::FrameTree,
185    frame_id: &str,
186    connection: &Arc<CdpConnection>,
187    session_id: &str,
188) -> Option<Frame> {
189    // Check if any direct child is the frame we're looking for
190    if let Some(ref child_frames) = tree.child_frames {
191        for child in child_frames {
192            if child.frame.id == frame_id {
193                // Found it - return the current frame as the parent
194                return Some(Frame::new(
195                    connection.clone(),
196                    session_id.to_string(),
197                    tree.frame.id.clone(),
198                    tree.frame.parent_id.clone(),
199                    tree.frame.loader_id.clone(),
200                    tree.frame.url.clone(),
201                    tree.frame.name.clone().unwrap_or_default(),
202                ));
203            }
204        }
205
206        // Recurse into children
207        for child in child_frames {
208            if let Some(parent) = find_parent_frame(child, frame_id, connection, session_id) {
209                return Some(parent);
210            }
211        }
212    }
213
214    None
215}