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    /// ```ignore
112    /// let button = page.frame_locator("#iframe").locator("button.submit");
113    /// button.click().await?;
114    /// ```
115    pub fn locator(&self, selector: impl Into<String>) -> FrameElementLocator<'a> {
116        FrameElementLocator::new(self.clone(), Selector::Css(selector.into()))
117    }
118
119    /// Create a locator for elements containing the specified text within this frame.
120    pub fn get_by_text(&self, text: impl Into<String>) -> FrameElementLocator<'a> {
121        FrameElementLocator::new(
122            self.clone(),
123            Selector::Text {
124                text: text.into(),
125                exact: false,
126            },
127        )
128    }
129
130    /// Create a locator for elements with exact text content within this frame.
131    pub fn get_by_text_exact(&self, text: impl Into<String>) -> FrameElementLocator<'a> {
132        FrameElementLocator::new(
133            self.clone(),
134            Selector::Text {
135                text: text.into(),
136                exact: true,
137            },
138        )
139    }
140
141    /// Create a locator for elements with the specified ARIA role within this frame.
142    pub fn get_by_role(&self, role: AriaRole) -> FrameRoleLocatorBuilder<'a> {
143        FrameRoleLocatorBuilder::new(self.clone(), role)
144    }
145
146    /// Create a locator for elements with the specified test ID within this frame.
147    pub fn get_by_test_id(&self, test_id: impl Into<String>) -> FrameElementLocator<'a> {
148        FrameElementLocator::new(self.clone(), Selector::TestId(test_id.into()))
149    }
150
151    /// Create a locator for form controls by their associated label text within this frame.
152    pub fn get_by_label(&self, label: impl Into<String>) -> FrameElementLocator<'a> {
153        FrameElementLocator::new(self.clone(), Selector::Label(label.into()))
154    }
155
156    /// Create a locator for inputs by their placeholder text within this frame.
157    pub fn get_by_placeholder(&self, placeholder: impl Into<String>) -> FrameElementLocator<'a> {
158        FrameElementLocator::new(self.clone(), Selector::Placeholder(placeholder.into()))
159    }
160
161    /// Create a frame locator for a nested iframe within this frame.
162    ///
163    /// # Example
164    ///
165    /// ```ignore
166    /// // Access element in nested frame
167    /// page.frame_locator("#outer-frame")
168    ///     .frame_locator("#inner-frame")
169    ///     .locator("button")
170    ///     .click()
171    ///     .await?;
172    /// ```
173    pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator<'a> {
174        FrameLocator::with_parent(
175            self.page,
176            selector.into(),
177            self.parent_selectors.clone(),
178            self.frame_selector.clone(),
179        )
180    }
181
182    /// Get the frame selector.
183    pub fn selector(&self) -> &str {
184        &self.frame_selector
185    }
186
187    /// Get the parent selectors (for nested frames).
188    pub fn parent_selectors(&self) -> &[String] {
189        &self.parent_selectors
190    }
191
192    /// Build the JavaScript expression to access the frame's content document.
193    pub(crate) fn to_js_frame_access(&self) -> String {
194        let mut js = String::new();
195
196        // Start from the top-level document
197        js.push_str("(function() {\n");
198        js.push_str("  let doc = document;\n");
199
200        // Navigate through parent frames
201        for parent_selector in &self.parent_selectors {
202            js.push_str(&format!(
203                "  const parent = doc.querySelector({});\n",
204                super::locator::selector::js_string_literal(parent_selector)
205            ));
206            js.push_str("  if (!parent || !parent.contentDocument) return null;\n");
207            js.push_str("  doc = parent.contentDocument;\n");
208        }
209
210        // Access the final frame
211        js.push_str(&format!(
212            "  const frame = doc.querySelector({});\n",
213            super::locator::selector::js_string_literal(&self.frame_selector)
214        ));
215        js.push_str("  if (!frame || !frame.contentDocument) return null;\n");
216        js.push_str("  return frame.contentDocument;\n");
217        js.push_str("})()");
218        js
219    }
220}
221
222/// A locator for elements within a frame.
223///
224/// This combines a `FrameLocator` with an element `Selector` to locate
225/// elements inside an iframe.
226#[derive(Debug, Clone)]
227pub struct FrameElementLocator<'a> {
228    /// The frame locator.
229    frame_locator: FrameLocator<'a>,
230    /// The element selector within the frame.
231    selector: Selector,
232    /// Locator options.
233    options: LocatorOptions,
234}
235
236impl<'a> FrameElementLocator<'a> {
237    /// Create a new frame element locator.
238    fn new(frame_locator: FrameLocator<'a>, selector: Selector) -> Self {
239        Self {
240            frame_locator,
241            selector,
242            options: LocatorOptions::default(),
243        }
244    }
245
246    /// Set a custom timeout for this locator.
247    #[must_use]
248    pub fn timeout(mut self, timeout: Duration) -> Self {
249        self.options.timeout = timeout;
250        self
251    }
252
253    /// Create a child locator that further filters elements.
254    #[must_use]
255    pub fn locator(&self, selector: impl Into<String>) -> FrameElementLocator<'a> {
256        FrameElementLocator {
257            frame_locator: self.frame_locator.clone(),
258            selector: Selector::Chained(
259                Box::new(self.selector.clone()),
260                Box::new(Selector::Css(selector.into())),
261            ),
262            options: self.options.clone(),
263        }
264    }
265
266    /// Select the first matching element.
267    #[must_use]
268    pub fn first(&self) -> FrameElementLocator<'a> {
269        FrameElementLocator {
270            frame_locator: self.frame_locator.clone(),
271            selector: Selector::Nth {
272                base: Box::new(self.selector.clone()),
273                index: 0,
274            },
275            options: self.options.clone(),
276        }
277    }
278
279    /// Select the last matching element.
280    #[must_use]
281    pub fn last(&self) -> FrameElementLocator<'a> {
282        FrameElementLocator {
283            frame_locator: self.frame_locator.clone(),
284            selector: Selector::Nth {
285                base: Box::new(self.selector.clone()),
286                index: -1,
287            },
288            options: self.options.clone(),
289        }
290    }
291
292    /// Select the nth matching element (0-indexed).
293    #[must_use]
294    pub fn nth(&self, index: i32) -> FrameElementLocator<'a> {
295        FrameElementLocator {
296            frame_locator: self.frame_locator.clone(),
297            selector: Selector::Nth {
298                base: Box::new(self.selector.clone()),
299                index,
300            },
301            options: self.options.clone(),
302        }
303    }
304
305    /// Get the frame locator.
306    pub fn frame_locator(&self) -> &FrameLocator<'a> {
307        &self.frame_locator
308    }
309
310    /// Get the selector.
311    pub fn selector(&self) -> &Selector {
312        &self.selector
313    }
314
315    /// Get the locator options.
316    pub(crate) fn options(&self) -> &LocatorOptions {
317        &self.options
318    }
319
320    /// Build the JavaScript expression to query elements within the frame.
321    fn to_js_expression(&self) -> String {
322        let frame_access = self.frame_locator.to_js_frame_access();
323        let element_selector = self.selector.to_js_expression();
324
325        format!(
326            r"(function() {{
327                const frameDoc = {frame_access};
328                if (!frameDoc) return {{ found: false, count: 0, error: 'Frame not found or not accessible' }};
329                
330                // Override document for the selector expression
331                const originalDocument = document;
332                try {{
333                    // Create a modified expression that uses frameDoc instead of document
334                    const elements = (function() {{
335                        const document = frameDoc;
336                        return Array.from({element_selector});
337                    }})();
338                    return elements;
339                }} catch (e) {{
340                    return [];
341                }}
342            }})()"
343        )
344    }
345}
346
347/// Builder for role-based frame locators.
348#[derive(Debug)]
349pub struct FrameRoleLocatorBuilder<'a> {
350    frame_locator: FrameLocator<'a>,
351    role: AriaRole,
352    name: Option<String>,
353}
354
355impl<'a> FrameRoleLocatorBuilder<'a> {
356    fn new(frame_locator: FrameLocator<'a>, role: AriaRole) -> Self {
357        Self {
358            frame_locator,
359            role,
360            name: None,
361        }
362    }
363
364    /// Filter by accessible name.
365    #[must_use]
366    pub fn with_name(mut self, name: impl Into<String>) -> Self {
367        self.name = Some(name.into());
368        self
369    }
370
371    /// Build the locator.
372    pub fn build(self) -> FrameElementLocator<'a> {
373        FrameElementLocator::new(
374            self.frame_locator,
375            Selector::Role {
376                role: self.role,
377                name: self.name,
378            },
379        )
380    }
381}
382
383impl<'a> From<FrameRoleLocatorBuilder<'a>> for FrameElementLocator<'a> {
384    fn from(builder: FrameRoleLocatorBuilder<'a>) -> Self {
385        builder.build()
386    }
387}
388
389