appium_client/
lib.rs

1//! Rust client for Appium Server, for automated mobile app testing
2//!
3//! It is based on [fantoccini](https://github.com/jonhoo/fantoccini) and retains all capabilities
4//! of fantoccini's client, such as screenshotting, touch actions, getting page source etc.
5//!
6//! Please note that this is only a client of [Appium](https://appium.io/docs/en/2.1/), so also check out [Appium docs](https://appium.io/docs/en/2.1/).
7//!
8//! ## Creating a client
9//! To create a client, you need [capabilities] for the Appium session.
10//! Capabilities describe what device you use and they will determine what features are available to you.
11//!
12//! After creating a desired set of capabilities, use [ClientBuilder] to create a client.
13//! And you also need a running Appium server, see Appium docs for how to set up one (<https://appium.io/docs/en/2.1/quickstart/>).
14//!
15//! Creating an iOS capabilities and client:
16//! ```no_run
17//! use appium_client::capabilities::{AppCapable, UdidCapable};
18//! use appium_client::capabilities::ios::IOSCapabilities;
19//! use appium_client::ClientBuilder;
20//! use appium_client::commands::ios::ShakesDevice;
21//!
22//!# #[tokio::main]
23//!# async fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! let mut capabilities = IOSCapabilities::new_xcui();
25//! capabilities.udid("000011114567899");
26//! capabilities.app("/apps/sample.ipa");
27//!
28//! let client = ClientBuilder::native(capabilities)
29//!    .connect("http://localhost:4723/wd/hub/")
30//!    .await?;
31//!
32//! // now you can use the client to issue commands and find elements on screen
33//! # Ok(())
34//! # }
35//! ```
36//!
37//! ## Finding elements
38//! This appium-client adds support for Appium locators such as iOS Class Chain, or UiAutomator.
39//! See [find] for more info on Appium locators.
40//!
41//! Basic usage:
42//! ```no_run
43//! use appium_client::capabilities::android::AndroidCapabilities;
44//!# use appium_client::capabilities::{AppCapable, UdidCapable, UiAutomator2AppCompatible};
45//! use appium_client::ClientBuilder;
46//! use appium_client::find::{AppiumFind, By};
47//!
48//!# #[tokio::main]
49//!# async fn main() -> Result<(), Box<dyn std::error::Error>> {
50//!# // create capabilities
51//! let mut capabilities = AndroidCapabilities::new_uiautomator();
52//!# capabilities.udid("emulator-5554");
53//!# capabilities.app("/apps/sample.apk");
54//!# capabilities.app_wait_activity("com.example.AppActivity");
55//!#
56//! // create the client
57//! let client = ClientBuilder::native(capabilities)
58//!     .connect("http://localhost:4723/wd/hub/")
59//!     .await?;
60//!
61//! // locate an element (find it)
62//! let element = client
63//!     .find_by(By::accessibility_id("Click this"))
64//!     .await?;
65//!
66//! element.click().await?;
67//! # Ok(())
68//! # }
69//! ```
70//!
71//! ## Wait for an element to appear
72//! Appium locators can be also waited on (just like you can wait for element with fantoccini),
73//! see [wait] to learn how to wait.
74//!
75//! Basic usage:
76//! ```no_run
77//! use appium_client::capabilities::android::AndroidCapabilities;
78//!# use appium_client::capabilities::{AppCapable, UdidCapable, UiAutomator2AppCompatible};
79//! use appium_client::ClientBuilder;
80//! use appium_client::find::{AppiumFind, By};
81//!
82//!# #[tokio::main]
83//!# async fn main() -> Result<(), Box<dyn std::error::Error>> {
84//!# // create capabilities
85//!# use appium_client::wait::AppiumWait;
86//! let mut capabilities = AndroidCapabilities::new_uiautomator();
87//!# capabilities.udid("emulator-5554");
88//!# capabilities.app("/apps/sample.apk");
89//!# capabilities.app_wait_activity("com.example.AppActivity");
90//!#
91//! // create the client
92//! let client = ClientBuilder::native(capabilities)
93//!     .connect("http://localhost:4723/wd/hub/")
94//!     .await?;
95//!
96//! // wait until element appears
97//! let element = client
98//!     .appium_wait()
99//!     .for_element(By::accessibility_id("Click this"))
100//!     .await?;
101//!
102//! element.click().await?;
103//! # Ok(())
104//! # }
105//! ```
106//!
107//! ## Commands
108//! If you want to rotate the emulator's screen, or send keys, or do some other things supported by Appium,
109//! then you can use features implemented in [commands] module.
110//!
111//! Those commands should be available to you depending whether you created [AndroidCapabilities] or [IOSCapabilities].
112//!
113//! If you wish to issue a custom command (not implemented by this lib), then use `issue_command(Custom)`.
114//!
115//! ```no_run
116//! use http::Method;
117//! use serde_json::json;
118//! use appium_client::capabilities::android::AndroidCapabilities;
119//! use appium_client::capabilities::{AppCapable, UdidCapable, UiAutomator2AppCompatible};
120//! use appium_client::ClientBuilder;
121//! use appium_client::commands::AppiumCommand;
122//! use appium_client::commands::keyboard::HidesKeyboard;
123//! use appium_client::find::{AppiumFind, By};
124//!
125//!# #[tokio::main]
126//!# async fn main() -> Result<(), Box<dyn std::error::Error>> {
127//! // create capabilities
128//! let mut capabilities = AndroidCapabilities::new_uiautomator();
129//! capabilities.udid("emulator-5554");
130//! capabilities.app("/apps/sample.apk");
131//! capabilities.app_wait_activity("com.example.AppActivity");
132//!
133//! let client = ClientBuilder::native(capabilities)
134//!    .connect("http://localhost:4723/wd/hub/")
135//!    .await?;
136//!
137//! // this feature is implemented in commands by this lib
138//! client.hide_keyboard().await?;
139//!
140//! // use some quirky feature of Appium (not implemented in commands module)
141//! // you can issue_cmd if you see that I didn't implement something
142//! client.issue_cmd(AppiumCommand::Custom(
143//!     Method::POST,
144//!     "quirky_feature".to_string(),
145//!     Some(json!({
146//!         "tap": "everywhere"
147//!     }))
148//! )).await?;
149//!
150//!#     Ok(())
151//!# }
152//! ```
153//!
154//! ## More documentation
155//!
156//! See the [readme](https://github.com/multicatch/appium-client/blob/master/README.md) or [examples](https://github.com/multicatch/appium-client/tree/master/examples)
157//! to learn how to use this library.
158
159use std::marker::PhantomData;
160use std::ops::{Deref, DerefMut};
161use std::sync::Arc;
162use fantoccini::error;
163use http::Method;
164use hyper::client::connect;
165use log::error;
166use tokio::spawn;
167use crate::capabilities::android::AndroidCapabilities;
168use crate::capabilities::AppiumCapability;
169use crate::capabilities::ios::IOSCapabilities;
170use crate::commands::AppiumCommand;
171
172pub mod capabilities;
173pub mod commands;
174pub mod find;
175pub mod wait;
176
177/// Client builder
178///
179/// Use this struct to build Appium client.
180/// This struct has methods that will guide you through all necessary things needed to construct a client.
181///
182/// Do not create an instance of [Client] yourself, use this builder.
183pub struct ClientBuilder<C, Caps>
184    where
185        C: connect::Connect + Send + Sync + Clone + Unpin,
186        Caps: AppiumCapability
187{
188    fantoccini_builder: fantoccini::ClientBuilder<C>,
189    caps: PhantomData<Caps>,
190}
191
192#[cfg(feature = "native-tls")]
193impl<Caps> ClientBuilder<hyper_tls::HttpsConnector<hyper::client::HttpConnector>, Caps>
194    where Caps: AppiumCapability
195{
196    pub fn native(capabilities: Caps) -> ClientBuilder<hyper_tls::HttpsConnector<hyper::client::HttpConnector>, Caps> {
197        ClientBuilder::new(fantoccini::ClientBuilder::native(), capabilities)
198    }
199}
200
201#[cfg(feature = "rustls-tls")]
202impl<Caps> ClientBuilder<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>, Caps>
203    where Caps: AppiumCapability
204{
205    pub fn rustls(capabilities: Caps) -> ClientBuilder<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>, Caps> {
206        ClientBuilder::new(fantoccini::ClientBuilder::rustls(), capabilities)
207    }
208}
209
210impl<C, Caps> ClientBuilder<C, Caps>
211    where
212        C: connect::Connect + Send + Sync + Clone + Unpin + 'static,
213        Caps: AppiumCapability
214{
215    pub fn new(mut builder: fantoccini::ClientBuilder<C>, capabilities: Caps) -> ClientBuilder<C, Caps> {
216        builder.capabilities(capabilities.clone());
217
218        ClientBuilder {
219            fantoccini_builder: builder,
220            caps: PhantomData,
221        }
222    }
223
224    pub async fn connect(&self, webdriver: &str) -> Result<Client<Caps>, error::NewSessionError> {
225        let inner = self.fantoccini_builder.connect(webdriver).await?;
226        Ok(Client {
227            inner,
228            caps: PhantomData,
229        })
230    }
231}
232
233/// Generic Appium client
234///
235/// This client represents an Appium client that will connect to an Appium server
236/// and send command to said server.
237///
238/// Depending on chosen capabilities ([AppiumCapability]), the client will have different traits.
239/// Which means - different available features.
240///
241/// Check out [AndroidClient] and [IOSClient] in docs to see their features (available commands).
242///
243/// **Note**: [Client] automatically ends Appium session on drop (end of lifetime). This is the only way to end session.
244pub struct Client<Caps>
245    where Caps: AppiumCapability {
246    inner: fantoccini::Client,
247    caps: PhantomData<Caps>,
248}
249
250pub trait AppiumClientTrait: DerefMut<Target=fantoccini::Client> {}
251
252/// Client used to automate Android testing
253///
254/// To create [AndroidClient], you need to use [ClientBuilder] and [AndroidCapabilities].
255/// Rust type system will automatically pick up that by using those capabilities, you mean to control an Android device.
256///
257/// See trait implementations to check available features (commands) of this client.
258///
259/// ```no_run
260/// use appium_client::capabilities::{AppCapable, UdidCapable, UiAutomator2AppCompatible};
261/// use appium_client::capabilities::android::AndroidCapabilities;
262/// use appium_client::ClientBuilder;
263///
264///# #[tokio::main]
265///# async fn main() -> Result<(), Box<dyn std::error::Error>> {
266/// let mut capabilities = AndroidCapabilities::new_uiautomator();
267/// capabilities.udid("emulator-5554");
268/// capabilities.app("/apps/sample.apk");
269/// capabilities.app_wait_activity("com.example.AppActivity");
270///
271/// let client = ClientBuilder::native(capabilities)
272///    .connect("http://localhost:4723/wd/hub/")
273///    .await?;
274///
275/// // congratulations, you have successfully created an AndroidClient
276/// # Ok(())
277/// # }
278/// ```
279pub type AndroidClient = Client<AndroidCapabilities>;
280
281/// Client used to automate iOS testing
282///
283/// To create [IOSClient], you need to use [ClientBuilder] and [IOSCapabilities].
284/// Rust type system will automatically pick up that by using those capabilities, you mean to control an iOS device.
285///
286/// See trait implementations to check available features (commands) of this client.
287///
288/// ```no_run
289/// use appium_client::capabilities::{AppCapable, UdidCapable};
290/// use appium_client::capabilities::ios::IOSCapabilities;
291/// use appium_client::ClientBuilder;
292///
293///# #[tokio::main]
294///# async fn main() -> Result<(), Box<dyn std::error::Error>> {
295/// let mut capabilities = IOSCapabilities::new_xcui();
296/// capabilities.udid("000011114567899");
297/// capabilities.app("/apps/sample.ipa");
298///
299/// let client = ClientBuilder::native(capabilities)
300///    .connect("http://localhost:4723/wd/hub/")
301///    .await?;
302///
303/// // congratulations, you have successfully created an IOSClient
304/// # Ok(())
305/// # }
306/// ```
307pub type IOSClient = Client<IOSCapabilities>;
308
309impl<Caps> AppiumClientTrait for Client<Caps>
310    where Caps: AppiumCapability {}
311
312impl<Caps> Deref for Client<Caps>
313    where Caps: AppiumCapability
314{
315    type Target = fantoccini::Client;
316
317    fn deref(&self) -> &Self::Target {
318        &self.inner
319    }
320}
321
322impl<Caps> DerefMut for Client<Caps>
323    where Caps: AppiumCapability
324{
325    fn deref_mut(&mut self) -> &mut Self::Target {
326        &mut self.inner
327    }
328}
329
330impl<Caps> Drop for Client<Caps>
331    where Caps: AppiumCapability {
332    fn drop(&mut self) {
333        let client = Arc::new(self.inner.clone());
334        spawn(async move {
335            let client = client.deref().clone();
336            // end session
337            if let Err(e) = client.issue_cmd(AppiumCommand::Custom(
338                Method::DELETE,
339                "".to_string(),
340                None
341            )).await {
342                error!("Error while ending session: {e}");
343            }
344
345            // clean up fantoccini
346            if let Err(e) = client.close().await {
347                error!("Error while issuing shutdown: {e}");
348            };
349        });
350    }
351}