appium_client/find.rs
1//! Find API for locating elements on screen
2//!
3//! This API can be used to find elements on screen or in a known parent.
4//!
5//! ## Basic usage
6//! ```no_run
7//!# use appium_client::capabilities::android::AndroidCapabilities;
8//!# use appium_client::capabilities::{AppCapable, UdidCapable, UiAutomator2AppCompatible};
9//!# use appium_client::ClientBuilder;
10//!# use appium_client::find::{AppiumFind, By};
11//!#
12//!# #[tokio::main]
13//!# async fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! // create capabilities & client
15//! let mut capabilities = AndroidCapabilities::new_uiautomator();
16//!# capabilities.udid("emulator-5554");
17//!# capabilities.app("/apps/sample.apk");
18//!# capabilities.app_wait_activity("com.example.AppActivity");
19//!
20//! let client = ClientBuilder::native(capabilities)
21//! .connect("http://localhost:4723/wd/hub/")
22//! .await?;
23//!
24//! // locate an element (find it)
25//! let element = client
26//! .find_by(By::accessibility_id("Click this"))
27//! .await?;
28//!
29//! // locate all matching elements by given xpath
30//! let elements = client
31//! .find_all_by(By::xpath("//*[contains(@resource-id, 'test')]"))
32//! .await?;
33//! # Ok(())
34//! # }
35//! ```
36//!
37//! Notice that if you wish to get only one element (the first match), you can use [AppiumFind::find_by].
38//! If you want all matches on a screen, you use [AppiumFind::find_all_by].
39//!
40//! ## Custom locator strategy
41//!
42//! If none of the options available in [By] work with your driver, then you might use [By::custom_kind] to specify custom location strategy (and search query).
43//!
44//! Using [By::custom_kind] is basically the same as using any other [By] variant, but you need to write more and the compiler won't tell you that you made a typo.
45//!
46//! Some Appium docs on the matter of locators (selectors): <https://appium.github.io/appium.io/docs/en/writing-running-appium/finding-elements/>
47//!
48//! Example:
49//! ```no_run
50//!# use appium_client::capabilities::android::AndroidCapabilities;
51//!# use appium_client::capabilities::{AppCapable, UdidCapable, UiAutomator2AppCompatible};
52//!# use appium_client::ClientBuilder;
53//!# use appium_client::find::{AppiumFind, By};
54//!#
55//!# #[tokio::main]
56//!# async fn main() -> Result<(), Box<dyn std::error::Error>> {
57//!# let mut capabilities = AndroidCapabilities::new_uiautomator();
58//!# capabilities.udid("emulator-5554");
59//!# capabilities.app("/apps/sample.apk");
60//!# capabilities.app_wait_activity("com.example.AppActivity");
61//!#
62//!# let client = ClientBuilder::native(capabilities)
63//!# .connect("http://localhost:4723/wd/hub/")
64//!# .await?;
65//!#
66//! // locate an element (find it)
67//! let element = client
68//! .find_by(By::accessibility_id("Find me"))
69//! .await?;
70//!
71//! // do the same, but more explicitly
72//! let element = client
73//! .find_by(By::custom_kind("accessibility id", "Find me"))
74//! .await?;
75//! # Ok(())
76//! # }
77//! ```
78//!
79use std::collections::HashMap;
80use fantoccini::elements::{Element, ElementRef};
81use fantoccini::Client;
82use fantoccini::error::CmdError;
83use serde::Serializer;
84use serde_derive::Serialize;
85use crate::commands::AppiumCommand;
86use async_trait::async_trait;
87
88/// Locators supported by Appium
89///
90/// If you wish to use your very own locator (e.g. something I didn't implement in this enum),
91/// just use [By::CustomKind].
92#[derive(Debug, PartialEq, Clone)]
93pub enum By {
94 Id(String),
95 Name(String),
96 Xpath(String),
97 UiAutomator(String),
98 AndroidDataMatcher(String),
99 AndroidViewMatcher(String),
100 AndroidViewTag(String),
101 IosClassChain(String),
102 IosNsPredicate(String),
103 AccessibilityId(String),
104 ClassName(String),
105 Image(String),
106 Custom(String),
107 CustomKind(String, String)
108}
109
110#[derive(Debug, PartialEq, Serialize, Clone)]
111pub struct LocatorParameters {
112 pub using: String,
113 pub value: String,
114}
115
116impl By {
117 /// Native element identifier. resource-id for android; name for iOS.
118 pub fn id(id: &str) -> By {
119 By::Id(id.to_string())
120 }
121
122 /// Name of element.
123 pub fn name(name: &str) -> By {
124 By::Name(name.to_string())
125 }
126
127 /// Search the app XML source using xpath (not recommended, has performance issues).
128 pub fn xpath(query: &str) -> By {
129 By::Xpath(query.to_string())
130 }
131
132 /// Use the UI Automator API, in particular the UiSelector class to locate elements. (UiAutomator2 only).
133 ///
134 /// In Appium you send the Java code, as a string, to the server, which executes it in the application’s environment,
135 /// returning the element or elements.
136 ///
137 /// See <https://developer.android.com/reference/androidx/test/uiautomator/UiSelector>
138 pub fn uiautomator(query: &str) -> By {
139 By::UiAutomator(query.to_string())
140 }
141
142 /// Locate an element using Espresso [DataMatcher](https://developer.android.com/reference/android/support/test/espresso/Espresso#ondata). (Espresso only)
143 pub fn android_data_matcher(query: &str) -> By {
144 By::AndroidDataMatcher(query.to_string())
145 }
146
147 /// Locate an element using Espresso [ViewMatcher](https://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers). (Espresso only)
148 pub fn android_view_matcher(query: &str) -> By {
149 By::AndroidViewMatcher(query.to_string())
150 }
151
152 /// Locate an element by its [view tag](https://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers.html#withTagValue%28org.hamcrest.Matcher%3Cjava.lang.Object%3E). (Espresso only)
153 pub fn android_view_tag(query: &str) -> By {
154 By::AndroidViewTag(query.to_string())
155 }
156
157 /// Locate an element by a [class chain](https://pavankovurru.github.io/Appium_Mobile_Automation_Framework/documents/README_IOS.html#ios-class-chain-strategy) - a faster, but less powerful alternative to XPath on iOS.
158 pub fn ios_class_chain(query: &str) -> By {
159 By::IosClassChain(query.to_string())
160 }
161
162 /// A string corresponding to a recursive element search using the [iOS Predicate](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/ios/ios-predicate.md). (iOS 10.0 and above)
163 pub fn ios_ns_predicate(query: &str) -> By {
164 By::IosNsPredicate(query.to_string())
165 }
166
167 /// Read a unique identifier for a UI element.
168 ///
169 /// For XCUITest it is the element's accessibility-id attribute. For Android it is the element's content-desc attribute.
170 pub fn accessibility_id(id: &str) -> By {
171 By::AccessibilityId(id.to_string())
172 }
173
174 /// Locate element by its class name.
175 ///
176 /// For IOS it is the full name of the XCUI element and begins with XCUIElementType.
177 /// For Android it is the full name of the UIAutomator2 class (e.g.: android.widget.TextView)
178 pub fn class_name(class_name: &str) -> By {
179 By::ClassName(class_name.to_string())
180 }
181
182 /// Locate an element by matching it with a base 64 encoded image file
183 pub fn image(base64_template: &str) -> By {
184 By::Image(base64_template.to_string())
185 }
186
187 /// Custom locator for use with plugins registered via the customFindModules capability.
188 pub fn custom(query: &str) -> By {
189 By::Custom(query.to_string())
190 }
191
192 /// A locator for non-standard locators
193 ///
194 /// You can define what type of locator to use, so you're free to use anything here.
195 pub fn custom_kind(using: &str, value: &str) -> By {
196 By::CustomKind(using.to_string(), value.to_string())
197 }
198}
199
200impl From<By> for LocatorParameters {
201 fn from(val: By) -> Self {
202 let (using, value) = match val {
203 By::Id(value) => ("id".to_string(), value),
204 By::Name(value) => ("name".to_string(), value),
205 By::Xpath(value) => ("xpath".to_string(), value),
206 By::UiAutomator(value) => ("-android uiautomator".to_string(), value),
207 By::AndroidDataMatcher(value) => ("-android datamatcher".to_string(), value),
208 By::AndroidViewMatcher(value) => ("-android viewmatcher".to_string(), value),
209 By::AndroidViewTag(value) => ("-android viewtag".to_string(), value),
210 By::IosClassChain(value) => ("-ios class chain".to_string(), value),
211 By::IosNsPredicate(value) => ("-ios predicate string".to_string(), value),
212 By::AccessibilityId(value) => ("accessibility id".to_string(), value),
213 By::Image(value) => ("-image".to_string(), value),
214 By::ClassName(value) => ("class name".to_string(), value),
215 By::Custom(value) => ("-custom".to_string(), value),
216 By::CustomKind(kind, value) => (kind, value)
217 };
218
219 LocatorParameters {
220 using,
221 value
222 }
223 }
224}
225
226impl serde::Serialize for By {
227 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
228 let locator_params: LocatorParameters = self.clone().into();
229 locator_params.serialize(serializer)
230 }
231}
232
233#[async_trait]
234pub trait AppiumFind {
235 /// Locates an element by given strategy.
236 async fn find_by(&self, search: By) -> Result<Element, CmdError>;
237
238 /// Locates all elements matching criteria.
239 async fn find_all_by(&self, search: By) -> Result<Vec<Element>, CmdError>;
240}
241
242#[async_trait]
243impl AppiumFind for Client {
244 async fn find_by(&self, search: By) -> Result<Element, CmdError> {
245 let value = self.issue_cmd(AppiumCommand::FindElement(search)).await?;
246 let map: HashMap<String, String> = serde_json::from_value(value.clone())?;
247
248 map.get("ELEMENT")
249 .ok_or_else(|| CmdError::NotW3C(value))
250 .map(|element| Element::from_element_id(
251 self.clone(),
252 ElementRef::from(element.clone())
253 ))
254 }
255
256 async fn find_all_by(&self, search: By) -> Result<Vec<Element>, CmdError> {
257 let value = self.issue_cmd(AppiumCommand::FindElements(search)).await?;
258 let result: Vec<HashMap<String, String>> = serde_json::from_value(value)?;
259
260 let elements = result.into_iter()
261 .filter_map(|map| map.get("ELEMENT").cloned())
262 .map(|element| Element::from_element_id(
263 self.clone(),
264 ElementRef::from(element)
265 ))
266 .collect();
267
268 Ok(elements)
269 }
270}
271
272#[async_trait]
273impl AppiumFind for Element {
274 async fn find_by(&self, search: By) -> Result<Element, CmdError> {
275 let client = self.clone().client();
276 let element_ref = self.element_id();
277 let value = client.issue_cmd(AppiumCommand::FindElementWithContext(search, element_ref.to_string())).await?;
278 let map: HashMap<String, String> = serde_json::from_value(value.clone())?;
279
280 map.get("ELEMENT")
281 .ok_or_else(|| CmdError::NotW3C(value))
282 .map(|element| Element::from_element_id(
283 client,
284 ElementRef::from(element.clone())
285 ))
286 }
287
288 async fn find_all_by(&self, search: By) -> Result<Vec<Element>, CmdError> {
289 let client = self.clone().client();
290 let element_ref = self.element_id();
291 let value = client.issue_cmd(AppiumCommand::FindElementsWithContext(search, element_ref.to_string())).await?;
292 let result: Vec<HashMap<String, String>> = serde_json::from_value(value)?;
293
294 let elements = result.into_iter()
295 .filter_map(|map| map.get("ELEMENT").cloned())
296 .map(|element| Element::from_element_id(
297 client.clone(),
298 ElementRef::from(element)
299 ))
300 .collect();
301
302 Ok(elements)
303 }
304}