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