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!(context_id = context_id, "Using cached isolated world context");
112                return Ok(context_id);
113            }
114        }
115
116        // Create a new isolated world
117        debug!("Creating new isolated world");
118        let result: viewpoint_cdp::protocol::page::CreateIsolatedWorldResult = self
119            .connection
120            .send_command(
121                "Page.createIsolatedWorld",
122                Some(viewpoint_cdp::protocol::page::CreateIsolatedWorldParams {
123                    frame_id: self.id.clone(),
124                    world_name: Some(world_name.to_string()),
125                    grant_univeral_access: Some(true),
126                }),
127                Some(&self.session_id),
128            )
129            .await?;
130
131        let context_id = result.execution_context_id;
132        debug!(context_id = context_id, "Created isolated world");
133
134        // Cache the context ID
135        self.set_execution_context(world_name.to_string(), context_id);
136
137        Ok(context_id)
138    }
139}
140
141/// Recursively find child frames of a given frame ID.
142pub(super) fn find_child_frames(
143    tree: &viewpoint_cdp::protocol::page::FrameTree,
144    parent_id: &str,
145    connection: &Arc<CdpConnection>,
146    session_id: &str,
147) -> Vec<Frame> {
148    let mut children = Vec::new();
149
150    // Check if this is the parent we're looking for
151    if tree.frame.id == parent_id {
152        // Return all direct children
153        if let Some(ref child_frames) = tree.child_frames {
154            for child in child_frames {
155                children.push(Frame::new(
156                    connection.clone(),
157                    session_id.to_string(),
158                    child.frame.id.clone(),
159                    Some(parent_id.to_string()),
160                    child.frame.loader_id.clone(),
161                    child.frame.url.clone(),
162                    child.frame.name.clone().unwrap_or_default(),
163                ));
164            }
165        }
166    } else {
167        // Recurse into children to find the parent
168        if let Some(ref child_frames) = tree.child_frames {
169            for child in child_frames {
170                let found = find_child_frames(child, parent_id, connection, session_id);
171                children.extend(found);
172            }
173        }
174    }
175
176    children
177}
178
179/// Recursively find the parent frame of a given frame ID.
180pub(super) fn find_parent_frame(
181    tree: &viewpoint_cdp::protocol::page::FrameTree,
182    frame_id: &str,
183    connection: &Arc<CdpConnection>,
184    session_id: &str,
185) -> Option<Frame> {
186    // Check if any direct child is the frame we're looking for
187    if let Some(ref child_frames) = tree.child_frames {
188        for child in child_frames {
189            if child.frame.id == frame_id {
190                // Found it - return the current frame as the parent
191                return Some(Frame::new(
192                    connection.clone(),
193                    session_id.to_string(),
194                    tree.frame.id.clone(),
195                    tree.frame.parent_id.clone(),
196                    tree.frame.loader_id.clone(),
197                    tree.frame.url.clone(),
198                    tree.frame.name.clone().unwrap_or_default(),
199                ));
200            }
201        }
202
203        // Recurse into children
204        for child in child_frames {
205            if let Some(parent) = find_parent_frame(child, frame_id, connection, session_id) {
206                return Some(parent);
207            }
208        }
209    }
210
211    None
212}