Skip to content

Commit

Permalink
Merge pull request #16 from telekom/feature/support-apps
Browse files Browse the repository at this point in the history
Support native app automation
  • Loading branch information
martingrossmann authored Jan 11, 2024
2 parents 9c52330 + 4537161 commit b94ab3d
Show file tree
Hide file tree
Showing 28 changed files with 1,191 additions and 376 deletions.
263 changes: 1 addition & 262 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,268 +78,7 @@ Maven:

## Documentation

### Use appium features

The Appium connector will register `mobile_chrome` and `mobile_safari` as available browser configurations for your `{browser}`
value. Further the connector will provide the `AppiumDriverManager` that you can use to unlock appium related features on the
implementation of `WebDriver` interface.

*Appium test class*

```java
public class ExampleTest extends TesterraTest {

@Test
public void testT01_My_first_test() {

WebDriver webDriver = WEB_DRIVER_MANAGER.getWebDriver();
WEB_DRIVER_MANAGER.unwrapWebDriver(webDriver, AppiumDriver.class).ifPresent(appiumDriver -> {
appiumDriver.rotate(ScreenOrientation.LANDSCAPE);
});
}
}
```
### Device filtering

If you have to run your tests on specific mobile devices available on your mobile device farm, you can use
the `tt.mobile.device.query.android` and `tt.mobile.device.query.ios` to filter for your devices by different properties. To reserve
a device and run tests on it you can simply use `WebDriverManager.getWebDriver()` call. If one or more device(s) matches your
filter, the driver will be instantiated, otherwise the call will fail.

The following query attributes are available for the device query strings.

- @category
- @os
- @version
- @manufacture
- @model
- @name

Please keep in mind, that you have to specify the device query for each operating system with the properties mentioned above. The
default values will provide you a device with given operating system and of `@category PHONE`.

#### Screenshots

Screenshots on test case failure works out of the box, because Appium is implementing the necessary interfaces of Selenium to achieve this.

### Properties

| Property | default | Description |
|--------------------------------|---------------------------------------|------------------------------------------------------------------|
| tt.mobile.grid.url | NONE | Grid URL of Appium / Selenium Grid ending on "wd/hub" |
| tt.mobile.grid.access.key | NONE | Access key of your user and project at Appium server, if needed. |
| tt.mobile.device.query.ios | "@os='ios' and @category='PHONE'" | Define the requested iOS device. |
| tt.mobile.device.query.android | "@os='android' and @category='PHONE'" | Define the requested Android device. |


### AppiumDriverRequest

You can also create new sessions by using the `WebDriverRequest` interface.

```java
AppiumDriverRequest appiumRequest = new AppiumDriverRequest();
appiumRequest.setAccessKey(String);
appiumRequest.setDeviceQuery(String);
appiumRequest.setServerUrl(URL);
appiumRequest.setStartupTimeoutSeconds(int);
appiumRequest.setReuseTimeoutSeconds(int);

WebDriver appiumDriver = WEB_DRIVER_MANAGER.getWebDriver(appiumRequest);
```

### Using Experitest SeeTest as Appium server

For Experitest SeeTest as Appium platform we provide the additional module ``appium-seetest`` with the following features:

* Video support
* Define Appium version

Gradle:

```groovy
// Version from this module
implementation 'io.testerra:appium-seetest:2.0'
```

Maven:

```xml
<!-- Version from this module -->
<dependency>
<groupId>io.testerra</groupId>
<artifactId>appium-seetest</artifactId>
<version>2.0</version>
</dependency>
```

#### Video support

Add the following properties:

```properties
# Comes from Testerra
tt.screencaster.active=true
# Optional for get a video for every test
tt.appium.seetest.video.onsuccess=true
```

#### Appium version

Add the following property:

```properties
tt.appium.seetest.appium.version=1.22.3
```

#### Properties

| Property | default | Description |
|------------------------------------------|---------|---------------------------------------------|
| tt.appium.seetest.video.onsuccess | false | Get a video for passed tests. |
| tt.appium.seetest.video.onfailed | true | Get a video for failed tests. |
| tt.appium.seetest.video.download.timeout | 20 sec | Set the timeout for video download. |
| tt.appium.seetest.appium.version | na. | Set the Appium version of SeeTest platform. |

## WinAppDriver support

The Appium connector also supports automation of Windows application using the `WindowsDriver` in two setup scenarios.

