Skip to main content

allwright/
client_web_locators.rs

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}