diff --git a/.github/workflows/gradle.yml b/.github/workflows/ci.yml similarity index 85% rename from .github/workflows/gradle.yml rename to .github/workflows/ci.yml index 252710957..41b5c892d 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ concurrency: cancel-in-progress: true env: + CI: true ANDROID_SDK_VERSION: "28" ANDROID_EMU_NAME: test ANDROID_EMU_TARGET: default @@ -28,6 +29,7 @@ env: IOS_PLATFORM_VERSION: "17.5" FLUTTER_ANDROID_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/app-debug.apk" FLUTTER_IOS_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/ios.zip" + PREBUILT_WDA_PATH: ${{ github.workspace }}/wda/WebDriverAgentRunner-Runner.app jobs: build: @@ -35,7 +37,7 @@ jobs: strategy: matrix: include: - - java: 11 + - java: 17 # Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available platform: macos-14 e2e-tests: ios @@ -51,13 +53,15 @@ jobs: e2e-tests: flutter-android - java: 21 platform: ubuntu-latest + - java: 25 + platform: ubuntu-latest fail-fast: false runs-on: ${{ matrix.platform }} name: JDK ${{ matrix.java }} - ${{ matrix.platform }} ${{ matrix.e2e-tests }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Enable KVM group perms if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android' @@ -67,25 +71,28 @@ jobs: sudo udevadm trigger --name-match=kvm - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: ${{ matrix.java }} - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - - name: Build with Gradle + - name: Build with Gradle against Selenium nightly build run: | - latest_snapshot=$(curl -sf https://oss.sonatype.org/content/repositories/snapshots/org/seleniumhq/selenium/selenium-api/ | \ - python -c "import sys,re; print(re.findall(r'\d+\.\d+\.\d+-SNAPSHOT', sys.stdin.read())[-1])") + latest_snapshot=$(curl -sf https://raw.githubusercontent.com/SeleniumHQ/selenium/refs/heads/trunk/java/version.bzl | grep 'SE_VERSION' | sed 's/.*"\(.*\)".*/\1/') echo ">>> $latest_snapshot" echo "latest_snapshot=$latest_snapshot" >> "$GITHUB_ENV" ./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot + - name: Build with Gradle against stable Selenium version + run: | + ./gradlew clean build -PisCI + - name: Install Node.js if: ${{ matrix.e2e-tests }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 'lts/*' @@ -134,12 +141,14 @@ jobs: with: model: "${{ env.IOS_DEVICE_NAME }}" os_version: "${{ env.IOS_PLATFORM_VERSION }}" + wait_for_boot: true + shutdown_after_job: false - name: Install XCUITest driver if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios' run: appium driver install xcuitest - - name: Prebuild XCUITest driver + - name: Download prebuilt WDA if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios' - run: appium driver run xcuitest build-wda --sdk=${{ env.IOS_PLATFORM_VERSION }} --name='${{ env.IOS_DEVICE_NAME }}' + run: appium driver run xcuitest download-wda-sim --platform=ios --outdir=$(dirname "$PREBUILT_WDA_PATH") - name: Run iOS E2E tests if: matrix.e2e-tests == 'ios' run: ./gradlew e2eIosTest -PisCI -Pselenium.version=$latest_snapshot diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 393ca3bd6..1658a2957 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -1,6 +1,7 @@ name: Conventional Commits on: pull_request: + types: [opened, edited, synchronize, reopened] jobs: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6b2a253c2..72edaf3bd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,20 +6,23 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '11' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: Publish package env: - MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} - MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} - PGP_SECRET: ${{ secrets.SIGNING_KEY }} - PGP_PASSPHRASE: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew publish + JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.SIGNING_PUBLIC_KEY }} + JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_SIGNING_KEY }} + JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_SIGNING_PASSWORD }} + JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME: ${{ secrets.OSSRH_USERNAME }} + JRELEASER_MAVENCENTRAL_SONATYPE_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + run: | + ./gradlew publish + ./gradlew jreleaserDeploy diff --git a/.gitignore b/.gitignore index dea3a9d88..da44d7acb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ classes/ /.settings .classpath .project +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 09c42fd19..1f189e2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +_10.0.0_ +- **[DOCUMENTATION]** + - Document the migration guide from v9 to v10 [#2331](https://github.com/appium/java-client/pull/2331) + - updated maven central release badge [#2316](https://github.com/appium/java-client/pull/2316) + - updated CI badge to use ci.yml workflow [#2317](https://github.com/appium/java-client/pull/2317) +- **[BREAKING CHANGE]** [#2327](https://github.com/appium/java-client/pull/2327) + - Removed all deprecated methods with Selenium's Location and LocationContext (these classes have been removed in Selenium 4.35.0) +- **[ENHANCEMENTS]** + - Proxy commands issues via RemoteWebElement [#2311](https://github.com/appium/java-client/pull/2311) + - Automated Release to Maven Central Repository using JReleaser [#2313](https://github.com/appium/java-client/pull/2313) +- **[BUG FIX]** + - Possible NPE in initBiDi() [#2325](https://github.com/appium/java-client/pull/2325) +- **[DEPENDENCY CHANGE]** + - Bump minimum Selenium version to 4.35.0 [#2327](https://github.com/appium/java-client/pull/2327) + - Bump org.junit.jupiter:junit-jupiter from 5.13.2 to 5.13.3 [#2314](https://github.com/appium/java-client/pull/2314) + - Bump io.github.bonigarcia:webdrivermanager [#2322](https://github.com/appium/java-client/pull/2322) + - Bump com.gradleup.shadow from 8.3.7 to 8.3.8 [#2315](https://github.com/appium/java-client/pull/2315) + - Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0 [#2320](https://github.com/appium/java-client/pull/2320) + +_9.5.0_ +- **[ENHANCEMENTS]** + - Allow extension capability keys to contain dot characters [#2271](https://github.com/appium/java-client/pull/2271) + - Add a client for Appium server storage plugin [#2275](https://github.com/appium/java-client/pull/2275) + - Swap check for `Widget` and `WebElement` [#2277](https://github.com/appium/java-client/pull/2277) + - Add compatibility with Selenium `4.34.0` [#2298](https://github.com/appium/java-client/pull/2298) + - Add new option classes for `prebuiltWDAPath` and `usePreinstalledWDA` XCUITest capabilities [#2304](https://github.com/appium/java-client/pull/2304) +- **[REFACTOR]** + - Migrate from JSR 305 to [JSpecify](https://jspecify.dev/)'s nullability annotations [#2281](https://github.com/appium/java-client/pull/2281) +- **[DEPENDENCY UPDATES]** + - Bump minimum supported Selenium version from `4.26.0` to `4.34.0` [#2305](https://github.com/appium/java-client/pull/2305) + - Bump Gson from `2.11.0` to `2.13.1` [#2267](https://github.com/appium/java-client/pull/2267), [#2286](https://github.com/appium/java-client/pull/2286), [#2290](https://github.com/appium/java-client/pull/2290) + - Bump SLF4J from `2.0.16` to `2.0.17` [#2274](https://github.com/appium/java-client/pull/2274) + +_9.4.0_ +- **[ENHANCEMENTS]** + - Implement `HasBiDi` interface support in `AppiumDriver` [#2250](https://github.com/appium/java-client/pull/2250), [#2254](https://github.com/appium/java-client/pull/2254), [#2256](https://github.com/appium/java-client/pull/2256) + - Add compatibility with Selenium `4.28.0` [#2249](https://github.com/appium/java-client/pull/2249) +- **[BUG FIX]** + - Fix scroll issue in flutter integration driver [#2227](https://github.com/appium/java-client/pull/2227) + - Fix the definition of `logcatFilterSpecs` option [#2258](https://github.com/appium/java-client/pull/2258) + - Use `WeakHashMap` for caching proxy classes [#2260](https://github.com/appium/java-client/pull/2260) +- **[DEPENDENCY UPDATES]** + - Bump minimum supported Selenium version from `4.19.0` to `4.26.0` [#2246](https://github.com/appium/java-client/pull/2246) + - Bump Apache Commons Lang from `3.15.0` to `3.16.1` [#2220](https://github.com/appium/java-client/pull/2220), [#2228](https://github.com/appium/java-client/pull/2228) + - Bump SLF4J from `2.0.13` to `2.0.16` [#2221](https://github.com/appium/java-client/pull/2221) _9.3.0_ - **[ENHANCEMENTS]** @@ -641,14 +686,14 @@ _8.6.0_ *6.0.0-BETA1* - **[ENHANCEMENT]** **[REFACTOR]** **[BREAKING CHANGE]** **[MAJOR CHANGE]** Improvements of the TouchActions API [#756](https://github.com/appium/java-client/pull/756), [#760](https://github.com/appium/java-client/pull/760): - - `io.appium.java_client.touch.ActionOptions` and sublasses were added + - `io.appium.java_client.touch.ActionOptions` and subclasses were added - old methods of the `TouchActions` were marked `@Deprecated` - new methods which take new options. -- **[ENHANCEMENT]**. Appium drivr local service uses default process environment by default. [#753](https://github.com/appium/java-client/pull/753) +- **[ENHANCEMENT]**. Appium driver local service uses default process environment by default. [#753](https://github.com/appium/java-client/pull/753) - **[BUG FIX]**. Removed 'set' prefix from waitForIdleTimeout setting. [#754](https://github.com/appium/java-client/pull/754) - **[BUG FIX]**. The asking for session details was optimized. Issue report [764](https://github.com/appium/java-client/issues/764). FIX [#769](https://github.com/appium/java-client/pull/769) -- **[BUG FIX]** **[REFACTOR]**. Inconcistent MissingParameterException was removed. Improvements of MultiTouchAction. Report: [#102](https://github.com/appium/java-client/issues/102). FIX [#772](https://github.com/appium/java-client/pull/772) +- **[BUG FIX]** **[REFACTOR]**. Inconsistent MissingParameterException was removed. Improvements of MultiTouchAction. Report: [#102](https://github.com/appium/java-client/issues/102). FIX [#772](https://github.com/appium/java-client/pull/772) - **[DEPENDENCY UPDATES]** - `org.apache.commons:commons-lang3` was updated to 3.7 - `commons-io:commons-io` was updated to 2.6 diff --git a/README.md b/README.md index 91e61c688..4d235791a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # java-client -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.appium/java-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.appium/java-client) +[![Maven Central Version](https://img.shields.io/maven-central/v/io.appium/java-client)](https://central.sonatype.com/artifact/io.appium/java-client) [![Javadocs](https://www.javadoc.io/badge/io.appium/java-client.svg)](https://www.javadoc.io/doc/io.appium/java-client) -[![Appium Java Client CI](https://github.com/appium/java-client/actions/workflows/gradle.yml/badge.svg)](https://github.com/appium/java-client/actions/workflows/gradle.yml) +[![Appium Java Client CI](https://github.com/appium/java-client/actions/workflows/ci.yml/badge.svg)](https://github.com/appium/java-client/actions/workflows/ci.yml) This is the Java language bindings for writing Appium Tests that conform to [WebDriver Protocol](https://w3c.github.io/webdriver/) + +## v9 to v10 Migration + +Follow the [v9 to v10 Migration Guide](./docs/v9-to-v10-migration-guide.md) to streamline the migration process. + ## v8 to v9 Migration Since v9 the client only supports Java 11 and above. @@ -94,8 +99,12 @@ dependencies { ### Compatibility Matrix Appium Java Client | Selenium client -----------------------------------------------------------------------------------------------------|----------------- - `9.2.1`(known issues: appium/java-client#2145, appium/java-client#2146), `9.2.2`, `9.2.3`, `9.3.0` | `4.19.0`, `4.19.1`, `4.20.0`, `4.21.0`, `4.22.0`, `4.23.0` +----------------------------------------------------------------------------------------------------|----------------------------- +`next` (not released yet) | `4.40.0` +`10.0.0` | `4.35.0`, `4.36.0`, `4.37.0`, `4.38.0`, `4.39.0` +`9.5.0` | `4.34.0` +`9.4.0` | `4.26.0`, `4.27.0`, `4.28.0`, `4.28.1`, `4.29.0`, `4.30.0`, `4.31.0`, `4.32.0`, `4.33.0` + `9.2.1`(known issues: appium/java-client#2145, appium/java-client#2146), `9.2.2`, `9.2.3`, `9.3.0` | `4.19.0`, `4.19.1`, `4.20.0`, `4.21.0`, `4.22.0`, `4.23.0`, `4.23.1`, `4.24.0`, `4.25.0`, `4.26.0`, `4.27.0` `9.1.0`, `9.2.0` | `4.17.0`, `4.18.0`, `4.18.1` `9.0.0` | `4.14.1`, `4.15.0`, `4.16.0` (partially [corrupted](https://github.com/SeleniumHQ/selenium/issues/13256)), `4.16.1` N/A | `4.14.0` @@ -173,7 +182,7 @@ UiAutomator2Options options = new UiAutomator2Options() .setApp("/home/myapp.apk"); AndroidDriver driver = new AndroidDriver( // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub - new URL("http://127.0.0.1:4723"), options + new URI("http://127.0.0.1:4723").toURL(), options ); try { WebElement el = driver.findElement(AppiumBy.xpath("//Button")); @@ -192,7 +201,7 @@ XCUITestOptions options = new XCUITestOptions() .setApp("/home/myapp.ipa"); IOSDriver driver = new IOSDriver( // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub - new URL("http://127.0.0.1:4723"), options + new URI("http://127.0.0.1:4723").toURL(), options ); try { WebElement el = driver.findElement(AppiumBy.accessibilityId("myId")); @@ -213,7 +222,7 @@ BaseOptions options = new BaseOptions() .amend("mycapability2", "capvalue2"); AppiumDriver driver = new AppiumDriver( // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub - new URL("http://127.0.0.1:4723"), options + new URI("http://127.0.0.1:4723").toURL(), options ); try { WebElement el = driver.findElement(AppiumBy.className("myClass")); diff --git a/build.gradle b/build.gradle index 4e0b67886..60340fbfe 100644 --- a/build.gradle +++ b/build.gradle @@ -7,19 +7,33 @@ plugins { id 'maven-publish' id 'jacoco' id 'signing' - id 'org.owasp.dependencycheck' version '11.1.0' - id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'org.owasp.dependencycheck' version '12.2.0' + id 'com.gradleup.shadow' version '9.3.1' + id 'org.jreleaser' version '1.21.0' } +ext { + seleniumVersion = project.property('selenium.version') + appiumClientVersion = project.property('appiumClient.version') + slf4jVersion = '2.0.17' +} + +group = 'io.appium' +version = appiumClientVersion + repositories { mavenCentral() if (project.hasProperty("isCI")) { maven { - url uri('https://oss.sonatype.org/content/repositories/snapshots/') + name = 'Central Portal Snapshots' + url = 'https://central.sonatype.com/repository/maven-snapshots/' mavenContent { snapshotsOnly() } + content { + includeGroup("org.seleniumhq.selenium") + } } } } @@ -31,15 +45,9 @@ java { withSourcesJar() } -ext { - seleniumVersion = project.property('selenium.version') - appiumClientVersion = project.property('appiumClient.version') - slf4jVersion = '2.0.16' -} - dependencies { - compileOnly 'org.projectlombok:lombok:1.18.36' - annotationProcessor 'org.projectlombok:lombok:1.18.36' + compileOnly 'org.projectlombok:lombok:1.18.42' + annotationProcessor 'org.projectlombok:lombok:1.18.42' if (project.hasProperty("isCI")) { api "org.seleniumhq.selenium:selenium-api:${seleniumVersion}" @@ -65,8 +73,9 @@ dependencies { } } } - implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.google.code.gson:gson:2.13.2' implementation "org.slf4j:slf4j-api:${slf4jVersion}" + implementation 'org.jspecify:jspecify:1.0.0' } dependencyCheck { @@ -74,7 +83,7 @@ dependencyCheck { } jacoco { - toolVersion = '0.8.12' + toolVersion = '0.8.13' } tasks.withType(JacocoReport).configureEach { @@ -90,7 +99,7 @@ jacocoTestReport.dependsOn test apply plugin: 'checkstyle' checkstyle { - toolVersion = '10.17.0' + toolVersion = '10.23.1' configFile = configDirectory.file('appium-style.xml').get().getAsFile() showViolations = true ignoreFailures = false @@ -159,27 +168,31 @@ publishing { } repositories { maven { - credentials { - username = System.getenv("MAVEN_USERNAME") - password = System.getenv("MAVEN_PASSWORD") - } - def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/'" - url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + url = layout.buildDirectory.dir('staging-deploy') } } } -signing { - required { !'true'.equalsIgnoreCase(project.findProperty('signingDisabled')) } - def signingKey = System.getenv("PGP_SECRET") - def signingPassword = System.getenv("PGP_PASSPHRASE") - useInMemoryPgpKeys(signingKey, signingPassword) - sign publishing.publications.mavenJava +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } } wrapper { - gradleVersion = '8.9' + gradleVersion = '9.1.0' distributionType = Wrapper.DistributionType.ALL } @@ -195,7 +208,7 @@ testing { configureEach { useJUnitJupiter() dependencies { - implementation 'org.junit.jupiter:junit-jupiter:5.11.3' + implementation 'org.junit.jupiter:junit-jupiter:5.14.2' runtimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.hamcrest:hamcrest:3.0' runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}" @@ -213,7 +226,7 @@ testing { test { dependencies { implementation "org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}" - implementation('io.github.bonigarcia:webdrivermanager:5.9.2') { + implementation('io.github.bonigarcia:webdrivermanager:6.3.3') { exclude group: 'org.seleniumhq.selenium' } } @@ -233,7 +246,7 @@ testing { dependencies { implementation project() implementation(sourceSets.test.output) - implementation('org.apache.commons:commons-lang3:3.17.0') + implementation('org.apache.commons:commons-lang3:3.20.0') } targets.configureEach { @@ -257,7 +270,7 @@ testing { dependencies { implementation project() implementation(sourceSets.test.output) - implementation('io.github.bonigarcia:webdrivermanager:5.9.2') { + implementation('io.github.bonigarcia:webdrivermanager:6.3.3') { exclude group: 'org.seleniumhq.selenium' } } diff --git a/docs/Advanced-By.md b/docs/Advanced-By.md index 4226ed9f6..96609f2a7 100644 --- a/docs/Advanced-By.md +++ b/docs/Advanced-By.md @@ -48,7 +48,7 @@ XCUIElementTypeCell[$label == 'here'$] #### Handling Quote Marks Most of the time, you can treat pairs of single quotes or double quotes -interchangably. If you're searching with a string that contains quote marks, +interchangeably. If you're searching with a string that contains quote marks, though, you [need to be careful](https://stackoverflow.com/q/14116217). ```c diff --git a/docs/How-to-report-an-issue.md b/docs/How-to-report-an-issue.md index 95be8f584..863c90187 100644 --- a/docs/How-to-report-an-issue.md +++ b/docs/How-to-report-an-issue.md @@ -1,6 +1,6 @@ # Be sure that it is not a server-side problem if you are facing something that looks like a bug -The Appium Java client is the thin client which just sends requests and receives responces generally. +The Appium Java client is the thin client which just sends requests and receives responses generally. Be sure that this bug is not reported [here](https://github.com/appium/appium/issues) and/or there is no progress on this issue. @@ -13,8 +13,8 @@ If it is the feature request then there should be the description of this featur ### Environment (bug report) -* java client build version or git revision if you use some shapshot: -* Appium server version or git revision if you use some shapshot: +* java client build version or git revision if you use some snapshot: +* Appium server version or git revision if you use some snapshot: * Desktop OS/version used to run Appium if necessary: * Node.js version (unless using Appium.app|exe) or Appium CLI or Appium.app|exe: * Mobile platform/version under test: @@ -32,7 +32,7 @@ You can git clone https://github.com/appium/appium/tree/master/sample-code or ht Also you can create a [gist](https://gist.github.com) with pasted java code sample or paste it at ussue description using markdown. About markdown please read [Mastering markdown](https://guides.github.com/features/mastering-markdown/) and [Writing on GitHub](https://help.github.com/categories/writing-on-github/) -### Ecxeption stacktraces (bug report) +### Exception stacktraces (bug report) There should be created a [gist](https://gist.github.com) with pasted stacktrace of exception thrown by java. diff --git a/docs/Page-objects.md b/docs/Page-objects.md index ce7a04f1f..f3e1c9627 100644 --- a/docs/Page-objects.md +++ b/docs/Page-objects.md @@ -16,7 +16,7 @@ WebElement someElement; List someElements; ``` -# If there is need to use convinient locators for mobile native applications then the following is available: +# If there is need to use convenient locators for mobile native applications then the following is available: ```java import io.appium.java_client.android.AndroidElement; @@ -324,13 +324,13 @@ A typical page object could look like: ```java public class RottenTomatoesScreen { - //convinient locator + //convenient locator private List titles; - //convinient locator + //convenient locator private List scores; - //convinient locator + //convenient locator private List castings; //element declaration goes on @@ -371,13 +371,13 @@ public class Movie extends Widget{ super(element); } - //convinient locator + //convenient locator private AndroidElement title; - //convinient locator + //convenient locator private AndroidElement score; - //convinient locator + //convenient locator private AndroidElement casting; public String getTitle(params){ @@ -404,7 +404,7 @@ So, now page object looks ```java public class RottenTomatoesScreen { - @AndroidFindBy(a locator which convinient to find a single movie-root - element) + @AndroidFindBy(a locator which convenient to find a single movie-root - element) private List movies; //element declaration goes on @@ -427,7 +427,7 @@ public class RottenTomatoesScreen { Then ```java //the class is annotated !!! -@AndroidFindBy(a locator which convinient to find a single movie-root - element) +@AndroidFindBy(a locator which convenient to find a single movie-root - element) public class Movie extends Widget{ ... } @@ -658,7 +658,7 @@ This use case has some restrictions; - All classes which are declared by the OverrideWidget annotation should be subclasses of the class declared by field -- All classes which are declared by the OverrideWidget should not be abstract. If a declared class is overriden partially like +- All classes which are declared by the OverrideWidget should not be abstract. If a declared class is overridden partially like ```java //above is the other field declaration diff --git a/docs/The-event_firing.md b/docs/The-event_firing.md index 527fddeb8..ff77c1247 100644 --- a/docs/The-event_firing.md +++ b/docs/The-event_firing.md @@ -154,3 +154,60 @@ This proxy is not tied to WebDriver descendants and could be used to any classes change/replace the original methods behavior. It is important to know that callbacks are **not** invoked for methods derived from the standard `Object` class, like `toString` or `equals`. Check [unit tests](../src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java) for more examples. + +#### ElementAwareWebDriverListener + +A specialized MethodCallListener that listens to all method calls on a WebDriver instance and automatically wraps any returned RemoteWebElement (or list of elements) with a proxy. This enables your listener to intercept and react to method calls on both: + +- The driver itself (e.g., findElement, getTitle) + +- Any elements returned by the driver (e.g., click, isSelected on a WebElement) + +```java +import io.appium.java_client.ios.IOSDriver; +import io.appium.java_client.ios.options.XCUITestOptions; +import io.appium.java_client.proxy.ElementAwareWebDriverListener; +import io.appium.java_client.proxy.Helpers; +import io.appium.java_client.proxy.MethodCallListener; + + +// ... + +final StringBuilder acc = new StringBuilder(); + +var listener = new ElementAwareWebDriverListener() { + @Override + public void beforeCall(Object target, Method method, Object[] args) { + acc.append("beforeCall ").append(method.getName()).append("\n"); + } +}; + +IOSDriver decoratedDriver = createProxy( + IOSDriver.class, + new Object[]{new URL("http://localhost:4723/"), new XCUITestOptions()}, + new Class[]{URL.class, Capabilities.class}, + listener +); + +WebElement element = decoratedDriver.findElement(By.id("button")); +element::click; + +List elements = decoratedDriver.findElements(By.id("button")); +elements.get(1).isSelected(); + +assertThat(acc.toString().trim()).isEqualTo( + String.join("\n", + "beforeCall findElement", + "beforeCall click", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities", + "beforeCall findElements", + "beforeCall isSelected", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities" + ) +); + +``` diff --git a/docs/environment.md b/docs/environment.md index 1af2f306c..a08a6ea6e 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -32,4 +32,4 @@ Remember to reload the config after it has been changed by restarting the comman Also, it is possible to set variables on [per-process](https://stackoverflow.com/questions/10856129/setting-an-environment-variable-before-a-command-in-bash-not-working-for-second) basis. This might be handy if Appium is set up to start automatically with the operating system, because on early stages of system initialization it is possible that the "usual" environment has not been loaded yet. -In case the Appium process is started programatically, for example with java client's `AppiumDriverLocalService` helper class, then it might be necessary to setup the environment [in the client code](https://github.com/appium/java-client/pull/753), because prior to version 6.0 the client does not inherit it from the parent process by default. +In case the Appium process is started programmatically, for example with java client's `AppiumDriverLocalService` helper class, then it might be necessary to setup the environment [in the client code](https://github.com/appium/java-client/pull/753), because prior to version 6.0 the client does not inherit it from the parent process by default. diff --git a/docs/release.md b/docs/release.md index f2a6c1ae6..9a84ee016 100644 --- a/docs/release.md +++ b/docs/release.md @@ -9,21 +9,17 @@ Its target auditory is project maintainers. 1. Bump the `appiumClient.version` number in [gradle.properties](../gradle.properties). 1. Create a pull request to approve the changelog and version bump. 1. Merge the pull request after it is approved. -1. Create and push a new repository tag. The tag name should look like - `v..`. -1. Create a new [Release](https://github.com/appium/java-client/releases/new) in GitHub. - Paste the above changelist into the release notes. Make sure the name of the new release - matches to the name of the above tag. -1. Open [Sonatype](https://oss.sonatype.org/#welcome) in your browser. -1. Login to Nexus using 1Password credentials. Ask Appium maintainers - if you need access to the team's 1Password vault. -1. Navigate to `Staging Repositories`. -1. Select the corresponding release and click `Close`. Note that it may take - some minutes until Sonatype picks up the GitHub release. -1. Wait until checks are completed. -1. Click `Release`. -1. After the new release is published, it becomes available in - [Maven Central](https://repo1.maven.org/maven2/io/appium/java-client/) - within 30 minutes. Once artifacts are in Maven Central, it normally - takes 1-2 hours before they appear in - [search results](https://central.sonatype.com/artifact/io.appium/java-client). +1. Create and push a new repository tag. The tag name should look like + `v..`. +1. Create a new [Release](https://github.com/appium/java-client/releases/new) in GitHub. + Paste the above changelist into the release notes. Make sure the name of the new release + matches to the name of the above tag. +1. Open [Maven Central Repository](https://central.sonatype.com/) in your browser. +1. Log in to the `Maven Central Repository` using the credentials stored in 1Password. If you need access to the team's 1Password vault, contact the Appium maintainers. +1. Navigate to the `Publish` section. +1. Under `Deployments`, you will see the latest deployment being published. Note: Sometimes the status may remain in the `publishing` state for an extended period, but it will eventually complete. +1. After the new release is published, it becomes available in + [Maven Central](https://repo1.maven.org/maven2/io/appium/java-client/) + within 30 minutes. Once artifacts are in Maven Central, it normally + takes 1-2 hours before they appear in + [search results](https://central.sonatype.com/artifact/io.appium/java-client). diff --git a/docs/v9-to-v10-migration-guide.md b/docs/v9-to-v10-migration-guide.md new file mode 100644 index 000000000..40f7e89fe --- /dev/null +++ b/docs/v9-to-v10-migration-guide.md @@ -0,0 +1,17 @@ +This is the list of main changes between major versions 9 and 10 of Appium +java client. This list should help you to successfully migrate your +existing automated tests codebase. + + +## The minimum supported Selenium version is set to 4.35.0 + +- Selenium versions below 4.35.0 won't work with Appium java client 10+. +Check the [Compatibility Matrix](../README.md#compatibility-matrix) for more +details about versions compatibility. + +## Removed previously deprecated items + +- `org.openqa.selenium.remote.html5.RemoteLocationContext`, `org.openqa.selenium.html5.Location` and + `org.openqa.selenium.html5.LocationContext` imports have been removed since they don't exist + in Selenium lib anymore. Use appropriate replacements from this library instead for APIs and + interfaces that were using deprecated classes, like `io.appium.java_client.Location`. diff --git a/gradle.properties b/gradle.properties index 10017f9d5..1540f2707 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.daemon=true -selenium.version=4.19.0 +selenium.version=4.40.0 # Please increment the value in a release -appiumClient.version=9.3.0 +appiumClient.version=10.0.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197..61285a659 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dedd5d1e6..5f38436fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6d6..adff685a0 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -173,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -206,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9b42019c7..e509b2dd8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidBiDiTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidBiDiTest.java new file mode 100644 index 000000000..9901b50d6 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidBiDiTest.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.android; + +import org.junit.jupiter.api.Test; +import org.openqa.selenium.bidi.Event; +import org.openqa.selenium.bidi.log.LogEntry; +import org.openqa.selenium.bidi.module.LogInspector; + +import java.util.concurrent.CopyOnWriteArrayList; + +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class AndroidBiDiTest extends BaseAndroidTest { + + @Test + public void listenForAndroidLogsGeneric() { + var logs = new CopyOnWriteArrayList<>(); + var listenerId = driver.getBiDi().addListener( + NATIVE_CONTEXT, + new Event("log.entryAdded", m -> m), + logs::add + ); + try { + driver.getPageSource(); + } finally { + driver.getBiDi().removeListener(listenerId); + } + assertFalse(logs.isEmpty()); + } + + @Test + public void listenForAndroidLogsSpecific() { + var logs = new CopyOnWriteArrayList(); + try (var logInspector = new LogInspector(NATIVE_CONTEXT, driver)) { + logInspector.onLog(logs::add); + driver.getPageSource(); + } + assertFalse(logs.isEmpty()); + } + +} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java index fdc47664b..1a9a5657d 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -31,7 +32,7 @@ public class AndroidContextTest extends BaseAndroidTest { } @Test public void testGetContext() { - assertEquals("NATIVE_APP", driver.getContext()); + assertEquals(NATIVE_CONTEXT, driver.getContext()); } @Test public void testGetContextHandles() { @@ -42,8 +43,8 @@ public class AndroidContextTest extends BaseAndroidTest { driver.getContextHandles(); driver.context("WEBVIEW_io.appium.android.apis"); assertEquals(driver.getContext(), "WEBVIEW_io.appium.android.apis"); - driver.context("NATIVE_APP"); - assertEquals(driver.getContext(), "NATIVE_APP"); + driver.context(NATIVE_CONTEXT); + assertEquals(driver.getContext(), NATIVE_CONTEXT); } @Test public void testContextError() { diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java index 79d327ae1..0db6f2647 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java @@ -18,6 +18,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static java.time.Duration.ofMillis; import static java.time.Duration.ofSeconds; import static org.hamcrest.MatcherAssert.assertThat; @@ -75,7 +76,7 @@ public static void startWebViewActivity() { @BeforeEach public void setUp() { - driver.context("NATIVE_APP"); + driver.context(NATIVE_CONTEXT); } @Test diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java index b9abd9ff6..5fef68b48 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java @@ -6,6 +6,7 @@ import java.time.Duration; +import static java.util.Locale.ROOT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.is; @@ -26,7 +27,7 @@ public void verifyBasicScreenRecordingWorks() throws InterruptedException { .withTimeLimit(Duration.ofSeconds(5)) ); } catch (WebDriverException e) { - if (e.getMessage().toLowerCase().contains("emulator")) { + if (e.getMessage() != null && e.getMessage().toLowerCase(ROOT).contains("emulator")) { // screen recording only works on real devices return; } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java index 347304cf5..1325a0f85 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java @@ -19,6 +19,7 @@ import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; +import io.appium.java_client.utils.TestUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -44,7 +45,8 @@ public class BaseAndroidTest { UiAutomator2Options options = new UiAutomator2Options() .setDeviceName("Android Emulator") - .setApp(TestResources.API_DEMOS_APK.toString()) + .enableBiDi() + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .eventTimings(); driver = new AndroidDriver(service.getUrl(), options); } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java index f26469cb8..2245b1be3 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java @@ -19,6 +19,7 @@ import io.appium.java_client.android.options.EspressoOptions; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; +import io.appium.java_client.utils.TestUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -42,7 +43,7 @@ public class BaseEspressoTest { EspressoOptions options = new EspressoOptions() .setDeviceName("Android Emulator") - .setApp(TestResources.API_DEMOS_APK.toString()) + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .eventTimings(); driver = new AndroidDriver(service.getUrl(), options); } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/TestResources.java b/src/e2eAndroidTest/java/io/appium/java_client/android/TestResources.java deleted file mode 100644 index 149a72a4a..000000000 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/TestResources.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.appium.java_client.android; - -import io.appium.java_client.TestUtils; - -import java.nio.file.Path; - -public class TestResources { - public static final Path API_DEMOS_APK = TestUtils.resourcePathToAbsolutePath("ApiDemos-debug.apk"); - - private TestResources() { - } -} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java index d482284b2..235e7a5e9 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java @@ -1,8 +1,7 @@ package io.appium.java_client.service.local; -import io.appium.java_client.TestUtils; -import io.appium.java_client.android.TestResources; import io.appium.java_client.android.options.UiAutomator2Options; +import io.appium.java_client.utils.TestUtils; import io.github.bonigarcia.wdm.WebDriverManager; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -17,7 +16,6 @@ import java.util.List; import java.util.Map; -import static io.appium.java_client.TestUtils.getLocalIp4Address; import static io.appium.java_client.service.local.AppiumDriverLocalService.buildDefaultService; import static io.appium.java_client.service.local.AppiumServiceBuilder.APPIUM_PATH; import static io.appium.java_client.service.local.AppiumServiceBuilder.BROADCAST_IP4_ADDRESS; @@ -25,6 +23,7 @@ import static io.appium.java_client.service.local.flags.GeneralServerFlag.BASEPATH; import static io.appium.java_client.service.local.flags.GeneralServerFlag.CALLBACK_ADDRESS; import static io.appium.java_client.service.local.flags.GeneralServerFlag.SESSION_OVERRIDE; +import static io.appium.java_client.utils.TestUtils.getLocalIp4Address; import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; import static java.lang.System.getProperty; import static java.lang.System.setProperty; @@ -50,7 +49,7 @@ class ServerBuilderTest { private static final String PATH_TO_APPIUM_NODE_IN_PROPERTIES = getProperty(APPIUM_PATH); /** - * This is the path to the stub main.js file + * This is the path to the stub main.js file. */ private static final Path PATH_T0_TEST_MAIN_JS = TestUtils.resourcePathToAbsolutePath("main.js"); @@ -148,7 +147,7 @@ void checkAbilityToStartServiceUsingCapabilities() { .setNewCommandTimeout(Duration.ofSeconds(60)) .setAppPackage("io.appium.android.apis") .setAppActivity(".view.WebView1") - .setApp(TestResources.API_DEMOS_APK.toString()) + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .setChromedriverExecutable(chromeManager.getDownloadedDriverPath()); service = new AppiumServiceBuilder().withCapabilities(options).build(); @@ -164,7 +163,7 @@ void checkAbilityToStartServiceUsingCapabilitiesAndFlags() { .setNewCommandTimeout(Duration.ofSeconds(60)) .setAppPackage("io.appium.android.apis") .setAppActivity(".view.WebView1") - .setApp(TestResources.API_DEMOS_APK.toString()) + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .setChromedriverExecutable(chromeManager.getDownloadedDriverPath()) .amend("winPath", "C:\\selenium\\app.apk") .amend("unixPath", "/selenium/app.apk") diff --git a/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java index 77a7cc585..131610d35 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java @@ -17,11 +17,11 @@ package io.appium.java_client.service.local; import io.appium.java_client.android.AndroidDriver; -import io.appium.java_client.android.TestResources; import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.remote.AutomationName; import io.appium.java_client.remote.MobilePlatform; import io.appium.java_client.service.local.flags.GeneralServerFlag; +import io.appium.java_client.utils.TestUtils; import io.github.bonigarcia.wdm.WebDriverManager; import org.junit.jupiter.api.Test; import org.openqa.selenium.Capabilities; @@ -44,7 +44,7 @@ void startingAndroidAppWithCapabilitiesOnlyTest() { AndroidDriver driver = new AndroidDriver(new UiAutomator2Options() .setDeviceName("Android Emulator") .autoGrantPermissions() - .setApp(TestResources.API_DEMOS_APK.toString())); + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL)); try { Capabilities caps = driver.getCapabilities(); @@ -53,7 +53,7 @@ void startingAndroidAppWithCapabilitiesOnlyTest() { ); assertEquals(AutomationName.ANDROID_UIAUTOMATOR2, caps.getCapability(AUTOMATION_NAME_OPTION)); assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); - assertEquals(TestResources.API_DEMOS_APK.toString(), caps.getCapability(APP_OPTION)); + assertEquals(TestUtils.ANDROID_APIDEMOS_APK_URL, caps.getCapability(APP_OPTION)); } finally { driver.quit(); } @@ -68,7 +68,7 @@ void startingAndroidAppWithCapabilitiesAndServiceTest() { AndroidDriver driver = new AndroidDriver(builder, new UiAutomator2Options() .setDeviceName("Android Emulator") .autoGrantPermissions() - .setApp(TestResources.API_DEMOS_APK.toString())); + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL)); try { Capabilities caps = driver.getCapabilities(); @@ -88,7 +88,7 @@ void startingAndroidAppWithCapabilitiesAndFlagsOnServerSideTest() { .fullReset() .autoGrantPermissions() .setNewCommandTimeout(Duration.ofSeconds(60)) - .setApp(TestResources.API_DEMOS_APK.toString()); + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL); WebDriverManager chromeManager = chromedriver(); chromeManager.setup(); diff --git a/src/e2eAndroidTest/resources/ApiDemos-debug.apk b/src/e2eAndroidTest/resources/ApiDemos-debug.apk deleted file mode 100644 index 62a1fd607..000000000 Binary files a/src/e2eAndroidTest/resources/ApiDemos-debug.apk and /dev/null differ diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java index a0dd5ccaa..a141f01ef 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java @@ -35,6 +35,7 @@ class BaseFlutterTest { private static AppiumDriverLocalService service; protected static FlutterIntegrationTestDriver driver; protected static final By LOGIN_BUTTON = AppiumBy.flutterText("Login"); + private static String PREBUILT_WDA_PATH = System.getenv("PREBUILT_WDA_PATH"); /** * initialization. @@ -47,7 +48,7 @@ public static void beforeClass() { // Flutter driver mocking command requires adb_shell permission to set certain permissions // to the AUT. This can be removed once the server logic is updated to use a different approach // for setting the permission - .withArgument(GeneralServerFlag.ALLOW_INSECURE, "adb_shell") + .withArgument(GeneralServerFlag.ALLOW_INSECURE, "*:adb_shell") .build(); service.start(); } @@ -74,15 +75,19 @@ void startSession() throws MalformedURLException { String platformVersion = System.getenv("IOS_PLATFORM_VERSION") != null ? System.getenv("IOS_PLATFORM_VERSION") : "14.5"; - driver = new FlutterIOSDriver(service.getUrl(), flutterOptions - .setXCUITestOptions(new XCUITestOptions() - .setApp(System.getProperty("flutterApp")) - .setDeviceName(deviceName) - .setPlatformVersion(platformVersion) - .setWdaLaunchTimeout(Duration.ofMinutes(4)) - .setSimulatorStartupTimeout(Duration.ofMinutes(5)) - .eventTimings() - ) + XCUITestOptions xcuiTestOptions = new XCUITestOptions() + .setApp(System.getProperty("flutterApp")) + .setDeviceName(deviceName) + .setPlatformVersion(platformVersion) + .setWdaLaunchTimeout(Duration.ofMinutes(4)) + .setSimulatorStartupTimeout(Duration.ofMinutes(5)) + .eventTimings(); + if (PREBUILT_WDA_PATH != null) { + xcuiTestOptions.usePreinstalledWda().setPrebuiltWdaPath(PREBUILT_WDA_PATH); + } + driver = new FlutterIOSDriver( + service.getUrl(), + flutterOptions.setXCUITestOptions(xcuiTestOptions) ); } } diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java index 0f056e51a..9e7f60bda 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java @@ -1,12 +1,12 @@ package io.appium.java_client.android; import io.appium.java_client.AppiumBy; -import io.appium.java_client.TestUtils; import io.appium.java_client.flutter.commands.DoubleClickParameter; import io.appium.java_client.flutter.commands.DragAndDropParameter; import io.appium.java_client.flutter.commands.LongPressParameter; import io.appium.java_client.flutter.commands.ScrollParameter; import io.appium.java_client.flutter.commands.WaitParameter; +import io.appium.java_client.utils.TestUtils; import org.junit.jupiter.api.Test; import org.openqa.selenium.Point; import org.openqa.selenium.WebElement; @@ -160,4 +160,37 @@ void testCameraMocking() throws IOException { driver.findElement(AppiumBy.flutterText("PICK")).click(); assertTrue(driver.findElement(AppiumBy.flutterText("Success!")).isDisplayed()); } + + @Test + void testScrollTillVisibleForAncestor() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy.FlutterBy ancestorBy = AppiumBy.flutterAncestor( + AppiumBy.flutterText("Child 2"), + AppiumBy.flutterKey("parent_card_4") + ); + + assertEquals(0, driver.findElements(ancestorBy).size()); + driver.scrollTillVisible(new ScrollParameter(ancestorBy)); + assertEquals(1, driver.findElements(ancestorBy).size()); + } + + @Test + void testScrollTillVisibleForDescendant() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy.FlutterBy descendantBy = AppiumBy.flutterDescendant( + AppiumBy.flutterKey("parent_card_4"), + AppiumBy.flutterText("Child 2") + ); + + assertEquals(0, driver.findElements(descendantBy).size()); + driver.scrollTillVisible(new ScrollParameter(descendantBy)); + // Make sure the card is visible after scrolling + assertEquals(1, driver.findElements(descendantBy).size()); + } } diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java index e8f78e414..f00301885 100644 --- a/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java @@ -5,6 +5,7 @@ import org.openqa.selenium.WebElement; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class FinderTests extends BaseFlutterTest { @@ -51,4 +52,33 @@ void testFlutterSemanticsLabel() { assertEquals(messageField.getText(), "Hello world"); } + + @Test + void testFlutterDescendant() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy descendantBy = AppiumBy.flutterDescendant( + AppiumBy.flutterKey("parent_card_1"), + AppiumBy.flutterText("Child 2") + ); + WebElement childElement = driver.findElement(descendantBy); + assertEquals("Child 2", + childElement.getText()); + } + + @Test + void testFlutterAncestor() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Nested Scroll"); + + AppiumBy ancestorBy = AppiumBy.flutterAncestor( + AppiumBy.flutterText("Child 2"), + AppiumBy.flutterKey("parent_card_1") + ); + WebElement parentElement = driver.findElement(ancestorBy); + assertTrue(parentElement.isDisplayed()); + } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java index 25574d727..58461127b 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java @@ -1,27 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.ios; -import io.appium.java_client.TestUtils; import io.appium.java_client.ios.options.XCUITestOptions; import org.junit.jupiter.api.BeforeAll; +import org.openqa.selenium.By; import org.openqa.selenium.SessionNotCreatedException; +import java.net.MalformedURLException; +import java.net.URL; import java.time.Duration; -public class AppIOSTest extends BaseIOSTest { - protected static final String BUNDLE_ID = "io.appium.TestApp"; +import static io.appium.java_client.AppiumBy.accessibilityId; +import static io.appium.java_client.AppiumBy.iOSClassChain; +import static io.appium.java_client.AppiumBy.iOSNsPredicateString; +import static io.appium.java_client.utils.TestUtils.IOS_SIM_VODQA_RELEASE_URL; - private static final String TEST_APP_ZIP = TestUtils.resourcePathToAbsolutePath("TestApp.app.zip").toString(); +public class AppIOSTest extends BaseIOSTest { + protected static final String BUNDLE_ID = "org.reactjs.native.example.VodQAReactNative"; + protected static final By LOGIN_LINK_ID = accessibilityId("login"); + protected static final By USERNAME_EDIT_PREDICATE = iOSNsPredicateString("name == \"username\""); + protected static final By PASSWORD_EDIT_PREDICATE = iOSNsPredicateString("name == \"password\""); + protected static final By SLIDER_MENU_ITEM_PREDICATE = iOSNsPredicateString("name == \"slider1\""); + protected static final By VODQA_LOGO_CLASS_CHAIN = iOSClassChain( + "**/XCUIElementTypeImage[`name CONTAINS \"vodqa\"`]" + ); @BeforeAll - public static void beforeClass() { + public static void beforeClass() throws MalformedURLException { startAppiumServer(); XCUITestOptions options = new XCUITestOptions() .setPlatformVersion(PLATFORM_VERSION) .setDeviceName(DEVICE_NAME) .setCommandTimeouts(Duration.ofSeconds(240)) - .setApp(TEST_APP_ZIP) + .setApp(new URL(IOS_SIM_VODQA_RELEASE_URL)) + .enableBiDi() .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT); + if (PREBUILT_WDA_PATH != null) { + options.usePreinstalledWda().setPrebuiltWdaPath(PREBUILT_WDA_PATH); + } try { driver = new IOSDriver(service.getUrl(), options); } catch (SessionNotCreatedException e) { diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java index cc571b6fa..baee302da 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java @@ -18,9 +18,11 @@ import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; +import io.appium.java_client.service.local.flags.GeneralServerFlag; import org.junit.jupiter.api.AfterAll; import java.time.Duration; +import java.util.Optional; @SuppressWarnings("checkstyle:HideUtilityClassConstructor") public class BaseIOSTest { @@ -28,14 +30,14 @@ public class BaseIOSTest { protected static AppiumDriverLocalService service; protected static IOSDriver driver; protected static final int PORT = 4723; - public static final String DEVICE_NAME = System.getenv("IOS_DEVICE_NAME") != null - ? System.getenv("IOS_DEVICE_NAME") - : "iPhone 12"; - public static final String PLATFORM_VERSION = System.getenv("IOS_PLATFORM_VERSION") != null - ? System.getenv("IOS_PLATFORM_VERSION") - : "14.5"; + public static final String DEVICE_NAME = Optional.ofNullable(System.getenv("IOS_DEVICE_NAME")) + .orElse("iPhone 17"); + public static final String PLATFORM_VERSION = Optional.ofNullable(System.getenv("IOS_PLATFORM_VERSION")) + .orElse("26.0"); public static final Duration WDA_LAUNCH_TIMEOUT = Duration.ofMinutes(4); public static final Duration SERVER_START_TIMEOUT = Duration.ofMinutes(3); + protected static String PREBUILT_WDA_PATH = System.getenv("PREBUILT_WDA_PATH"); + /** * Starts a local server. @@ -47,6 +49,7 @@ public static AppiumDriverLocalService startAppiumServer() { .withIPAddress("127.0.0.1") .usingPort(PORT) .withTimeout(SERVER_START_TIMEOUT) + .withArgument(GeneralServerFlag.ALLOW_INSECURE, "*:session_discovery") .build(); service.start(); return service; diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java index 752a0c539..8c54b30c4 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java @@ -16,22 +16,23 @@ package io.appium.java_client.ios; -import io.appium.java_client.TestUtils; import io.appium.java_client.ios.options.XCUITestOptions; import org.junit.jupiter.api.BeforeAll; import org.openqa.selenium.SessionNotCreatedException; +import java.io.IOException; +import java.net.URL; import java.time.Duration; import java.util.function.Supplier; -public class BaseIOSWebViewTest extends BaseIOSTest { - private static final String VODQA_ZIP = TestUtils.resourcePathToAbsolutePath("vodqa.zip").toString(); +import static io.appium.java_client.utils.TestUtils.IOS_SIM_VODQA_RELEASE_URL; +public class BaseIOSWebViewTest extends BaseIOSTest { private static final Duration WEB_VIEW_DETECT_INTERVAL = Duration.ofSeconds(2); private static final Duration WEB_VIEW_DETECT_DURATION = Duration.ofSeconds(30); @BeforeAll - public static void beforeClass() { + public static void beforeClass() throws IOException { startAppiumServer(); XCUITestOptions options = new XCUITestOptions() @@ -39,8 +40,10 @@ public static void beforeClass() { .setDeviceName(DEVICE_NAME) .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT) .setCommandTimeouts(Duration.ofSeconds(240)) - .setShowIosLog(true) - .setApp(VODQA_ZIP); + .setApp(new URL(IOS_SIM_VODQA_RELEASE_URL)); + if (PREBUILT_WDA_PATH != null) { + options.usePreinstalledWda().setPrebuiltWdaPath(PREBUILT_WDA_PATH); + } Supplier createDriver = () -> new IOSDriver(service.getUrl(), options); try { driver = createDriver.get(); diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java index 710f5dbf1..7468e89e9 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java @@ -34,6 +34,9 @@ public class BaseSafariTest extends BaseIOSTest { .setPlatformVersion(PLATFORM_VERSION) .setWebviewConnectTimeout(WEBVIEW_CONNECT_TIMEOUT) .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT); + if (PREBUILT_WDA_PATH != null) { + options.usePreinstalledWda().setPrebuiltWdaPath(PREBUILT_WDA_PATH); + } driver = new IOSDriver(service.getUrl(), options); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java index b8ed26a44..b3a58e588 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java @@ -25,6 +25,6 @@ public class ClipboardTest extends AppIOSTest { @Test public void verifySetAndGetClipboardText() { final String text = "Happy testing"; driver.setClipboardText(text); - assertEquals(driver.getClipboardText(), text); + assertEquals(text, driver.getClipboardText()); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java index 7d91e596a..eea8899b5 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java @@ -16,7 +16,6 @@ package io.appium.java_client.ios; -import io.appium.java_client.AppiumBy; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.MethodOrderer; @@ -35,34 +34,10 @@ @TestMethodOrder(MethodOrderer.MethodName.class) public class IOSAlertTest extends AppIOSTest { - private static final Duration ALERT_TIMEOUT = Duration.ofSeconds(5); private static final int CLICK_RETRIES = 2; - private static final String IOS_AUTOMATION_TEXT = "show alert"; - private final WebDriverWait waiter = new WebDriverWait(driver, ALERT_TIMEOUT); - private void ensureAlertPresence() { - int retry = 0; - // CI might not be performant enough, so we need to retry - while (true) { - try { - driver.findElement(AppiumBy.accessibilityId(IOS_AUTOMATION_TEXT)).click(); - } catch (WebDriverException e) { - // ignore - } - try { - waiter.until(alertIsPresent()); - return; - } catch (TimeoutException e) { - retry++; - if (retry >= CLICK_RETRIES) { - throw e; - } - } - } - } - @AfterEach public void afterEach() { try { @@ -97,4 +72,26 @@ public void getAlertTextTest() { ensureAlertPresence(); assertFalse(StringUtils.isBlank(driver.switchTo().alert().getText())); } + + private void ensureAlertPresence() { + int retry = 0; + // CI might not be performant enough, so we need to retry + while (true) { + try { + driver.findElement(PASSWORD_EDIT_PREDICATE).sendKeys("foo"); + driver.findElement(LOGIN_LINK_ID).click(); + } catch (WebDriverException e) { + // ignore + } + try { + waiter.until(alertIsPresent()); + return; + } catch (TimeoutException e) { + retry++; + if (retry >= CLICK_RETRIES) { + throw e; + } + } + } + } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSAppStringsTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSAppStringsTest.java deleted file mode 100644 index dc552dc91..000000000 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSAppStringsTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * 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 io.appium.java_client.ios; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -public class IOSAppStringsTest extends AppIOSTest { - - @Test public void getAppStrings() { - assertNotEquals(0, driver.getAppStringMap().size()); - } - - @Test public void getGetAppStringsUsingLang() { - assertNotEquals(0, driver.getAppStringMap("en").size()); - } - - @Test public void getAppStringsUsingLangAndFileStrings() { - assertNotEquals(0, driver.getAppStringMap("en", "Localizable.strings").size()); - } -} diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java new file mode 100644 index 000000000..d6288165d --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSBiDiTest.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.ios; + +import org.junit.jupiter.api.Test; +import org.openqa.selenium.bidi.Event; +import org.openqa.selenium.bidi.log.LogEntry; +import org.openqa.selenium.bidi.module.LogInspector; + +import java.util.concurrent.CopyOnWriteArrayList; + +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class IOSBiDiTest extends AppIOSTest { + + @Test + public void listenForIosLogsGeneric() { + var logs = new CopyOnWriteArrayList<>(); + var listenerId = driver.getBiDi().addListener( + NATIVE_CONTEXT, + new Event("log.entryAdded", m -> m), + logs::add + ); + try { + driver.getPageSource(); + } finally { + driver.getBiDi().removeListener(listenerId); + } + assertFalse(logs.isEmpty()); + } + + @Test + public void listenForIosLogsSpecific() { + var logs = new CopyOnWriteArrayList(); + try (var logInspector = new LogInspector(NATIVE_CONTEXT, driver)) { + logInspector.onLog(logs::add); + driver.getPageSource(); + } + assertFalse(logs.isEmpty()); + } + +} diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java index f2ac548b1..b746edd24 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java @@ -17,8 +17,11 @@ package io.appium.java_client.ios; import io.appium.java_client.NoSuchContextException; +import io.appium.java_client.utils.TestUtils; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,21 +30,30 @@ public class IOSContextTest extends BaseIOSWebViewTest { @Test public void testGetContext() { - assertEquals("NATIVE_APP", driver.getContext()); + assertEquals(NATIVE_CONTEXT, driver.getContext()); } @Test public void testGetContextHandles() { - assertEquals(driver.getContextHandles().size(), 2); + // this test is not stable in the CI env due to simulator slowness + Assumptions.assumeFalse(TestUtils.isCiEnv()); + + assertEquals(2, driver.getContextHandles().size()); } @Test public void testSwitchContext() throws InterruptedException { + // this test is not stable in the CI env due to simulator slowness + Assumptions.assumeFalse(TestUtils.isCiEnv()); + driver.getContextHandles(); findAndSwitchToWebView(); assertThat(driver.getContext(), containsString("WEBVIEW")); - driver.context("NATIVE_APP"); + driver.context(NATIVE_CONTEXT); } @Test public void testContextError() { + // this test is not stable in the CI env due to simulator slowness + Assumptions.assumeFalse(TestUtils.isCiEnv()); + assertThrows(NoSuchContextException.class, () -> driver.context("Planet of the Ape-ium")); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java index 438178e36..c729d5b93 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java @@ -21,9 +21,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.openqa.selenium.By; import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebElement; +import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.remote.Response; import org.openqa.selenium.remote.http.HttpMethod; @@ -31,10 +30,9 @@ import java.time.Duration; import java.util.Map; -import static io.appium.java_client.TestUtils.waitUntilTrue; +import static io.appium.java_client.utils.TestUtils.waitUntilTrue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -50,28 +48,27 @@ public void setupEach() { @Test public void addCustomCommandTest() { - driver.addCommand(HttpMethod.GET, "/sessions", "getSessions"); + driver.addCommand(HttpMethod.GET, "/appium/sessions", "getSessions"); final Response getSessions = driver.execute("getSessions"); assertNotNull(getSessions.getSessionId()); } @Test public void addCustomCommandWithSessionIdTest() { - driver.addCommand(HttpMethod.POST, "/session/" + driver.getSessionId() + "/appium/app/strings", - "getAppStrings"); - final Response getStrings = driver.execute("getAppStrings"); - assertNotNull(getStrings.getSessionId()); + driver.addCommand(HttpMethod.GET, "/session/" + driver.getSessionId() + "/appium/settings", + "getSessionSettings"); + final Response getSessionSettings = driver.execute("getSessionSettings"); + assertNotNull(getSessionSettings.getSessionId()); } @Test public void addCustomCommandWithElementIdTest() { - WebElement intA = driver.findElement(By.id("IntegerA")); - intA.clear(); + var usernameEdit = driver.findElement(USERNAME_EDIT_PREDICATE); driver.addCommand(HttpMethod.POST, String.format("/session/%s/appium/element/%s/value", driver.getSessionId(), - ((RemoteWebElement) intA).getId()), "setNewValue"); + ((RemoteWebElement) usernameEdit).getId()), "setNewValue"); final Response setNewValue = driver.execute("setNewValue", - Map.of("id", ((RemoteWebElement) intA).getId(), "text", "8")); + Map.of("id", ((RemoteWebElement) usernameEdit).getId(), "text", "foo")); assertNotNull(setNewValue.getSessionId()); } @@ -97,10 +94,17 @@ public void getDeviceTimeTest() { } @Test public void orientationTest() { - assertEquals(ScreenOrientation.PORTRAIT, driver.getOrientation()); - driver.rotate(ScreenOrientation.LANDSCAPE); - assertEquals(ScreenOrientation.LANDSCAPE, driver.getOrientation()); - driver.rotate(ScreenOrientation.PORTRAIT); + rotateWithRetry(ScreenOrientation.LANDSCAPE); + waitUntilTrue( + () -> driver.getOrientation() == ScreenOrientation.LANDSCAPE, + Duration.ofSeconds(5), Duration.ofMillis(500) + ); + + rotateWithRetry(ScreenOrientation.PORTRAIT); + waitUntilTrue( + () -> driver.getOrientation() == ScreenOrientation.PORTRAIT, + Duration.ofSeconds(5), Duration.ofMillis(500) + ); } @Test public void lockTest() { @@ -114,17 +118,15 @@ public void getDeviceTimeTest() { } @Test public void pullFileTest() { - byte[] data = driver.pullFile(String.format("@%s/TestApp", BUNDLE_ID)); + byte[] data = driver.pullFile(String.format("@%s/VodQAReactNative", BUNDLE_ID)); assertThat(data.length, greaterThan(0)); } @Test public void keyboardTest() { - WebElement element = driver.findElement(By.id("IntegerA")); - element.click(); + driver.findElement(USERNAME_EDIT_PREDICATE).click(); assertTrue(driver.isKeyboardShown()); } - @Disabled("The app crashes when restored from the background") @Test public void putAppIntoBackgroundAndRestoreTest() { final long msStarted = System.currentTimeMillis(); @@ -132,7 +134,6 @@ public void putAppIntoBackgroundAndRestoreTest() { assertThat(System.currentTimeMillis() - msStarted, greaterThan(3000L)); } - @Disabled("The app crashes when restored from the background") @Test public void applicationsManagementTest() { driver.runAppInBackground(Duration.ofSeconds(-1)); @@ -145,23 +146,26 @@ public void applicationsManagementTest() { Duration.ofSeconds(10), Duration.ofSeconds(1)); } - @Disabled("The app crashes when restored from the background") - @Test - public void putAIntoBackgroundWithoutRestoreTest() { - waitUntilTrue(() -> !driver.findElements(By.id("IntegerA")).isEmpty(), - Duration.ofSeconds(10), Duration.ofSeconds(1)); - driver.runAppInBackground(Duration.ofSeconds(-1)); - waitUntilTrue(() -> driver.findElements(By.id("IntegerA")).isEmpty(), - Duration.ofSeconds(10), Duration.ofSeconds(1)); - driver.activateApp(BUNDLE_ID); - } - - @Disabled - @Test public void touchIdTest() { - driver.toggleTouchIDEnrollment(true); - driver.performTouchID(true); - driver.performTouchID(false); - //noinspection SimplifiableAssertion,EqualsWithItself - assertEquals(true, true); + private void rotateWithRetry(ScreenOrientation orientation) { + final int maxRetries = 3; + final Duration retryDelay = Duration.ofSeconds(1); + + for (int attempt = 0; attempt < maxRetries; attempt++) { + try { + driver.rotate(orientation); + return; + } catch (WebDriverException e) { + if (attempt < maxRetries - 1) { + try { + Thread.sleep(retryDelay.toMillis()); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException(ie); + } + continue; + } + throw e; + } + } } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSElementTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSElementTest.java index 5d3d943a5..1439a4100 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSElementTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSElementTest.java @@ -1,37 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.ios; -import io.appium.java_client.AppiumBy; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.time.Duration; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNot.not; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.openqa.selenium.By.className; -@TestMethodOrder(MethodOrderer.MethodName.class) public class IOSElementTest extends AppIOSTest { + private static final By SLIDER_CLASS = className("XCUIElementTypeSlider"); - @Test - public void findByAccessibilityIdTest() { - assertThat(driver.findElements(AppiumBy.accessibilityId("Compute Sum")).size(), not(is(0))); - } - - // FIXME: Stabilize the test on CI - @Disabled @Test public void setValueTest() { - WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20)); + driver.findElement(LOGIN_LINK_ID).click(); + driver.findElement(SLIDER_MENU_ITEM_PREDICATE).click(); - WebElement slider = wait.until( - driver1 -> driver1.findElement(AppiumBy.className("XCUIElementTypeSlider"))); - slider.sendKeys("0%"); - assertEquals("0%", slider.getAttribute("value")); + WebElement slider; + try { + slider = driver.findElement(SLIDER_CLASS); + } catch (WebDriverException e) { + Assumptions.assumeTrue( + false, + "The slider element is not presented properly by the current RN build" + ); + return; + } + var previousValue = slider.getAttribute("value"); + slider.sendKeys("0.5"); + assertNotEquals(slider.getAttribute("value"), previousValue); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java index 5af445b0b..e3fa303e6 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java @@ -16,37 +16,25 @@ package io.appium.java_client.ios; -import io.appium.java_client.AppiumBy; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class IOSSearchingTest extends AppIOSTest { - - @Test public void findByAccessibilityIdTest() { - assertNotEquals(driver - .findElement(AppiumBy.accessibilityId("ComputeSumButton")) - .getText(), null); - assertNotEquals(driver - .findElements(AppiumBy.accessibilityId("ComputeSumButton")) - .size(), 0); + @Test + public void findByAccessibilityIdTest() { + assertNotNull(driver.findElement(LOGIN_LINK_ID).getText()); + assertNotEquals(0, driver.findElements(LOGIN_LINK_ID).size()); } - @Test public void findByByIosPredicatesTest() { - assertNotEquals(driver - .findElement(AppiumBy.iOSNsPredicateString("name like 'Answer'")) - .getText(), null); - assertNotEquals(driver - .findElements(AppiumBy.iOSNsPredicateString("name like 'Answer'")) - .size(), 0); + @Test + public void findByByIosPredicatesTest() { + assertNotNull(driver.findElement(USERNAME_EDIT_PREDICATE).getText()); + assertNotEquals(0, driver.findElements(USERNAME_EDIT_PREDICATE).size()); } @Test public void findByByIosClassChainTest() { - assertNotEquals(driver - .findElement(AppiumBy.iOSClassChain("**/XCUIElementTypeButton")) - .getText(), null); - assertNotEquals(driver - .findElements(AppiumBy.iOSClassChain("**/XCUIElementTypeButton")) - .size(), 0); + assertNotEquals(0, driver.findElements(VODQA_LOGO_CLASS_CHAIN).size()); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java index 60943342e..1895e3517 100644 --- a/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java @@ -1,6 +1,8 @@ package io.appium.java_client.ios; import io.appium.java_client.AppiumBy; +import io.appium.java_client.utils.TestUtils; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -15,6 +17,9 @@ public class IOSWebViewTest extends BaseIOSWebViewTest { @Test public void webViewPageTestCase() throws InterruptedException { + // this test is not stable in the CI env + Assumptions.assumeFalse(TestUtils.isCiEnv()); + new WebDriverWait(driver, LOOKUP_TIMEOUT) .until(ExpectedConditions.presenceOfElementLocated(By.id("login"))) .click(); diff --git a/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java b/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java index 9660da6a4..7d89bd331 100644 --- a/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java @@ -32,12 +32,8 @@ import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.CHAIN; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @TestMethodOrder(MethodOrderer.MethodName.class) @@ -46,52 +42,40 @@ public class XCUITModeTest extends AppIOSTest { private boolean populated = false; @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "label contains 'Compute'") - @iOSXCUITFindBy(className = "XCUIElementTypeButton") - private WebElement computeButton; + @iOSXCUITFindBy(iOSNsPredicate = "name == \"assets/assets/vodqa.png\"") + @iOSXCUITFindBy(className = "XCUIElementTypeImage") + private WebElement logoImageAllPossible; @HowToUseLocators(iOSXCUITAutomation = CHAIN) - @iOSXCUITFindBy(iOSNsPredicate = "name like 'Answer'") - private WebElement answer; + @iOSXCUITFindBy(iOSNsPredicate = "name CONTAINS 'vodqa'") + private WebElement logoImageChain; - @iOSXCUITFindBy(iOSNsPredicate = "name = 'IntegerA'") - private WebElement textField1; + @iOSXCUITFindBy(iOSNsPredicate = "name == 'username'") + private WebElement usernameFieldPredicate; - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "name = 'IntegerB'") - @iOSXCUITFindBy(accessibility = "IntegerB") - private WebElement textField2; - - @iOSXCUITFindBy(iOSNsPredicate = "name ENDSWITH 'Gesture'") - private WebElement gesture; - - @iOSXCUITFindBy(className = "XCUIElementTypeSlider") - private WebElement slider; + @iOSXCUITFindBy(iOSNsPredicate = "name ENDSWITH '.png'") + private WebElement logoImagePredicate; - @iOSXCUITFindBy(id = "locationStatus") - private WebElement locationStatus; - - @HowToUseLocators(iOSXCUITAutomation = CHAIN) - @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'contact'") - private WebElement contactAlert; + @iOSXCUITFindBy(className = "XCUIElementTypeImage") + private WebElement logoImageClass; - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'location'") - private WebElement locationAlert; + @iOSXCUITFindBy(accessibility = "login") + private WebElement loginLinkAccId; - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeTextField[2]") - private WebElement secondTextField; + @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeTextField[`name == \"username\"`]") + private WebElement usernameFieldClassChain; - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton[-1]") - private WebElement lastButton; + @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeSecureTextField[`name == \"password\"`][-1]") + private WebElement passwordFieldClassChain; - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton") - private List allButtons; + @iOSXCUITFindBy(iOSClassChain = "**/*[`type CONTAINS \"TextField\"`]") + private List allTextFields; /** * The setting up. */ - @BeforeEach public void setUp() { + @BeforeEach + public void setUp() { if (!populated) { PageFactory.initElements(new AppiumFieldDecorator(driver), this); } @@ -99,51 +83,48 @@ public class XCUITModeTest extends AppIOSTest { populated = true; } - @Test public void findByXCUITSelectorTest() { - assertNotEquals(null, computeButton.getText()); - } - - @Test public void findElementByNameTest() { - assertEquals("TextField1", textField1.getText()); - } - - @Test public void findElementByClassNameTest() { - assertEquals("50%", slider.getAttribute("value")); + @Test + public void findByXCUITSelectorTest() { + assertTrue(logoImageAllPossible.isDisplayed()); } - @Test public void pageObjectChainingTest() { - assertTrue(contactAlert.isDisplayed()); + @Test + public void findElementByNameTest() { + assertTrue(usernameFieldPredicate.isDisplayed()); } - @Test public void findElementByIdTest() { - assertTrue(locationStatus.isDisplayed()); + @Test + public void findElementByClassNameTest() { + assertTrue(logoImageClass.isDisplayed()); } - @Test public void nativeSelectorTest() { - assertTrue(locationAlert.isDisplayed()); + @Test + public void pageObjectChainingTest() { + assertTrue(logoImageChain.isDisplayed()); } - @Test public void findElementByClassChain() { - assertThat(secondTextField.getAttribute("name"), equalTo("IntegerB")); + @Test + public void findElementByIdTest() { + assertTrue(loginLinkAccId.isDisplayed()); } - @Test public void findElementByClassChainWithNegativeIndex() { - assertThat(lastButton.getAttribute("name"), equalTo("Check calendar authorized")); + @Test + public void nativeSelectorTest() { + assertTrue(logoImagePredicate.isDisplayed()); } - @Test public void findMultipleElementsByClassChain() { - assertThat(allButtons.size(), is(greaterThan(1))); + @Test + public void findElementByClassChain() { + assertTrue(usernameFieldClassChain.isDisplayed()); } - @Test public void findElementByXUISelectorTest() { - assertNotNull(gesture.getText()); + @Test + public void findElementByClassChainWithNegativeIndex() { + assertTrue(passwordFieldClassChain.isDisplayed()); } - @Test public void setValueTest() { - textField1.sendKeys("2"); - textField2.sendKeys("4"); - driver.hideKeyboard(); - computeButton.click(); - assertEquals("6", answer.getText()); + @Test + public void findMultipleElementsByClassChain() { + assertThat(allTextFields.size(), is(greaterThan(1))); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java b/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java index b262e93af..256c43835 100644 --- a/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java @@ -16,7 +16,6 @@ package io.appium.java_client.service.local; -import io.appium.java_client.TestUtils; import io.appium.java_client.ios.BaseIOSTest; import io.appium.java_client.ios.IOSDriver; import io.appium.java_client.ios.options.XCUITestOptions; @@ -27,7 +26,12 @@ import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; + import static io.appium.java_client.remote.options.SupportsDeviceNameOption.DEVICE_NAME_OPTION; +import static io.appium.java_client.utils.TestUtils.IOS_SIM_VODQA_RELEASE_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -35,14 +39,13 @@ import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; class StartingAppLocallyIosTest { - private static final String UI_CATALOG_ZIP = TestUtils.resourcePathToAbsolutePath("UICatalog.app.zip").toString(); - @Test - void startingIOSAppWithCapabilitiesOnlyTest() { + void startingIOSAppWithCapabilitiesOnlyTest() throws MalformedURLException { + var appUrl = new URL(IOS_SIM_VODQA_RELEASE_URL); XCUITestOptions options = new XCUITestOptions() .setPlatformVersion(BaseIOSTest.PLATFORM_VERSION) .setDeviceName(BaseIOSTest.DEVICE_NAME) - .setApp(UI_CATALOG_ZIP) + .setApp(appUrl) .setWdaLaunchTimeout(BaseIOSTest.WDA_LAUNCH_TIMEOUT); IOSDriver driver = new IOSDriver(options); try { @@ -52,19 +55,19 @@ void startingIOSAppWithCapabilitiesOnlyTest() { assertEquals(Platform.IOS, caps.getPlatformName()); assertNotNull(caps.getDeviceName().orElse(null)); assertEquals(BaseIOSTest.PLATFORM_VERSION, caps.getPlatformVersion().orElse(null)); - assertEquals(UI_CATALOG_ZIP, caps.getApp().orElse(null)); + assertEquals(appUrl.toString(), caps.getApp().orElse(null)); } finally { driver.quit(); } } - @Test - void startingIOSAppWithCapabilitiesAndServiceTest() { + void startingIOSAppWithCapabilitiesAndServiceTest() throws MalformedURLException { + var appUrl = new URL(IOS_SIM_VODQA_RELEASE_URL); XCUITestOptions options = new XCUITestOptions() .setPlatformVersion(BaseIOSTest.PLATFORM_VERSION) .setDeviceName(BaseIOSTest.DEVICE_NAME) - .setApp(UI_CATALOG_ZIP) + .setApp(appUrl) .setWdaLaunchTimeout(BaseIOSTest.WDA_LAUNCH_TIMEOUT); AppiumServiceBuilder builder = new AppiumServiceBuilder() @@ -75,7 +78,7 @@ void startingIOSAppWithCapabilitiesAndServiceTest() { IOSDriver driver = new IOSDriver(builder, options); try { Capabilities caps = driver.getCapabilities(); - assertTrue(caps.getCapability(PLATFORM_NAME) + assertTrue(Objects.requireNonNull(caps.getCapability(PLATFORM_NAME)) .toString().equalsIgnoreCase(MobilePlatform.IOS)); assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); } finally { @@ -84,14 +87,15 @@ void startingIOSAppWithCapabilitiesAndServiceTest() { } @Test - void startingIOSAppWithCapabilitiesAndFlagsOnServerSideTest() { + void startingIOSAppWithCapabilitiesAndFlagsOnServerSideTest() throws MalformedURLException { + var appUrl = new URL(IOS_SIM_VODQA_RELEASE_URL); XCUITestOptions serverOptions = new XCUITestOptions() .setPlatformVersion(BaseIOSTest.PLATFORM_VERSION) .setDeviceName(BaseIOSTest.DEVICE_NAME) .setWdaLaunchTimeout(BaseIOSTest.WDA_LAUNCH_TIMEOUT); XCUITestOptions clientOptions = new XCUITestOptions() - .setApp(UI_CATALOG_ZIP); + .setApp(appUrl); AppiumServiceBuilder builder = new AppiumServiceBuilder() .withArgument(GeneralServerFlag.SESSION_OVERRIDE) diff --git a/src/e2eIosTest/resources/TestApp.app.zip b/src/e2eIosTest/resources/TestApp.app.zip deleted file mode 100644 index 9bce35781..000000000 Binary files a/src/e2eIosTest/resources/TestApp.app.zip and /dev/null differ diff --git a/src/e2eIosTest/resources/UICatalog.app.zip b/src/e2eIosTest/resources/UICatalog.app.zip deleted file mode 100644 index e483caad5..000000000 Binary files a/src/e2eIosTest/resources/UICatalog.app.zip and /dev/null differ diff --git a/src/e2eIosTest/resources/vodqa.zip b/src/e2eIosTest/resources/vodqa.zip deleted file mode 100644 index 74b980dec..000000000 Binary files a/src/e2eIosTest/resources/vodqa.zip and /dev/null differ diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index aca01fea7..1c24b29c1 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -23,9 +23,12 @@ import org.openqa.selenium.By.Remotable; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; +import org.openqa.selenium.json.Json; import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static com.google.common.base.Strings.isNullOrEmpty; @@ -84,10 +87,10 @@ public static By androidDataMatcher(final String dataMatcherString) { } /** - * Refer to https://developer.android.com/training/testing/ui-automator + * Refer to UI Automator . * * @param uiautomatorText is Android UIAutomator string - * @return an instance of {@link AppiumBy.ByAndroidUIAutomator} + * @return an instance of {@link ByAndroidUIAutomator} */ public static By androidUIAutomator(final String uiautomatorText) { return new ByAndroidUIAutomator(uiautomatorText); @@ -169,9 +172,9 @@ public static By custom(final String selector) { * as for OpenCV library. * @return an instance of {@link ByImage} * @see - * The documentation on Image Comparison Features + * The documentation on Image Comparison Features * @see - * The settings available for lookup fine-tuning + * The settings available for lookup fine-tuning * @since Appium 1.8.2 */ public static By image(final String b64Template) { @@ -250,6 +253,57 @@ public static FlutterBy flutterSemanticsLabel(final String semanticsLabel) { return new ByFlutterSemanticsLabel(semanticsLabel); } + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the parent widget locator + * @param matching represents the descendant widget locator to match + * @param matchRoot determines whether to include the root widget in the search + * @param skipOffstage determines whether to skip offstage widgets + * @return an instance of {@link AppiumBy.ByFlutterDescendant} + */ + public static FlutterBy flutterDescendant( + final FlutterBy of, + final FlutterBy matching, + boolean matchRoot, + boolean skipOffstage) { + return new ByFlutterDescendant(of, matching, matchRoot, skipOffstage); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the parent widget locator + * @param matching represents the descendant widget locator to match + * @return an instance of {@link AppiumBy.ByFlutterDescendant} + */ + public static FlutterBy flutterDescendant(final FlutterBy of, final FlutterBy matching) { + return flutterDescendant(of, matching, false, true); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the child widget locator + * @param matching represents the ancestor widget locator to match + * @param matchRoot determines whether to include the root widget in the search + * @return an instance of {@link AppiumBy.ByFlutterAncestor} + */ + public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching, boolean matchRoot) { + return new ByFlutterAncestor(of, matching, matchRoot); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode since version 1.4.0. + * + * @param of represents the child widget locator + * @param matching represents the ancestor widget locator to match + * @return an instance of {@link AppiumBy.ByFlutterAncestor} + */ + public static FlutterBy flutterAncestor(final FlutterBy of, final FlutterBy matching) { + return flutterAncestor(of, matching, false); + } + public static class ByAccessibilityId extends AppiumBy implements Serializable { public ByAccessibilityId(String accessibilityId) { super("accessibility id", accessibilityId, "accessibilityId"); @@ -328,6 +382,32 @@ protected FlutterBy(String selector, String locatorString, String locatorName) { } } + public abstract static class FlutterByHierarchy extends FlutterBy { + private static final Json JSON = new Json(); + + protected FlutterByHierarchy( + String selector, + FlutterBy of, + FlutterBy matching, + Map properties, + String locatorName) { + super(selector, formatLocator(of, matching, properties), locatorName); + } + + static Map parseFlutterLocator(FlutterBy by) { + Parameters params = by.getRemoteParameters(); + return Map.of("using", params.using(), "value", params.value()); + } + + static String formatLocator(FlutterBy of, FlutterBy matching, Map properties) { + Map locator = new HashMap<>(); + locator.put("of", parseFlutterLocator(of)); + locator.put("matching", parseFlutterLocator(matching)); + locator.put("parameters", properties); + return JSON.toJson(locator); + } + } + public static class ByFlutterType extends FlutterBy implements Serializable { protected ByFlutterType(String locatorString) { super("-flutter type", locatorString, "flutterType"); @@ -358,4 +438,23 @@ protected ByFlutterTextContaining(String locatorString) { } } + public static class ByFlutterDescendant extends FlutterByHierarchy implements Serializable { + protected ByFlutterDescendant(FlutterBy of, FlutterBy matching, boolean matchRoot, boolean skipOffstage) { + super( + "-flutter descendant", + of, + matching, + Map.of("matchRoot", matchRoot, "skipOffstage", skipOffstage), "flutterDescendant"); + } + } + + public static class ByFlutterAncestor extends FlutterByHierarchy implements Serializable { + protected ByFlutterAncestor(FlutterBy of, FlutterBy matching, boolean matchRoot) { + super( + "-flutter ancestor", + of, + matching, + Map.of("matchRoot", matchRoot), "flutterAncestor"); + } + } } diff --git a/src/main/java/io/appium/java_client/AppiumClientConfig.java b/src/main/java/io/appium/java_client/AppiumClientConfig.java index e6ebfd0bb..49097f341 100644 --- a/src/main/java/io/appium/java_client/AppiumClientConfig.java +++ b/src/main/java/io/appium/java_client/AppiumClientConfig.java @@ -18,12 +18,12 @@ import io.appium.java_client.internal.filters.AppiumIdempotencyFilter; import io.appium.java_client.internal.filters.AppiumUserAgentFilter; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.Credentials; import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.http.ClientConfig; import org.openqa.selenium.remote.http.Filter; -import javax.annotation.Nullable; import javax.net.ssl.SSLContext; import java.net.Proxy; import java.net.URI; diff --git a/src/main/java/io/appium/java_client/AppiumCommandInfo.java b/src/main/java/io/appium/java_client/AppiumCommandInfo.java index cea6016d5..e41ba3699 100644 --- a/src/main/java/io/appium/java_client/AppiumCommandInfo.java +++ b/src/main/java/io/appium/java_client/AppiumCommandInfo.java @@ -26,7 +26,7 @@ public class AppiumCommandInfo extends CommandInfo { @Getter(AccessLevel.PUBLIC) private final HttpMethod method; /** - * It conntains method and URL of the command. + * It contains method and URL of the command. * * @param url command URL * @param method is http-method diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index c8d660e9f..7fa2b3629 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -17,22 +17,26 @@ package io.appium.java_client; import io.appium.java_client.internal.CapabilityHelpers; -import io.appium.java_client.internal.ReflectionHelpers; import io.appium.java_client.internal.SessionHelpers; import io.appium.java_client.remote.AppiumCommandExecutor; import io.appium.java_client.remote.AppiumW3CHttpCommandCodec; import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsWebSocketUrlOption; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; import lombok.Getter; +import org.jspecify.annotations.NonNull; import org.openqa.selenium.Capabilities; import org.openqa.selenium.ImmutableCapabilities; -import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.OutputType; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.BiDiException; +import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.remote.CapabilityType; +import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.DriverCommand; import org.openqa.selenium.remote.ErrorHandler; import org.openqa.selenium.remote.ExecuteMethod; @@ -40,10 +44,11 @@ import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.Response; import org.openqa.selenium.remote.codec.w3c.W3CHttpResponseCodec; -import org.openqa.selenium.remote.html5.RemoteLocationContext; import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpMethod; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.Collections; @@ -68,16 +73,18 @@ public class AppiumDriver extends RemoteWebDriver implements LogsEvents, HasBrowserCheck, CanRememberExtensionPresence, - HasSettings { + HasSettings, + HasBiDi { private static final ErrorHandler ERROR_HANDLER = new ErrorHandler(new ErrorCodesMobile(), true); // frequently used command parameters @Getter private final URL remoteAddress; - @Deprecated(forRemoval = true) - protected final RemoteLocationContext locationContext; private final ExecuteMethod executeMethod; private final Set absentExtensionNames = new HashSet<>(); + private URI biDiUri; + private BiDi biDi; + private boolean wasBiDiRequested = false; /** * Creates a new instance based on command {@code executor} and {@code capabilities}. @@ -90,7 +97,6 @@ public class AppiumDriver extends RemoteWebDriver implements public AppiumDriver(HttpCommandExecutor executor, Capabilities capabilities) { super(executor, capabilities); this.executeMethod = new AppiumExecutionMethod(this); - locationContext = new RemoteLocationContext(executeMethod); super.setErrorHandler(ERROR_HANDLER); this.remoteAddress = executor.getAddressOfRemoteServer(); } @@ -147,17 +153,15 @@ public AppiumDriver(Capabilities capabilities) { * !!! This API is supposed to be used for **debugging purposes only**. * * @param remoteSessionAddress The address of the **running** session including the session identifier. - * @param platformName The name of the target platform. - * @param automationName The name of the target automation. + * @param platformName The name of the target platform. + * @param automationName The name of the target automation. */ public AppiumDriver(URL remoteSessionAddress, String platformName, String automationName) { super(); - ReflectionHelpers.setPrivateFieldValue( - RemoteWebDriver.class, this, "capabilities", new ImmutableCapabilities( - Map.of( - PLATFORM_NAME, platformName, - APPIUM_PREFIX + AUTOMATION_NAME_OPTION, automationName - ) + this.capabilities = new ImmutableCapabilities( + Map.of( + PLATFORM_NAME, platformName, + APPIUM_PREFIX + AUTOMATION_NAME_OPTION, automationName ) ); SessionHelpers.SessionAddress sessionAddress = SessionHelpers.parseSessionAddress(remoteSessionAddress); @@ -168,61 +172,12 @@ public AppiumDriver(URL remoteSessionAddress, String platformName, String automa executor.setResponseCodec(new W3CHttpResponseCodec()); setCommandExecutor(executor); this.executeMethod = new AppiumExecutionMethod(this); - locationContext = new RemoteLocationContext(executeMethod); super.setErrorHandler(ERROR_HANDLER); this.remoteAddress = executor.getAddressOfRemoteServer(); setSessionId(sessionAddress.getId()); } - /** - * Changes platform name if it is not set and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultName a platformName value which has to be set up - * @return {@link Capabilities} with changed platform name value or the original capabilities - */ - protected static Capabilities ensurePlatformName( - Capabilities originalCapabilities, String defaultName) { - return originalCapabilities.getPlatformName() == null - ? originalCapabilities.merge(new ImmutableCapabilities(PLATFORM_NAME, defaultName)) - : originalCapabilities; - } - - /** - * Changes automation name if it is not set and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultName a platformName value which has to be set up - * @return {@link Capabilities} with changed mobile automation name value or the original capabilities - */ - protected static Capabilities ensureAutomationName( - Capabilities originalCapabilities, String defaultName) { - String currentAutomationName = CapabilityHelpers.getCapability( - originalCapabilities, AUTOMATION_NAME_OPTION, String.class); - if (isNullOrEmpty(currentAutomationName)) { - String capabilityName = originalCapabilities.getCapabilityNames() - .contains(AUTOMATION_NAME_OPTION) ? AUTOMATION_NAME_OPTION : APPIUM_PREFIX + AUTOMATION_NAME_OPTION; - return originalCapabilities.merge(new ImmutableCapabilities(capabilityName, defaultName)); - } - return originalCapabilities; - } - - /** - * Changes platform and automation names if they are not set - * and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultPlatformName a platformName value which has to be set up - * @param defaultAutomationName The default automation name to set up for this class - * @return {@link Capabilities} with changed platform/automation name value or the original capabilities - */ - protected static Capabilities ensurePlatformAndAutomationNames( - Capabilities originalCapabilities, String defaultPlatformName, String defaultAutomationName) { - Capabilities capsWithPlatformFixed = ensurePlatformName(originalCapabilities, defaultPlatformName); - return ensureAutomationName(capsWithPlatformFixed, defaultAutomationName); - } - @Override public ExecuteMethod getExecuteMethod() { return executeMethod; @@ -246,58 +201,23 @@ public Map getStatus() { * @param methodName The name of custom appium command. */ public void addCommand(HttpMethod httpMethod, String url, String methodName) { + CommandInfo commandInfo; switch (httpMethod) { case GET: - MobileCommand.commandRepository.put(methodName, MobileCommand.getC(url)); + commandInfo = MobileCommand.getC(url); break; case POST: - MobileCommand.commandRepository.put(methodName, MobileCommand.postC(url)); + commandInfo = MobileCommand.postC(url); break; case DELETE: - MobileCommand.commandRepository.put(methodName, MobileCommand.deleteC(url)); + commandInfo = MobileCommand.deleteC(url); break; default: throw new WebDriverException(String.format("Unsupported HTTP Method: %s. Only %s methods are supported", httpMethod, Arrays.toString(HttpMethod.values()))); } - ((AppiumCommandExecutor) getCommandExecutor()).refreshAdditionalCommands(); - } - - @Override - protected void startSession(Capabilities capabilities) { - var response = Optional.ofNullable( - execute(DriverCommand.NEW_SESSION(singleton(capabilities))) - ).orElseThrow(() -> new SessionNotCreatedException( - "The underlying command executor returned a null response." - )); - - var rawCapabilities = Optional.ofNullable(response.getValue()) - .map(value -> { - if (!(value instanceof Map)) { - throw new SessionNotCreatedException(String.format( - "The underlying command executor returned a response " - + "with a non well formed payload: %s", response) - ); - } - //noinspection unchecked - return (Map) value; - }) - .orElseThrow(() -> new SessionNotCreatedException( - "The underlying command executor returned a response without payload: " + response) - ); - - // TODO: remove this workaround for Selenium API enforcing some legacy capability values in major version - rawCapabilities.remove("platform"); - if (rawCapabilities.containsKey(CapabilityType.BROWSER_NAME) - && isNullOrEmpty((String) rawCapabilities.get(CapabilityType.BROWSER_NAME))) { - rawCapabilities.remove(CapabilityType.BROWSER_NAME); - } - MutableCapabilities returnedCapabilities = new BaseOptions<>(rawCapabilities); - ReflectionHelpers.setPrivateFieldValue( - RemoteWebDriver.class, this, "capabilities", returnedCapabilities - ); - setSessionId(response.getSessionId()); + ((AppiumCommandExecutor) getCommandExecutor()).defineCommand(methodName, commandInfo); } @Override @@ -344,9 +264,164 @@ public AppiumDriver markExtensionAbsence(String extName) { return this; } + @Override + public Optional maybeGetBiDi() { + return Optional.ofNullable(this.biDi); + } + + @Override + @NonNull + public BiDi getBiDi() { + var webSocketUrl = ((BaseOptions) this.capabilities).getWebSocketUrl().orElseThrow( + () -> { + var suffix = wasBiDiRequested + ? "Do both the server and the driver declare BiDi support?" + : String.format("Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL); + return new BiDiException(String.format( + "BiDi is not enabled for this driver session. %s", suffix + )); + } + ); + if (this.biDiUri == null) { + throw new BiDiException( + String.format( + "BiDi is not enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", + SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl + ) + ); + } + if (this.biDi == null) { + // This should not happen + throw new IllegalStateException(); + } + return this.biDi; + } + protected HttpClient getHttpClient() { - return ReflectionHelpers.getPrivateFieldValue( - HttpCommandExecutor.class, getCommandExecutor(), "client", HttpClient.class + return ((HttpCommandExecutor) getCommandExecutor()).client; + } + + @Override + protected void startSession(Capabilities requestCapabilities) { + var response = Optional.ofNullable( + execute(DriverCommand.NEW_SESSION(singleton(requestCapabilities))) + ).orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a null response." + )); + + var rawResponseCapabilities = Optional.ofNullable(response.getValue()) + .map(value -> { + if (!(value instanceof Map)) { + throw new SessionNotCreatedException(String.format( + "The underlying command executor returned a response " + + "with a non well formed payload: %s", response) + ); + } + //noinspection unchecked + return (Map) value; + }) + .orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a response without payload: " + response) + ); + + // TODO: remove this workaround for Selenium API enforcing some legacy capability values in major version + rawResponseCapabilities.remove("platform"); + if (rawResponseCapabilities.containsKey(CapabilityType.BROWSER_NAME) + && isNullOrEmpty((String) rawResponseCapabilities.get(CapabilityType.BROWSER_NAME))) { + rawResponseCapabilities.remove(CapabilityType.BROWSER_NAME); + } + this.capabilities = new BaseOptions<>(rawResponseCapabilities); + this.wasBiDiRequested = Boolean.TRUE.equals( + requestCapabilities.getCapability(SupportsWebSocketUrlOption.WEB_SOCKET_URL) ); + if (wasBiDiRequested) { + this.initBiDi((BaseOptions) capabilities); + } + setSessionId(response.getSessionId()); + } + + /** + * Changes platform name if it is not set and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultName a platformName value which has to be set up + * @return {@link Capabilities} with changed platform name value or the original capabilities + */ + protected static Capabilities ensurePlatformName( + Capabilities originalCapabilities, String defaultName) { + return originalCapabilities.getPlatformName() == null + ? originalCapabilities.merge(new ImmutableCapabilities(PLATFORM_NAME, defaultName)) + : originalCapabilities; + } + + /** + * Changes automation name if it is not set and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultName a platformName value which has to be set up + * @return {@link Capabilities} with changed mobile automation name value or the original capabilities + */ + protected static Capabilities ensureAutomationName( + Capabilities originalCapabilities, String defaultName) { + String currentAutomationName = CapabilityHelpers.getCapability( + originalCapabilities, AUTOMATION_NAME_OPTION, String.class); + if (isNullOrEmpty(currentAutomationName)) { + String capabilityName = originalCapabilities.getCapabilityNames() + .contains(AUTOMATION_NAME_OPTION) ? AUTOMATION_NAME_OPTION : APPIUM_PREFIX + AUTOMATION_NAME_OPTION; + return originalCapabilities.merge(new ImmutableCapabilities(capabilityName, defaultName)); + } + return originalCapabilities; + } + + /** + * Changes platform and automation names if they are not set + * and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultPlatformName a platformName value which has to be set up + * @param defaultAutomationName The default automation name to set up for this class + * @return {@link Capabilities} with changed platform/automation name value or the original capabilities + */ + protected static Capabilities ensurePlatformAndAutomationNames( + Capabilities originalCapabilities, String defaultPlatformName, String defaultAutomationName) { + Capabilities capsWithPlatformFixed = ensurePlatformName(originalCapabilities, defaultPlatformName); + return ensureAutomationName(capsWithPlatformFixed, defaultAutomationName); + } + + private void initBiDi(BaseOptions responseCaps) { + var webSocketUrl = responseCaps.getWebSocketUrl(); + if (webSocketUrl.isEmpty()) { + return; + } + URISyntaxException uriSyntaxError = null; + try { + this.biDiUri = new URI(String.valueOf(webSocketUrl.get())); + } catch (URISyntaxException e) { + uriSyntaxError = e; + } + if (uriSyntaxError != null || this.biDiUri.getScheme() == null) { + var message = String.format( + "BiDi cannot be enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", + SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl.get() + ); + if (uriSyntaxError == null) { + throw new BiDiException(message); + } + throw new BiDiException(message, uriSyntaxError); + } + var executor = getCommandExecutor(); + final HttpClient wsClient; + AppiumClientConfig wsConfig; + if (executor instanceof AppiumCommandExecutor) { + wsConfig = ((AppiumCommandExecutor) executor).getAppiumClientConfig().baseUri(biDiUri); + wsClient = ((AppiumCommandExecutor) executor).getHttpClientFactory().createClient(wsConfig); + } else { + wsConfig = AppiumClientConfig.defaultConfig().baseUri(biDiUri); + wsClient = HttpClient.Factory.createDefault().createClient(wsConfig); + } + var biDiConnection = new org.openqa.selenium.bidi.Connection(wsClient, biDiUri.toString()); + this.biDi = new BiDi(biDiConnection, wsConfig.wsTimeout()); } } diff --git a/src/main/java/io/appium/java_client/AppiumFluentWait.java b/src/main/java/io/appium/java_client/AppiumFluentWait.java index 6361a5652..a284e1ebb 100644 --- a/src/main/java/io/appium/java_client/AppiumFluentWait.java +++ b/src/main/java/io/appium/java_client/AppiumFluentWait.java @@ -17,7 +17,6 @@ package io.appium.java_client; import com.google.common.base.Throwables; -import io.appium.java_client.internal.ReflectionHelpers; import lombok.AccessLevel; import lombok.Getter; import org.openqa.selenium.TimeoutException; @@ -114,43 +113,32 @@ public AppiumFluentWait withPollDelay(Duration pollDelay) { return this; } - private B getPrivateFieldValue(String fieldName, Class fieldType) { - return ReflectionHelpers.getPrivateFieldValue(FluentWait.class, this, fieldName, fieldType); - } - - private Object getPrivateFieldValue(String fieldName) { - return getPrivateFieldValue(fieldName, Object.class); - } - protected Clock getClock() { - return getPrivateFieldValue("clock", Clock.class); + return clock; } protected Duration getTimeout() { - return getPrivateFieldValue("timeout", Duration.class); + return timeout; } protected Duration getInterval() { - return getPrivateFieldValue("interval", Duration.class); + return interval; } protected Sleeper getSleeper() { - return getPrivateFieldValue("sleeper", Sleeper.class); + return sleeper; } - @SuppressWarnings("unchecked") protected List> getIgnoredExceptions() { - return getPrivateFieldValue("ignoredExceptions", List.class); + return ignoredExceptions; } - @SuppressWarnings("unchecked") protected Supplier getMessageSupplier() { - return getPrivateFieldValue("messageSupplier", Supplier.class); + return messageSupplier; } - @SuppressWarnings("unchecked") protected T getInput() { - return (T) getPrivateFieldValue("input"); + return (T) input; } /** diff --git a/src/main/java/io/appium/java_client/CommandExecutionHelper.java b/src/main/java/io/appium/java_client/CommandExecutionHelper.java index e64470b38..b56a2f4ac 100644 --- a/src/main/java/io/appium/java_client/CommandExecutionHelper.java +++ b/src/main/java/io/appium/java_client/CommandExecutionHelper.java @@ -16,9 +16,9 @@ package io.appium.java_client; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.Response; -import javax.annotation.Nullable; import java.util.Collections; import java.util.Map; diff --git a/src/main/java/io/appium/java_client/ComparesImages.java b/src/main/java/io/appium/java_client/ComparesImages.java index 5a9a58b1c..4f44d6e0a 100644 --- a/src/main/java/io/appium/java_client/ComparesImages.java +++ b/src/main/java/io/appium/java_client/ComparesImages.java @@ -23,8 +23,8 @@ import io.appium.java_client.imagecomparison.OccurrenceMatchingResult; import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; import io.appium.java_client.imagecomparison.SimilarityMatchingResult; +import org.jspecify.annotations.Nullable; -import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/io/appium/java_client/ExecuteCDPCommand.java b/src/main/java/io/appium/java_client/ExecuteCDPCommand.java index 8b9f18317..7114da787 100644 --- a/src/main/java/io/appium/java_client/ExecuteCDPCommand.java +++ b/src/main/java/io/appium/java_client/ExecuteCDPCommand.java @@ -16,9 +16,9 @@ package io.appium.java_client; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.Response; -import javax.annotation.Nullable; import java.util.Collections; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/io/appium/java_client/ExecutesDriverScript.java b/src/main/java/io/appium/java_client/ExecutesDriverScript.java index 1ffebedb9..2509dba85 100644 --- a/src/main/java/io/appium/java_client/ExecutesDriverScript.java +++ b/src/main/java/io/appium/java_client/ExecutesDriverScript.java @@ -18,9 +18,9 @@ import io.appium.java_client.driverscripts.ScriptOptions; import io.appium.java_client.driverscripts.ScriptValue; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.Response; -import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/io/appium/java_client/HasBrowserCheck.java b/src/main/java/io/appium/java_client/HasBrowserCheck.java index 76094b5ca..a75ffbfd4 100644 --- a/src/main/java/io/appium/java_client/HasBrowserCheck.java +++ b/src/main/java/io/appium/java_client/HasBrowserCheck.java @@ -1,15 +1,18 @@ package io.appium.java_client; import io.appium.java_client.internal.CapabilityHelpers; -import org.openqa.selenium.ContextAware; +import io.appium.java_client.remote.SupportsContextSwitching; import org.openqa.selenium.HasCapabilities; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.CapabilityType; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; public interface HasBrowserCheck extends ExecutesMethod, HasCapabilities { + String NATIVE_CONTEXT = "NATIVE_APP"; + /** * Validates if the driver is currently in a web browser context. * @@ -27,12 +30,12 @@ default boolean isBrowser() { // ignore } } - if (!(this instanceof ContextAware)) { + if (!(this instanceof SupportsContextSwitching)) { return false; } try { - var context = ((ContextAware) this).getContext(); - return context != null && !context.toUpperCase().contains("NATIVE_APP"); + var context = ((SupportsContextSwitching) this).getContext(); + return context != null && !context.toUpperCase(ROOT).contains(NATIVE_CONTEXT); } catch (WebDriverException e) { return false; } diff --git a/src/main/java/io/appium/java_client/InteractsWithApps.java b/src/main/java/io/appium/java_client/InteractsWithApps.java index 9fe25dc24..0ca018abb 100644 --- a/src/main/java/io/appium/java_client/InteractsWithApps.java +++ b/src/main/java/io/appium/java_client/InteractsWithApps.java @@ -22,10 +22,10 @@ import io.appium.java_client.appmanagement.BaseOptions; import io.appium.java_client.appmanagement.BaseRemoveApplicationOptions; import io.appium.java_client.appmanagement.BaseTerminateApplicationOptions; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.InvalidArgumentException; import org.openqa.selenium.UnsupportedCommandException; -import javax.annotation.Nullable; import java.time.Duration; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/io/appium/java_client/Location.java b/src/main/java/io/appium/java_client/Location.java index 336c09797..322665a42 100644 --- a/src/main/java/io/appium/java_client/Location.java +++ b/src/main/java/io/appium/java_client/Location.java @@ -19,8 +19,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; - -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * Represents the physical location. diff --git a/src/main/java/io/appium/java_client/MobileCommand.java b/src/main/java/io/appium/java_client/MobileCommand.java index 029c1abb7..b4df90047 100644 --- a/src/main/java/io/appium/java_client/MobileCommand.java +++ b/src/main/java/io/appium/java_client/MobileCommand.java @@ -21,10 +21,10 @@ import io.appium.java_client.imagecomparison.ComparisonMode; import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.http.HttpMethod; -import javax.annotation.Nullable; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collections; diff --git a/src/main/java/io/appium/java_client/android/AndroidDriver.java b/src/main/java/io/appium/java_client/android/AndroidDriver.java index dfd9a879a..aa1d30159 100644 --- a/src/main/java/io/appium/java_client/android/AndroidDriver.java +++ b/src/main/java/io/appium/java_client/android/AndroidDriver.java @@ -43,7 +43,6 @@ import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; import org.openqa.selenium.remote.HttpCommandExecutor; -import org.openqa.selenium.remote.html5.RemoteLocationContext; import org.openqa.selenium.remote.http.ClientConfig; import org.openqa.selenium.remote.http.HttpClient; @@ -251,20 +250,6 @@ public AndroidBatteryInfo getBatteryInfo() { return new AndroidBatteryInfo(CommandExecutionHelper.executeScript(this, "mobile: batteryInfo")); } - /** - * Provides the location context. - * - * @return instance of {@link RemoteLocationContext} - * @deprecated This method, {@link org.openqa.selenium.html5.LocationContext} and {@link RemoteLocationContext} - * interface are deprecated, use {@link #getLocation()} and - * {@link #setLocation(io.appium.java_client.Location)} instead. - */ - @Override - @Deprecated(forRemoval = true) - public RemoteLocationContext getLocationContext() { - return locationContext; - } - @Override public synchronized StringWebSocketClient getLogcatClient() { if (logcatClient == null) { diff --git a/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java b/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java index 8c294c4c0..cacc04137 100644 --- a/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java +++ b/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java @@ -21,6 +21,8 @@ import java.util.Map; +import static java.util.Locale.ROOT; + /** * This util class helps to prepare parameters of Android-specific JSONWP * commands. @@ -241,7 +243,7 @@ public class AndroidMobileCommandHelper extends MobileCommand { String phoneNumber, GsmCallActions gsmCallActions) { return Map.entry(GSM_CALL, Map.of( "phoneNumber", phoneNumber, - "action", gsmCallActions.name().toLowerCase() + "action", gsmCallActions.name().toLowerCase(ROOT) )); } @@ -275,7 +277,7 @@ public class AndroidMobileCommandHelper extends MobileCommand { @Deprecated public static Map.Entry> gsmVoiceCommand( GsmVoiceState gsmVoiceState) { - return Map.entry(GSM_VOICE, Map.of("state", gsmVoiceState.name().toLowerCase())); + return Map.entry(GSM_VOICE, Map.of("state", gsmVoiceState.name().toLowerCase(ROOT))); } /** @@ -289,7 +291,7 @@ public class AndroidMobileCommandHelper extends MobileCommand { @Deprecated public static Map.Entry> networkSpeedCommand( NetworkSpeed networkSpeed) { - return Map.entry(NETWORK_SPEED, Map.of("netspeed", networkSpeed.name().toLowerCase())); + return Map.entry(NETWORK_SPEED, Map.of("netspeed", networkSpeed.name().toLowerCase(ROOT))); } /** @@ -317,7 +319,7 @@ public class AndroidMobileCommandHelper extends MobileCommand { @Deprecated public static Map.Entry> powerACCommand( PowerACState powerACState) { - return Map.entry(POWER_AC_STATE, Map.of("state", powerACState.name().toLowerCase())); + return Map.entry(POWER_AC_STATE, Map.of("state", powerACState.name().toLowerCase(ROOT))); } /** diff --git a/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java b/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java index 49a657898..8b7018e76 100644 --- a/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java +++ b/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java @@ -25,6 +25,7 @@ import java.util.Map; import static io.appium.java_client.MobileCommand.SET_CLIPBOARD; +import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; public interface HasAndroidClipboard extends HasClipboard { @@ -39,7 +40,7 @@ default void setClipboard(String label, ClipboardContentType contentType, byte[] CommandExecutionHelper.execute(this, Map.entry(SET_CLIPBOARD, Map.of( "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), - "contentType", contentType.name().toLowerCase(), + "contentType", contentType.name().toLowerCase(ROOT), "label", requireNonNull(label) ) )); diff --git a/src/main/java/io/appium/java_client/android/StartsActivity.java b/src/main/java/io/appium/java_client/android/StartsActivity.java index 9f56d7b1a..23b0ad7a9 100644 --- a/src/main/java/io/appium/java_client/android/StartsActivity.java +++ b/src/main/java/io/appium/java_client/android/StartsActivity.java @@ -19,10 +19,9 @@ import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.UnsupportedCommandException; -import javax.annotation.Nullable; - import java.util.Map; import static io.appium.java_client.MobileCommand.CURRENT_ACTIVITY; diff --git a/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java b/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java index c60d8eaf9..bb618b8cd 100644 --- a/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java +++ b/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java @@ -14,6 +14,7 @@ import static io.appium.java_client.MobileCommand.POWER_AC_STATE; import static io.appium.java_client.MobileCommand.POWER_CAPACITY; import static io.appium.java_client.MobileCommand.SEND_SMS; +import static java.util.Locale.ROOT; public interface SupportsSpecialEmulatorCommands extends ExecutesMethod, CanRememberExtensionPresence { @@ -53,7 +54,7 @@ default void makeGsmCall(String phoneNumber, GsmCallActions gsmCallAction) { try { CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( "phoneNumber", phoneNumber, - "action", gsmCallAction.toString().toLowerCase() + "action", gsmCallAction.toString().toLowerCase(ROOT) )); } catch (UnsupportedCommandException e) { // TODO: Remove the fallback @@ -61,7 +62,7 @@ default void makeGsmCall(String phoneNumber, GsmCallActions gsmCallAction) { markExtensionAbsence(extName), Map.entry(GSM_CALL, Map.of( "phoneNumber", phoneNumber, - "action", gsmCallAction.toString().toLowerCase() + "action", gsmCallAction.toString().toLowerCase(ROOT) )) ); } @@ -99,14 +100,14 @@ default void setGsmVoice(GsmVoiceState gsmVoiceState) { final String extName = "mobile: gsmVoice"; try { CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "state", gsmVoiceState.toString().toLowerCase() + "state", gsmVoiceState.toString().toLowerCase(ROOT) )); } catch (UnsupportedCommandException e) { // TODO: Remove the fallback CommandExecutionHelper.execute( markExtensionAbsence(extName), Map.entry(GSM_VOICE, Map.of( - "state", gsmVoiceState.name().toLowerCase() + "state", gsmVoiceState.name().toLowerCase(ROOT) )) ); } @@ -121,14 +122,14 @@ default void setNetworkSpeed(NetworkSpeed networkSpeed) { final String extName = "mobile: networkSpeed"; try { CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "speed", networkSpeed.toString().toLowerCase() + "speed", networkSpeed.toString().toLowerCase(ROOT) )); } catch (UnsupportedCommandException e) { // TODO: Remove the fallback CommandExecutionHelper.execute( markExtensionAbsence(extName), Map.entry(NETWORK_SPEED, Map.of( - "netspeed", networkSpeed.name().toLowerCase() + "netspeed", networkSpeed.name().toLowerCase(ROOT) )) ); } @@ -165,14 +166,14 @@ default void setPowerAC(PowerACState powerACState) { final String extName = "mobile: powerAC"; try { CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "state", powerACState.toString().toLowerCase() + "state", powerACState.toString().toLowerCase(ROOT) )); } catch (UnsupportedCommandException e) { // TODO: Remove the fallback CommandExecutionHelper.execute( markExtensionAbsence(extName), Map.entry(POWER_AC_STATE, Map.of( - "state", powerACState.name().toLowerCase() + "state", powerACState.name().toLowerCase(ROOT) )) ); } diff --git a/src/main/java/io/appium/java_client/android/nativekey/AndroidKey.java b/src/main/java/io/appium/java_client/android/nativekey/AndroidKey.java index c0a809801..4138ea69f 100644 --- a/src/main/java/io/appium/java_client/android/nativekey/AndroidKey.java +++ b/src/main/java/io/appium/java_client/android/nativekey/AndroidKey.java @@ -1094,7 +1094,7 @@ public enum AndroidKey { TV_SATELLITE_SERVICE(240), /** * Key code constant: Toggle Network key. - * Toggles selecting broacast services. + * Toggles selecting broadcast services. */ TV_NETWORK(241), /** diff --git a/src/main/java/io/appium/java_client/android/nativekey/KeyEventMetaModifier.java b/src/main/java/io/appium/java_client/android/nativekey/KeyEventMetaModifier.java index e4f998b99..b32de52bc 100644 --- a/src/main/java/io/appium/java_client/android/nativekey/KeyEventMetaModifier.java +++ b/src/main/java/io/appium/java_client/android/nativekey/KeyEventMetaModifier.java @@ -23,103 +23,103 @@ public enum KeyEventMetaModifier { */ SELECTING(0x800), /** - *