* Using an Appium-Server
* Using a WinAppServer

### Using Appium-Server (*tbd*)
*(Documentation missing)*

### Using WinAppDriver

The [WinAppDriver](https://github.com/microsoft/WinAppDriver) is like a Selenium server for Windows applications. You can download it from the official project website or just install it via. [chocolatey](https://chocolatey.org/)

```shell
choco install winappdriver
```

Before you are able to use it, you should make sure:

* That the Windows Operating System runs in Development Mode.
* The WinAppDriver able to run.

```text
"C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe"
Windows Application Driver listening for requests at: http://127.0.0.1:4723/
Press ENTER to exit.
```
You can customize the connection using [Properties](#Properties)

## Starting an application

You can start applications in several ways:

* Start applications by the executable path.
* Start drivers for the Windows Desktop.
* Start drivers from known application window title.
* Start application from internal application id (*Documentation unknown*)

### Start applications from path

The application path gets translated to the application id and also sets the working directory based on its parent.

```java
WinAppDriverRequest appRequest = new WinAppDriverRequest();
appRequest.setApplicationPath("C:\\Program Files (x86)\\Application\\Application.exe");
```

**NOTE**: Some applications require their working directory set to the parent of the application binary to run properly.

For example: `C:\\Program Files (x86)\\Application\\Application.exe` needs to run in `C:\\Program Files (x86)\\Application`. When setting the application path, the `WinAppDriverRequests` tries to detect the parent directory by using Java `Path.getParent()`, but this seems to be buggy when using Windows paths in Posix environments. Therefore, we've implemented a workaround to detect the base directory by using basic regular expressions with `/` and `\\` delimiters.

### Start a Desktop driver
```java
WinAppDriverRequest appRequest = new WinAppDriverRequest();
appRequest.setDesktopApplication();
```

### Start driver from known window title

This will try to initialize the driver by an already opened application identified by its window title. Otherwise, it will try to start by given application id.

```java
WinAppDriverRequest appRequest = new WinAppDriverRequest();
appRequest.reuseApplicationByWindowTitle("My App");
appRequest.setApplicationPath("C:\\Program Files (x86)\\Application\\Application.exe");
```

### Start applications from application id

Starting the application from application id is currently unknown. However, this application id starts the default calculator app.

```java
WinAppDriverRequest appRequest = new WinAppDriverRequest();
appRequest.setApplication("Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
```

### Retrieving element selectors

The WinAppDriver project provides a binary release of tool named [UIRecorder](https://github.com/microsoft/WinAppDriver/releases/tag/UIR-v1.1) for retrieving element selectors xPath by hovering and focusing elements.

### Find UiElements

Most of the applications support finding elements using `AutomationId` attribute selector.

```java
PreparedLocator automationLocator = LOCATE.prepare("//*[@AutomationId=\"%s\"]");
UiElement num1Btn = find(automationLocator.with("num1Button"));
```

### Accessing the native WindowsDriver API

All features of the `WindowsDriver` implementation are hidden by the `WebDriver` by default.
To retrieve the raw WindowsDriver, you can unwrap it via. `WebDriverManager`.

```java
Optional<WindowsDriver> optionalWindowsDriver = WEB_DRIVER_MANAGER.unwrapWebDriver(appDriver, WindowsDriver.class);

optionalWindowsDriver.ifPresent(windowsDriver -> {
// Native API access here
});
```

### Closing applications

The application will automatically be closed, when `WebDriver.quit()` gets called managed by Testerra on session end. But that closes the application's window which doesn't mean that the application is forced to quit. It could still be opened as a system service available by System tray icons.

There is an experimental feature to force quit an application: https://github.com/Microsoft/WinAppDriver/issues/159

Anyway, if you want to prevent Testerra from closing your `WinAppDriver`, just configure it on the `WinAppDriverRequest`.

```java
appRequest.setShutdownAfterTest(false);
appRequest.setShutdownAfterExecution(false);
```

### Properties

The WinAppDriver implementation provides the following properties.

|Property|default|Description|
|---|---|---|
|`tt.winapp.server.url`|`http://localhost:4723/`|URL of the WinAppDriver or Appium / Selenium Grid ending on "wd/hub"|
|`tt.winapp.reuse.timeout.seconds`|`2`|Timeout for finding reusable applications. |
|`tt.winapp.startup.timeout.seconds`|`8`|Timeout for general driver startup. |

### Troubleshooting

**Symptom: Application forget settings after restart**
- Solution: Try to set the working directory manually.

**Symptom: Elements are not interactable on remote WinAppDriver**
- Reason: When closing RDP connections, the Desktop gets non-interactable as default behaviour.
- Solution: You can use VNC instead or configure your RDP to keep sessions active. More information can be found here: https://github.com/microsoft/WinAppDriver/issues/1510

**Symptom: Non-interactable elements on non-focused windows**
- Reason: Some applications get minimized when they loose focus.
- Solution: Try to bring the window to the front by calling `webDriver.manage().window().setPosition(new Point(0,0));`
Please have a look to the [Wiki pages](https://github.com/telekom/testerra-appium-connector/wiki).

## Publication

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,26 @@ public SeeTestRestClient(String url) {
}

public Optional<JsonArray> getAbout() {
Response response = this.getBuilder("/applications", SeeTestApis.PM).get();
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
log().debug("No SeeTest host found. (Response status {})", response.getStatus());
return Optional.empty();
}
try {
Response response = this.getBuilder("/applications", SeeTestApis.PM).get();
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
log().debug("No SeeTest host found. (Response status {})", response.getStatus());
return Optional.empty();
}

Gson gson = new Gson();
JsonArray jsonArray = gson.fromJson(response.readEntity(String.class), JsonArray.class);
if (jsonArray.isJsonArray()) {
return Optional.of(jsonArray);
} else {
log().debug("Cannot read about response: {}", response.readEntity(String.class));
Gson gson = new Gson();
JsonArray jsonArray = gson.fromJson(response.readEntity(String.class), JsonArray.class);
if (jsonArray.isJsonArray()) {
return Optional.of(jsonArray);
} else {
log().debug("Cannot read about response: {}", response.readEntity(String.class));
return Optional.empty();
}
} catch (Exception e) {
log().warn("Cannot get information from SeeTest server");
return Optional.empty();
}

}

private Invocation.Builder getBuilder(String path, SeeTestApis api) {
Expand Down
4 changes: 4 additions & 0 deletions appium/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ dependencies {
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.apache.commons:commons-lang3:3.12.0'

// For AppiumClassFinder
implementation 'org.reflections:reflections:0.9.12'

testImplementation 'io.testerra:driver-ui:' + testerraTestVersion
// testImplementation 'io.testerra:driver-ui'
testImplementation 'io.testerra:report-ng:' + testerraTestVersion
testImplementation 'io.appium:java-client:7.3.0'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Testerra
*
* (C) 2022, Daniel Eckardt, T-Systems MMS GmbH, Deutsche Telekom AG
*
* Deutsche Telekom AG and all other contributors /
* copyright owners license this file to you under the Apache
* License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package eu.tsystems.mms.tic.testframework.appium;

import io.appium.java_client.AppiumDriver;

/**
* Created on 2023-03-27
*
* @author mgn
*/
public enum AppiumContext {

/**
* The context ({@link AppiumDriver#getContext()}) of the current view of the application is an operating system native.
*/
NATIVE_APP,
/**
* The context ({@link AppiumDriver#getContext()}) of the current view of the application is a (embedded) browser. {@see
* http://appium.io/docs/en/writing-running-appium/web/hybrid/index.html}
*/
WEBVIEW;

public static AppiumContext parse(String string) {
// there's only two NATIVE_APP contexts (NATIVE_APP and NATIVE_APP_INSTRUMENTED),
// but an infinite amount of WEBVIEW contexts, like WEBVIEW, SAFARI, CHROMIUM, WEBVIEW_1, WEBVIEW_2 etc.
// that's why we just assume that any context starting with NATIVE_APP is native and the rest WEBVIEW:
AppiumContext result = string == null || string.toLowerCase().startsWith("native_app") ? NATIVE_APP : WEBVIEW;
// LOGGER.debug(String.format("Parsed %s as %s", string, result));
return result;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
* A list ob supported browsers
*/
public class Browsers {
public static final String windows ="windows";
public static final String windows = "windows";
public static final String mobile_chrome = "mobile_chrome";
public static final String mobile_safari = "mobile_safari";
// Default value for AppiumDriverRequest
public static final String mobile = "mobile";
}
Loading

0 comments on commit b94ab3d

Please sign in to comment.