Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/scripts/resolve-appium-versions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Resolves the versions of the Appium server and of a driver to install in e2e jobs.
// Usage: node resolve-appium-versions.mjs [--stable] <server package> <driver package>
// Both get their beta versions if both have a beta that is newer than the latest stable release,
// otherwise both get the latest stable ones. With --stable the stable versions are always used.
// The result is written to the step outputs `server` and `driver`.
import {execFile} from 'node:child_process';
import {appendFile} from 'node:fs/promises';
import {promisify} from 'node:util';

const execFileAsync = promisify(execFile);

async function distTags(pkg) {
const {stdout} = await execFileAsync('npm', ['view', pkg, 'dist-tags', '--json']);
return JSON.parse(stdout);
}

// A prerelease of a version is older than that version, so only a higher major.minor.patch is newer
function isBetaNewerThanStable(beta, stable) {
const core = (version) => version.split('-')[0].split('.').map(Number);
const [b, s] = [core(beta), core(stable)];
for (let i = 0; i < 3; i++) {
if (b[i] !== s[i]) {
return b[i] > s[i];
}
}
return false;
}

async function main(args) {
const stableOnly = args.includes('--stable');
const [serverPackage, driverPackage] = args.filter((arg) => arg !== '--stable');
if (!serverPackage || !driverPackage) {
throw new Error('Usage: resolve-appium-versions.mjs [--stable] <server package> <driver package>');
}

const packages = await Promise.all(
[serverPackage, driverPackage].map(async (name) => {
const {latest, beta} = await distTags(name);
return {name, latest, beta, betaIsNewer: Boolean(beta) && isBetaNewerThanStable(beta, latest)};
}),
);
const useBeta = !stableOnly && packages.every((p) => p.betaIsNewer);
const [server, driver] = packages.map((p) => (useBeta ? p.beta : p.latest));

const summary = [
`Appium versions to install: ${useBeta ? 'beta' : 'stable'}${stableOnly ? ' (forced)' : ''}`,
...packages.map((p) => `- ${p.name}: stable ${p.latest}, beta ${p.beta ?? 'none'} => ${useBeta ? p.beta : p.latest}`),
].join('\n');
console.log(summary);
if (process.env.GITHUB_STEP_SUMMARY) {
await appendFile(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`);
}
if (process.env.GITHUB_OUTPUT) {
await appendFile(process.env.GITHUB_OUTPUT, `server=${server}\ndriver=${driver}\n`);
}
}

try {
await main(process.argv.slice(2));
} catch (e) {
console.error(e.message);
process.exit(1);
}
19 changes: 15 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,22 @@ jobs:
with:
node-version: 'lts/*'

# The beta versions of the server and of the driver are used if both are newer than the stable ones,
# except for the Flutter tests, which always use the stable ones
- name: Resolve Appium versions
Comment thread
harsha509 marked this conversation as resolved.
id: appium-versions
run: >-
node .github/scripts/resolve-appium-versions.mjs
${{ startsWith(matrix.e2e-tests, 'flutter') && '--stable' || '' }}
appium
${{ (matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android') && 'appium-uiautomator2-driver' || 'appium-xcuitest-driver' }}

- name: Install Appium
run: npm install --location=global appium
run: npm install --location=global appium@${{ steps.appium-versions.outputs.server }}

- name: Install UIA2 driver
if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android'
run: appium driver install uiautomator2
run: appium driver install uiautomator2@${{ steps.appium-versions.outputs.driver }}

- name: Install Flutter Integration driver
if: matrix.e2e-tests == 'flutter-android' || matrix.e2e-tests == 'flutter-ios'
Expand Down Expand Up @@ -168,16 +178,17 @@ jobs:

- name: Prepare iOS simulator
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
uses: futureware-tech/simulator-action@v5
uses: futureware-tech/simulator-action@v6
with:
model: "${{ env.IOS_DEVICE_NAME }}"
os_version: "${{ env.IOS_PLATFORM_VERSION }}"
wait_for_boot: true
settle_timeout_seconds: 180
shutdown_after_job: false

- name: Install XCUITest driver
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
run: appium driver install xcuitest
run: appium driver install xcuitest@${{ steps.appium-versions.outputs.driver }}

- name: Download prebuilt WDA
if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios'
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ possible platforms, e.g. mobile and desktop operating systems, IOT devices, etc.
talks to the server through its own HTTP client, and it is not so strictly focused on web-browser
related operations.

Code that needs the Selenium `RemoteWebDriver`, for example the Selenium `Augmenter`, or Selenium BiDi modules,
can use the optional `io.appium:java-client-selenium-bridge` artifact. See [Selenium interoperability](docs/selenium-bridge.md).

## Appium Server Service Wrapper

Appium java client provides a dedicated class to control Appium server execution.
Expand Down
122 changes: 65 additions & 57 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.apache.tools.ant.filters.*
import org.gradle.api.publish.maven.MavenPom

plugins {
id 'java-library'
Expand All @@ -21,20 +22,22 @@ ext {
group = 'io.appium'
version = appiumClientVersion

repositories {
mavenCentral()
allprojects {
repositories {
mavenCentral()

// Only add Selenium snapshot repo when resolving a SNAPSHOT version.
// Release versions (e.g. from gradle.properties) resolve from Maven Central only.
if (project.property('selenium.version').toString().contains('SNAPSHOT')) {
maven {
name = 'Central Portal Snapshots'
url = 'https://central.sonatype.com/repository/maven-snapshots/'
mavenContent {
snapshotsOnly()
}
content {
includeGroup("org.seleniumhq.selenium")
// Only add Selenium snapshot repo when resolving a SNAPSHOT version.
// Release versions (e.g. from gradle.properties) resolve from Maven Central only.
if (project.property('selenium.version').toString().contains('SNAPSHOT')) {
maven {
name = 'Central Portal Snapshots'
url = 'https://central.sonatype.com/repository/maven-snapshots/'
mavenContent {
snapshotsOnly()
}
content {
includeGroup("org.seleniumhq.selenium")
}
}
}
}
Expand Down Expand Up @@ -111,6 +114,53 @@ javadoc {
options.addStringOption('encoding', 'UTF-8')
}

ext.configureCommonPom = { MavenPom pom ->
pom.url = 'http://appium.io'
pom.developers {
developer {
name = 'Jonah Stiennon'
email = 'jonahss@gmail.com'
url = 'https://github.com/jonahss'
id = 'jonahss'
}
developer {
name = 'Sergey Tikhomirov'
email = 'tichomirovsergey@gmail.com'
url = 'https://github.com/TikhomirovSergey'
id = 'TikhomirovSergey'
}
developer {
name = 'Srinivasan Sekar'
email = 'srinivasan.sekar1990@gmail.com'
url = 'https://github.com/SrinivasanTarget'
id = 'SrinivasanTarget'
}
developer {
name = 'Mykola Mokhnach'
url = 'https://github.com/mykola-mokhnach'
id = 'mykola-mokhnach'
}
developer {
name = 'Valery Yatsynovich'
url = 'https://github.com/valfirst'
id = 'valfirst'
}
}
pom.licenses {
license {
name = 'Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
distribution = 'repo'
}
}
pom.scm {
url = 'https://github.com/appium/java-client'
connection = 'scm:git:ssh://git@github.com/appium/java-client.git'
developerConnection = 'scm:git:ssh://git@github.com/appium/java-client.git'
tag = 'HEAD'
}
}

publishing {
publications {
mavenJava(MavenPublication) {
Expand All @@ -121,50 +171,7 @@ publishing {
pom {
name = 'java-client'
description = 'Java client for Appium Mobile Webdriver'
url = 'http://appium.io'
developers {
developer {
name = 'Jonah Stiennon'
email = 'jonahss@gmail.com'
url = 'https://github.com/jonahss'
id = 'jonahss'
}
developer {
name = 'Sergey Tikhomirov'
email = 'tichomirovsergey@gmail.com'
url = 'https://github.com/TikhomirovSergey'
id = 'TikhomirovSergey'
}
developer {
name = 'Srinivasan Sekar'
email = 'srinivasan.sekar1990@gmail.com'
url = 'https://github.com/SrinivasanTarget'
id = 'SrinivasanTarget'
}
developer {
name = 'Mykola Mokhnach'
url = 'https://github.com/mykola-mokhnach'
id = 'mykola-mokhnach'
}
developer {
name = 'Valery Yatsynovich'
url = 'https://github.com/valfirst'
id = 'valfirst'
}
}
licenses {
license {
name = 'Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
distribution = 'repo'
}
}
scm {
url = 'https://github.com/appium/java-client'
connection = 'scm:git:ssh://git@github.com/appium/java-client.git'
developerConnection = 'scm:git:ssh://git@github.com/appium/java-client.git'
tag = 'HEAD'
}
configureCommonPom(it)
}
}
}
Expand All @@ -187,6 +194,7 @@ jreleaser {
active = 'ALWAYS'
url = 'https://central.sonatype.com/api/v1/publisher'
stagingRepository('build/staging-deploy')
stagingRepository('selenium-bridge/build/staging-deploy')
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions docs/selenium-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Selenium interoperability

Appium Java client depends on `selenium-api` only, and its drivers are not Selenium `RemoteWebDriver` instances.
The optional `java-client-selenium-bridge` artifact adapts an Appium driver for the Selenium code that needs one,
for example the Selenium `Augmenter`, or the BiDi modules like `LogInspector`. It depends on
`selenium-remote-driver`, so it is compiled against the Selenium releases from 4.50.0 on, like the core artifact.

```gradle
dependencies {
implementation('io.appium:java-client:X.Y.Z')
implementation('io.appium:java-client-selenium-bridge:X.Y.Z')
}
```

## RemoteWebDriver

`SeleniumBridge.asRemoteWebDriver` returns a Selenium `RemoteWebDriver` that works in the session of the Appium
driver. Both share the same session and the same HTTP client, so commands can be mixed freely. As long as the
result is referenced, the same instance is returned for the same Appium driver. Quitting the Selenium driver
quits the session.

```java
var driver = new AndroidDriver(serverUrl, options);
RemoteWebDriver selenium = SeleniumBridge.asRemoteWebDriver(driver);
WebDriver augmented = new Augmenter().augment(selenium);
```

Elements are not interchangeable between the two drivers: an element that one driver found cannot be passed to
the other, for example as a script argument. Find the element again with the driver that needs it.

The commands of Selenium that Appium does not serve, like downloads, are not available.

## BiDi

Create the session with the `webSocketUrl` capability (`options.enableBiDi()`), then pass the Selenium driver
to a BiDi module. Selenium opens the WebSocket connection when the bridge is created, with the timeouts, proxy,
credentials and SSL context of the `AppiumClientConfig` of the driver. If the connection cannot be made, the
BiDi modules fail with a `BiDiException`.

```java
var driver = new AndroidDriver(serverUrl, options.enableBiDi());
var selenium = SeleniumBridge.asRemoteWebDriver(driver);
try (var logInspector = new LogInspector(selenium)) {
logInspector.onGenericLog(entry -> System.out.println(entry.getText()));
driver.getPageSource();
} finally {
selenium.closeBiDi();
}
```

The connection stays open until `closeBiDi()` or `quit()`, so release it when you are done with the BiDi modules.
`closeBiDi()` keeps the session open, but the modules that were created before cannot be used afterwards.
The next `asRemoteWebDriver` call opens a new connection.

To customize the HTTP client that opens the WebSocket connection, use the `asRemoteWebDriver` overload that takes
a Selenium `HttpClient.Factory`. It creates a new Selenium driver with its own connection on every call.
7 changes: 6 additions & 1 deletion docs/v10-to-v11-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ in your own build.
are `io.appium.java_client.remote.AppiumWebElement` instead of `org.openqa.selenium.remote.RemoteWebElement`.
Both still implement `WebDriver` and `WebElement` from `selenium-api`, so code that is typed against the
interfaces does not need any change. Casts to `RemoteWebDriver` and `RemoteWebElement`, and code that
requires them (for example the Selenium `Augmenter`), must be replaced.
requires them (for example the Selenium `Augmenter`), must be replaced. The optional
`io.appium:java-client-selenium-bridge` artifact adapts an Appium driver to a `RemoteWebDriver` that works in the
same session, see [Selenium interoperability](selenium-bridge.md).
- The following types moved to the `io.appium.java_client.remote` package: `Response`, `Command`,
`CommandPayload`, `SessionId`, `DriverCommand`, `ExecuteMethod`, `CommandExecutor`, `ErrorHandler`,
`ErrorCodes`, `ScreenshotException` and `UnreachableBrowserException`.
Expand Down Expand Up @@ -77,3 +79,6 @@ returns a `Map`, because Guava is not a dependency anymore.
- `AppiumDriver` does not implement `HasBiDi` anymore and the `getBiDi` and `maybeGetBiDi` methods are removed.
Selenium deprecated them for removal and changed the BiDi API in an incompatible way in the recent releases.
The BiDi session address is still available in the `webSocketUrl` capability of the created session.
- To use the Selenium BiDi modules (for example `LogInspector`), add the `io.appium:java-client-selenium-bridge`
artifact and wrap the driver with `SeleniumBridge.asRemoteWebDriver(driver)`. See
[Selenium interoperability](selenium-bridge.md).
Loading
Loading