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}