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}