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