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}