viewpoint_core/page/frame_page_methods/
mod.rs

1//! Page methods for frame management.
2//!
3//! This module contains the frame-related methods on the `Page` struct.
4
5use tracing::instrument;
6
7use super::Page;
8use super::frame::Frame;
9use super::frame_locator::FrameLocator;
10use crate::error::PageError;
11
12// =========================================================================
13// Page Frame Methods
14// =========================================================================
15
16impl Page {
17    /// Create a locator for an iframe element.
18    ///
19    /// Frame locators allow targeting elements inside iframes. They can be chained
20    /// to navigate through nested iframes.
21    ///
22    /// # Example
23    ///
24    /// ```no_run
25    /// use viewpoint_core::Page;
26    ///
27    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
28    /// // Target an element inside an iframe
29    /// page.frame_locator("#my-iframe")
30    ///     .locator("button")
31    ///     .click()
32    ///     .await?;
33    ///
34    /// // Navigate through nested iframes
35    /// page.frame_locator("#outer")
36    ///     .frame_locator("#inner")
37    ///     .locator("input")
38    ///     .fill("text")
39    ///     .await?;
40    /// # Ok(())
41    /// # }
42    /// ```
43    pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator<'_> {
44        FrameLocator::new(self, selector)
45    }
46
47    /// Get the main frame of the page.
48    ///
49    /// The main frame is the top-level frame that contains the page content.
50    /// All other frames are child frames (iframes) of this frame.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the frame tree cannot be retrieved.
55    ///
56    /// # Example
57    ///
58    /// ```no_run
59    /// use viewpoint_core::Page;
60    ///
61    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
62    /// let main_frame = page.main_frame().await?;
63    /// println!("Main frame URL: {}", main_frame.url());
64    /// # Ok(())
65    /// # }
66    /// ```
67    #[instrument(level = "debug", skip(self))]
68    pub async fn main_frame(&self) -> Result<Frame, PageError> {
69        if self.closed {
70            return Err(PageError::Closed);
71        }
72
73        let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
74            .connection
75            .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
76            .await?;
77
78        let frame_info = result.frame_tree.frame;
79
80        Ok(Frame::with_context_registry(
81            self.connection.clone(),
82            self.session_id.clone(),
83            frame_info.id,
84            frame_info.parent_id,
85            frame_info.loader_id,
86            frame_info.url,
87            frame_info.name.unwrap_or_default(),
88            self.context_registry.clone(),
89        ))
90    }
91
92    /// Get all frames in the page, including the main frame and all iframes.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error if the frame tree cannot be retrieved.
97    ///
98    /// # Example
99    ///
100    /// ```no_run
101    /// use viewpoint_core::Page;
102    ///
103    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
104    /// let frames = page.frames().await?;
105    /// for frame in frames {
106    ///     println!("Frame: {} - {}", frame.name(), frame.url());
107    /// }
108    /// # Ok(())
109    /// # }
110    /// ```
111    #[instrument(level = "debug", skip(self))]
112    pub async fn frames(&self) -> Result<Vec<Frame>, PageError> {
113        if self.closed {
114            return Err(PageError::Closed);
115        }
116
117        let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
118            .connection
119            .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
120            .await?;
121
122        let mut frames = Vec::new();
123        self.collect_frames(&result.frame_tree, &mut frames);
124
125        Ok(frames)
126    }
127
128    /// Collect frames recursively from a frame tree.
129    fn collect_frames(
130        &self,
131        tree: &viewpoint_cdp::protocol::page::FrameTree,
132        frames: &mut Vec<Frame>,
133    ) {
134        let frame_info = &tree.frame;
135
136        frames.push(Frame::with_context_registry(
137            self.connection.clone(),
138            self.session_id.clone(),
139            frame_info.id.clone(),
140            frame_info.parent_id.clone(),
141            frame_info.loader_id.clone(),
142            frame_info.url.clone(),
143            frame_info.name.clone().unwrap_or_default(),
144            self.context_registry.clone(),
145        ));
146
147        if let Some(children) = &tree.child_frames {
148            for child in children {
149                self.collect_frames(child, frames);
150            }
151        }
152    }
153
154    /// Get a frame by its name attribute.
155    ///
156    /// Returns `None` if no frame with the given name is found.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the frame tree cannot be retrieved.
161    ///
162    /// # Example
163    ///
164    /// ```no_run
165    /// use viewpoint_core::Page;
166    ///
167    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
168    /// if let Some(frame) = page.frame("payment-frame").await? {
169    ///     frame.goto("https://payment.example.com").await?;
170    /// }
171    /// # Ok(())
172    /// # }
173    /// ```
174    #[instrument(level = "debug", skip(self), fields(name = %name))]
175    pub async fn frame(&self, name: &str) -> Result<Option<Frame>, PageError> {
176        let frames = self.frames().await?;
177
178        for frame in frames {
179            if frame.name() == name {
180                return Ok(Some(frame));
181            }
182        }
183
184        Ok(None)
185    }
186
187    /// Get a frame by URL pattern.
188    ///
189    /// The pattern can be a glob pattern (e.g., "**/payment/**") or an exact URL.
190    /// Returns the first frame whose URL matches the pattern.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if the frame tree cannot be retrieved.
195    ///
196    /// # Example
197    ///
198    /// ```no_run
199    /// use viewpoint_core::Page;
200    ///
201    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
202    /// if let Some(frame) = page.frame_by_url("**/checkout/**").await? {
203    ///     println!("Found checkout frame: {}", frame.url());
204    /// }
205    /// # Ok(())
206    /// # }
207    /// ```
208    #[instrument(level = "debug", skip(self), fields(pattern = %pattern))]
209    pub async fn frame_by_url(&self, pattern: &str) -> Result<Option<Frame>, PageError> {
210        let frames = self.frames().await?;
211
212        // Convert glob pattern to regex
213        let regex_pattern = pattern
214            .replace("**", ".*")
215            .replace('*', "[^/]*")
216            .replace('?', ".");
217
218        let regex = regex::Regex::new(&format!("^{regex_pattern}$"))
219            .map_err(|e| PageError::EvaluationFailed(format!("Invalid URL pattern: {e}")))?;
220
221        for frame in frames {
222            if regex.is_match(&frame.url()) {
223                return Ok(Some(frame));
224            }
225        }
226
227        Ok(None)
228    }
229}