This mask is used to check whether one of the ALT meta keys is pressed.

+ * This mask is used to check whether one of the ALT meta keys is pressed. * * @see AndroidKey#ALT_LEFT * @see AndroidKey#ALT_RIGHT */ ALT_ON(0x02), /** - *

This mask is used to check whether the left ALT meta key is pressed.

+ * This mask is used to check whether the left ALT meta key is pressed. * * @see AndroidKey#ALT_LEFT */ ALT_LEFT_ON(0x10), /** - *

This mask is used to check whether the right the ALT meta key is pressed.

+ * This mask is used to check whether the right the ALT meta key is pressed. * * @see AndroidKey#ALT_RIGHT */ ALT_RIGHT_ON(0x20), /** - *

This mask is used to check whether one of the SHIFT meta keys is pressed.

+ * This mask is used to check whether one of the SHIFT meta keys is pressed. * * @see AndroidKey#SHIFT_LEFT * @see AndroidKey#SHIFT_RIGHT */ SHIFT_ON(0x1), /** - *

This mask is used to check whether the left SHIFT meta key is pressed.

+ * This mask is used to check whether the left SHIFT meta key is pressed. * * @see AndroidKey#SHIFT_LEFT */ SHIFT_LEFT_ON(0x40), /** - *

This mask is used to check whether the right SHIFT meta key is pressed.

