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