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}