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}