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::new(
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        ))
89    }
90
91    /// Get all frames in the page, including the main frame and all iframes.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the frame tree cannot be retrieved.
96    ///
97    /// # Example
98    ///
99    /// ```no_run
100    /// use viewpoint_core::Page;
101    ///
102    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
103    /// let frames = page.frames().await?;
104    /// for frame in frames {
105    ///     println!("Frame: {} - {}", frame.name(), frame.url());
106    /// }
107    /// # Ok(())
108    /// # }
109    /// ```
110    #[instrument(level = "debug", skip(self))]
111    pub async fn frames(&self) -> Result<Vec<Frame>, PageError> {
112        if self.closed {
113            return Err(PageError::Closed);
114        }
115
116        let result: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
117            .connection
118            .send_command("Page.getFrameTree", None::<()>, Some(&self.session_id))
119            .await?;
120
121        let mut frames = Vec::new();
122        self.collect_frames(&result.frame_tree, &mut frames);
123
124        Ok(frames)
125    }
126
127    /// Collect frames recursively from a frame tree.
128    fn collect_frames(
129        &self,
130        tree: &viewpoint_cdp::protocol::page::FrameTree,
131        frames: &mut Vec<Frame>,
132    ) {
133        let frame_info = &tree.frame;
134
135        frames.push(Frame::new(
136            self.connection.clone(),
137            self.session_id.clone(),
138            frame_info.id.clone(),
139            frame_info.parent_id.clone(),
140            frame_info.loader_id.clone(),
141            frame_info.url.clone(),
142            frame_info.name.clone().unwrap_or_default(),
143        ));
144
145        if let Some(children) = &tree.child_frames {
146            for child in children {
147                self.collect_frames(child, frames);
148            }
149        }
150    }
151
152    /// Get a frame by its name attribute.
153    ///
154    /// Returns `None` if no frame with the given name is found.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the frame tree cannot be retrieved.
159    ///
160    /// # Example
161    ///
162    /// ```no_run
163    /// use viewpoint_core::Page;
164    ///
165    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
166    /// if let Some(frame) = page.frame("payment-frame").await? {
167    ///     frame.goto("https://payment.example.com").await?;
168    /// }
169    /// # Ok(())
170    /// # }
171    /// ```
172    #[instrument(level = "debug", skip(self), fields(name = %name))]
173    pub async fn frame(&self, name: &str) -> Result<Option<Frame>, PageError> {
174        let frames = self.frames().await?;
175
176        for frame in frames {
177            if frame.name() == name {
178                return Ok(Some(frame));
179            }
180        }
181
182        Ok(None)
183    }
184
185    /// Get a frame by URL pattern.
186    ///
187    /// The pattern can be a glob pattern (e.g., "**/payment/**") or an exact URL.
188    /// Returns the first frame whose URL matches the pattern.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the frame tree cannot be retrieved.
193    ///
194    /// # Example
195    ///
196    /// ```no_run
197    /// use viewpoint_core::Page;
198    ///
199    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
200    /// if let Some(frame) = page.frame_by_url("**/checkout/**").await? {
201    ///     println!("Found checkout frame: {}", frame.url());
202    /// }
203    /// # Ok(())
204    /// # }
205    /// ```
206    #[instrument(level = "debug", skip(self), fields(pattern = %pattern))]
207    pub async fn frame_by_url(&self, pattern: &str) -> Result<Option<Frame>, PageError> {
208        let frames = self.frames().await?;
209
210        // Convert glob pattern to regex
211        let regex_pattern = pattern
212            .replace("**", ".*")
213            .replace('*', "[^/]*")
214            .replace('?', ".");
215
216        let regex = regex::Regex::new(&format!("^{regex_pattern}$"))
217            .map_err(|e| PageError::EvaluationFailed(format!("Invalid URL pattern: {e}")))?;
218
219        for frame in frames {
220            if regex.is_match(&frame.url()) {
221                return Ok(Some(frame));
222            }
223        }
224
225        Ok(None)
226    }
227}