1use super::types::{Locator, Tab};
2use serde::Serialize;
3use serde_json::json;
4
5#[derive(Clone, Debug, Serialize)]
6#[serde(untagged)]
7pub enum TextMatcher {
8 Text(String),
9 Regex { regex: String, flags: String },
10}
11impl From<&str> for TextMatcher {
12 fn from(value: &str) -> Self {
13 Self::Text(value.into())
14 }
15}
16impl From<String> for TextMatcher {
17 fn from(value: String) -> Self {
18 Self::Text(value)
19 }
20}
21#[derive(Clone, Debug, Default, Serialize)]
22pub struct TextOptions {
23 pub exact: bool,
24}
25#[derive(Clone, Debug, Default, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct RoleOptions {
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub name: Option<TextMatcher>,
30 pub exact: bool,
31 pub include_hidden: bool,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub checked: Option<bool>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub disabled: Option<bool>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub expanded: Option<bool>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub level: Option<u32>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub pressed: Option<bool>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub selected: Option<bool>,
44}
45#[derive(Clone, Default)]
46pub struct LocatorFilterOptions {
47 pub has: Option<Locator>,
48 pub has_not: Option<Locator>,
49 pub has_text: Option<TextMatcher>,
50 pub has_not_text: Option<TextMatcher>,
51 pub visible: Option<bool>,
52}
53fn semantic_selector(spec: serde_json::Value) -> String {
54 format!(
55 "aw={}",
56 serde_json::to_string(&spec.to_string()).expect("JSON string")
57 )
58}
59impl Tab {
60 pub fn get_by_role(&self, role: impl Into<String>) -> Locator {
61 self.get_by_role_with_options(role, RoleOptions::default())
62 }
63 pub fn get_by_role_with_options(
64 &self,
65 role: impl Into<String>,
66 options: RoleOptions,
67 ) -> Locator {
68 assert!(options.level != Some(0), "Role level must be positive");
69 let mut spec = serde_json::to_value(options).expect("role options");
70 spec["kind"] = json!("role");
71 spec["role"] = json!(role.into());
72 self.locator(semantic_selector(spec))
73 }
74 pub fn get_by_text(&self, text: impl Into<TextMatcher>) -> Locator {
75 self.get_by_text_with_options(text, TextOptions::default())
76 }
77 pub fn get_by_text_with_options(
78 &self,
79 text: impl Into<TextMatcher>,
80 options: TextOptions,
81 ) -> Locator {
82 self.locator(semantic_selector(
83 json!({"kind":"text","text":text.into(),"exact":options.exact}),
84 ))
85 }
86 pub fn get_by_label(&self, text: impl Into<TextMatcher>) -> Locator {
87 self.get_by_label_with_options(text, TextOptions::default())
88 }
89 pub fn get_by_label_with_options(
90 &self,
91 text: impl Into<TextMatcher>,
92 options: TextOptions,
93 ) -> Locator {
94 self.locator(semantic_selector(
95 json!({"kind":"label","text":text.into(),"exact":options.exact}),
96 ))
97 }
98 pub fn get_by_placeholder(&self, text: impl Into<TextMatcher>) -> Locator {
99 self.get_by_placeholder_with_options(text, TextOptions::default())
100 }
101 pub fn get_by_placeholder_with_options(
102 &self,
103 text: impl Into<TextMatcher>,
104 options: TextOptions,
105 ) -> Locator {
106 self.locator(semantic_selector(
107 json!({"kind":"placeholder","text":text.into(),"exact":options.exact}),
108 ))
109 }
110 pub fn get_by_alt_text(&self, text: impl Into<TextMatcher>) -> Locator {
111 self.get_by_alt_text_with_options(text, TextOptions::default())
112 }
113 pub fn get_by_alt_text_with_options(
114 &self,
115 text: impl Into<TextMatcher>,
116 options: TextOptions,
117 ) -> Locator {
118 self.locator(semantic_selector(
119 json!({"kind":"altText","text":text.into(),"exact":options.exact}),
120 ))
121 }
122 pub fn get_by_title(&self, text: impl Into<TextMatcher>) -> Locator {
123 self.get_by_title_with_options(text, TextOptions::default())
124 }
125 pub fn get_by_title_with_options(
126 &self,
127 text: impl Into<TextMatcher>,
128 options: TextOptions,
129 ) -> Locator {
130 self.locator(semantic_selector(
131 json!({"kind":"title","text":text.into(),"exact":options.exact}),
132 ))
133 }
134 pub fn get_by_test_id(&self, text: impl Into<TextMatcher>) -> Locator {
135 self.get_by_test_id_with_options(text, TextOptions::default())
136 }
137 pub fn get_by_test_id_with_options(
138 &self,
139 text: impl Into<TextMatcher>,
140 options: TextOptions,
141 ) -> Locator {
142 self.locator(semantic_selector(
143 json!({"kind":"testId","text":text.into(),"exact":options.exact}),
144 ))
145 }
146}
147impl Locator {
148 pub fn get_by_role(&self, role: impl Into<String>) -> Locator {
149 self.get_by_role_with_options(role, RoleOptions::default())
150 }
151 pub fn get_by_role_with_options(
152 &self,
153 role: impl Into<String>,
154 options: RoleOptions,
155 ) -> Locator {
156 assert!(options.level != Some(0), "Role level must be positive");
157 let mut spec = serde_json::to_value(options).expect("role options");
158 spec["kind"] = json!("role");
159 spec["role"] = json!(role.into());
160 self.locator(semantic_selector(spec))
161 }
162 pub fn get_by_text(&self, text: impl Into<TextMatcher>) -> Locator {
163 self.get_by_text_with_options(text, TextOptions::default())
164 }
165 pub fn get_by_text_with_options(
166 &self,
167 text: impl Into<TextMatcher>,
168 options: TextOptions,
169 ) -> Locator {
170 self.locator(semantic_selector(
171 json!({"kind":"text","text":text.into(),"exact":options.exact}),
172 ))
173 }
174 pub fn get_by_label(&self, text: impl Into<TextMatcher>) -> Locator {
175 self.get_by_label_with_options(text, TextOptions::default())
176 }
177 pub fn get_by_label_with_options(
178 &self,
179 text: impl Into<TextMatcher>,
180 options: TextOptions,
181 ) -> Locator {
182 self.locator(semantic_selector(
183 json!({"kind":"label","text":text.into(),"exact":options.exact}),
184 ))
185 }
186 pub fn get_by_placeholder(&self, text: impl Into<TextMatcher>) -> Locator {
187 self.get_by_placeholder_with_options(text, TextOptions::default())
188 }
189 pub fn get_by_placeholder_with_options(
190 &self,
191 text: impl Into<TextMatcher>,
192 options: TextOptions,
193 ) -> Locator {
194 self.locator(semantic_selector(
195 json!({"kind":"placeholder","text":text.into(),"exact":options.exact}),
196 ))
197 }
198 pub fn get_by_alt_text(&self, text: impl Into<TextMatcher>) -> Locator {
199 self.get_by_alt_text_with_options(text, TextOptions::default())
200 }
201 pub fn get_by_alt_text_with_options(
202 &self,
203 text: impl Into<TextMatcher>,
204 options: TextOptions,
205 ) -> Locator {
206 self.locator(semantic_selector(
207 json!({"kind":"altText","text":text.into(),"exact":options.exact}),
208 ))
209 }
210 pub fn get_by_title(&self, text: impl Into<TextMatcher>) -> Locator {
211 self.get_by_title_with_options(text, TextOptions::default())
212 }
213 pub fn get_by_title_with_options(
214 &self,
215 text: impl Into<TextMatcher>,
216 options: TextOptions,
217 ) -> Locator {
218 self.locator(semantic_selector(
219 json!({"kind":"title","text":text.into(),"exact":options.exact}),
220 ))
221 }
222 pub fn get_by_test_id(&self, text: impl Into<TextMatcher>) -> Locator {
223 self.get_by_test_id_with_options(text, TextOptions::default())
224 }
225 pub fn get_by_test_id_with_options(
226 &self,
227 text: impl Into<TextMatcher>,
228 options: TextOptions,
229 ) -> Locator {
230 self.locator(semantic_selector(
231 json!({"kind":"testId","text":text.into(),"exact":options.exact}),
232 ))
233 }
234}
235impl Locator {
236 pub fn not(&self, other: &Locator) -> Locator {
237 assert!(
238 std::sync::Arc::ptr_eq(&self.page.inner, &other.page.inner),
239 "Excluded locators must belong to the same page"
240 );
241 self.locator(semantic_selector(
242 json!({"kind":"exclude", "selector":other.selector}),
243 ))
244 }
245
246 pub fn filter(&self, options: LocatorFilterOptions) -> Locator {
247 let mut spec = json!({"kind":"filter"});
248 for (key, inner) in [("has", options.has), ("hasNot", options.has_not)] {
249 if let Some(inner) = inner {
250 assert!(
251 std::sync::Arc::ptr_eq(&self.page.inner, &inner.page.inner),
252 "Filter locators must belong to the same page"
253 );
254 spec[key] = json!(inner.selector);
255 }
256 }
257 if let Some(text) = options.has_text {
258 spec["hasText"] = json!(text);
259 }
260 if let Some(text) = options.has_not_text {
261 spec["hasNotText"] = json!(text);
262 }
263 if let Some(visible) = options.visible {
264 spec["visible"] = json!(visible);
265 }
266 self.locator(semantic_selector(spec))
267 }
268 pub fn nth(&self, index: i32) -> Locator {
269 self.locator(semantic_selector(json!({"kind":"nth", "index":index})))
270 }
271 pub fn first(&self) -> Locator {
272 self.nth(0)
273 }
274 pub fn last(&self) -> Locator {
275 self.nth(-1)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::super::selectors::{chain_selector_for_transport, normalize_selector_for_transport};
282 use super::*;
283
284 #[test]
285 fn quoted_regex_and_false_options_survive_chaining() {
286 let options = RoleOptions {
287 name: Some(TextMatcher::Regex {
288 regex: "^Save \"now\"$".into(),
289 flags: "i".into(),
290 }),
291 pressed: Some(false),
292 ..Default::default()
293 };
294 let mut spec = serde_json::to_value(options).unwrap();
295 assert_eq!(spec["pressed"], false);
296 assert!(spec.get("checked").is_none());
297 spec["kind"] = json!("role");
298 spec["role"] = json!("button");
299 let selector = semantic_selector(spec);
300 let chained = chain_selector_for_transport("article", &selector);
301 let chained = chain_selector_for_transport(&chained, "xpath=//span");
302 assert_eq!(normalize_selector_for_transport(&chained), chained);
303 assert!(chained.starts_with("css=\"article\" aw="));
304 assert!(chained.ends_with("xpath=\"//span\""));
305 }
306}