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}