+ * This mask is used to check whether the right SHIFT meta key is pressed. * * @see AndroidKey#SHIFT_RIGHT */ SHIFT_RIGHT_ON(0x80), /** - *

This mask is used to check whether the SYM meta key is pressed.

+ * This mask is used to check whether the SYM meta key is pressed. */ SYM_ON(0x4), /** - *

This mask is used to check whether the FUNCTION meta key is pressed.

+ * This mask is used to check whether the FUNCTION meta key is pressed. */ FUNCTION_ON(0x8), /** - *

This mask is used to check whether one of the CTRL meta keys is pressed.

+ * This mask is used to check whether one of the CTRL meta keys is pressed. * * @see AndroidKey#CTRL_LEFT * @see AndroidKey#CTRL_RIGHT */ CTRL_ON(0x1000), /** - *

This mask is used to check whether the left CTRL meta key is pressed.

+ * This mask is used to check whether the left CTRL meta key is pressed. * * @see AndroidKey#CTRL_LEFT */ CTRL_LEFT_ON(0x2000), /** - *

This mask is used to check whether the right CTRL meta key is pressed.

+ * This mask is used to check whether the right CTRL meta key is pressed. * * @see AndroidKey#CTRL_RIGHT */ CTRL_RIGHT_ON(0x4000), /** - *

This mask is used to check whether one of the META meta keys is pressed.

+ * This mask is used to check whether one of the META meta keys is pressed. * * @see AndroidKey#META_LEFT * @see AndroidKey#META_RIGHT */ META_ON(0x10000), /** - *

This mask is used to check whether the left META meta key is pressed.

+ * This mask is used to check whether the left META meta key is pressed. * * @see AndroidKey#META_LEFT */ META_LEFT_ON(0x20000), /** - *

This mask is used to check whether the right META meta key is pressed.

+ * This mask is used to check whether the right META meta key is pressed. * * @see AndroidKey#META_RIGHT */ META_RIGHT_ON(0x40000), /** - *

This mask is used to check whether the CAPS LOCK meta key is on.

+ * This mask is used to check whether the CAPS LOCK meta key is on. * * @see AndroidKey#CAPS_LOCK */ CAPS_LOCK_ON(0x100000), /** - *

This mask is used to check whether the NUM LOCK meta key is on.

+ * This mask is used to check whether the NUM LOCK meta key is on. * * @see AndroidKey#NUM_LOCK */ NUM_LOCK_ON(0x200000), /** - *

This mask is used to check whether the SCROLL LOCK meta key is on.

+ * This mask is used to check whether the SCROLL LOCK meta key is on. * * @see AndroidKey#SCROLL_LOCK */ diff --git a/src/main/java/io/appium/java_client/android/options/EspressoOptions.java b/src/main/java/io/appium/java_client/android/options/EspressoOptions.java index 8cbe2cf33..da14a620e 100644 --- a/src/main/java/io/appium/java_client/android/options/EspressoOptions.java +++ b/src/main/java/io/appium/java_client/android/options/EspressoOptions.java @@ -101,7 +101,10 @@ import java.util.Map; /** - * https://github.com/appium/appium-espresso-driver#capabilities + * Provides options specific to the Espresso Driver. + * + *

For more details, refer to the + * capabilities documentation

*/ public class EspressoOptions extends BaseOptions implements // General options: https://github.com/appium/appium-uiautomator2-driver#general diff --git a/src/main/java/io/appium/java_client/android/options/UiAutomator2Options.java b/src/main/java/io/appium/java_client/android/options/UiAutomator2Options.java index 3c6a7bc51..77115496f 100644 --- a/src/main/java/io/appium/java_client/android/options/UiAutomator2Options.java +++ b/src/main/java/io/appium/java_client/android/options/UiAutomator2Options.java @@ -107,7 +107,10 @@ import java.util.Map; /** - * https://github.com/appium/appium-uiautomator2-driver#capabilities + * Provides options specific to the UiAutomator2 Driver. + * + *

