diff --git a/.github/workflows/gradle.yml b/.github/workflows/ci.yml similarity index 61% rename from .github/workflows/gradle.yml rename to .github/workflows/ci.yml index 252710957..765e0c6f7 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/ci.yml @@ -19,15 +19,17 @@ concurrency: cancel-in-progress: true env: + CI: true ANDROID_SDK_VERSION: "28" ANDROID_EMU_NAME: test ANDROID_EMU_TARGET: default - # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md - XCODE_VERSION: "15.4" - IOS_DEVICE_NAME: iPhone 15 - IOS_PLATFORM_VERSION: "17.5" + # https://github.com/actions/runner-images/blob/main/images/macos (macos-15 for Xcode 16 / iOS 18.5) + XCODE_VERSION: "16.4" + IOS_DEVICE_NAME: iPhone 16 + IOS_PLATFORM_VERSION: "18.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,29 +37,74 @@ jobs: strategy: matrix: include: - - java: 11 - # 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 - 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: flutter-ios + selenium: stable + - java: 21 + selenium: stable + - java: 25 + selenium: stable - java: 17 - platform: ubuntu-latest + selenium: snapshot + - java: 21 + selenium: snapshot + - java: 25 + selenium: snapshot + fail-fast: false + + runs-on: ubuntu-latest + + continue-on-error: ${{ matrix.selenium == 'snapshot' }} + + name: JDK ${{ matrix.java }} build (${{ matrix.selenium }}) + steps: + - uses: actions/checkout@v7 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: ${{ matrix.java }} + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Resolve Selenium snapshot version + if: matrix.selenium == 'snapshot' + run: | + 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" + + - name: Build with Gradle (stable Selenium) + if: matrix.selenium == 'stable' + run: ./gradlew clean build + + - name: Build with Gradle (snapshot Selenium) + if: matrix.selenium == 'snapshot' + run: ./gradlew clean build -Pselenium.version=$latest_snapshot + + e2e: + needs: [build] + if: github.event_name == 'pull_request' + + strategy: + matrix: + include: + - platform: macos-15 + e2e-tests: ios + - platform: macos-15 + e2e-tests: flutter-ios + - platform: ubuntu-latest e2e-tests: android - - java: 17 - platform: ubuntu-latest + - platform: ubuntu-latest e2e-tests: flutter-android - - java: 21 - platform: ubuntu-latest fail-fast: false runs-on: ${{ matrix.platform }} - name: JDK ${{ matrix.java }} - ${{ matrix.platform }} ${{ matrix.e2e-tests }} + name: "e2e - ${{ matrix.platform }} ${{ matrix.e2e-tests }}" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Enable KVM group perms if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android' @@ -66,31 +113,21 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + - name: Set up JDK + uses: actions/setup-java@v5 with: distribution: 'zulu' - java-version: ${{ matrix.java }} + java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Build with Gradle - 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])") - echo ">>> $latest_snapshot" - echo "latest_snapshot=$latest_snapshot" >> "$GITHUB_ENV" - ./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot + uses: gradle/actions/setup-gradle@v5 - name: Install Node.js - if: ${{ matrix.e2e-tests }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: 'lts/*' - name: Install Appium - if: ${{ matrix.e2e-tests }} run: npm install --location=global appium - name: Install UIA2 driver @@ -105,7 +142,7 @@ jobs: if: matrix.e2e-tests == 'android' uses: reactivecircus/android-emulator-runner@v2 with: - script: ./gradlew e2eAndroidTest -PisCI -Pselenium.version=$latest_snapshot + script: ./gradlew e2eAndroidTest api-level: ${{ env.ANDROID_SDK_VERSION }} avd-name: ${{ env.ANDROID_EMU_NAME }} disable-spellchecker: true @@ -116,7 +153,7 @@ jobs: if: matrix.e2e-tests == 'flutter-android' uses: reactivecircus/android-emulator-runner@v2 with: - script: ./gradlew e2eFlutterTest -Pplatform="android" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_ANDROID_APP }} + script: ./gradlew e2eFlutterTest -Pplatform="android" -PflutterApp=${{ env.FLUTTER_ANDROID_APP }} api-level: ${{ env.ANDROID_SDK_VERSION }} avd-name: ${{ env.ANDROID_EMU_NAME }} disable-spellchecker: true @@ -128,22 +165,28 @@ jobs: uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: "${{ env.XCODE_VERSION }}" + - name: Prepare iOS simulator if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios' - uses: futureware-tech/simulator-action@v4 + uses: futureware-tech/simulator-action@v5 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 + run: ./gradlew e2eIosTest - name: Run Flutter iOS E2E tests if: matrix.e2e-tests == 'flutter-ios' - run: ./gradlew e2eFlutterTest -Pplatform="ios" -Pselenium.version=$latest_snapshot -PisCI -PflutterApp=${{ env.FLUTTER_IOS_APP }} + run: ./gradlew e2eFlutterTest -Pplatform="ios" -PflutterApp=${{ env.FLUTTER_IOS_APP }} diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 393ca3bd6..3e8521897 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -1,15 +1,11 @@ name: Conventional Commits on: pull_request: + types: [opened, edited, synchronize, reopened] jobs: lint: - name: https://www.conventionalcommits.org - runs-on: ubuntu-latest - steps: - - uses: beemojs/conventional-pr-action@v3 - with: - config-preset: angular - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: appium/appium-workflows/.github/workflows/pr-title.yml@main + with: + config-preset: angular diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6b2a253c2..661e5e3cb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,24 +2,35 @@ name: Publish package to the Maven Central Repository on: release: types: [created] + workflow_dispatch: + branches: [master] + inputs: + release_tag: + description: 'Tag to publish (e.g. v10.1.0). Use for re-publishing a release.' + required: true jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.release_tag) || github.ref }} - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: '11' + java-version: '17' 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..d89b0acb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,71 @@ 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.1.1_ +- **[BUG FIX]** + - Perform additional security checks on `overrideServerUrl` API [#2408](https://github.com/appium/java-client/pull/2408) + - Make the library compatible to Selenium 4.44 [#2410](https://github.com/appium/java-client/pull/2410) +- **[DEPENDENCY CHANGE]** + - Bump minimum Selenium version from `4.40.0` to `4.42.0` [#2390](https://github.com/appium/java-client/pull/2390) + +_10.1.0_ +- **[ENHANCEMENTS]** + - Add support for descendant and ancestor flutter locators [#2357](https://github.com/appium/java-client/pull/2357) +- **[BUG FIX]** + - Fix `ListOutputStream.close()` to close all streams on failure [#2392](https://github.com/appium/java-client/pull/2392) + - Avoid using system default locale [#2352](https://github.com/appium/java-client/pull/2352) +- **[REFACTOR]** + - Avoid using reflection to manage `AppiumCommandExecutor` state [#2356](https://github.com/appium/java-client/pull/2356) +- **[DEPENDENCY CHANGE]** + - Bump minimum Selenium version from `4.35.0` to `4.40.0` [#2383](https://github.com/appium/java-client/pull/2383) + - Bump com.google.code.gson:gson from `2.13.1` to `2.13.2` [#2342](https://github.com/appium/java-client/pull/2342) + - Bump org.apache.commons:commons-lang3 from `3.18.0` to `3.20.0` [#2350](https://github.com/appium/java-client/pull/2350), [#2374](https://github.com/appium/java-client/pull/2374) + +_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 +706,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..a7761c8e4 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,13 @@ 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` +----------------------------------------------------------------------------------------------------|----------------------------- +`10.1.1` | `4.42.0`, `4.43.0` +`10.1.0` | `4.40.0`, `4.41.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 +183,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 +202,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 +223,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..7ef290e2c 100644 --- a/build.gradle +++ b/build.gradle @@ -7,23 +7,43 @@ 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.2' + id 'com.gradleup.shadow' version '9.6.1' + id 'org.jreleaser' version '1.25.0' } +ext { + seleniumVersion = project.property('selenium.version') + appiumClientVersion = project.property('appiumClient.version') + slf4jVersion = '2.0.18' +} + +group = 'io.appium' +version = appiumClientVersion + repositories { mavenCentral() - if (project.hasProperty("isCI")) { + // Only add Selenium snapshot repo when resolving a SNAPSHOT version. + // Release versions (e.g. from gradle.properties) resolve from Maven Central only. + if (project.property('selenium.version').toString().contains('SNAPSHOT')) { maven { - 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") + } } } } +configurations.configureEach { + resolutionStrategy.cacheChangingModulesFor 1, 'minutes' +} + java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -31,17 +51,12 @@ 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.46' + annotationProcessor 'org.projectlombok:lombok:1.18.46' - if (project.hasProperty("isCI")) { + // Pin exact Selenium version when using a SNAPSHOT (needs snapshot repo); otherwise use version range. + if (seleniumVersion.toString().contains('SNAPSHOT')) { api "org.seleniumhq.selenium:selenium-api:${seleniumVersion}" api "org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}" api "org.seleniumhq.selenium:selenium-support:${seleniumVersion}" @@ -65,8 +80,9 @@ dependencies { } } } - implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.google.code.gson:gson:2.14.0' implementation "org.slf4j:slf4j-api:${slf4jVersion}" + implementation 'org.jspecify:jspecify:1.0.1' } dependencyCheck { @@ -74,7 +90,7 @@ dependencyCheck { } jacoco { - toolVersion = '0.8.12' + toolVersion = '0.8.13' } tasks.withType(JacocoReport).configureEach { @@ -90,7 +106,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 +175,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 +215,7 @@ testing { configureEach { useJUnitJupiter() dependencies { - implementation 'org.junit.jupiter:junit-jupiter:5.11.3' + implementation 'org.junit.jupiter:junit-jupiter:5.14.4' runtimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'org.hamcrest:hamcrest:3.0' runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}" @@ -213,7 +233,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.4') { exclude group: 'org.seleniumhq.selenium' } } @@ -233,7 +253,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 +277,7 @@ testing { dependencies { implementation project() implementation(sourceSets.test.output) - implementation('io.github.bonigarcia:webdrivermanager:5.9.2') { + implementation('io.github.bonigarcia:webdrivermanager:6.3.4') { 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/The-starting-of-an-app-using-Appium-node-server-started-programmatically.md b/docs/The-starting-of-an-app-using-Appium-node-server-started-programmatically.md index 9397385c5..4b8942292 100644 --- a/docs/The-starting-of-an-app-using-Appium-node-server-started-programmatically.md +++ b/docs/The-starting-of-an-app-using-Appium-node-server-started-programmatically.md @@ -1,7 +1,7 @@ # Requirements -- Installed Node.js 7 or greater. +- Installed Node.js 7 or greater. -- At least an appium server instance installed via __npm__. +- At least an appium server instance installed via __npm__. # The basic principle. @@ -10,7 +10,7 @@ It works the similar way as common [ChromeDriver](https://seleniumhq.github.io/s # How to prepare the local service before the starting -## If there is no specific parameters then +## If there is no specific parameters then ```java import io.appium.java_client.service.local.AppiumDriverLocalService; @@ -20,7 +20,7 @@ It works the similar way as common [ChromeDriver](https://seleniumhq.github.io/s service.start(); ... service.stop(); -``` +``` ### FYI @@ -32,20 +32,23 @@ AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService( It is more usual for UNIX/LINUX-like OS's. Also there are situations when should be used an another Node.JS instance, e.g. the instance which is installed in the directory that differs from one defined at the PATH environmental variable. The same may be true for Appium node server (it is related to _appium.js_ file (v <= 1.4.16) and _main.js_ (v >= 1.5.0)). -At this case user is able to set up values of the **NODE_BINARY_PATH** (The environmental variable used to define the path to executable NodeJS file (node.exe for WIN and node for Linux/MacOS X)) and the **APPIUM_BINARY_PATH** (The environmental variable used to define the path to executable appium.js (1.4.x and lower) or main.js (1.5.x and higher)) environmental variables/system properties. Also it is possible to define these values programmatically: +At this case user is able to set up values of the **NODE_BINARY_PATH** (The environmental variable used to define the path to executable NodeJS file (node.exe for WIN and node for Linux/MacOS X)) and the **APPIUM_BINARY_PATH** (The environmental variable used to define the path to executable appium.js (1.4.x and lower) or main.js (1.5.x and higher)) environmental variables/system properties. Also it is possible to define these values programmatically: ```java +import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.service.local.AppiumServiceBuilder; + //appium.node.js.exec.path -System.setProperty(AppiumServiceBuilder.NODE_PATH , +System.setProperty(AppiumServiceBuilder.NODE_PATH, "the path to the desired node.js executable"); -System.setProperty(AppiumServiceBuilder.APPIUM_PATH , +System.setProperty(AppiumServiceBuilder.APPIUM_PATH, "the path to the desired appium.js or main.js"); AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); ``` -## If there should be non default parameters specified then +## If there should be non default parameters specified then ```java import io.appium.java_client.service.local.AppiumDriverLocalService; @@ -57,9 +60,9 @@ AppiumDriverLocalService service = AppiumDriverLocalService. buildService(new AppiumServiceBuilder(). withArgument(GeneralServerFlag.TEMP_DIRECTORY, "The_path_to_the_temporary_directory")); -``` +``` -or +or ```java import io.appium.java_client.service.local.AppiumDriverLocalService; @@ -70,7 +73,7 @@ import io.appium.java_client.service.local.flags.GeneralServerFlag; AppiumDriverLocalService service = new AppiumServiceBuilder(). withArgument(GeneralServerFlag.TEMP_DIRECTORY, "The_path_to_the_temporary_directory").build(); -``` +``` Lists of available server side flags are here: @@ -78,56 +81,56 @@ Lists of available server side flags are here: - io.appium.java_client.service.local.flags.AndroidServerFlag; - io.appium.java_client.service.local.flags.IOSServerFlag - + ## Which parameters also can be defined - If it is necessary to define some specific port or any free port ```java new AppiumServiceBuilder().usingPort(4000); -``` +``` -or +or ```java new AppiumServiceBuilder().usingAnyFreePort(); -``` +``` - If it is necessary to use another IP address ```java new AppiumServiceBuilder().withIPAddress("127.0.0.1"); -``` +``` - If it is necessary to define output log file ```java -import java.io.File; +import java.io.File; ... new AppiumServiceBuilder().withLogFile(logFile); -``` +``` - If it is necessary to define another Node.js executable file ```java import java.io.File; - + ... new AppiumServiceBuilder().usingDriverExecutable(nodeJSExecutable); -``` +``` - If it is necessary to define another appium.js/main.js file ```java import java.io.File; - + ... -//appiumJS is the full or relative path to +//appiumJS is the full or relative path to //the appium.js (v<=1.4.16) or main.js (v>=1.5.0) new AppiumServiceBuilder().withAppiumJS(new File(appiumJS)); -``` +``` - It is possible to define server capabilities (node server v >= 1.5.0) @@ -152,7 +155,7 @@ serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emul serverCapabilities.setCapability(MobileCapabilityType.FULL_RESET, true); serverCapabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 60); serverCapabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); -serverCapabilities.setCapability(AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE, +serverCapabilities.setCapability(AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE, chrome.getAbsolutePath()); //this capability set can be used for all cases AppiumServiceBuilder builder = new AppiumServiceBuilder(). @@ -160,31 +163,31 @@ withCapabilities(serverCapabilities); AppiumDriverLocalService service = builder.build(); DesiredCapabilities clientCapabilities = new DesiredCapabilities(); -clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, +clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, "io.appium.android.apis"); -clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, +clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, ".view.WebView1"); ``` then ```java -AndroidDriver driver = +AndroidDriver driver = new AndroidDriver<>(service, clientCapabilities); ``` -or +or ```java -AndroidDriver driver = +AndroidDriver driver = new AndroidDriver<>(builder, clientCapabilities); ``` -or +or ```java service.start(); -AndroidDriver driver = +AndroidDriver driver = new AndroidDriver<>(service.getUrl(), clientCapabilities); ``` @@ -219,7 +222,7 @@ public AndroidDriver(org.openqa.selenium.remote.http.HttpClient.Factory httpClie org.openqa.selenium.Capabilities desiredCapabilities) public AndroidDriver(org.openqa.selenium.Capabilities desiredCapabilities) -``` +``` ```java public IOSDriver(URL remoteAddress, @@ -247,20 +250,20 @@ public IOSDriver(org.openqa.selenium.remote.http.HttpClient.Factory httpClientFa org.openqa.selenium.Capabilities desiredCapabilities) public IOSDriver(org.openqa.selenium.Capabilities desiredCapabilities) -``` +``` An instance of __AppiumDriverLocalService__ which has passed through constructors will be stopped when ```java driver.quit(); -``` +``` If it is necessary to keep the service alive during a long time then something like that is available ```java service.start(); - + .... new IOSDriver(service.getUrl(), capabilities) -``` +``` 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/v7-to-v8-migration-guide.md b/docs/v7-to-v8-migration-guide.md index b3be7def0..dcd578fc4 100644 --- a/docs/v7-to-v8-migration-guide.md +++ b/docs/v7-to-v8-migration-guide.md @@ -85,7 +85,7 @@ Please use [W3C Actions](https://w3c.github.io/webdriver/#actions) instead or the corresponding extension methods for the driver (if available). Check - https://www.youtube.com/watch?v=oAJ7jwMNFVU - - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + - https://www.headspin.io/blog/ios-specific-touch-action-methods - Android gesture shortcuts: * [mobile: longClickGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-longclickgesture) * [mobile: doubleClickGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-doubleclickgesture) @@ -107,7 +107,7 @@ Check * [mobile: dragFromToForDuration](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-dragfromtoforduration) * [mobile: dragFromToWithVelocity](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-dragfromtowithvelocity) * [mobile: scrollToElement](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-scrolltoelement) - - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + - https://www.headspin.io/blog/automating-complex-gestures-with-the-w3c-actions-api for more details on how to properly apply W3C Actions to your automation context. ## resetApp/launchApp/closeApp 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..d2927cceb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.daemon=true -selenium.version=4.19.0 +selenium.version=4.42.0 # Please increment the value in a release -appiumClient.version=9.3.0 +appiumClient.version=10.1.1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c3521197..b1b8ef56b 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..874732021 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ 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.6.1-all.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index f5feea6d6..249efbb03 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. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -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..8508ef684 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :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 %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/jitpack.yml b/jitpack.yml index e5b145a9d..294617da9 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,4 +1,4 @@ jdk: - - openjdk11 + - openjdk17 install: - ./gradlew clean build publishToMavenLocal -PsigningDisabled=true 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..b810784e1 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; @@ -176,7 +176,7 @@ public AppiumClientConfig authenticateAs(Credentials credentials) { /** * Whether enable directConnect feature described in - * + * * Connecting Directly to Appium Hosts in Distributed Environments. * * @param directConnect if enable the directConnect feature 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..44cf6ed17 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); @@ -166,63 +170,15 @@ public AppiumDriver(URL remoteSessionAddress, String platformName, String automa ); executor.setCommandCodec(new AppiumW3CHttpCommandCodec()); executor.setResponseCodec(new W3CHttpResponseCodec()); + executor.refreshAdditionalCommands(); 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 +202,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 +265,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/AppiumExecutionMethod.java b/src/main/java/io/appium/java_client/AppiumExecutionMethod.java index 34a848f79..a77056894 100644 --- a/src/main/java/io/appium/java_client/AppiumExecutionMethod.java +++ b/src/main/java/io/appium/java_client/AppiumExecutionMethod.java @@ -16,6 +16,8 @@ package io.appium.java_client; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.ExecuteMethod; import org.openqa.selenium.remote.Response; @@ -35,7 +37,8 @@ public AppiumExecutionMethod(AppiumDriver driver) { * @param parameters is a map which contains parameter names as keys and parameter values * @return a command execution result */ - public Object execute(String commandName, Map parameters) { + @Nullable + public Object execute(@NonNull String commandName, @Nullable Map parameters) { Response response; if (parameters == null || parameters.isEmpty()) { diff --git a/src/main/java/io/appium/java_client/AppiumFluentWait.java b/src/main/java/io/appium/java_client/AppiumFluentWait.java index 6361a5652..1496eea3c 100644 --- a/src/main/java/io/appium/java_client/AppiumFluentWait.java +++ b/src/main/java/io/appium/java_client/AppiumFluentWait.java @@ -17,9 +17,11 @@ 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.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.support.ui.FluentWait; @@ -33,7 +35,9 @@ import java.util.function.Function; import java.util.function.Supplier; +@NullMarked public class AppiumFluentWait extends FluentWait { + @Nullable private Function pollingStrategy = null; private static final Duration DEFAULT_POLL_DELAY_DURATION = Duration.ZERO; @@ -114,43 +118,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); + protected Supplier<@Nullable String> getMessageSupplier() { + return messageSupplier; } - @SuppressWarnings("unchecked") protected T getInput() { - return (T) getPrivateFieldValue("input"); + return (T) input; } /** @@ -215,7 +208,7 @@ public AppiumFluentWait withPollingStrategy(Function * @throws TimeoutException If the timeout expires. */ @Override - public V until(Function isTrue) { + public @NonNull V until(Function isTrue) { final var start = getClock().instant(); // Adding pollDelay to end instant will allow to verify the condition for the expected timeout duration. final var end = start.plus(getTimeout()).plus(pollDelay); @@ -223,7 +216,8 @@ public V until(Function isTrue) { return performIteration(isTrue, start, end); } - private V performIteration(Function isTrue, Instant start, Instant end) { + private @NonNull V performIteration( + Function isTrue, Instant start, Instant end) { var iterationNumber = 1; Throwable lastException; @@ -257,8 +251,9 @@ private V performIteration(Function isTrue, Instant start, Ins } } - private void handleTimeoutException(Throwable lastException, Function isTrue) { - var message = Optional.ofNullable(getMessageSupplier()) + private void handleTimeoutException( + @Nullable Throwable lastException, Function isTrue) { + var message = Optional.of(getMessageSupplier()) .map(Supplier::get) .orElseGet(() -> "waiting for " + isTrue); 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/MultiTouchAction.java b/src/main/java/io/appium/java_client/MultiTouchAction.java index d82b47b1f..5895588ba 100644 --- a/src/main/java/io/appium/java_client/MultiTouchAction.java +++ b/src/main/java/io/appium/java_client/MultiTouchAction.java @@ -45,8 +45,8 @@ * extension methods for the driver (if available). * Check * - https://www.youtube.com/watch?v=oAJ7jwMNFVU - * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods - * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * - https://www.headspin.io/blog/ios-specific-touch-action-methods + * - https://www.headspin.io/blog/automating-complex-gestures-with-the-w3c-actions-api * for more details. */ @Deprecated diff --git a/src/main/java/io/appium/java_client/PerformsTouchActions.java b/src/main/java/io/appium/java_client/PerformsTouchActions.java index 539a38a52..46cfc0b50 100644 --- a/src/main/java/io/appium/java_client/PerformsTouchActions.java +++ b/src/main/java/io/appium/java_client/PerformsTouchActions.java @@ -28,8 +28,8 @@ * extension methods for the driver (if available). * Check * - https://www.youtube.com/watch?v=oAJ7jwMNFVU - * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods - * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * - https://www.headspin.io/blog/ios-specific-touch-action-methods + * - https://www.headspin.io/blog/automating-complex-gestures-with-the-w3c-actions-api * for more details. */ @Deprecated diff --git a/src/main/java/io/appium/java_client/TouchAction.java b/src/main/java/io/appium/java_client/TouchAction.java index 6f6621f0b..21062ef95 100644 --- a/src/main/java/io/appium/java_client/TouchAction.java +++ b/src/main/java/io/appium/java_client/TouchAction.java @@ -47,8 +47,8 @@ * extension methods for the driver (if available). * Check * - https://www.youtube.com/watch?v=oAJ7jwMNFVU - * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods - * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * - https://www.headspin.io/blog/ios-specific-touch-action-methods + * - https://www.headspin.io/blog/automating-complex-gestures-with-the-w3c-actions-api * for more details. */ @Deprecated 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/AndroidTouchAction.java b/src/main/java/io/appium/java_client/android/AndroidTouchAction.java index 0fa31cffa..55f167604 100644 --- a/src/main/java/io/appium/java_client/android/AndroidTouchAction.java +++ b/src/main/java/io/appium/java_client/android/AndroidTouchAction.java @@ -28,8 +28,8 @@ * extension methods for the driver (if available). * Check * - https://www.youtube.com/watch?v=oAJ7jwMNFVU - * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods - * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * - https://www.headspin.io/blog/ios-specific-touch-action-methods + * - https://www.headspin.io/blog/automating-complex-gestures-with-the-w3c-actions-api * for more details. */ @Deprecated 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/DirectConnectUrlSafety.java b/src/main/java/io/appium/java_client/internal/DirectConnectUrlSafety.java new file mode 100644 index 000000000..af6d48de1 --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/DirectConnectUrlSafety.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.internal; + +import org.openqa.selenium.SessionNotCreatedException; + +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; + +/** + * Validates URLs used with {@code overrideServerUrl} (for example after a {@code directConnect} + * response). Refuses the override when any resolved address is loopback, link-local (including + * typical cloud metadata IPv4 link-local space), unspecified, or multicast. + * + *

