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