For more details, refer to the + * capabilities documentation

*/ public class UiAutomator2Options extends BaseOptions implements // General options: https://github.com/appium/appium-uiautomator2-driver#general diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsLogcatFilterSpecsOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsLogcatFilterSpecsOption.java index 6aca7a15e..f58076fe6 100644 --- a/src/main/java/io/appium/java_client/android/options/adb/SupportsLogcatFilterSpecsOption.java +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsLogcatFilterSpecsOption.java @@ -20,6 +20,7 @@ import io.appium.java_client.remote.options.CanSetCapability; import org.openqa.selenium.Capabilities; +import java.util.List; import java.util.Optional; public interface SupportsLogcatFilterSpecsOption> extends @@ -27,25 +28,28 @@ public interface SupportsLogcatFilterSpecsOption> exten String LOGCAT_FILTER_SPECS_OPTION = "logcatFilterSpecs"; /** - * Series of tag[:priority] where tag is a log component tag (or * for all) - * and priority is: V Verbose, D Debug, I Info, W Warn, E Error, F Fatal, - * S Silent (supress all output). '' means ':d' and tag by itself means tag:v. - * If not specified on the commandline, filterspec is set from ANDROID_LOG_TAGS. - * If no filterspec is found, filter defaults to '*:I'. + * Allows to customize logcat output filtering. * - * @param format The filter specifier. + * @param format The filter specifier. Consists from series of `tag[:priority]` items, + * where `tag` is a log component tag (or `*` to match any value) + * and `priority`: V (Verbose), D (Debug), I (Info), W (Warn), E (Error), F (Fatal), + * S (Silent - suppresses all output). `tag` without `priority` defaults to `tag:v`. + * If not specified, filterspec is set from ANDROID_LOG_TAGS environment variable. + * If no filterspec is found, filter defaults to `*:I`, which means + * to only show log lines with any tag and the log level INFO or higher. * @return self instance for chaining. */ - default T setLogcatFilterSpecs(String format) { + default T setLogcatFilterSpecs(List format) { return amend(LOGCAT_FILTER_SPECS_OPTION, format); } /** * Get the logcat filter format. * - * @return Format specifier. + * @return Format specifier. See the documentation on {@link #setLogcatFilterSpecs(List)} for more details. */ - default Optional getLogcatFilterSpecs() { - return Optional.ofNullable((String) getCapability(LOGCAT_FILTER_SPECS_OPTION)); + default Optional> getLogcatFilterSpecs() { + //noinspection unchecked + return Optional.ofNullable((List) getCapability(LOGCAT_FILTER_SPECS_OPTION)); } } diff --git a/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java b/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java index e6366f708..d7e34242e 100644 --- a/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java +++ b/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java @@ -29,10 +29,11 @@ import java.net.URL; /** - *

