viewpoint_core/page/frame_locator/
mod.rs

1//! Frame locator for interacting with iframe content.
2//!
3//! `FrameLocator` provides a way to locate and interact with elements inside
4//! iframes without needing to directly access Frame objects.
5
6// Allow dead code for frame locator methods (spec: frames)
7
8use std::time::Duration;
9
10use super::locator::{AriaRole, LocatorOptions, Selector};
11use crate::Page;
12
13/// Default timeout for frame locator operations.
14const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
15
16/// A locator for finding and interacting with iframe content.
17///
18/// `FrameLocator` represents a view into an iframe on the page. It provides
19/// methods to locate elements within the iframe using the same patterns
20/// as page-level locators.
21///
22/// # Example
23///
24/// ```
25/// # #[cfg(feature = "integration")]
26/// # tokio_test::block_on(async {
27/// # use viewpoint_core::Browser;
28/// use viewpoint_core::AriaRole;
29/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
30/// # let context = browser.new_context().await.unwrap();
31/// # let page = context.new_page().await.unwrap();
32/// # page.goto("about:blank").goto().await.unwrap();
33///
34/// // Locate elements inside an iframe
35/// page.frame_locator("#my-iframe")
36///     .locator("button")
37///     .click()
38///     .await.ok();
39///
40/// // Use semantic locators inside frames
41/// page.frame_locator("#payment-frame")
42///     .get_by_role(AriaRole::Button)
43///     .with_name("Submit")
44///     .build()
45///     .click()
46///     .await.ok();
47///
48/// // Nested frames
49/// page.frame_locator("#outer")
50///     .frame_locator("#inner")
51///     .locator("input")
52///     .fill("text")
53///     .await.ok();
54/// # });
55/// ```
56#[derive(Debug, Clone)]
57pub struct FrameLocator<'a> {
58    /// Reference to the page.
59    page: &'a Page,
60    /// Selector for the iframe element.
61    frame_selector: String,
62    /// Parent frame locators (for nested frames).
63    parent_selectors: Vec<String>,
64    /// Timeout for operations.
65    timeout: Duration,
66}
67
68impl<'a> FrameLocator<'a> {
69    /// Create a new frame locator.
70    pub(crate) fn new(page: &'a Page, selector: impl Into<String>) -> Self {
71        Self {
72            page,
73            frame_selector: selector.into(),
74            parent_selectors: Vec::new(),
75            timeout: DEFAULT_TIMEOUT,
76        }
77    }
78
79    /// Create a nested frame locator with parent context.
80    fn with_parent(
81        page: &'a Page,
82        frame_selector: String,
83        mut parent_selectors: Vec<String>,
84        parent_selector: String,
85    ) -> Self {
86        parent_selectors.push(parent_selector);
87        Self {
88            page,
89            frame_selector,
90            parent_selectors,
91            timeout: DEFAULT_TIMEOUT,
92        }
93    }
94
95    /// Set a custom timeout for this frame locator.
96    #[must_use]
97    pub fn timeout(mut self, timeout: Duration) -> Self {
98        self.timeout = timeout;
99        self
100    }
101
102    /// Get the page this frame locator belongs to.
103    pub fn page(&self) -> &'a Page {
104        self.page
105    }
106
107    /// Create a locator for elements within this frame.
108    ///
109    /// # Example
110    ///
111    /// ```no_run
112    /// use viewpoint_core::Page;
113    ///
114    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
115    /// let button = page.frame_locator("#iframe").locator("button.submit");
116    /// button.click().await?;
117    /// # Ok(())
118    /// # }
119    /// ```
120    pub fn locator(&self, selector: impl Into<String>) -> FrameElementLocator<'a> {
121        FrameElementLocator::new(self.clone(), Selector::Css(selector.into()))
122    }
123
124    /// Create a locator for elements containing the specified text within this frame.
125    pub fn get_by_text(&self, text: impl Into<String>) -> FrameElementLocator<'a> {
126        FrameElementLocator::new(
127            self.clone(),
128            Selector::Text {
129                text: text.into(),
130                exact: false,
131            },
132        )
133    }
134
135    /// Create a locator for elements with exact text content within this frame.
136    pub fn get_by_text_exact(&self, text: impl Into<String>) -> FrameElementLocator<'a> {
137        FrameElementLocator::new(
138            self.clone(),
139            Selector::Text {
140                text: text.into(),
141                exact: true,
142            },
143        )
144    }
145
146    /// Create a locator for elements with the specified ARIA role within this frame.
147    pub fn get_by_role(&self, role: AriaRole) -> FrameRoleLocatorBuilder<'a> {
148        FrameRoleLocatorBuilder::new(self.clone(), role)
149    }
150
151    /// Create a locator for elements with the specified test ID within this frame.
152    pub fn get_by_test_id(&self, test_id: impl Into<String>) -> FrameElementLocator<'a> {
153        FrameElementLocator::new(self.clone(), Selector::TestId(test_id.into()))
154    }
155
156    /// Create a locator for form controls by their associated label text within this frame.
157    pub fn get_by_label(&self, label: impl Into<String>) -> FrameElementLocator<'a> {
158        FrameElementLocator::new(self.clone(), Selector::Label(label.into()))
159    }
160
161    /// Create a locator for inputs by their placeholder text within this frame.
162    pub fn get_by_placeholder(&self, placeholder: impl Into<String>) -> FrameElementLocator<'a> {
163        FrameElementLocator::new(self.clone(), Selector::Placeholder(placeholder.into()))
164    }
165
166    /// Create a frame locator for a nested iframe within this frame.
167    ///
168    /// # Example
169    ///
170    /// ```no_run
171    /// use viewpoint_core::Page;
172    ///
173    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
174    /// // Access element in nested frame
175    /// page.frame_locator("#outer-frame")
176    ///     .frame_locator("#inner-frame")
177    ///     .locator("button")
178    ///     .click()
179    ///     .await?;
180    /// # Ok(())
181    /// # }
182    /// ```
183    pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator<'a> {
184        FrameLocator::with_parent(
185            self.page,
186            selector.into(),
187            self.parent_selectors.clone(),
188            self.frame_selector.clone(),
189        )
190    }
191
192    /// Get the frame selector.
193    pub fn selector(&self) -> &str {
194        &self.frame_selector
195    }
196
197    /// Get the parent selectors (for nested frames).
198    pub fn parent_selectors(&self) -> &[String] {
199        &self.parent_selectors
200    }
201
202    /// Build the JavaScript expression to access the frame's content document.
203    pub(crate) fn to_js_frame_access(&self) -> String {
204        let mut js = String::new();
205
206        // Start from the top-level document
207        js.push_str("(function() {\n");
208        js.push_str("  let doc = document;\n");
209
210        // Navigate through parent frames
211        for parent_selector in &self.parent_selectors {
212            js.push_str(&format!(
213                "  const parent = doc.querySelector({});\n",
214                super::locator::selector::js_string_literal(parent_selector)
215            ));
216            js.push_str("  if (!parent || !parent.contentDocument) return null;\n");
217            js.push_str("  doc = parent.contentDocument;\n");
218        }
219
220        // Access the final frame
221        js.push_str(&format!(
222            "  const frame = doc.querySelector({});\n",
223            super::locator::selector::js_string_literal(&self.frame_selector)
224        ));
225        js.push_str("  if (!frame || !frame.contentDocument) return null;\n");
226        js.push_str("  return frame.contentDocument;\n");
227        js.push_str("})()");
228        js
229    }
230}
231
232/// A locator for elements within a frame.
233///
234/// This combines a `FrameLocator` with an element `Selector` to locate
235/// elements inside an iframe.
236#[derive(Debug, Clone)]
237pub struct FrameElementLocator<'a> {
238    /// The frame locator.
239    frame_locator: FrameLocator<'a>,
240    /// The element selector within the frame.
241    selector: Selector,
242    /// Locator options.
243    options: LocatorOptions,
244}
245
246impl<'a> FrameElementLocator<'a> {
247    /// Create a new frame element locator.
248    fn new(frame_locator: FrameLocator<'a>, selector: Selector) -> Self {
249        Self {
250            frame_locator,
251            selector,
252            options: LocatorOptions::default(),
253        }
254    }
255
256    /// Set a custom timeout for this locator.
257    #[must_use]
258    pub fn timeout(mut self, timeout: Duration) -> Self {
259        self.options.timeout = timeout;
260        self
261    }
262
263    /// Create a child locator that further filters elements.
264    #[must_use]
265    pub fn locator(&self, selector: impl Into<String>) -> FrameElementLocator<'a> {
266        FrameElementLocator {
267            frame_locator: self.frame_locator.clone(),
268            selector: Selector::Chained(
269                Box::new(self.selector.clone()),
270                Box::new(Selector::Css(selector.into())),
271            ),
272            options: self.options.clone(),
273        }
274    }
275
276    /// Select the first matching element.
277    #[must_use]
278    pub fn first(&self) -> FrameElementLocator<'a> {
279        FrameElementLocator {
280            frame_locator: self.frame_locator.clone(),
281            selector: Selector::Nth {
282                base: Box::new(self.selector.clone()),
283                index: 0,
284            },
285            options: self.options.clone(),
286        }
287    }
288
289    /// Select the last matching element.
290    #[must_use]
291    pub fn last(&self) -> FrameElementLocator<'a> {
292        FrameElementLocator {
293            frame_locator: self.frame_locator.clone(),
294            selector: Selector::Nth {
295                base: Box::new(self.selector.clone()),
296                index: -1,
297            },
298            options: self.options.clone(),
299        }
300    }
301
302    /// Select the nth matching element (0-indexed).
303    #[must_use]
304    pub fn nth(&self, index: i32) -> FrameElementLocator<'a> {
305        FrameElementLocator {
306            frame_locator: self.frame_locator.clone(),
307            selector: Selector::Nth {
308                base: Box::new(self.selector.clone()),
309                index,
310            },
311            options: self.options.clone(),
312        }
313    }
314
315    /// Get the frame locator.
316    pub fn frame_locator(&self) -> &FrameLocator<'a> {
317        &self.frame_locator
318    }
319
320    /// Get the selector.
321    pub fn selector(&self) -> &Selector {
322        &self.selector
323    }
324
325    /// Get the locator options.
326    pub(crate) fn options(&self) -> &LocatorOptions {
327        &self.options
328    }
329
330    /// Build the JavaScript expression to query elements within the frame.
331    fn to_js_expression(&self) -> String {
332        let frame_access = self.frame_locator.to_js_frame_access();
333        let element_selector = self.selector.to_js_expression();
334
335        format!(
336            r"(function() {{
337                const frameDoc = {frame_access};
338                if (!frameDoc) return {{ found: false, count: 0, error: 'Frame not found or not accessible' }};
339                
340                // Override document for the selector expression
341                const originalDocument = document;
342                try {{
343                    // Create a modified expression that uses frameDoc instead of document
344                    const elements = (function() {{
345                        const document = frameDoc;
346                        return Array.from({element_selector});
347                    }})();
348                    return elements;
349                }} catch (e) {{
350                    return [];
351                }}
352            }})()"
353        )
354    }
355}
356
357/// Builder for role-based frame locators.
358#[derive(Debug)]
359pub struct FrameRoleLocatorBuilder<'a> {
360    frame_locator: FrameLocator<'a>,
361    role: AriaRole,
362    name: Option<String>,
363}
364
365impl<'a> FrameRoleLocatorBuilder<'a> {
366    fn new(frame_locator: FrameLocator<'a>, role: AriaRole) -> Self {
367        Self {
368            frame_locator,
369            role,
370            name: None,
371        }
372    }
373
374    /// Filter by accessible name.
375    #[must_use]
376    pub fn with_name(mut self, name: impl Into<String>) -> Self {
377        self.name = Some(name.into());
378        self
379    }
380
381    /// Build the locator.
382    pub fn build(self) -> FrameElementLocator<'a> {
383        FrameElementLocator::new(
384            self.frame_locator,
385            Selector::Role {
386                role: self.role,
387                name: self.name,
388            },
389        )
390    }
391}
392
393impl<'a> From<FrameRoleLocatorBuilder<'a>> for FrameElementLocator<'a> {
394    fn from(builder: FrameRoleLocatorBuilder<'a>) -> Self {
395        builder.build()
396    }
397}
398
399