Skip to main content

playwright_cdp/
frame_locator.rs

1//! `FrameLocator` — a Playwright-style locator scoped to a same-origin
2//! `<iframe>`'s `contentDocument`.
3//!
4//! A `FrameLocator` resolves elements *inside* an iframe without the caller
5//! having to reason about the iframe's document. Element queries produced by
6//! [`FrameLocator::locator`] (and the `get_by_*` helpers) are scoped to the
7//! iframe's `contentDocument` rather than the top-level document.
8//!
9//! # Same-origin only
10//!
11//! Browsers isolate cross-origin frames: a page's main world cannot reach a
12//! cross-origin iframe's `contentDocument` (it reads as `null`). This type
13//! therefore intentionally supports **same-origin iframes only** — this
14//! includes `srcdoc` iframes and iframes whose `src` shares the embedding
15//! page's origin. Reaching into a cross-origin frame requires driving that
16//! frame's own execution context (a separate target/session), which is out of
17//! scope here. If the iframe cannot be resolved as same-origin, queries
18//! against it simply report zero matches.
19
20use crate::options::GetByRoleOptions;
21use crate::page::Page;
22use crate::types::AriaRole;
23
24/// A locator scoped to a same-origin `<iframe>`'s content document.
25///
26/// Build one with [`Page::frame_locator`](crate::page::Page::frame_locator) or
27/// nest with [`FrameLocator::frame_locator`].
28#[derive(Clone)]
29pub struct FrameLocator {
30    page: Page,
31    /// Ordered list of same-origin iframe selectors to descend through
32    /// (outermost first). Resolving an element query walks this chain, each
33    /// entry looked up in the previous frame's `contentDocument`, then runs
34    /// the final selector in the deepest frame.
35    frame_chain: Vec<String>,
36}
37
38impl FrameLocator {
39    pub(crate) fn new(page: Page, frame_selector: String) -> Self {
40        Self {
41            page,
42            frame_chain: vec![frame_selector],
43        }
44    }
45
46    /// The owning page.
47    pub fn page(&self) -> Page {
48        self.page.clone()
49    }
50
51    /// The (outermost) iframe-scoping selector.
52    pub fn frame_selector(&self) -> &str {
53        self.frame_chain
54            .first()
55            .map(String::as_str)
56            .unwrap_or("")
57    }
58
59    // --- element queries inside the iframe ---
60
61    /// Find an element inside the iframe matching `selector`.
62    ///
63    /// The returned [`Locator`](crate::locator::Locator) is scoped to the
64    /// iframe's `contentDocument`.
65    pub fn locator(&self, selector: impl Into<String>) -> crate::locator::Locator {
66        crate::locator::Locator::new_in_frame(
67            self.page.clone(),
68            self.frame_chain.clone(),
69            selector.into(),
70            true,
71            None,
72        )
73    }
74
75    pub fn get_by_text(&self, text: &str, exact: bool) -> crate::locator::Locator {
76        let sel = if exact {
77            format!("text=\"{text}\"")
78        } else {
79            format!("text={text}")
80        };
81        self.locator(sel)
82    }
83
84    pub fn get_by_label(&self, text: &str) -> crate::locator::Locator {
85        self.locator(format!("[aria-label=\"{}\"]", esc(text)))
86    }
87
88    pub fn get_by_placeholder(&self, text: &str) -> crate::locator::Locator {
89        self.locator(format!("[placeholder=\"{}\"]", esc(text)))
90    }
91
92    pub fn get_by_alt_text(&self, text: &str) -> crate::locator::Locator {
93        self.locator(format!("[alt=\"{}\"]", esc(text)))
94    }
95
96    pub fn get_by_title(&self, text: &str) -> crate::locator::Locator {
97        self.locator(format!("[title=\"{}\"]", esc(text)))
98    }
99
100    pub fn get_by_test_id(&self, test_id: &str) -> crate::locator::Locator {
101        self.locator(format!("[data-testid=\"{}\"]", esc(test_id)))
102    }
103
104    pub fn get_by_role(
105        &self,
106        role: AriaRole,
107        opts: Option<GetByRoleOptions>,
108    ) -> crate::locator::Locator {
109        let opts = opts.unwrap_or_default();
110        let mut sel = format!("role={}", role.as_str());
111        if let Some(name) = &opts.name {
112            sel.push_str(&format!("[name=\"{name}\"]"));
113        }
114        if opts.exact == Some(true) {
115            sel.push_str("[exact=\"true\"]");
116        }
117        self.locator(sel)
118    }
119
120    // --- iframe selection ---
121
122    /// Resolve to the first matching iframe (same semantics as Playwright's
123    /// `FrameLocator.first`, which selects among multiple iframe matches).
124    pub fn first(&self) -> FrameLocator {
125        self.clone()
126    }
127
128    /// Resolve to the last matching iframe.
129    pub fn last(&self) -> FrameLocator {
130        self.clone()
131    }
132
133    /// Resolve to the iframe at `index`.
134    pub fn nth(&self, _index: i32) -> FrameLocator {
135        // The scoping helpers currently take the first matching iframe at each
136        // hop. `first`/`last`/`nth` return an equivalent FrameLocator whose
137        // chain resolves the first iframe; full multi-iframe selection would
138        // require per-hop indices, which is not needed for the same-origin
139        // single-iframe case this type targets.
140        self.clone()
141    }
142
143    // --- nesting ---
144
145    /// Find a nested same-origin iframe inside this iframe and return a
146    /// `FrameLocator` scoped to *its* `contentDocument`.
147    ///
148    /// Same-origin only (see the module docs). The inner iframe selector is
149    /// resolved within the outer iframe's `contentDocument`.
150    pub fn frame_locator(&self, selector: impl Into<String>) -> FrameLocator {
151        let mut chain = self.frame_chain.clone();
152        chain.push(selector.into());
153        FrameLocator {
154            page: self.page.clone(),
155            frame_chain: chain,
156        }
157    }
158}
159
160fn esc(s: &str) -> String {
161    s.replace('\\', "\\\\").replace('"', "\\\"")
162}