ChromiumDriver is an officially supported Appium driver created to automate Mobile browsers + * ChromiumDriver is an officially supported Appium driver created to automate Mobile browsers * and web views based on the Chromium engine. The driver uses W3CWebDriver protocol and is built - * on top of chromium driver server.

+ * on top of chromium driver server. *
+ * *

Read appium-chromium-driver * for more details on how to configure and use it.

*/ diff --git a/src/main/java/io/appium/java_client/chromium/options/ChromiumOptions.java b/src/main/java/io/appium/java_client/chromium/options/ChromiumOptions.java index dfc5d7d02..2f25eeff4 100644 --- a/src/main/java/io/appium/java_client/chromium/options/ChromiumOptions.java +++ b/src/main/java/io/appium/java_client/chromium/options/ChromiumOptions.java @@ -24,7 +24,7 @@ import java.util.Map; /** - *

Options class that sets options for Chromium when testing websites.

+ * Options class that sets options for Chromium when testing websites. *
* @see appium-chromium-driver usage section */ diff --git a/src/main/java/io/appium/java_client/clipboard/HasClipboard.java b/src/main/java/io/appium/java_client/clipboard/HasClipboard.java index 24384da13..ae507f940 100644 --- a/src/main/java/io/appium/java_client/clipboard/HasClipboard.java +++ b/src/main/java/io/appium/java_client/clipboard/HasClipboard.java @@ -27,6 +27,7 @@ import static io.appium.java_client.MobileCommand.GET_CLIPBOARD; import static io.appium.java_client.MobileCommand.SET_CLIPBOARD; +import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; public interface HasClipboard extends ExecutesMethod, CanRememberExtensionPresence { @@ -40,7 +41,7 @@ default void setClipboard(ClipboardContentType contentType, byte[] base64Content final String extName = "mobile: setClipboard"; var args = Map.of( "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), - "contentType", contentType.name().toLowerCase() + "contentType", contentType.name().toLowerCase(ROOT) ); try { CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); @@ -58,7 +59,7 @@ default void setClipboard(ClipboardContentType contentType, byte[] base64Content */ default String getClipboard(ClipboardContentType contentType) { final String extName = "mobile: getClipboard"; - var args = Map.of("contentType", contentType.name().toLowerCase()); + var args = Map.of("contentType", contentType.name().toLowerCase(ROOT)); try { return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); } catch (UnsupportedCommandException e) { diff --git a/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java b/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java index 41b5cd78e..7eac89083 100644 --- a/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java +++ b/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Map; +import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; @@ -59,7 +60,7 @@ public ScriptOptions withTimeout(long timeoutMs) { */ public Map build() { var map = new HashMap(); - ofNullable(scriptType).ifPresent(x -> map.put("type", x.name().toLowerCase())); + ofNullable(scriptType).ifPresent(x -> map.put("type", x.name().toLowerCase(ROOT))); ofNullable(timeoutMs).ifPresent(x -> map.put("timeout", x)); return Collections.unmodifiableMap(map); } diff --git a/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java b/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java index 6a00c0510..2e5a83430 100644 --- a/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java +++ b/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java @@ -13,7 +13,10 @@ import java.util.Map; /** - * https://github.com/AppiumTestDistribution/appium-flutter-integration-driver#capabilities-for-appium-flutter-integration-driver + * Provides options specific to the Appium Flutter Integration Driver. + * + *

For more details, refer to the + * capabilities documentation

*/ public class FlutterDriverOptions extends BaseOptions implements SupportsFlutterSystemPortOption, diff --git a/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java b/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java index 4eb74e82a..1d11378e2 100644 --- a/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java +++ b/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java @@ -7,8 +7,8 @@ * Flutter applications, extending WebDriver and providing additional capabilities for * interacting with Flutter-specific elements and behaviors. * - *

This interface serves as a common entity for drivers that support Flutter applications - * on different platforms, such as Android and iOS.

+ *

This interface serves as a common entity for drivers that support Flutter applications + * on different platforms, such as Android and iOS.

* * @see WebDriver * @see SupportsGestureOnFlutterElements diff --git a/src/main/java/io/appium/java_client/gecko/options/GeckoOptions.java b/src/main/java/io/appium/java_client/gecko/options/GeckoOptions.java index 084400142..2e1b4f1fd 100644 --- a/src/main/java/io/appium/java_client/gecko/options/GeckoOptions.java +++ b/src/main/java/io/appium/java_client/gecko/options/GeckoOptions.java @@ -31,7 +31,10 @@ import java.util.Map; /** - * https://github.com/appium/appium-geckodriver#usage + * Provides options specific to the Geckodriver. + * + *

For more details, refer to the + * capabilities documentation

*/ public class GeckoOptions extends BaseOptions implements SupportsBrowserNameOption, diff --git a/src/main/java/io/appium/java_client/gecko/options/SupportsVerbosityOption.java b/src/main/java/io/appium/java_client/gecko/options/SupportsVerbosityOption.java index 60e479079..32d37f61e 100644 --- a/src/main/java/io/appium/java_client/gecko/options/SupportsVerbosityOption.java +++ b/src/main/java/io/appium/java_client/gecko/options/SupportsVerbosityOption.java @@ -22,6 +22,8 @@ import java.util.Optional; +import static java.util.Locale.ROOT; + public interface SupportsVerbosityOption> extends Capabilities, CanSetCapability { String VERBOSITY_OPTION = "verbosity"; @@ -34,7 +36,7 @@ public interface SupportsVerbosityOption> extends * @return self instance for chaining. */ default T setVerbosity(Verbosity verbosity) { - return amend(VERBOSITY_OPTION, verbosity.name().toLowerCase()); + return amend(VERBOSITY_OPTION, verbosity.name().toLowerCase(ROOT)); } /** @@ -45,7 +47,7 @@ default T setVerbosity(Verbosity verbosity) { default Optional getVerbosity() { return Optional.ofNullable(getCapability(VERBOSITY_OPTION)) .map(String::valueOf) - .map(String::toUpperCase) + .map(verbosity -> verbosity.toUpperCase(ROOT)) .map(Verbosity::valueOf); } } diff --git a/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java b/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java index 0fba408d4..e73be71c8 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java +++ b/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java @@ -42,7 +42,7 @@ protected Map getResultAsMap() { } /** - * Verifies if the corresponding property is present in the commend result + * Verifies if the corresponding property is present in the command result * and throws an exception if not. * * @param propertyName the actual property name to be verified for presence diff --git a/src/main/java/io/appium/java_client/internal/CapabilityHelpers.java b/src/main/java/io/appium/java_client/internal/CapabilityHelpers.java index 855a40cdb..345e60a9c 100644 --- a/src/main/java/io/appium/java_client/internal/CapabilityHelpers.java +++ b/src/main/java/io/appium/java_client/internal/CapabilityHelpers.java @@ -16,9 +16,9 @@ package io.appium.java_client.internal; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; -import javax.annotation.Nullable; import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; diff --git a/src/main/java/io/appium/java_client/internal/ReflectionHelpers.java b/src/main/java/io/appium/java_client/internal/ReflectionHelpers.java index 10375e2ad..dd131fc65 100644 --- a/src/main/java/io/appium/java_client/internal/ReflectionHelpers.java +++ b/src/main/java/io/appium/java_client/internal/ReflectionHelpers.java @@ -44,23 +44,4 @@ public static T setPrivateFieldValue(Class cls, T target, String fieldNam } return target; } - - /** - * Fetches the value of a private instance field. - * - * @param cls The target class or a superclass. - * @param target Target instance. - * @param fieldName Target field name. - * @param fieldType Field type. - * @return The retrieved field value. - */ - public static T getPrivateFieldValue(Class cls, Object target, String fieldName, Class fieldType) { - try { - final Field f = cls.getDeclaredField(fieldName); - f.setAccessible(true); - return fieldType.cast(f.get(target)); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new WebDriverException(e); - } - } } diff --git a/src/main/java/io/appium/java_client/internal/filters/AppiumIdempotencyFilter.java b/src/main/java/io/appium/java_client/internal/filters/AppiumIdempotencyFilter.java index 9a8f8156b..b075c9b6b 100644 --- a/src/main/java/io/appium/java_client/internal/filters/AppiumIdempotencyFilter.java +++ b/src/main/java/io/appium/java_client/internal/filters/AppiumIdempotencyFilter.java @@ -20,7 +20,8 @@ import org.openqa.selenium.remote.http.HttpHandler; import org.openqa.selenium.remote.http.HttpMethod; -import java.util.UUID; +import static java.util.Locale.ROOT; +import static java.util.UUID.randomUUID; public class AppiumIdempotencyFilter implements Filter { // https://github.com/appium/appium-base-driver/pull/400 @@ -30,7 +31,7 @@ public class AppiumIdempotencyFilter implements Filter { public HttpHandler apply(HttpHandler next) { return req -> { if (req.getMethod() == HttpMethod.POST && req.getUri().endsWith("/session")) { - req.setHeader(IDEMPOTENCY_KEY_HEADER, UUID.randomUUID().toString().toLowerCase()); + req.setHeader(IDEMPOTENCY_KEY_HEADER, randomUUID().toString().toLowerCase(ROOT)); } return next.execute(req); }; diff --git a/src/main/java/io/appium/java_client/internal/filters/AppiumUserAgentFilter.java b/src/main/java/io/appium/java_client/internal/filters/AppiumUserAgentFilter.java index d4f1842ce..030666ab6 100644 --- a/src/main/java/io/appium/java_client/internal/filters/AppiumUserAgentFilter.java +++ b/src/main/java/io/appium/java_client/internal/filters/AppiumUserAgentFilter.java @@ -17,13 +17,14 @@ package io.appium.java_client.internal.filters; import io.appium.java_client.internal.Config; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.http.AddSeleniumUserAgent; import org.openqa.selenium.remote.http.Filter; import org.openqa.selenium.remote.http.HttpHandler; import org.openqa.selenium.remote.http.HttpHeader; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import static java.util.Locale.ROOT; /** * Manage Appium Client configurations. @@ -41,7 +42,7 @@ public class AppiumUserAgentFilter implements Filter { */ public static final String USER_AGENT = buildUserAgentHeaderValue(AddSeleniumUserAgent.USER_AGENT); - private static String buildUserAgentHeaderValue(@Nonnull String previousUA) { + private static String buildUserAgentHeaderValue(@NonNull String previousUA) { return String.format("%s%s (%s)", USER_AGENT_PREFIX, Config.main().getValue(VERSION_KEY, String.class), previousUA); } @@ -55,7 +56,7 @@ private static String buildUserAgentHeaderValue(@Nonnull String previousUA) { * like by this filter. */ private static boolean containsAppiumName(@Nullable String userAgent) { - return userAgent != null && userAgent.toLowerCase().contains(USER_AGENT_PREFIX.toLowerCase()); + return userAgent != null && userAgent.toLowerCase(ROOT).contains(USER_AGENT_PREFIX.toLowerCase(ROOT)); } /** diff --git a/src/main/java/io/appium/java_client/ios/IOSDriver.java b/src/main/java/io/appium/java_client/ios/IOSDriver.java index 6cc48469e..0fd5cbf20 100644 --- a/src/main/java/io/appium/java_client/ios/IOSDriver.java +++ b/src/main/java/io/appium/java_client/ios/IOSDriver.java @@ -44,7 +44,6 @@ import org.openqa.selenium.remote.DriverCommand; import org.openqa.selenium.remote.HttpCommandExecutor; import org.openqa.selenium.remote.Response; -import org.openqa.selenium.remote.html5.RemoteLocationContext; import org.openqa.selenium.remote.http.ClientConfig; import org.openqa.selenium.remote.http.HttpClient; @@ -279,20 +278,6 @@ class IOSAlert implements Alert { } - /** - * Provides the location context. - * - * @return instance of {@link RemoteLocationContext} - * @deprecated This method, {@link org.openqa.selenium.html5.LocationContext} and {@link RemoteLocationContext} - * interface are deprecated, use {@link #getLocation()} and - * {@link #setLocation(io.appium.java_client.Location)} instead. - */ - @Override - @Deprecated(forRemoval = true) - public RemoteLocationContext getLocationContext() { - return locationContext; - } - @Override public synchronized StringWebSocketClient getSyslogClient() { if (syslogClient == null) { diff --git a/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java index a608cae0f..5c56cd9a5 100644 --- a/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java +++ b/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.Map; +import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; @@ -73,7 +74,7 @@ public enum VideoQuality { * @return self instance for chaining. */ public IOSStartScreenRecordingOptions withVideoQuality(VideoQuality videoQuality) { - this.videoQuality = requireNonNull(videoQuality).name().toLowerCase(); + this.videoQuality = requireNonNull(videoQuality).name().toLowerCase(ROOT); return this; } diff --git a/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java b/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java index 53465b4dd..41d5047a2 100644 --- a/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java +++ b/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java @@ -55,6 +55,7 @@ import io.appium.java_client.ios.options.wda.SupportsKeychainOptions; import io.appium.java_client.ios.options.wda.SupportsMaxTypingFrequencyOption; import io.appium.java_client.ios.options.wda.SupportsMjpegServerPortOption; +import io.appium.java_client.ios.options.wda.SupportsPrebuiltWdaPathOption; import io.appium.java_client.ios.options.wda.SupportsProcessArgumentsOption; import io.appium.java_client.ios.options.wda.SupportsResultBundlePathOption; import io.appium.java_client.ios.options.wda.SupportsScreenshotQualityOption; @@ -66,6 +67,7 @@ import io.appium.java_client.ios.options.wda.SupportsUseNativeCachingStrategyOption; import io.appium.java_client.ios.options.wda.SupportsUseNewWdaOption; import io.appium.java_client.ios.options.wda.SupportsUsePrebuiltWdaOption; +import io.appium.java_client.ios.options.wda.SupportsUsePreinstalledWdaOption; import io.appium.java_client.ios.options.wda.SupportsUseSimpleBuildTestOption; import io.appium.java_client.ios.options.wda.SupportsUseXctestrunFileOption; import io.appium.java_client.ios.options.wda.SupportsWaitForIdleTimeoutOption; @@ -119,7 +121,10 @@ import java.util.Map; /** - * https://github.com/appium/appium-xcuitest-driver#capabilities + * Provides options specific to the XCUITest Driver. + * + *

For more details, refer to the + * capabilities documentation

*/ public class XCUITestOptions extends BaseOptions implements // General options: https://github.com/appium/appium-xcuitest-driver#general @@ -153,6 +158,8 @@ public class XCUITestOptions extends BaseOptions implements SupportsWdaBaseUrlOption, SupportsShowXcodeLogOption, SupportsUsePrebuiltWdaOption, + SupportsUsePreinstalledWdaOption, + SupportsPrebuiltWdaPathOption, SupportsShouldUseSingletonTestManagerOption, SupportsWaitForIdleTimeoutOption, SupportsUseXctestrunFileOption, diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorPasteboardAutomaticSyncOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorPasteboardAutomaticSyncOption.java index 665c4d368..5f3f5615b 100644 --- a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorPasteboardAutomaticSyncOption.java +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorPasteboardAutomaticSyncOption.java @@ -22,6 +22,8 @@ import java.util.Optional; +import static java.util.Locale.ROOT; + public interface SupportsSimulatorPasteboardAutomaticSyncOption> extends Capabilities, CanSetCapability { String SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC = "simulatorPasteboardAutomaticSync"; @@ -37,7 +39,7 @@ public interface SupportsSimulatorPasteboardAutomaticSyncOption getSimulatorPasteboardAutomaticSync() { return Optional.ofNullable(getCapability(SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC)) - .map(v -> PasteboardSyncState.valueOf(String.valueOf(v).toUpperCase())); + .map(v -> PasteboardSyncState.valueOf(String.valueOf(v).toUpperCase(ROOT))); } } diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsPrebuiltWdaPathOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsPrebuiltWdaPathOption.java new file mode 100644 index 000000000..7754f232c --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsPrebuiltWdaPathOption.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.ios.options.wda; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +public interface SupportsPrebuiltWdaPathOption> extends + Capabilities, CanSetCapability { + String PREBUILT_WDA_PATH_OPTION = "prebuiltWDAPath"; + + /** + * The full path to the prebuilt WebDriverAgent-Runner application + * package to be installed if appium:usePreinstalledWDA capability + * is enabled. The package's bundle identifier could be customized via + * appium:updatedWDABundleId capability. + * + * @param path The full path to the bundle .app file on the server file system. + * @return self instance for chaining. + */ + default T setPrebuiltWdaPath(String path) { + return amend(PREBUILT_WDA_PATH_OPTION, path); + } + + /** + * Get prebuilt WebDriverAgent path. + * + * @return The full path to the bundle .app file on the server file system. + */ + default Optional getPrebuiltWdaPath() { + return Optional.ofNullable((String) getCapability(PREBUILT_WDA_PATH_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsUsePreinstalledWdaOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUsePreinstalledWdaOption.java new file mode 100644 index 000000000..0ae2dbcfd --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUsePreinstalledWdaOption.java @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.ios.options.wda; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsUsePreinstalledWdaOption> extends + Capabilities, CanSetCapability { + String USE_PREINSTALLED_WDA_OPTION = "usePreinstalledWDA"; + + /** + * Whether to launch a preinstalled WebDriverAgentRunner application using a custom XCTest API client. + * + * @return self instance for chaining. + */ + default T usePreinstalledWda() { + return amend(USE_PREINSTALLED_WDA_OPTION, true); + } + + /** + * Whether to launch a preinstalled WebDriverAgentRunner application using a custom XCTest API client. + * Defaults to false. + * + * @param value Either true or false. + * @return self instance for chaining. + */ + default T setUsePreinstalledWda(boolean value) { + return amend(USE_PREINSTALLED_WDA_OPTION, value); + } + + /** + * Get whether to launch a preinstalled WebDriverAgentRunner application using a custom XCTest API client. + * + * @return True or false. + */ + default Optional doesUsePreinstalledWda() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_PREINSTALLED_WDA_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/Mac2Options.java b/src/main/java/io/appium/java_client/mac/options/Mac2Options.java index 5325ee284..230c04c90 100644 --- a/src/main/java/io/appium/java_client/mac/options/Mac2Options.java +++ b/src/main/java/io/appium/java_client/mac/options/Mac2Options.java @@ -27,7 +27,10 @@ import java.util.Optional; /** - * https://github.com/appium/appium-mac2-driver#capabilities + * Provides options specific to the Appium Mac2 Driver. + * + *

For more details, refer to the + * capabilities documentation

*/ public class Mac2Options extends BaseOptions implements SupportsSystemPortOption, diff --git a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java index 77b3120fc..f423d1dca 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java @@ -19,9 +19,9 @@ import io.appium.java_client.pagefactory.bys.builder.AppiumByBuilder; import io.appium.java_client.pagefactory.locator.CacheableElementLocatorFactory; import io.appium.java_client.pagefactory.locator.CacheableLocator; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.SearchContext; -import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; diff --git a/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java b/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java index 05fa41a42..792932cd4 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java @@ -19,6 +19,8 @@ import io.appium.java_client.internal.CapabilityHelpers; import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.locator.CacheableLocator; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.HasCapabilities; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebDriver; @@ -30,8 +32,6 @@ import org.openqa.selenium.support.pagefactory.ElementLocatorFactory; import org.openqa.selenium.support.pagefactory.FieldDecorator; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -123,7 +123,7 @@ contextReference, duration, new WidgetByBuilder(platform, automation) ); } - @Nonnull + @NonNull private static WeakReference requireWebDriverReference(SearchContext searchContext) { var wd = unpackObjectFromSearchContext( checkNotNull(searchContext, "The provided search context cannot be null"), @@ -193,8 +193,8 @@ protected boolean isDecoratableList(Field field) { * @return a field value or null. */ public Object decorate(ClassLoader ignored, Field field) { - Object result = defaultElementFieldDecorator.decorate(ignored, field); - return result == null ? decorateWidget(field) : result; + Object result = decorateWidget(field); + return result == null ? defaultElementFieldDecorator.decorate(ignored, field) : result; } @Nullable diff --git a/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java b/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java index e08f413dd..09984729c 100644 --- a/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java +++ b/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java @@ -28,6 +28,7 @@ import static io.appium.java_client.remote.MobilePlatform.ANDROID; import static io.appium.java_client.remote.MobilePlatform.IOS; import static io.appium.java_client.remote.MobilePlatform.WINDOWS; +import static java.util.Locale.ROOT; class OverrideWidgetReader { private static final Class EMPTY = Widget.class; @@ -74,7 +75,7 @@ static Class getDefaultOrHTMLWidgetClass( static Class getMobileNativeWidgetClass(Class declaredClass, AnnotatedElement annotatedElement, String platform) { - String transformedPlatform = String.valueOf(platform).toUpperCase().trim(); + String transformedPlatform = String.valueOf(platform).toUpperCase(ROOT).trim(); if (ANDROID.equalsIgnoreCase(transformedPlatform)) { return getConvenientClass(declaredClass, annotatedElement, ANDROID_UI_AUTOMATOR); diff --git a/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java index bfc358bd8..46d946628 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java @@ -19,11 +19,11 @@ import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.interceptors.InterceptorOfASingleElement; import io.appium.java_client.pagefactory.locator.CacheableLocator; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.PageFactory; -import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -48,11 +48,9 @@ public class WidgetInterceptor extends InterceptorOfASingleElement { * Proxy interceptor class for widgets. */ public WidgetInterceptor( - @Nullable - CacheableLocator locator, + @Nullable CacheableLocator locator, WeakReference driverReference, - @Nullable - WeakReference cachedElementReference, + @Nullable WeakReference cachedElementReference, Map> instantiationMap, Duration duration ) { diff --git a/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java index ff9983f8c..bb4bb1889 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java @@ -19,10 +19,10 @@ import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.interceptors.InterceptorOfAListOfElements; import io.appium.java_client.pagefactory.locator.CacheableLocator; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -50,8 +50,7 @@ public class WidgetListInterceptor extends InterceptorOfAListOfElements { * Proxy interceptor class for lists of widgets. */ public WidgetListInterceptor( - @Nullable - CacheableLocator locator, + @Nullable CacheableLocator locator, WeakReference driver, Map> instantiationMap, Class declaredType, diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/ContentMappedBy.java b/src/main/java/io/appium/java_client/pagefactory/bys/ContentMappedBy.java index 6c0c0f99f..14967c6d7 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/ContentMappedBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/ContentMappedBy.java @@ -17,11 +17,11 @@ package io.appium.java_client.pagefactory.bys; import lombok.EqualsAndHashCode; +import org.jspecify.annotations.NonNull; import org.openqa.selenium.By; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; -import javax.annotation.Nonnull; import java.util.List; import java.util.Map; @@ -43,7 +43,7 @@ public ContentMappedBy(Map map) { * @param type required content type {@link ContentType} * @return self-reference. */ - public By useContent(@Nonnull ContentType type) { + public By useContent(@NonNull ContentType type) { requireNonNull(type); currentContent = type; return this; diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java index 0e7146a0c..73f6717aa 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/AppiumByBuilder.java @@ -16,11 +16,11 @@ package io.appium.java_client.pagefactory.bys.builder; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.By; import org.openqa.selenium.support.pagefactory.AbstractAnnotations; import org.openqa.selenium.support.pagefactory.ByAll; -import javax.annotation.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; diff --git a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java index fc35f9992..3f8bd4fdf 100644 --- a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java +++ b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java @@ -17,10 +17,10 @@ package io.appium.java_client.pagefactory.interceptors; import io.appium.java_client.proxy.MethodCallListener; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.pagefactory.ElementLocator; -import javax.annotation.Nullable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java index 738f49823..968ff824d 100644 --- a/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java +++ b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfASingleElement.java @@ -17,13 +17,13 @@ package io.appium.java_client.pagefactory.interceptors; import io.appium.java_client.proxy.MethodCallListener; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.support.pagefactory.ElementLocator; -import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.util.Objects; @@ -34,8 +34,7 @@ public abstract class InterceptorOfASingleElement implements MethodCallListener private final WeakReference driverReference; public InterceptorOfASingleElement( - @Nullable - ElementLocator locator, + @Nullable ElementLocator locator, WeakReference driverReference ) { this.locator = locator; diff --git a/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java b/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java index 190f9c4ae..eeb706b09 100644 --- a/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java +++ b/src/main/java/io/appium/java_client/pagefactory/utils/WebDriverUnpackUtility.java @@ -18,22 +18,21 @@ import io.appium.java_client.HasBrowserCheck; import io.appium.java_client.pagefactory.bys.ContentType; -import org.openqa.selenium.ContextAware; +import io.appium.java_client.remote.SupportsContextSwitching; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.WrapsElement; -import javax.annotation.Nullable; - import java.util.Optional; +import static io.appium.java_client.HasBrowserCheck.NATIVE_CONTEXT; import static io.appium.java_client.pagefactory.bys.ContentType.HTML_OR_DEFAULT; import static io.appium.java_client.pagefactory.bys.ContentType.NATIVE_MOBILE_SPECIFIC; +import static java.util.Locale.ROOT; public final class WebDriverUnpackUtility { - private static final String NATIVE_APP_PATTERN = "NATIVE_APP"; - private WebDriverUnpackUtility() { } @@ -94,12 +93,13 @@ public static WebDriver unpackWebDriverFromSearchContext(@Nullable SearchContext * {@link WebDriver} or {@link org.openqa.selenium.WebElement} or some other * user's extension/implementation. * Note: if you want to use your own implementation then it should - * implement {@link ContextAware} or {@link WrapsDriver} or {@link HasBrowserCheck} + * implement {@link SupportsContextSwitching} or {@link WrapsDriver} or {@link HasBrowserCheck} * @return current content type. It depends on current context. If current context is * NATIVE_APP it will return {@link ContentType#NATIVE_MOBILE_SPECIFIC}. * {@link ContentType#HTML_OR_DEFAULT} will be returned if the current context is WEB_VIEW. * {@link ContentType#HTML_OR_DEFAULT} also will be returned if the given - * {@link SearchContext} instance doesn't implement {@link ContextAware} and {@link WrapsDriver} + * {@link SearchContext} instance doesn't implement {@link SupportsContextSwitching} and + * {@link WrapsDriver} */ public static ContentType getCurrentContentType(SearchContext context) { var browserCheckHolder = unpackObjectFromSearchContext(context, HasBrowserCheck.class); @@ -107,9 +107,9 @@ public static ContentType getCurrentContentType(SearchContext context) { return NATIVE_MOBILE_SPECIFIC; } - var contextAware = unpackObjectFromSearchContext(context, ContextAware.class); - if (contextAware.map(ContextAware::getContext) - .filter(c -> c.toUpperCase().contains(NATIVE_APP_PATTERN)).isPresent()) { + var contextAware = unpackObjectFromSearchContext(context, SupportsContextSwitching.class); + if (contextAware.map(SupportsContextSwitching::getContext) + .filter(c -> c.toUpperCase(ROOT).contains(NATIVE_CONTEXT)).isPresent()) { return NATIVE_MOBILE_SPECIFIC; } diff --git a/src/main/java/io/appium/java_client/plugins/storage/StorageClient.java b/src/main/java/io/appium/java_client/plugins/storage/StorageClient.java new file mode 100644 index 000000000..013782ec8 --- /dev/null +++ b/src/main/java/io/appium/java_client/plugins/storage/StorageClient.java @@ -0,0 +1,248 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.plugins.storage; + +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.remote.ErrorCodec; +import org.openqa.selenium.remote.codec.AbstractHttpResponseCodec; +import org.openqa.selenium.remote.codec.w3c.W3CHttpResponseCodec; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.Contents; +import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.http.HttpHeader; +import org.openqa.selenium.remote.http.HttpMethod; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; +import org.openqa.selenium.remote.http.WebSocket; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static io.appium.java_client.plugins.storage.StorageUtils.calcSha1Digest; +import static io.appium.java_client.plugins.storage.StorageUtils.streamFileToWebSocket; + +/** + * This is a Java implementation of the Appium server storage plugin client. + * See the plugin README + * for more details. + */ +public class StorageClient { + public static final String PREFIX = "/storage"; + private final Json json = new Json(); + private final AbstractHttpResponseCodec responseCodec = new W3CHttpResponseCodec(); + private final ErrorCodec errorCodec = ErrorCodec.createDefault(); + + private final URL baseUrl; + private final HttpClient httpClient; + + public StorageClient(URL baseUrl) { + this.baseUrl = baseUrl; + this.httpClient = HttpClient.Factory.createDefault().createClient(baseUrl); + } + + public StorageClient(ClientConfig clientConfig) { + this.httpClient = HttpClient.Factory.createDefault().createClient(clientConfig); + this.baseUrl = clientConfig.baseUrl(); + } + + /** + * Adds a local file to the server storage. + * The remote file name is be set to the same value as the local file name. + * + * @param file File instance. + */ + public void add(File file) { + add(file, file.getName()); + } + + /** + * Adds a local file to the server storage. + * + * @param file File instance. + * @param name The remote file name. + */ + public void add(File file, String name) { + var request = new HttpRequest(HttpMethod.POST, formatPath(baseUrl, PREFIX, "add").toString()); + var httpResponse = httpClient.execute(setJsonPayload(request, Map.of( + "name", name, + "sha1", calcSha1Digest(file) + ))); + Map value = requireResponseValue(httpResponse); + final var wsTtlMs = (Long) value.get("ttlMs"); + //noinspection unchecked + var wsInfo = (Map) value.get("ws"); + var streamWsPathname = (String) wsInfo.get("stream"); + var eventWsPathname = (String) wsInfo.get("events"); + final var completion = new CountDownLatch(1); + final var lastException = new AtomicReference(null); + try (var streamWs = httpClient.openSocket( + new HttpRequest(HttpMethod.POST, formatPath(baseUrl, streamWsPathname).toString()), + new WebSocket.Listener() {} + ); var eventsWs = httpClient.openSocket( + new HttpRequest(HttpMethod.POST, formatPath(baseUrl, eventWsPathname).toString()), + new EventWsListener(lastException, completion) + )) { + streamFileToWebSocket(file, streamWs); + streamWs.close(); + if (!completion.await(wsTtlMs, TimeUnit.MILLISECONDS)) { + throw new IllegalStateException(String.format( + "Could not receive a confirmation about adding '%s' to the server storage within %sms timeout", + name, wsTtlMs + )); + } + var exc = lastException.get(); + if (exc != null) { + throw exc instanceof RuntimeException ? (RuntimeException) exc : new WebDriverException(exc); + } + } catch (InterruptedException e) { + throw new WebDriverException(e); + } + } + + /** + * Lists items that exist in the storage. + * + * @return All storage items. + */ + public List list() { + var request = new HttpRequest(HttpMethod.GET, formatPath(baseUrl, PREFIX, "list").toString()); + var httpResponse = httpClient.execute(request); + List> items = requireResponseValue(httpResponse); + return items.stream().map(item -> new StorageItem( + (String) item.get("name"), + (String) item.get("path"), + (Long) item.get("size") + )).collect(Collectors.toList()); + } + + /** + * Deletes an item from the server storage. + * + * @param name The name of the item to be deleted. + * @return true if the dletion was successful. + */ + public boolean delete(String name) { + var request = new HttpRequest(HttpMethod.POST, formatPath(baseUrl, PREFIX, "delete").toString()); + var httpResponse = httpClient.execute(setJsonPayload(request, Map.of( + "name", name + ))); + return requireResponseValue(httpResponse); + } + + /** + * Resets all items of the server storage. + */ + public void reset() { + var request = new HttpRequest(HttpMethod.POST, formatPath(baseUrl, PREFIX, "reset").toString()); + var httpResponse = httpClient.execute(request); + requireResponseValue(httpResponse); + } + + private static URL formatPath(URL url, String... suffixes) { + if (suffixes.length == 0) { + return url; + } + try { + var uri = url.toURI(); + var updatedPath = (uri.getPath() + "/" + String.join("/", suffixes)).replaceAll("(/{2,})", "/"); + return new URI( + uri.getScheme(), + uri.getAuthority(), + uri.getHost(), + uri.getPort(), + updatedPath, + uri.getQuery(), + uri.getFragment() + ).toURL(); + } catch (URISyntaxException | MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + private HttpRequest setJsonPayload(HttpRequest request, Map payload) { + var strData = json.toJson(payload); + var data = strData.getBytes(StandardCharsets.UTF_8); + request.setHeader(HttpHeader.ContentLength.getName(), String.valueOf(data.length)); + request.setHeader(HttpHeader.ContentType.getName(), "application/json; charset=utf-8"); + request.setContent(Contents.bytes(data)); + return request; + } + + private T requireResponseValue(HttpResponse httpResponse) { + var response = responseCodec.decode(httpResponse); + var value = response.getValue(); + if (value instanceof WebDriverException) { + throw (WebDriverException) value; + } + //noinspection unchecked + return (T) response.getValue(); + } + + private final class EventWsListener implements WebSocket.Listener { + private final AtomicReference lastException; + private final CountDownLatch completion; + + public EventWsListener(AtomicReference lastException, CountDownLatch completion) { + this.lastException = lastException; + this.completion = completion; + } + + @Override + public void onBinary(byte[] data) { + extractException(new String(data, StandardCharsets.UTF_8)).ifPresent(lastException::set); + completion.countDown(); + } + + @Override + public void onText(CharSequence data) { + extractException(data.toString()).ifPresent(lastException::set); + completion.countDown(); + } + + @Override + public void onError(Throwable cause) { + lastException.set(cause); + completion.countDown(); + } + + private Optional extractException(String payload) { + try { + Map record = json.toType(payload, Json.MAP_TYPE); + //noinspection unchecked + var value = (Map) record.get("value"); + if ((Boolean) value.get("success")) { + return Optional.empty(); + } + return Optional.of(errorCodec.decode(record)); + } catch (Exception e) { + return Optional.of(new WebDriverException(payload, e)); + } + } + } +} diff --git a/src/main/java/io/appium/java_client/plugins/storage/StorageItem.java b/src/main/java/io/appium/java_client/plugins/storage/StorageItem.java new file mode 100644 index 000000000..17ae1472e --- /dev/null +++ b/src/main/java/io/appium/java_client/plugins/storage/StorageItem.java @@ -0,0 +1,10 @@ +package io.appium.java_client.plugins.storage; + +import lombok.Value; + +@Value +public class StorageItem { + String name; + String path; + long size; +} diff --git a/src/main/java/io/appium/java_client/plugins/storage/StorageUtils.java b/src/main/java/io/appium/java_client/plugins/storage/StorageUtils.java new file mode 100644 index 000000000..3ef6c943c --- /dev/null +++ b/src/main/java/io/appium/java_client/plugins/storage/StorageUtils.java @@ -0,0 +1,90 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.plugins.storage; + +import org.openqa.selenium.remote.http.WebSocket; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Formatter; + +public class StorageUtils { + private static final int BUFFER_SIZE = 0xFFFF; + + private StorageUtils() { + } + + /** + * Calculates SHA1 hex digest of the given file. + * + * @param source The file instance to calculate the hash for. + * @return Hash digest represented as a string of hexadecimal numbers. + */ + public static String calcSha1Digest(File source) { + MessageDigest sha1sum; + try { + sha1sum = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + var buffer = new byte[BUFFER_SIZE]; + int bytesRead; + try (var in = new BufferedInputStream(new FileInputStream(source))) { + while ((bytesRead = in.read(buffer)) != -1) { + sha1sum.update(buffer, 0, bytesRead); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return byteToHex(sha1sum.digest()); + } + + /** + * Feeds the content of the given file to the provided web socket. + * + * @param source The source file instance. + * @param socket The destination web socket. + */ + public static void streamFileToWebSocket(File source, WebSocket socket) { + var buffer = new byte[BUFFER_SIZE]; + int bytesRead; + try (var in = new BufferedInputStream(new FileInputStream(source))) { + while ((bytesRead = in.read(buffer)) != -1) { + var currentBuffer = new byte[bytesRead]; + System.arraycopy(buffer, 0, currentBuffer, 0, bytesRead); + socket.sendBinary(currentBuffer); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String byteToHex(final byte[] hash) { + var formatter = new Formatter(); + for (byte b : hash) { + formatter.format("%02x", b); + } + var result = formatter.toString(); + formatter.close(); + return result; + } +} diff --git a/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java new file mode 100644 index 000000000..3540b5e7d --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/ElementAwareWebDriverListener.java @@ -0,0 +1,107 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.proxy; + +import net.bytebuddy.matcher.ElementMatchers; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.RemoteWebElement; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import static io.appium.java_client.proxy.Helpers.OBJECT_METHOD_NAMES; +import static io.appium.java_client.proxy.Helpers.createProxy; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +public class ElementAwareWebDriverListener implements MethodCallListener, ProxyAwareListener { + private WebDriver parent; + + /** + * Attaches the WebDriver proxy instance to this listener. + *

+ * The listener stores the WebDriver instance to associate it as parent to RemoteWebElement proxies. + * + * @param proxy A proxy instance of {@link WebDriver}. + */ + @Override + public void attachProxyInstance(Object proxy) { + if (proxy instanceof WebDriver) { + this.parent = (WebDriver) proxy; + } + } + + /** + * Intercepts method calls on a proxied WebDriver. + *

+ * If the result of the method call is a {@link RemoteWebElement}, + * it is wrapped with a proxy to allow further interception of RemoteWebElement method calls. + * If the result is a list, each item is checked, and all RemoteWebElements are + * individually proxied. All other return types are passed through unmodified. + * Avoid overriding this method, it will alter the behaviour of the listener. + * + * @param obj The object on which the method was invoked. + * @param method The method being invoked. + * @param args The arguments passed to the method. + * @param original A {@link Callable} that represents the original method execution. + * @return The (possibly wrapped) result of the method call. + * @throws Throwable if the original method or any wrapping logic throws an exception. + */ + @Override + public Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { + Object result = original.call(); + + if (result instanceof RemoteWebElement) { + return wrapElement((RemoteWebElement) result); + } + + if (result instanceof List) { + return ((List) result).stream() + .map(item -> item instanceof RemoteWebElement ? wrapElement( + (RemoteWebElement) item) : item) + .collect(Collectors.toList()); + } + + return result; + } + + private RemoteWebElement wrapElement( + RemoteWebElement original + ) { + RemoteWebElement proxy = createProxy( + RemoteWebElement.class, + new Object[]{}, + new Class[]{}, + Collections.singletonList(this), + ElementMatchers.not( + namedOneOf( + OBJECT_METHOD_NAMES.toArray(new String[0])) + .or(ElementMatchers.named("setId").or(ElementMatchers.named("setParent"))) + ) + ); + + proxy.setId(original.getId()); + + proxy.setParent((RemoteWebDriver) parent); + + return proxy; + } + +} diff --git a/src/main/java/io/appium/java_client/proxy/Helpers.java b/src/main/java/io/appium/java_client/proxy/Helpers.java index d162c3ed5..e420d494e 100644 --- a/src/main/java/io/appium/java_client/proxy/Helpers.java +++ b/src/main/java/io/appium/java_client/proxy/Helpers.java @@ -26,14 +26,14 @@ import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; +import org.jspecify.annotations.Nullable; -import javax.annotation.Nullable; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import java.util.WeakHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -50,7 +50,8 @@ public class Helpers { // the performance and to avoid extensive memory usage for our case, where // the amount of instrumented proxy classes we create is low in comparison to the amount // of proxy instances. - private static final ConcurrentMap> CACHED_PROXY_CLASSES = new ConcurrentHashMap<>(); + private static final Map> CACHED_PROXY_CLASSES = + Collections.synchronizedMap(new WeakHashMap<>()); private Helpers() { } @@ -144,6 +145,12 @@ public static T createProxy( try { T result = cls.cast(proxyClass.getConstructor(constructorArgTypes).newInstance(constructorArgs)); ((HasMethodCallListeners) result).setMethodCallListeners(listeners.toArray(MethodCallListener[]::new)); + + listeners.stream() + .filter(ProxyAwareListener.class::isInstance) + .map(ProxyAwareListener.class::cast) + .forEach(listener -> listener.attachProxyInstance(result)); + return result; } catch (SecurityException | ReflectiveOperationException e) { throw new IllegalStateException(String.format("Unable to create a proxy of %s", cls.getName()), e); diff --git a/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java b/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java new file mode 100644 index 000000000..f25c48a79 --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/ProxyAwareListener.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.proxy; + +/** + * Extension of {@link MethodCallListener} that allows access to the proxy instance it depends on. + *

+ * This interface is intended for listeners that need a reference to the proxy object. + *

+ * The {@link #attachProxyInstance(Object)} method will be invoked immediately after the proxy is created, + * allowing the listener to bind to it before any method interception begins. + *

+ * Example usage: Working with elements such as + * {@code RemoteWebElement} that require runtime mutation (e.g. setting parent driver or element ID). + */ +public interface ProxyAwareListener extends MethodCallListener { + + /** + * Binds the listener to the proxy instance passed. + *

+ * This is called once, immediately after proxy creation and before the proxy is returned to the caller. + * + * @param proxy the proxy instance created via {@code createProxy} that this listener is attached to. + */ + void attachProxyInstance(Object proxy); +} + diff --git a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java index 118f0ff81..ad6bb36c3 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -19,6 +19,9 @@ import com.google.common.base.Throwables; import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.internal.ReflectionHelpers; +import lombok.Getter; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.Command; @@ -33,12 +36,11 @@ import org.openqa.selenium.remote.ResponseCodec; import org.openqa.selenium.remote.codec.w3c.W3CHttpCommandCodec; import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.http.HttpClient.Factory; import org.openqa.selenium.remote.http.HttpRequest; import org.openqa.selenium.remote.http.HttpResponse; import org.openqa.selenium.remote.service.DriverService; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.IOException; import java.net.ConnectException; import java.net.MalformedURLException; @@ -51,12 +53,11 @@ import static java.util.Optional.ofNullable; import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION; +@NullMarked public class AppiumCommandExecutor extends HttpCommandExecutor { private final Optional serviceOptional; - - private final HttpClient.Factory httpClientFactory; - + @Getter private final AppiumClientConfig appiumClientConfig; /** @@ -64,32 +65,31 @@ public class AppiumCommandExecutor extends HttpCommandExecutor { * * @param additionalCommands is the map of Appium commands * @param service take a look at {@link DriverService} - * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param httpClientFactory take a look at {@link Factory} * @param appiumClientConfig take a look at {@link AppiumClientConfig} */ public AppiumCommandExecutor( - @Nonnull Map additionalCommands, + Map additionalCommands, @Nullable DriverService service, - @Nullable HttpClient.Factory httpClientFactory, - @Nonnull AppiumClientConfig appiumClientConfig) { + @Nullable Factory httpClientFactory, + AppiumClientConfig appiumClientConfig) { super(additionalCommands, appiumClientConfig, - ofNullable(httpClientFactory).orElseGet(AppiumCommandExecutor::getDefaultClientFactory) + ofNullable(httpClientFactory).orElseGet(HttpCommandExecutor::getDefaultClientFactory) ); serviceOptional = ofNullable(service); - this.httpClientFactory = httpClientFactory; this.appiumClientConfig = appiumClientConfig; } public AppiumCommandExecutor(Map additionalCommands, DriverService service, - HttpClient.Factory httpClientFactory) { + @Nullable Factory httpClientFactory) { this(additionalCommands, requireNonNull(service), httpClientFactory, AppiumClientConfig.defaultConfig().baseUrl(requireNonNull(service).getUrl())); } public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer, - HttpClient.Factory httpClientFactory) { + @Nullable Factory httpClientFactory) { this(additionalCommands, null, httpClientFactory, AppiumClientConfig.defaultConfig().baseUrl(requireNonNull(addressOfRemoteServer))); } @@ -119,38 +119,36 @@ public AppiumCommandExecutor(Map additionalCommands, this(additionalCommands, service, HttpClient.Factory.createDefault(), appiumClientConfig); } - @SuppressWarnings("SameParameterValue") - protected B getPrivateFieldValue( - Class cls, String fieldName, Class fieldType) { - return ReflectionHelpers.getPrivateFieldValue(cls, this, fieldName, fieldType); - } - + @Deprecated @SuppressWarnings("SameParameterValue") protected void setPrivateFieldValue( Class cls, String fieldName, Object newValue) { ReflectionHelpers.setPrivateFieldValue(cls, this, fieldName, newValue); } - protected Map getAdditionalCommands() { - //noinspection unchecked - return getPrivateFieldValue(HttpCommandExecutor.class, "additionalCommands", Map.class); + public Map getAdditionalCommands() { + return additionalCommands; } + public Factory getHttpClientFactory() { + return httpClientFactory; + } + + @Nullable protected CommandCodec getCommandCodec() { - //noinspection unchecked - return getPrivateFieldValue(HttpCommandExecutor.class, "commandCodec", CommandCodec.class); + return this.commandCodec; } public void setCommandCodec(CommandCodec newCodec) { - setPrivateFieldValue(HttpCommandExecutor.class, "commandCodec", newCodec); + this.commandCodec = newCodec; } public void setResponseCodec(ResponseCodec codec) { - setPrivateFieldValue(HttpCommandExecutor.class, "responseCodec", codec); + this.responseCodec = codec; } protected HttpClient getClient() { - return getPrivateFieldValue(HttpCommandExecutor.class, "client", HttpClient.class); + return this.client; } /** @@ -160,12 +158,8 @@ protected HttpClient getClient() { * @param serverUrl A url to override. */ protected void overrideServerUrl(URL serverUrl) { - if (this.appiumClientConfig == null) { - return; - } - setPrivateFieldValue(HttpCommandExecutor.class, "client", - ofNullable(this.httpClientFactory).orElseGet(AppiumCommandExecutor::getDefaultClientFactory) - .createClient(this.appiumClientConfig.baseUrl(serverUrl))); + HttpClient newClient = getHttpClientFactory().createClient(appiumClientConfig.baseUrl(serverUrl)); + setPrivateFieldValue(HttpCommandExecutor.class, "client", newClient); } private Response createSession(Command command) throws IOException { @@ -183,7 +177,7 @@ private Response createSession(Command command) throws IOException { refreshAdditionalCommands(); setResponseCodec(dialect.getResponseCodec()); Response response = result.createResponse(); - if (this.appiumClientConfig != null && this.appiumClientConfig.isDirectConnectEnabled()) { + if (appiumClientConfig.isDirectConnectEnabled()) { setDirectConnect(response); } @@ -191,7 +185,11 @@ private Response createSession(Command command) throws IOException { } public void refreshAdditionalCommands() { - getAdditionalCommands().forEach(this::defineCommand); + getAdditionalCommands().forEach(super::defineCommand); + } + + public void defineCommand(String commandName, CommandInfo info) { + super.defineCommand(commandName, info); } @SuppressWarnings("unchecked") diff --git a/src/main/java/io/appium/java_client/remote/DirectConnect.java b/src/main/java/io/appium/java_client/remote/DirectConnect.java index 809fdc736..fb1a05c51 100644 --- a/src/main/java/io/appium/java_client/remote/DirectConnect.java +++ b/src/main/java/io/appium/java_client/remote/DirectConnect.java @@ -18,8 +18,8 @@ import lombok.AccessLevel; import lombok.Getter; +import org.jspecify.annotations.Nullable; -import javax.annotation.Nullable; import java.net.MalformedURLException; import java.net.URL; import java.util.Map; diff --git a/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java b/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java index b126e4fa1..c576583dc 100644 --- a/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java +++ b/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java @@ -19,12 +19,11 @@ import io.appium.java_client.ExecutesMethod; import io.appium.java_client.MobileCommand; import io.appium.java_client.NoSuchContextException; -import org.openqa.selenium.ContextAware; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.Response; -import javax.annotation.Nullable; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -32,7 +31,7 @@ import static java.util.Objects.requireNonNull; -public interface SupportsContextSwitching extends WebDriver, ContextAware, ExecutesMethod { +public interface SupportsContextSwitching extends WebDriver, ExecutesMethod { /** * Switches to the given context. * diff --git a/src/main/java/io/appium/java_client/remote/SupportsLocation.java b/src/main/java/io/appium/java_client/remote/SupportsLocation.java index 91e12f0fa..c19dcc96c 100644 --- a/src/main/java/io/appium/java_client/remote/SupportsLocation.java +++ b/src/main/java/io/appium/java_client/remote/SupportsLocation.java @@ -18,40 +18,16 @@ import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import io.appium.java_client.Location; import io.appium.java_client.MobileCommand; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.html5.Location; -import org.openqa.selenium.html5.LocationContext; -import org.openqa.selenium.remote.html5.RemoteLocationContext; import java.util.HashMap; import java.util.Map; import java.util.Optional; -public interface SupportsLocation extends WebDriver, ExecutesMethod, LocationContext { - - /** - * Provides the location context. - * - * @return instance of {@link RemoteLocationContext} - * @deprecated This method, {@link LocationContext} and {@link RemoteLocationContext} interface are deprecated, use - * {@link #getLocation()} and {@link #setLocation(io.appium.java_client.Location)} instead. - */ - @Deprecated(forRemoval = true) - RemoteLocationContext getLocationContext(); - - /** - * Gets the current device's geolocation coordinates. - * - * @return A {@link Location} containing the location information. Returns null if the location is not available - * @deprecated This method and whole {@link LocationContext} interface are deprecated, use {@link #getLocation()} - * instead. - */ - @Deprecated(forRemoval = true) - default Location location() { - return getLocationContext().location(); - } +public interface SupportsLocation extends WebDriver, ExecutesMethod { /** * Gets the current device's geolocation coordinates. @@ -59,9 +35,9 @@ default Location location() { * @return A {@link Location} containing the location information. Throws {@link WebDriverException} if the * location is not available. */ - default io.appium.java_client.Location getLocation() { + default Location getLocation() { Map result = CommandExecutionHelper.execute(this, MobileCommand.GET_LOCATION); - return new io.appium.java_client.Location( + return new Location( result.get("latitude").doubleValue(), result.get("longitude").doubleValue(), Optional.ofNullable(result.get("altitude")).map(Number::doubleValue).orElse(null) @@ -72,20 +48,8 @@ default io.appium.java_client.Location getLocation() { * Sets the current device's geolocation coordinates. * * @param location A {@link Location} containing the new location information. - * @deprecated This method and whole {@link LocationContext} interface are deprecated, use - * {@link #setLocation(io.appium.java_client.Location)} instead. */ - @Deprecated(forRemoval = true) default void setLocation(Location location) { - getLocationContext().setLocation(location); - } - - /** - * Sets the current device's geolocation coordinates. - * - * @param location A {@link Location} containing the new location information. - */ - default void setLocation(io.appium.java_client.Location location) { var locationParameters = new HashMap(); locationParameters.put("latitude", location.getLatitude()); locationParameters.put("longitude", location.getLongitude()); diff --git a/src/main/java/io/appium/java_client/remote/SupportsRotation.java b/src/main/java/io/appium/java_client/remote/SupportsRotation.java index 74397a3e0..eb8a52b44 100644 --- a/src/main/java/io/appium/java_client/remote/SupportsRotation.java +++ b/src/main/java/io/appium/java_client/remote/SupportsRotation.java @@ -25,6 +25,8 @@ import java.util.Map; +import static java.util.Locale.ROOT; + public interface SupportsRotation extends WebDriver, ExecutesMethod { /** * Get device rotation. @@ -43,7 +45,7 @@ default void rotate(DeviceRotation rotation) { default void rotate(ScreenOrientation orientation) { execute(MobileCommand.SET_SCREEN_ORIENTATION, - Map.of("orientation", orientation.value().toUpperCase())); + Map.of("orientation", orientation.value().toUpperCase(ROOT))); } /** @@ -54,6 +56,6 @@ default void rotate(ScreenOrientation orientation) { default ScreenOrientation getOrientation() { Response response = execute(MobileCommand.GET_SCREEN_ORIENTATION); String orientation = String.valueOf(response.getValue()); - return ScreenOrientation.valueOf(orientation.toUpperCase()); + return ScreenOrientation.valueOf(orientation.toUpperCase(ROOT)); } } diff --git a/src/main/java/io/appium/java_client/remote/options/BaseOptions.java b/src/main/java/io/appium/java_client/remote/options/BaseOptions.java index dff4f5c44..cc544022c 100644 --- a/src/main/java/io/appium/java_client/remote/options/BaseOptions.java +++ b/src/main/java/io/appium/java_client/remote/options/BaseOptions.java @@ -16,6 +16,7 @@ package io.appium.java_client.remote.options; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.Platform; @@ -23,7 +24,6 @@ import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.CapabilityType; -import javax.annotation.Nullable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Map; @@ -49,7 +49,8 @@ public class BaseOptions> extends MutableCapabilities i SupportsFullResetOption, SupportsNewCommandTimeoutOption, SupportsBrowserNameOption, - SupportsPlatformVersionOption { + SupportsPlatformVersionOption, + SupportsWebSocketUrlOption { /** * Creates new instance with no preset capabilities. @@ -163,4 +164,4 @@ public Object getCapability(String capabilityName) { public static String toW3cName(String capName) { return W3CCapabilityKeys.INSTANCE.test(capName) ? capName : APPIUM_PREFIX + capName; } -} \ No newline at end of file +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsOrientationOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsOrientationOption.java index be2dd1a5d..2f5ef1645 100644 --- a/src/main/java/io/appium/java_client/remote/options/SupportsOrientationOption.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsOrientationOption.java @@ -21,6 +21,8 @@ import java.util.Optional; +import static java.util.Locale.ROOT; + public interface SupportsOrientationOption> extends Capabilities, CanSetCapability { String ORIENTATION_OPTION = "orientation"; @@ -44,7 +46,7 @@ default Optional getOrientation() { return Optional.ofNullable(getCapability(ORIENTATION_OPTION)) .map(v -> v instanceof ScreenOrientation ? (ScreenOrientation) v - : ScreenOrientation.valueOf((String.valueOf(v)).toUpperCase()) + : ScreenOrientation.valueOf((String.valueOf(v)).toUpperCase(ROOT)) ); } } diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsPageLoadStrategyOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsPageLoadStrategyOption.java index 752966ef6..63511c9b0 100644 --- a/src/main/java/io/appium/java_client/remote/options/SupportsPageLoadStrategyOption.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsPageLoadStrategyOption.java @@ -21,6 +21,8 @@ import java.util.Optional; +import static java.util.Locale.ROOT; + public interface SupportsPageLoadStrategyOption> extends Capabilities, CanSetCapability { String PAGE_LOAD_STRATEGY_OPTION = "pageLoadStrategy"; @@ -43,7 +45,7 @@ default T setPageLoadStrategy(PageLoadStrategy strategy) { default Optional getPageLoadStrategy() { return Optional.ofNullable(getCapability(PAGE_LOAD_STRATEGY_OPTION)) .map(String::valueOf) - .map(String::toUpperCase) + .map(strategy -> strategy.toUpperCase(ROOT)) .map(PageLoadStrategy::valueOf); } } diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsWebSocketUrlOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsWebSocketUrlOption.java new file mode 100644 index 000000000..1e14174cc --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsWebSocketUrlOption.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +public interface SupportsWebSocketUrlOption> extends + Capabilities, CanSetCapability { + String WEB_SOCKET_URL = "webSocketUrl"; + + /** + * Enable BiDi session support. + * + * @return self instance for chaining. + */ + default T enableBiDi() { + return amend(WEB_SOCKET_URL, true); + } + + /** + * Whether to enable BiDi session support. + * + * @return self instance for chaining. + */ + default T setWebSocketUrl(boolean value) { + return amend(WEB_SOCKET_URL, value); + } + + /** + * For input capabilities: whether enable BiDi session support is enabled. + * For session creation response capabilities: BiDi web socket URL. + * + * @return If called on request capabilities if BiDi support is enabled for the driver session + */ + default Optional getWebSocketUrl() { + return Optional.ofNullable(getCapability(WEB_SOCKET_URL)); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/UnhandledPromptBehavior.java b/src/main/java/io/appium/java_client/remote/options/UnhandledPromptBehavior.java index 3aa5f4add..52c2ea9d5 100644 --- a/src/main/java/io/appium/java_client/remote/options/UnhandledPromptBehavior.java +++ b/src/main/java/io/appium/java_client/remote/options/UnhandledPromptBehavior.java @@ -19,6 +19,8 @@ import java.util.Arrays; import java.util.stream.Collectors; +import static java.util.Locale.ROOT; + public enum UnhandledPromptBehavior { DISMISS, ACCEPT, DISMISS_AND_NOTIFY, ACCEPT_AND_NOTIFY, @@ -26,7 +28,7 @@ public enum UnhandledPromptBehavior { @Override public String toString() { - return name().toLowerCase().replace("_", " "); + return name().toLowerCase(ROOT).replace("_", " "); } /** diff --git a/src/main/java/io/appium/java_client/remote/options/W3CCapabilityKeys.java b/src/main/java/io/appium/java_client/remote/options/W3CCapabilityKeys.java index b29150311..09ff1680f 100644 --- a/src/main/java/io/appium/java_client/remote/options/W3CCapabilityKeys.java +++ b/src/main/java/io/appium/java_client/remote/options/W3CCapabilityKeys.java @@ -23,7 +23,7 @@ public class W3CCapabilityKeys implements Predicate { public static final W3CCapabilityKeys INSTANCE = new W3CCapabilityKeys(); private static final Predicate ACCEPTED_W3C_PATTERNS = Stream.of( - "^[\\w-]+:.*$", + "^[\\w-\\.]+:.*$", "^acceptInsecureCerts$", "^browserName$", "^browserVersion$", diff --git a/src/main/java/io/appium/java_client/safari/options/SafariOptions.java b/src/main/java/io/appium/java_client/safari/options/SafariOptions.java index fa170587b..9639509a1 100644 --- a/src/main/java/io/appium/java_client/safari/options/SafariOptions.java +++ b/src/main/java/io/appium/java_client/safari/options/SafariOptions.java @@ -31,7 +31,10 @@ import java.util.Map; /** - * https://github.com/appium/appium-safari-driver#usage + * Provides options specific to the Safari Driver. + * + *

For more details, refer to the + * capabilities documentation

*/ public class SafariOptions extends BaseOptions implements SupportsBrowserNameOption, diff --git a/src/main/java/io/appium/java_client/service/local/AppiumDriverLocalService.java b/src/main/java/io/appium/java_client/service/local/AppiumDriverLocalService.java index 8026300ad..70c9f024a 100644 --- a/src/main/java/io/appium/java_client/service/local/AppiumDriverLocalService.java +++ b/src/main/java/io/appium/java_client/service/local/AppiumDriverLocalService.java @@ -18,13 +18,13 @@ import lombok.Getter; import lombok.SneakyThrows; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.os.ExternalProcess; import org.openqa.selenium.remote.service.DriverService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; -import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -44,6 +44,7 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static io.appium.java_client.service.local.AppiumServiceBuilder.BROADCAST_IP4_ADDRESS; import static io.appium.java_client.service.local.AppiumServiceBuilder.BROADCAST_IP6_ADDRESS; +import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; import static org.slf4j.event.Level.DEBUG; @@ -407,7 +408,7 @@ private static Slf4jLogMessageContext parseSlf4jContextFromLogMessage(String log String loggerName = APPIUM_SERVICE_SLF4J_LOGGER_PREFIX; Level level = INFO; if (m.find()) { - loggerName += "." + m.group(2).toLowerCase().replaceAll("\\s+", ""); + loggerName += "." + m.group(2).toLowerCase(ROOT).replaceAll("\\s+", ""); if (m.group(1) != null) { level = DEBUG; } diff --git a/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java b/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java index ad3729e77..b22c93937 100644 --- a/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java +++ b/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java @@ -20,19 +20,18 @@ import com.google.gson.GsonBuilder; import io.appium.java_client.android.options.context.SupportsChromedriverExecutableOption; import io.appium.java_client.android.options.signing.SupportsKeystoreOptions; -import io.appium.java_client.internal.ReflectionHelpers; import io.appium.java_client.remote.MobileBrowserType; import io.appium.java_client.remote.options.SupportsAppOption; import io.appium.java_client.service.local.flags.GeneralServerFlag; import io.appium.java_client.service.local.flags.ServerArgument; import lombok.SneakyThrows; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; import org.openqa.selenium.os.ExecutableFinder; import org.openqa.selenium.remote.Browser; import org.openqa.selenium.remote.service.DriverService; -import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -50,6 +49,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; @@ -144,7 +144,7 @@ private static File findNpm() { private static File findMainScript() { File npm = findNpm(); - List cmdLine = System.getProperty("os.name").toLowerCase().contains("win") + List cmdLine = System.getProperty("os.name").toLowerCase(ROOT).contains("win") // npm is a batch script, so on windows we need to use cmd.exe in order to execute it ? Arrays.asList("cmd.exe", "/c", String.format("\"%s\" root -g", npm.getAbsolutePath())) : Arrays.asList(npm.getAbsolutePath(), "root", "-g"); @@ -400,10 +400,7 @@ protected List createArgs() { @Override protected void loadSystemProperties() { - File driverExecutable = ReflectionHelpers.getPrivateFieldValue( - DriverService.Builder.class, this, "exe", File.class - ); - if (driverExecutable == null) { + if (this.exe == null) { usingDriverExecutable(findDefaultExecutable()); } } @@ -475,4 +472,4 @@ protected AppiumDriverLocalService createDriverService(File nodeJSExecutable, in return new AppiumDriverLocalService(ipAddress, nodeJSExecutable, nodeJSPort, startupTimeout, nodeArguments, nodeEnvironment).withBasePath(basePath); } -} \ No newline at end of file +} diff --git a/src/main/java/io/appium/java_client/windows/options/WindowsOptions.java b/src/main/java/io/appium/java_client/windows/options/WindowsOptions.java index fd4d125b0..257c2807a 100644 --- a/src/main/java/io/appium/java_client/windows/options/WindowsOptions.java +++ b/src/main/java/io/appium/java_client/windows/options/WindowsOptions.java @@ -28,7 +28,7 @@ import java.util.Optional; /** - * https://github.com/appium/appium-windows-driver#usage + * https://github.com/appium/appium-windows-driver#usage. */ public class WindowsOptions extends BaseOptions implements SupportsAppOption, diff --git a/src/main/java/io/appium/java_client/ws/StringWebSocketClient.java b/src/main/java/io/appium/java_client/ws/StringWebSocketClient.java index 33c5d8aa6..f080c061c 100644 --- a/src/main/java/io/appium/java_client/ws/StringWebSocketClient.java +++ b/src/main/java/io/appium/java_client/ws/StringWebSocketClient.java @@ -16,12 +16,12 @@ package io.appium.java_client.ws; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpMethod; import org.openqa.selenium.remote.http.HttpRequest; import org.openqa.selenium.remote.http.WebSocket; -import javax.annotation.Nullable; import java.lang.ref.WeakReference; import java.net.URI; import java.util.List; diff --git a/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java b/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java index c07df5b68..f4d4aab96 100644 --- a/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java +++ b/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java @@ -19,7 +19,6 @@ import org.openqa.selenium.Alert; import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; -import org.openqa.selenium.ContextAware; import org.openqa.selenium.Cookie; import org.openqa.selenium.HasCapabilities; import org.openqa.selenium.JavascriptExecutor; @@ -39,8 +38,7 @@ import java.util.Map; import java.util.Set; -public class EmptyWebDriver implements WebDriver, ContextAware, - JavascriptExecutor, HasCapabilities, TakesScreenshot { +public class EmptyWebDriver implements WebDriver, JavascriptExecutor, HasCapabilities, TakesScreenshot { public EmptyWebDriver() { } @@ -48,18 +46,6 @@ private static List createStubList() { return List.of(new StubWebElement(), new StubWebElement()); } - public WebDriver context(String name) { - return null; - } - - public Set getContextHandles() { - return null; - } - - public String getContext() { - return ""; - } - public void get(String url) { } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java index 40c672ae7..c918db58e 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java @@ -16,12 +16,13 @@ package io.appium.java_client.pagefactory_tests; -import io.appium.java_client.TestUtils; import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.pagefactory.AndroidFindBy; import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.HowToUseLocators; +import io.appium.java_client.pagefactory.Widget; import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.utils.TestUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.openqa.selenium.WebDriver; @@ -37,6 +38,7 @@ import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; import static java.time.Duration.ofSeconds; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -69,6 +71,7 @@ public class DesktopBrowserCompatibilityTest { assertNotEquals(0, main.size()); assertNull(trap1); assertNull(trap2); + foundLinks.forEach(element -> assertFalse(Widget.class.isAssignableFrom(element.getClass()))); } finally { driver.quit(); } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java index 636d33c4e..d31a5bf93 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java @@ -11,6 +11,7 @@ import org.openqa.selenium.logging.Logs; import org.openqa.selenium.remote.Response; +import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -155,20 +156,72 @@ public Cookie getCookieNamed(String name) { @Override public Timeouts timeouts() { return new Timeouts() { - @Override + /** + * Does nothing. + * + * @param time The amount of time to wait. + * @param unit The unit of measure for {@code time}. + * @return A self reference. + * @deprecated Kept for the backward compatibility, should be removed when a minimum Selenium + * version is bumped to 4.33.0 or higher. + */ + @Deprecated public Timeouts implicitlyWait(long time, TimeUnit unit) { return this; } - @Override + public Timeouts implicitlyWait(Duration duration) { + return this; + } + + /** + * Does nothing. + * + * @param time The timeout value. + * @param unit The unit of time. + * @return A self reference. + * @deprecated Kept for the backward compatibility, should be removed when Selenium client removes + * this method from its interface. + */ + @Deprecated public Timeouts setScriptTimeout(long time, TimeUnit unit) { return this; } - @Override + /** + * Does nothing. + * + * @param duration The timeout value. + * @return A self reference. + * @deprecated Kept for the backward compatibility, should be removed when Selenium client removes + * this method from its interface. + */ + @Deprecated + public Timeouts setScriptTimeout(Duration duration) { + return this; + } + + public Timeouts scriptTimeout(Duration duration) { + return this; + } + + /** + * Does nothing. + * + * @param time The timeout value. + * @param unit The unit of time. + * @return A self reference. + * @deprecated Kept for the backward compatibility, should be removed when Selenium client removes + * this method from its interface. + */ + @Deprecated public Timeouts pageLoadTimeout(long time, TimeUnit unit) { return this; } + + public Timeouts pageLoadTimeout(Duration duration) { + return this; + } }; } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/DefaultStubWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/DefaultStubWidget.java index 5977646d7..7de8cf327 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/DefaultStubWidget.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/DefaultStubWidget.java @@ -1,11 +1,17 @@ package io.appium.java_client.pagefactory_tests.widget.tests; import io.appium.java_client.pagefactory.Widget; +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.Point; +import org.openqa.selenium.Rectangle; +import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import java.util.List; -public class DefaultStubWidget extends Widget { +public class DefaultStubWidget extends Widget implements WebElement { protected DefaultStubWidget(WebElement element) { super(element); } @@ -22,4 +28,79 @@ public List getSubWidgets() { public String toString() { return getWrappedElement().toString(); } + + @Override + public void click() { + getWrappedElement().click(); + } + + @Override + public void submit() { + getWrappedElement().submit(); + } + + @Override + public void sendKeys(CharSequence... keysToSend) { + getWrappedElement().sendKeys(keysToSend); + } + + @Override + public void clear() { + getWrappedElement().clear(); + } + + @Override + public String getTagName() { + return getWrappedElement().getTagName(); + } + + @Override + public @Nullable String getAttribute(String name) { + return getWrappedElement().getAttribute(name); + } + + @Override + public boolean isSelected() { + return getWrappedElement().isSelected(); + } + + @Override + public boolean isEnabled() { + return getWrappedElement().isEnabled(); + } + + @Override + public String getText() { + return getWrappedElement().getText(); + } + + @Override + public boolean isDisplayed() { + return getWrappedElement().isDisplayed(); + } + + @Override + public Point getLocation() { + return getWrappedElement().getLocation(); + } + + @Override + public Dimension getSize() { + return getWrappedElement().getSize(); + } + + @Override + public Rectangle getRect() { + return getWrappedElement().getRect(); + } + + @Override + public String getCssValue(String propertyName) { + return getWrappedElement().getCssValue(propertyName); + } + + @Override + public X getScreenshotAs(OutputType target) throws WebDriverException { + return getWrappedElement().getScreenshotAs(target); + } } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java index c3ee905fb..c7e50ef5f 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedAppTest.java @@ -6,10 +6,12 @@ import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; import io.appium.java_client.pagefactory_tests.widget.tests.DefaultStubWidget; import io.appium.java_client.pagefactory_tests.widget.tests.android.DefaultAndroidWidget; +import org.hamcrest.Matchers; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; import java.util.List; import java.util.stream.Stream; @@ -58,6 +60,8 @@ void checkThatWidgetsAreCreatedCorrectly(AbstractApp app, WebDriver driver, assertThat("Expected widget class was " + widgetClass.getName(), app.getWidget().getSelfReference().getClass(), equalTo(widgetClass)); + assertThat(app.getWidget().getSelfReference(), + Matchers.instanceOf(WebElement.class)); List> classes = app.getWidgets().stream().map(abstractWidget -> abstractWidget .getSelfReference().getClass()) diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java index 3c1c9145d..26e0d2f74 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/CombinedWidgetTest.java @@ -12,20 +12,29 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.lessThan; import static org.openqa.selenium.support.PageFactory.initElements; @SuppressWarnings({"unchecked", "unused"}) public class CombinedWidgetTest { + /** + * Based on how many Proxy Classes are created during this test class, + * this number is used to determine if the cache is being purged correctly between tests. + */ + private static final int THRESHOLD_SIZE = 50; + /** * Test data generation. * @@ -57,6 +66,7 @@ public static Stream data() { @ParameterizedTest @MethodSource("data") void checkThatWidgetsAreCreatedCorrectly(AbstractApp app, WebDriver driver, Class widgetClass) { + assertProxyClassCacheGrowth(); initElements(new AppiumFieldDecorator(driver), app); assertThat("Expected widget class was " + widgetClass.getName(), app.getWidget().getSubWidget().getSelfReference().getClass(), @@ -161,4 +171,32 @@ public List getWidgets() { return multipleWidgets; } } + + + /** + * Assert proxy class cache growth for this test class. + * The (@link io.appium.java_client.proxy.Helpers#CACHED_PROXY_CLASSES) should be populated during these tests. + * Prior to the Caching issue being resolved + * - the CACHED_PROXY_CLASSES would grow indefinitely, resulting in an Out Of Memory exception. + * - this ParameterizedTest would have the CACHED_PROXY_CLASSES grow to 266 entries. + */ + private void assertProxyClassCacheGrowth() { + System.gc(); //Trying to force a collection for more accurate check numbers + assertThat( + "Proxy Class Cache threshold is " + THRESHOLD_SIZE, + getCachedProxyClassesSize(), + lessThan(THRESHOLD_SIZE) + ); + } + + private int getCachedProxyClassesSize() { + try { + Field cpc = Class.forName("io.appium.java_client.proxy.Helpers").getDeclaredField("CACHED_PROXY_CLASSES"); + cpc.setAccessible(true); + Map cachedProxyClasses = (Map) cpc.get(null); + return cachedProxyClasses.size(); + } catch (NoSuchFieldException | ClassNotFoundException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/io/appium/java_client/plugin/StorageTest.java b/src/test/java/io/appium/java_client/plugin/StorageTest.java new file mode 100644 index 000000000..a12708dc7 --- /dev/null +++ b/src/test/java/io/appium/java_client/plugin/StorageTest.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.plugin; + +import io.appium.java_client.plugins.storage.StorageClient; +import io.appium.java_client.utils.TestUtils; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StorageTest { + private StorageClient storageClient; + + @BeforeEach + void before() throws MalformedURLException { + // These tests assume Appium server with storage plugin is already running + // at the given baseUrl + Assumptions.assumeFalse(TestUtils.isCiEnv()); + storageClient = new StorageClient(new URL("http://127.0.0.1:4723")); + storageClient.reset(); + } + + @Test + void shouldBeAbleToPerformBasicStorageActions() { + assertTrue(storageClient.list().isEmpty()); + var name = "hello appium - saved page.htm"; + var testFile = TestUtils.resourcePathToAbsolutePath("html/" + name).toFile(); + storageClient.add(testFile); + assertItemsCount(1); + assertTrue(storageClient.delete(name)); + assertItemsCount(0); + storageClient.add(testFile); + assertItemsCount(1); + storageClient.reset(); + assertItemsCount(0); + } + + private void assertItemsCount(int expected) { + var items = storageClient.list(); + assertEquals(expected, items.size()); + } +} diff --git a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java index a8767629d..af0ca78d9 100644 --- a/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -19,14 +19,20 @@ import io.appium.java_client.ios.IOSDriver; import io.appium.java_client.ios.options.XCUITestOptions; import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; +import org.openqa.selenium.NoSuchSessionException; +import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.remote.UnreachableBrowserException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.Callable; import static io.appium.java_client.proxy.Helpers.createProxy; @@ -45,6 +51,31 @@ public FakeIOSDriver(URL url, Capabilities caps) { @Override protected void startSession(Capabilities capabilities) { } + + @Override + public WebElement findElement(By locator) { + RemoteWebElement webElement = new RemoteWebElement(); + webElement.setId(locator.toString()); + webElement.setParent(this); + return webElement; + } + + @Override + public List findElements(By locator) { + List webElements = new ArrayList<>(); + + RemoteWebElement webElement1 = new RemoteWebElement(); + webElement1.setId("1234"); + webElement1.setParent(this); + webElements.add(webElement1); + + RemoteWebElement webElement2 = new RemoteWebElement(); + webElement2.setId("5678"); + webElement2.setParent(this); + webElements.add(webElement2); + + return webElements; + } } @Test @@ -133,4 +164,53 @@ public Object onError(Object obj, Method method, Object[] args, Throwable e) thr "onError get") ))); } + + + @Test + void shouldFireEventsForAllWebDriverCommands() throws MalformedURLException { + final StringBuilder acc = new StringBuilder(); + + var remoteWebElementListener = new ElementAwareWebDriverListener() { + @Override + public void beforeCall(Object target, Method method, Object[] args) { + acc.append("beforeCall ").append(method.getName()).append("\n"); + } + }; + + FakeIOSDriver driver = createProxy( + FakeIOSDriver.class, + new Object[] {new URL("http://localhost:4723/"), new XCUITestOptions()}, + new Class[] {URL.class, Capabilities.class}, + remoteWebElementListener + ); + + WebElement element = driver.findElement(By.id("button")); + + assertThrows( + NoSuchSessionException.class, + element::click + ); + + List elements = driver.findElements(By.id("button")); + + assertThrows( + NoSuchSessionException.class, + () -> elements.get(1).isSelected() + ); + + assertThat(acc.toString().trim(), is(equalTo( + String.join("\n", + "beforeCall findElement", + "beforeCall click", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities", + "beforeCall findElements", + "beforeCall isSelected", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities" + ) + ))); + } } diff --git a/src/test/java/io/appium/java_client/remote/AppiumCommandExecutorTest.java b/src/test/java/io/appium/java_client/remote/AppiumCommandExecutorTest.java new file mode 100644 index 000000000..38b1b6459 --- /dev/null +++ b/src/test/java/io/appium/java_client/remote/AppiumCommandExecutorTest.java @@ -0,0 +1,41 @@ +package io.appium.java_client.remote; + +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.MobileCommand; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class AppiumCommandExecutorTest { + private static final String APPIUM_URL = "https://appium.example.com"; + + private AppiumCommandExecutor createExecutor() { + URL baseUrl; + try { + baseUrl = new URL(APPIUM_URL); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + AppiumClientConfig clientConfig = AppiumClientConfig.defaultConfig().baseUrl(baseUrl); + return new AppiumCommandExecutor(MobileCommand.commandRepository, clientConfig); + } + + @Test + void getAdditionalCommands() { + assertNotNull(createExecutor().getAdditionalCommands()); + } + + @Test + void getHttpClientFactory() { + assertNotNull(createExecutor().getHttpClientFactory()); + } + + @Test + void overrideServerUrl() { + assertDoesNotThrow(() -> createExecutor().overrideServerUrl(new URL("https://direct.example.com"))); + } +} diff --git a/src/test/java/io/appium/java_client/remote/options/BaseOptionsTest.java b/src/test/java/io/appium/java_client/remote/options/BaseOptionsTest.java new file mode 100644 index 000000000..50e36818d --- /dev/null +++ b/src/test/java/io/appium/java_client/remote/options/BaseOptionsTest.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.remote.options; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BaseOptionsTest { + + @ParameterizedTest + @CsvSource({ + "test, appium:test", + "appium:test, appium:test", + "browserName, browserName", + "digital.ai:accessKey, digital.ai:accessKey", + "digital-ai:accessKey, digital-ai:accessKey", + "digital-ai:my_custom-cap:xyz, digital-ai:my_custom-cap:xyz", + "digital-ai:my_custom-cap?xyz, digital-ai:my_custom-cap?xyz", + }) + void verifyW3CMapping(String capName, String expected) { + var w3cName = BaseOptions.toW3cName(capName); + assertEquals(expected, w3cName); + } +} \ No newline at end of file diff --git a/src/test/java/io/appium/java_client/TestUtils.java b/src/test/java/io/appium/java_client/utils/TestUtils.java similarity index 70% rename from src/test/java/io/appium/java_client/TestUtils.java rename to src/test/java/io/appium/java_client/utils/TestUtils.java index 1d650777c..8aa90892c 100644 --- a/src/test/java/io/appium/java_client/TestUtils.java +++ b/src/test/java/io/appium/java_client/utils/TestUtils.java @@ -1,11 +1,27 @@ -package io.appium.java_client; +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.utils; + +import org.jspecify.annotations.Nullable; import org.openqa.selenium.Dimension; import org.openqa.selenium.Point; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebElement; -import javax.annotation.Nullable; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; @@ -18,6 +34,11 @@ import java.util.function.Supplier; public class TestUtils { + public static final String IOS_SIM_VODQA_RELEASE_URL = + "https://github.com/appium/VodQAReactNative/releases/download/v1.2.3/VodQAReactNative-simulator-release.zip"; + public static final String ANDROID_APIDEMOS_APK_URL = + "https://github.com/appium/android-apidemos/releases/download/v6.0.2/ApiDemos-debug.apk"; + private TestUtils() { } @@ -78,4 +99,8 @@ public static Point getCenter(WebElement webElement, @Nullable Point location) { } return new Point(location.x + dim.width / 2, location.y + dim.height / 2); } + + public static boolean isCiEnv() { + return System.getenv("CI") != null; + } }