This is not a full "public internet only" policy: RFC 1918 private space, shared + * address space ({@code 100.64.0.0/10}), IPv6 unique-local ({@code fc00::/7}), and other addresses + * outside the checks above are still accepted. Stricter control belongs at the application or + * network layer (allowlists, egress rules, etc.). + */ +public final class DirectConnectUrlSafety { + + private DirectConnectUrlSafety() { + } + + /** + * Ensures the given URL's host does not resolve to loopback, link-local, unspecified, or + * multicast addresses (see class documentation for what is still allowed). + * + * @param url candidate server URL + * @throws SessionNotCreatedException if the host is missing, cannot be resolved, or resolves + * to any address in the disallowed categories + */ + public static void requireSafeOverrideTarget(URL url) throws SessionNotCreatedException { + String host = url.getHost(); + if (host == null || host.isEmpty()) { + throw new SessionNotCreatedException( + "Refusing to override the server URL: the URL must include a non-empty host."); + } + + final InetAddress[] addresses; + try { + addresses = InetAddress.getAllByName(host); + } catch (UnknownHostException e) { + throw new SessionNotCreatedException( + "Refusing to override the server URL: cannot resolve host '" + host + "'.", e); + } + + for (InetAddress address : addresses) { + if (isDisallowed(address)) { + throw new SessionNotCreatedException(String.format( + "Refusing to override the server URL: host '%s' resolves to %s, which is not " + + "allowed (loopback, link-local, unspecified, or multicast address).", + host, + address.getHostAddress())); + } + } + } + + private static boolean isDisallowed(InetAddress address) { + return address.isLoopbackAddress() + || address.isLinkLocalAddress() + || address.isAnyLocalAddress() + || address.isMulticastAddress(); + } +} 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/IOSTouchAction.java b/src/main/java/io/appium/java_client/ios/IOSTouchAction.java index aca87955d..0414726d0 100644 --- a/src/main/java/io/appium/java_client/ios/IOSTouchAction.java +++ b/src/main/java/io/appium/java_client/ios/IOSTouchAction.java @@ -30,8 +30,8 @@ * extension methods for the driver (if available). * Check * - https://www.youtube.com/watch?v=oAJ7jwMNFVU - * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods - * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * - https://www.headspin.io/blog/ios-specific-touch-action-methods + * - https://www.headspin.io/blog/automating-complex-gestures-with-the-w3c-actions-api * for more details. */ @Deprecated 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..54b590201 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -18,7 +18,11 @@ import com.google.common.base.Throwables; import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.internal.DirectConnectUrlSafety; 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 +37,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 +54,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 +66,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,53 +120,54 @@ 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; } /** * Override the http client in the HttpCommandExecutor class with a new http client instance with the given URL. * It uses the same http client factory and client config for the new http client instance * if the constructor got them. - * @param serverUrl A url to override. + * + * @param serverUrl URL to use for subsequent HTTP requests. Before switching clients, the host is + * resolved and the override is refused if any resolved address is loopback, + * link-local (including IPv4 link-local such as metadata-service ranges), + * unspecified ({@code 0.0.0.0} / {@code ::}), or multicast. Private (RFC 1918), + * carrier-grade NAT ({@code 100.64.0.0/10}), IPv6 unique-local ({@code fc00::/7}), + * and other non-special addresses are not rejected by this check. */ 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))); + DirectConnectUrlSafety.requireSafeOverrideTarget(serverUrl); + HttpClient newClient = getHttpClientFactory().createClient(appiumClientConfig.baseUrl(serverUrl)); + setPrivateFieldValue(HttpCommandExecutor.class, "client", newClient); } private Response createSession(Command command) throws IOException { @@ -183,7 +185,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 +193,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..33983663e 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; @@ -117,6 +118,24 @@ public URL getUrl() { return basePath == null ? url : addSuffix(url, basePath); } + /** + * System property checked by Selenium {@code DriverFinder} when the executable path is not set + * explicitly. Matches {@link AppiumServiceBuilder#NODE_PATH}. + */ + @Override + public String getDriverProperty() { + return AppiumServiceBuilder.NODE_PATH; + } + + /** + * Environment variable checked by Selenium {@code DriverFinder} when the executable path is not + * set explicitly. Matches {@link AppiumServiceBuilder#NODE_PATH}. + */ + @Override + public String getDriverEnvironmentVariable() { + return AppiumServiceBuilder.NODE_PATH; + } + @Override public boolean isRunning() { lock.lock(); @@ -407,7 +426,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..b9337212a 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; @@ -64,11 +64,10 @@ public final class AppiumServiceBuilder public static final String APPIUM_PATH = "APPIUM_BINARY_PATH"; /** - * The environmental variable used to define - * the path to executable NodeJS file (node.exe for WIN and - * node for Linux/MacOS X). + * System property and environment variable name for the Node.js executable path + * (node.exe on Windows, node on Linux/macOS). */ - private static final String NODE_PATH = "NODE_BINARY_PATH"; + public static final String NODE_PATH = "NODE_BINARY_PATH"; public static final String BROADCAST_IP4_ADDRESS = "0.0.0.0"; public static final String BROADCAST_IP6_ADDRESS = "::"; @@ -144,7 +143,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 +399,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 +471,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/service/local/ListOutputStream.java b/src/main/java/io/appium/java_client/service/local/ListOutputStream.java index 7173963ad..f2dc66eb2 100644 --- a/src/main/java/io/appium/java_client/service/local/ListOutputStream.java +++ b/src/main/java/io/appium/java_client/service/local/ListOutputStream.java @@ -62,8 +62,20 @@ public void flush() throws IOException { @Override public void close() throws IOException { + IOException first = null; for (OutputStream stream : streams) { - stream.close(); + try { + stream.close(); + } catch (IOException e) { + if (first == null) { + first = e; + } else { + first.addSuppressed(e); + } + } + } + if (first != null) { + throw first; } } 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/internal/DirectConnectUrlSafetyTest.java b/src/test/java/io/appium/java_client/internal/DirectConnectUrlSafetyTest.java new file mode 100644 index 000000000..beda427f9 --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/DirectConnectUrlSafetyTest.java @@ -0,0 +1,37 @@ +package io.appium.java_client.internal; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.openqa.selenium.SessionNotCreatedException; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class DirectConnectUrlSafetyTest { + + @Test + void allowsDocumentationNetIpLiteral() throws MalformedURLException { + // RFC 5737 TEST-NET-3; parsed locally without DNS + assertDoesNotThrow(() -> DirectConnectUrlSafety.requireSafeOverrideTarget( + new URL("https://203.0.113.1/wd/hub"))); + } + + @ParameterizedTest(name = "[{index}] {0}") + @ValueSource(strings = { + "https://127.0.0.1:4443/wd/hub", + "https://localhost:4443/wd/hub", + "https://169.254.169.254/wd/hub", + "https://[::1]:4443/wd/hub", + "https://0.0.0.0:4443/wd/hub", + "https://[::]:4443/wd/hub", + "https://224.0.0.1:4443/wd/hub", + }) + void rejectsDisallowedOverrideTargets(String urlSpec) throws MalformedURLException { + assertThrows(SessionNotCreatedException.class, + () -> DirectConnectUrlSafety.requireSafeOverrideTarget(new URL(urlSpec))); + } +} 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..b11bd6273 --- /dev/null +++ b/src/test/java/io/appium/java_client/remote/AppiumCommandExecutorTest.java @@ -0,0 +1,49 @@ +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 org.openqa.selenium.SessionNotCreatedException; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +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() throws MalformedURLException { + assertDoesNotThrow(() -> createExecutor().overrideServerUrl(new URL("https://203.0.113.1/wd/hub"))); + } + + @Test + void overrideServerUrlRejectsLoopbackTarget() throws MalformedURLException { + assertThrows(SessionNotCreatedException.class, + () -> createExecutor().overrideServerUrl(new URL("https://127.0.0.1:4443/wd/hub"))); + } +} 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; + } }