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}