diff --git a/.azure-templates/bootstrap_steps.yml b/.azure-templates/bootstrap_steps.yml deleted file mode 100644 index baf4b6979..000000000 --- a/.azure-templates/bootstrap_steps.yml +++ /dev/null @@ -1,10 +0,0 @@ -steps: - - task: NodeTool@0 - inputs: - versionSpec: "$(NODE_VERSION)" - - script: | - npm config delete prefix - npm config set prefix $NVM_DIR/versions/node/`node --version` - node --version - - npm install -g appium@next diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..49cf4f31e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @mykola-mokhnach @SrinivasanTarget @saikrishna321 @valfirst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..41b5c892d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,158 @@ +name: Appium Java Client CI + +on: + push: + branches: + - master + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + branches: + - master + paths-ignore: + - 'docs/**' + - '*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + 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" + 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: + + strategy: + matrix: + include: + - java: 17 + # Need to use specific (not `-latest`) version of macOS to be sure the required version of Xcode/simulator is available + platform: macos-14 + e2e-tests: ios + - 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 + - java: 17 + platform: ubuntu-latest + e2e-tests: android + - java: 17 + platform: ubuntu-latest + e2e-tests: flutter-android + - java: 21 + platform: ubuntu-latest + - java: 25 + platform: ubuntu-latest + fail-fast: false + + runs-on: ${{ matrix.platform }} + + name: JDK ${{ matrix.java }} - ${{ matrix.platform }} ${{ matrix.e2e-tests }} + steps: + - uses: actions/checkout@v6 + + - name: Enable KVM group perms + if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android' + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - 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: Build with Gradle against Selenium nightly build + 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" + ./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot + + - name: Build with Gradle against stable Selenium version + run: | + ./gradlew clean build -PisCI + + - name: Install Node.js + if: ${{ matrix.e2e-tests }} + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + + - name: Install Appium + if: ${{ matrix.e2e-tests }} + run: npm install --location=global appium + + - name: Install UIA2 driver + if: matrix.e2e-tests == 'android' || matrix.e2e-tests == 'flutter-android' + run: appium driver install uiautomator2 + + - name: Install Flutter Integration driver + if: matrix.e2e-tests == 'flutter-android' || matrix.e2e-tests == 'flutter-ios' + run: appium driver install appium-flutter-integration-driver --source npm + + - name: Run Android E2E tests + if: matrix.e2e-tests == 'android' + uses: reactivecircus/android-emulator-runner@v2 + with: + script: ./gradlew e2eAndroidTest -PisCI -Pselenium.version=$latest_snapshot + api-level: ${{ env.ANDROID_SDK_VERSION }} + avd-name: ${{ env.ANDROID_EMU_NAME }} + disable-spellchecker: true + disable-animations: true + target: ${{ env.ANDROID_EMU_TARGET }} + + - name: Run Flutter Android E2E tests + 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 }} + api-level: ${{ env.ANDROID_SDK_VERSION }} + avd-name: ${{ env.ANDROID_EMU_NAME }} + disable-spellchecker: true + disable-animations: true + target: ${{ env.ANDROID_EMU_TARGET }} + + - name: Select Xcode + if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios' + 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 + 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: Download prebuilt WDA + if: matrix.e2e-tests == 'ios' || matrix.e2e-tests == 'flutter-ios' + 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 + + - 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 }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml deleted file mode 100644 index ba0201318..000000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Validate Gradle Wrapper" - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index e5981b475..000000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Appium Java Client CI - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - - runs-on: macOS-latest - - strategy: - matrix: - java: [ 8, 11, 17 ] - - name: JDK ${{ matrix.java }} - steps: - - uses: actions/checkout@v3 - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: ${{ matrix.java }} - cache: 'gradle' - - name: Build with Gradle - run: ./gradlew clean build -x test -x checkstyleTest diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 000000000..1658a2957 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,16 @@ +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 }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..72edaf3bd --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,28 @@ +name: Publish package to the Maven Central Repository +on: + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Java + uses: actions/setup-java@v5 + with: + java-version: '11' + distribution: 'zulu' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + + - name: Publish package + env: + 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 new file mode 100644 index 000000000..1f189e2cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1144 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +_10.0.0_ +- **[DOCUMENTATION]** + - Document the migration guide from v9 to v10 [#2331](https://github.com/appium/java-client/pull/2331) + - updated maven central release badge [#2316](https://github.com/appium/java-client/pull/2316) + - updated CI badge to use ci.yml workflow [#2317](https://github.com/appium/java-client/pull/2317) +- **[BREAKING CHANGE]** [#2327](https://github.com/appium/java-client/pull/2327) + - Removed all deprecated methods with Selenium's Location and LocationContext (these classes have been removed in Selenium 4.35.0) +- **[ENHANCEMENTS]** + - Proxy commands issues via RemoteWebElement [#2311](https://github.com/appium/java-client/pull/2311) + - Automated Release to Maven Central Repository using JReleaser [#2313](https://github.com/appium/java-client/pull/2313) +- **[BUG FIX]** + - Possible NPE in initBiDi() [#2325](https://github.com/appium/java-client/pull/2325) +- **[DEPENDENCY CHANGE]** + - Bump minimum Selenium version to 4.35.0 [#2327](https://github.com/appium/java-client/pull/2327) + - Bump org.junit.jupiter:junit-jupiter from 5.13.2 to 5.13.3 [#2314](https://github.com/appium/java-client/pull/2314) + - Bump io.github.bonigarcia:webdrivermanager [#2322](https://github.com/appium/java-client/pull/2322) + - Bump com.gradleup.shadow from 8.3.7 to 8.3.8 [#2315](https://github.com/appium/java-client/pull/2315) + - Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0 [#2320](https://github.com/appium/java-client/pull/2320) + +_9.5.0_ +- **[ENHANCEMENTS]** + - Allow extension capability keys to contain dot characters [#2271](https://github.com/appium/java-client/pull/2271) + - Add a client for Appium server storage plugin [#2275](https://github.com/appium/java-client/pull/2275) + - Swap check for `Widget` and `WebElement` [#2277](https://github.com/appium/java-client/pull/2277) + - Add compatibility with Selenium `4.34.0` [#2298](https://github.com/appium/java-client/pull/2298) + - Add new option classes for `prebuiltWDAPath` and `usePreinstalledWDA` XCUITest capabilities [#2304](https://github.com/appium/java-client/pull/2304) +- **[REFACTOR]** + - Migrate from JSR 305 to [JSpecify](https://jspecify.dev/)'s nullability annotations [#2281](https://github.com/appium/java-client/pull/2281) +- **[DEPENDENCY UPDATES]** + - Bump minimum supported Selenium version from `4.26.0` to `4.34.0` [#2305](https://github.com/appium/java-client/pull/2305) + - Bump Gson from `2.11.0` to `2.13.1` [#2267](https://github.com/appium/java-client/pull/2267), [#2286](https://github.com/appium/java-client/pull/2286), [#2290](https://github.com/appium/java-client/pull/2290) + - Bump SLF4J from `2.0.16` to `2.0.17` [#2274](https://github.com/appium/java-client/pull/2274) + +_9.4.0_ +- **[ENHANCEMENTS]** + - Implement `HasBiDi` interface support in `AppiumDriver` [#2250](https://github.com/appium/java-client/pull/2250), [#2254](https://github.com/appium/java-client/pull/2254), [#2256](https://github.com/appium/java-client/pull/2256) + - Add compatibility with Selenium `4.28.0` [#2249](https://github.com/appium/java-client/pull/2249) +- **[BUG FIX]** + - Fix scroll issue in flutter integration driver [#2227](https://github.com/appium/java-client/pull/2227) + - Fix the definition of `logcatFilterSpecs` option [#2258](https://github.com/appium/java-client/pull/2258) + - Use `WeakHashMap` for caching proxy classes [#2260](https://github.com/appium/java-client/pull/2260) +- **[DEPENDENCY UPDATES]** + - Bump minimum supported Selenium version from `4.19.0` to `4.26.0` [#2246](https://github.com/appium/java-client/pull/2246) + - Bump Apache Commons Lang from `3.15.0` to `3.16.1` [#2220](https://github.com/appium/java-client/pull/2220), [#2228](https://github.com/appium/java-client/pull/2228) + - Bump SLF4J from `2.0.13` to `2.0.16` [#2221](https://github.com/appium/java-client/pull/2221) + +_9.3.0_ +- **[ENHANCEMENTS]** + - Add support for FlutterIOSDriver. [#2206](https://github.com/appium/java-client/pull/2206) + - add support for FlutterAndroidDriver. [#2203](https://github.com/appium/java-client/pull/2203) + - Add locator types supported by flutter integration driver. [#2201](https://github.com/appium/java-client/pull/2201) + - add flutter driver commands to support camera mocking. [#2207](https://github.com/appium/java-client/pull/2207) + - Add ability to use secure WebSocket to listen Logcat messages. [#2182](https://github.com/appium/java-client/pull/2182) + - Add mobile: replacements to clipboard API wrappers. [#2188](https://github.com/appium/java-client/pull/2188) +- **[DEPRECATION]** + - Deprecate obsolete TouchAction helpers. [#2199](https://github.com/appium/java-client/pull/2199) +- **[REFACTOR]** + - Bump iOS version in CI. [#2167](https://github.com/appium/java-client/pull/2167) +- **[DOCUMENTATION]** + - README updates. [#2193](https://github.com/appium/java-client/pull/2193) +- **[DEPENDENCY UPDATES]** + - `org.junit.jupiter:junit-jupiter` was updated to 5.10.3. + - `org.projectlombok:lombok` was updated to 1.18.34. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.9.1. + - `org.owasp.dependencycheck` was updated to 10.0.3. + - `org.apache.commons:commons-lang3` was updated to 3.15.0. + +_9.2.3_ +- **[BUG FIX]** + - Properly represent `FeaturesMatchingResult` model if `multiple` option is enabled [#2170](https://github.com/appium/java-client/pull/2170) + - Use current class loader for the ByteBuddy wrapper [#2172](https://github.com/appium/java-client/pull/2172) \ + This fixes errors like `NoClassDefFoundError: org/openqa/selenium/remote/RemoteWebElement`, `NoClassDefFoundError: io/appium/java_client/proxy/HasMethodCallListeners` when `PageFactory` is used. + - Correct extension name for `mobile: replaceElementValue` [#2171](https://github.com/appium/java-client/pull/2171) +- **[DEPRECATION]** + - Deprecate `AppiumProtocolHandshake` class [#2173](https://github.com/appium/java-client/pull/2173) \ + The original `ProtocolHandshake` class only supports W3C protocol now. There is no need to hack it anymore. +- **[REFACTOR]** + - Replace Guava `HttpHeaders` with Selenium `HttpHeader` [#2151](https://github.com/appium/java-client/pull/2151) +- **[DEPENDENCY CHANGE]** + - Bump SLF4J from `2.0.12` to `2.0.13` [#2158](https://github.com/appium/java-client/pull/2158) + - Bump Gson from `2.10.1` to `2.11.0` [#2175](https://github.com/appium/java-client/pull/2175) + +_9.2.2_ +- **[BUG FIX]** + - fix: Fix building of Android key event parameters [#2145](https://github.com/appium/java-client/pull/2145) + - fix: Fix building of Android geo location parameters [#2146](https://github.com/appium/java-client/pull/2146) + +_9.2.1_ +- **[REFACTOR]** + - Replace private usages of Guava Collections API with Java Collections API [#2136](https://github.com/appium/java-client/pull/2136) + - Remove usages of Guava's `@VisibleForTesting` annotation [#2138](https://github.com/appium/java-client/pull/2138). Previously opened internal API marked with `@VisibleForTesting` annotation is private now: + - `io.appium.java_client.internal.filters.AppiumUserAgentFilter#containsAppiumName` + - `io.appium.java_client.service.local.AppiumDriverLocalService#parseSlf4jContextFromLogMessage` +- **[DEPENDENCY CHANGE]** + - Bump minimum supported Selenium version from `4.17.0` to `4.19.0` [#2142](https://github.com/appium/java-client/pull/2142) + +_9.2.0_ +- **[ENHANCEMENTS]** + - Incorporate poll delay mechanism into `AppiumFluentWait` [#2116](https://github.com/appium/java-client/pull/2116) (Closes [#2111](https://github.com/appium/java-client/pull/2111)) + - Make server startup error messages more useful [#2130](https://github.com/appium/java-client/pull/2130) +- **[BUG FIX]** + - Set correct geolocation coordinates of the current device [#2109](https://github.com/appium/java-client/pull/2109) (Fixes [#2108](https://github.com/appium/java-client/pull/2108)) + - Always release annotated element reference from the builder instance [#2128](https://github.com/appium/java-client/pull/2128) + - Cache dynamic proxy classes created by ByteBuddy [#2129](https://github.com/appium/java-client/pull/2129) (Fixes [#2119](https://github.com/appium/java-client/pull/2119)) +- **[DEPENDENCY CHANGE]** + - Bump SLF4J from `2.0.11` to `2.0.12` [#2115](https://github.com/appium/java-client/pull/2115) +- **[DOCUMENTATION]** + - Improve release steps [#2107](https://github.com/appium/java-client/pull/2107) + +_9.1.0_ +- **[ENHANCEMENTS]** + - Introduce better constructor argument validation for the `AppiumFieldDecorator` class. [#2070](https://github.com/appium/java-client/pull/2070) + - Add `toString` to `AppiumClientConfig`. [#2076](https://github.com/appium/java-client/pull/2076) + - Perform listeners cleanup periodically. [#2077](https://github.com/appium/java-client/pull/2077) + - Add non-W3C context, orientation and rotation management endpoints removed from Selenium client. [#2093](https://github.com/appium/java-client/pull/2093) + - Add non-W3C Location-management endpoints deprecated in Selenium client. [#2098](https://github.com/appium/java-client/pull/2098) +- **[BUG FIX]** + - Properly unwrap driver instance if the `ContextAware` object is deeply nested. [#2052](https://github.com/appium/java-client/pull/2052) + - Update hashing and iteration logic of page object items. [#2067](https://github.com/appium/java-client/pull/2067) + - Assign method call listeners directly to the proxy instance. [#2102](https://github.com/appium/java-client/pull/2102) + - Use JDK 11 to build Jitpack snapshots. [#2083](https://github.com/appium/java-client/pull/2083) +- **[DEPRECATION]** + - Deprecate custom functional interfaces. [#2055](https://github.com/appium/java-client/pull/2055) +- **[REFACTOR]** + - Use Java 9+ APIs instead of outdated/3rd-party APIs. [#2048](https://github.com/appium/java-client/pull/2048) + - Migrate to new Selenium API for process management. [#2054](https://github.com/appium/java-client/pull/2054) +- **[DEPENDENCY CHANGE]** + - Bump minimum supported Selenium version from `4.14.1` to `4.17.0`. + - Bump SLF4J from `2.0.9` to `2.0.11`. [#2091](https://github.com/appium/java-client/pull/2091), [#2099](https://github.com/appium/java-client/pull/2099) +- **[DOCUMENTATION]** + - Describe the release procedure. [#2104](https://github.com/appium/java-client/pull/2104) + +_9.0.0_ +- **[DOCUMENTATION]** + - Add 8 to 9 migration guide. [#2039](https://github.com/appium/java-client/pull/2039) +- **[BREAKING CHANGE]** [#2036](https://github.com/appium/java-client/pull/2036) + - Set minimum Java version to 11. + - The previously deprecated MobileBy class has been removed. Use AppiumBy instead. + - The previously deprecated launchApp, resetApp and closeApp methods have been removed. Use extension methods instead. + - The previously deprecated WindowsBy class and related location strategies have been removed. + - The previously deprecated ByAll class has been removed in favour of the Selenium one. + - The previously deprecated AndroidMobileCapabilityType interface has been removed. Use driver options instead + - The previously deprecated IOSMobileCapabilityType interface has been removed. Use driver options instead + - The previously deprecated MobileCapabilityType interface has been removed. Use driver options instead + - The previously deprecated MobileOptions class has been removed. Use driver options instead + - The previously deprecated YouiEngineCapabilityType interface has been removed. Use driver options instead + - Removed several misspelled methods. Use properly spelled alternatives instead + - Removed startActivity method from AndroidDriver. Use 'mobile: startActivity' extension method instead + - Removed the previously deprecated APPIUM constant from the AutomationName interface + - Removed the previously deprecated PRE_LAUNCH value from the GeneralServerFlag enum + - Moved AppiumUserAgentFilter class to io.appium.java_client.internal.filters package +- **[REFACTOR]** + - Align Selenium version in test dependencies. [#2042](https://github.com/appium/java-client/pull/2042) +- **[DEPENDENCY CHANGE]** + - Removed dependencies to Apache Commons libraries. + +_8.6.0_ + +- **[BUG FIX]** + - Exclude abstract methods from proxy matching. [#1937](https://github.com/appium/java-client/pull/1937) + - AppiumClientConfig#readTimeout to call super.readTimeout. [#1959](https://github.com/appium/java-client/pull/1959) + - Use weak references to elements inside of interceptor objects. [#1981](https://github.com/appium/java-client/pull/1981) + - Correct spelling and semantic mistakes in method naming. [#1970](https://github.com/appium/java-client/pull/1970) + - Change scope of selenium-support dependency to compile. [#2019](https://github.com/appium/java-client/pull/2019) + - Fix Code style issues to match Java standards. [#2017](https://github.com/appium/java-client/pull/2017) + - class of proxy method in AppiumClientConfig. [#2026](https://github.com/appium/java-client/pull/2026) +- **[ENHANCEMENTS]** + - Mark Windows page object annotations as deprecated. [#1938](https://github.com/appium/java-client/pull/1938) + - Deprecate obsolete capabilities constants. [#1961](https://github.com/appium/java-client/pull/1961) + - patch AutomationName with Chromium. [#1993](https://github.com/appium/java-client/pull/1993) + - Implementation of Chromium driver plus capabilities. [#2003](https://github.com/appium/java-client/pull/2003) +- **[REFACTOR]** + - Increase server start timeout for iOS tests. [#1983](https://github.com/appium/java-client/pull/1983) + - Fix Android test: --base-path arg must start with /. [#1952](https://github.com/appium/java-client/pull/1952) + - Added fixes for No service provider found for `io.appium.java_client.events.api.Listener`. [#1975](https://github.com/appium/java-client/pull/1975) + - Run tests against latest Selenium release. [#1978](https://github.com/appium/java-client/pull/1978) + - Use server releases from the main branch for testing. [#1994](https://github.com/appium/java-client/pull/1994) + - Remove obsolete API calls from tests. [#2006](https://github.com/appium/java-client/pull/2006) + - Automate more static code checks. [#2028](https://github.com/appium/java-client/pull/2028) + - Limit the maximum selenium version to 4.14. [#2031](https://github.com/appium/java-client/pull/2031) + - Remove the obsolete commons-validator dependency. [#2032](https://github.com/appium/java-client/pull/2032) +- **[DOCUMENTATION]** + - Add the latest versions of clients to the compatibility matrix. [#1935](https://github.com/appium/java-client/pull/1935) + - Added correct url path for the latest appium documentation. [#1974](https://github.com/appium/java-client/pull/1974) + - Add Selenium 4.11.0, 4.12.0, 4.12.1 & 4.13.0 to compatibility matrix. [#1986](https://github.com/appium/java-client/pull/1986) & [#1999](https://github.com/appium/java-client/pull/1999) & [#2002](https://github.com/appium/java-client/pull/2025) & [#1986](https://github.com/appium/java-client/pull/2025) + - Add known compatibility issue for Selenium 4.12.1. [#2008](https://github.com/appium/java-client/pull/2008) +- **[DEPENDENCY UPDATES]** + - `org.owasp.dependencycheck` was updated to 8.4.0. + - `org.junit.jupiter:junit-jupiter` was updated to 5.10.0. + - `commons-io:commons-io` was updated to 2.14.0. + - `checkstyle` was updated to 10.12.1. + - `org.apache.commons:commons-lang3` was updated to 3.13.0. + - `gradle` was updated to 8.4.0. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.5.3. + - `org.seleniumhq.selenium:selenium-bom` was updated to 4.13.0. + - `org.projectlombok:lombok` was updated to 1.18.30. + +*8.5.1* +- **[BUG FIX]** + - Use correct exception type for fallback at file/folder pulling. [#1912](https://github.com/appium/java-client/pull/1912) + - Update autoWebview capability name. [#1917](https://github.com/appium/java-client/pull/1917) +- **[REFACTOR]** + - Move execution of E2E tests to GitHub Actions. [#1913](https://github.com/appium/java-client/pull/1913) + - Replace cglib with bytebuddy. [#1923](https://github.com/appium/java-client/pull/1923) + - Improve the error message on service startup. [#1928](https://github.com/appium/java-client/pull/1928) +- **[DOCUMENTATION]** + - Initiate Selenium client compatibility matrix. [#1918](https://github.com/appium/java-client/pull/1918) +- **[DEPENDENCY UPDATES]** + - `io.github.bonigarcia:webdrivermanager` was updated to 5.3.3. + - `org.projectlombok:lombok` was updated to 1.18.28. + - `commons-io:commons-io` was updated to 2.12.0. + +*8.5.0* +- **[BUG FIX]** + - Restore Jitpack builds. [#1911](https://github.com/appium/java-client/pull/1911) + - Add fallback commands for file management APIs. [#1910](https://github.com/appium/java-client/pull/1910) +- **[REFACTOR]** + - Replace performance data APIs with mobile extensions. [#1905](https://github.com/appium/java-client/pull/1905) +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 4.9.1. + - `org.junit.jupiter:junit-jupiter` was updated to 5.9.3. + +*8.4.0* +- **[ENHANCEMENTS]** + - Added possibility to connect to a running session. [#1813](https://github.com/appium/java-client/pull/1813) + - deprecate tapWithShortPressDuration capability.[#1825](https://github.com/appium/java-client/pull/1825) + - Add SupportsEnforceAppInstallOption to XCUITestOptions.[#1895](https://github.com/appium/java-client/pull/1895) +- **[BUG FIX]** + - Use ipv4 address instead of localhost. [#1815](https://github.com/appium/java-client/pull/1815) + - Fix test broken by updates in `appium-xcuitest-driver`. [#1839](https://github.com/appium/java-client/pull/1839) + - Merge misc tests suite into unit tests suite. [#1850](https://github.com/appium/java-client/pull/1850) + - Avoid NPE in destroyProcess call. [#1878](https://github.com/appium/java-client/pull/1878) + - Send arguments for mobile methods depending on the target platform. [#1897](https://github.com/appium/java-client/pull/1897) +- **[REFACTOR]** + - Run Gradle wrapper validation only on Gradle files changes. [#1828](https://github.com/appium/java-client/pull/1828) + - Skip GH Actions build on changes in docs. [#1829](https://github.com/appium/java-client/pull/1829) + - Remove Checkstyle exclusion of removed Selenium package. [#1831](https://github.com/appium/java-client/pull/1831) + - Enable Checkstyle checks for test code. [#1843](https://github.com/appium/java-client/pull/1843) + - Configure `CODEOWNERS` to automate review requests. [#1846](https://github.com/appium/java-client/pull/1846) + - Enable execution of unit tests in CI. [#1845](https://github.com/appium/java-client/pull/1845) + - Add Simple SLF4J binding to unit tests runtime. [#1848](https://github.com/appium/java-client/pull/1848) + - Improve performance of proxy `Interceptor` logging. [#1849](https://github.com/appium/java-client/pull/1849) + - Make unit tests execution a part of Gradle build lifecycle. [#1853](https://github.com/appium/java-client/pull/1853) + - Replace non-W3C API calls with corresponding extension calls in app management. [#1883](https://github.com/appium/java-client/pull/1883) + - Switch the time getter to use mobile extensions. [#1884](https://github.com/appium/java-client/pull/1884) + - Switch file management APIs to use mobile: extensions. [#1886](https://github.com/appium/java-client/pull/1886) + - Use mobile extensions for app strings getters and keyboard commands. [#1890](https://github.com/appium/java-client/pull/1890) + - Finish replacing iOS extensions with their mobile alternatives. [#1892](https://github.com/appium/java-client/pull/1892) + - Change some Android APIs to use mobile extensions. [#1893](https://github.com/appium/java-client/pull/1893) + - Change backgroundApp command to use the corresponding mobile extension. [#1896](https://github.com/appium/java-client/pull/1896) + - Switch more Android helpers to use extensions. [#1898](https://github.com/appium/java-client/pull/1898) + - Perform xcuitest driver prebuild. [#1900](https://github.com/appium/java-client/pull/1900) + - Finish migrating Android helpers to mobile extensions. [#1901](https://github.com/appium/java-client/pull/1901) + - Avoid sending unnecessary requests if corresponding extensions are absent. [#1903](https://github.com/appium/java-client/pull/1903) +- **[DOCUMENTATION]** + - Describe transitive Selenium dependencies management. [#1827](https://github.com/appium/java-client/pull/1827) + - Fix build badge to point GH Actions CI. [#1844](https://github.com/appium/java-client/pull/1844) +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 4.8.2. + - `org.slf4j:slf4j-api` was updated to 2.0.7. + - `org.owasp.dependencycheck` was updated to 8.2.1. + - `gradle` was updated to 8.1.0. + - `com.google.code.gson:gson` was updated to 2.10.1. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.3.2. + - `org.junit.jupiter:junit-jupiter` was updated to 5.9.2. + - `checkstyle` was updated to 10.0. + - `jacoco` was updated to 0.8.8. + - `org.projectlombok:lombok` was updated to 1.18.26. + - `com.github.johnrengelman.shadow` was updated to 8.1.1. + +*8.3.0* +- **[DOCUMENTATION]** + - Added troubleshooting section. [#1808](https://github.com/appium/java-client/pull/1808) + - Added CHANGELOG.md. [#1810](https://github.com/appium/java-client/pull/1810) +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 4.7.0. + - `org.slf4j:slf4j-api` was updated to 2.0.5. + +*8.2.1* +- **[ENHANCEMENTS]** + - BYACCESSABILITY is deprecated in favor of BYACCESSIBILITY. [#1752](https://github.com/appium/java-client/pull/1752) + - Connect directly to Appium Hosts in Distributed Environments. [#1747](https://github.com/appium/java-client/pull/1747) + - use own User Agent. [#1779](https://github.com/appium/java-client/pull/1779) + - Add alternative proxy implementation. [#1790](https://github.com/appium/java-client/pull/1790) + - Automated artefact publish to maven central. [#1803](https://github.com/appium/java-client/pull/1803) & [#1807](https://github.com/appium/java-client/pull/1807) +- **[BUG FIX]** + - Enforce usage of Base64 compliant with RFC 4648 for all operations. [#1785](https://github.com/appium/java-client/pull/1785) + - Override getScreenshotAs to support the legacy base64 encoding. [#1787](https://github.com/appium/java-client/pull/1787) +- **[REFACTOR]** + - BYACCESSABILITY is deprecated in favor of BYACCESSIBILITY. [#1752](https://github.com/appium/java-client/pull/1752) + - JUnit5 test classes and methods are updated to have default package visibility. [#1755](https://github.com/appium/java-client/pull/1755) + - Verify if the PR title complies with conventional commits spec. [#1757](https://github.com/appium/java-client/pull/1757) + - Use Lombok in direct connect class. [#1789](https://github.com/appium/java-client/pull/1789) + - Update readme and remove obsolete documents. [#1792](https://github.com/appium/java-client/pull/1792) + - Remove unnecessary annotation. [#1791](https://github.com/appium/java-client/pull/1791) + - Force unified imports order. [#1793](https://github.com/appium/java-client/pull/1793) +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 4.5.0. + - `org.owasp.dependencycheck` was updated to 7.3.2. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.3.1. + - `org.junit.jupiter:junit-jupiter` was updated to 5.9.1. + - `org.slf4j:slf4j-api` was updated to 2.0.4. + - `com.google.code.gson:gson` was updated to 2.10.0. + +*8.2.0* +- **[ENHANCEMENTS]** + - AppiumDriverLocalService can handle outputStreams. [#1709](https://github.com/appium/java-client/pull/1709) + - Add creating a driver with ClientConfig. [#1735](https://github.com/appium/java-client/pull/1735) +- **[BUG FIX]** + - Update the environment argument type for mac SupportsEnvironmentOption. [#1712](https://github.com/appium/java-client/pull/1712) +- **[REFACTOR]** + - Deprecate Appium ByAll in favour of Selenium ByAll. [#1740](https://github.com/appium/java-client/pull/1740) + - Bump Node.js version in pipeline. [#1713](https://github.com/appium/java-client/pull/1713) + - Switch unit tests to run on Junit 5 Jupiter Platform. [#1721](https://github.com/appium/java-client/pull/1721) + - Clean up unit tests asserting thrown exceptions. [#1741](https://github.com/appium/java-client/pull/1741) + - Fix open notification test. [#1749](https://github.com/appium/java-client/pull/1749) + - update Azure pipeline to use macos-11 VM image. [#1728](https://github.com/appium/java-client/pull/1728) +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 4.4.0. + - `org.owasp.dependencycheck` was updated to 7.1.2. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.3.0. + - `gradle` was updated to 7.5.1. + - `com.google.code.gson:gson` was updated to 2.9.1. + +*8.1.1* +- **[BUG FIX]** + - Perform safe typecast while getting the platform name. [#1702](https://github.com/appium/java-client/pull/1702) + - Add prefix to platformVersion capability name. [#1704](https://github.com/appium/java-client/pull/1704) +- **[REFACTOR]** + - Update e2e tests to make it green. [#1706](https://github.com/appium/java-client/pull/1706) + - Ignore the test which has a connected server issue. [#1699](https://github.com/appium/java-client/pull/1699) + +*8.1.0* +- **[ENHANCEMENTS]** + - Add new EspressoBuildConfig options. [#1687](https://github.com/appium/java-client/pull/1687) +- **[DOCUMENTATION]** + - delete all references to removed MobileElement class. [#1677](https://github.com/appium/java-client/pull/1677) +- **[BUG FIX]** + - Pass orientation name capability in uppercase. [#1686](https://github.com/appium/java-client/pull/1686) + - correction for ping method to get proper status URL. [#1661](https://github.com/appium/java-client/pull/1661) + - Remove deprecated option classes. [#1679](https://github.com/appium/java-client/pull/1679) + - Remove obsolete event firing decorators. [#1676](https://github.com/appium/java-client/pull/1676) +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 4.2.0. + - `org.owasp.dependencycheck` was updated to 7.1.0.1. + - `org.springframework:spring-context` was removed. [#1676](https://github.com/appium/java-client/pull/1676) + - `org.aspectj:aspectjweaver` was updated to 1.9.9. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.2.0. + - `org.projectlombok:lombok` was updated to 1.18.24. + +*8.0.0* +- **[DOCUMENTATION]** + - Set minimum Java version to 1.8.0. [#1631](https://github.com/appium/java-client/pull/1631) +- **[BUG FIX]** + - Make interfaces public to fix decorator creation. [#1644](https://github.com/appium/java-client/pull/1644) + - Do not convert argument names to lowercase. [#1627](https://github.com/appium/java-client/pull/1627) + - Avoid fallback to css for id and name locator annotations. [#1622](https://github.com/appium/java-client/pull/1622) + - Fix handling of chinese characters in `AppiumDriverLocalService`. [#1618](https://github.com/appium/java-client/pull/1618) +- **[DEPENDENCY UPDATES]** + - `org.owasp.dependencycheck` was updated to 7.0.0. + - `org.springframework:spring-context` was updated to 5.3.16. + - `actions/setup-java` was updated to 3. + - `actions/checkout` was updated to 3. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.1.0. + - `org.aspectj:aspectjweaver` was updated to 1.9.8. + - `org.slf4j:slf4j-api` was updated to 1.7.36. + - `com.github.johnrengelman.shadow` was updated to 7.1.2. + +*8.0.0-beta2* +- **[DOCUMENTATION]** + - Add a link to options builder examples to the migration guide. [#1595](https://github.com/appium/java-client/pull/1595) +- **[BUG FIX]** + - Filter out proxyClassLookup method from Proxy class (for Java 16+) in AppiumByBuilder. [#1575](https://github.com/appium/java-client/pull/1575) +- **[REFACTOR]** + - Add more nice functional stuff into page factory helpers. [#1584](https://github.com/appium/java-client/pull/1584) + - Switch e2e tests to use Appium2. [#1603](https://github.com/appium/java-client/pull/1603) + - relax constraints of Selenium dependencies versions. [#1606](https://github.com/appium/java-client/pull/1606) +- **[DEPENDENCY UPDATES]** + - Upgrade to Selenium 4.1.1. [#1613](https://github.com/appium/java-client/pull/1613) + - `org.owasp.dependencycheck` was updated to 6.5.1. + - `org.springframework:spring-context` was updated to 5.3.14. + - `actions/setup-java` was updated to 2.4.0. + - `gradle` was updated to 7.3. + +*8.0.0-beta* +- **[ENHANCEMENTS]** + - Start adding UiAutomator2 options. [#1543](https://github.com/appium/java-client/pull/1543) + - Add more UiAutomator2 options. [#1545](https://github.com/appium/java-client/pull/1545) + - Finish creating options for UiAutomator2 driver. [#1548](https://github.com/appium/java-client/pull/1548) + - Add WDA-related XCUITestOptions. [#1552](https://github.com/appium/java-client/pull/1552) + - Add web view options for XCUITest driver. [#1557](https://github.com/appium/java-client/pull/1557) + - Add the rest of XCUITest driver options. [#1561](https://github.com/appium/java-client/pull/1561) + - Add Espresso options. [#1563](https://github.com/appium/java-client/pull/1563) + - Add Windows driver options. [#1564](https://github.com/appium/java-client/pull/1564) + - Add Mac2 driver options. [#1565](https://github.com/appium/java-client/pull/1565) + - Add Gecko driver options. [#1573](https://github.com/appium/java-client/pull/1573) + - Add Safari driver options. [#1576](https://github.com/appium/java-client/pull/1576) + - Start adding XCUITest driver options. [#1551](https://github.com/appium/java-client/pull/1551) + - Implement driver-specific W3C option classes. [#1540](https://github.com/appium/java-client/pull/1540) + - Update Service to properly work with options. [#1550](https://github.com/appium/java-client/pull/1550) +- **[BREAKING CHANGE]** + - Migrate to Selenium 4. [#1531](https://github.com/appium/java-client/pull/1531) + - Make sure we only write W3C payload into create session command. [#1537](https://github.com/appium/java-client/pull/1537) + - Use the new session payload creator inherited from Selenium. [#1535](https://github.com/appium/java-client/pull/1535) + - unify locator factories naming and toString implementations. [#1538](https://github.com/appium/java-client/pull/1538) + - drop support of deprecated Selendroid driver. [#1553](https://github.com/appium/java-client/pull/1553) + - switch to javac compiler. [#1556](https://github.com/appium/java-client/pull/1556) + - revise used Selenium dependencies. [#1560](https://github.com/appium/java-client/pull/1560) + - change prefix to AppiumBy in locator toString implementation. [#1559](https://github.com/appium/java-client/pull/1559) + - enable dependencies caching. [#1567](https://github.com/appium/java-client/pull/1567) + - Include more tests into the pipeline. [#1566](https://github.com/appium/java-client/pull/1566) + - Tune setting of default platform names. [#1570](https://github.com/appium/java-client/pull/1570) + - Deprecate custom event listener implementation and default to the one provided by Selenium4. [#1541](https://github.com/appium/java-client/pull/1541) + - Deprecate touch actions. [#1569](https://github.com/appium/java-client/pull/1569) + - Deprecate legacy app management helpers. [#1571](https://github.com/appium/java-client/pull/1571) + - deprecate Windows UIAutomation selector. [#1562](https://github.com/appium/java-client/pull/1562) + - Remove unused entities. [#1572](https://github.com/appium/java-client/pull/1572) + - Remove setElementValue helper. [#1577](https://github.com/appium/java-client/pull/1577) + - Remove selenium package override. [#1555](https://github.com/appium/java-client/pull/1555) + - remove redundant exclusion of Gradle task signMavenJavaPublication. [#1568](https://github.com/appium/java-client/pull/1568) +- **[DEPENDENCY UPDATES]** + - `org.owasp.dependencycheck` was updated to 6.4.1. + - `com.google.code.gson:gson` was updated to 2.8.9. + +*7.6.0* +- **[ENHANCEMENTS]** + - Add custom commands dynamically [Appium 2.0]. [#1506](https://github.com/appium/java-client/pull/1506) + - New General Server flags are added [Appium 2.0]. [#1511](https://github.com/appium/java-client/pull/1511) + - Add support of extended Android geolocation. [#1492](https://github.com/appium/java-client/pull/1492) +- **[BUG FIX]** + - AndroidGeoLocation: update the constructor signature to mimic order of parameters in `org.openqa.selenium.html5.Location`. [#1526](https://github.com/appium/java-client/pull/1526) + - Prevent duplicate builds for PRs from base repo branches. [#1496](https://github.com/appium/java-client/pull/1496) + - Enable Dependabot for GitHub actions. [#1500](https://github.com/appium/java-client/pull/1500) + - bind mac2element in element map for mac platform. [#1474](https://github.com/appium/java-client/pull/1474) +- **[DEPENDENCY UPDATES]** + - `org.owasp.dependencycheck` was updated to 6.3.2. + - `org.projectlombok:lombok` was updated to 1.18.22. + - `com.github.johnrengelman.shadow` was updated to 7.1.0. + - `actions/setup-java` was updated to 2.3.1. + - `io.github.bonigarcia:webdrivermanager` was updated to 5.0.3. + - `org.springframework:spring-context` was updated to 5.3.10. + - `org.slf4j:slf4j-api` was updated to 1.7.32. + - `com.google.code.gson:gson` was updated to 2.8.8. + - `gradle` was updated to 7.1.1. + - `commons-io:commons-io` was updated to 2.11.0. + - `org.aspectj:aspectjweaver` was updated to 1.9.7. + - `org.eclipse.jdt:ecj` was updated to 3.26.0. + - `'junit:junit` was updated to 4.13.2. + +*7.5.1* +- **[ENHANCEMENTS]** + - Add iOS related annotations to tvOS. [#1456](https://github.com/appium/java-client/pull/1456) +- **[BUG FIX]** + - Bring back automatic quote escaping for desired capabilities command line arguments on windows. [#1454](https://github.com/appium/java-client/pull/1454) +- **[DEPENDENCY UPDATES]** + - `org.owasp.dependencycheck` was updated to 6.1.2. + - `org.eclipse.jdt:ecj` was updated to 3.25.0. + +*7.5.0* +- **[ENHANCEMENTS]** + - Add support for Appium Mac2Driver. [#1439](https://github.com/appium/java-client/pull/1439) + - Add support for multiple image occurrences. [#1445](https://github.com/appium/java-client/pull/1445) + - `BOUND_ELEMENTS_BY_INDEX` Setting was added. [#1418](https://github.com/appium/java-client/pull/1418) +- **[BUG FIX]** + - Use lower case for Windows platform key in ElementMap. [#1421](https://github.com/appium/java-client/pull/1421) +- **[DEPENDENCY UPDATES]** + - `org.apache.commons:commons-lang3` was updated to 3.12.0. + - `org.springframework:spring-context` was updated to 5.3.4. + - `org.owasp.dependencycheck` was updated to 6.1.0. + - `io.github.bonigarcia:webdrivermanager` was updated to 4.3.1. + - `org.eclipse.jdt:ecj` was updated to 3.24.0. + - `org.projectlombok:lombok` was updated to 1.18.16. + - `jcenter` repository was removed. + +*7.4.1* +- **[BUG FIX]** + - Fix the configuration of `selenium-java` dependency. [#1417](https://github.com/appium/java-client/pull/1417) +- **[DEPENDENCY UPDATES]** + - `gradle` was updated to 6.7.1. + + +*7.4.0* +- **[ENHANCEMENTS]** + - Add ability to set multiple settings. [#1409](https://github.com/appium/java-client/pull/1409) + - Support to execute Chrome DevTools Protocol commands against Android Chrome browser session. [#1375](https://github.com/appium/java-client/pull/1375) + - Add new upload options i.e withHeaders, withFormFields and withFileFieldName. [#1342](https://github.com/appium/java-client/pull/1342) + - Add AndroidOptions and iOSOptions. [#1331](https://github.com/appium/java-client/pull/1331) + - Add idempotency key to session creation requests. [#1327](https://github.com/appium/java-client/pull/1327) + - Add support for Android capability types: `buildToolsVersion`, `enforceAppInstall`, `ensureWebviewsHavePages`, `webviewDevtoolsPort`, and `remoteAppsCacheLimit`. [#1326](https://github.com/appium/java-client/pull/1326) + - Added OTHER_APPS and PRINT_PAGE_SOURCE_ON_FIND_FAILURE Mobile Capability Types. [#1323](https://github.com/appium/java-client/pull/1323) + - Make settings available for all AppiumDriver instances. [#1318](https://github.com/appium/java-client/pull/1318) + - Add wrappers for the Windows screen recorder. [#1313](https://github.com/appium/java-client/pull/1313) + - Add GitHub Action validating Gradle wrapper. [#1296](https://github.com/appium/java-client/pull/1296) + - Add support for Android viewmatcher. [#1293](https://github.com/appium/java-client/pull/1293) + - Update web view detection algorithm for iOS tests. [#1294](https://github.com/appium/java-client/pull/1294) + - Add allow-insecure and deny-insecure server flags. [#1282](https://github.com/appium/java-client/pull/1282) +- **[BUG FIX]** + - Fix jitpack build failures. [#1389](https://github.com/appium/java-client/pull/1389) + - Fix parse platformName if it is passed as enum item. [#1369](https://github.com/appium/java-client/pull/1369) + - Increase the timeout for graceful AppiumDriverLocalService termination. [#1354](https://github.com/appium/java-client/pull/1354) + - Avoid casting to RemoteWebElement in ElementOptions. [#1345](https://github.com/appium/java-client/pull/1345) + - Properly translate desiredCapabilities into a command line argument. [#1337](https://github.com/appium/java-client/pull/1337) + - Change getDeviceTime to call the `mobile` implementation. [#1332](https://github.com/appium/java-client/pull/1332) + - Remove appiumVersion from MobileCapabilityType. [#1325](https://github.com/appium/java-client/pull/1325) + - Set appropriate fluent wait timeouts. [#1316](https://github.com/appium/java-client/pull/1316) +- **[DOCUMENTATION UPDATES]** + - Update Appium Environment Troubleshooting. [#1358](https://github.com/appium/java-client/pull/1358) + - Address warnings printed by docs linter. [#1355](https://github.com/appium/java-client/pull/1355) + - Add java docs for various Mobile Options. [#1331](https://github.com/appium/java-client/pull/1331) + - Add AndroidFindBy, iOSXCUITFindBy and WindowsFindBy docs. [#1311](https://github.com/appium/java-client/pull/1311) + - Renamed maim.js to main.js. [#1277](https://github.com/appium/java-client/pull/1277) + - Improve Readability of Issue Template. [#1260](https://github.com/appium/java-client/pull/1260) + +*7.3.0* +- **[ENHANCEMENTS]** + - Add support for logging custom events on the Appium Server. [#1262](https://github.com/appium/java-client/pull/1262) + - Update Appium executable detection implementation. [#1256](https://github.com/appium/java-client/pull/1256) + - Avoid through NPE if any setting value is null. [#1241](https://github.com/appium/java-client/pull/1241) + - Settings API was improved to accept string names. [#1240](https://github.com/appium/java-client/pull/1240) + - Switch `runAppInBackground` iOS implementation in sync with other platforms. [#1229](https://github.com/appium/java-client/pull/1229) + - JavaDocs for AndroidMobileCapabilityType was updated. [#1238](https://github.com/appium/java-client/pull/1238) + - Github Actions were introduced instead of TravisCI. [#1219](https://github.com/appium/java-client/pull/1219) +- **[BUG FIX]** + - Fix return type of `getSystemBars` API. [#1216](https://github.com/appium/java-client/pull/1216) + - Avoid using `getSession` call for capabilities values retrieval [W3C Support]. [#1204](https://github.com/appium/java-client/pull/1204) + - Fix pagefactory list element initialisation when parameterised by generic type. [#1237](https://github.com/appium/java-client/pull/1237) + - Fix AndroidKey commands. [#1250](https://github.com/appium/java-client/pull/1250) + +*7.2.0* +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was reverted to stable version 3.141.59. [#1209](https://github.com/appium/java-client/pull/1209) + - `org.projectlombok:lombok:1.18.8` was introduced. [#1193](https://github.com/appium/java-client/pull/1193) +- **[ENHANCEMENTS]** + - `videoFilters` property was added to IOSStartScreenRecordingOptions. [#1180](https://github.com/appium/java-client/pull/1180) +- **[IMPROVEMENTS]** + - `Selendroid` automationName was deprecated. [#1198](https://github.com/appium/java-client/pull/1198) + - JavaDocs for AndroidMobileCapabilityType and IOSMobileCapabilityType were updated. [#1204](https://github.com/appium/java-client/pull/1204) + - JitPack builds were fixed. [#1203](https://github.com/appium/java-client/pull/1203) + +*7.1.0* +- **[ENHANCEMENTS]** + - Added an ability to get all the session details. [#1167 ](https://github.com/appium/java-client/pull/1167) + - `TRACK_SCROLL_EVENTS`, `ALLOW_INVISIBLE_ELEMENTS`, `ENABLE_NOTIFICATION_LISTENER`, + `NORMALIZE_TAG_NAMES` and `SHUTDOWN_ON_POWER_DISCONNECT` Android Settings were added. + - `KEYBOARD_AUTOCORRECTION`, `MJPEG_SCALING_FACTOR`, + `MJPEG_SERVER_SCREENSHOT_QUALITY`, `MJPEG_SERVER_FRAMERATE`, `SCREENSHOT_QUALITY` + and `KEYBOARD_PREDICTION` iOS Settings were added. + - `GET_MATCHED_IMAGE_RESULT`, `FIX_IMAGE_TEMPLATE_SCALE`, + `SHOULD_USE_COMPACT_RESPONSES`, `ELEMENT_RESPONSE_ATTRIBUTES` and + `DEFAULT_IMAGE_TEMPLATE_SCALE` settings were added for both Android and iOS [#1166](https://github.com/appium/java-client/pull/1166), [#1156 ](https://github.com/appium/java-client/pull/1156) and [#1120](https://github.com/appium/java-client/pull/1120) + - The new interface `io.appium.java_client.ExecutesDriverScript ` was added. [#1165](https://github.com/appium/java-client/pull/1165) + - Added an ability to get status of appium server. [#1153 ](https://github.com/appium/java-client/pull/1153) + - `tvOS` platform support was added. [#1142 ](https://github.com/appium/java-client/pull/1142) + - The new interface `io.appium.java_client. FindsByAndroidDataMatcher` was added. [#1106](https://github.com/appium/java-client/pull/1106) + - The selector strategy `io.appium.java_client.MobileBy.ByAndroidDataMatcher` was added. [#1106](https://github.com/appium/java-client/pull/1106) + - Selendroid for android and UIAutomation for iOS are removed. [#1077 ](https://github.com/appium/java-client/pull/1077) + - **[BUG FIX]** Platform Name enforced on driver creation is avoided now. [#1164 ](https://github.com/appium/java-client/pull/1164) + - **[BUG FIX]** Send both signalStrengh and signalStrength for `GSM_SIGNAL`. [#1115 ](https://github.com/appium/java-client/pull/1115) + - **[BUG FIX]** Null pointer exceptions when calling getCapabilities is handled better. [#1094 ](https://github.com/appium/java-client/pull/1094) + +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 4.0.0-alpha-1. + - `org.aspectj:aspectjweaver` was updated to 1.9.4. + - `org.apache.httpcomponents:httpclient` was updated to 4.5.9. + - `cglib:cglib` was updated to 3.2.12. + - `org.springframework:spring-context` was updated to 5.1.8.RELEASE. + - `io.github.bonigarcia:webdrivermanager` was updated to 3.6.1. + - `org.eclipse.jdt:ecj` was updated to 3.18.0. + - `com.github.jengelman.gradle.plugins:shadow` was updated to 5.1.0. + - `checkstyle` was updated to 8.22. + - `gradle` was updated to 5.4. + - `dependency-check-gradle` was updated to 5.1.0. + - `org.slf4j:slf4j-api` was updated to 1.7.26. + - `org.apache.commons:commons-lang3` was updated to 3.9. + +*7.0.0* +- **[ENHANCEMENTS]** + - The new interface `io.appium.java_client.FindsByAndroidViewTag` was added. [#996](https://github.com/appium/java-client/pull/996) + - The selector strategy `io.appium.java_client.MobileBy.ByAndroidViewTag` was added. [#996](https://github.com/appium/java-client/pull/996) + - The new interface `io.appium.java_client.FindsByImage` was added. [#990](https://github.com/appium/java-client/pull/990) + - The selector strategy `io.appium.java_client.MobileBy.ByImage` was added. [#990](https://github.com/appium/java-client/pull/990) + - The new interface `io.appium.java_client.FindsByCustom` was added. [#1041](https://github.com/appium/java-client/pull/1041) + - The selector strategy `io.appium.java_client.MobileBy.ByCustom` was added. [#1041](https://github.com/appium/java-client/pull/1041) + - DatatypeConverter is replaced with Base64 for JDK 9 compatibility. [#999](https://github.com/appium/java-client/pull/999) + - Expand touch options API to accept coordinates as Point. [#997](https://github.com/appium/java-client/pull/997) + - W3C capabilities written into firstMatch entity instead of alwaysMatch. [#1010](https://github.com/appium/java-client/pull/1010) + - `Selendroid` for android and `UIAutomation` for iOS is deprecated. [#1034](https://github.com/appium/java-client/pull/1034) and [#1074](https://github.com/appium/java-client/pull/1074) + - `videoScale` and `fps` screen recording options are introduced for iOS. [#1067](https://github.com/appium/java-client/pull/1067) + - `NORMALIZE_TAG_NAMES` setting was introduced for android. [#1073](https://github.com/appium/java-client/pull/1073) + - `threshold` argument was added to OccurrenceMatchingOptions. [#1060](https://github.com/appium/java-client/pull/1060) + - `org.openqa.selenium.internal.WrapsElement` replaced by `org.openqa.selenium.WrapsElement`. [#1053](https://github.com/appium/java-client/pull/1053) + - SLF4J logging support added into Appium Driver local service. [#1014](https://github.com/appium/java-client/pull/1014) + - `IMAGE_MATCH_THRESHOLD`, `FIX_IMAGE_FIND_SCREENSHOT_DIMENSIONS`, `FIX_IMAGE_TEMPLATE_SIZE`, `CHECK_IMAGE_ELEMENT_STALENESS`, `UPDATE_IMAGE_ELEMENT_POSITION` and `IMAGE_ELEMENT_TAP_STRATEGY` setting was introduced for image elements. [#1011](https://github.com/appium/java-client/pull/1011) +- **[BUG FIX]** Better handling of InvocationTargetException [#968](https://github.com/appium/java-client/pull/968) +- **[BUG FIX]** Map sending keys to active element for W3C compatibility. [#966](https://github.com/appium/java-client/pull/966) +- **[BUG FIX]** Error message on session creation is improved. [#994](https://github.com/appium/java-client/pull/994) +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 3.141.59. + - `com.google.code.gson:gson` was updated to 2.8.5. + - `org.apache.httpcomponents:httpclient` was updated to 4.5.6. + - `cglib:cglib` was updated to 3.2.8. + - `org.apache.commons:commons-lang3` was updated to 3.8. + - `org.springframework:spring-context` was updated to 5.1.0.RELEASE. + - `io.github.bonigarcia:webdrivermanager` was updated to 3.0.0. + - `org.eclipse.jdt:ecj` was updated to 3.14.0. + - `org.slf4j:slf4j-api` was updated to 1.7.25. + - `jacoco` was updated to 0.8.2. + - `checkstyle` was updated to 8.12. + - `gradle` was updated to 4.10.1. + - `org.openpnp:opencv` was removed. + +*6.1.0* +- **[BUG FIX]** Initing web socket clients lazily. Report [#911](https://github.com/appium/java-client/issues/911). FIX: [#912](https://github.com/appium/java-client/pull/912). +- **[BUG FIX]** Fix session payload for W3C. [#913](https://github.com/appium/java-client/pull/913) +- **[ENHANCEMENT]** Added TouchAction constructor argument verification [#923](https://github.com/appium/java-client/pull/923) +- **[BUG FIX]** Set retry flag to true by default for OkHttpFactory. [#928](https://github.com/appium/java-client/pull/928) +- **[BUG FIX]** Fix class cast exception on getting battery info. [#935](https://github.com/appium/java-client/pull/935) +- **[ENHANCEMENT]** Added an optional format argument to getDeviceTime and update the documentation. [#939](https://github.com/appium/java-client/pull/939) +- **[ENHANCEMENT]** The switching web socket client implementation to okhttp library. [#941](https://github.com/appium/java-client/pull/941) +- **[BUG FIX]** Fix of the bug [#924](https://github.com/appium/java-client/issues/924). [#951](https://github.com/appium/java-client/pull/951) + +*6.0.0* +- **[ENHANCEMENT]** Added an ability to set pressure value for iOS. [#879](https://github.com/appium/java-client/pull/879) +- **[ENHANCEMENT]** Added new server arguments `RELAXED_SECURITY` and `ENABLE_HEAP_DUMP`. [#880](https://github.com/appium/java-client/pull/880) +- **[BUG FIX]** Use default Selenium HTTP client factory [#877](https://github.com/appium/java-client/pull/877) +- **[ENHANCEMENT]** Supporting syslog broadcast with iOS [#871](https://github.com/appium/java-client/pull/871) +- **[ENHANCEMENT]** Added isKeyboardShown command for iOS [#887](https://github.com/appium/java-client/pull/887) +- **[ENHANCEMENT]** Added battery information accessors [#882](https://github.com/appium/java-client/pull/882) +- **[BREAKING CHANGE]** Removal of deprecated code. [#881](https://github.com/appium/java-client/pull/881) +- **[BUG FIX]** Added `NewAppiumSessionPayload`. Bug report: [#875](https://github.com/appium/java-client/issues/875). FIX: [#894](https://github.com/appium/java-client/pull/894) +- **[ENHANCEMENT]** Added ESPRESSO automation name [#908](https://github.com/appium/java-client/pull/908) +- **[ENHANCEMENT]** Added a method for output streams cleanup [#909](https://github.com/appium/java-client/pull/909) +- **[DEPENDENCY UPDATES]** + - `com.google.code.gson:gson` was updated to 2.8.4 + - `org.springframework:spring-context` was updated to 5.0.5.RELEASE + - `org.aspectj:aspectjweaver` was updated to 1.9.1 + - `org.glassfish.tyrus:tyrus-clien` was updated to 1.13.1 + - `org.glassfish.tyrus:tyrus-container-grizzly` was updated to 1.2.1 + - `org.seleniumhq.selenium:selenium-java` was updated to 3.12.0 + + +*6.0.0-BETA5* +- **[ENHANCEMENT]** Added clipboard handlers. [#855](https://github.com/appium/java-client/pull/855) [#869](https://github.com/appium/java-client/pull/869) +- **[ENHANCEMENT]** Added wrappers for Android logcat broadcaster. [#858](https://github.com/appium/java-client/pull/858) +- **[ENHANCEMENT]** Add bugreport option to Android screen recorder. [#852](https://github.com/appium/java-client/pull/852) +- **[BUG FIX]** Avoid amending parameters for SET_ALERT_VALUE endpoint. [#867](https://github.com/appium/java-client/pull/867) +- **[BREAKING CHANGE]** Refactor network connection setting on Android. [#865](https://github.com/appium/java-client/pull/865) +- **[BUG FIX]** **[BREAKING CHANGE]** Refactor of the `io.appium.java_client.AppiumFluentWait`. It uses `java.time.Duration` for time settings instead of `org.openqa.selenium.support.ui.Duration` and `java.util.concurrent.TimeUnit` [#863](https://github.com/appium/java-client/pull/863) +- **[BREAKING CHANGE]** `io.appium.java_client.pagefactory.TimeOutDuration` became deprecated. It is going to be removed. Use `java.time.Duration` instead. FIX [#742](https://github.com/appium/java-client/issues/742) [#863](https://github.com/appium/java-client/pull/863). +- **[BREAKING CHANGE]** `io.appium.java_client.pagefactory.WithTimeOut#unit` became deprecated. It is going to be removed. Use `io.appium.java_client.pagefactory.WithTimeOut#chronoUnit` instead. FIX [#742](https://github.com/appium/java-client/issues/742) [#863](https://github.com/appium/java-client/pull/863). +- **[BREAKING CHANGE]** constructors of `io.appium.java_client.pagefactory.AppiumElementLocatorFactory`, `io.appium.java_client.pagefactory.AppiumFieldDecorator` and `io.appium.java_client.pagefactory.AppiumElementLocator` which use `io.appium.java_client.pagefactory.TimeOutDuration` as a parameter became deprecated. Use new constructors which use `java.time.Duration`. +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 3.11.0 + +*6.0.0-BETA4* +- **[ENHANCEMENT]** Added handler for isDispalyed in W3C mode. [#833](https://github.com/appium/java-client/pull/833) +- **[ENHANCEMENT]** Added handlers for sending SMS, making GSM Call, setting GSM signal, voice, power capacity and power AC. [#834](https://github.com/appium/java-client/pull/834) +- **[ENHANCEMENT]** Added handlers for toggling wifi, airplane mode and data in android. [#835](https://github.com/appium/java-client/pull/835) +- **[DEPENDENCY UPDATES]** + - `org.apache.httpcomponents:httpclient` was updated to 4.5.5 + - `cglib:cglib` was updated to 3.2.6 + - `org.springframework:spring-context` was updated to 5.0.3.RELEASE + +*6.0.0-BETA3* +- **[DEPENDENCY UPDATES]** + - `org.seleniumhq.selenium:selenium-java` was updated to 3.9.1 +- **[BREAKING CHANGE]** Removal of deprecated listener-methods from the AlertEventListener. [#797](https://github.com/appium/java-client/pull/797) +- **[BUG FIX]**. Fix the `pushFile` command. [#812](https://github.com/appium/java-client/pull/812) [#816](https://github.com/appium/java-client/pull/816) +- **[ENHANCEMENT]**. Implemented custom command codec. [#817](https://github.com/appium/java-client/pull/817), [#825](https://github.com/appium/java-client/pull/825) +- **[ENHANCEMENT]** Added handlers for lock/unlock in iOS. [#799](https://github.com/appium/java-client/pull/799) +- **[ENHANCEMENT]** AddEd endpoints for screen recording API for iOS and Android. [#814](https://github.com/appium/java-client/pull/814) +- **[MAJOR ENHANCEMENT]** W3C compliance was provided. [#829](https://github.com/appium/java-client/pull/829) +- **[ENHANCEMENT]** New capability `MobileCapabilityType.FORCE_MJSONWP` [#829](https://github.com/appium/java-client/pull/829) +- **[ENHANCEMENT]** Updated applications management endpoints. [#824](https://github.com/appium/java-client/pull/824) + +*6.0.0-BETA2* +- **[ENHANCEMENT]** The `fingerPrint` ability was added. It is supported by Android for now. [#473](https://github.com/appium/java-client/pull/473) [#786](https://github.com/appium/java-client/pull/786) +- **[BUG FIX]**. Less strict verification of the `PointOption`. [#795](https://github.com/appium/java-client/pull/795) + +*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 subclasses were added + - old methods of the `TouchActions` were marked `@Deprecated` + - new methods which take new options. +- **[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]**. 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 + - `org.springframework:spring-context` was updated to 5.0.2.RELEASE + - `org.aspectj:aspectjweaver` was updated to 1.8.13 + - `org.seleniumhq.selenium:selenium-java` was updated to 3.7.1 + +*5.0.4* +- **[BUG FIX]**. Client was crashing when user was testing iOS with server 1.7.0. Report: [#732](https://github.com/appium/java-client/issues/732). Fix: [#733](https://github.com/appium/java-client/pull/733). +- **[REFACTOR]** **[BREAKING CHANGE]** Excessive invocation of the implicit waiting timeout was removed. This is the breaking change because API of `AppiumElementLocator` and `AppiumElementLocatorFactory` was changed. Request: [#735](https://github.com/appium/java-client/issues/735), FIXES: [#738](https://github.com/appium/java-client/pull/738), [#741](https://github.com/appium/java-client/pull/741) +- **[DEPENDENCY UPDATES]** + - org.seleniumhq.selenium:selenium-java to 3.6.0 + - com.google.code.gson:gson to 2.8.2 + - org.springframework:spring-context to 5.0.0.RELEASE + - org.aspectj:aspectjweaver to 1.8.11 + +*5.0.3* +- **[BUG FIX]** Selenuim version was reverted from boundaries to the single number. Issue report: [#718](https://github.com/appium/java-client/issues/718). FIX: [#722](https://github.com/appium/java-client/pull/722) +- **[ENHANCEMENT]** The `pushFile` was added to IOSDriver. Feature request: [#720](https://github.com/appium/java-client/issues/720). Implementation: [#721](https://github.com/appium/java-client/pull/721). This feature requires appium node server v>=1.7.0 + +*5.0.2* **[BUG FIX RELEASE]** +- **[BUG FIX]** Dependency conflict resolving. The report: [#714](https://github.com/appium/java-client/issues/714). The fix: [#717](https://github.com/appium/java-client/pull/717). This change may affect users who use htmlunit-driver and/or phantomjsdriver. At this case it is necessary to add it to dependency list and to exclude old selenium versions. + +*5.0.1* **[BUG FIX RELEASE]** +- **[BUG FIX]** The fix of the element genering on iOS was fixed. Issue report: [#704](https://github.com/appium/java-client/issues/704). Fix: [#705](https://github.com/appium/java-client/pull/705) + +*5.0.0* +- **[REFACTOR]** **[BREAKING CHANGE]** 5.0.0 finalization. Removal of obsolete code. [#660](https://github.com/appium/java-client/pull/660) +- **[ENHANCEMENT]** Enable nativeWebTap setting for iOS. [#658](https://github.com/appium/java-client/pull/658) +- **[ENHANCEMENT]** The `getCurrentPackage` was added. [#657](https://github.com/appium/java-client/pull/657) +- **[ENHANCEMENT]** The `toggleTouchIDEnrollment` was added. [#659](https://github.com/appium/java-client/pull/659) +- **[BUG FIX]** The clearing of existing actions/parameters after perform is invoked. [#663](https://github.com/appium/java-client/pull/663) +- **[BUG FIX]** [#669](https://github.com/appium/java-client/pull/669) missed parameters of the `OverrideWidget` were added: + - `iOSXCUITAutomation` + - `windowsAutomation` +- **[BUG FIX]** ByAll was re-implemented. [#680](https://github.com/appium/java-client/pull/680) +- **[BUG FIX]** **[BREAKING CHANGE]** The issue of compliance with Selenium grid 3.x was fixed. This change is breaking because now java_client is compatible with appiun server v>=1.6.5. Issue report [#655](https://github.com/appium/java-client/issues/655). FIX [#682](https://github.com/appium/java-client/pull/682) +- **[BUG FIX]** issues related to latest Selenium changes were fixed. Issue report [#696](https://github.com/appium/java-client/issues/696). Fix: [#699](https://github.com/appium/java-client/pull/699). +- **[UPDATE]** Dependency update + - `selenium-java` was updated to 3.5.x + - `org.apache.commons-lang3` was updated to 3.6 + - `org.springframework.spring-context` was updated to 4.3.10.RELEASE +- **[ENHANCEMENT]** Update of the touch ID enroll method. The older `PerformsTouchID#toggleTouchIDEnrollment` was marked `Deprecated`. + It is recoomended to use `PerformsTouchID#toggleTouchIDEnrollment(boolean)` instead. [#695](https://github.com/appium/java-client/pull/695) + + +*5.0.0-BETA9* +- **[ENHANCEMENT]** Page factory: Mixed locator strategies were implemented. Feature request:[#565](https://github.com/appium/java-client/issues/565) Implementation: [#646](https://github.com/appium/java-client/pull/646) +- **[DEPRECATED]** All the content of the `io.appium.java_client.youiengine` package was marked `Deprecated`. It is going to be removed. [#652](https://github.com/appium/java-client/pull/652) +- **[UPDATE]** Update of the `com.google.code.gson:gson` to v2.8.1. + +*5.0.0-BETA8* +- **[ENHANCEMENT]** Page factory classes became which had package visibility are `public` now. [#630](https://github.com/appium/java-client/pull/630) + - `io.appium.java_client.pagefactory.AppiumElementLocatorFactory` + - `io.appium.java_client.pagefactory.DefaultElementByBuilder` + - `io.appium.java_client.pagefactory.WidgetByBuilder` + +- **[ENHANCEMENT]** New capabilities were added [#626](https://github.com/appium/java-client/pull/626): + - `AndroidMobileCapabilityType#AUTO_GRANT_PERMISSIONS` + - `AndroidMobileCapabilityType#ANDROID_NATURAL_ORIENTATION` + - `IOSMobileCapabilityType#XCODE_ORG_ID` + - `IOSMobileCapabilityType#XCODE_SIGNING_ID` + - `IOSMobileCapabilityType#UPDATE_WDA_BUNDLEID` + - `IOSMobileCapabilityType#RESET_ON_SESSION_START_ONLY` + - `IOSMobileCapabilityType#COMMAND_TIMEOUTS` + - `IOSMobileCapabilityType#WDA_STARTUP_RETRIES` + - `IOSMobileCapabilityType#WDA_STARTUP_RETRY_INTERVAL` + - `IOSMobileCapabilityType#CONNECT_HARDWARE_KEYBOARD` + - `IOSMobileCapabilityType#MAX_TYPING_FREQUENCY` + - `IOSMobileCapabilityType#SIMPLE_ISVISIBLE_CHECK` + - `IOSMobileCapabilityType#USE_CARTHAGE_SSL` + - `IOSMobileCapabilityType#SHOULD_USE_SINGLETON_TESTMANAGER` + - `IOSMobileCapabilityType#START_IWDP` + - `IOSMobileCapabilityType#ALLOW_TOUCHID_ENROLL` + - `MobileCapabilityType#EVENT_TIMINGS` + +- **[UPDATE]** Dependencies were updated: + - `org.seleniumhq.selenium:selenium-java` was updated to 3.4.0 + - `cglib:cglib` was updated to 3.2.5 + - `org.apache.httpcomponents:httpclient` was updated to 4.5.3 + - `commons-validator:commons-validator` was updated to 1.6 + - `org.springframework:spring-context` was updated to 4.3.8.RELEASE + + +*5.0.0-BETA7* +- **[ENHANCEMENT]** The ability to customize the polling strategy of the waiting was provided. [#612](https://github.com/appium/java-client/pull/612) +- **[ENHANCEMENT]** **[REFACTOR]** Methods which were representing time deltas instead of elementary types became `Deprecated`. Methods which use `java.time.Duration` are suugested to be used. [#611](https://github.com/appium/java-client/pull/611) +- **[ENHANCEMENT]** The ability to calculate screenshots overlap was included. [#595](https://github.com/appium/java-client/pull/595). + + +*5.0.0-BETA6* +- **[UPDATE]** Update to Selenium 3.3.1 +- **[ENHANCEMENT]** iOS XCUIT mode automation: API to run application in background was added. [#593](https://github.com/appium/java-client/pull/593) +- **[BUG FIX]** Issue report: [#594](https://github.com/appium/java-client/issues/594). FIX: [#597](https://github.com/appium/java-client/pull/597) +- **[ENHANCEMENT]** The class chain locator was added. [#599](https://github.com/appium/java-client/pull/599) + + +*5.0.0-BETA5* +- **[UPDATE]** Update to Selenium 3.2.0 +- **[BUG FIX]** Excessive dependency on `guava` was removed. It causes errors. Issue report: [#588](https://github.com/appium/java-client/issues/588). FIX: [#589](https://github.com/appium/java-client/pull/589). +- **[ENHANCEMENT]**. The capability `io.appium.java_client.remote.AndroidMobileCapabilityType#SYSTEM_PORT` was added. [#591](https://github.com/appium/java-client/pull/591) + +*5.0.0-BETA4* +- **[ENHANCEMENT]** Android. API to read the performance data was added. [#562](https://github.com/appium/java-client/pull/562) +- **[REFACTOR]** Android. Simplified the activity starting by reducing the number of parameters through POJO clas. Old methods which start activities were marked `@Deprecated`. [#579](https://github.com/appium/java-client/pull/579) [#585](https://github.com/appium/java-client/pull/585) +- **[BUG FIX]** Issue report:[#574](https://github.com/appium/java-client/issues/574). Fix:[#582](https://github.com/appium/java-client/pull/582) + +*5.0.0-BETA3* +[BUG FIX] +- **[BUG FIX]**:Issue report: [#567](https://github.com/appium/java-client/issues/567). Fix: [#568](https://github.com/appium/java-client/pull/568) + +*5.0.0-BETA2* +- **[BUG FIX]**:Issue report: [#549](https://github.com/appium/java-client/issues/549). Fix: [#551](https://github.com/appium/java-client/pull/551) +- New capabilities were added [#533](https://github.com/appium/java-client/pull/553): + - `IOSMobileCapabilityType#USE_NEW_WDA` + - `IOSMobileCapabilityType#WDA_LAUNCH_TIMEOUT` + - `IOSMobileCapabilityType#WDA_CONNECTION_TIMEOUT` + +The capability `IOSMobileCapabilityType#REAL_DEVICE_LOGGER` was removed. [#533](https://github.com/appium/java-client/pull/553) + +- **[BUG FIX]/[ENHANCEMENT]**. Issue report: [#552](https://github.com/appium/java-client/issues/552). FIX [#556](https://github.com/appium/java-client/pull/556) + - Additional methods were added to the `io.appium.java_client.HasSessionDetails` + - `String getPlatformName()` + - `String getAutomationName()` + - `boolean isBrowser()` + - `io.appium.java_client.HasSessionDetails` is used by the ` io.appium.java_client.internal.JsonToMobileElementConverter ` to define which instance of the `org.openqa.selenium.WebElement` subclass should be created. + +- **[ENHANCEMENT]**: The additional event firing feature. PR: [#559](https://github.com/appium/java-client/pull/559). The [WIKI chapter about the event firing](https://github.com/appium/java-client/blob/master/docs/The-event_firing.md) was updated. + +*5.0.0-BETA1* +- **[MAJOR ENHANCEMENT]**: Migration to Java 8. Epic: [#399](https://github.com/appium/java-client/issues/399) + - API with default implementation. PR [#470](https://github.com/appium/java-client/pull/470) + - Tools that provide _Page Object_ engines were redesigned. The migration to [repeatable annotations](http://docs.oracle.com/javase/tutorial/java/annotations/repeating.html). Details you can read there: [#497](https://github.com/appium/java-client/pull/497). [Documentation was synced as well](https://github.com/appium/java-client/blob/master/docs/Page-objects.md#also-it-is-possible-to-define-chained-or-any-possible-locators). + - The new functional interface `io.appium.java_client.functions.AppiumFunctio`n was designed. It extends `java.util.function.Function` and `com.google.common.base.Function`. It was designed in order to provide compatibility with the `org.openqa.selenium.support.ui.Wait` [#543](https://github.com/appium/java-client/pull/543) + - The new functional interface `io.appium.java_client.functions.ExpectedCondition` was designed. It extends `io.appium.java_client.functions.AppiumFunction` and ```org.openqa.selenium.support.ui.ExpectedCondition```. [#543](https://github.com/appium/java-client/pull/543) + - The new functional interface `io.appium.java_client.functions.ActionSupplier` was designed. It extends ```java.util.function.Supplier```. [#543](https://github.com/appium/java-client/pull/543) + +- **[MAJOR ENHANCEMENT]**: Migration from Maven to Gradle. Feature request is [#214](https://github.com/appium/java-client/issues/214). Fixes: [#442](https://github.com/appium/java-client/pull/442), [#465](https://github.com/appium/java-client/pull/465). + +- **[MAJOR ENHANCEMENT]** **[MAJOR REFACTORING]**. Non-abstract **AppiumDriver**: + - Now the `io.appium.java_client.AppiumDriver` can use an instance of any `io.appium.java_client.MobileBy` subclass for the searching. It should work as expected when current session supports the given selector. It will throw `org.openqa.selenium.WebDriverException` otherwise. [#462](https://github.com/appium/java-client/pull/462) + - The new interface `io.appium.java_client.FindsByFluentSelector` was added. [#462](https://github.com/appium/java-client/pull/462) + - API was redesigned: + + these interfaces were marked deprecated and they are going to be removed [#513](https://github.com/appium/java-client/pull/513)[#514](https://github.com/appium/java-client/pull/514): + - `io.appium.java_client.DeviceActionShortcuts` + - `io.appium.java_client.android.AndroidDeviceActionShortcuts` + - `io.appium.java_client.ios.IOSDeviceActionShortcuts` + + instead following inerfaces were designed: + - `io.appium.java_client.HasDeviceTime` + - `io.appium.java_client.HidesKeyboard` + - `io.appium.java_client.HidesKeyboardWithKeyName` + - `io.appium.java_client.PressesKeyCode` + - `io.appium.java_client.ios.ShakesDevice` + - `io.appium.java_client.HasSessionDetails` + _That was done because Windows automation tools have some features that were considered as Android-specific and iOS-specific._ + + The list of classes and methods which were marked _deprecated_ and they are going to be removed + - `AppiumDriver#swipe(int, int, int, int, int)` + - `AppiumDriver#pinch(WebElement)` + - `AppiumDriver#pinch(int, int)` + - `AppiumDriver#zoom(WebElement)` + - `AppiumDriver#zoom(int, int)` + - `AppiumDriver#tap(int, WebElement, int)` + - `AppiumDriver#tap(int, int, int, int)` + - `AppiumDriver#swipe(int, int, int, int, int)` + - `MobileElement#swipe(SwipeElementDirection, int)` + - `MobileElement#swipe(SwipeElementDirection, int, int, int)` + - `MobileElement#zoom()` + - `MobileElement#pinch()` + - `MobileElement#tap(int, int)` + - `io.appium.java_client.SwipeElementDirection` and `io.appium.java_client.TouchebleElement` also were marked deprecated. + + redesign of `TouchAction` and `MultiTouchAction` + - constructors were redesigned. There is no strict binding of `AppiumDriver` and `TouchAction` /`MultiTouchAction`. They can consume any instance of a class that implements `PerformsTouchActions`. + - `io.appium.java_client.ios.IOSTouchAction` was added. It extends `io.appium.java_client.TouchAction`. + - the new interface `io.appium.java_client.PerformsActions` was added. It unifies `TouchAction` and `MultiTouchAction` now. [#543](https://github.com/appium/java-client/pull/543) + + `JsonToMobileElementConverter` re-design [#532](https://github.com/appium/java-client/pull/532): + - unused `MobileElementToJsonConverter` was removed + - `JsonToMobileElementConverter` is not rhe abstract class now. It generates instances of MobileElement subclasses according to current session parameters + - `JsonToAndroidElementConverter` is deprecated now + - `JsonToIOSElementConverter` is depreacated now + - `JsonToYouiEngineElementConverter` is deprecated now. + - constructors of 'AppiumDriver' were re-designed. + - constructors of 'AndroidDriver' were re-designed. + - constructors of 'IOSDriver' were re-designed. + +- **[MAJOR ENHANCEMENT]** Windows automation. Epic [#471](https://github.com/appium/java-client/issues/471) + - The new interface `io.appium.java_client.FindsByWindowsAutomation` was added. [#462](https://github.com/appium/java-client/pull/462). With [@jonstoneman](https://github.com/jonstoneman) 's authorship. + - The new selector strategy `io.appium.java_client.MobileBy.ByWindowsAutomation` was added. [#462](https://github.com/appium/java-client/pull/462). With [@jonstoneman](https://github.com/jonstoneman) 's authorship. + - `io.appium.java_client.windows.WindowsDriver` was designed. [#538](https://github.com/appium/java-client/pull/538) + - `io.appium.java_client.windows.WindowsElement` was designed. [#538](https://github.com/appium/java-client/pull/538) + - `io.appium.java_client.windows.WindowsKeyCode ` was added. [#538](https://github.com/appium/java-client/pull/538) + - Page object tools were updated [#538](https://github.com/appium/java-client/pull/538) + - the `io.appium.java_client.pagefactory.WindowsFindBy` annotation was added. + - `io.appium.java_client.pagefactory.AppiumFieldDecorator` and supporting tools were actualized. + +- **[MAJOR ENHANCEMENT]** iOS XCUIT mode automation: + - `io.appium.java_client.remote.AutomationName#IOS_XCUI_TEST` was added + - The new interface `io.appium.java_client.FindsByIosNSPredicate` was added. [#462](https://github.com/appium/java-client/pull/462). With [@rafael-chavez](https://github.com/rafael-chavez) 's authorship. It is implemented by `io.appium.java_client.ios.IOSDriver` and `io.appium.java_client.ios.IOSElement`. + - The new selector strategy `io.appium.java_client.MobileBy.ByIosNsPredicate` was added. [#462](https://github.com/appium/java-client/pull/462). With [@rafael-chavez](https://github.com/rafael-chavez) 's authorship. + - Page object tools were updated [#545](https://github.com/appium/java-client/pull/545), [#546](https://github.com/appium/java-client/pull/546) + - the `io.appium.java_client.pagefactory.iOSXCUITFindBy` annotation was added. + - `io.appium.java_client.pagefactory.AppiumFieldDecorator` and supporting tools were actualized. + +- [ENHANCEMENT] Added the ability to set UiAutomator Congfigurator values. [#410](https://github.com/appium/java-client/pull/410). + [#477](https://github.com/appium/java-client/pull/477). +- [ENHANCEMENT]. Additional methods which perform device rotation were implemented. [#489](https://github.com/appium/java-client/pull/489). [#439](https://github.com/appium/java-client/pull/439). But it works for iOS in XCUIT mode and for Android in UIAutomator2 mode only. The feature request: [#7131](https://github.com/appium/appium/issues/7131) +- [ENHANCEMENT]. TouchID Implementation (iOS Sim Only). Details: [#509](https://github.com/appium/java-client/pull/509) +- [ENHANCEMENT]. The ability to use port, ip and log file as server arguments was provided. Feature request: [#521](https://github.com/appium/java-client/issues/521). Fixes: [#522](https://github.com/appium/java-client/issues/522), [#524](https://github.com/appium/java-client/issues/524). +- [ENHANCEMENT]. The new interface ```io.appium.java_client.android.HasDeviceDetails``` was added. It is implemented by ```io.appium.java_client.android.AndroidDriver``` by default. [#518](https://github.com/appium/java-client/pull/518) +- [ENHANCEMENT]. New touch actions were added. ```io.appium.java_client.ios.IOSTouchAction#doubleTap(WebElement, int, int)``` and ```io.appium.java_client.ios.IOSTouchAction#doubleTap(WebElement)```. [#523](https://github.com/appium/java-client/pull/523), [#444](https://github.com/appium/java-client/pull/444) +- [ENHANCEMENT]. All constructors declared by `io.appium.java_client.AppiumDriver` are public now. +- [BUG FIX]: There was the issue when "@WithTimeout" was changing general timeout of the waiting for elements. Bug report: [#467](https://github.com/appium/java-client/issues/467). Fixes: [#468](https://github.com/appium/java-client/issues/468), [#469](https://github.com/appium/java-client/issues/469), [#480](https://github.com/appium/java-client/issues/480). Read: [supported-settings](https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md#supported-settings) +- Added the server flag `io.appium.java_client.service.local.flags.AndroidServerFlag#REBOOT`. [#476](https://github.com/appium/java-client/pull/476) +- Added `io.appium.java_client.remote.AndroidMobileCapabilityType.APP_WAIT_DURATION ` capability. [#461](https://github.com/appium/java-client/pull/461) +- the new automation type `io.appium.java_client.remote.MobilePlatform#ANDROID_UIAUTOMATOR2` was add. +- the new automation type `io.appium.java_client.remote.MobilePlatform#YOUI_ENGINE` was add. +- Additional capabilities were addede: + - `IOSMobileCapabilityType#CUSTOM_SSL_CERT` + - `IOSMobileCapabilityType#TAP_WITH_SHORT_PRESS_DURATION` + - `IOSMobileCapabilityType#SCALE_FACTOR` + - `IOSMobileCapabilityType#WDA_LOCAL_PORT` + - `IOSMobileCapabilityType#SHOW_XCODE_LOG` + - `IOSMobileCapabilityType#REAL_DEVICE_LOGGER` + - `IOSMobileCapabilityType#IOS_INSTALL_PAUSE` + - `IOSMobileCapabilityType#XCODE_CONFIG_FILE` + - `IOSMobileCapabilityType#KEYCHAIN_PASSWORD` + - `IOSMobileCapabilityType#USE_PREBUILT_WDA` + - `IOSMobileCapabilityType#PREVENT_WDAATTACHMENTS` + - `IOSMobileCapabilityType#WEB_DRIVER_AGENT_URL` + - `IOSMobileCapabilityType#KEYCHAIN_PATH` + - `MobileCapabilityType#CLEAR_SYSTEM_FILES` +- **[UPDATE]** to Selenium 3.0.1. +- **[UPDATE]** to Spring Framework 4.3.5.RELEASE. +- **[UPDATE]** to AspectJ weaver 1.8.10. + + + +*4.1.2* + +- Following capabilities were added: + - `io.appium.java_client.remote.AndroidMobileCapabilityType.ANDROID_INSTALL_TIMEOUT` + - `io.appium.java_client.remote.AndroidMobileCapabilityType.NATIVE_WEB_SCREENSHOT` + - `io.appium.java_client.remote.AndroidMobileCapabilityType.ANDROID_SCREENSHOT_PATH`. The pull request: [#452](https://github.com/appium/java-client/pull/452) +- `org.openqa.selenium.Alert` was reimplemented for iOS. Details: [#459](https://github.com/appium/java-client/pull/459) +- The deprecated `io.appium.java_client.generic.searchcontext` was removed. +- The dependency on `com.google.code.gson` was updated to 2.7. Also it was adde to exclusions + for `org.seleniumhq.selenium` `selenium-java`. +- The new AutomationName was added. IOS_XCUI_TEST. It is needed for the further development. +- The new MobilePlatform was added. WINDOWS. It is needed for the further development. + +*4.1.1* + +BUG FIX: Issue [#450](https://github.com/appium/java-client/issues/450). Fix: [#451](https://github.com/appium/java-client/issues/451). Thanks to [@tutunang](https://github.com/appium/java-client/pull/451) for the report. + +*4.1.0* +- all code marked `@Deprecated` was removed. +- `getSessionDetails()` was added. Thanks to [@saikrishna321](https://github.com/saikrishna321) for the contribution. +- FIX [#362](https://github.com/appium/java-client/issues/362), [#220](https://github.com/appium/java-client/issues/220), [#323](https://github.com/appium/java-client/issues/323). Details read there: [#413](https://github.com/appium/java-client/pull/413) +- FIX [#392](https://github.com/appium/java-client/issues/392). Thanks to [@truebit](https://github.com/truebit) for the bug report. +- The dependency on `cglib` was replaced by the dependency on `cglib-nodep`. FIX [#418](https://github.com/appium/java-client/issues/418) +- The casting to the weaker interface `HasIdentity` instead of class `RemoteWebElement` was added. It is the internal refactoring of the `TouchAction`. [#432](https://github.com/appium/java-client/pull/432). Thanks to [@asolntsev](https://github.com/asolntsev) for the contribution. +- The `setValue` method was moved to `MobileElement`. It works against text input elements on Android. +- The dependency on `org.springframework` `spring-context` v`4.3.2.RELEASE` was added +- The dependency on `org.aspectj` `aspectjweaver` v`1.8.9` was added +- ENHANCEMENT: The alternative event firing engine. The feature request: [#242](https://github.com/appium/java-client/issues/242). + Implementation: [#437](https://github.com/appium/java-client/pull/437). Also [new WIKI chapter](https://github.com/appium/java-client/blob/master/docs/The-event_firing.md) was added. +- ENHANCEMENT: Convenient access to specific commands for each supported mobile OS. Details: [#445](https://github.com/appium/java-client/pull/445) +- dependencies and plugins were updated +- ENHANCEMENT: `YouiEngineDriver` was added. Details: [appium server #6215](https://github.com/appium/appium/pull/6215), [#429](https://github.com/appium/java-client/pull/429), [#448](https://github.com/appium/java-client/pull/448). It is just the draft of the new solution that is going to be extended further. Please stay tuned. There are many interesting things are coming up. Thanks to `You I Engine` team for the contribution. + +*4.0.0* +- all code marked `@Deprecated` was removed. Java client won't support old servers (v<1.5.0) + anymore. +- the ability to start an activity using Android intent actions, intent categories, flags and arguments + was added to `AndroidDriver`. Thanks to [@saikrishna321](https://github.com/saikrishna321) for the contribution. +- `scrollTo()` and `scrollToExact()` became deprecated. They are going to be removed in the next release. +- The interface `io.appium.java_client.ios.GetsNamedTextField` and the declared method `T getNamedTextField(String name)` are + deprecated as well. They are going to be removed in the next release. +- Methods `findElements(String by, String using)` and `findElement(String by, String using)` of `org.openga.selenium.remote.RemoteWebdriver` are public now. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget). +- the `io.appium.java_client.NetworkConnectionSetting` class was marked deprecated +- the enum `io.appium.java_client.android.Connection` was added. All supported network bitmasks are defined there. +- Android. Old methods which get/set connection were marked `@Deprecated` +- Android. New methods which consume/return `io.appium.java_client.android.Connection` were added. +- the `commandRepository` field is public now. The modification of the `MobileCommand` +- Constructors like `AppiumDriver(HttpCommandExecutor executor, Capabilities capabilities)` were added to + `io.appium.java_client.android.AndroidDriver` and `io.appium.java_client.ios.IOSDriver` +- The refactoring of `io.appium.java_client.internal.JsonToMobileElementConverter`. Now it accepts + `org.openqa.selenium.remote.RemoteWebDriver` as the constructor parameter. It is possible to re-use + `io.appium.java_client.android.internal.JsonToAndroidElementConverter` or + `io.appium.java_client.ios.internal.JsonToIOSElementConverter` by RemoteWebDriver when it is needed. +- Constructors of the abstract `io.appium.java_client.AppiumDriver` were redesigned. Now they require + a subclass of `io.appium.java_client.internal.JsonToMobileElementConverter`. Constructors of + `io.appium.java_client.android.AndroidDriver` and `io.appium.java_client.ios.IOSDriver` are same still. +- The `pushFile(String remotePath, File file)` was added to AndroidDriver +- FIX of TouchAction. Instances of the TouchAction class are reusable now +- FIX of the swiping issue (iOS, server version >= 1.5.0). Now the swiping is implemented differently by + AndroidDriver and IOSDriver. Thanks to [@truebit](https://github.com/truebit) and [@nuggit32](https://github.com/nuggit32) for the catching. +- the project was integrated with [maven-checkstyle-plugin](https://maven.apache.org/plugins/maven-checkstyle-plugin/). Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the work +- source code was improved according to code style checking rules. +- the integration with `org.owasp dependency-check-maven` was added. Thanks to [@saikrishna321](https://github.com/saikrishna321) + for the work. +- the integration with `org.jacoco jacoco-maven-plugin` was added. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the contribution. + +*3.4.1* +- Update to Selenium v2.53.0 +- all dependencies were updated to latest versions +- the dependency on org.apache.commons commons-lang3 v3.4 was added +- the fix of Widget method invocation.[#340](https://github.com/appium/java-client/issues/340). A class visibility was taken into account. Thanks to [aznime](https://github.com/aznime) for the catching. + Server flags were added: + - GeneralServerFlag.ASYNC_TRACE + - IOSServerFlag.WEBKIT_DEBUG_PROXY_PORT +- Source code was formatted using [eclipse-java-google-style.xml](https://google-styleguide.googlecode.com/svn/trunk/eclipse-java-google-style.xml). This is not the complete solution. The code style checking is going to be added further. Thanks to [SrinivasanTarget](https://github.com/SrinivasanTarget) for the work! + +*3.4.0* +- Update to Selenium v2.52.0 +- `getAppStrings()` methods are deprecated now. They are going to be removed. `getAppStringMap()` methods were added and now return a map with app strings (keys and values) + instead of a string. Thanks to [@rgonalo](https://github.com/rgonalo) for the contribution. +- Add `getAppStringMap(String language, String stringFile)` method to allow searching app strings in the specified file +- FIXED of the bug which causes deadlocks of AppiumDriver LocalService in multithreading. Thanks to [saikrishna321](https://github.com/saikrishna321) for the [bug report](https://github.com/appium/java-client/issues/283). +- FIXED Zoom methods, thanks to [@kkhaidukov](https://github.com/kkhaidukov) +- FIXED The issue of compatibility of AppiumServiceBuilder with Appium node server v >= 1.5.x. Take a look at [#305](https://github.com/appium/java-client/issues/305) +- `getDeviceTime()` was added. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the contribution. +- FIXED `longPressKeyCode()` methods. Now they use the convenient JSONWP command.Thanks to [@kirillbilchenko](https://github.com/kirillbilchenko) for the proposed fix. +- FIXED javadoc. +- Page object tools were updated. Details read here: [#311](https://github.com/appium/java-client/issues/311), [#313](https://github.com/appium/java-client/pull/313), [#317](https://github.com/appium/java-client/pull/317). By.name locator strategy is deprecated for Android and iOS. It is still valid for the Selendroid mode. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the helping. +- The method `lockScreen(seconds)` is deprecated and it is going to be removed in the next release. Since Appium node server v1.5.x it is recommended to use + `AndroidDriver.lockDevice()...AndroidDriver.unlockDevice()` or `IOSDriver.lockDevice(int seconds)` instead. Thanks to [@namannigam](https://github.com/namannigam) for + the catching. Read [#315](https://github.com/appium/java-client/issues/315) +- `maven-release-plugin` was added to POM.XML configuration +- [#320](https://github.com/appium/java-client/issues/320) fix. The `Widget.getSelfReference()` was added. This method allows to extract a real widget-object from inside a proxy at some extraordinary situations. Read: [PR](https://github.com/appium/java-client/pull/327). Thanks to [SergeyErmakovMercDev](https://github.com/SergeyErmakovMercDev) for the reporting. +- all capabilities were added according to [this description](https://github.com/appium/appium/blob/1.5/docs/en/writing-running-appium/caps.md). There are three classes: `io.appium.java_client.remote.MobileCapabilityType` (just modified), `io.appium.java_client.remote.AndroidMobileCapabilityType` (android-specific capabilities), `io.appium.java_client.remote.IOSMobileCapabilityType` (iOS-specific capabilities). Details are here: [#326](https://github.com/appium/java-client/pull/326) +- some server flags were marked `deprecated` because they are deprecated since server node v1.5.x. These flags are going to be removed at the java client release. Details are here: [#326](https://github.com/appium/java-client/pull/326) +- The ability to start Appium node programmatically using desired capabilities. This feature is compatible with Appium node server v >= 1.5.x. Details are here: [#326](https://github.com/appium/java-client/pull/326) + +*3.3.0* +- updated the dependency on Selenium to version 2.48.2 +- bug fix and enhancements of io.appium.java_client.service.local.AppiumDriverLocalService + - FIXED bug which was found and reproduced with Eclipse for Mac OS X. Please read about details here: [#252](https://github.com/appium/java-client/issues/252) + Thanks to [saikrishna321](https://github.com/saikrishna321) for the bug report + - FIXED bug which was found out by [Jonahss](https://github.com/Jonahss). Thanks for the reporting. Details: [#272](https://github.com/appium/java-client/issues/272) + and [#273](https://github.com/appium/java-client/issues/273) + - For starting an appium server using localService, added additional environment variable to specify the location of Node.js binary: NODE_BINARY_PATH + - The ability to set additional output streams was provided +- The additional __startActivity()__ method was added to AndroidDriver. It allows to start activities without the stopping of a target app + Thanks to [deadmoto](https://github.com/deadmoto) for the contribution +- The additional extension of the Page Object design pattern was designed. Please read about details here: [#267](https://github.com/appium/java-client/pull/267) +- New public constructors to AndroidDriver/IOSDriver that allow passing a custom HttpClient.Factory Details: [#276](https://github.com/appium/java-client/pull/278) thanks to [baechul](https://github.com/baechul) + +*3.2.0* +- updated the dependency on Selenium to version 2.47.1 +- the new dependency on commons-validator v1.4.1 +- the ability to start programmatically/silently an Appium node server is provided now. Details please read at [#240](https://github.com/appium/java-client/pull/240). + Historical reference: [The similar solution](https://github.com/Genium-Framework/Appium-Support) has been designed by [@Hassan-Radi](https://github.com/Hassan-Radi). + The mentioned framework and the current solution use different approaches. +- Throwing declarations were added to some searching methods. The __"getMouse"__ method of RemoteWebDriver was marked __Deprecated__ +- Add `replaceValue` method for elements. +- Replace `sendKeyEvent()` method in android with pressKeyCode(int key) and added: pressKeyCode(int key, Integer metastate), longPressKeyCode(int key), longPressKeyCode(int key, Integer metastate) + +*3.1.1* +- Page-object findBy strategies are now aware of which driver (iOS or Android) you are using. For more details see the Pull Request: https://github.com/appium/java-client/pull/213 +- If somebody desires to use their own Webdriver implementation then it has to implement HasCapabilities. +- Added a new annotation: `WithTimeout`. This annotation allows one to specify a specific timeout for finding an element which overrides the drivers default timeout. For more info see: https://github.com/appium/java-client/pull/210 +- Corrected an uninformative Exception message. + +*3.0.0* +- AppiumDriver class is now a Generic. This allows us to return elements of class MobileElement (and its subclasses) instead of always returning WebElements and requiring users to cast to MobileElement. See https://github.com/appium/java-client/pull/182 +- Full set of Android KeyEvents added. +- Selenium client version updated to 2.46 +- PageObject enhancements +- Junit dependency removed + +*2.2.0* +- Added new TouchAction methods for LongPress, on an element, at x,y coordinates, or at an offset from within an element +- SwipeElementDirection changed. Read the documentation, it's now smarter about how/where to swipe +- Added APPIUM_VERSION MobileCapabilityType +- `sendKeyEvent()` moved from AppiumDriver to AndroidDriver +- `linkText` and `partialLinkText` locators added +- setValue() moved from MobileElement to iOSElement +- Fixed Selendroid PageAnnotations + +*2.1.0* +- Moved hasAppString() from AndroidDriver to AppiumDriver +- Fixes to PageFactory +- Added @AndroidFindAll and @iOSFindAll +- Added toggleLocationServices() to AndroidDriver +- Added touchAction methods to MobileElement, so now you can do `element.pinch()`, `element.zoom()`, etc. +- Added the ability to choose a direction to swipe over an element. Use the `SwipeElementDirection` enums: `UP, DOWN, LEFT, RIGHT` + +*2.0.0* +- AppiumDriver is now an abstract class, use IOSDriver and AndroidDriver which both extend it. You no longer need to include the `PLATFORM_NAME` desired capability since it's automatic for each class. Thanks to @TikhomirovSergey for all their work +- ScrollTo() and ScrollToExact() methods reimplemented +- Zoom() and Pinch() are now a little smarter and less likely to fail if you element is near the edge of the screen. Congratulate @BJap on their first PR! + +*1.7.0* +- Removed `scrollTo()` and `scrollToExact()` methods because they relied on `complexFind()`. They will be added back in the next version! +- Removed `complexFind()` +- Added `startActivity()` method +- Added `isLocked()` method +- Added `getSettings()` and `ignoreUnimportantViews()` methods + +*1.6.2* +- Added MobilePlatform interface (Android, IOS, FirefoxOS) +- Added MobileBrowserType interface (Safari, Browser, Chromium, Chrome) +- Added MobileCapabilityType.APP_WAIT_ACTIVITY +- Fixed small Integer cast issue (in Eclipse it won't compile) +- Set -source and -target of the Java Compiler to 1.7 (for maven compiler plugin) +- Fixed bug in Page Factory + +*1.6.1* +- Fixed the logic for checking connection status on NetworkConnectionSetting objects + +*1.6.0* +- Added @findBy annotations. Explanation here: https://github.com/appium/java-client/pull/68 Thanks to TikhomirovSergey +- Appium Driver now implements LocationContext interface, so setLocation() works for setting GPS coordinates + +*1.5.0* +- Added MobileCapabilityType enums for desired capabilities +- `findElement` and `findElements` return MobileElement objects (still need to be casted, but no longer instantiated) +- new appium v1.2 `hideKeyboard()` strategies added +- `getNetworkConnection()` and `setNetworkConnection()` commands added + +*1.4.0* +- Added openNotifications() method, to open the notifications shade on Android +- Added pullFolder() method, to pull an entire folder as a zip archive from a device/simulator +- Upgraded Selenium dependency to 2.42.2 + +*1.3.0* +- MultiGesture with a single TouchAction fixed for Android +- Now depends upon Selenium java client 2.42.1 +- Cleanup of Errorcode handling, due to merging a change into Selenium + +*1.2.1* +- fix dependency issue + +*1.2.0* +- complexFind() now returns MobileElement objects +- added scrollTo() and scrollToExact() methods for use with complexFind() + +*1.1.0* +- AppiumDriver now implements Rotatable. rotate() and getOrientation() methods added +- when no appium server is running, the proper error is thrown, instead of a NullPointerException + +*1.0.2* +- recompiled to include some missing methods such as shake() and complexFind() diff --git a/README.md b/README.md index 39c936ba5..4d235791a 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,61 @@ # 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) -[![Build Status](https://travis-ci.org/appium/java-client.svg?branch=master)](https://travis-ci.org/appium/java-client) +[![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 binding for writing Appium Tests, conforms to [Mobile JSON Wire Protocol](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md) +This is the Java language bindings for writing Appium Tests that conform to [WebDriver Protocol](https://w3c.github.io/webdriver/) -[API docs](https://www.javadoc.io/doc/io.appium/java-client) -### Features and other interesting information +## v9 to v10 Migration -[Tech stack](https://github.com/appium/java-client/blob/master/docs/Tech-stack.md) +Follow the [v9 to v10 Migration Guide](./docs/v9-to-v10-migration-guide.md) to streamline the migration process. -[How to install the project](https://github.com/appium/java-client/blob/master/docs/Installing-the-project.md) +## v8 to v9 Migration -[WIKI](https://github.com/appium/java-client/wiki) +Since v9 the client only supports Java 11 and above. +Follow the [v8 to v9 Migration Guide](./docs/v8-to-v9-migration-guide.md) to streamline the migration process. -## v8 Migration +## v7 to v8 Migration -Since version 8 Appium Java Client had several major changes, which might require to -update your client code. Make sure to follow the [v7 to v8 Migration Guide](https://github.com/appium/java-client/blob/master/docs/v7-to-v8-migration-guide.md) -in order to streamline the migration process. +Since version 8 Appium Java Client had several major changes, which might require to +update your client code. Make sure to follow the [v7 to v8 Migration Guide](./docs/v7-to-v8-migration-guide.md) +to streamline the migration process. -## How to install latest java client Beta/Snapshots +## Add Appium java client to your test framework -Java client project is available to use even before it is officially published to maven central. Refer [jitpack.io](https://jitpack.io/#appium/java-client) +### Stable -### Maven +#### Maven - - Add the following to pom.xml: +Add the following to pom.xml: + +```xml + + io.appium + java-client + ${version.you.require} + test + +``` + +#### Gradle + +Add the following to build.gradle: + +```groovy +dependencies { + testImplementation 'io.appium:java-client:${version.you.require}' +} +``` + +### Beta/Snapshots + +Java client project is available to use even before it is officially published to Maven Central. Refer [jitpack.io](https://jitpack.io/#appium/java-client) + +#### Maven + +Add the following to pom.xml: ```xml @@ -39,851 +66,208 @@ Java client project is available to use even before it is officially published t ``` - - Add the dependency: - +Add the dependency: + ```xml com.github.appium java-client latest commit ID from master branch -``` +``` -### Gradle +#### Gradle - - Add the JitPack repository to your build file. Add it in your root build.gradle at the end of repositories: - -``` +Add the JitPack repository to your build file. Add it to your root build.gradle at the end of repositories: + +```groovy allprojects { repositories { - ... + // ... maven { url 'https://jitpack.io' } } } ``` - - Add the dependency: - -``` +Add the dependency: + +```groovy dependencies { implementation 'com.github.appium:java-client:latest commit id from master branch' } ``` +### Compatibility Matrix + Appium Java Client | Selenium client +----------------------------------------------------------------------------------------------------|----------------------------- +`next` (not released yet) | `4.40.0` +`10.0.0` | `4.35.0`, `4.36.0`, `4.37.0`, `4.38.0`, `4.39.0` +`9.5.0` | `4.34.0` +`9.4.0` | `4.26.0`, `4.27.0`, `4.28.0`, `4.28.1`, `4.29.0`, `4.30.0`, `4.31.0`, `4.32.0`, `4.33.0` + `9.2.1`(known issues: appium/java-client#2145, appium/java-client#2146), `9.2.2`, `9.2.3`, `9.3.0` | `4.19.0`, `4.19.1`, `4.20.0`, `4.21.0`, `4.22.0`, `4.23.0`, `4.23.1`, `4.24.0`, `4.25.0`, `4.26.0`, `4.27.0` + `9.1.0`, `9.2.0` | `4.17.0`, `4.18.0`, `4.18.1` + `9.0.0` | `4.14.1`, `4.15.0`, `4.16.0` (partially [corrupted](https://github.com/SeleniumHQ/selenium/issues/13256)), `4.16.1` + N/A | `4.14.0` + `8.5.0`, `8.5.1`, `8.6.0` | `4.9.1`, `4.10.0`, `4.11.0`, `4.12.0`, `4.12.1` (known issue: appium/java-client#2004), `4.13.0` + `8.4.0` | `4.8.2`, `4.8.3`, `4.9.0` + `8.3.0` | `4.7.0`, `4.7.1`, `4.7.2`, `4.8.0`, `4.8.1` + `8.2.1` | `4.5.0`, `4.5.1`, `4.5.2`, `4.5.3`, `4.6.0` + +#### Why is it so complicated? + +Selenium client does not follow [Semantic Versioning](https://semver.org/), so breaking changes might be introduced +even in patches, which requires the Appium team to update the Java client in response. + +#### How to pin Selenium dependencies? + +Appium Java Client declares Selenium dependencies using an open version range which is handled differently by different +build tools. Sometimes users may want to pin used Selenium dependencies for [various reasons](https://github.com/appium/java-client/issues/1823). +Follow the [Transitive Dependencies Management article](docs/transitive-dependencies-management.md) for more information +about establishing a fixed Selenium version for your Java test framework. + +## Drivers Support + +Appium java client has dedicated classes to support the following Appium drivers: + +- [UiAutomator2](https://github.com/appium/appium-uiautomator2-driver) and [Espresso](https://github.com/appium/appium-espresso-driver): [AndroidDriver](src/main/java/io/appium/java_client/android/AndroidDriver.java) +- [XCUITest](https://github.com/appium/appium-xcuitest-driver): [IOSDriver](src/main/java/io/appium/java_client/ios/IOSDriver.java) +- [Windows](https://github.com/appium/appium-windows-driver): [WindowsDriver](src/main/java/io/appium/java_client/windows/WindowsDriver.java) +- [Safari](https://github.com/appium/appium-safari-driver): [SafariDriver](src/main/java/io/appium/java_client/safari/SafariDriver.java) +- [Gecko](https://github.com/appium/appium-geckodriver): [GeckoDriver](src/main/java/io/appium/java_client/gecko/GeckoDriver.java) +- [Mac2](https://github.com/appium/appium-mac2-driver): [Mac2Driver](src/main/java/io/appium/java_client/mac/Mac2Driver.java) + +To automate other platforms that are not listed above you could use +[AppiumDriver](src/main/java/io/appium/java_client/AppiumDriver.java) or its custom derivatives. + +Appium java client is built on top of Selenium and implements the same interfaces that the foundation +[RemoteWebDriver](https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/remote/RemoteWebDriver.java) +does. However, Selenium lib is mostly focused on web browser automation while +Appium is universal and covers a wide range of possible platforms, e.g. mobile and desktop +operating systems, IOT devices, etc. Thus, the foundation `AppiumDriver` class in this package +extends `RemoteWebDriver` with additional features, and makes it more flexible, so it is not so +strictly focused on web-browser related operations. + +## Appium Server Service Wrapper + +Appium java client provides a dedicated class to control Appium server execution. +The class is [AppiumDriverLocalService](src/main/java/io/appium/java_client/service/local/AppiumDriverLocalService.java). +It allows to run and verify the Appium server **locally** from your test framework code +and provides several convenient shortcuts. The service could be used as below: + +```java +AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); +service.start(); +try { + // do stuff with drivers +} finally { + service.stop(); +} +``` + +You could customize the service behavior, for example, provide custom +command line arguments or change paths to server executables +using [AppiumServiceBuilder](src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java) + +**Note** + +> AppiumDriverLocalService does not support server management on non-local hosts + +## Usage Examples + +### UiAutomator2 + +```java +UiAutomator2Options options = new UiAutomator2Options() + .setUdid("123456") + .setApp("/home/myapp.apk"); +AndroidDriver driver = new AndroidDriver( + // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub + new URI("http://127.0.0.1:4723").toURL(), options +); +try { + WebElement el = driver.findElement(AppiumBy.xpath("//Button")); + el.click(); + driver.getPageSource(); +} finally { + driver.quit(); +} +``` + +### XCUITest + +```java +XCUITestOptions options = new XCUITestOptions() + .setUdid("123456") + .setApp("/home/myapp.ipa"); +IOSDriver driver = new IOSDriver( + // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub + new URI("http://127.0.0.1:4723").toURL(), options +); +try { + WebElement el = driver.findElement(AppiumBy.accessibilityId("myId")); + el.click(); + driver.getPageSource(); +} finally { + driver.quit(); +} +``` + +### Any generic driver that does not have a dedicated class + +```java +BaseOptions options = new BaseOptions() + .setPlatformName("myplatform") + .setAutomationName("mydriver") + .amend("mycapability1", "capvalue1") + .amend("mycapability2", "capvalue2"); +AppiumDriver driver = new AppiumDriver( + // The default URL in Appium 1 is http://127.0.0.1:4723/wd/hub + new URI("http://127.0.0.1:4723").toURL(), options +); +try { + WebElement el = driver.findElement(AppiumBy.className("myClass")); + el.click(); + driver.getPageSource(); +} finally { + driver.quit(); +} +``` + +Check the corresponding driver's READMEs to know the list of capabilities and features it supports. + +You can find many more code examples by checking client's +[unit and integration tests](src/test/java/io/appium/java_client). + +## Troubleshooting + +### InaccessibleObjectException is thrown in runtime if Java 16+ is used + +Appium Java client uses reflective access to private members of other modules +to ensure proper functionality of several features, like the Page Object model. +If you get a runtime exception and `InaccessibleObjectException` is present in +the stack trace and your Java runtime is at version 16 or higher, then consider the following +[Oracle's tutorial](https://docs.oracle.com/en/java/javase/16/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-7BB28E4D-99B3-4078-BDC4-FC24180CE82B) +and/or checking [existing issues](https://github.com/appium/java-client/search?q=InaccessibleObjectException&type=issues) +for possible solutions. The idea there would be to explicitly allow +access for particular modules using `--add-exports/--add-opens` command line arguments. + +Another possible, but weakly advised solution, would be to downgrade Java to +version 15 or lower. + +### Issues related to environment variables' presence or to their values + +Such issues are usually the case when the Appium server is started directly from your +framework code rather than run separately by a script or manually. Depending +on the way the server process is started it may or may not inherit the currently +active shell environment. That is why you may still receive errors about the variables' +presence even though these variables are defined for your command line interpreter. +Again, there is no universal solution to that, as there are many ways to spin up a new +server process. Consider checking the [Appium Environment Troubleshooting](docs/environment.md) +document for more information on how to debug and fix process environment issues. + ## Changelog -*8.1.1* -- **[BUG FIX]** - - Perform safe typecast while getting the platform name. [#1702](https://github.com/appium/java-client/pull/1702) - - Add prefix to platformVersion capability name. [#1704](https://github.com/appium/java-client/pull/1704) -- **[REFRACTOR]** - - Update e2e tests to make it green. [#1706](https://github.com/appium/java-client/pull/1706) - - Ignore the test which has a connected server issue. [#1699](https://github.com/appium/java-client/pull/1699) - -*8.1.0* -- **[ENHANCEMENTS]** - - Add new EspressoBuildConfig options. [#1687](https://github.com/appium/java-client/pull/1687) -- **[DOCUMENTATION]** - - delete all references to removed MobileElement class. [#1677](https://github.com/appium/java-client/pull/1677) -- **[BUG FIX]** - - Pass orientation name capability in uppercase. [#1686](https://github.com/appium/java-client/pull/1686) - - correction for ping method to get proper status URL. [#1661](https://github.com/appium/java-client/pull/1661) - - Remove deprecated option classes. [#1679](https://github.com/appium/java-client/pull/1679) - - Remove obsolete event firing decorators. [#1676](https://github.com/appium/java-client/pull/1676) -- **[DEPENDENCY UPDATES]** - - `org.seleniumhq.selenium:selenium-java` was updated to 4.2.0. - - `org.owasp.dependencycheck` was updated to 7.1.0.1. - - `org.springframework:spring-context` was removed. [#1676](https://github.com/appium/java-client/pull/1676) - - `org.aspectj:aspectjweaver` was updated to 1.9.9. - - `io.github.bonigarcia:webdrivermanager` was updated to 5.2.0. - - `org.projectlombok:lombok` was updated to 1.18.24. - -*8.0.0* -- **[DOCUMENTATION]** - - Set minimum Java version to 1.8.0. [#1631](https://github.com/appium/java-client/pull/1631) -- **[BUG FIX]** - - Make interfaces public to fix decorator creation. [#1644](https://github.com/appium/java-client/pull/1644) - - Do not convert argument names to lowercase. [#1627](https://github.com/appium/java-client/pull/1627) - - Avoid fallback to css for id and name locator annotations. [#1622](https://github.com/appium/java-client/pull/1622) - - Fix handling of chinese characters in `AppiumDriverLocalService`. [#1618](https://github.com/appium/java-client/pull/1618) -- **[DEPENDENCY UPDATES]** - - `org.owasp.dependencycheck` was updated to 7.0.0. - - `org.springframework:spring-context` was updated to 5.3.16. - - `actions/setup-java` was updated to 3. - - `actions/checkout` was updated to 3. - - `io.github.bonigarcia:webdrivermanager` was updated to 5.1.0. - - `org.aspectj:aspectjweaver` was updated to 1.9.8. - - `org.slf4j:slf4j-api` was updated to 1.7.36. - - `com.github.johnrengelman.shadow` was updated to 7.1.2. - -*8.0.0-beta2* -- **[DOCUMENTATION]** - - Add a link to options builder examples to the migration guide. [#1595](https://github.com/appium/java-client/pull/1595) -- **[BUG FIX]** - - Filter out proxyClassLookup method from Proxy class (for Java 16+) in AppiumByBuilder. [#1575](https://github.com/appium/java-client/pull/1575) -- **[REFRACTOR]** - - Add more nice functional stuff into page factory helpers. [#1584](https://github.com/appium/java-client/pull/1584) - - Switch e2e tests to use Appium2. [#1603](https://github.com/appium/java-client/pull/1603) - - relax constraints of Selenium dependencies versions. [#1606](https://github.com/appium/java-client/pull/1606) -- **[DEPENDENCY UPDATES]** - - Upgrade to Selenium 4.1.1. [#1613](https://github.com/appium/java-client/pull/1613) - - `org.owasp.dependencycheck` was updated to 6.5.1. - - `org.springframework:spring-context` was updated to 5.3.14. - - `actions/setup-java` was updated to 2.4.0. - - `gradle` was updated to 7.3. - -*8.0.0-beta* -- **[ENHANCEMENTS]** - - Start adding UiAutomator2 options. [#1543](https://github.com/appium/java-client/pull/1543) - - Add more UiAutomator2 options. [#1545](https://github.com/appium/java-client/pull/1545) - - Finish creating options for UiAutomator2 driver. [#1548](https://github.com/appium/java-client/pull/1548) - - Add WDA-related XCUITestOptions. [#1552](https://github.com/appium/java-client/pull/1552) - - Add web view options for XCUITest driver. [#1557](https://github.com/appium/java-client/pull/1557) - - Add the rest of XCUITest driver options. [#1561](https://github.com/appium/java-client/pull/1561) - - Add Espresso options. [#1563](https://github.com/appium/java-client/pull/1563) - - Add Windows driver options. [#1564](https://github.com/appium/java-client/pull/1564) - - Add Mac2 driver options. [#1565](https://github.com/appium/java-client/pull/1565) - - Add Gecko driver options. [#1573](https://github.com/appium/java-client/pull/1573) - - Add Safari driver options. [#1576](https://github.com/appium/java-client/pull/1576) - - Start adding XCUITest driver options. [#1551](https://github.com/appium/java-client/pull/1551) - - Implement driver-specific W3C option classes. [#1540](https://github.com/appium/java-client/pull/1540) - - Update Service to properly work with options. [#1550](https://github.com/appium/java-client/pull/1550) -- **[BREAKING CHANGE]** - - Migrate to Selenium 4. [#1531](https://github.com/appium/java-client/pull/1531) - - Make sure we only write W3C payload into create session command. [#1537](https://github.com/appium/java-client/pull/1537) - - Use the new session payload creator inherited from Selenium. [#1535](https://github.com/appium/java-client/pull/1535) - - unify locator factories naming and toString implementations. [#1538](https://github.com/appium/java-client/pull/1538) - - drop support of deprecated Selendroid driver. [#1553](https://github.com/appium/java-client/pull/1553) - - switch to javac compiler. [#1556](https://github.com/appium/java-client/pull/1556) - - revise used Selenium dependencies. [#1560](https://github.com/appium/java-client/pull/1560) - - change prefix to AppiumBy in locator toString implementation. [#1559](https://github.com/appium/java-client/pull/1559) - - enable dependencies caching. [#1567](https://github.com/appium/java-client/pull/1567) - - Include more tests into the pipeline. [#1566](https://github.com/appium/java-client/pull/1566) - - Tune setting of default platform names. [#1570](https://github.com/appium/java-client/pull/1570) - - Deprecate custom event listener implementation and default to the one provided by Selenium4. [#1541](https://github.com/appium/java-client/pull/1541) - - Deprecate touch actions. [#1569](https://github.com/appium/java-client/pull/1569) - - Deprecate legacy app management helpers. [#1571](https://github.com/appium/java-client/pull/1571) - - deprecate Windows UIAutomation selector. [#1562](https://github.com/appium/java-client/pull/1562) - - Remove unused entities. [#1572](https://github.com/appium/java-client/pull/1572) - - Remove setElementValue helper. [#1577](https://github.com/appium/java-client/pull/1577) - - Remove selenium package override. [#1555](https://github.com/appium/java-client/pull/1555) - - remove redundant exclusion of Gradle task signMavenJavaPublication. [#1568](https://github.com/appium/java-client/pull/1568) -- **[DEPENDENCY UPDATES]** - - `org.owasp.dependencycheck` was updated to 6.4.1. - - `com.google.code.gson:gson` was updated to 2.8.9. - -*7.6.0* -- **[ENHANCEMENTS]** - - Add custom commands dynamically [Appium 2.0]. [#1506](https://github.com/appium/java-client/pull/1506) - - New General Server flags are added [Appium 2.0]. [#1511](https://github.com/appium/java-client/pull/1511) - - Add support of extended Android geolocation. [#1492](https://github.com/appium/java-client/pull/1492) -- **[BUG FIX]** - - AndroidGeoLocation: update the constructor signature to mimic order of parameters in `org.openqa.selenium.html5.Location`. [#1526](https://github.com/appium/java-client/pull/1526) - - Prevent duplicate builds for PRs from base repo branches. [#1496](https://github.com/appium/java-client/pull/1496) - - Enable Dependabot for GitHub actions. [#1500](https://github.com/appium/java-client/pull/1500) - - bind mac2element in element map for mac platform. [#1474](https://github.com/appium/java-client/pull/1474) -- **[DEPENDENCY UPDATES]** - - `org.owasp.dependencycheck` was updated to 6.3.2. - - `org.projectlombok:lombok` was updated to 1.18.22. - - `com.github.johnrengelman.shadow` was updated to 7.1.0. - - `actions/setup-java` was updated to 2.3.1. - - `io.github.bonigarcia:webdrivermanager` was updated to 5.0.3. - - `org.springframework:spring-context` was updated to 5.3.10. - - `org.slf4j:slf4j-api` was updated to 1.7.32. - - `com.google.code.gson:gson` was updated to 2.8.8. - - `gradle` was updated to 7.1.1. - - `commons-io:commons-io` was updated to 2.11.0. - - `org.aspectj:aspectjweaver` was updated to 1.9.7. - - `org.eclipse.jdt:ecj` was updated to 3.26.0. - - `'junit:junit` was updated to 4.13.2. - -*7.5.1* -- **[ENHANCEMENTS]** - - Add iOS related annotations to tvOS. [#1456](https://github.com/appium/java-client/pull/1456) -- **[BUG FIX]** - - Bring back automatic quote escaping for desired capabilities command line arguments on windows. [#1454](https://github.com/appium/java-client/pull/1454) -- **[DEPENDENCY UPDATES]** - - `org.owasp.dependencycheck` was updated to 6.1.2. - - `org.eclipse.jdt:ecj` was updated to 3.25.0. - -*7.5.0* -- **[ENHANCEMENTS]** - - Add support for Appium Mac2Driver. [#1439](https://github.com/appium/java-client/pull/1439) - - Add support for multiple image occurrences. [#1445](https://github.com/appium/java-client/pull/1445) - - `BOUND_ELEMENTS_BY_INDEX` Setting was added. [#1418](https://github.com/appium/java-client/pull/1418) -- **[BUG FIX]** - - Use lower case for Windows platform key in ElementMap. [#1421](https://github.com/appium/java-client/pull/1421) -- **[DEPENDENCY UPDATES]** - - `org.apache.commons:commons-lang3` was updated to 3.12.0. - - `org.springframework:spring-context` was updated to 5.3.4. - - `org.owasp.dependencycheck` was updated to 6.1.0. - - `io.github.bonigarcia:webdrivermanager` was updated to 4.3.1. - - `org.eclipse.jdt:ecj` was updated to 3.24.0. - - `org.projectlombok:lombok` was updated to 1.18.16. - - `jcenter` repository was removed. - -*7.4.1* -- **[BUG FIX]** - - Fix the configuration of `selenium-java` dependency. [#1417](https://github.com/appium/java-client/pull/1417) -- **[DEPENDENCY UPDATES]** - - `gradle` was updated to 6.7.1. - - -*7.4.0* -- **[ENHANCEMENTS]** - - Add ability to set multiple settings. [#1409](https://github.com/appium/java-client/pull/1409) - - Support to execute Chrome DevTools Protocol commands against Android Chrome browser session. [#1375](https://github.com/appium/java-client/pull/1375) - - Add new upload options i.e withHeaders, withFormFields and withFileFieldName. [#1342](https://github.com/appium/java-client/pull/1342) - - Add AndroidOptions and iOSOptions. [#1331](https://github.com/appium/java-client/pull/1331) - - Add idempotency key to session creation requests. [#1327](https://github.com/appium/java-client/pull/1327) - - Add support for Android capability types: `buildToolsVersion`, `enforceAppInstall`, `ensureWebviewsHavePages`, `webviewDevtoolsPort`, and `remoteAppsCacheLimit`. [#1326](https://github.com/appium/java-client/pull/1326) - - Added OTHER_APPS and PRINT_PAGE_SOURCE_ON_FIND_FAILURE Mobile Capability Types. [#1323](https://github.com/appium/java-client/pull/1323) - - Make settings available for all AppiumDriver instances. [#1318](https://github.com/appium/java-client/pull/1318) - - Add wrappers for the Windows screen recorder. [#1313](https://github.com/appium/java-client/pull/1313) - - Add GitHub Action validating Gradle wrapper. [#1296](https://github.com/appium/java-client/pull/1296) - - Add support for Android viewmatcher. [#1293](https://github.com/appium/java-client/pull/1293) - - Update web view detection algorithm for iOS tests. [#1294](https://github.com/appium/java-client/pull/1294) - - Add allow-insecure and deny-insecure server flags. [#1282](https://github.com/appium/java-client/pull/1282) -- **[BUG FIX]** - - Fix jitpack build failures. [#1389](https://github.com/appium/java-client/pull/1389) - - Fix parse platformName if it is passed as enum item. [#1369](https://github.com/appium/java-client/pull/1369) - - Increase the timeout for graceful AppiumDriverLocalService termination. [#1354](https://github.com/appium/java-client/pull/1354) - - Avoid casting to RemoteWebElement in ElementOptions. [#1345](https://github.com/appium/java-client/pull/1345) - - Properly translate desiredCapabilities into a command line argument. [#1337](https://github.com/appium/java-client/pull/1337) - - Change getDeviceTime to call the `mobile` implementation. [#1332](https://github.com/appium/java-client/pull/1332) - - Remove appiumVersion from MobileCapabilityType. [#1325](https://github.com/appium/java-client/pull/1325) - - Set appropriate fluent wait timeouts. [#1316](https://github.com/appium/java-client/pull/1316) -- **[DOCUMENTATION UPDATES]** - - Update Appium Environment Troubleshooting. [#1358](https://github.com/appium/java-client/pull/1358) - - Address warnings printed by docs linter. [#1355](https://github.com/appium/java-client/pull/1355) - - Add java docs for various Mobile Options. [#1331](https://github.com/appium/java-client/pull/1331) - - Add AndroidFindBy, iOSXCUITFindBy and WindowsFindBy docs. [#1311](https://github.com/appium/java-client/pull/1311) - - Renamed maim.js to main.js. [#1277](https://github.com/appium/java-client/pull/1277) - - Improve Readability of Issue Template. [#1260](https://github.com/appium/java-client/pull/1260) - -*7.3.0* -- **[ENHANCEMENTS]** - - Add support for logging custom events on the Appium Server. [#1262](https://github.com/appium/java-client/pull/1262) - - Update Appium executable detection implementation. [#1256](https://github.com/appium/java-client/pull/1256) - - Avoid through NPE if any setting value is null. [#1241](https://github.com/appium/java-client/pull/1241) - - Settings API was improved to accept string names. [#1240](https://github.com/appium/java-client/pull/1240) - - Switch `runAppInBackground` iOS implementation in sync with other platforms. [#1229](https://github.com/appium/java-client/pull/1229) - - JavaDocs for AndroidMobileCapabilityType was updated. [#1238](https://github.com/appium/java-client/pull/1238) - - Github Actions were introduced instead of TravisCI. [#1219](https://github.com/appium/java-client/pull/1219) -- **[BUG FIX]** - - Fix return type of `getSystemBars` API. [#1216](https://github.com/appium/java-client/pull/1216) - - Avoid using `getSession` call for capabilities values retrieval [W3C Support]. [#1204](https://github.com/appium/java-client/pull/1204) - - Fix pagefactory list element initialisation when parameterised by generic type. [#1237](https://github.com/appium/java-client/pull/1237) - - Fix AndroidKey commands. [#1250](https://github.com/appium/java-client/pull/1250) - -*7.2.0* -- **[DEPENDENCY UPDATES]** - - `org.seleniumhq.selenium:selenium-java` was reverted to stable version 3.141.59. [#1209](https://github.com/appium/java-client/pull/1209) - - `org.projectlombok:lombok:1.18.8` was introduced. [#1193](https://github.com/appium/java-client/pull/1193) -- **[ENHANCEMENTS]** - - `videoFilters` property was added to IOSStartScreenRecordingOptions. [#1180](https://github.com/appium/java-client/pull/1180) -- **[IMPROVEMENTS]** - - `Selendroid` automationName was deprecated. [#1198](https://github.com/appium/java-client/pull/1198) - - JavaDocs for AndroidMobileCapabilityType and IOSMobileCapabilityType were updated. [#1204](https://github.com/appium/java-client/pull/1204) - - JitPack builds were fixed. [#1203](https://github.com/appium/java-client/pull/1203) - -*7.1.0* -- **[ENHANCEMENTS]** - - Added an ability to get all the session details. [#1167 ](https://github.com/appium/java-client/pull/1167) - - `TRACK_SCROLL_EVENTS`, `ALLOW_INVISIBLE_ELEMENTS`, `ENABLE_NOTIFICATION_LISTENER`, - `NORMALIZE_TAG_NAMES` and `SHUTDOWN_ON_POWER_DISCONNECT` Android Settings were added. - - `KEYBOARD_AUTOCORRECTION`, `MJPEG_SCALING_FACTOR`, - `MJPEG_SERVER_SCREENSHOT_QUALITY`, `MJPEG_SERVER_FRAMERATE`, `SCREENSHOT_QUALITY` - and `KEYBOARD_PREDICTION` iOS Settings were added. - - `GET_MATCHED_IMAGE_RESULT`, `FIX_IMAGE_TEMPLATE_SCALE`, - `SHOULD_USE_COMPACT_RESPONSES`, `ELEMENT_RESPONSE_ATTRIBUTES` and - `DEFAULT_IMAGE_TEMPLATE_SCALE` settings were added for both Android and iOS [#1166](https://github.com/appium/java-client/pull/1166), [#1156 ](https://github.com/appium/java-client/pull/1156) and [#1120](https://github.com/appium/java-client/pull/1120) - - The new interface `io.appium.java_client.ExecutesDriverScript ` was added. [#1165](https://github.com/appium/java-client/pull/1165) - - Added an ability to get status of appium server. [#1153 ](https://github.com/appium/java-client/pull/1153) - - `tvOS` platform support was added. [#1142 ](https://github.com/appium/java-client/pull/1142) - - The new interface `io.appium.java_client. FindsByAndroidDataMatcher` was added. [#1106](https://github.com/appium/java-client/pull/1106) - - The selector strategy `io.appium.java_client.MobileBy.ByAndroidDataMatcher` was added. [#1106](https://github.com/appium/java-client/pull/1106) - - Selendroid for android and UIAutomation for iOS are removed. [#1077 ](https://github.com/appium/java-client/pull/1077) - - **[BUG FIX]** Platform Name enforced on driver creation is avoided now. [#1164 ](https://github.com/appium/java-client/pull/1164) - - **[BUG FIX]** Send both signalStrengh and signalStrength for `GSM_SIGNAL`. [#1115 ](https://github.com/appium/java-client/pull/1115) - - **[BUG FIX]** Null pointer exceptions when calling getCapabilities is handled better. [#1094 ](https://github.com/appium/java-client/pull/1094) - -- **[DEPENDENCY UPDATES]** - - `org.seleniumhq.selenium:selenium-java` was updated to 4.0.0-alpha-1. - - `org.aspectj:aspectjweaver` was updated to 1.9.4. - - `org.apache.httpcomponents:httpclient` was updated to 4.5.9. - - `cglib:cglib` was updated to 3.2.12. - - `org.springframework:spring-context` was updated to 5.1.8.RELEASE. - - `io.github.bonigarcia:webdrivermanager` was updated to 3.6.1. - - `org.eclipse.jdt:ecj` was updated to 3.18.0. - - `com.github.jengelman.gradle.plugins:shadow` was updated to 5.1.0. - - `checkstyle` was updated to 8.22. - - `gradle` was updated to 5.4. - - `dependency-check-gradle` was updated to 5.1.0. - - `org.slf4j:slf4j-api` was updated to 1.7.26. - - `org.apache.commons:commons-lang3` was updated to 3.9. - -*7.0.0* -- **[ENHANCEMENTS]** - - The new interface `io.appium.java_client.FindsByAndroidViewTag` was added. [#996](https://github.com/appium/java-client/pull/996) - - The selector strategy `io.appium.java_client.MobileBy.ByAndroidViewTag` was added. [#996](https://github.com/appium/java-client/pull/996) - - The new interface `io.appium.java_client.FindsByImage` was added. [#990](https://github.com/appium/java-client/pull/990) - - The selector strategy `io.appium.java_client.MobileBy.ByImage` was added. [#990](https://github.com/appium/java-client/pull/990) - - The new interface `io.appium.java_client.FindsByCustom` was added. [#1041](https://github.com/appium/java-client/pull/1041) - - The selector strategy `io.appium.java_client.MobileBy.ByCustom` was added. [#1041](https://github.com/appium/java-client/pull/1041) - - DatatypeConverter is replaced with Base64 for JDK 9 compatibility. [#999](https://github.com/appium/java-client/pull/999) - - Expand touch options API to accept coordinates as Point. [#997](https://github.com/appium/java-client/pull/997) - - W3C capabilities written into firstMatch entity instead of alwaysMatch. [#1010](https://github.com/appium/java-client/pull/1010) - - `Selendroid` for android and `UIAutomation` for iOS is deprecated. [#1034](https://github.com/appium/java-client/pull/1034) and [#1074](https://github.com/appium/java-client/pull/1074) - - `videoScale` and `fps` screen recording options are introduced for iOS. [#1067](https://github.com/appium/java-client/pull/1067) - - `NORMALIZE_TAG_NAMES` setting was introduced for android. [#1073](https://github.com/appium/java-client/pull/1073) - - `threshold` argument was added to OccurrenceMatchingOptions. [#1060](https://github.com/appium/java-client/pull/1060) - - `org.openqa.selenium.internal.WrapsElement` replaced by `org.openqa.selenium.WrapsElement`. [#1053](https://github.com/appium/java-client/pull/1053) - - SLF4J logging support added into Appium Driver local service. [#1014](https://github.com/appium/java-client/pull/1014) - - `IMAGE_MATCH_THRESHOLD`, `FIX_IMAGE_FIND_SCREENSHOT_DIMENSIONS`, `FIX_IMAGE_TEMPLATE_SIZE`, `CHECK_IMAGE_ELEMENT_STALENESS`, `UPDATE_IMAGE_ELEMENT_POSITION` and `IMAGE_ELEMENT_TAP_STRATEGY` setting was introduced for image elements. [#1011](https://github.com/appium/java-client/pull/1011) -- **[BUG FIX]** Better handling of InvocationTargetException [#968](https://github.com/appium/java-client/pull/968) -- **[BUG FIX]** Map sending keys to active element for W3C compatibility. [#966](https://github.com/appium/java-client/pull/966) -- **[BUG FIX]** Error message on session creation is improved. [#994](https://github.com/appium/java-client/pull/994) -- **[DEPENDENCY UPDATES]** - - `org.seleniumhq.selenium:selenium-java` was updated to 3.141.59. - - `com.google.code.gson:gson` was updated to 2.8.5. - - `org.apache.httpcomponents:httpclient` was updated to 4.5.6. - - `cglib:cglib` was updated to 3.2.8. - - `org.apache.commons:commons-lang3` was updated to 3.8. - - `org.springframework:spring-context` was updated to 5.1.0.RELEASE. - - `io.github.bonigarcia:webdrivermanager` was updated to 3.0.0. - - `org.eclipse.jdt:ecj` was updated to 3.14.0. - - `org.slf4j:slf4j-api` was updated to 1.7.25. - - `jacoco` was updated to 0.8.2. - - `checkstyle` was updated to 8.12. - - `gradle` was updated to 4.10.1. - - `org.openpnp:opencv` was removed. - -*6.1.0* -- **[BUG FIX]** Initing web socket clients lazily. Report [#911](https://github.com/appium/java-client/issues/911). FIX: [#912](https://github.com/appium/java-client/pull/912). -- **[BUG FIX]** Fix session payload for W3C. [#913](https://github.com/appium/java-client/pull/913) -- **[ENHANCEMENT]** Added TouchAction constructor argument verification [#923](https://github.com/appium/java-client/pull/923) -- **[BUG FIX]** Set retry flag to true by default for OkHttpFactory. [#928](https://github.com/appium/java-client/pull/928) -- **[BUG FIX]** Fix class cast exception on getting battery info. [#935](https://github.com/appium/java-client/pull/935) -- **[ENHANCEMENT]** Added an optional format argument to getDeviceTime and update the documentation. [#939](https://github.com/appium/java-client/pull/939) -- **[ENHANCEMENT]** The switching web socket client implementation to okhttp library. [#941](https://github.com/appium/java-client/pull/941) -- **[BUG FIX]** Fix of the bug [#924](https://github.com/appium/java-client/issues/924). [#951](https://github.com/appium/java-client/pull/951) - -*6.0.0* -- **[ENHANCEMENT]** Added an ability to set pressure value for iOS. [#879](https://github.com/appium/java-client/pull/879) -- **[ENHANCEMENT]** Added new server arguments `RELAXED_SECURITY` and `ENABLE_HEAP_DUMP`. [#880](https://github.com/appium/java-client/pull/880) -- **[BUG FIX]** Use default Selenium HTTP client factory [#877](https://github.com/appium/java-client/pull/877) -- **[ENHANCEMENT]** Supporting syslog broadcast with iOS [#871](https://github.com/appium/java-client/pull/871) -- **[ENHANCEMENT]** Added isKeyboardShown command for iOS [#887](https://github.com/appium/java-client/pull/887) -- **[ENHANCEMENT]** Added battery information accessors [#882](https://github.com/appium/java-client/pull/882) -- **[BREAKING CHANGE]** Removal of deprecated code. [#881](https://github.com/appium/java-client/pull/881) -- **[BUG FIX]** Added `NewAppiumSessionPayload`. Bug report: [#875](https://github.com/appium/java-client/issues/875). FIX: [#894](https://github.com/appium/java-client/pull/894) -- **[ENHANCEMENT]** Added ESPRESSO automation name [#908](https://github.com/appium/java-client/pull/908) -- **[ENHANCEMENT]** Added a method for output streams cleanup [#909](https://github.com/appium/java-client/pull/909) -- **[DEPENDENCY UPDATES]** - - `com.google.code.gson:gson` was updated to 2.8.4 - - `org.springframework:spring-context` was updated to 5.0.5.RELEASE - - `org.aspectj:aspectjweaver` was updated to 1.9.1 - - `org.glassfish.tyrus:tyrus-clien` was updated to 1.13.1 - - `org.glassfish.tyrus:tyrus-container-grizzly` was updated to 1.2.1 - - `org.seleniumhq.selenium:selenium-java` was updated to 3.12.0 - - -*6.0.0-BETA5* -- **[ENHANCEMENT]** Added clipboard handlers. [#855](https://github.com/appium/java-client/pull/855) [#869](https://github.com/appium/java-client/pull/869) -- **[ENHANCEMENT]** Added wrappers for Android logcat broadcaster. [#858](https://github.com/appium/java-client/pull/858) -- **[ENHANCEMENT]** Add bugreport option to Android screen recorder. [#852](https://github.com/appium/java-client/pull/852) -- **[BUG FIX]** Avoid amending parameters for SET_ALERT_VALUE endpoint. [#867](https://github.com/appium/java-client/pull/867) -- **[BREAKING CHANGE]** Refactor network connection setting on Android. [#865](https://github.com/appium/java-client/pull/865) -- **[BUG FIX]** **[BREAKING CHANGE]** Refactor of the `io.appium.java_client.AppiumFluentWait`. It uses `java.time.Duration` for time settings instead of `org.openqa.selenium.support.ui.Duration` and `java.util.concurrent.TimeUnit` [#863](https://github.com/appium/java-client/pull/863) -- **[BREAKING CHANGE]** `io.appium.java_client.pagefactory.TimeOutDuration` became deprecated. It is going to be removed. Use `java.time.Duration` instead. FIX [#742](https://github.com/appium/java-client/issues/742) [#863](https://github.com/appium/java-client/pull/863). -- **[BREAKING CHANGE]** `io.appium.java_client.pagefactory.WithTimeOut#unit` became deprecated. It is going to be removed. Use `io.appium.java_client.pagefactory.WithTimeOut#chronoUnit` instead. FIX [#742](https://github.com/appium/java-client/issues/742) [#863](https://github.com/appium/java-client/pull/863). -- **[BREAKING CHANGE]** constructors of `io.appium.java_client.pagefactory.AppiumElementLocatorFactory`, `io.appium.java_client.pagefactory.AppiumFieldDecorator` and `io.appium.java_client.pagefactory.AppiumElementLocator` which use `io.appium.java_client.pagefactory.TimeOutDuration` as a parameter became deprecated. Use new constructors which use `java.time.Duration`. -- **[DEPENDENCY UPDATES]** - - `org.seleniumhq.selenium:selenium-java` was updated to 3.11.0 - -*6.0.0-BETA4* -- **[ENHANCEMENT]** Added handler for isDispalyed in W3C mode. [#833](https://github.com/appium/java-client/pull/833) -- **[ENHANCEMENT]** Added handlers for sending SMS, making GSM Call, setting GSM signal, voice, power capacity and power AC. [#834](https://github.com/appium/java-client/pull/834) -- **[ENHANCEMENT]** Added handlers for toggling wifi, airplane mode and data in android. [#835](https://github.com/appium/java-client/pull/835) -- **[DEPENDENCY UPDATES]** - - `org.apache.httpcomponents:httpclient` was updated to 4.5.5 - - `cglib:cglib` was updated to 3.2.6 - - `org.springframework:spring-context` was updated to 5.0.3.RELEASE - -*6.0.0-BETA3* -- **[DEPENDENCY UPDATES]** - - `org.seleniumhq.selenium:selenium-java` was updated to 3.9.1 -- **[BREAKING CHANGE]** Removal of deprecated listener-methods from the AlertEventListener. [#797](https://github.com/appium/java-client/pull/797) -- **[BUG FIX]**. Fix the `pushFile` command. [#812](https://github.com/appium/java-client/pull/812) [#816](https://github.com/appium/java-client/pull/816) -- **[ENHANCEMENT]**. Implemented custom command codec. [#817](https://github.com/appium/java-client/pull/817), [#825](https://github.com/appium/java-client/pull/825) -- **[ENHANCEMENT]** Added handlers for lock/unlock in iOS. [#799](https://github.com/appium/java-client/pull/799) -- **[ENHANCEMENT]** AddEd endpoints for screen recording API for iOS and Android. [#814](https://github.com/appium/java-client/pull/814) -- **[MAJOR ENHANCEMENT]** W3C compliance was provided. [#829](https://github.com/appium/java-client/pull/829) -- **[ENHANCEMENT]** New capability `MobileCapabilityType.FORCE_MJSONWP` [#829](https://github.com/appium/java-client/pull/829) -- **[ENHANCEMENT]** Updated applications management endpoints. [#824](https://github.com/appium/java-client/pull/824) - -*6.0.0-BETA2* -- **[ENHANCEMENT]** The `fingerPrint` ability was added. It is supported by Android for now. [#473](https://github.com/appium/java-client/pull/473) [#786](https://github.com/appium/java-client/pull/786) -- **[BUG FIX]**. Less strict verification of the `PointOption`. [#795](https://github.com/appium/java-client/pull/795) - -*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 - - 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) -- **[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) -- **[DEPENDENCY UPDATES]** - - `org.apache.commons:commons-lang3` was updated to 3.7 - - `commons-io:commons-io` was updated to 2.6 - - `org.springframework:spring-context` was updated to 5.0.2.RELEASE - - `org.aspectj:aspectjweaver` was updated to 1.8.13 - - `org.seleniumhq.selenium:selenium-java` was updated to 3.7.1 - -*5.0.4* -- **[BUG FIX]**. Client was crashing when user was testing iOS with server 1.7.0. Report: [#732](https://github.com/appium/java-client/issues/732). Fix: [#733](https://github.com/appium/java-client/pull/733). -- **[REFACTOR]** **[BREAKING CHANGE]** Excessive invocation of the implicit waiting timeout was removed. This is the breaking change because API of `AppiumElementLocator` and `AppiumElementLocatorFactory` was changed. Request: [#735](https://github.com/appium/java-client/issues/735), FIXES: [#738](https://github.com/appium/java-client/pull/738), [#741](https://github.com/appium/java-client/pull/741) -- **[DEPENDENCY UPDATES]** - - org.seleniumhq.selenium:selenium-java to 3.6.0 - - com.google.code.gson:gson to 2.8.2 - - org.springframework:spring-context to 5.0.0.RELEASE - - org.aspectj:aspectjweaver to 1.8.11 - -*5.0.3* -- **[BUG FIX]** Selenuim version was reverted from boundaries to the single number. Issue report: [#718](https://github.com/appium/java-client/issues/718). FIX: [#722](https://github.com/appium/java-client/pull/722) -- **[ENHANCEMENT]** The `pushFile` was added to IOSDriver. Feature request: [#720](https://github.com/appium/java-client/issues/720). Implementation: [#721](https://github.com/appium/java-client/pull/721). This feature requires appium node server v>=1.7.0 - -*5.0.2* **[BUG FIX RELEASE]** -- **[BUG FIX]** Dependency conflict resolving. The report: [#714](https://github.com/appium/java-client/issues/714). The fix: [#717](https://github.com/appium/java-client/pull/717). This change may affect users who use htmlunit-driver and/or phantomjsdriver. At this case it is necessary to add it to dependency list and to exclude old selenium versions. - -*5.0.1* **[BUG FIX RELEASE]** -- **[BUG FIX]** The fix of the element genering on iOS was fixed. Issue report: [#704](https://github.com/appium/java-client/issues/704). Fix: [#705](https://github.com/appium/java-client/pull/705) - -*5.0.0* -- **[REFACTOR]** **[BREAKING CHANGE]** 5.0.0 finalization. Removal of obsolete code. [#660](https://github.com/appium/java-client/pull/660) -- **[ENHANCEMENT]** Enable nativeWebTap setting for iOS. [#658](https://github.com/appium/java-client/pull/658) -- **[ENHANCEMENT]** The `getCurrentPackage` was added. [#657](https://github.com/appium/java-client/pull/657) -- **[ENHANCEMENT]** The `toggleTouchIDEnrollment` was added. [#659](https://github.com/appium/java-client/pull/659) -- **[BUG FIX]** The clearing of existing actions/parameters after perform is invoked. [#663](https://github.com/appium/java-client/pull/663) -- **[BUG FIX]** [#669](https://github.com/appium/java-client/pull/669) missed parameters of the `OverrideWidget` were added: - - `iOSXCUITAutomation` - - `windowsAutomation` -- **[BUG FIX]** ByAll was re-implemented. [#680](https://github.com/appium/java-client/pull/680) -- **[BUG FIX]** **[BREAKING CHANGE]** The issue of compliance with Selenium grid 3.x was fixed. This change is breaking because now java_client is compatible with appiun server v>=1.6.5. Issue report [#655](https://github.com/appium/java-client/issues/655). FIX [#682](https://github.com/appium/java-client/pull/682) -- **[BUG FIX]** issues related to latest Selenium changes were fixed. Issue report [#696](https://github.com/appium/java-client/issues/696). Fix: [#699](https://github.com/appium/java-client/pull/699). -- **[UPDATE]** Dependency update - - `selenium-java` was updated to 3.5.x - - `org.apache.commons-lang3` was updated to 3.6 - - `org.springframework.spring-context` was updated to 4.3.10.RELEASE -- **[ENHANCEMENT]** Update of the touch ID enroll method. The older `PerformsTouchID#toggleTouchIDEnrollment` was marked `Deprecated`. -It is recoomended to use `PerformsTouchID#toggleTouchIDEnrollment(boolean)` instead. [#695](https://github.com/appium/java-client/pull/695) - - -*5.0.0-BETA9* -- **[ENHANCEMENT]** Page factory: Mixed locator strategies were implemented. Feature request:[#565](https://github.com/appium/java-client/issues/565) Implementation: [#646](https://github.com/appium/java-client/pull/646) -- **[DEPRECATED]** All the content of the `io.appium.java_client.youiengine` package was marked `Deprecated`. It is going to be removed. [#652](https://github.com/appium/java-client/pull/652) -- **[UPDATE]** Update of the `com.google.code.gson:gson` to v2.8.1. - -*5.0.0-BETA8* -- **[ENHANCEMENT]** Page factory classes became which had package visibility are `public` now. [#630](https://github.com/appium/java-client/pull/630) - - `io.appium.java_client.pagefactory.AppiumElementLocatorFactory` - - `io.appium.java_client.pagefactory.DefaultElementByBuilder` - - `io.appium.java_client.pagefactory.WidgetByBuilder` - -- **[ENHANCEMENT]** New capabilities were added [#626](https://github.com/appium/java-client/pull/626): - - `AndroidMobileCapabilityType#AUTO_GRANT_PERMISSIONS` - - `AndroidMobileCapabilityType#ANDROID_NATURAL_ORIENTATION` - - `IOSMobileCapabilityType#XCODE_ORG_ID` - - `IOSMobileCapabilityType#XCODE_SIGNING_ID` - - `IOSMobileCapabilityType#UPDATE_WDA_BUNDLEID` - - `IOSMobileCapabilityType#RESET_ON_SESSION_START_ONLY` - - `IOSMobileCapabilityType#COMMAND_TIMEOUTS` - - `IOSMobileCapabilityType#WDA_STARTUP_RETRIES` - - `IOSMobileCapabilityType#WDA_STARTUP_RETRY_INTERVAL` - - `IOSMobileCapabilityType#CONNECT_HARDWARE_KEYBOARD` - - `IOSMobileCapabilityType#MAX_TYPING_FREQUENCY` - - `IOSMobileCapabilityType#SIMPLE_ISVISIBLE_CHECK` - - `IOSMobileCapabilityType#USE_CARTHAGE_SSL` - - `IOSMobileCapabilityType#SHOULD_USE_SINGLETON_TESTMANAGER` - - `IOSMobileCapabilityType#START_IWDP` - - `IOSMobileCapabilityType#ALLOW_TOUCHID_ENROLL` - - `MobileCapabilityType#EVENT_TIMINGS` - -- **[UPDATE]** Dependencies were updated: - - `org.seleniumhq.selenium:selenium-java` was updated to 3.4.0 - - `cglib:cglib` was updated to 3.2.5 - - `org.apache.httpcomponents:httpclient` was updated to 4.5.3 - - `commons-validator:commons-validator` was updated to 1.6 - - `org.springframework:spring-context` was updated to 4.3.8.RELEASE - - -*5.0.0-BETA7* -- **[ENHANCEMENT]** The ability to customize the polling strategy of the waiting was provided. [#612](https://github.com/appium/java-client/pull/612) -- **[ENHANCEMENT]** **[REFACTOR]** Methods which were representing time deltas instead of elementary types became `Deprecated`. Methods which use `java.time.Duration` are suugested to be used. [#611](https://github.com/appium/java-client/pull/611) -- **[ENHANCEMENT]** The ability to calculate screenshots overlap was included. [#595](https://github.com/appium/java-client/pull/595). - - -*5.0.0-BETA6* -- **[UPDATE]** Update to Selenium 3.3.1 -- **[ENHANCEMENT]** iOS XCUIT mode automation: API to run application in background was added. [#593](https://github.com/appium/java-client/pull/593) -- **[BUG FIX]** Issue report: [#594](https://github.com/appium/java-client/issues/594). FIX: [#597](https://github.com/appium/java-client/pull/597) -- **[ENHANCEMENT]** The class chain locator was added. [#599](https://github.com/appium/java-client/pull/599) - - -*5.0.0-BETA5* -- **[UPDATE]** Update to Selenium 3.2.0 -- **[BUG FIX]** Excessive dependency on `guava` was removed. It causes errors. Issue report: [#588](https://github.com/appium/java-client/issues/588). FIX: [#589](https://github.com/appium/java-client/pull/589). -- **[ENHANCEMENT]**. The capability `io.appium.java_client.remote.AndroidMobileCapabilityType#SYSTEM_PORT` was added. [#591](https://github.com/appium/java-client/pull/591) - -*5.0.0-BETA4* -- **[ENHANCEMENT]** Android. API to read the performance data was added. [#562](https://github.com/appium/java-client/pull/562) -- **[REFACTOR]** Android. Simplified the activity starting by reducing the number of parameters through POJO clas. Old methods which start activities were marked `@Deprecated`. [#579](https://github.com/appium/java-client/pull/579) [#585](https://github.com/appium/java-client/pull/585) -- **[BUG FIX]** Issue report:[#574](https://github.com/appium/java-client/issues/574). Fix:[#582](https://github.com/appium/java-client/pull/582) - -*5.0.0-BETA3* -[BUG FIX] -- **[BUG FIX]**:Issue report: [#567](https://github.com/appium/java-client/issues/567). Fix: [#568](https://github.com/appium/java-client/pull/568) - -*5.0.0-BETA2* -- **[BUG FIX]**:Issue report: [#549](https://github.com/appium/java-client/issues/549). Fix: [#551](https://github.com/appium/java-client/pull/551) -- New capabilities were added [#533](https://github.com/appium/java-client/pull/553): - - `IOSMobileCapabilityType#USE_NEW_WDA` - - `IOSMobileCapabilityType#WDA_LAUNCH_TIMEOUT` - - `IOSMobileCapabilityType#WDA_CONNECTION_TIMEOUT` - -The capability `IOSMobileCapabilityType#REAL_DEVICE_LOGGER` was removed. [#533](https://github.com/appium/java-client/pull/553) - -- **[BUG FIX]/[ENHANCEMENT]**. Issue report: [#552](https://github.com/appium/java-client/issues/552). FIX [#556](https://github.com/appium/java-client/pull/556) - - Additional methods were added to the `io.appium.java_client.HasSessionDetails` - - `String getPlatformName()` - - `String getAutomationName()` - - `boolean isBrowser()` - - `io.appium.java_client.HasSessionDetails` is used by the ` io.appium.java_client.internal.JsonToMobileElementConverter ` to define which instance of the `org.openqa.selenium.WebElement` subclass should be created. - -- **[ENHANCEMENT]**: The additional event firing feature. PR: [#559](https://github.com/appium/java-client/pull/559). The [WIKI chapter about the event firing](https://github.com/appium/java-client/blob/master/docs/The-event_firing.md) was updated. - -*5.0.0-BETA1* -- **[MAJOR ENHANCEMENT]**: Migration to Java 8. Epic: [#399](https://github.com/appium/java-client/issues/399) - - API with default implementation. PR [#470](https://github.com/appium/java-client/pull/470) - - Tools that provide _Page Object_ engines were redesigned. The migration to [repeatable annotations](http://docs.oracle.com/javase/tutorial/java/annotations/repeating.html). Details you can read there: [#497](https://github.com/appium/java-client/pull/497). [Documentation was synced as well](https://github.com/appium/java-client/blob/master/docs/Page-objects.md#also-it-is-possible-to-define-chained-or-any-possible-locators). - - The new functional interface `io.appium.java_client.functions.AppiumFunctio`n was designed. It extends `java.util.function.Function` and `com.google.common.base.Function`. It was designed in order to provide compatibility with the `org.openqa.selenium.support.ui.Wait` [#543](https://github.com/appium/java-client/pull/543) - - The new functional interface `io.appium.java_client.functions.ExpectedCondition` was designed. It extends `io.appium.java_client.functions.AppiumFunction` and ```org.openqa.selenium.support.ui.ExpectedCondition```. [#543](https://github.com/appium/java-client/pull/543) - - The new functional interface `io.appium.java_client.functions.ActionSupplier` was designed. It extends ```java.util.function.Supplier```. [#543](https://github.com/appium/java-client/pull/543) - -- **[MAJOR ENHANCEMENT]**: Migration from Maven to Gradle. Feature request is [#214](https://github.com/appium/java-client/issues/214). Fixes: [#442](https://github.com/appium/java-client/pull/442), [#465](https://github.com/appium/java-client/pull/465). - -- **[MAJOR ENHANCEMENT]** **[MAJOR REFACTORING]**. Non-abstract **AppiumDriver**: - - Now the `io.appium.java_client.AppiumDriver` can use an instance of any `io.appium.java_client.MobileBy` subclass for the searching. It should work as expected when current session supports the given selector. It will throw `org.openqa.selenium.WebDriverException` otherwise. [#462](https://github.com/appium/java-client/pull/462) - - The new interface `io.appium.java_client.FindsByFluentSelector` was added. [#462](https://github.com/appium/java-client/pull/462) - - API was redesigned: - - these interfaces were marked deprecated and they are going to be removed [#513](https://github.com/appium/java-client/pull/513)[#514](https://github.com/appium/java-client/pull/514): - - `io.appium.java_client.DeviceActionShortcuts` - - `io.appium.java_client.android.AndroidDeviceActionShortcuts` - - `io.appium.java_client.ios.IOSDeviceActionShortcuts` - - instead following inerfaces were designed: - - `io.appium.java_client.HasDeviceTime` - - `io.appium.java_client.HidesKeyboard` - - `io.appium.java_client.HidesKeyboardWithKeyName` - - `io.appium.java_client.PressesKeyCode` - - `io.appium.java_client.ios.ShakesDevice` - - `io.appium.java_client.HasSessionDetails` - _That was done because Windows automation tools have some features that were considered as Android-specific and iOS-specific._ - - The list of classes and methods which were marked _deprecated_ and they are going to be removed - - `AppiumDriver#swipe(int, int, int, int, int)` - - `AppiumDriver#pinch(WebElement)` - - `AppiumDriver#pinch(int, int)` - - `AppiumDriver#zoom(WebElement)` - - `AppiumDriver#zoom(int, int)` - - `AppiumDriver#tap(int, WebElement, int)` - - `AppiumDriver#tap(int, int, int, int)` - - `AppiumDriver#swipe(int, int, int, int, int)` - - `MobileElement#swipe(SwipeElementDirection, int)` - - `MobileElement#swipe(SwipeElementDirection, int, int, int)` - - `MobileElement#zoom()` - - `MobileElement#pinch()` - - `MobileElement#tap(int, int)` - - `io.appium.java_client.SwipeElementDirection` and `io.appium.java_client.TouchebleElement` also were marked deprecated. - - redesign of `TouchAction` and `MultiTouchAction` - - constructors were redesigned. There is no strict binding of `AppiumDriver` and `TouchAction` /`MultiTouchAction`. They can consume any instance of a class that implements `PerformsTouchActions`. - - `io.appium.java_client.ios.IOSTouchAction` was added. It extends `io.appium.java_client.TouchAction`. - - the new interface `io.appium.java_client.PerformsActions` was added. It unifies `TouchAction` and `MultiTouchAction` now. [#543](https://github.com/appium/java-client/pull/543) - - `JsonToMobileElementConverter` re-design [#532](https://github.com/appium/java-client/pull/532): - - unused `MobileElementToJsonConverter` was removed - - `JsonToMobileElementConverter` is not rhe abstract class now. It generates instances of MobileElement subclasses according to current session parameters - - `JsonToAndroidElementConverter` is deprecated now - - `JsonToIOSElementConverter` is depreacated now - - `JsonToYouiEngineElementConverter` is deprecated now. - - constructors of 'AppiumDriver' were re-designed. - - constructors of 'AndroidDriver' were re-designed. - - constructors of 'IOSDriver' were re-designed. - -- **[MAJOR ENHANCEMENT]** Windows automation. Epic [#471](https://github.com/appium/java-client/issues/471) - - The new interface `io.appium.java_client.FindsByWindowsAutomation` was added. [#462](https://github.com/appium/java-client/pull/462). With [@jonstoneman](https://github.com/jonstoneman) 's authorship. - - The new selector strategy `io.appium.java_client.MobileBy.ByWindowsAutomation` was added. [#462](https://github.com/appium/java-client/pull/462). With [@jonstoneman](https://github.com/jonstoneman) 's authorship. - - `io.appium.java_client.windows.WindowsDriver` was designed. [#538](https://github.com/appium/java-client/pull/538) - - `io.appium.java_client.windows.WindowsElement` was designed. [#538](https://github.com/appium/java-client/pull/538) - - `io.appium.java_client.windows.WindowsKeyCode ` was added. [#538](https://github.com/appium/java-client/pull/538) - - Page object tools were updated [#538](https://github.com/appium/java-client/pull/538) - - the `io.appium.java_client.pagefactory.WindowsFindBy` annotation was added. - - `io.appium.java_client.pagefactory.AppiumFieldDecorator` and supporting tools were actualized. - -- **[MAJOR ENHANCEMENT]** iOS XCUIT mode automation: - - `io.appium.java_client.remote.AutomationName#IOS_XCUI_TEST` was added - - The new interface `io.appium.java_client.FindsByIosNSPredicate` was added. [#462](https://github.com/appium/java-client/pull/462). With [@rafael-chavez](https://github.com/rafael-chavez) 's authorship. It is implemented by `io.appium.java_client.ios.IOSDriver` and `io.appium.java_client.ios.IOSElement`. - - The new selector strategy `io.appium.java_client.MobileBy.ByIosNsPredicate` was added. [#462](https://github.com/appium/java-client/pull/462). With [@rafael-chavez](https://github.com/rafael-chavez) 's authorship. - - Page object tools were updated [#545](https://github.com/appium/java-client/pull/545), [#546](https://github.com/appium/java-client/pull/546) - - the `io.appium.java_client.pagefactory.iOSXCUITFindBy` annotation was added. - - `io.appium.java_client.pagefactory.AppiumFieldDecorator` and supporting tools were actualized. - -- [ENHANCEMENT] Added the ability to set UiAutomator Congfigurator values. [#410](https://github.com/appium/java-client/pull/410). -[#477](https://github.com/appium/java-client/pull/477). -- [ENHANCEMENT]. Additional methods which perform device rotation were implemented. [#489](https://github.com/appium/java-client/pull/489). [#439](https://github.com/appium/java-client/pull/439). But it works for iOS in XCUIT mode and for Android in UIAutomator2 mode only. The feature request: [#7131](https://github.com/appium/appium/issues/7131) -- [ENHANCEMENT]. TouchID Implementation (iOS Sim Only). Details: [#509](https://github.com/appium/java-client/pull/509) -- [ENHANCEMENT]. The ability to use port, ip and log file as server arguments was provided. Feature request: [#521](https://github.com/appium/java-client/issues/521). Fixes: [#522](https://github.com/appium/java-client/issues/522), [#524](https://github.com/appium/java-client/issues/524). -- [ENHANCEMENT]. The new interface ```io.appium.java_client.android.HasDeviceDetails``` was added. It is implemented by ```io.appium.java_client.android.AndroidDriver``` by default. [#518](https://github.com/appium/java-client/pull/518) -- [ENHANCEMENT]. New touch actions were added. ```io.appium.java_client.ios.IOSTouchAction#doubleTap(WebElement, int, int)``` and ```io.appium.java_client.ios.IOSTouchAction#doubleTap(WebElement)```. [#523](https://github.com/appium/java-client/pull/523), [#444](https://github.com/appium/java-client/pull/444) -- [ENHANCEMENT]. All constructors declared by `io.appium.java_client.AppiumDriver` are public now. -- [BUG FIX]: There was the issue when "@WithTimeout" was changing general timeout of the waiting for elements. Bug report: [#467](https://github.com/appium/java-client/issues/467). Fixes: [#468](https://github.com/appium/java-client/issues/468), [#469](https://github.com/appium/java-client/issues/469), [#480](https://github.com/appium/java-client/issues/480). Read: [supported-settings](https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md#supported-settings) -- Added the server flag `io.appium.java_client.service.local.flags.AndroidServerFlag#REBOOT`. [#476](https://github.com/appium/java-client/pull/476) -- Added `io.appium.java_client.remote.AndroidMobileCapabilityType.APP_WAIT_DURATION ` capability. [#461](https://github.com/appium/java-client/pull/461) -- the new automation type `io.appium.java_client.remote.MobilePlatform#ANDROID_UIAUTOMATOR2` was add. -- the new automation type `io.appium.java_client.remote.MobilePlatform#YOUI_ENGINE` was add. -- Additional capabilities were addede: - - `IOSMobileCapabilityType#CUSTOM_SSL_CERT` - - `IOSMobileCapabilityType#TAP_WITH_SHORT_PRESS_DURATION` - - `IOSMobileCapabilityType#SCALE_FACTOR` - - `IOSMobileCapabilityType#WDA_LOCAL_PORT` - - `IOSMobileCapabilityType#SHOW_XCODE_LOG` - - `IOSMobileCapabilityType#REAL_DEVICE_LOGGER` - - `IOSMobileCapabilityType#IOS_INSTALL_PAUSE` - - `IOSMobileCapabilityType#XCODE_CONFIG_FILE` - - `IOSMobileCapabilityType#KEYCHAIN_PASSWORD` - - `IOSMobileCapabilityType#USE_PREBUILT_WDA` - - `IOSMobileCapabilityType#PREVENT_WDAATTACHMENTS` - - `IOSMobileCapabilityType#WEB_DRIVER_AGENT_URL` - - `IOSMobileCapabilityType#KEYCHAIN_PATH` - - `MobileCapabilityType#CLEAR_SYSTEM_FILES` -- **[UPDATE]** to Selenium 3.0.1. -- **[UPDATE]** to Spring Framework 4.3.5.RELEASE. -- **[UPDATE]** to AspectJ weaver 1.8.10. - - - -*4.1.2* - -- Following capabilities were added: - - `io.appium.java_client.remote.AndroidMobileCapabilityType.ANDROID_INSTALL_TIMEOUT` - - `io.appium.java_client.remote.AndroidMobileCapabilityType.NATIVE_WEB_SCREENSHOT` - - `io.appium.java_client.remote.AndroidMobileCapabilityType.ANDROID_SCREENSHOT_PATH`. The pull request: [#452](https://github.com/appium/java-client/pull/452) -- `org.openqa.selenium.Alert` was reimplemented for iOS. Details: [#459](https://github.com/appium/java-client/pull/459) -- The deprecated `io.appium.java_client.generic.searchcontext` was removed. -- The dependency on `com.google.code.gson` was updated to 2.7. Also it was adde to exclusions -for `org.seleniumhq.selenium` `selenium-java`. -- The new AutomationName was added. IOS_XCUI_TEST. It is needed for the further development. -- The new MobilePlatform was added. WINDOWS. It is needed for the further development. - -*4.1.1* - -BUG FIX: Issue [#450](https://github.com/appium/java-client/issues/450). Fix: [#451](https://github.com/appium/java-client/issues/451). Thanks to [@tutunang](https://github.com/appium/java-client/pull/451) for the report. - -*4.1.0* -- all code marked `@Deprecated` was removed. -- `getSessionDetails()` was added. Thanks to [@saikrishna321](https://github.com/saikrishna321) for the contribution. -- FIX [#362](https://github.com/appium/java-client/issues/362), [#220](https://github.com/appium/java-client/issues/220), [#323](https://github.com/appium/java-client/issues/323). Details read there: [#413](https://github.com/appium/java-client/pull/413) -- FIX [#392](https://github.com/appium/java-client/issues/392). Thanks to [@truebit](https://github.com/truebit) for the bug report. -- The dependency on `cglib` was replaced by the dependency on `cglib-nodep`. FIX [#418](https://github.com/appium/java-client/issues/418) -- The casting to the weaker interface `HasIdentity` instead of class `RemoteWebElement` was added. It is the internal refactoring of the `TouchAction`. [#432](https://github.com/appium/java-client/pull/432). Thanks to [@asolntsev](https://github.com/asolntsev) for the contribution. -- The `setValue` method was moved to `MobileElement`. It works against text input elements on Android. -- The dependency on `org.springframework` `spring-context` v`4.3.2.RELEASE` was added -- The dependency on `org.aspectj` `aspectjweaver` v`1.8.9` was added -- ENHANCEMENT: The alternative event firing engine. The feature request: [#242](https://github.com/appium/java-client/issues/242). -Implementation: [#437](https://github.com/appium/java-client/pull/437). Also [new WIKI chapter](https://github.com/appium/java-client/blob/master/docs/The-event_firing.md) was added. -- ENHANCEMENT: Convenient access to specific commands for each supported mobile OS. Details: [#445](https://github.com/appium/java-client/pull/445) -- dependencies and plugins were updated -- ENHANCEMENT: `YouiEngineDriver` was added. Details: [appium server #6215](https://github.com/appium/appium/pull/6215), [#429](https://github.com/appium/java-client/pull/429), [#448](https://github.com/appium/java-client/pull/448). It is just the draft of the new solution that is going to be extended further. Please stay tuned. There are many interesting things are coming up. Thanks to `You I Engine` team for the contribution. - -*4.0.0* -- all code marked `@Deprecated` was removed. Java client won't support old servers (v<1.5.0) -anymore. -- the ability to start an activity using Android intent actions, intent categories, flags and arguments -was added to `AndroidDriver`. Thanks to [@saikrishna321](https://github.com/saikrishna321) for the contribution. -- `scrollTo()` and `scrollToExact()` became deprecated. They are going to be removed in the next release. -- The interface `io.appium.java_client.ios.GetsNamedTextField` and the declared method `T getNamedTextField(String name)` are -deprecated as well. They are going to be removed in the next release. -- Methods `findElements(String by, String using)` and `findElement(String by, String using)` of `org.openga.selenium.remote.RemoteWebdriver` are public now. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget). -- the `io.appium.java_client.NetworkConnectionSetting` class was marked deprecated -- the enum `io.appium.java_client.android.Connection` was added. All supported network bitmasks are defined there. -- Android. Old methods which get/set connection were marked `@Deprecated` -- Android. New methods which consume/return `io.appium.java_client.android.Connection` were added. -- the `commandRepository` field is public now. The modification of the `MobileCommand` -- Constructors like `AppiumDriver(HttpCommandExecutor executor, Capabilities capabilities)` were added to -`io.appium.java_client.android.AndroidDriver` and `io.appium.java_client.ios.IOSDriver` -- The refactoring of `io.appium.java_client.internal.JsonToMobileElementConverter`. Now it accepts -`org.openqa.selenium.remote.RemoteWebDriver` as the constructor parameter. It is possible to re-use -`io.appium.java_client.android.internal.JsonToAndroidElementConverter` or -`io.appium.java_client.ios.internal.JsonToIOSElementConverter` by RemoteWebDriver when it is needed. -- Constructors of the abstract `io.appium.java_client.AppiumDriver` were redesigned. Now they require -a subclass of `io.appium.java_client.internal.JsonToMobileElementConverter`. Constructors of -`io.appium.java_client.android.AndroidDriver` and `io.appium.java_client.ios.IOSDriver` are same still. -- The `pushFile(String remotePath, File file)` was added to AndroidDriver -- FIX of TouchAction. Instances of the TouchAction class are reusable now -- FIX of the swiping issue (iOS, server version >= 1.5.0). Now the swiping is implemented differently by -AndroidDriver and IOSDriver. Thanks to [@truebit](https://github.com/truebit) and [@nuggit32](https://github.com/nuggit32) for the catching. -- the project was integrated with [maven-checkstyle-plugin](https://maven.apache.org/plugins/maven-checkstyle-plugin/). Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the work -- source code was improved according to code style checking rules. -- the integration with `org.owasp dependency-check-maven` was added. Thanks to [@saikrishna321](https://github.com/saikrishna321) -for the work. -- the integration with `org.jacoco jacoco-maven-plugin` was added. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the contribution. - -*3.4.1* -- Update to Selenium v2.53.0 -- all dependencies were updated to latest versions -- the dependency on org.apache.commons commons-lang3 v3.4 was added -- the fix of Widget method invocation.[#340](https://github.com/appium/java-client/issues/340). A class visibility was taken into account. Thanks to [aznime](https://github.com/aznime) for the catching. -Server flags were added: - - GeneralServerFlag.ASYNC_TRACE - - IOSServerFlag.WEBKIT_DEBUG_PROXY_PORT -- Source code was formatted using [eclipse-java-google-style.xml](https://google-styleguide.googlecode.com/svn/trunk/eclipse-java-google-style.xml). This is not the complete solution. The code style checking is going to be added further. Thanks to [SrinivasanTarget](https://github.com/SrinivasanTarget) for the work! - -*3.4.0* -- Update to Selenium v2.52.0 -- `getAppStrings()` methods are deprecated now. They are going to be removed. `getAppStringMap()` methods were added and now return a map with app strings (keys and values) -instead of a string. Thanks to [@rgonalo](https://github.com/rgonalo) for the contribution. -- Add `getAppStringMap(String language, String stringFile)` method to allow searching app strings in the specified file -- FIXED of the bug which causes deadlocks of AppiumDriver LocalService in multithreading. Thanks to [saikrishna321](https://github.com/saikrishna321) for the [bug report](https://github.com/appium/java-client/issues/283). -- FIXED Zoom methods, thanks to [@kkhaidukov](https://github.com/kkhaidukov) -- FIXED The issue of compatibility of AppiumServiceBuilder with Appium node server v >= 1.5.x. Take a look at [#305](https://github.com/appium/java-client/issues/305) -- `getDeviceTime()` was added. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the contribution. -- FIXED `longPressKeyCode()` methods. Now they use the convenient JSONWP command.Thanks to [@kirillbilchenko](https://github.com/kirillbilchenko) for the proposed fix. -- FIXED javadoc. -- Page object tools were updated. Details read here: [#311](https://github.com/appium/java-client/issues/311), [#313](https://github.com/appium/java-client/pull/313), [#317](https://github.com/appium/java-client/pull/317). By.name locator strategy is deprecated for Android and iOS. It is still valid for the Selendroid mode. Thanks to [@SrinivasanTarget](https://github.com/SrinivasanTarget) for the helping. -- The method `lockScreen(seconds)` is deprecated and it is going to be removed in the next release. Since Appium node server v1.5.x it is recommended to use -`AndroidDriver.lockDevice()...AndroidDriver.unlockDevice()` or `IOSDriver.lockDevice(int seconds)` instead. Thanks to [@namannigam](https://github.com/namannigam) for -the catching. Read [#315](https://github.com/appium/java-client/issues/315) -- `maven-release-plugin` was added to POM.XML configuration -- [#320](https://github.com/appium/java-client/issues/320) fix. The `Widget.getSelfReference()` was added. This method allows to extract a real widget-object from inside a proxy at some extraordinary situations. Read: [PR](https://github.com/appium/java-client/pull/327). Thanks to [SergeyErmakovMercDev](https://github.com/SergeyErmakovMercDev) for the reporting. -- all capabilities were added according to [this description](https://github.com/appium/appium/blob/1.5/docs/en/writing-running-appium/caps.md). There are three classes: `io.appium.java_client.remote.MobileCapabilityType` (just modified), `io.appium.java_client.remote.AndroidMobileCapabilityType` (android-specific capabilities), `io.appium.java_client.remote.IOSMobileCapabilityType` (iOS-specific capabilities). Details are here: [#326](https://github.com/appium/java-client/pull/326) -- some server flags were marked `deprecated` because they are deprecated since server node v1.5.x. These flags are going to be removed at the java client release. Details are here: [#326](https://github.com/appium/java-client/pull/326) -- The ability to start Appium node programmatically using desired capabilities. This feature is compatible with Appium node server v >= 1.5.x. Details are here: [#326](https://github.com/appium/java-client/pull/326) - -*3.3.0* -- updated the dependency on Selenium to version 2.48.2 -- bug fix and enhancements of io.appium.java_client.service.local.AppiumDriverLocalService - - FIXED bug which was found and reproduced with Eclipse for Mac OS X. Please read about details here: [#252](https://github.com/appium/java-client/issues/252) - Thanks to [saikrishna321](https://github.com/saikrishna321) for the bug report - - FIXED bug which was found out by [Jonahss](https://github.com/Jonahss). Thanks for the reporting. Details: [#272](https://github.com/appium/java-client/issues/272) - and [#273](https://github.com/appium/java-client/issues/273) - - For starting an appium server using localService, added additional environment variable to specify the location of Node.js binary: NODE_BINARY_PATH - - The ability to set additional output streams was provided -- The additional __startActivity()__ method was added to AndroidDriver. It allows to start activities without the stopping of a target app -Thanks to [deadmoto](https://github.com/deadmoto) for the contribution -- The additional extension of the Page Object design pattern was designed. Please read about details here: [#267](https://github.com/appium/java-client/pull/267) -- New public constructors to AndroidDriver/IOSDriver that allow passing a custom HttpClient.Factory Details: [#276](https://github.com/appium/java-client/pull/278) thanks to [baechul](https://github.com/baechul) - -*3.2.0* -- updated the dependency on Selenium to version 2.47.1 -- the new dependency on commons-validator v1.4.1 -- the ability to start programmatically/silently an Appium node server is provided now. Details please read at [#240](https://github.com/appium/java-client/pull/240). -Historical reference: [The similar solution](https://github.com/Genium-Framework/Appium-Support) has been designed by [@Hassan-Radi](https://github.com/Hassan-Radi). -The mentioned framework and the current solution use different approaches. -- Throwing declarations were added to some searching methods. The __"getMouse"__ method of RemoteWebDriver was marked __Deprecated__ -- Add `replaceValue` method for elements. -- Replace `sendKeyEvent()` method in android with pressKeyCode(int key) and added: pressKeyCode(int key, Integer metastate), longPressKeyCode(int key), longPressKeyCode(int key, Integer metastate) - -*3.1.1* -- Page-object findBy strategies are now aware of which driver (iOS or Android) you are using. For more details see the Pull Request: https://github.com/appium/java-client/pull/213 -- If somebody desires to use their own Webdriver implementation then it has to implement HasCapabilities. -- Added a new annotation: `WithTimeout`. This annotation allows one to specify a specific timeout for finding an element which overrides the drivers default timeout. For more info see: https://github.com/appium/java-client/pull/210 -- Corrected an uninformative Exception message. - -*3.0.0* -- AppiumDriver class is now a Generic. This allows us to return elements of class MobileElement (and its subclasses) instead of always returning WebElements and requiring users to cast to MobileElement. See https://github.com/appium/java-client/pull/182 -- Full set of Android KeyEvents added. -- Selenium client version updated to 2.46 -- PageObject enhancements -- Junit dependency removed - -*2.2.0* -- Added new TouchAction methods for LongPress, on an element, at x,y coordinates, or at an offset from within an element -- SwipeElementDirection changed. Read the documentation, it's now smarter about how/where to swipe -- Added APPIUM_VERSION MobileCapabilityType -- `sendKeyEvent()` moved from AppiumDriver to AndroidDriver -- `linkText` and `partialLinkText` locators added -- setValue() moved from MobileElement to iOSElement -- Fixed Selendroid PageAnnotations - -*2.1.0* -- Moved hasAppString() from AndroidDriver to AppiumDriver -- Fixes to PageFactory -- Added @AndroidFindAll and @iOSFindAll -- Added toggleLocationServices() to AndroidDriver -- Added touchAction methods to MobileElement, so now you can do `element.pinch()`, `element.zoom()`, etc. -- Added the ability to choose a direction to swipe over an element. Use the `SwipeElementDirection` enums: `UP, DOWN, LEFT, RIGHT` - -*2.0.0* -- AppiumDriver is now an abstract class, use IOSDriver and AndroidDriver which both extend it. You no longer need to include the `PLATFORM_NAME` desired capability since it's automatic for each class. Thanks to @TikhomirovSergey for all their work -- ScrollTo() and ScrollToExact() methods reimplemented -- Zoom() and Pinch() are now a little smarter and less likely to fail if you element is near the edge of the screen. Congratulate @BJap on their first PR! - -*1.7.0* -- Removed `scrollTo()` and `scrollToExact()` methods because they relied on `complexFind()`. They will be added back in the next version! -- Removed `complexFind()` -- Added `startActivity()` method -- Added `isLocked()` method -- Added `getSettings()` and `ignoreUnimportantViews()` methods - -*1.6.2* -- Added MobilePlatform interface (Android, IOS, FirefoxOS) -- Added MobileBrowserType interface (Safari, Browser, Chromium, Chrome) -- Added MobileCapabilityType.APP_WAIT_ACTIVITY -- Fixed small Integer cast issue (in Eclipse it won't compile) -- Set -source and -target of the Java Compiler to 1.7 (for maven compiler plugin) -- Fixed bug in Page Factory - -*1.6.1* -- Fixed the logic for checking connection status on NetworkConnectionSetting objects - -*1.6.0* -- Added @findBy annotations. Explanation here: https://github.com/appium/java-client/pull/68 Thanks to TikhomirovSergey -- Appium Driver now implements LocationContext interface, so setLocation() works for setting GPS coordinates - -*1.5.0* -- Added MobileCapabilityType enums for desired capabilities -- `findElement` and `findElements` return MobileElement objects (still need to be casted, but no longer instantiated) -- new appium v1.2 `hideKeyboard()` strategies added -- `getNetworkConnection()` and `setNetworkConnection()` commands added - -*1.4.0* -- Added openNotifications() method, to open the notifications shade on Android -- Added pullFolder() method, to pull an entire folder as a zip archive from a device/simulator -- Upgraded Selenium dependency to 2.42.2 - -*1.3.0* -- MultiGesture with a single TouchAction fixed for Android -- Now depends upon Selenium java client 2.42.1 -- Cleanup of Errorcode handling, due to merging a change into Selenium - -*1.2.1* -- fix dependency issue - -*1.2.0* -- complexFind() now returns MobileElement objects -- added scrollTo() and scrollToExact() methods for use with complexFind() - -*1.1.0* -- AppiumDriver now implements Rotatable. rotate() and getOrientation() methods added -- when no appium server is running, the proper error is thrown, instead of a NullPointerException - -*1.0.2* -- recompiled to include some missing methods such as shake() and complexFind() + +Visit [CHANGELOG.md](CHANGELOG.md) to see the full list of changes between versions. ## Running tests diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index f158f224d..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,78 +0,0 @@ -# Gradle -# Build your Java project and run tests with Gradle using a Gradle wrapper script. -# Add steps that analyze code, save build artifacts, deploy, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/java - -pool: - vmImage: 'macos-11' - -variables: - ANDROID_EMU_NAME: test - ANDROID_EMU_ABI: x86 - ANDROID_EMU_TARGET: android-28 - ANDROID_EMU_TAG: default - XCODE_VERSION: 13.2 - IOS_PLATFORM_VERSION: 15.2 - IOS_DEVICE_NAME: iPhone X - NODE_VERSION: 16.x - JDK_VERSION: 1.8 - -jobs: -- job: Android_E2E_Tests - steps: - - template: .azure-templates/bootstrap_steps.yml - - script: $NVM_DIR/versions/node/`node --version`/bin/appium driver install uiautomator2 - displayName: Install UIA2 driver - - script: | - echo "y" | $ANDROID_HOME/tools/bin/sdkmanager --install 'system-images;$(ANDROID_EMU_TARGET);$(ANDROID_EMU_TAG);$(ANDROID_EMU_ABI)' - echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -n "$(ANDROID_EMU_NAME)" -k 'system-images;$(ANDROID_EMU_TARGET);$(ANDROID_EMU_TAG);$(ANDROID_EMU_ABI)' --force - echo $ANDROID_HOME/emulator/emulator -list-avds - - echo "Starting emulator" - nohup $ANDROID_HOME/emulator/emulator -avd "$(ANDROID_EMU_NAME)" -no-snapshot -delay-adb > /dev/null 2>&1 & - $ANDROID_HOME/platform-tools/adb wait-for-device - $ANDROID_HOME/platform-tools/adb devices -l - echo "Emulator started" - displayName: Emulator configuration - - task: Gradle@2 - inputs: - gradleWrapperFile: 'gradlew' - gradleOptions: '-Xmx3072m' - javaHomeOption: 'JDKVersion' - jdkVersionOption: "$(JDK_VERSION)" - jdkArchitectureOption: 'x64' - publishJUnitResults: true - tasks: 'build' - options: 'uiAutomationTest -x checkstyleTest -x test' -- job: iOS_E2E_Tests -# timeoutInMinutes: '90' - steps: - - template: .azure-templates/bootstrap_steps.yml - - script: | - sudo xcode-select -s /Applications/Xcode_$(XCODE_VERSION).app/Contents/Developer - xcrun simctl list - displayName: Simulator configuration - - script: $NVM_DIR/versions/node/`node --version`/bin/appium driver install xcuitest - displayName: Install XCUITest driver - - task: Gradle@2 - inputs: - gradleWrapperFile: 'gradlew' - gradleOptions: '-Xmx3072m' - javaHomeOption: 'JDKVersion' - jdkVersionOption: "$(JDK_VERSION)" - jdkArchitectureOption: 'x64' - publishJUnitResults: true - tasks: 'build' - options: 'xcuiTest -x checkstyleTest -x test' -- job: Misc_Tests - steps: - - task: Gradle@2 - inputs: - gradleWrapperFile: 'gradlew' - gradleOptions: '-Xmx3072m' - javaHomeOption: 'JDKVersion' - jdkVersionOption: "$(JDK_VERSION)" - jdkArchitectureOption: 'x64' - publishJUnitResults: true - tasks: 'build' - options: 'miscTest -x checkstyleTest -x test -x signMavenJavaPublication' diff --git a/build.gradle b/build.gradle index f61424a81..60340fbfe 100644 --- a/build.gradle +++ b/build.gradle @@ -6,101 +6,104 @@ plugins { id 'eclipse' id 'maven-publish' id 'jacoco' - id 'checkstyle' id 'signing' - id 'org.owasp.dependencycheck' version '7.1.1' - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'org.owasp.dependencycheck' version '12.2.0' + id 'com.gradleup.shadow' version '9.3.1' + id 'org.jreleaser' version '1.21.0' } +ext { + seleniumVersion = project.property('selenium.version') + appiumClientVersion = project.property('appiumClient.version') + slf4jVersion = '2.0.17' +} + +group = 'io.appium' +version = appiumClientVersion + repositories { mavenCentral() + + if (project.hasProperty("isCI")) { + maven { + name = 'Central Portal Snapshots' + url = 'https://central.sonatype.com/repository/maven-snapshots/' + mavenContent { + snapshotsOnly() + } + content { + includeGroup("org.seleniumhq.selenium") + } + } + } } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 withJavadocJar() withSourcesJar() } -ext { - seleniumVersion = project.property('selenium.version') -} - dependencies { - compileOnly 'org.projectlombok:lombok:1.18.24' - annotationProcessor 'org.projectlombok:lombok:1.18.24' + compileOnly 'org.projectlombok:lombok:1.18.42' + annotationProcessor 'org.projectlombok:lombok:1.18.42' - api ('org.seleniumhq.selenium:selenium-api') { - version { - strictly "[${seleniumVersion}, 5.0)" - prefer "${seleniumVersion}" - } - } - api ('org.seleniumhq.selenium:selenium-remote-driver') { - version { - strictly "[${seleniumVersion}, 5.0)" - prefer "${seleniumVersion}" + if (project.hasProperty("isCI")) { + api "org.seleniumhq.selenium:selenium-api:${seleniumVersion}" + api "org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}" + api "org.seleniumhq.selenium:selenium-support:${seleniumVersion}" + } else { + api('org.seleniumhq.selenium:selenium-api') { + version { + strictly "[${seleniumVersion}, 5.0)" + prefer "${seleniumVersion}" + } } - } - implementation ('org.seleniumhq.selenium:selenium-support') { - version { - strictly "[${seleniumVersion}, 5.0)" - prefer "${seleniumVersion}" + api('org.seleniumhq.selenium:selenium-remote-driver') { + version { + strictly "[${seleniumVersion}, 5.0)" + prefer "${seleniumVersion}" + } } - } - implementation 'com.google.code.gson:gson:2.9.1' - implementation 'commons-codec:commons-codec:1.15' - implementation 'cglib:cglib:3.3.0' - implementation 'commons-validator:commons-validator:1.7' - implementation 'org.apache.commons:commons-lang3:3.12.0' - implementation 'commons-io:commons-io:2.11.0' - implementation 'org.slf4j:slf4j-api:1.7.36' - - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' - testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation (group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '5.2.3') { - exclude group: 'org.seleniumhq.selenium' - } - testImplementation ('org.seleniumhq.selenium:selenium-chrome-driver') { - version { - strictly "[${seleniumVersion}, 5.0)" - prefer "${seleniumVersion}" + api('org.seleniumhq.selenium:selenium-support') { + version { + strictly "[${seleniumVersion}, 5.0)" + prefer "${seleniumVersion}" + } } } -} - -ext { - Sources = fileTree("$buildDir/src/main/java").include('**/*.java') - Tests = fileTree("$buildDir/src/test/java").include('**/*.java') - Docs = file("$buildDir/doc") + implementation 'com.google.code.gson:gson:2.13.2' + implementation "org.slf4j:slf4j-api:${slf4jVersion}" + implementation 'org.jspecify:jspecify:1.0.0' } dependencyCheck { - failBuildOnCVSS=22 + failBuildOnCVSS = 22 } jacoco { - toolVersion = '0.8.5' + toolVersion = '0.8.13' } -tasks.withType(JacocoReport) { +tasks.withType(JacocoReport).configureEach { description = 'Generate Jacoco coverage reports after running tests' sourceSets sourceSets.main reports { html.required = true - html.destination file("${buildDir}/Reports/jacoco") + html.outputLocation = file("${buildDir}/Reports/jacoco") } } jacocoTestReport.dependsOn test +apply plugin: 'checkstyle' + checkstyle { - toolVersion = '8.32' - configFile = file("$projectDir/google-style.xml") + toolVersion = '10.23.1' + configFile = configDirectory.file('appium-style.xml').get().getAsFile() showViolations = true ignoreFailures = false } -checkstyleMain.excludes = ['**/org/openqa/selenium/**'] javadoc { options.addStringOption('encoding', 'UTF-8') @@ -111,7 +114,7 @@ publishing { mavenJava(MavenPublication) { groupId = 'io.appium' artifactId = 'java-client' - version = '8.1.1' + version = appiumClientVersion from components.java pom { name = 'java-client' @@ -165,71 +168,159 @@ publishing { } repositories { maven { - credentials { - username "$ossrhUsername" - password "$ossrhPassword" - } - 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 { - 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 = '7.5.1' + gradleVersion = '9.1.0' distributionType = Wrapper.DistributionType.ALL } processResources { filter ReplaceTokens, tokens: [ - 'selenium.version': seleniumVersion + 'selenium.version' : seleniumVersion, + 'appiumClient.version': appiumClientVersion ] } -task xcuiTest( type: Test ) { - useJUnitPlatform() - testLogging.showStandardStreams = true - testLogging.exceptionFormat = 'full' - filter { - includeTestsMatching 'io.appium.java_client.ios.*' - includeTestsMatching '*.pagefactory_tests.XCUITModeTest' - includeTestsMatching '*.pagefactory_tests.widget.tests.combined.*' - includeTestsMatching '*.pagefactory_tests.widget.tests.ios.*' - includeTestsMatching 'io.appium.java_client.service.local.StartingAppLocallyIosTest' - exclude '**/IOSScreenRecordTest.class' - exclude '**/ImagesComparisonTest.class' - } -} +testing { + suites { + configureEach { + useJUnitJupiter() + dependencies { + implementation 'org.junit.jupiter:junit-jupiter:5.14.2' + runtimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.hamcrest:hamcrest:3.0' + runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}" + } + targets.configureEach { + testTask.configure { + testLogging { + showStandardStreams = true + exceptionFormat = 'full' + } + } + } + } -task uiAutomationTest( type: Test ) { - useJUnitPlatform() - testLogging.showStandardStreams = true - testLogging.exceptionFormat = 'full' - filter { - includeTestsMatching 'io.appium.java_client.android.SettingTest' - includeTestsMatching 'io.appium.java_client.android.ClipboardTest' - includeTestsMatching '*.AndroidAppStringsTest' - includeTestsMatching '*.pagefactory_tests.widget.tests.android.*' - includeTestsMatching '*.pagefactory_tests.widget.tests.AndroidPageObjectTest' - includeTestsMatching 'io.appium.java_client.service.local.StartingAppLocallyAndroidTest' - includeTestsMatching 'io.appium.java_client.service.local.ServerBuilderTest' - includeTestsMatching 'io.appium.java_client.service.local.ThreadSafetyTest' - } -} + test { + dependencies { + implementation "org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}" + implementation('io.github.bonigarcia:webdrivermanager:6.3.3') { + exclude group: 'org.seleniumhq.selenium' + } + } + targets.configureEach { + testTask.configure { + finalizedBy jacocoTestReport + } + } + } + + e2eIosTest(JvmTestSuite) { + sources { + java { + srcDirs = ['src/e2eIosTest/java'] + } + } + dependencies { + implementation project() + implementation(sourceSets.test.output) + implementation('org.apache.commons:commons-lang3:3.20.0') + } -task miscTest( type: Test ) { - useJUnitPlatform() - testLogging.showStandardStreams = true - testLogging.exceptionFormat = 'full' - filter { - includeTestsMatching 'io.appium.java_client.touch.*' - includeTestsMatching 'io.appium.java_client.events.*' - includeTestsMatching 'io.appium.java_client.remote.*' - includeTestsMatching 'io.appium.java_client.drivers.options.*' + targets.configureEach { + testTask.configure { + shouldRunAfter(test) + filter { + exclude '**/IOSScreenRecordTest.class' + exclude '**/ImagesComparisonTest.class' + exclude '**/IOSNativeWebTapSettingTest.class' + } + } + } + } + + e2eAndroidTest(JvmTestSuite) { + sources { + java { + srcDirs = ['src/e2eAndroidTest/java'] + } + } + dependencies { + implementation project() + implementation(sourceSets.test.output) + implementation('io.github.bonigarcia:webdrivermanager:6.3.3') { + exclude group: 'org.seleniumhq.selenium' + } + } + + targets.configureEach { + testTask.configure { + shouldRunAfter(test) + filter { + // The following tests fail and should be reviewed/fixed + exclude '**/AndroidAbilityToUseSupplierTest.class' + exclude '**/AndroidConnectionTest.class' + exclude '**/AndroidContextTest.class' + exclude '**/AndroidDataMatcherTest.class' + exclude '**/AndroidDriverTest.class' + exclude '**/AndroidElementTest.class' + exclude '**/AndroidFunctionTest.class' + exclude '**/AndroidSearchingTest.class' + exclude '**/AndroidTouchTest.class' + exclude '**/AndroidViewMatcherTest.class' + exclude '**/ExecuteCDPCommandTest.class' + exclude '**/ExecuteDriverScriptTest.class' + exclude '**/FingerPrintTest.class' + exclude '**/ImagesComparisonTest.class' + exclude '**/KeyCodeTest.class' + exclude '**/LogEventTest.class' + exclude '**/UIAutomator2Test.class' + exclude '**/AndroidPageObjectTest.class' + exclude '**/MobileBrowserCompatibilityTest.class' + } + } + } + } + + e2eFlutterTest(JvmTestSuite) { + sources { + java { + srcDirs = ['src/e2eFlutterTest/java'] + } + } + dependencies { + implementation project() + implementation(sourceSets.test.output) + } + + targets.configureEach { + testTask.configure { + shouldRunAfter(test) + systemProperties project.properties.subMap(["platform", "flutterApp"]) + } + } + } } } diff --git a/google-style.xml b/config/checkstyle/appium-style.xml similarity index 87% rename from google-style.xml rename to config/checkstyle/appium-style.xml index 5762dbafb..b7473e937 100755 --- a/google-style.xml +++ b/config/checkstyle/appium-style.xml @@ -2,20 +2,10 @@ - - - @@ -41,9 +31,16 @@ - - - + + + + + + + + + + @@ -66,6 +63,7 @@ + @@ -131,6 +129,7 @@ + @@ -178,10 +177,9 @@ - - + @@ -202,9 +200,23 @@ + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 000000000..0587e646e --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,5 @@ + + + + + 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/Functions.md b/docs/Functions.md deleted file mode 100644 index bdf962f7c..000000000 --- a/docs/Functions.md +++ /dev/null @@ -1,147 +0,0 @@ -Appium java client has some features based on [Java 8 Functional interfaces](https://www.oreilly.com/learning/java-8-functional-interfaces). - -# Conditions - -```java -io.appium.java_client.functions.AppiumFunction -``` -It extends -```java -java.util.function.Function -``` -and -```java -com.google.common.base.Function -``` -to make end user available to use _org.openqa.selenium.support.ui.Wait_. There is additional interface -```java -io.appium.java_client.functions.ExpectedCondition -``` -which extends -```java -io.appium.java_client.functions.AppiumFunction -``` - -and - -```java -org.openqa.selenium.support.ui.ExpectedCondition -``` - -This feature provides the ability to create complex condition of the waiting for something. - -```java -//waiting for elements - private final AppiumFunction> searchingFunction = input -> { - List result = input.findElements(By.tagName("a")); - - if (result.size() > 0) { - return result; - } - return null; -}; - -//waiting for some context using regular expression pattern -private final AppiumFunction contextFunction = input -> { - Set contexts = driver.getContextHandles(); - String current = driver.getContext(); - contexts.forEach(context -> { - Matcher m = input.matcher(context); - if (m.find()) { - driver.context(context); - } - }); - if (!current.equals(driver.getContext())) { - return driver; - } - return null; -}; -``` - -## using one function as pre-condition - -```java -@Test public void tezt() { - .... - Wait wait = new FluentWait<>(Pattern.compile("WEBVIEW")) - .withTimeout(30, TimeUnit.SECONDS); - List elements = wait.until(searchingFunction.compose(contextFunction)); - .... -} -``` - -## using one function as post-condition - -```java -import org.openqa.selenium.support.ui.FluentWait; -import org.openqa.selenium.support.ui.Wait; - -@Test public void tezt() { - .... - Wait wait = new FluentWait<>(Pattern.compile("WEBVIEW")) - .withTimeout(30, TimeUnit.SECONDS); - List elements = wait.until(contextFunction.andThen(searchingFunction)); - .... -} -``` - -# Touch action supplier - -[About touch actions](https://github.com/appium/java-client/blob/master/docs/Touch-actions.md) - -You can use suppliers to declare touch/multitouch actions for some screens/tests. Also it is possible to -create gesture libraries/utils using suppliers. Appium java client provides this interface - -```java -io.appium.java_client.functions.ActionSupplier -``` - -## Samples - -```java -private final ActionSupplier horizontalSwipe = () -> { - driver.findElementById("io.appium.android.apis:id/gallery"); - - AndroidElement gallery = driver.findElementById("io.appium.android.apis:id/gallery"); - List images = gallery - .findElementsByClassName("android.widget.ImageView"); - Point location = gallery.getLocation(); - Point center = gallery.getCenter(); - - return new TouchAction(driver).press(images.get(2), -10, center.y - location.y) - .waitAction(2000).moveTo(gallery, 10, center.y - location.y).release(); -}; - -private final ActionSupplier verticalSwiping = () -> - new TouchAction(driver).press(driver.findElementByAccessibilityId("Gallery")) - .waitAction(2000).moveTo(driver.findElementByAccessibilityId("Auto Complete")).release(); - -@Test public void tezt() { - ... - horizontalSwipe.get().perform(); - ... - verticalSwiping.get().perform(); - ... -} -``` - -```java -public class GestureUtils { - - public static ActionSupplier swipe(final AppiumDriver driver, final params) { - return () -> { - new TouchAction(driver).press(params) - .waitAction(params).moveTo(params).release(); - }; - } -} - -public class SomeTest { - @Test public void tezt() { - ... - GestureUtils.swipe(driver, params).get().perform(); - ... - } -} - -``` \ No newline at end of file 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/Installing-the-project.md b/docs/Installing-the-project.md deleted file mode 100644 index 8417f9f79..000000000 --- a/docs/Installing-the-project.md +++ /dev/null @@ -1,91 +0,0 @@ -# Requirements - -Firstly you should install appium server. [Appium getting started](https://appium.io/docs/en/about-appium/getting-started/). The latest server version is recommended. - -Since version 5.x there many features based on Java 8. So we recommend to install JDK SE 8 and provide that source compatibility. - -# Maven - -Add the following to pom.xml: - -``` - - io.appium - java-client - ${version.you.require} - test - -``` - -If you haven't already, change the Java version: -``` - - 1.8 - 1.8 - -``` - -If it is necessary to change the version of Selenium then you can configure pom.xml like following: - -``` - - io.appium - java-client - ${version.you.require} - test - - - org.seleniumhq.selenium - selenium-java - - - - - - org.seleniumhq.selenium - selenium-java - ${selenium.version.you.require} - -``` - -# Gradle - -Add the following to build.gradle: - -``` -repositories { - jcenter() - maven { - url "http://repo.maven.apache.org/maven2" - } -} - -dependencies { - ... - testCompile group: 'io.appium', name: 'java-client', version: requiredVersion - ... -} -``` - -If it is necessary to change the version of Selenium then you can configure build.gradle like the sample below: - -``` -repositories { - jcenter() - maven { - url "http://repo.maven.apache.org/maven2" - } -} - -dependencies { - ... - testCompile group: 'io.appium', name: 'java-client', version: requiredVersion { - exclude module: 'selenium-java' - } - - testCompile group: 'org.seleniumhq.selenium', name: 'selenium-java', - version: requiredSeleniumVersion - ... -} -``` - diff --git a/docs/Page-objects.md b/docs/Page-objects.md index 7bc36a267..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; @@ -46,16 +46,15 @@ List someElements; # The example for the crossplatform mobile native testing ```java -import io.appium.java_client.MobileElement; import io.appium.java_client.pagefactory.*; @AndroidFindBy(someStrategy) @iOSFindBy(someStrategy) -MobileElement someElement; +WebElement someElement; @AndroidFindBy(someStrategy) //for the crossplatform mobile native @iOSFindBy(someStrategy) //testing -List someElements; +List someElements; ``` # The fully cross platform example @@ -325,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 @@ -372,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){ @@ -405,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 @@ -428,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{ ... } @@ -659,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/Tech-stack.md b/docs/Tech-stack.md deleted file mode 100644 index cbaa01b2e..000000000 --- a/docs/Tech-stack.md +++ /dev/null @@ -1,19 +0,0 @@ -![](https://cloud.githubusercontent.com/assets/4927589/21467582/df8ab94e-ca03-11e6-969c-c6d30c6add67.png) -![](https://cloud.githubusercontent.com/assets/4927589/21467509/a97e084e-ca01-11e6-9d04-4f2b8e1c72df.png) -![](https://cloud.githubusercontent.com/assets/4927589/21467524/187a333a-ca02-11e6-8e3c-14c411448fdb.png) -![](https://cloud.githubusercontent.com/assets/4927589/21467531/6f576f1a-ca02-11e6-9f2b-2551ea0e0753.png) + **AspectJ** and **CGlib** - -This project is based on [Selenium java client](https://github.com/SeleniumHQ/selenium/tree/master/java/client). It already depends on it and extends it to mobile platforms. - -This project is built by [gradle](https://gradle.org/) - -Also tech stack includes [Spring framework](https://spring.io/projects/spring-framework) in binding with AspectJ. This is used by [event firing feature](https://github.com/appium/java-client/blob/master/docs/The-event_firing.md). Also **CGlib** is used by [Page Object tools](https://github.com/appium/java-client/blob/master/docs/Page-objects.md). - -It is the client framework. It is the thin client which just sends requests to Appium server and receives responses. Also it has some -high-level features which were designed to simplify user's work. - -# It supports: - -![](https://cloud.githubusercontent.com/assets/4927589/21467612/4b6b3f70-ca05-11e6-9a31-d3820e98dac6.png) -![](https://cloud.githubusercontent.com/assets/4927589/21467614/73883828-ca05-11e6-846d-3ed8847a7e08.jpg) -![](https://cloud.githubusercontent.com/assets/4927589/21467621/aab3ff6c-ca05-11e6-9170-2e7a19d3307c.png) \ No newline at end of file diff --git a/docs/The-event_firing.md b/docs/The-event_firing.md index 7fa0a58d6..ff77c1247 100644 --- a/docs/The-event_firing.md +++ b/docs/The-event_firing.md @@ -3,7 +3,7 @@ since v8.0.0 # The purpose This feature allows end user to organize the event logging on the client side. -Also this feature may be useful in a binding with standard or custom reporting +Also, this feature may be useful in a binding with standard or custom reporting frameworks. The feature has been introduced first since Selenium API v4. # The API @@ -40,6 +40,7 @@ Listeners should implement WebDriverListener. It supports three types of events: To use this decorator you have to prepare a listener, create a decorator using this listener, decorate the original WebDriver instance with this decorator and use the new WebDriver instance created by the decorator instead of the original one: + ```java WebDriver original = new AndroidDriver(); // it is expected that MyListener class implements WebDriverListener @@ -66,6 +67,7 @@ decorated.get("http://example.com/"); WebElement header = decorated.findElement(By.tagName("h1")); // if an error happens during any of these calls the the onError event is fired ``` + The instance of WebDriver created by the decorator implements all the same interfaces as the original driver. A listener can subscribe to "specific" or "generic" events (or both). A "specific" event correspond to a single specific method, a "generic" event correspond to any @@ -74,3 +76,138 @@ implement a method with a name derived from the target method to be watched. The for "before"-events receive the parameters passed to the decorated method. The listener methods for "after"-events receive the parameters passed to the decorated method as well as the result returned by this method. + +## createProxy API (since Java Client 8.3.0) + +This API is unique to Appium Java Client and does not exist in Selenium. The reason for +its existence is the fact that the original event listeners API provided by Selenium is limited +because it can only use interface types for decorator objects. For example, the code below won't +work: + +```java +IOSDriver driver = new IOSDriver(new URL("http://doesnot.matter/"), new ImmutableCapabilities()) +{ + @Override + protected void startSession(Capabilities capabilities) + { + // Override in a sake of simplicity to avoid the actual session start + } +}; +WebDriverListener webDriverListener = new WebDriverListener() +{ +}; +IOSDriver decoratedDriver = (IOSDriver) new EventFiringDecorator(IOSDriver.class, webDriverListener).decorate( + driver); +``` + +The last line throws `ClassCastException` because `decoratedDriver` is of type `IOSDriver`, +which is a class rather than an interface. +See the issue [#1694](https://github.com/appium/java-client/issues/1694) for more +details. In order to workaround this limitation a special proxy implementation has been created, +which is capable of decorating class types: + +```java +import io.appium.java_client.proxy.MethodCallListener; +import io.appium.java_client.proxy.NotImplementedException; + +import static io.appium.java_client.proxy.Helpers.createProxy; + +// ... + +MethodCallListener listener = new MethodCallListener() { + @Override + public void beforeCall(Object target, Method method, Object[] args) { + if (!method.getName().equals("get")) { + throw new NotImplementedException(); + } + acc.append("beforeCall ").append(method.getName()).append("\n"); + } + + @Override + public void afterCall(Object target, Method method, Object[] args, Object result) { + if (!method.getName().equals("get")) { + throw new NotImplementedException(); + } + acc.append("afterCall ").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 +); + +decoratedDriver.get("http://example.com/"); + +assertThat(acc.toString().trim()).isEqualTo( + String.join("\n", + "beforeCall get", + "afterCall get" + ) +); +``` + +This proxy is not tied to WebDriver descendants and could be used to any classes that have +**public** constructors. It also allows to intercept exceptions thrown by **public** class methods and/or +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-Android-app.md b/docs/The-starting-of-an-Android-app.md deleted file mode 100644 index 466756677..000000000 --- a/docs/The-starting-of-an-Android-app.md +++ /dev/null @@ -1,156 +0,0 @@ -# Steps: - -- you have to prepare environment for Android. [Details are provided here](https://appium.io/docs/en/drivers/android-uiautomator2/#basic-setup) - -- it needs to launch the appium server. You can launch Appium desktop application. If you use the server installed via npm then - - _$ node **the_path_to_main.js_file** --arg1 value1 --arg2 value2_ -It is not necessary to use arguments. [The list of arguments](https://appium.io/docs/en/writing-running-appium/server-args/) - - -# The starting of an app - -It looks like creation of a common [RemoteWebDriver](https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/remote/RemoteWebDriver.html) instance. - -[Common capabilities](https://appium.io/docs/en/writing-running-appium/caps/#general-capabilities) - -[Android-specific capabilities](https://appium.io/docs/en/writing-running-appium/caps/#android-only) - -[Common capabilities provided by Java client](https://javadoc.io/page/io.appium/java-client/latest/io/appium/java_client/remote/MobileCapabilityType.html) - -[Android-specific capabilities provided by Java client](https://javadoc.io/page/io.appium/java-client/latest/io/appium/java_client/remote/AndroidMobileCapabilityType.html) - -```java -import java.io.File; -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.android.AndroidDriver; -import io.appium.java_client.MobileElement; -import java.net.URL; - -... -File app = new File("The absolute or relative path to an *.apk file"); -DesiredCapabilities capabilities = new DesiredCapabilities(); -capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); -capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); -capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); -//you are free to set additional capabilities -AppiumDriver driver = new AppiumDriver<>( -new URL("http://target_ip:used_port/wd/hub"), //if it needs to use locally started server -//then the target_ip is 127.0.0.1 or 0.0.0.0 -//the default port is 4723 -capabilities); -``` - -or - -```java -import java.io.File; -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.MobileElement; -import java.net.URL; - -... -File app = new File("The absolute or relative path to an *.apk file"); -DesiredCapabilities capabilities = new DesiredCapabilities(); -capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); -capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); -//you are free to set additional capabilities -AppiumDriver driver = new AndroidDriver<>( -new URL("http://target_ip:used_port/wd/hub"), //if it needs to use locally started server -//then the target_ip is 127.0.0.1 or 0.0.0.0 -//the default port is 4723 -capabilities); -``` - - -## If it needs to start browser then - -This capability should be used - -```java -capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.CHROME); -//if it is necessary to use the default Android browser then MobileBrowserType.BROWSER -//is your choice -``` - -## There are three automation types - -```java -capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.SELENDROID); -``` - -This automation type is usually recommended for old versions (<4.2) of Android. - -Default Android UIAutomator does not require any specific capability. However you can -```java -capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); -``` - -You have to define this automation type to be able to use Android UIAutomator2 for new Android versions -```java -capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2); -``` - -# Possible cases - -You can use ```io.appium.java_client.AppiumDriver``` and ```io.appium.java_client.android.AndroidDriver``` as well. The main difference -is that ```AndroidDriver``` implements all API that describes interaction with Android native/hybrid app. ```AppiumDriver``` allows to -use Android-specific API eventually. - - _The sample of the activity starting by_ ```io.appium.java_client.AppiumDriver``` - - ```java - import io.appium.java_client.android.StartsActivity; - import io.appium.java_client.android.Activity; - -... - -StartsActivity startsActivity = new StartsActivity() { - @Override - public Response execute(String driverCommand, Map parameters) { - return driver.execute(driverCommand, parameters); - } - - @Override - public Response execute(String driverCommand) { - return driver.execute(driverCommand); - } -}; - -Activity activity = new Activity("app package goes here", "app activity goes here") - .setWaitAppPackage("app wait package goes here"); - .setWaitAppActivity("app wait activity goes here"); -StartsActivity startsActivity.startActivity(activity); - ``` - -_Samples of the searching by AndroidUIAutomator using_ ```io.appium.java_client.AppiumDriver``` - -```java -import io.appium.java_client.FindsByAndroidUIAutomator; -import io.appium.java_client.android.AndroidElement; - -... - -FindsByAndroidUIAutomator findsByAndroidUIAutomator = - new FindsByAndroidUIAutomator() { - @Override - public AndroidElement findElement(String by, String using) { - return driver.findElement(by, using); - } - - @Override - public List findElements(String by, String using) { - return driver.findElements(by, using); - }; -}; - -findsByAndroidUIAutomator.findElementByAndroidUIAutomator("automatorString"); -``` - -```java -driver.findElement(MobileBy.AndroidUIAutomator("automatorString")); -``` - -All that ```AndroidDriver``` can do by design. 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 5d0fc1e13..9397385c5 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 @@ -7,30 +7,6 @@ It works the similar way as common [ChromeDriver](https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/chrome/ChromeDriver.html), [InternetExplorerDriver](https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/ie/InternetExplorerDriver.html) of Selenium project or [PhantomJSDriver](https://cdn.rawgit.com/detro/ghostdriver/master/binding/java/docs/javadoc/org/openqa/selenium/phantomjs/PhantomJSDriver.html). They use subclasses of the [DriverService](https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/remote/service/DriverService.html). -# Which capabilities this feature provides - -This feature provides abilities and options of the starting of a local Appium node server. End users still able to open apps as usual - -```java - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - capabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 120); - driver = new AndroidDriver<>(new URL("remoteOrLocalAddress"), capabilities); -``` - -when the server is launched locally\remotely. Also user is free to launch a local Appium node server and open their app for the further testing the following way: - -```java - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - capabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 120); - driver = new AndroidDriver<>(capabilities); -``` - # How to prepare the local service before the starting @@ -49,6 +25,7 @@ when the server is launched locally\remotely. Also user is free to launch a loca ### FYI There are possible problems related to local environment which could break this: + ```java AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); ``` diff --git a/docs/The-starting-of-an-iOS-app.md b/docs/The-starting-of-an-iOS-app.md deleted file mode 100644 index 19b5ba2a0..000000000 --- a/docs/The-starting-of-an-iOS-app.md +++ /dev/null @@ -1,122 +0,0 @@ -# Steps: - -- you have to prepare environment for iOS. [Details are provided here](https://appium.io/docs/en/drivers/ios-xcuitest/#basic-setup) - -- it needs to launch the appium server. You can launch Appium desktop application. If you use the server installed via npm then - - _$ node **the_path_to_js_file** --arg1 value1 --arg2 value2_ -It is not necessary to use arguments. [The list of arguments](https://appium.io/docs/en/writing-running-appium/server-args/) - -# The starting of an app - -It looks like creation of a common [RemoteWebDriver](https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/remote/RemoteWebDriver.html) instance. - -[Common capabilities](https://appium.io/docs/en/writing-running-appium/caps/#general-capabilities) - -[iOS-specific capabilities](https://appium.io/docs/en/writing-running-appium/caps/#ios-only) - -[Common capabilities provided by Java client](https://javadoc.io/page/io.appium/java-client/latest/io/appium/java_client/remote/MobileCapabilityType.html) - -[iOS-specific capabilities provided by Java client](https://javadoc.io/page/io.appium/java-client/latest/io/appium/java_client/remote/IOSMobileCapabilityType.html) - - -```java -import java.io.File; -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.MobileElement; -import java.net.URL; - -... -File app = new File("The absolute or relative path to an *.app, *.zip or ipa file"); -DesiredCapabilities capabilities = new DesiredCapabilities(); -capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); -capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "The_target_version"); -capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); -//The_target_version is the supported iOS version, e.g. 8.1, 8.2, 9.2 etc -capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); -//you are free to set additional capabilities -AppiumDriver driver = new AppiumDriver<>( -new URL("http://target_ip:used_port/wd/hub"), //if it needs to use locally started server -//then the target_ip is 127.0.0.1 or 0.0.0.0 -//the default port is 4723 -capabilities); -``` - -or - -```java -import java.io.File; -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.ios.IOSDriver; -import io.appium.java_client.MobileElement; -import java.net.URL; - -... -File app = new File("The absolute or relative path to an *.app, *.zip or ipa file"); -DesiredCapabilities capabilities = new DesiredCapabilities(); -capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); -capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "The_target_version"); -//The_target_version is the supported iOS version, e.g. 8.1, 8.2, 9.2 etc -capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); -//you are free to set additional capabilities -AppiumDriver driver = new IOSDriver<>( -new URL("http://target_ip:used_port/wd/hub"), //if it needs to use locally started server -//then the target_ip is 127.0.0.1 or 0.0.0.0 -//the default port is 4723 -capabilities); -``` - -## If it needs to start browser then - -```java -capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.SAFARI); -``` - -## There are two automation types - -Default iOS Automation (v < iOS 10.x) does not require any specific capability. However you can -```java -capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); -``` - -You have to define this automation type to be able to use XCUIT mode for new iOS versions (v > 10.x) -```java -capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.IOS_XCUI_TEST); -``` - -# Possible cases - -You can use ```io.appium.java_client.AppiumDriver``` and ```io.appium.java_client.ios.IOSDriver``` as well. The main difference -is that ```IOSDriver``` implements all API that describes interaction with iOS native/hybrid app. ```AppiumDriver``` allows to -use iOS-specific API eventually. - -_Samples of the searching by iOSNsPredicateString using_ ```io.appium.java_client.AppiumDriver``` - -```java -import io.appium.java_client.FindsByIosNSPredicate; -import io.appium.java_client.ios.IOSElement; - -... - -FindsByIosNSPredicate findsByIosNSPredicate = new FindsByIosNSPredicate() { - @Override - public IOSElement findElement(String by, String using) { - return driver.findElement(by, using); - } - - @Override - public List findElements(String by, String using) { - return driver.findElements(by, using); - } -}; - -findsByIosNSPredicate.findElementByIosNsPredicate("some predicate"); -``` - -```java -driver.findElement(MobileBy.iOSNsPredicateString("some predicate")); -``` - -All that ```IOSDriver``` can do by design. diff --git a/docs/Touch-actions.md b/docs/Touch-actions.md deleted file mode 100644 index 920a8d898..000000000 --- a/docs/Touch-actions.md +++ /dev/null @@ -1,44 +0,0 @@ -Appium server side provides abilities to emulate touch actions. It is possible construct single, complex and multiple touch actions. - -# How to use a single touch action - -```java -import io.appium.java_client.TouchAction; - -... -//tap -new TouchAction(driver) - .tap(driver - .findElementById("io.appium.android.apis:id/start")).perform(); -``` - -# How to construct complex actions - -```java -import io.appium.java_client.TouchAction; - -... -//swipe -TouchAction swipe = new TouchAction(driver).press(images.get(2), -10, center.y - location.y) - .waitAction(2000).moveTo(gallery, 10, center.y - location.y).release(); -swipe.perform(); -``` - -# How to construct multiple touch action. - -```java -import io.appium.java_client.TouchAction; -import io.appium.java_client.MultiTouchAction; - -... -//tap by few fingers - MultiTouchAction multiTouch = new MultiTouchAction(driver); - -for (int i = 0; i < fingers; i++) { - TouchAction tap = new TouchAction(driver); - multiTouch.add(tap.press(element).waitAction(duration).release()); -} - -multiTouch.perform(); -``` - 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 new file mode 100644 index 000000000..9a84ee016 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,25 @@ +# Appium Java Client Release Procedure + +This document describes the process of releasing this client to the Maven repository. +Its target auditory is project maintainers. + +## Release Steps + +1. Update the [Changelog](../CHANGELOG.md) for the given version based on previous commits. +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 [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/transitive-dependencies-management.md b/docs/transitive-dependencies-management.md new file mode 100644 index 000000000..dc148d816 --- /dev/null +++ b/docs/transitive-dependencies-management.md @@ -0,0 +1,68 @@ +# Maven + +Maven downloads dependency of [the latest version](https://cwiki.apache.org/confluence/display/MAVENOLD/Dependency+Mediation+and+Conflict+Resolution#DependencyMediationandConflictResolution-DependencyVersionRanges) +matching the declared range by default, in other words whenever new versions of Selenium 4 libraries are published +they are pulled transitively as Appium Java Client dependencies at the first project (re)build automatically. + +In order to pin Selenium dependencies they should be declared in `pom.xml` in the following way: + +```xml + + + io.appium + java-client + X.Y.Z + + + org.seleniumhq.selenium + selenium-api + + + org.seleniumhq.selenium + selenium-remote-driver + + + org.seleniumhq.selenium + selenium-support + + + + + org.seleniumhq.selenium + selenium-api + A.B.C + + + org.seleniumhq.selenium + selenium-remote-driver + A.B.C + + + org.seleniumhq.selenium + selenium-support + A.B.C + + +``` + +# Gradle + +Gradle uses [Module Metadata](https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.html) +to perform improved dependency resolution whenever it is available. Gradle Module Metadata for Appium Java Client is +published automatically with every release and is available on Maven Central. + +Appium Java Client declares [preferred](https://docs.gradle.org/current/userguide/rich_versions.html#rich-version-constraints) +Selenium dependencies version which is equal to the lowest boundary in the version range, i.e. the lowest compatible +Selenium dependencies are pulled by Gradle by default. It's strictly recommended to do not use versions lower than the +range boundary, because unresolvable compilation and runtime errors may occur. + +In order to use newer Selenium dependencies they should be explicitly added to Gradle build script (`build.gradle`): + +```gradle +dependencies { + implementation('io.appium:java-client:X.Y.Z') + implementation('org.seleniumhq.selenium:selenium-api:A.B.C') + implementation('org.seleniumhq.selenium:selenium-remote-driver:A.B.C') + implementation('org.seleniumhq.selenium:selenium-support:A.B.C') +} +``` diff --git a/docs/v7-to-v8-migration-guide.md b/docs/v7-to-v8-migration-guide.md index a0ab9d256..b3be7def0 100644 --- a/docs/v7-to-v8-migration-guide.md +++ b/docs/v7-to-v8-migration-guide.md @@ -86,8 +86,27 @@ 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://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android/android-mobile-gestures.md - - https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/ios/ios-xctest-mobile-gestures.md + - 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) + * [mobile: clickGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-clickgesture) + * [mobile: dragGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-draggesture) + * [mobile: flingGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-flinggesture) + * [mobile: pinchOpenGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-pinchopengesture) + * [mobile: pinchCloseGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-pinchclosegesture) + * [mobile: swipeGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-swipegesture) + * [mobile: scrollGesture](https://github.com/appium/appium-uiautomator2-driver/blob/master/docs/android-mobile-gestures.md#mobile-scrollgesture) + - iOS gesture shortcuts: + * [mobile: swipe](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-swipe) + * [mobile: scroll](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-scroll) + * [mobile: pinch](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-pinch) + * [mobile: doubleTap](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-doubletap) + * [mobile: touchAndHold](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-touchandhold) + * [mobile: twoFingerTap](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-twofingertap) + * [mobile: tap](https://github.com/appium/appium-xcuitest-driver/blob/master/docs/execute-methods.md#mobile-tap) + * [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 for more details on how to properly apply W3C Actions to your automation context. diff --git a/docs/v8-to-v9-migration-guide.md b/docs/v8-to-v9-migration-guide.md new file mode 100644 index 000000000..a0b57cd35 --- /dev/null +++ b/docs/v8-to-v9-migration-guide.md @@ -0,0 +1,46 @@ +This is the list of main changes between major versions 8 and 9 of Appium +java client. This list should help you to successfully migrate your +existing automated tests codebase. + + +## The support for Java compilers below version 11 has been dropped + +- The minimum supported Java version is now 11. The library won't work +with Java compilers below this version. + +## The minimum supported Selenium version is set to 4.14.1 + +- Selenium versions below 4.14.1 won't work with Appium java client 9+. +Check the [Compatibility Matrix](../README.md#compatibility-matrix) for more +details about versions compatibility. + +## Removed previously deprecated items + +- `MobileBy` class has been removed. Use +[AppiumBy](../src/main/java/io/appium/java_client/AppiumBy.java) instead +- `launchApp`, `resetApp` and `closeApp` methods along with their +`SupportsLegacyAppManagement` container. +Use [the corresponding extension methods](https://github.com/appium/appium/issues/15807) instead. +- `WindowsBy` class and related location strategies. +- `ByAll` class has been removed in favour of the same class from Selenium lib. +- `AndroidMobileCapabilityType` interface. Use +[UIAutomator2 driver options](../src/main/java/io/appium/java_client/android/options/UiAutomator2Options.java) +or [Espresso driver options](../src/main/java/io/appium/java_client/android/options/EspressoOptions.java) instead. +- `IOSMobileCapabilityType` interface. Use +[XCUITest driver options](../src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java) instead. +- `MobileCapabilityType` interface. Use +[driver options](../src/main/java/io/appium/java_client/remote/options/BaseOptions.java) instead. +- `MobileOptions` class. Use +[driver options](../src/main/java/io/appium/java_client/remote/options/BaseOptions.java) instead. +- `YouiEngineCapabilityType` interface. Use +[driver options](../src/main/java/io/appium/java_client/remote/options/BaseOptions.java) instead. +- Several misspelled methods. Use properly spelled alternatives instead. +- `startActivity` method from AndroidDriver. Use +[mobile: startActivity](https://github.com/appium/appium-uiautomator2-driver#mobile-startactivity) +extension method instead. +- `APPIUM` constant from the AutomationName interface. It is not needed anymore. +- `PRE_LAUNCH` value from the GeneralServerFlag enum. It is not needed anymore. + +## Moved items + +- `AppiumUserAgentFilter` class to `io.appium.java_client.internal.filters` package. 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 b11361057..1540f2707 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,5 @@ org.gradle.daemon=true -signing.keyId=YourKeyId -signing.password=YourPublicKeyPassword -signing.secretKeyRingFile=PathToYourKeyRingFile - -ossrhUsername=your-jira-id -ossrhPassword=your-jira-password - -selenium.version=4.4.0 +selenium.version=4.40.0 +# Please increment the value in a release +appiumClient.version=10.0.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f..61285a659 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8fad3f5a9..5f38436fc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb6c..adff685a0 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/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/. @@ -80,13 +82,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# 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\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,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. @@ -133,22 +132,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -165,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" ) @@ -193,16 +198,19 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * 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 53a6b238d..e509b2dd8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -42,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +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 @@ -56,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +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 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/jitpack.yml b/jitpack.yml index c4432703f..e5b145a9d 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,4 +1,4 @@ jdk: - - openjdk8 + - openjdk11 install: - - ./gradlew clean build publishToMavenLocal -x signMavenJavaPublication -x test -x checkstyleTest + - ./gradlew clean build publishToMavenLocal -PsigningDisabled=true diff --git a/src/test/java/io/appium/java_client/android/AndroidAppStringsTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidAppStringsTest.java similarity index 100% rename from src/test/java/io/appium/java_client/android/AndroidAppStringsTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidAppStringsTest.java index 9aaeb88b3..e3fefd9b0 100644 --- a/src/test/java/io/appium/java_client/android/AndroidAppStringsTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidAppStringsTest.java @@ -16,10 +16,10 @@ package io.appium.java_client.android; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + public class AndroidAppStringsTest extends BaseAndroidTest { @Test public void getAppStrings() { 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/test/java/io/appium/java_client/android/AndroidConnectionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidConnectionTest.java similarity index 100% rename from src/test/java/io/appium/java_client/android/AndroidConnectionTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidConnectionTest.java index 88e1c1d6e..4c8f03cd4 100644 --- a/src/test/java/io/appium/java_client/android/AndroidConnectionTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidConnectionTest.java @@ -16,15 +16,15 @@ package io.appium.java_client.android; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - import io.appium.java_client.android.connection.ConnectionState; import io.appium.java_client.android.connection.ConnectionStateBuilder; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + @TestMethodOrder(MethodOrderer.MethodName.class) public class AndroidConnectionTest extends BaseAndroidTest { diff --git a/src/test/java/io/appium/java_client/android/AndroidContextTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java similarity index 85% rename from src/test/java/io/appium/java_client/android/AndroidContextTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java index ed832f9e3..1a9a5657d 100644 --- a/src/test/java/io/appium/java_client/android/AndroidContextTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java @@ -16,23 +16,23 @@ package io.appium.java_client.android; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - import io.appium.java_client.NoSuchContextException; 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; + public class AndroidContextTest extends BaseAndroidTest { @BeforeAll public static void beforeClass2() throws Exception { - Activity activity = new Activity("io.appium.android.apis", ".view.WebView1"); - driver.startActivity(activity); + startActivity(".view.WebView1"); Thread.sleep(20000); } @Test public void testGetContext() { - assertEquals("NATIVE_APP", driver.getContext()); + assertEquals(NATIVE_CONTEXT, driver.getContext()); } @Test public void testGetContextHandles() { @@ -43,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/test/java/io/appium/java_client/android/AndroidDataMatcherTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDataMatcherTest.java similarity index 88% rename from src/test/java/io/appium/java_client/android/AndroidDataMatcherTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDataMatcherTest.java index 3db8ade05..83d8eabdf 100644 --- a/src/test/java/io/appium/java_client/android/AndroidDataMatcherTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDataMatcherTest.java @@ -16,8 +16,6 @@ package io.appium.java_client.android; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import io.appium.java_client.AppiumBy; import org.junit.jupiter.api.Test; import org.openqa.selenium.json.Json; @@ -25,6 +23,8 @@ import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; +import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -37,9 +37,9 @@ public void testFindByDataMatcher() { .elementToBeClickable(AppiumBy.accessibilityId("Graphics"))); driver.findElement(AppiumBy.accessibilityId("Graphics")).click(); - String selector = new Json().toJson(ImmutableMap.of( + String selector = new Json().toJson(Map.of( "name", "hasEntry", - "args", ImmutableList.of("title", "Sweep") + "args", List.of("title", "Sweep") )); assertNotNull(wait.until(ExpectedConditions diff --git a/src/test/java/io/appium/java_client/android/AndroidDriverTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDriverTest.java similarity index 92% rename from src/test/java/io/appium/java_client/android/AndroidDriverTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDriverTest.java index 145bd32dc..76753d75d 100644 --- a/src/test/java/io/appium/java_client/android/AndroidDriverTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDriverTest.java @@ -16,28 +16,29 @@ package io.appium.java_client.android; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.lessThan; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - +import io.appium.java_client.Location; import io.appium.java_client.appmanagement.ApplicationState; -import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.html5.Location; import java.io.File; import java.time.Duration; import java.util.ArrayList; +import java.util.Base64; import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class AndroidDriverTest extends BaseAndroidTest { @@ -148,14 +149,14 @@ public void isAppNotInstalledTest() { @Test public void closeAppTest() { - driver.closeApp(); - driver.launchApp(); + driver.executeScript("mobile: terminateApp", Map.of("appId", APP_ID)); + driver.executeScript("mobile: activateApp", Map.of("appId", APP_ID)); assertEquals(".ApiDemos", driver.currentActivity()); } @Test public void pushFileTest() { - byte[] data = Base64.encodeBase64( + byte[] data = Base64.getEncoder().encode( "The eventual code is no more than the deposit of your understanding. ~E. W. Dijkstra" .getBytes()); driver.pushFile("/data/local/tmp/remote.txt", data); @@ -191,7 +192,7 @@ public void toggleLocationServicesTest() { @Test public void geolocationTest() { - Location location = new Location(45, 45, 100); + Location location = new Location(45, 45, 100.0); driver.setLocation(location); } @@ -219,7 +220,7 @@ public void runAppInBackgroundTest() { long time = System.currentTimeMillis(); driver.runAppInBackground(Duration.ofSeconds(4)); long timeAfter = System.currentTimeMillis(); - assert (timeAfter - time > 3000); + assert timeAfter - time > 3000; } @Test @@ -236,19 +237,8 @@ public void testApplicationsManagement() throws InterruptedException { @Test public void pullFileTest() { - byte[] data = - driver.pullFile("/data/system/users/userlist.xml"); - assert (data.length > 0); - } - - @Test - public void resetTest() { - driver.resetApp(); - } - - @Test - public void endTestCoverage() { - driver.endTestCoverage("android.intent.action.MAIN", ""); + byte[] data = driver.pullFile("/data/system/users/userlist.xml"); + assert data.length > 0; } @Test @@ -260,7 +250,7 @@ public void deviceDetailsAndKeyboardTest() { @Test public void getSupportedPerformanceDataTypesTest() { - driver.startActivity(new Activity(APP_ID, ".ApiDemos")); + startActivity(".ApiDemos"); List dataTypes = new ArrayList<>(); dataTypes.add("cpuinfo"); @@ -275,13 +265,11 @@ public void getSupportedPerformanceDataTypesTest() { for (int i = 0; i < supportedPerformanceDataTypes.size(); ++i) { assertEquals(dataTypes.get(i), supportedPerformanceDataTypes.get(i)); } - - } @Test public void getPerformanceDataTest() { - driver.startActivity(new Activity(APP_ID, ".ApiDemos")); + startActivity(".ApiDemos"); List supportedPerformanceDataTypes = driver.getSupportedPerformanceDataTypes(); diff --git a/src/test/java/io/appium/java_client/android/AndroidElementTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidElementTest.java similarity index 90% rename from src/test/java/io/appium/java_client/android/AndroidElementTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidElementTest.java index 8ce9fd2fc..44c8473d6 100644 --- a/src/test/java/io/appium/java_client/android/AndroidElementTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidElementTest.java @@ -16,10 +16,6 @@ package io.appium.java_client.android; -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 io.appium.java_client.AppiumBy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,11 +23,14 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebElement; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + public class AndroidElementTest extends BaseAndroidTest { @BeforeEach public void setup() { - Activity activity = new Activity("io.appium.android.apis", ".ApiDemos"); - driver.startActivity(activity); + startActivity(".ApiDemos"); } @@ -57,8 +56,7 @@ public class AndroidElementTest extends BaseAndroidTest { @Test public void replaceValueTest() { String originalValue = "original value"; - Activity activity = new Activity("io.appium.android.apis", ".view.Controls1"); - driver.startActivity(activity); + startActivity(".view.Controls1"); WebElement editElement = driver .findElement(AppiumBy.androidUIAutomator("resourceId(\"io.appium.android.apis:id/edit\")")); editElement.sendKeys(originalValue); @@ -81,8 +79,7 @@ public class AndroidElementTest extends BaseAndroidTest { @Test public void setValueTest() { String value = "new value"; - Activity activity = new Activity("io.appium.android.apis", ".view.Controls1"); - driver.startActivity(activity); + startActivity(".view.Controls1"); WebElement editElement = driver .findElement(AppiumBy.androidUIAutomator("resourceId(\"io.appium.android.apis:id/edit\")")); editElement.sendKeys(value); diff --git a/src/test/java/io/appium/java_client/android/AndroidFunctionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java similarity index 96% rename from src/test/java/io/appium/java_client/android/AndroidFunctionTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java index 3b915eae1..0db6f2647 100644 --- a/src/test/java/io/appium/java_client/android/AndroidFunctionTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java @@ -1,12 +1,5 @@ package io.appium.java_client.android; -import static java.time.Duration.ofMillis; -import static java.time.Duration.ofSeconds; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.StringContains.containsString; -import static org.junit.jupiter.api.Assertions.assertThrows; - import io.appium.java_client.functions.AppiumFunction; import io.appium.java_client.functions.ExpectedCondition; import org.junit.jupiter.api.BeforeAll; @@ -25,6 +18,14 @@ 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; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class AndroidFunctionTest extends BaseAndroidTest { private final AppiumFunction> searchingFunction = input -> { @@ -68,15 +69,14 @@ public class AndroidFunctionTest extends BaseAndroidTest { @BeforeAll public static void startWebViewActivity() { if (driver != null) { - Activity activity = new Activity("io.appium.android.apis", ".view.WebView1"); - driver.startActivity(activity); + startActivity(".view.WebView1"); } } @BeforeEach public void setUp() { - driver.context("NATIVE_APP"); + driver.context(NATIVE_CONTEXT); } @Test diff --git a/src/test/java/io/appium/java_client/android/AndroidLogcatListenerTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidLogcatListenerTest.java similarity index 95% rename from src/test/java/io/appium/java_client/android/AndroidLogcatListenerTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidLogcatListenerTest.java index f1045c8cf..618da2e32 100644 --- a/src/test/java/io/appium/java_client/android/AndroidLogcatListenerTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidLogcatListenerTest.java @@ -1,7 +1,5 @@ package io.appium.java_client.android; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.apache.commons.lang3.time.DurationFormatUtils; import org.junit.jupiter.api.Test; @@ -9,6 +7,8 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class AndroidLogcatListenerTest extends BaseAndroidTest { @Test @@ -16,7 +16,7 @@ public void verifyLogcatListenerCanBeAssigned() { final Semaphore messageSemaphore = new Semaphore(1); final Duration timeout = Duration.ofSeconds(15); - driver.addLogcatMessagesListener((msg) -> messageSemaphore.release()); + driver.addLogcatMessagesListener(msg -> messageSemaphore.release()); driver.addLogcatConnectionListener(() -> System.out.println("Connected to the web socket")); driver.addLogcatDisconnectionListener(() -> System.out.println("Disconnected from the web socket")); driver.addLogcatErrorsListener(Throwable::printStackTrace); diff --git a/src/test/java/io/appium/java_client/android/AndroidScreenRecordTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java similarity index 84% rename from src/test/java/io/appium/java_client/android/AndroidScreenRecordTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java index 63510fc26..5fef68b48 100644 --- a/src/test/java/io/appium/java_client/android/AndroidScreenRecordTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java @@ -1,22 +1,22 @@ package io.appium.java_client.android; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.emptyString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.WebDriverException; 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; +import static org.hamcrest.Matchers.not; + public class AndroidScreenRecordTest extends BaseAndroidTest { @BeforeEach public void setUp() { - Activity activity = new Activity("io.appium.android.apis", ".ApiDemos"); - driver.startActivity(activity); + startActivity(".ApiDemos"); } @Test @@ -27,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/test/java/io/appium/java_client/android/AndroidSearchingTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidSearchingTest.java similarity index 91% rename from src/test/java/io/appium/java_client/android/AndroidSearchingTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidSearchingTest.java index 262b50c61..fb9275943 100644 --- a/src/test/java/io/appium/java_client/android/AndroidSearchingTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidSearchingTest.java @@ -16,30 +16,29 @@ package io.appium.java_client.android; -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 io.appium.java_client.AppiumBy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + public class AndroidSearchingTest extends BaseAndroidTest { @BeforeEach public void setup() { - Activity activity = new Activity("io.appium.android.apis", ".ApiDemos"); - driver.startActivity(activity); + startActivity(".ApiDemos"); } - @Test public void findByAccessibilityIdTest() { + @Test public void findByAccessibilityIdTest() { assertNotEquals(driver.findElement(AppiumBy.accessibilityId("Graphics")).getText(), null); assertEquals(driver.findElements(AppiumBy.accessibilityId("Graphics")).size(), 1); } - @Test public void findByAndroidUIAutomatorTest() { + @Test public void findByAndroidUIAutomatorTest() { assertNotEquals(driver .findElement(AppiumBy .androidUIAutomator("new UiSelector().clickable(true)")).getText(), null); diff --git a/src/test/java/io/appium/java_client/android/AndroidViewMatcherTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidViewMatcherTest.java similarity index 87% rename from src/test/java/io/appium/java_client/android/AndroidViewMatcherTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/AndroidViewMatcherTest.java index 852723694..80b60ab28 100644 --- a/src/test/java/io/appium/java_client/android/AndroidViewMatcherTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidViewMatcherTest.java @@ -16,8 +16,6 @@ package io.appium.java_client.android; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import io.appium.java_client.AppiumBy; import org.junit.jupiter.api.Test; import org.openqa.selenium.json.Json; @@ -25,6 +23,8 @@ import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; +import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -32,9 +32,9 @@ public class AndroidViewMatcherTest extends BaseEspressoTest { @Test public void testFindByViewMatcher() { - String selector = new Json().toJson(ImmutableMap.of( + String selector = new Json().toJson(Map.of( "name", "withText", - "args", ImmutableList.of("Animation"), + "args", List.of("Animation"), "class", "androidx.test.espresso.matcher.ViewMatchers" )); final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); diff --git a/src/test/java/io/appium/java_client/android/BaseAndroidTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java similarity index 80% rename from src/test/java/io/appium/java_client/android/BaseAndroidTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java index 3e75fe6f1..1325a0f85 100644 --- a/src/test/java/io/appium/java_client/android/BaseAndroidTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java @@ -18,13 +18,14 @@ 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; -import static io.appium.java_client.TestResources.apiDemosApk; +import java.util.Map; +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") public class BaseAndroidTest { public static final String APP_ID = "io.appium.android.apis"; protected static final int PORT = 4723; @@ -44,7 +45,8 @@ public class BaseAndroidTest { UiAutomator2Options options = new UiAutomator2Options() .setDeviceName("Android Emulator") - .setApp(apiDemosApk().toAbsolutePath().toString()) + .enableBiDi() + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .eventTimings(); driver = new AndroidDriver(service.getUrl(), options); } @@ -60,4 +62,13 @@ public class BaseAndroidTest { service.stop(); } } + + public static void startActivity(String name) { + driver.executeScript( + "mobile: startActivity", + Map.of( + "component", String.format("%s/%s", APP_ID, name) + ) + ); + } } diff --git a/src/test/java/io/appium/java_client/android/BaseEspressoTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java similarity index 92% rename from src/test/java/io/appium/java_client/android/BaseEspressoTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java index cfc690021..2245b1be3 100644 --- a/src/test/java/io/appium/java_client/android/BaseEspressoTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java @@ -19,11 +19,11 @@ 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; -import static io.appium.java_client.TestResources.apiDemosApk; - +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") public class BaseEspressoTest { private static AppiumDriverLocalService service; @@ -43,7 +43,7 @@ public class BaseEspressoTest { EspressoOptions options = new EspressoOptions() .setDeviceName("Android Emulator") - .setApp(apiDemosApk().toAbsolutePath().toString()) + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .eventTimings(); driver = new AndroidDriver(service.getUrl(), options); } diff --git a/src/test/java/io/appium/java_client/android/BatteryTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BatteryTest.java similarity index 100% rename from src/test/java/io/appium/java_client/android/BatteryTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/BatteryTest.java index 619fafac1..ae9fc9e26 100644 --- a/src/test/java/io/appium/java_client/android/BatteryTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BatteryTest.java @@ -16,13 +16,13 @@ package io.appium.java_client.android; +import org.junit.jupiter.api.Test; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import org.junit.jupiter.api.Test; - public class BatteryTest extends BaseAndroidTest { @Test public void veryGettingBatteryInformation() { diff --git a/src/test/java/io/appium/java_client/android/ClipboardTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ClipboardTest.java similarity index 86% rename from src/test/java/io/appium/java_client/android/ClipboardTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/ClipboardTest.java index cf9ee8581..8de3bda5c 100644 --- a/src/test/java/io/appium/java_client/android/ClipboardTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ClipboardTest.java @@ -16,15 +16,18 @@ package io.appium.java_client.android; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + public class ClipboardTest extends BaseAndroidTest { @BeforeEach public void setUp() { - driver.resetApp(); + driver.executeScript("mobile: terminateApp", Map.of("appId", APP_ID)); + driver.executeScript("mobile: activateApp", Map.of("appId", APP_ID)); } @Test public void verifySetAndGetClipboardText() { diff --git a/src/test/java/io/appium/java_client/android/ExecuteCDPCommandTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java similarity index 82% rename from src/test/java/io/appium/java_client/android/ExecuteCDPCommandTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java index fc1a138c6..1e0bff096 100644 --- a/src/test/java/io/appium/java_client/android/ExecuteCDPCommandTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java @@ -16,17 +16,15 @@ package io.appium.java_client.android; +import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.remote.MobileCapabilityType; import io.appium.java_client.service.local.AppiumDriverLocalService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; @@ -54,11 +52,9 @@ public void setUp() { service = AppiumDriverLocalService.buildDefaultService(); service.start(); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.CHROME); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, "UiAutomator2"); - driver = new AndroidDriver(service.getUrl(), capabilities); + driver = new AndroidDriver(service.getUrl(), new UiAutomator2Options() + .withBrowserName(MobileBrowserType.CHROME) + .setDeviceName("Android Emulator")); //This time out is set because test can be run on slow Android SDK emulator PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(5)), this); } diff --git a/src/test/java/io/appium/java_client/android/ExecuteDriverScriptTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteDriverScriptTest.java similarity index 100% rename from src/test/java/io/appium/java_client/android/ExecuteDriverScriptTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteDriverScriptTest.java diff --git a/src/test/java/io/appium/java_client/android/FingerPrintTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/FingerPrintTest.java similarity index 100% rename from src/test/java/io/appium/java_client/android/FingerPrintTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/FingerPrintTest.java index a32c482fe..4f1e17551 100644 --- a/src/test/java/io/appium/java_client/android/FingerPrintTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/FingerPrintTest.java @@ -16,8 +16,6 @@ package io.appium.java_client.android; -import static io.appium.java_client.AppiumBy.androidUIAutomator; - import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.service.local.AppiumDriverLocalService; import org.junit.jupiter.api.AfterAll; @@ -32,6 +30,8 @@ import java.time.Duration; +import static io.appium.java_client.AppiumBy.androidUIAutomator; + public class FingerPrintTest { private static AppiumDriverLocalService service; private static AndroidDriver driver; diff --git a/src/test/java/io/appium/java_client/android/ImagesComparisonTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ImagesComparisonTest.java similarity index 91% rename from src/test/java/io/appium/java_client/android/ImagesComparisonTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/ImagesComparisonTest.java index adc8795ea..3632bfbf8 100644 --- a/src/test/java/io/appium/java_client/android/ImagesComparisonTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ImagesComparisonTest.java @@ -16,12 +16,6 @@ package io.appium.java_client.android; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - import io.appium.java_client.imagecomparison.FeatureDetector; import io.appium.java_client.imagecomparison.FeaturesMatchingOptions; import io.appium.java_client.imagecomparison.FeaturesMatchingResult; @@ -30,15 +24,22 @@ import io.appium.java_client.imagecomparison.OccurrenceMatchingResult; import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; import io.appium.java_client.imagecomparison.SimilarityMatchingResult; -import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.Test; import org.openqa.selenium.OutputType; +import java.util.Base64; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + public class ImagesComparisonTest extends BaseAndroidTest { @Test public void verifyFeaturesMatching() { - byte[] screenshot = Base64.encodeBase64(driver.getScreenshotAs(OutputType.BYTES)); + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); FeaturesMatchingResult result = driver .matchImagesFeatures(screenshot, screenshot, new FeaturesMatchingOptions() .withDetectorName(FeatureDetector.ORB) @@ -56,7 +57,7 @@ public void verifyFeaturesMatching() { @Test public void verifyOccurrencesLookup() { - byte[] screenshot = Base64.encodeBase64(driver.getScreenshotAs(OutputType.BYTES)); + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); OccurrenceMatchingResult result = driver .findImageOccurrence(screenshot, screenshot, new OccurrenceMatchingOptions() .withEnabledVisualization()); @@ -66,7 +67,7 @@ public void verifyOccurrencesLookup() { @Test public void verifySimilarityCalculation() { - byte[] screenshot = Base64.encodeBase64(driver.getScreenshotAs(OutputType.BYTES)); + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); SimilarityMatchingResult result = driver .getImagesSimilarity(screenshot, screenshot, new SimilarityMatchingOptions() .withEnabledVisualization()); diff --git a/src/test/java/io/appium/java_client/android/KeyCodeTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/KeyCodeTest.java similarity index 96% rename from src/test/java/io/appium/java_client/android/KeyCodeTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/KeyCodeTest.java index 7a3ae47e3..7ed431166 100644 --- a/src/test/java/io/appium/java_client/android/KeyCodeTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/KeyCodeTest.java @@ -16,10 +16,6 @@ package io.appium.java_client.android; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.junit.jupiter.api.Assertions.assertTrue; - import io.appium.java_client.android.nativekey.AndroidKey; import io.appium.java_client.android.nativekey.KeyEvent; import io.appium.java_client.android.nativekey.KeyEventFlag; @@ -28,13 +24,16 @@ import org.junit.jupiter.api.Test; import org.openqa.selenium.By; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class KeyCodeTest extends BaseAndroidTest { private static final By PRESS_RESULT_VIEW = By.id("io.appium.android.apis:id/text"); @BeforeEach public void setUp() { - final Activity activity = new Activity(driver.getCurrentPackage(), ".text.KeyEventText"); - driver.startActivity(activity); + startActivity(".text.KeyEventText"); } @Test diff --git a/src/test/java/io/appium/java_client/android/LogEventTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/LogEventTest.java similarity index 94% rename from src/test/java/io/appium/java_client/android/LogEventTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/LogEventTest.java index a473fbe32..16d28f31e 100644 --- a/src/test/java/io/appium/java_client/android/LogEventTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/LogEventTest.java @@ -16,16 +16,16 @@ package io.appium.java_client.android; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; - import io.appium.java_client.serverevents.CommandEvent; import io.appium.java_client.serverevents.CustomEvent; -import io.appium.java_client.serverevents.TimedEvent; import io.appium.java_client.serverevents.ServerEvents; +import io.appium.java_client.serverevents.TimedEvent; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class LogEventTest extends BaseAndroidTest { @Test @@ -36,8 +36,8 @@ public void verifyLoggingCustomEvents() { driver.logEvent(evt); ServerEvents events = driver.getEvents(); boolean hasCustomEvent = events.events.stream().anyMatch((TimedEvent event) -> - event.name.equals("appium:funEvent") && - event.occurrences.get(0).intValue() > 0 + event.name.equals("appium:funEvent") + && event.occurrences.get(0).intValue() > 0 ); boolean hasCommandName = events.commands.stream().anyMatch((CommandEvent event) -> event.name.equals("logCustomEvent") diff --git a/src/test/java/io/appium/java_client/android/OpenNotificationsTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/OpenNotificationsTest.java similarity index 71% rename from src/test/java/io/appium/java_client/android/OpenNotificationsTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/OpenNotificationsTest.java index dde09bbc1..08bddc736 100644 --- a/src/test/java/io/appium/java_client/android/OpenNotificationsTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/OpenNotificationsTest.java @@ -1,24 +1,27 @@ package io.appium.java_client.android; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.openqa.selenium.By.id; - import org.junit.jupiter.api.Test; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.openqa.selenium.By.xpath; public class OpenNotificationsTest extends BaseAndroidTest { @Test public void openNotification() { - driver.closeApp(); + driver.executeScript("mobile: terminateApp", Map.of( + "appId", APP_ID + )); driver.openNotifications(); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20)); assertNotEquals(0, wait.until(input -> { List result = input - .findElements(id("com.android.systemui:id/settings_button")); + .findElements(xpath("//android.widget.Switch[contains(@content-desc, 'Wi-Fi')]")); return result.isEmpty() ? null : result; }).size()); diff --git a/src/test/java/io/appium/java_client/android/SettingTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/SettingTest.java similarity index 100% rename from src/test/java/io/appium/java_client/android/SettingTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/SettingTest.java diff --git a/src/test/java/io/appium/java_client/android/UIAutomator2Test.java b/src/e2eAndroidTest/java/io/appium/java_client/android/UIAutomator2Test.java similarity index 96% rename from src/test/java/io/appium/java_client/android/UIAutomator2Test.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/UIAutomator2Test.java index 6c2bf0857..47ac3239b 100644 --- a/src/test/java/io/appium/java_client/android/UIAutomator2Test.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/UIAutomator2Test.java @@ -1,8 +1,5 @@ package io.appium.java_client.android; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - import io.appium.java_client.AppiumBy; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; @@ -15,6 +12,9 @@ import java.time.Duration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + public class UIAutomator2Test extends BaseAndroidTest { @AfterEach @@ -59,8 +59,7 @@ public void testPortraitUpsideDown() { @Test public void testToastMSGIsDisplayed() { final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); - Activity activity = new Activity("io.appium.android.apis", ".view.PopupMenu1"); - driver.startActivity(activity); + startActivity(".view.PopupMenu1"); wait.until(ExpectedConditions.presenceOfElementLocated(AppiumBy .accessibilityId("Make a Popup!"))); diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java similarity index 85% rename from src/test/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java index 433aa2a1a..68e89ddb6 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/AndroidPageObjectTest.java @@ -16,16 +16,7 @@ package io.appium.java_client.pagefactory_tests; -import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; -import static java.time.Duration.ofSeconds; -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.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - import io.appium.java_client.android.BaseAndroidTest; - import io.appium.java_client.pagefactory.AndroidBy; import io.appium.java_client.pagefactory.AndroidFindAll; import io.appium.java_client.pagefactory.AndroidFindBy; @@ -33,6 +24,7 @@ import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.HowToUseLocators; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; @@ -43,8 +35,19 @@ import org.openqa.selenium.support.PageFactory; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; +import static java.time.Duration.ofSeconds; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"}) public class AndroidPageObjectTest extends BaseAndroidTest { private boolean populated = false; @@ -150,6 +153,10 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @FindBy(id = "fakeId") private List fakeElements; + @FindBy(className = "android.widget.TextView") + @CacheLookup + private List cachedViews; + @CacheLookup @FindBy(className = "android.widget.TextView") private WebElement cached; @@ -168,41 +175,41 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(id = "android:id/text1", priority = 2) @AndroidFindAll(value = { - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")"), - @AndroidBy(id = "android:id/fakeId")}, priority = 1) + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")"), + @AndroidBy(id = "android:id/fakeId")}, priority = 1) @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")") private WebElement androidElementViewFoundByMixedSearching; @AndroidFindBy(id = "android:id/text1", priority = 2) @AndroidFindAll(value = { - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")"), - @AndroidBy(id = "android:id/fakeId")}, priority = 1) + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")"), + @AndroidBy(id = "android:id/fakeId")}, priority = 1) @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")") private List androidElementsViewFoundByMixedSearching; @AndroidFindBys({ - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), - @AndroidBy(className = "android.widget.FrameLayout")}) + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), + @AndroidBy(className = "android.widget.FrameLayout")}) @AndroidFindBys({@AndroidBy(id = "android:id/text1", priority = 1), - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")")}) + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")")}) private WebElement androidElementViewFoundByMixedSearching2; @AndroidFindBys({ - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), - @AndroidBy(className = "android.widget.FrameLayout")}) + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), + @AndroidBy(className = "android.widget.FrameLayout")}) @AndroidFindBys({ - @AndroidBy(id = "android:id/text1", priority = 1), - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")")}) + @AndroidBy(id = "android:id/text1", priority = 1), + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")")}) private List androidElementsViewFoundByMixedSearching2; @HowToUseLocators(androidAutomation = ALL_POSSIBLE) @AndroidFindBy(id = "android:id/fakeId1") @AndroidFindBy(id = "android:id/fakeId2", priority = 1) @AndroidFindBys(value = { - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), - @AndroidBy(id = "android:id/text1", priority = 3), - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")", priority = 2), - @AndroidBy(className = "android.widget.FrameLayout")}, priority = 2) + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), + @AndroidBy(id = "android:id/text1", priority = 3), + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")", priority = 2), + @AndroidBy(className = "android.widget.FrameLayout")}, priority = 2) @AndroidFindBy(id = "android:id/fakeId3", priority = 3) @AndroidFindBy(id = "android:id/fakeId4", priority = 4) private WebElement androidElementViewFoundByMixedSearching3; @@ -211,10 +218,10 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(id = "android:id/fakeId1") @AndroidFindBy(id = "android:id/fakeId2", priority = 1) @AndroidFindBys(value = { - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), - @AndroidBy(id = "android:id/text1", priority = 3), - @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")", priority = 2), - @AndroidBy(className = "android.widget.FrameLayout")}, priority = 2) + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")", priority = 1), + @AndroidBy(id = "android:id/text1", priority = 3), + @AndroidBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")", priority = 2), + @AndroidBy(className = "android.widget.FrameLayout")}, priority = 2) @AndroidFindBy(id = "android:id/fakeId3", priority = 3) @AndroidFindBy(id = "android:id/fakeId4", priority = 4) private List androidElementsViewFoundByMixedSearching3; @@ -344,8 +351,22 @@ public class AndroidPageObjectTest extends BaseAndroidTest { assertNotEquals(ArrayList.class, fakeElements.getClass()); } - @Test public void checkCached() { + @Test public void checkCachedElements() { assertEquals(((RemoteWebElement) cached).getId(), ((RemoteWebElement) cached).getId()); + assertEquals(cached.hashCode(), cached.hashCode()); + //noinspection SimplifiableAssertion,EqualsWithItself + assertTrue(cached.equals(cached)); + } + + @Test public void checkCachedLists() { + assertEquals(cachedViews.hashCode(), cachedViews.hashCode()); + //noinspection SimplifiableAssertion,EqualsWithItself + assertTrue(cachedViews.equals(cachedViews)); + } + + @Test public void checkListHashing() { + assertFalse(cachedViews.isEmpty()); + assertEquals(cachedViews.size(), new HashSet<>(cachedViews).size()); } @Test @@ -365,6 +386,7 @@ public void checkThatElementSearchingThrowsExpectedExceptionIfChainedLocatorIsIn assertNotEquals(0, androidElementsViewFoundByMixedSearching.size()); } + @Disabled("FIXME") @Test public void checkMixedElementSearching2() { assertNotNull(androidElementViewFoundByMixedSearching2.getAttribute("text")); } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java similarity index 100% rename from src/test/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java index dad9d7a18..824261c52 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/pagefactory_tests/MobileBrowserCompatibilityTest.java @@ -16,8 +16,6 @@ package io.appium.java_client.pagefactory_tests; -import static java.time.Duration.ofSeconds; - import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.pagefactory.AndroidFindBy; @@ -37,6 +35,8 @@ import java.util.List; +import static java.time.Duration.ofSeconds; + public class MobileBrowserCompatibilityTest { private WebDriver driver; diff --git a/src/test/java/io/appium/java_client/service/local/ServerBuilderTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java similarity index 75% rename from src/test/java/io/appium/java_client/service/local/ServerBuilderTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java index cb97479e8..235e7a5e9 100644 --- a/src/test/java/io/appium/java_client/service/local/ServerBuilderTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java @@ -1,18 +1,32 @@ package io.appium.java_client.service.local; -import static io.appium.java_client.TestResources.apiDemosApk; -import static io.appium.java_client.TestUtils.getLocalIp4Address; +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; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + 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_IP_ADDRESS; +import static io.appium.java_client.service.local.AppiumServiceBuilder.BROADCAST_IP4_ADDRESS; import static io.appium.java_client.service.local.AppiumServiceBuilder.DEFAULT_APPIUM_PORT; 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; -import static java.nio.file.FileSystems.getDefault; import static java.util.Arrays.asList; import static java.util.Optional.ofNullable; import static java.util.concurrent.TimeUnit.SECONDS; @@ -24,23 +38,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.google.common.collect.ImmutableMap; -import io.appium.java_client.android.options.UiAutomator2Options; -import io.github.bonigarcia.wdm.WebDriverManager; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.nio.file.Path; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - @SuppressWarnings("ResultOfMethodCallIgnored") -public class ServerBuilderTest { +class ServerBuilderTest { /** * It may be impossible to find the path to the instance of appium server due to different circumstance. @@ -49,14 +48,10 @@ public class ServerBuilderTest { */ private static final String PATH_TO_APPIUM_NODE_IN_PROPERTIES = getProperty(APPIUM_PATH); - private static final Path ROOT_TEST_PATH = getDefault().getPath("src") - .resolve("test").resolve("java").resolve("io").resolve("appium").resolve("java_client"); - /** - * 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 = ROOT_TEST_PATH - .resolve("service").resolve("local").resolve("main.js"); + private static final Path PATH_T0_TEST_MAIN_JS = TestUtils.resourcePathToAbsolutePath("main.js"); private static String testIP; private AppiumDriverLocalService service; @@ -93,7 +88,7 @@ public void tearDown() throws Exception { } @Test - public void checkAbilityToAddLogMessageConsumer() { + void checkAbilityToAddLogMessageConsumer() { List log = new ArrayList<>(); service = buildDefaultService(); service.clearOutPutStreams(); @@ -103,44 +98,40 @@ public void checkAbilityToAddLogMessageConsumer() { } @Test - public void checkAbilityToStartDefaultService() { + void checkAbilityToStartDefaultService() { service = buildDefaultService(); service.start(); assertTrue(service.isRunning()); } @Test - public void checkAbilityToFindNodeDefinedInProperties() { - File definedNode = PATH_T0_TEST_MAIN_JS.toFile(); - setProperty(APPIUM_PATH, definedNode.getAbsolutePath()); - assertThat(new AppiumServiceBuilder().createArgs().get(0), is(definedNode.getAbsolutePath())); + void checkAbilityToFindNodeDefinedInProperties() { + setProperty(APPIUM_PATH, PATH_T0_TEST_MAIN_JS.toString()); + assertThat(new AppiumServiceBuilder().createArgs().get(0), is(PATH_T0_TEST_MAIN_JS.toString())); } @Test - public void checkAbilityToUseNodeDefinedExplicitly() { - File mainJS = PATH_T0_TEST_MAIN_JS.toFile(); - AppiumServiceBuilder builder = new AppiumServiceBuilder() - .withAppiumJS(mainJS); - assertThat(builder.createArgs().get(0), - is(mainJS.getAbsolutePath())); + void checkAbilityToUseNodeDefinedExplicitly() { + AppiumServiceBuilder builder = new AppiumServiceBuilder().withAppiumJS(PATH_T0_TEST_MAIN_JS.toFile()); + assertThat(builder.createArgs().get(0), is(PATH_T0_TEST_MAIN_JS.toString())); } @Test - public void checkAbilityToStartServiceOnAFreePort() { + void checkAbilityToStartServiceOnAFreePort() { service = new AppiumServiceBuilder().usingAnyFreePort().build(); service.start(); assertTrue(service.isRunning()); } @Test - public void checkAbilityToStartServiceUsingNonLocalhostIP() { + void checkAbilityToStartServiceUsingNonLocalhostIP() { service = new AppiumServiceBuilder().withIPAddress(testIP).build(); service.start(); assertTrue(service.isRunning()); } @Test - public void checkAbilityToStartServiceUsingFlags() { + void checkAbilityToStartServiceUsingFlags() { service = new AppiumServiceBuilder() .withArgument(CALLBACK_ADDRESS, testIP) .withArgument(SESSION_OVERRIDE) @@ -150,13 +141,13 @@ public void checkAbilityToStartServiceUsingFlags() { } @Test - public void checkAbilityToStartServiceUsingCapabilities() { + void checkAbilityToStartServiceUsingCapabilities() { UiAutomator2Options options = new UiAutomator2Options() .fullReset() .setNewCommandTimeout(Duration.ofSeconds(60)) .setAppPackage("io.appium.android.apis") .setAppActivity(".view.WebView1") - .setApp(apiDemosApk().toAbsolutePath().toString()) + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .setChromedriverExecutable(chromeManager.getDownloadedDriverPath()); service = new AppiumServiceBuilder().withCapabilities(options).build(); @@ -165,21 +156,20 @@ public void checkAbilityToStartServiceUsingCapabilities() { } @Test - public void checkAbilityToStartServiceUsingCapabilitiesAndFlags() { - File app = ROOT_TEST_PATH.resolve("ApiDemos-debug.apk").toFile(); + void checkAbilityToStartServiceUsingCapabilitiesAndFlags() { UiAutomator2Options options = new UiAutomator2Options() .fullReset() .setNewCommandTimeout(Duration.ofSeconds(60)) .setAppPackage("io.appium.android.apis") .setAppActivity(".view.WebView1") - .setApp(app.getAbsolutePath()) + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) .setChromedriverExecutable(chromeManager.getDownloadedDriverPath()) .amend("winPath", "C:\\selenium\\app.apk") .amend("unixPath", "/selenium/app.apk") .amend("quotes", "\"'") .setChromeOptions( - ImmutableMap.of("env", ImmutableMap.of("test", "value"), "val2", 0) + Map.of("env", Map.of("test", "value"), "val2", 0) ); service = new AppiumServiceBuilder() @@ -191,10 +181,10 @@ public void checkAbilityToStartServiceUsingCapabilitiesAndFlags() { } @Test - public void checkAbilityToChangeOutputStream() throws Exception { + void checkAbilityToChangeOutputStream() throws Exception { testLogFile = new File("test"); testLogFile.createNewFile(); - stream = new FileOutputStream(testLogFile); + stream = Files.newOutputStream(testLogFile.toPath()); service = buildDefaultService(); service.addOutPutStream(stream); service.start(); @@ -202,10 +192,10 @@ public void checkAbilityToChangeOutputStream() throws Exception { } @Test - public void checkAbilityToChangeOutputStreamAfterTheServiceIsStarted() throws Exception { + void checkAbilityToChangeOutputStreamAfterTheServiceIsStarted() throws Exception { testLogFile = new File("test"); testLogFile.createNewFile(); - stream = new FileOutputStream(testLogFile); + stream = Files.newOutputStream(testLogFile.toPath()); service = buildDefaultService(); service.start(); service.addOutPutStream(stream); @@ -214,7 +204,7 @@ public void checkAbilityToChangeOutputStreamAfterTheServiceIsStarted() throws Ex } @Test - public void checkAbilityToShutDownService() { + void checkAbilityToShutDownService() { service = buildDefaultService(); service.start(); service.stop(); @@ -222,7 +212,7 @@ public void checkAbilityToShutDownService() { } @Test - public void checkAbilityToStartAndShutDownFewServices() throws Exception { + void checkAbilityToStartAndShutDownFewServices() throws Exception { List services = asList( new AppiumServiceBuilder().usingAnyFreePort().build(), new AppiumServiceBuilder().usingAnyFreePort().build(), @@ -236,7 +226,7 @@ public void checkAbilityToStartAndShutDownFewServices() throws Exception { } @Test - public void checkAbilityToStartServiceWithLogFile() throws Exception { + void checkAbilityToStartServiceWithLogFile() throws Exception { testLogFile = new File("Log.txt"); testLogFile.createNewFile(); service = new AppiumServiceBuilder().withLogFile(testLogFile).build(); @@ -246,7 +236,7 @@ public void checkAbilityToStartServiceWithLogFile() throws Exception { } @Test - public void checkAbilityToStartServiceWithPortUsingFlag() { + void checkAbilityToStartServiceWithPortUsingFlag() { String port = "8996"; String expectedUrl = String.format("http://0.0.0.0:%s/", port); @@ -259,7 +249,7 @@ public void checkAbilityToStartServiceWithPortUsingFlag() { } @Test - public void checkAbilityToStartServiceWithPortUsingShortFlag() { + void checkAbilityToStartServiceWithPortUsingShortFlag() { String port = "8996"; String expectedUrl = String.format("http://0.0.0.0:%s/", port); @@ -272,7 +262,7 @@ public void checkAbilityToStartServiceWithPortUsingShortFlag() { } @Test - public void checkAbilityToStartServiceWithIpUsingFlag() { + void checkAbilityToStartServiceWithIpUsingFlag() { String expectedUrl = String.format("http://%s:4723/", testIP); service = new AppiumServiceBuilder() @@ -284,7 +274,7 @@ public void checkAbilityToStartServiceWithIpUsingFlag() { } @Test - public void checkAbilityToStartServiceWithIpUsingShortFlag() { + void checkAbilityToStartServiceWithIpUsingShortFlag() { String expectedUrl = String.format("http://%s:4723/", testIP); service = new AppiumServiceBuilder() @@ -296,7 +286,7 @@ public void checkAbilityToStartServiceWithIpUsingShortFlag() { } @Test - public void checkAbilityToStartServiceWithLogFileUsingFlag() { + void checkAbilityToStartServiceWithLogFileUsingFlag() { testLogFile = new File("Log2.txt"); service = new AppiumServiceBuilder() @@ -307,7 +297,7 @@ public void checkAbilityToStartServiceWithLogFileUsingFlag() { } @Test - public void checkAbilityToStartServiceWithLogFileUsingShortFlag() { + void checkAbilityToStartServiceWithLogFileUsingShortFlag() { testLogFile = new File("Log3.txt"); service = new AppiumServiceBuilder() @@ -318,37 +308,37 @@ public void checkAbilityToStartServiceWithLogFileUsingShortFlag() { } @Test - public void checkAbilityToStartServiceUsingValidBasePathWithMultiplePathParams() { - String baseUrl = String.format("http://%s:%d/", BROADCAST_IP_ADDRESS, DEFAULT_APPIUM_PORT); - String basePath = "wd/hub"; + void checkAbilityToStartServiceUsingValidBasePathWithMultiplePathParams() { + String basePath = "/wd/hub"; service = new AppiumServiceBuilder().withArgument(BASEPATH, basePath).build(); service.start(); assertTrue(service.isRunning()); + String baseUrl = String.format("http://%s:%d", BROADCAST_IP4_ADDRESS, DEFAULT_APPIUM_PORT); assertEquals(baseUrl + basePath + "/", service.getUrl().toString()); } @Test - public void checkAbilityToStartServiceUsingValidBasePathWithSinglePathParams() { - String baseUrl = String.format("http://%s:%d/", BROADCAST_IP_ADDRESS, DEFAULT_APPIUM_PORT); + void checkAbilityToStartServiceUsingValidBasePathWithSinglePathParams() { String basePath = "/wd/"; service = new AppiumServiceBuilder().withArgument(BASEPATH, basePath).build(); service.start(); assertTrue(service.isRunning()); + String baseUrl = String.format("http://%s:%d/", BROADCAST_IP4_ADDRESS, DEFAULT_APPIUM_PORT); assertEquals(baseUrl + basePath.substring(1), service.getUrl().toString()); } @Test - public void checkAbilityToValidateBasePathForEmptyBasePath() { + void checkAbilityToValidateBasePathForEmptyBasePath() { assertThrows(IllegalArgumentException.class, () -> new AppiumServiceBuilder().withArgument(BASEPATH, "")); } @Test - public void checkAbilityToValidateBasePathForBlankBasePath() { + void checkAbilityToValidateBasePathForBlankBasePath() { assertThrows(IllegalArgumentException.class, () -> new AppiumServiceBuilder().withArgument(BASEPATH, " ")); } @Test - public void checkAbilityToValidateBasePathForNullBasePath() { + void checkAbilityToValidateBasePathForNullBasePath() { assertThrows(NullPointerException.class, () -> new AppiumServiceBuilder().withArgument(BASEPATH, null)); } } diff --git a/src/test/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java similarity index 72% rename from src/test/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java index 3d248f462..131610d35 100644 --- a/src/test/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java @@ -19,45 +19,48 @@ import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobileCapabilityType; 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; import java.time.Duration; -import static io.appium.java_client.TestResources.apiDemosApk; +import static io.appium.java_client.remote.options.SupportsAppOption.APP_OPTION; +import static io.appium.java_client.remote.options.SupportsAutomationNameOption.AUTOMATION_NAME_OPTION; +import static io.appium.java_client.remote.options.SupportsDeviceNameOption.DEVICE_NAME_OPTION; import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; -public class StartingAppLocallyAndroidTest { +class StartingAppLocallyAndroidTest { @Test - public void startingAndroidAppWithCapabilitiesOnlyTest() { + void startingAndroidAppWithCapabilitiesOnlyTest() { AndroidDriver driver = new AndroidDriver(new UiAutomator2Options() .setDeviceName("Android Emulator") .autoGrantPermissions() - .setApp(apiDemosApk().toAbsolutePath().toString())); + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL)); try { Capabilities caps = driver.getCapabilities(); assertTrue(MobilePlatform.ANDROID.equalsIgnoreCase( - String.valueOf(caps.getCapability(MobileCapabilityType.PLATFORM_NAME))) + String.valueOf(caps.getCapability(PLATFORM_NAME))) ); - assertEquals(AutomationName.ANDROID_UIAUTOMATOR2, caps.getCapability(MobileCapabilityType.AUTOMATION_NAME)); - assertNotNull(caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - assertEquals(apiDemosApk().toAbsolutePath().toString(), caps.getCapability(MobileCapabilityType.APP)); + assertEquals(AutomationName.ANDROID_UIAUTOMATOR2, caps.getCapability(AUTOMATION_NAME_OPTION)); + assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); + assertEquals(TestUtils.ANDROID_APIDEMOS_APK_URL, caps.getCapability(APP_OPTION)); } finally { driver.quit(); } } @Test - public void startingAndroidAppWithCapabilitiesAndServiceTest() { + void startingAndroidAppWithCapabilitiesAndServiceTest() { AppiumServiceBuilder builder = new AppiumServiceBuilder() .withArgument(GeneralServerFlag.SESSION_OVERRIDE) .withArgument(GeneralServerFlag.STRICT_CAPS); @@ -65,27 +68,27 @@ public void startingAndroidAppWithCapabilitiesAndServiceTest() { AndroidDriver driver = new AndroidDriver(builder, new UiAutomator2Options() .setDeviceName("Android Emulator") .autoGrantPermissions() - .setApp(apiDemosApk().toAbsolutePath().toString())); + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL)); try { Capabilities caps = driver.getCapabilities(); assertTrue(MobilePlatform.ANDROID.equalsIgnoreCase( - String.valueOf(caps.getCapability(MobileCapabilityType.PLATFORM_NAME))) + String.valueOf(caps.getCapability(PLATFORM_NAME))) ); - assertNotNull(caps.getCapability(MobileCapabilityType.DEVICE_NAME)); + assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); } finally { driver.quit(); } } @Test - public void startingAndroidAppWithCapabilitiesAndFlagsOnServerSideTest() { + void startingAndroidAppWithCapabilitiesAndFlagsOnServerSideTest() { UiAutomator2Options serverOptions = new UiAutomator2Options() .setDeviceName("Android Emulator") .fullReset() .autoGrantPermissions() .setNewCommandTimeout(Duration.ofSeconds(60)) - .setApp(apiDemosApk().toAbsolutePath().toString()); + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL); WebDriverManager chromeManager = chromedriver(); chromeManager.setup(); @@ -105,9 +108,9 @@ public void startingAndroidAppWithCapabilitiesAndFlagsOnServerSideTest() { Capabilities caps = driver.getCapabilities(); assertTrue(MobilePlatform.ANDROID.equalsIgnoreCase( - String.valueOf(caps.getCapability(MobileCapabilityType.PLATFORM_NAME))) + String.valueOf(caps.getCapability(PLATFORM_NAME))) ); - assertNotNull(caps.getCapability(MobileCapabilityType.DEVICE_NAME)); + assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); } finally { driver.quit(); } diff --git a/src/test/java/io/appium/java_client/service/local/ThreadSafetyTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ThreadSafetyTest.java similarity index 95% rename from src/test/java/io/appium/java_client/service/local/ThreadSafetyTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/service/local/ThreadSafetyTest.java index 50d59a5df..8087da057 100644 --- a/src/test/java/io/appium/java_client/service/local/ThreadSafetyTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ThreadSafetyTest.java @@ -1,14 +1,12 @@ package io.appium.java_client.service.local; +import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.jupiter.api.Test; - -public class ThreadSafetyTest { +class ThreadSafetyTest { private final AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); private final Action run = new Action() { @@ -32,7 +30,8 @@ public class ThreadSafetyTest { }; private final Action stop2 = stop.clone(); - @Test public void whenFewTreadsDoTheSameWork() throws Throwable { + @Test + void whenFewTreadsDoTheSameWork() throws Throwable { TestThread runTestThread = new TestThread<>(run); TestThread runTestThread2 = new TestThread<>(run2); @@ -116,7 +115,8 @@ public class ThreadSafetyTest { } - @Test public void whenFewTreadsDoDifferentWork() throws Throwable { + @Test + void whenFewTreadsDoDifferentWork() throws Throwable { TestThread runTestThread = new TestThread<>(run); TestThread runTestThread2 = new TestThread<>(run2); diff --git a/src/test/java/io/appium/java_client/service/local/main.js b/src/e2eAndroidTest/resources/main.js similarity index 100% rename from src/test/java/io/appium/java_client/service/local/main.js rename to src/e2eAndroidTest/resources/main.js diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java new file mode 100644 index 000000000..a141f01ef --- /dev/null +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/BaseFlutterTest.java @@ -0,0 +1,115 @@ +package io.appium.java_client.android; + +import io.appium.java_client.AppiumBy; +import io.appium.java_client.android.options.UiAutomator2Options; +import io.appium.java_client.flutter.FlutterDriverOptions; +import io.appium.java_client.flutter.FlutterIntegrationTestDriver; +import io.appium.java_client.flutter.android.FlutterAndroidDriver; +import io.appium.java_client.flutter.commands.ScrollParameter; +import io.appium.java_client.flutter.ios.FlutterIOSDriver; +import io.appium.java_client.ios.options.XCUITestOptions; +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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import java.net.MalformedURLException; +import java.time.Duration; +import java.util.Optional; + +class BaseFlutterTest { + + private static final boolean IS_ANDROID = Optional + .ofNullable(System.getProperty("platform")) + .orElse("android") + .equalsIgnoreCase("android"); + private static final String APP_ID = IS_ANDROID + ? "com.example.appium_testing_app" : "com.example.appiumTestingApp"; + protected static final int PORT = 4723; + + 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. + */ + @BeforeAll + public static void beforeClass() { + service = new AppiumServiceBuilder() + .withIPAddress("127.0.0.1") + .usingPort(PORT) + // 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") + .build(); + service.start(); + } + + @BeforeEach + void startSession() throws MalformedURLException { + FlutterDriverOptions flutterOptions = new FlutterDriverOptions() + .setFlutterServerLaunchTimeout(Duration.ofMinutes(2)) + .setFlutterSystemPort(9999) + .setFlutterElementWaitTimeout(Duration.ofSeconds(10)) + .setFlutterEnableMockCamera(true); + + if (IS_ANDROID) { + driver = new FlutterAndroidDriver(service.getUrl(), flutterOptions + .setUiAutomator2Options(new UiAutomator2Options() + .setApp(System.getProperty("flutterApp")) + .setAutoGrantPermissions(true) + .eventTimings()) + ); + } else { + String deviceName = System.getenv("IOS_DEVICE_NAME") != null + ? System.getenv("IOS_DEVICE_NAME") + : "iPhone 12"; + String platformVersion = System.getenv("IOS_PLATFORM_VERSION") != null + ? System.getenv("IOS_PLATFORM_VERSION") + : "14.5"; + 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) + ); + } + } + + @AfterEach + void stopSession() { + if (driver != null) { + driver.quit(); + } + } + + @AfterAll + static void afterClass() { + if (service.isRunning()) { + service.stop(); + } + } + + void openScreen(String screenTitle) { + ScrollParameter scrollOptions = new ScrollParameter( + AppiumBy.flutterText(screenTitle), ScrollParameter.ScrollDirection.DOWN); + WebElement element = driver.scrollTillVisible(scrollOptions); + element.click(); + } +} diff --git a/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java new file mode 100644 index 000000000..9e7f60bda --- /dev/null +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/CommandTest.java @@ -0,0 +1,196 @@ +package io.appium.java_client.android; + +import io.appium.java_client.AppiumBy; +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; + +import java.io.IOException; +import java.time.Duration; + +import static java.lang.Boolean.parseBoolean; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CommandTest extends BaseFlutterTest { + + private static final AppiumBy.FlutterBy MESSAGE_FIELD = AppiumBy.flutterKey("message_field"); + private static final AppiumBy.FlutterBy TOGGLE_BUTTON = AppiumBy.flutterKey("toggle_button"); + + @Test + void testWaitCommand() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Lazy Loading"); + + WebElement messageField = driver.findElement(MESSAGE_FIELD); + WebElement toggleButton = driver.findElement(TOGGLE_BUTTON); + + assertEquals(messageField.getText(), "Hello world"); + toggleButton.click(); + assertEquals(messageField.getText(), "Hello world"); + + WaitParameter waitParameter = new WaitParameter().setLocator(MESSAGE_FIELD); + + driver.waitForInVisible(waitParameter); + assertEquals(0, driver.findElements(MESSAGE_FIELD).size()); + toggleButton.click(); + driver.waitForVisible(waitParameter); + assertEquals(1, driver.findElements(MESSAGE_FIELD).size()); + assertEquals(messageField.getText(), "Hello world"); + } + + @Test + void testScrollTillVisibleCommand() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Vertical Swiping"); + + WebElement firstElement = driver.scrollTillVisible(new ScrollParameter(AppiumBy.flutterText("Java"))); + assertTrue(parseBoolean(firstElement.getAttribute("displayed"))); + + WebElement lastElement = driver.scrollTillVisible(new ScrollParameter(AppiumBy.flutterText("Protractor"))); + assertTrue(parseBoolean(lastElement.getAttribute("displayed"))); + assertFalse(parseBoolean(firstElement.getAttribute("displayed"))); + + firstElement = driver.scrollTillVisible( + new ScrollParameter(AppiumBy.flutterText("Java"), + ScrollParameter.ScrollDirection.UP) + ); + assertTrue(parseBoolean(firstElement.getAttribute("displayed"))); + assertFalse(parseBoolean(lastElement.getAttribute("displayed"))); + } + + @Test + void testScrollTillVisibleWithScrollParametersCommand() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Vertical Swiping"); + + ScrollParameter scrollParameter = new ScrollParameter(AppiumBy.flutterText("Playwright")); + scrollParameter + .setScrollView(AppiumBy.flutterType("Scrollable")) + .setMaxScrolls(30) + .setDelta(30) + // Drag duration currently works when the value is greater than 33 secs + .setDragDuration(Duration.ofMillis(35000)) + .setSettleBetweenScrollsTimeout(5000); + + WebElement element = driver.scrollTillVisible(scrollParameter); + assertTrue(parseBoolean(element.getAttribute("displayed"))); + } + + @Test + void testDoubleClickCommand() { + driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click(); + openScreen("Double Tap"); + + WebElement doubleTapButton = driver + .findElement(AppiumBy.flutterKey("double_tap_button")) + .findElement(AppiumBy.flutterText("Double Tap")); + assertEquals("Double Tap", doubleTapButton.getText()); + + AppiumBy.FlutterBy okButton = AppiumBy.flutterText("Ok"); + AppiumBy.FlutterBy successPopup = AppiumBy.flutterTextContaining("Successful"); + + driver.performDoubleClick(new DoubleClickParameter().setElement(doubleTapButton)); + assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful"); + driver.findElement(okButton).click(); + + driver.performDoubleClick(new DoubleClickParameter() + .setElement(doubleTapButton) + .setOffset(new Point(10, 2)) + ); + assertEquals(driver.findElement(successPopup).getText(), "Double Tap Successful"); + driver.findElement(okButton).click(); + } + + @Test + void testLongPressCommand() { + driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click(); + openScreen("Long Press"); + + AppiumBy.FlutterBy successPopup = AppiumBy.flutterText("It was a long press"); + WebElement longPressButton = driver + .findElement(AppiumBy.flutterKey("long_press_button")); + + driver.performLongPress(new LongPressParameter().setElement(longPressButton)); + assertEquals(driver.findElement(successPopup).getText(), "It was a long press"); + assertTrue(driver.findElement(successPopup).isDisplayed()); + } + + @Test + void testDragAndDropCommand() { + driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click(); + openScreen("Drag & Drop"); + + driver.performDragAndDrop(new DragAndDropParameter( + driver.findElement(AppiumBy.flutterKey("drag_me")), + driver.findElement(AppiumBy.flutterKey("drop_zone")) + )); + assertTrue(driver.findElement(AppiumBy.flutterText("The box is dropped")).isDisplayed()); + assertEquals(driver.findElement(AppiumBy.flutterText("The box is dropped")).getText(), "The box is dropped"); + + } + + @Test + void testCameraMocking() throws IOException { + driver.findElement(BaseFlutterTest.LOGIN_BUTTON).click(); + openScreen("Image Picker"); + + final String successQr = driver.injectMockImage( + TestUtils.resourcePathToAbsolutePath("success_qr.png").toFile()); + driver.injectMockImage( + TestUtils.resourcePathToAbsolutePath("second_qr.png").toFile()); + + driver.findElement(AppiumBy.flutterKey("capture_image")).click(); + driver.findElement(AppiumBy.flutterText("PICK")).click(); + assertTrue(driver.findElement(AppiumBy.flutterText("SecondInjectedImage")).isDisplayed()); + + driver.activateInjectedImage(successQr); + + driver.findElement(AppiumBy.flutterKey("capture_image")).click(); + 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 new file mode 100644 index 000000000..f00301885 --- /dev/null +++ b/src/e2eFlutterTest/java/io/appium/java_client/android/FinderTests.java @@ -0,0 +1,84 @@ +package io.appium.java_client.android; + +import io.appium.java_client.AppiumBy; +import org.junit.jupiter.api.Test; +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 { + + @Test + void testFlutterByKey() { + WebElement userNameField = driver.findElement(AppiumBy.flutterKey("username_text_field")); + assertEquals("admin", userNameField.getText()); + userNameField.clear(); + driver.findElement(AppiumBy.flutterKey("username_text_field")).sendKeys("admin123"); + assertEquals("admin123", userNameField.getText()); + } + + @Test + void testFlutterByType() { + WebElement loginButton = driver.findElement(AppiumBy.flutterType("ElevatedButton")); + assertEquals(loginButton.findElement(AppiumBy.flutterType("Text")).getText(), "Login"); + } + + @Test + void testFlutterText() { + WebElement loginButton = driver.findElement(AppiumBy.flutterText("Login")); + assertEquals(loginButton.getText(), "Login"); + loginButton.click(); + + assertEquals(1, driver.findElements(AppiumBy.flutterText("Slider")).size()); + } + + @Test + void testFlutterTextContaining() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + assertEquals(driver.findElement(AppiumBy.flutterTextContaining("Vertical")).getText(), + "Vertical Swiping"); + } + + @Test + void testFlutterSemanticsLabel() { + WebElement loginButton = driver.findElement(BaseFlutterTest.LOGIN_BUTTON); + loginButton.click(); + openScreen("Lazy Loading"); + + WebElement messageField = driver.findElement(AppiumBy.flutterSemanticsLabel("message_field")); + 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/e2eFlutterTest/resources/second_qr.png b/src/e2eFlutterTest/resources/second_qr.png new file mode 100644 index 000000000..355548c30 Binary files /dev/null and b/src/e2eFlutterTest/resources/second_qr.png differ diff --git a/src/e2eFlutterTest/resources/success_qr.png b/src/e2eFlutterTest/resources/success_qr.png new file mode 100644 index 000000000..8896d86f6 Binary files /dev/null and b/src/e2eFlutterTest/resources/success_qr.png differ diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java new file mode 100644 index 000000000..58461127b --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/AppIOSTest.java @@ -0,0 +1,64 @@ +/* + * 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.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; + +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; + +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() throws MalformedURLException { + startAppiumServer(); + + XCUITestOptions options = new XCUITestOptions() + .setPlatformVersion(PLATFORM_VERSION) + .setDeviceName(DEVICE_NAME) + .setCommandTimeouts(Duration.ofSeconds(240)) + .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) { + options.useNewWDA(); + driver = new IOSDriver(service.getUrl(), options); + } + } +} diff --git a/src/test/java/io/appium/java_client/ios/BaseIOSTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java similarity index 69% rename from src/test/java/io/appium/java_client/ios/BaseIOSTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java index dfdf3c4da..baee302da 100644 --- a/src/test/java/io/appium/java_client/ios/BaseIOSTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java @@ -18,22 +18,26 @@ 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 { 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 Duration WDA_LAUNCH_TIMEOUT = Duration.ofSeconds(240); + 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. @@ -44,6 +48,8 @@ public static AppiumDriverLocalService startAppiumServer() { service = new AppiumServiceBuilder() .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/test/java/io/appium/java_client/ios/BaseIOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java similarity index 87% rename from src/test/java/io/appium/java_client/ios/BaseIOSWebViewTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java index c461ded8a..8c54b30c4 100644 --- a/src/test/java/io/appium/java_client/ios/BaseIOSWebViewTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java @@ -21,24 +21,29 @@ import org.openqa.selenium.SessionNotCreatedException; import java.io.IOException; +import java.net.URL; import java.time.Duration; import java.util.function.Supplier; -import static io.appium.java_client.TestResources.vodQaAppZip; +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(1); - private static final Duration WEB_VIEW_DETECT_DURATION = Duration.ofSeconds(15); + 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() throws IOException { startAppiumServer(); XCUITestOptions options = new XCUITestOptions() + .setPlatformVersion(PLATFORM_VERSION) .setDeviceName(DEVICE_NAME) .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT) .setCommandTimeouts(Duration.ofSeconds(240)) - .setApp(vodQaAppZip().toAbsolutePath().toString()); + .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/test/java/io/appium/java_client/ios/BaseSafariTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java similarity index 78% rename from src/test/java/io/appium/java_client/ios/BaseSafariTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java index 654c4e58b..7468e89e9 100644 --- a/src/test/java/io/appium/java_client/ios/BaseSafariTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java @@ -20,18 +20,23 @@ import io.appium.java_client.remote.MobileBrowserType; import org.junit.jupiter.api.BeforeAll; -import java.io.IOException; +import java.time.Duration; public class BaseSafariTest extends BaseIOSTest { + private static final Duration WEBVIEW_CONNECT_TIMEOUT = Duration.ofSeconds(30); - @BeforeAll public static void beforeClass() throws IOException { + @BeforeAll public static void beforeClass() { startAppiumServer(); XCUITestOptions options = new XCUITestOptions() .withBrowserName(MobileBrowserType.SAFARI) .setDeviceName(DEVICE_NAME) .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/test/java/io/appium/java_client/ios/ClipboardTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java similarity index 94% rename from src/test/java/io/appium/java_client/ios/ClipboardTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java index af6b8a51d..b3a58e588 100644 --- a/src/test/java/io/appium/java_client/ios/ClipboardTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java @@ -16,15 +16,15 @@ package io.appium.java_client.ios; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + 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/test/java/io/appium/java_client/ios/IOSAlertTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java similarity index 93% rename from src/test/java/io/appium/java_client/ios/IOSAlertTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java index 3c731e007..eea8899b5 100644 --- a/src/test/java/io/appium/java_client/ios/IOSAlertTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java @@ -16,11 +16,6 @@ package io.appium.java_client.ios; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; - -import io.appium.java_client.AppiumBy; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.MethodOrderer; @@ -33,35 +28,15 @@ import java.time.Duration; import java.util.function.Supplier; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; + @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 final WebDriverWait waiter = new WebDriverWait(driver, ALERT_TIMEOUT); - private static final String iOSAutomationText = "show alert"; - - private void ensureAlertPresence() { - int retry = 0; - // CI might not be performant enough, so we need to retry - while (true) { - try { - driver.findElement(AppiumBy.accessibilityId(iOSAutomationText)).click(); - } catch (WebDriverException e) { - // ignore - } - try { - waiter.until(alertIsPresent()); - return; - } catch (TimeoutException e) { - retry++; - if (retry >= CLICK_RETRIES) { - throw e; - } - } - } - } @AfterEach public void afterEach() { @@ -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/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/test/java/io/appium/java_client/ios/IOSContextTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java similarity index 68% rename from src/test/java/io/appium/java_client/ios/IOSContextTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java index 5c51aa677..b746edd24 100644 --- a/src/test/java/io/appium/java_client/ios/IOSContextTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSContextTest.java @@ -16,32 +16,44 @@ 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; import static org.junit.jupiter.api.Assertions.assertThrows; -import io.appium.java_client.NoSuchContextException; -import org.junit.jupiter.api.Test; - 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/test/java/io/appium/java_client/ios/IOSDriverTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java similarity index 59% rename from src/test/java/io/appium/java_client/ios/IOSDriverTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java index bcfd41d7d..c729d5b93 100644 --- a/src/test/java/io/appium/java_client/ios/IOSDriverTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java @@ -16,32 +16,27 @@ package io.appium.java_client.ios; -import static io.appium.java_client.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; -import static org.junit.jupiter.api.Assertions.fail; - -import com.google.common.collect.ImmutableMap; +import io.appium.java_client.Location; import io.appium.java_client.appmanagement.ApplicationState; -import io.appium.java_client.remote.HideKeyboardStrategy; 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.html5.Location; +import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.remote.Response; import org.openqa.selenium.remote.http.HttpMethod; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; +import java.util.Map; + +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class IOSDriverTest extends AppIOSTest { @BeforeEach @@ -53,27 +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/launch", "launchApplication"); - final Response launchApplication = driver.execute("launchApplication"); - assertNotNull(launchApplication.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", - ImmutableMap.of("id", ((RemoteWebElement) intA).getId(), "value", "8")); + Map.of("id", ((RemoteWebElement) usernameEdit).getId(), "text", "foo")); assertNotNull(setNewValue.getSessionId()); } @@ -84,19 +79,13 @@ public void getDeviceTimeTest() { } @Test public void resetTest() { - driver.resetApp(); - } - - @Test public void hideKeyboardWithParametersTest() { - new WebDriverWait(driver, Duration.ofSeconds(30)) - .until(ExpectedConditions.presenceOfElementLocated(By.id("IntegerA"))) - .click(); - driver.hideKeyboard(HideKeyboardStrategy.PRESS_KEY, "Done"); + driver.executeScript("mobile: terminateApp", Map.of("bundleId", BUNDLE_ID)); + driver.executeScript("mobile: activateApp", Map.of("bundleId", BUNDLE_ID)); } @Disabled @Test public void geolocationTest() { - Location location = new Location(45, 45, 100); + Location location = new Location(45, 45, 100.0); try { driver.setLocation(location); } catch (Exception e) { @@ -105,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() { @@ -122,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(); @@ -140,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)); @@ -153,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 - 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 new file mode 100644 index 000000000..1439a4100 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSElementTest.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; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.openqa.selenium.By.className; + +public class IOSElementTest extends AppIOSTest { + private static final By SLIDER_CLASS = className("XCUIElementTypeSlider"); + + @Test + public void setValueTest() { + driver.findElement(LOGIN_LINK_ID).click(); + driver.findElement(SLIDER_MENU_ITEM_PREDICATE).click(); + + 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/test/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java similarity index 93% rename from src/test/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java index e4d27f327..8c1bc3fee 100644 --- a/src/test/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java @@ -1,20 +1,18 @@ package io.appium.java_client.ios; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.time.Duration; - -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertTrue; + public class IOSNativeWebTapSettingTest extends BaseSafariTest { @Test - @Disabled("https://github.com/appium/appium/issues/17014") public void nativeWebTapSettingTest() { assertTrue(driver.isBrowser()); driver.get("https://saucelabs.com/test/guinea-pig"); diff --git a/src/test/java/io/appium/java_client/ios/IOSScreenRecordTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSScreenRecordTest.java similarity index 100% rename from src/test/java/io/appium/java_client/ios/IOSScreenRecordTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSScreenRecordTest.java index cf88b7b4a..170ea9b6a 100644 --- a/src/test/java/io/appium/java_client/ios/IOSScreenRecordTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSScreenRecordTest.java @@ -1,14 +1,14 @@ package io.appium.java_client.ios; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import org.junit.jupiter.api.Test; - -import java.time.Duration; - public class IOSScreenRecordTest extends AppIOSTest { @Test diff --git a/src/test/java/io/appium/java_client/ios/IOSAppStringsTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java similarity index 55% rename from src/test/java/io/appium/java_client/ios/IOSAppStringsTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java index 6080737f8..e3fa303e6 100644 --- a/src/test/java/io/appium/java_client/ios/IOSAppStringsTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java @@ -16,21 +16,25 @@ package io.appium.java_client.ios; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - import org.junit.jupiter.api.Test; -public class IOSAppStringsTest extends AppIOSTest { +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; - @Test public void getAppStrings() { - assertNotEquals(0, driver.getAppStringMap().size()); +public class IOSSearchingTest extends AppIOSTest { + @Test + public void findByAccessibilityIdTest() { + assertNotNull(driver.findElement(LOGIN_LINK_ID).getText()); + assertNotEquals(0, driver.findElements(LOGIN_LINK_ID).size()); } - @Test public void getGetAppStringsUsingLang() { - assertNotEquals(0, driver.getAppStringMap("en").size()); + @Test + public void findByByIosPredicatesTest() { + assertNotNull(driver.findElement(USERNAME_EDIT_PREDICATE).getText()); + assertNotEquals(0, driver.findElements(USERNAME_EDIT_PREDICATE).size()); } - @Test public void getAppStringsUsingLangAndFileStrings() { - assertNotEquals(0, driver.getAppStringMap("en", "Localizable.strings").size()); + @Test public void findByByIosClassChainTest() { + assertNotEquals(0, driver.findElements(VODQA_LOGO_CLASS_CHAIN).size()); } } diff --git a/src/test/java/io/appium/java_client/ios/IOSSyslogListenerTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSyslogListenerTest.java similarity index 95% rename from src/test/java/io/appium/java_client/ios/IOSSyslogListenerTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSSyslogListenerTest.java index d5e2b377f..b2c4c96bd 100644 --- a/src/test/java/io/appium/java_client/ios/IOSSyslogListenerTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSyslogListenerTest.java @@ -1,7 +1,5 @@ package io.appium.java_client.ios; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.apache.commons.lang3.time.DurationFormatUtils; import org.junit.jupiter.api.Test; @@ -9,6 +7,8 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class IOSSyslogListenerTest extends AppIOSTest { @Test @@ -16,7 +16,7 @@ public void verifySyslogListenerCanBeAssigned() { final Semaphore messageSemaphore = new Semaphore(1); final Duration timeout = Duration.ofSeconds(15); - driver.addSyslogMessagesListener((msg) -> messageSemaphore.release()); + driver.addSyslogMessagesListener(msg -> messageSemaphore.release()); driver.addSyslogConnectionListener(() -> System.out.println("Connected to the web socket")); driver.addSyslogDisconnectionListener(() -> System.out.println("Disconnected from the web socket")); driver.addSyslogErrorsListener(Throwable::printStackTrace); diff --git a/src/test/java/io/appium/java_client/ios/IOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java similarity index 85% rename from src/test/java/io/appium/java_client/ios/IOSWebViewTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java index d03f1acdc..1895e3517 100644 --- a/src/test/java/io/appium/java_client/ios/IOSWebViewTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java @@ -1,20 +1,25 @@ package io.appium.java_client.ios; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.time.Duration; - 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; import org.openqa.selenium.support.ui.WebDriverWait; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertTrue; + public class IOSWebViewTest extends BaseIOSWebViewTest { private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(30); @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/test/java/io/appium/java_client/ios/ImagesComparisonTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/ImagesComparisonTest.java similarity index 91% rename from src/test/java/io/appium/java_client/ios/ImagesComparisonTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/ImagesComparisonTest.java index ef52196b5..8534f8f35 100644 --- a/src/test/java/io/appium/java_client/ios/ImagesComparisonTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/ImagesComparisonTest.java @@ -16,12 +16,6 @@ package io.appium.java_client.ios; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - import io.appium.java_client.imagecomparison.FeatureDetector; import io.appium.java_client.imagecomparison.FeaturesMatchingOptions; import io.appium.java_client.imagecomparison.FeaturesMatchingResult; @@ -30,15 +24,22 @@ import io.appium.java_client.imagecomparison.OccurrenceMatchingResult; import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; import io.appium.java_client.imagecomparison.SimilarityMatchingResult; -import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.Test; import org.openqa.selenium.OutputType; +import java.util.Base64; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + public class ImagesComparisonTest extends AppIOSTest { @Test public void verifyFeaturesMatching() { - byte[] screenshot = Base64.encodeBase64(driver.getScreenshotAs(OutputType.BYTES)); + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); FeaturesMatchingResult result = driver .matchImagesFeatures(screenshot, screenshot, new FeaturesMatchingOptions() .withDetectorName(FeatureDetector.ORB) @@ -56,7 +57,7 @@ public void verifyFeaturesMatching() { @Test public void verifyOccurrencesSearch() { - byte[] screenshot = Base64.encodeBase64(driver.getScreenshotAs(OutputType.BYTES)); + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); OccurrenceMatchingResult result = driver .findImageOccurrence(screenshot, screenshot, new OccurrenceMatchingOptions() .withEnabledVisualization()); @@ -66,7 +67,7 @@ public void verifyOccurrencesSearch() { @Test public void verifySimilarityCalculation() { - byte[] screenshot = Base64.encodeBase64(driver.getScreenshotAs(OutputType.BYTES)); + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); SimilarityMatchingResult result = driver .getImagesSimilarity(screenshot, screenshot, new SimilarityMatchingOptions() .withEnabledVisualization()); diff --git a/src/test/java/io/appium/java_client/ios/RotationTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/RotationTest.java similarity index 100% rename from src/test/java/io/appium/java_client/ios/RotationTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/RotationTest.java index 21d177fff..1d741845f 100644 --- a/src/test/java/io/appium/java_client/ios/RotationTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/RotationTest.java @@ -16,12 +16,12 @@ package io.appium.java_client.ios; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.openqa.selenium.DeviceRotation; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class RotationTest extends AppIOSTest { @AfterEach public void afterMethod() { diff --git a/src/test/java/io/appium/java_client/ios/SettingTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/SettingTest.java similarity index 100% rename from src/test/java/io/appium/java_client/ios/SettingTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/SettingTest.java index 9fc2bfa4b..647b93e2d 100644 --- a/src/test/java/io/appium/java_client/ios/SettingTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/SettingTest.java @@ -20,12 +20,12 @@ import io.appium.java_client.Setting; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - import java.util.EnumMap; import java.util.HashMap; import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class SettingTest extends AppIOSTest { @Test public void testSetShouldUseCompactResponses() { 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 new file mode 100644 index 000000000..7d89bd331 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java @@ -0,0 +1,130 @@ +/* + * 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.pagefactory_tests; + +import io.appium.java_client.ios.AppIOSTest; +import io.appium.java_client.pagefactory.AppiumFieldDecorator; +import io.appium.java_client.pagefactory.HowToUseLocators; +import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.PageFactory; + +import java.util.List; + +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.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.MethodName.class) +public class XCUITModeTest extends AppIOSTest { + + private boolean populated = false; + + @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) + @iOSXCUITFindBy(iOSNsPredicate = "name == \"assets/assets/vodqa.png\"") + @iOSXCUITFindBy(className = "XCUIElementTypeImage") + private WebElement logoImageAllPossible; + + @HowToUseLocators(iOSXCUITAutomation = CHAIN) + @iOSXCUITFindBy(iOSNsPredicate = "name CONTAINS 'vodqa'") + private WebElement logoImageChain; + + @iOSXCUITFindBy(iOSNsPredicate = "name == 'username'") + private WebElement usernameFieldPredicate; + + @iOSXCUITFindBy(iOSNsPredicate = "name ENDSWITH '.png'") + private WebElement logoImagePredicate; + + @iOSXCUITFindBy(className = "XCUIElementTypeImage") + private WebElement logoImageClass; + + @iOSXCUITFindBy(accessibility = "login") + private WebElement loginLinkAccId; + + @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeTextField[`name == \"username\"`]") + private WebElement usernameFieldClassChain; + + @iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeSecureTextField[`name == \"password\"`][-1]") + private WebElement passwordFieldClassChain; + + @iOSXCUITFindBy(iOSClassChain = "**/*[`type CONTAINS \"TextField\"`]") + private List allTextFields; + + /** + * The setting up. + */ + @BeforeEach + public void setUp() { + if (!populated) { + PageFactory.initElements(new AppiumFieldDecorator(driver), this); + } + + populated = true; + } + + @Test + public void findByXCUITSelectorTest() { + assertTrue(logoImageAllPossible.isDisplayed()); + } + + @Test + public void findElementByNameTest() { + assertTrue(usernameFieldPredicate.isDisplayed()); + } + + @Test + public void findElementByClassNameTest() { + assertTrue(logoImageClass.isDisplayed()); + } + + @Test + public void pageObjectChainingTest() { + assertTrue(logoImageChain.isDisplayed()); + } + + @Test + public void findElementByIdTest() { + assertTrue(loginLinkAccId.isDisplayed()); + } + + @Test + public void nativeSelectorTest() { + assertTrue(logoImagePredicate.isDisplayed()); + } + + @Test + public void findElementByClassChain() { + assertTrue(usernameFieldClassChain.isDisplayed()); + } + + @Test + public void findElementByClassChainWithNegativeIndex() { + assertTrue(passwordFieldClassChain.isDisplayed()); + } + + @Test + public void findMultipleElementsByClassChain() { + assertThat(allTextFields.size(), is(greaterThan(1))); + } +} diff --git a/src/test/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java b/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java similarity index 74% rename from src/test/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java rename to src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java index ec629f8ba..256c43835 100644 --- a/src/test/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java @@ -16,30 +16,36 @@ package io.appium.java_client.service.local; -import static io.appium.java_client.TestResources.uiCatalogAppZip; -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; - import io.appium.java_client.ios.BaseIOSTest; import io.appium.java_client.ios.IOSDriver; import io.appium.java_client.ios.options.XCUITestOptions; import io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobileCapabilityType; import io.appium.java_client.remote.MobilePlatform; import io.appium.java_client.service.local.flags.GeneralServerFlag; import org.junit.jupiter.api.Test; import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; -public class StartingAppLocallyIosTest { +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; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; + +class StartingAppLocallyIosTest { @Test - public 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(uiCatalogAppZip().toAbsolutePath().toString()) + .setApp(appUrl) .setWdaLaunchTimeout(BaseIOSTest.WDA_LAUNCH_TIMEOUT); IOSDriver driver = new IOSDriver(options); try { @@ -49,49 +55,52 @@ public void startingIOSAppWithCapabilitiesOnlyTest() { assertEquals(Platform.IOS, caps.getPlatformName()); assertNotNull(caps.getDeviceName().orElse(null)); assertEquals(BaseIOSTest.PLATFORM_VERSION, caps.getPlatformVersion().orElse(null)); - assertEquals(uiCatalogAppZip().toAbsolutePath().toString(), caps.getApp().orElse(null)); + assertEquals(appUrl.toString(), caps.getApp().orElse(null)); } finally { driver.quit(); } } - @Test - public 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(uiCatalogAppZip().toAbsolutePath().toString()) + .setApp(appUrl) .setWdaLaunchTimeout(BaseIOSTest.WDA_LAUNCH_TIMEOUT); AppiumServiceBuilder builder = new AppiumServiceBuilder() .withArgument(GeneralServerFlag.SESSION_OVERRIDE) - .withArgument(GeneralServerFlag.STRICT_CAPS); + .withArgument(GeneralServerFlag.STRICT_CAPS) + .withTimeout(BaseIOSTest.SERVER_START_TIMEOUT); IOSDriver driver = new IOSDriver(builder, options); try { Capabilities caps = driver.getCapabilities(); - assertTrue(caps.getCapability(MobileCapabilityType.PLATFORM_NAME) + assertTrue(Objects.requireNonNull(caps.getCapability(PLATFORM_NAME)) .toString().equalsIgnoreCase(MobilePlatform.IOS)); - assertNotNull(caps.getCapability(MobileCapabilityType.DEVICE_NAME)); + assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); } finally { driver.quit(); } } @Test - public 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(uiCatalogAppZip().toAbsolutePath().toString()); + .setApp(appUrl); AppiumServiceBuilder builder = new AppiumServiceBuilder() .withArgument(GeneralServerFlag.SESSION_OVERRIDE) .withArgument(GeneralServerFlag.STRICT_CAPS) + .withTimeout(BaseIOSTest.SERVER_START_TIMEOUT) .withCapabilities(serverOptions); IOSDriver driver = new IOSDriver(builder, clientOptions); diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java index 10a8c5b76..1c24b29c1 100644 --- a/src/main/java/io/appium/java_client/AppiumBy.java +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -16,36 +16,47 @@ package io.appium.java_client; +import com.google.common.base.Preconditions; +import lombok.EqualsAndHashCode; import lombok.Getter; -import org.apache.commons.lang3.Validate; import org.openqa.selenium.By; 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; + +@EqualsAndHashCode(callSuper = true) public abstract class AppiumBy extends By implements Remotable { - @Getter private final Parameters remoteParameters; + @Getter + private final Parameters remoteParameters; private final String locatorName; protected AppiumBy(String selector, String locatorString, String locatorName) { - Validate.notBlank(locatorString, "Must supply a not empty locator value."); + Preconditions.checkArgument(!isNullOrEmpty(locatorString), "Must supply a not empty locator value."); this.remoteParameters = new Parameters(selector, locatorString); this.locatorName = locatorName; } - @Override public List findElements(SearchContext context) { + @Override + public List findElements(SearchContext context) { return context.findElements(this); } - @Override public WebElement findElement(SearchContext context) { + @Override + public WebElement findElement(SearchContext context) { return context.findElement(this); } - @Override public String toString() { + @Override + public String toString() { return String.format("%s.%s: %s", AppiumBy.class.getSimpleName(), locatorName, remoteParameters.value()); } @@ -55,6 +66,7 @@ protected AppiumBy(String selector, String locatorString, String locatorName) { * About iOS accessibility * https://developer.apple.com/library/ios/documentation/UIKit/Reference/ * UIAccessibilityIdentification_Protocol/index.html + * * @param accessibilityId id is a convenient UI automation accessibility Id. * @return an instance of {@link AppiumBy.ByAndroidUIAutomator} */ @@ -64,9 +76,10 @@ public static By accessibilityId(final String accessibilityId) { /** * This locator strategy is only available in Espresso Driver mode. + * * @param dataMatcherString is a valid json string detailing hamcrest matcher for Espresso onData(). - * See - * the documentation for more details + * See + * the documentation for more details * @return an instance of {@link AppiumBy.ByAndroidDataMatcher} */ public static By androidDataMatcher(final String dataMatcherString) { @@ -74,9 +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); @@ -84,9 +98,10 @@ public static By androidUIAutomator(final String uiautomatorText) { /** * This locator strategy is only available in Espresso Driver mode. + * * @param viewMatcherString is a valid json string detailing hamcrest matcher for Espresso onView(). - * See - * the documentation for more details + * See + * the documentation for more details * @return an instance of {@link AppiumBy.ByAndroidViewMatcher} */ public static By androidViewMatcher(final String viewMatcherString) { @@ -95,9 +110,10 @@ public static By androidViewMatcher(final String viewMatcherString) { /** * This locator strategy is available in Espresso Driver mode. - * @since Appium 1.8.2 beta + * * @param tag is a view tag string * @return an instance of {@link ByAndroidViewTag} + * @since Appium 1.8.2 beta */ public static By androidViewTag(final String tag) { return new ByAndroidViewTag(tag); @@ -106,6 +122,7 @@ public static By androidViewTag(final String tag) { /** * For IOS it is the full name of the XCUI element and begins with XCUIElementType. * For Android it is the full name of the UIAutomator2 class (e.g.: android.widget.TextView) + * * @param selector the class name of the element * @return an instance of {@link ByClassName} */ @@ -116,6 +133,7 @@ public static By className(final String selector) { /** * For IOS the element name. * For Android it is the resource identifier. + * * @param selector element id * @return an instance of {@link ById} */ @@ -126,6 +144,7 @@ public static By id(final String selector) { /** * For IOS the element name. * For Android it is the resource identifier. + * * @param selector element id * @return an instance of {@link ByName} */ @@ -147,16 +166,16 @@ public static By custom(final String selector) { /** * This locator strategy is available only if OpenCV libraries and - * NodeJS bindings are installed on the server machine. + * Node.js bindings are installed on the server machine. * - * @see - * The documentation on Image Comparison Features - * @see - * The settings available for lookup fine-tuning - * @since Appium 1.8.2 * @param b64Template base64-encoded template image string. Supported image formats are the same * as for OpenCV library. * @return an instance of {@link ByImage} + * @see + * The documentation on Image Comparison Features + * @see + * The settings available for lookup fine-tuning + * @since Appium 1.8.2 */ public static By image(final String b64Template) { return new ByImage(b64Template); @@ -164,6 +183,7 @@ public static By image(final String b64Template) { /** * This locator strategy is available in XCUITest Driver mode. + * * @param iOSClassChainString is a valid class chain locator string. * See * the documentation for more details @@ -175,6 +195,7 @@ public static By iOSClassChain(final String iOSClassChainString) { /** * This locator strategy is available in XCUITest Driver mode. + * * @param iOSNsPredicateString is an iOS NsPredicate String * @return an instance of {@link AppiumBy.ByIosNsPredicate} */ @@ -182,6 +203,107 @@ public static By iOSNsPredicateString(final String iOSNsPredicateString) { return new ByIosNsPredicate(iOSNsPredicateString); } + /** + * This locator strategy is available in FlutterIntegration Driver mode. + * + * @param selector is the value defined to the key attribute of the flutter element + * @return an instance of {@link AppiumBy.ByFlutterKey} + */ + public static FlutterBy flutterKey(final String selector) { + return new ByFlutterKey(selector); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode. + * + * @param selector is the Type of widget mounted in the app tree + * @return an instance of {@link AppiumBy.ByFlutterType} + */ + public static FlutterBy flutterType(final String selector) { + return new ByFlutterType(selector); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode. + * + * @param selector is the text that is present on the widget + * @return an instance of {@link AppiumBy.ByFlutterText} + */ + public static FlutterBy flutterText(final String selector) { + return new ByFlutterText(selector); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode. + * + * @param selector is the text that is partially present on the widget + * @return an instance of {@link AppiumBy.ByFlutterTextContaining} + */ + public static FlutterBy flutterTextContaining(final String selector) { + return new ByFlutterTextContaining(selector); + } + + /** + * This locator strategy is available in FlutterIntegration Driver mode. + * + * @param semanticsLabel represents the value assigned to the label attribute of semantics element + * @return an instance of {@link AppiumBy.ByFlutterSemanticsLabel} + */ + 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"); @@ -253,6 +375,86 @@ protected ByIosNsPredicate(String locatorString) { super("-ios predicate string", locatorString, "iOSNsPredicate"); } } -} + public abstract static class FlutterBy extends AppiumBy { + protected FlutterBy(String selector, String locatorString, String locatorName) { + super(selector, locatorString, 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"); + } + } + + public static class ByFlutterKey extends FlutterBy implements Serializable { + protected ByFlutterKey(String locatorString) { + super("-flutter key", locatorString, "flutterKey"); + } + } + + public static class ByFlutterSemanticsLabel extends FlutterBy implements Serializable { + protected ByFlutterSemanticsLabel(String locatorString) { + super("-flutter semantics label", locatorString, "flutterSemanticsLabel"); + } + } + + public static class ByFlutterText extends FlutterBy implements Serializable { + protected ByFlutterText(String locatorString) { + super("-flutter text", locatorString, "flutterText"); + } + } + + public static class ByFlutterTextContaining extends FlutterBy implements Serializable { + protected ByFlutterTextContaining(String locatorString) { + super("-flutter text containing", locatorString, "flutterTextContaining"); + } + } + + 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 new file mode 100644 index 000000000..49097f341 --- /dev/null +++ b/src/main/java/io/appium/java_client/AppiumClientConfig.java @@ -0,0 +1,232 @@ +/* + * 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; + +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.net.ssl.SSLContext; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.Duration; + +/** + * A class to store the appium http client configuration. + */ +public class AppiumClientConfig extends ClientConfig { + private final boolean directConnect; + + private static final Filter DEFAULT_FILTERS = new AppiumUserAgentFilter() + .andThen(new AppiumIdempotencyFilter()); + + private static final String DEFAULT_HTTP_VERSION = "HTTP_1_1"; + + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(10); + + private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(10); + + /** + * Client side configuration. + * + * @param baseUri Base URL the client sends HTTP request to. + * @param connectionTimeout The client connection timeout. + * @param readTimeout The client read timeout. + * @param filters Filters to modify incoming {@link org.openqa.selenium.remote.http.HttpRequest} or outgoing + * {@link org.openqa.selenium.remote.http.HttpResponse}. + * @param proxy The client proxy preference. + * @param credentials Credentials used for authenticating http requests + * @param sslContext SSL context (if present) + * @param directConnect If directConnect is enabled. + */ + protected AppiumClientConfig( + URI baseUri, + Duration connectionTimeout, + Duration readTimeout, + Filter filters, + @Nullable Proxy proxy, + @Nullable Credentials credentials, + @Nullable SSLContext sslContext, + @Nullable String version, + Boolean directConnect) { + super(baseUri, connectionTimeout, readTimeout, filters, proxy, credentials, sslContext, version); + + this.directConnect = Require.nonNull("Direct Connect", directConnect); + } + + /** + * Return the instance of {@link AppiumClientConfig} with a default config. + * @return the instance of {@link AppiumClientConfig}. + */ + public static AppiumClientConfig defaultConfig() { + return new AppiumClientConfig( + null, + DEFAULT_CONNECTION_TIMEOUT, + DEFAULT_READ_TIMEOUT, + DEFAULT_FILTERS, + null, + null, + null, + DEFAULT_HTTP_VERSION, + false); + } + + /** + * Return the instance of {@link AppiumClientConfig} from the given {@link ClientConfig} parameters. + * @param clientConfig take a look at {@link ClientConfig} + * @return the instance of {@link AppiumClientConfig}. + */ + public static AppiumClientConfig fromClientConfig(ClientConfig clientConfig) { + return new AppiumClientConfig( + clientConfig.baseUri(), + clientConfig.connectionTimeout(), + clientConfig.readTimeout(), + clientConfig.filter(), + clientConfig.proxy(), + clientConfig.credentials(), + clientConfig.sslContext(), + clientConfig.version(), + false); + } + + private AppiumClientConfig buildAppiumClientConfig(ClientConfig clientConfig, Boolean directConnect) { + return new AppiumClientConfig( + clientConfig.baseUri(), + clientConfig.connectionTimeout(), + clientConfig.readTimeout(), + clientConfig.filter(), + clientConfig.proxy(), + clientConfig.credentials(), + clientConfig.sslContext(), + clientConfig.version(), + directConnect); + } + + @Override + public AppiumClientConfig baseUri(URI baseUri) { + ClientConfig clientConfig = super.baseUri(baseUri); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig baseUrl(URL baseUrl) { + try { + return baseUri(Require.nonNull("Base URL", baseUrl).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Override + public AppiumClientConfig connectionTimeout(Duration timeout) { + ClientConfig clientConfig = super.connectionTimeout(timeout); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig readTimeout(Duration timeout) { + ClientConfig clientConfig = super.readTimeout(timeout); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig withFilter(Filter filter) { + ClientConfig clientConfig = super.withFilter(filter); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig withRetries() { + ClientConfig clientConfig = super.withRetries(); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + + @Override + public AppiumClientConfig proxy(Proxy proxy) { + ClientConfig clientConfig = super.proxy(proxy); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + @Override + public AppiumClientConfig authenticateAs(Credentials credentials) { + ClientConfig clientConfig = super.authenticateAs(credentials); + return buildAppiumClientConfig(clientConfig, directConnect); + } + + /** + * Whether enable directConnect feature described in + * + * Connecting Directly to Appium Hosts in Distributed Environments. + * + * @param directConnect if enable the directConnect feature + * @return A new instance of AppiumClientConfig + */ + public AppiumClientConfig directConnect(boolean directConnect) { + // follows ClientConfig's design + return new AppiumClientConfig( + this.baseUri(), + this.connectionTimeout(), + this.readTimeout(), + this.filter(), + this.proxy(), + this.credentials(), + this.sslContext(), + this.version(), + directConnect + ); + } + + /** + * Whether enable directConnect feature is enabled. + * + * @return If the directConnect is enabled. Defaults false. + */ + public boolean isDirectConnectEnabled() { + return directConnect; + } + + @Override + public String toString() { + return "AppiumClientConfig{" + + "baseUri=" + + this.baseUri() + + ", connectionTimeout=" + + this.connectionTimeout() + + ", readTimeout=" + + this.readTimeout() + + ", filters=" + + this.filter() + + ", proxy=" + + this.proxy() + + ", credentials=" + + this.credentials() + + ", sslcontext=" + + this.sslContext() + + ", version=" + + this.version() + + ", directConnect=" + + this.directConnect + + '}'; + } +} 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 0f109af2c..7fa2b3629 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -16,40 +16,52 @@ package io.appium.java_client; -import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; -import static io.appium.java_client.remote.MobileCapabilityType.AUTOMATION_NAME; -import static io.appium.java_client.remote.MobileCapabilityType.PLATFORM_NAME; -import static org.apache.commons.lang3.StringUtils.isBlank; - import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.internal.SessionHelpers; import io.appium.java_client.remote.AppiumCommandExecutor; -import io.appium.java_client.remote.AppiumNewSessionCommandPayload; -import io.appium.java_client.remote.MobileCapabilityType; +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; import org.openqa.selenium.remote.HttpCommandExecutor; import org.openqa.selenium.remote.RemoteWebDriver; 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.codec.w3c.W3CHttpResponseCodec; import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpMethod; -import java.lang.reflect.Field; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; +import static io.appium.java_client.remote.options.SupportsAutomationNameOption.AUTOMATION_NAME_OPTION; +import static java.util.Collections.singleton; +import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; /** * Default Appium driver implementation. @@ -60,13 +72,19 @@ public class AppiumDriver extends RemoteWebDriver implements ExecutesDriverScript, LogsEvents, HasBrowserCheck, - HasSettings { + CanRememberExtensionPresence, + HasSettings, + HasBiDi { - private static final ErrorHandler errorHandler = new ErrorHandler(new ErrorCodesMobile(), true); + private static final ErrorHandler ERROR_HANDLER = new ErrorHandler(new ErrorCodesMobile(), true); // frequently used command parameters + @Getter private final URL remoteAddress; - 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}. @@ -79,12 +97,11 @@ 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(errorHandler); + super.setErrorHandler(ERROR_HANDLER); this.remoteAddress = executor.getAddressOfRemoteServer(); } - public AppiumDriver(ClientConfig clientConfig, Capabilities capabilities) { + public AppiumDriver(AppiumClientConfig clientConfig, Capabilities capabilities) { this(new AppiumCommandExecutor(MobileCommand.commandRepository, clientConfig), capabilities); } @@ -129,54 +146,36 @@ public AppiumDriver(Capabilities capabilities) { } /** - * Changes platform name if it is not set and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultName a {@link MobileCapabilityType#PLATFORM_NAME} 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. + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! This API is supposed to be used for **debugging purposes only**. * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultName a {@link MobileCapabilityType#AUTOMATION_NAME} value which has - * to be set up - * @return {@link Capabilities} with changed mobile automation name value or the original capabilities + * @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. */ - protected static Capabilities ensureAutomationName( - Capabilities originalCapabilities, String defaultName) { - String currentAutomationName = CapabilityHelpers.getCapability( - originalCapabilities, AUTOMATION_NAME, String.class); - if (isBlank(currentAutomationName)) { - String capabilityName = originalCapabilities.getCapabilityNames() - .contains(AUTOMATION_NAME) ? AUTOMATION_NAME : APPIUM_PREFIX + AUTOMATION_NAME; - return originalCapabilities.merge(new ImmutableCapabilities(capabilityName, defaultName)); - } - return originalCapabilities; - } + public AppiumDriver(URL remoteSessionAddress, String platformName, String automationName) { + super(); + this.capabilities = new ImmutableCapabilities( + Map.of( + PLATFORM_NAME, platformName, + APPIUM_PREFIX + AUTOMATION_NAME_OPTION, automationName + ) + ); + SessionHelpers.SessionAddress sessionAddress = SessionHelpers.parseSessionAddress(remoteSessionAddress); + AppiumCommandExecutor executor = new AppiumCommandExecutor( + MobileCommand.commandRepository, sessionAddress.getServerUrl() + ); + executor.setCommandCodec(new AppiumW3CHttpCommandCodec()); + executor.setResponseCodec(new W3CHttpResponseCodec()); + setCommandExecutor(executor); + this.executeMethod = new AppiumExecutionMethod(this); + super.setErrorHandler(ERROR_HANDLER); + this.remoteAddress = executor.getAddressOfRemoteServer(); - /** - * Changes platform and automation names if they are not set - * and returns merged capabilities. - * - * @param originalCapabilities the given {@link Capabilities}. - * @param defaultPlatformName a {@link MobileCapabilityType#PLATFORM_NAME} 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); + setSessionId(sessionAddress.getId()); } @Override @@ -202,73 +201,227 @@ 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(); + ((AppiumCommandExecutor) getCommandExecutor()).defineCommand(methodName, commandInfo); + } + + @Override + public Response execute(String driverCommand, Map parameters) { + return super.execute(driverCommand, parameters); } - public URL getRemoteAddress() { - return remoteAddress; + @Override + public Response execute(String command) { + return super.execute(command, Collections.emptyMap()); } @Override - protected void startSession(Capabilities capabilities) { - Response response = execute(new AppiumNewSessionCommandPayload(capabilities)); - if (response == null) { - throw new SessionNotCreatedException( - "The underlying command executor returned a null response."); + public X getScreenshotAs(OutputType outputType) { + // TODO: Eventually we should not override this method. + // TODO: Although, we have a legacy burden, + // TODO: so it's impossible to do it the other way as of Oct 29 2022. + // TODO: See https://github.com/SeleniumHQ/selenium/issues/11168 + return super.getScreenshotAs(new OutputType() { + @Override + public X convertFromBase64Png(String base64Png) { + String rfc4648Base64 = base64Png.replaceAll("\\r?\\n", ""); + return outputType.convertFromBase64Png(rfc4648Base64); + } + + @Override + public X convertFromPngBytes(byte[] png) { + return outputType.convertFromPngBytes(png); + } + }); + } + + @Override + public AppiumDriver assertExtensionExists(String extName) { + if (absentExtensionNames.contains(extName)) { + throw new UnsupportedCommandException(); } + return this; + } - Object responseValue = response.getValue(); - if (responseValue == null) { - throw new SessionNotCreatedException( - "The underlying command executor returned a response without payload: " - + response); + @Override + public AppiumDriver markExtensionAbsence(String extName) { + absentExtensionNames.add(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 (!(responseValue instanceof Map)) { - throw new SessionNotCreatedException( - "The underlying command executor returned a response with a non well formed payload: " - + response); + if (this.biDi == null) { + // This should not happen + throw new IllegalStateException(); } + return this.biDi; + } + + protected HttpClient getHttpClient() { + 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) + ); - @SuppressWarnings("unchecked") Map rawCapabilities = (Map) responseValue; - // A workaround for Selenium API enforcing some legacy capability values - rawCapabilities.remove(CapabilityType.PLATFORM); - if (rawCapabilities.containsKey(CapabilityType.BROWSER_NAME) - && isBlank((String) rawCapabilities.get(CapabilityType.BROWSER_NAME))) { - rawCapabilities.remove(CapabilityType.BROWSER_NAME); + // 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); } - MutableCapabilities returnedCapabilities = new BaseOptions<>(rawCapabilities); - try { - Field capsField = RemoteWebDriver.class.getDeclaredField("capabilities"); - capsField.setAccessible(true); - capsField.set(this, returnedCapabilities); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new WebDriverException(e); + 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()); } - @Override - public Response execute(String driverCommand, Map parameters) { - return super.execute(driverCommand, parameters); + /** + * 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; } - @Override - public Response execute(String command) { - return super.execute(command, Collections.emptyMap()); + /** + * 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 3abe1ef4f..34a848f79 100644 --- a/src/main/java/io/appium/java_client/AppiumExecutionMethod.java +++ b/src/main/java/io/appium/java_client/AppiumExecutionMethod.java @@ -16,8 +16,6 @@ package io.appium.java_client; -import com.google.common.collect.ImmutableMap; - import org.openqa.selenium.remote.ExecuteMethod; import org.openqa.selenium.remote.Response; @@ -41,7 +39,7 @@ public Object execute(String commandName, Map parameters) { Response response; if (parameters == null || parameters.isEmpty()) { - response = driver.execute(commandName, ImmutableMap.of()); + response = driver.execute(commandName, Map.of()); } else { response = driver.execute(commandName, parameters); } diff --git a/src/main/java/io/appium/java_client/AppiumFluentWait.java b/src/main/java/io/appium/java_client/AppiumFluentWait.java index 8f197ef47..a284e1ebb 100644 --- a/src/main/java/io/appium/java_client/AppiumFluentWait.java +++ b/src/main/java/io/appium/java_client/AppiumFluentWait.java @@ -17,7 +17,6 @@ package io.appium.java_client; import com.google.common.base.Throwables; - import lombok.AccessLevel; import lombok.Getter; import org.openqa.selenium.TimeoutException; @@ -25,17 +24,20 @@ import org.openqa.selenium.support.ui.FluentWait; import org.openqa.selenium.support.ui.Sleeper; -import java.lang.reflect.Field; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; public class AppiumFluentWait extends FluentWait { private Function pollingStrategy = null; + private static final Duration DEFAULT_POLL_DELAY_DURATION = Duration.ZERO; + private Duration pollDelay = DEFAULT_POLL_DELAY_DURATION; + public static class IterationInfo { /** * The current iteration number. @@ -99,55 +101,44 @@ public AppiumFluentWait(T input, Clock clock, Sleeper sleeper) { super(input, clock, sleeper); } - private B getPrivateFieldValue(String fieldName, Class fieldType) { - try { - final Field f = getClass().getSuperclass().getDeclaredField(fieldName); - f.setAccessible(true); - return fieldType.cast(f.get(this)); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new WebDriverException(e); - } - } - - private Object getPrivateFieldValue(String fieldName) { - try { - final Field f = getClass().getSuperclass().getDeclaredField(fieldName); - f.setAccessible(true); - return f.get(this); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new WebDriverException(e); - } + /** + * Sets how long to wait before starting to evaluate condition to be true. + * The default pollDelay is {@link #DEFAULT_POLL_DELAY_DURATION}. + * + * @param pollDelay The pollDelay duration. + * @return A self reference. + */ + public AppiumFluentWait withPollDelay(Duration pollDelay) { + this.pollDelay = pollDelay; + return this; } protected Clock getClock() { - return getPrivateFieldValue("clock", Clock.class); + return clock; } protected Duration getTimeout() { - return getPrivateFieldValue("timeout", Duration.class); + return timeout; } protected Duration getInterval() { - return getPrivateFieldValue("interval", Duration.class); + return interval; } protected Sleeper getSleeper() { - return getPrivateFieldValue("sleeper", Sleeper.class); + return sleeper; } - @SuppressWarnings("unchecked") protected List> getIgnoredExceptions() { - return getPrivateFieldValue("ignoredExceptions", List.class); + return ignoredExceptions; } - @SuppressWarnings("unchecked") protected Supplier getMessageSupplier() { - return getPrivateFieldValue("messageSupplier", Supplier.class); + return messageSupplier; } - @SuppressWarnings("unchecked") protected T getInput() { - return (T) getPrivateFieldValue("input"); + return (T) input; } /** @@ -213,10 +204,19 @@ public AppiumFluentWait withPollingStrategy(Function */ @Override public V until(Function isTrue) { - final Instant start = getClock().instant(); - final Instant end = getClock().instant().plus(getTimeout()); - long iterationNumber = 1; + 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); + + return performIteration(isTrue, start, end); + } + + private V performIteration(Function isTrue, Instant start, Instant end) { + var iterationNumber = 1; Throwable lastException; + + sleepInterruptibly(pollDelay); + while (true) { try { V value = isTrue.apply(getInput()); @@ -235,32 +235,51 @@ public V until(Function isTrue) { // Check the timeout after evaluating the function to ensure conditions // with a zero timeout can succeed. if (end.isBefore(getClock().instant())) { - String message = getMessageSupplier() != null ? getMessageSupplier().get() : null; - - String timeoutMessage = String.format( - "Expected condition failed: %s (tried for %d second(s) with %s interval)", - message == null ? "waiting for " + isTrue : message, - getTimeout().getSeconds(), getInterval()); - throw timeoutException(timeoutMessage, lastException); + handleTimeoutException(lastException, isTrue); } - try { - Duration interval = getInterval(); - if (pollingStrategy != null) { - final IterationInfo info = new IterationInfo(iterationNumber, - Duration.between(start, getClock().instant()), getTimeout(), - interval); - interval = pollingStrategy.apply(info); - } - getSleeper().sleep(interval); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new WebDriverException(e); - } + var interval = getIntervalWithPollingStrategy(start, iterationNumber); + sleepInterruptibly(interval); + ++iterationNumber; } } + private void handleTimeoutException(Throwable lastException, Function isTrue) { + var message = Optional.ofNullable(getMessageSupplier()) + .map(Supplier::get) + .orElseGet(() -> "waiting for " + isTrue); + + var timeoutMessage = String.format( + "Expected condition failed: %s (tried for %s ms with an interval of %s ms)", + message, + getTimeout().toMillis(), + getInterval().toMillis() + ); + + throw timeoutException(timeoutMessage, lastException); + } + + private Duration getIntervalWithPollingStrategy(Instant start, long iterationNumber) { + var interval = getInterval(); + return Optional.ofNullable(pollingStrategy) + .map(strategy -> strategy.apply(new IterationInfo( + iterationNumber, + Duration.between(start, getClock().instant()), getTimeout(), interval))) + .orElse(interval); + } + + private void sleepInterruptibly(Duration duration) { + try { + if (!duration.isZero() && !duration.isNegative()) { + getSleeper().sleep(duration); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new WebDriverException(e); + } + } + protected Throwable propagateIfNotIgnored(Throwable e) { for (Class ignoredException : getIgnoredExceptions()) { if (ignoredException.isInstance(e)) { diff --git a/src/main/java/io/appium/java_client/CanRememberExtensionPresence.java b/src/main/java/io/appium/java_client/CanRememberExtensionPresence.java new file mode 100644 index 000000000..36cd4b903 --- /dev/null +++ b/src/main/java/io/appium/java_client/CanRememberExtensionPresence.java @@ -0,0 +1,25 @@ +package io.appium.java_client; + +import org.openqa.selenium.UnsupportedCommandException; + +public interface CanRememberExtensionPresence { + /** + * Verifies if the given extension is not present in the list of absent extensions + * for the given driver instance. + * This API is designed for private usage. + * + * @param extName extension name. + * @return self instance for chaining. + * @throws UnsupportedCommandException if the extension is listed in the list of absents. + */ + ExecutesMethod assertExtensionExists(String extName); + + /** + * Marks the given extension as absent for the given driver instance. + * This API is designed for private usage. + * + * @param extName extension name. + * @return self instance for chaining. + */ + ExecutesMethod markExtensionAbsence(String extName); +} diff --git a/src/main/java/io/appium/java_client/CommandExecutionHelper.java b/src/main/java/io/appium/java_client/CommandExecutionHelper.java index 00557b6da..b56a2f4ac 100644 --- a/src/main/java/io/appium/java_client/CommandExecutionHelper.java +++ b/src/main/java/io/appium/java_client/CommandExecutionHelper.java @@ -16,25 +16,57 @@ package io.appium.java_client; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.Response; +import java.util.Collections; import java.util.Map; +import static org.openqa.selenium.remote.DriverCommand.EXECUTE_SCRIPT; + public final class CommandExecutionHelper { - public static T execute(ExecutesMethod executesMethod, - Map.Entry> keyValuePair) { + private CommandExecutionHelper() { + } + + @Nullable + public static T execute( + ExecutesMethod executesMethod, Map.Entry> keyValuePair + ) { return handleResponse(executesMethod.execute(keyValuePair.getKey(), keyValuePair.getValue())); } + @Nullable public static T execute(ExecutesMethod executesMethod, String command) { return handleResponse(executesMethod.execute(command)); } + @Nullable private static T handleResponse(Response response) { - if (response != null) { - return (T) response.getValue(); - } - return null; + //noinspection unchecked + return response == null ? null : (T) response.getValue(); + } + + @Nullable + public static T executeScript(ExecutesMethod executesMethod, String scriptName) { + return executeScript(executesMethod, scriptName, null); + } + + /** + * Simplifies arguments preparation for the script execution command. + * + * @param executesMethod Method executor instance. + * @param scriptName Extension script name. + * @param args Extension script arguments (if present). + * @return Script execution result. + */ + @Nullable + public static T executeScript( + ExecutesMethod executesMethod, String scriptName, @Nullable Map args + ) { + return execute(executesMethod, Map.entry(EXECUTE_SCRIPT, Map.of( + "script", scriptName, + "args", (args == null || args.isEmpty()) ? Collections.emptyList() : Collections.singletonList(args) + ))); } } diff --git a/src/main/java/io/appium/java_client/ComparesImages.java b/src/main/java/io/appium/java_client/ComparesImages.java index 3cb85036c..4f44d6e0a 100644 --- a/src/main/java/io/appium/java_client/ComparesImages.java +++ b/src/main/java/io/appium/java_client/ComparesImages.java @@ -16,8 +16,6 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.compareImagesCommand; - import io.appium.java_client.imagecomparison.ComparisonMode; import io.appium.java_client.imagecomparison.FeaturesMatchingOptions; import io.appium.java_client.imagecomparison.FeaturesMatchingResult; @@ -25,13 +23,15 @@ import io.appium.java_client.imagecomparison.OccurrenceMatchingResult; import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; import io.appium.java_client.imagecomparison.SimilarityMatchingResult; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; +import org.jspecify.annotations.Nullable; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; import java.util.Map; -import javax.annotation.Nullable; + +import static io.appium.java_client.MobileCommand.compareImagesCommand; public interface ComparesImages extends ExecutesMethod { @@ -93,8 +93,8 @@ default FeaturesMatchingResult matchImagesFeatures(File image1, File image2) thr */ default FeaturesMatchingResult matchImagesFeatures(File image1, File image2, @Nullable FeaturesMatchingOptions options) throws IOException { - return matchImagesFeatures(Base64.encodeBase64(FileUtils.readFileToByteArray(image1)), - Base64.encodeBase64(FileUtils.readFileToByteArray(image2)), options); + return matchImagesFeatures(Base64.getEncoder().encode(Files.readAllBytes(image1.toPath())), + Base64.getEncoder().encode(Files.readAllBytes(image2.toPath())), options); } /** @@ -126,8 +126,7 @@ default OccurrenceMatchingResult findImageOccurrence(byte[] fullImage, byte[] pa @Nullable OccurrenceMatchingOptions options) { Object response = CommandExecutionHelper.execute(this, compareImagesCommand(ComparisonMode.MATCH_TEMPLATE, fullImage, partialImage, options)); - //noinspection unchecked - return new OccurrenceMatchingResult((Map) response); + return new OccurrenceMatchingResult(response); } /** @@ -160,8 +159,8 @@ default OccurrenceMatchingResult findImageOccurrence(File fullImage, File partia default OccurrenceMatchingResult findImageOccurrence(File fullImage, File partialImage, @Nullable OccurrenceMatchingOptions options) throws IOException { - return findImageOccurrence(Base64.encodeBase64(FileUtils.readFileToByteArray(fullImage)), - Base64.encodeBase64(FileUtils.readFileToByteArray(partialImage)), options); + return findImageOccurrence(Base64.getEncoder().encode(Files.readAllBytes(fullImage.toPath())), + Base64.getEncoder().encode(Files.readAllBytes(partialImage.toPath())), options); } /** @@ -227,7 +226,7 @@ default SimilarityMatchingResult getImagesSimilarity(File image1, File image2) t default SimilarityMatchingResult getImagesSimilarity(File image1, File image2, @Nullable SimilarityMatchingOptions options) throws IOException { - return getImagesSimilarity(Base64.encodeBase64(FileUtils.readFileToByteArray(image1)), - Base64.encodeBase64(FileUtils.readFileToByteArray(image2)), options); + return getImagesSimilarity(Base64.getEncoder().encode(Files.readAllBytes(image1.toPath())), + Base64.getEncoder().encode(Files.readAllBytes(image2.toPath())), options); } -} \ No newline at end of file +} diff --git a/src/main/java/io/appium/java_client/ErrorCodesMobile.java b/src/main/java/io/appium/java_client/ErrorCodesMobile.java index 924625b08..c70514b0f 100644 --- a/src/main/java/io/appium/java_client/ErrorCodesMobile.java +++ b/src/main/java/io/appium/java_client/ErrorCodesMobile.java @@ -17,8 +17,6 @@ package io.appium.java_client; -import com.google.common.collect.ImmutableMap; - import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.ErrorCodes; @@ -33,9 +31,7 @@ public class ErrorCodesMobile extends ErrorCodes { public static final int NO_SUCH_CONTEXT = 35; - private static Map statusToState = - ImmutableMap.builder().put(NO_SUCH_CONTEXT, "No such context found") - .build(); + private static Map statusToState = Map.of(NO_SUCH_CONTEXT, "No such context found"); /** * Returns the exception type that corresponds to the given {@code statusCode}. All unrecognized @@ -44,6 +40,7 @@ public class ErrorCodesMobile extends ErrorCodes { * @param statusCode The status code to convert. * @return The exception type that corresponds to the provided status */ + @Override public Class getExceptionType(int statusCode) { switch (statusCode) { case NO_SUCH_CONTEXT: @@ -61,6 +58,7 @@ public Class getExceptionType(int statusCode) { * @return The exception type that corresponds to the provided error message or {@code null} if * there are no matching mobile exceptions. */ + @Override public Class getExceptionType(String message) { for (Map.Entry entry : statusToState.entrySet()) { if (message.contains(entry.getValue())) { @@ -76,6 +74,7 @@ public Class getExceptionType(String message) { * @param thrown The thrown error. * @return The corresponding status code for the given thrown error. */ + @Override public int toStatusCode(Throwable thrown) { if (thrown instanceof NoSuchContextException) { return NO_SUCH_CONTEXT; diff --git a/src/main/java/io/appium/java_client/ExecuteCDPCommand.java b/src/main/java/io/appium/java_client/ExecuteCDPCommand.java index 8cf09f357..7114da787 100644 --- a/src/main/java/io/appium/java_client/ExecuteCDPCommand.java +++ b/src/main/java/io/appium/java_client/ExecuteCDPCommand.java @@ -16,16 +16,15 @@ package io.appium.java_client; -import com.google.common.collect.ImmutableMap; +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; -import static com.google.common.base.Preconditions.checkNotNull; import static io.appium.java_client.MobileCommand.EXECUTE_GOOGLE_CDP_COMMAND; +import static java.util.Objects.requireNonNull; public interface ExecuteCDPCommand extends ExecutesMethod { @@ -40,11 +39,11 @@ public interface ExecuteCDPCommand extends ExecutesMethod { */ default Map executeCdpCommand(String command, @Nullable Map params) { Map data = new HashMap<>(); - data.put("cmd", checkNotNull(command)); + data.put("cmd", requireNonNull(command)); data.put("params", params == null ? Collections.emptyMap() : params); Response response = execute(EXECUTE_GOOGLE_CDP_COMMAND, data); //noinspection unchecked - return ImmutableMap.copyOf((Map) response.getValue()); + return Collections.unmodifiableMap((Map) response.getValue()); } /** diff --git a/src/main/java/io/appium/java_client/ExecutesDriverScript.java b/src/main/java/io/appium/java_client/ExecutesDriverScript.java index 997b061a2..2509dba85 100644 --- a/src/main/java/io/appium/java_client/ExecutesDriverScript.java +++ b/src/main/java/io/appium/java_client/ExecutesDriverScript.java @@ -18,14 +18,14 @@ 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; -import static com.google.common.base.Preconditions.checkNotNull; import static io.appium.java_client.MobileCommand.EXECUTE_DRIVER_SCRIPT; +import static java.util.Objects.requireNonNull; public interface ExecutesDriverScript extends ExecutesMethod { @@ -46,7 +46,7 @@ public interface ExecutesDriverScript extends ExecutesMethod { */ default ScriptValue executeDriverScript(String script, @Nullable ScriptOptions options) { Map data = new HashMap<>(); - data.put("script", checkNotNull(script)); + data.put("script", requireNonNull(script)); if (options != null) { data.putAll(options.build()); } diff --git a/src/main/java/io/appium/java_client/HasAppStrings.java b/src/main/java/io/appium/java_client/HasAppStrings.java index 0c9b3905f..1224b26f9 100644 --- a/src/main/java/io/appium/java_client/HasAppStrings.java +++ b/src/main/java/io/appium/java_client/HasAppStrings.java @@ -16,46 +16,75 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.GET_STRINGS; -import static io.appium.java_client.MobileCommand.prepareArguments; +import org.openqa.selenium.UnsupportedCommandException; -import java.util.AbstractMap; import java.util.Map; -public interface HasAppStrings extends ExecutesMethod { +import static io.appium.java_client.MobileCommand.GET_STRINGS; + +public interface HasAppStrings extends ExecutesMethod, CanRememberExtensionPresence { /** * Get all defined Strings from an app for the default language. + * See the documentation for 'mobile: getAppStrings' extension for more details. * * @return a map with localized strings defined in the app */ default Map getAppStringMap() { - return CommandExecutionHelper.execute(this, GET_STRINGS); + final String extName = "mobile: getAppStrings"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute(markExtensionAbsence(extName), GET_STRINGS); + } } /** * Get all defined Strings from an app for the specified language. + * See the documentation for 'mobile: getAppStrings' extension for more details. * * @param language strings language code * @return a map with localized strings defined in the app */ default Map getAppStringMap(String language) { - return CommandExecutionHelper.execute(this, new AbstractMap.SimpleEntry<>(GET_STRINGS, - prepareArguments("language", language))); + final String extName = "mobile: getAppStrings"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "language", language + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(GET_STRINGS, Map.of("language", language)) + ); + } } /** * Get all defined Strings from an app for the specified language and - * strings filename. + * strings filename. See the documentation for 'mobile: getAppStrings' + * extension for more details. * * @param language strings language code - * @param stringFile strings filename + * @param stringFile strings filename. Ignored on Android * @return a map with localized strings defined in the app */ default Map getAppStringMap(String language, String stringFile) { - String[] parameters = new String[] {"language", "stringFile"}; - Object[] values = new Object[] {language, stringFile}; - return CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(GET_STRINGS, prepareArguments(parameters, values))); + final String extName = "mobile: getAppStrings"; + Map args = Map.of( + "language", language, + "stringFile", stringFile + ); + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(GET_STRINGS, args) + ); + } } } diff --git a/src/main/java/io/appium/java_client/HasBrowserCheck.java b/src/main/java/io/appium/java_client/HasBrowserCheck.java index efa5c6c62..a75ffbfd4 100644 --- a/src/main/java/io/appium/java_client/HasBrowserCheck.java +++ b/src/main/java/io/appium/java_client/HasBrowserCheck.java @@ -1,19 +1,18 @@ package io.appium.java_client; -import com.google.common.collect.ImmutableMap; 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 java.util.Collections; - -import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.openqa.selenium.remote.DriverCommand.EXECUTE_SCRIPT; +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. * @@ -22,21 +21,21 @@ public interface HasBrowserCheck extends ExecutesMethod, HasCapabilities { default boolean isBrowser() { String browserName = CapabilityHelpers.getCapability(getCapabilities(), CapabilityType.BROWSER_NAME, String.class); - if (!isBlank(browserName)) { + if (!isNullOrEmpty(browserName)) { try { - return (boolean) execute(EXECUTE_SCRIPT, ImmutableMap.of( - "script", "return !!window.navigator;", - "args", Collections.emptyList() - )).getValue(); + return requireNonNull( + CommandExecutionHelper.executeScript(this, "return !!window.navigator;") + ); } catch (WebDriverException ign) { // ignore } } - if (!(this instanceof ContextAware)) { + if (!(this instanceof SupportsContextSwitching)) { return false; } try { - return !containsIgnoreCase(((ContextAware) this).getContext(), "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/HasDeviceTime.java b/src/main/java/io/appium/java_client/HasDeviceTime.java index fa9df8997..e450f28f1 100644 --- a/src/main/java/io/appium/java_client/HasDeviceTime.java +++ b/src/main/java/io/appium/java_client/HasDeviceTime.java @@ -16,14 +16,6 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.GET_DEVICE_TIME; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.remote.DriverCommand; -import org.openqa.selenium.remote.Response; - import java.util.Map; public interface HasDeviceTime extends ExecutesMethod { @@ -39,12 +31,9 @@ public interface HasDeviceTime extends ExecutesMethod { * @return Device time string */ default String getDeviceTime(String format) { - Map params = ImmutableMap.of( - "script", "mobile: getDeviceTime", - "args", ImmutableList.of(ImmutableMap.of("format", format)) + return CommandExecutionHelper.executeScript( + this, "mobile: getDeviceTime", Map.of("format", format) ); - Response response = execute(DriverCommand.EXECUTE_SCRIPT, params); - return response.getValue().toString(); } /** @@ -54,7 +43,6 @@ default String getDeviceTime(String format) { * @return Device time string */ default String getDeviceTime() { - Response response = execute(GET_DEVICE_TIME); - return response.getValue().toString(); + return CommandExecutionHelper.executeScript(this, "mobile: getDeviceTime"); } } diff --git a/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java b/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java index 7a9d6febb..b242d2b01 100644 --- a/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java +++ b/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java @@ -1,15 +1,27 @@ package io.appium.java_client; +import org.openqa.selenium.UnsupportedCommandException; + import static io.appium.java_client.MobileCommand.isKeyboardShownCommand; +import static java.util.Objects.requireNonNull; -public interface HasOnScreenKeyboard extends ExecutesMethod { +public interface HasOnScreenKeyboard extends ExecutesMethod, CanRememberExtensionPresence { /** - * Check if the keyboard is displayed. + * Check if the on-screen keyboard is displayed. + * See the documentation for 'mobile: isKeyboardShown' extension for more details. * * @return true if keyboard is displayed. False otherwise */ default boolean isKeyboardShown() { - return CommandExecutionHelper.execute(this, isKeyboardShownCommand()); + final String extName = "mobile: isKeyboardShown"; + try { + return requireNonNull(CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName)); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return requireNonNull( + CommandExecutionHelper.execute(markExtensionAbsence(extName), isKeyboardShownCommand()) + ); + } } } diff --git a/src/main/java/io/appium/java_client/HasSettings.java b/src/main/java/io/appium/java_client/HasSettings.java index 8210123a7..73befa6f5 100644 --- a/src/main/java/io/appium/java_client/HasSettings.java +++ b/src/main/java/io/appium/java_client/HasSettings.java @@ -16,9 +16,6 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.getSettingsCommand; -import static io.appium.java_client.MobileCommand.setSettingsCommand; - import org.openqa.selenium.remote.Response; import java.util.EnumMap; @@ -26,6 +23,9 @@ import java.util.Map.Entry; import java.util.stream.Collectors; +import static io.appium.java_client.MobileCommand.getSettingsCommand; +import static io.appium.java_client.MobileCommand.setSettingsCommand; + public interface HasSettings extends ExecutesMethod { /** diff --git a/src/main/java/io/appium/java_client/HidesKeyboard.java b/src/main/java/io/appium/java_client/HidesKeyboard.java index 5f292b0ce..a6f522102 100644 --- a/src/main/java/io/appium/java_client/HidesKeyboard.java +++ b/src/main/java/io/appium/java_client/HidesKeyboard.java @@ -16,14 +16,26 @@ package io.appium.java_client; +import org.openqa.selenium.UnsupportedCommandException; + import static io.appium.java_client.MobileCommand.HIDE_KEYBOARD; -public interface HidesKeyboard extends ExecutesMethod { +public interface HidesKeyboard extends ExecutesMethod, CanRememberExtensionPresence { /** * Hides the keyboard if it is showing. + * If the on-screen keyboard does not have any dedicated button that + * hides it then an error is going to be thrown. In such case you must emulate + * same actions an app user would do to hide the keyboard. + * See the documentation for 'mobile: hideKeyboard' extension for more details. */ default void hideKeyboard() { - execute(HIDE_KEYBOARD); + final String extName = "mobile: hideKeyboard"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), HIDE_KEYBOARD); + } } } diff --git a/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java b/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java index 2549ad018..c2a84bb11 100644 --- a/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java +++ b/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java @@ -16,31 +16,34 @@ package io.appium.java_client; +import org.openqa.selenium.UnsupportedCommandException; + +import java.util.List; +import java.util.Map; + import static io.appium.java_client.MobileCommand.hideKeyboardCommand; public interface HidesKeyboardWithKeyName extends HidesKeyboard { /** * Hides the keyboard by pressing the button specified by keyName if it is - * showing. + * showing. If the on-screen keyboard does not have any dedicated button that + * hides it then an error is going to be thrown. In such case you must emulate + * same actions an app user would do to hide the keyboard. + * See the documentation for 'mobile: hideKeyboard' extension for more details. * * @param keyName The button pressed by the mobile driver to attempt hiding the * keyboard. */ default void hideKeyboard(String keyName) { - CommandExecutionHelper.execute(this, hideKeyboardCommand(keyName)); - } - - /** - * Hides the keyboard if it is showing. Hiding the keyboard often - * depends on the way an app is implemented, no single strategy always - * works. - * - * @param strategy HideKeyboardStrategy. - * @param keyName a String, representing the text displayed on the button of the - * keyboard you want to press. For example: "Done". - */ - default void hideKeyboard(String strategy, String keyName) { - CommandExecutionHelper.execute(this, hideKeyboardCommand(strategy, keyName)); + final String extName = "mobile: hideKeyboard"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "keys", List.of(keyName) + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), hideKeyboardCommand(keyName)); + } } } diff --git a/src/main/java/io/appium/java_client/InteractsWithApps.java b/src/main/java/io/appium/java_client/InteractsWithApps.java index f1a15146e..0ca018abb 100644 --- a/src/main/java/io/appium/java_client/InteractsWithApps.java +++ b/src/main/java/io/appium/java_client/InteractsWithApps.java @@ -16,29 +16,32 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.ACTIVATE_APP; -import static io.appium.java_client.MobileCommand.INSTALL_APP; -import static io.appium.java_client.MobileCommand.IS_APP_INSTALLED; -import static io.appium.java_client.MobileCommand.QUERY_APP_STATE; -import static io.appium.java_client.MobileCommand.REMOVE_APP; -import static io.appium.java_client.MobileCommand.RUN_APP_IN_BACKGROUND; -import static io.appium.java_client.MobileCommand.TERMINATE_APP; -import static io.appium.java_client.MobileCommand.prepareArguments; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.appmanagement.ApplicationState; import io.appium.java_client.appmanagement.BaseActivateApplicationOptions; import io.appium.java_client.appmanagement.BaseInstallApplicationOptions; +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 java.time.Duration; -import java.util.AbstractMap; -import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.ACTIVATE_APP; +import static io.appium.java_client.MobileCommand.INSTALL_APP; +import static io.appium.java_client.MobileCommand.IS_APP_INSTALLED; +import static io.appium.java_client.MobileCommand.QUERY_APP_STATE; +import static io.appium.java_client.MobileCommand.REMOVE_APP; +import static io.appium.java_client.MobileCommand.RUN_APP_IN_BACKGROUND; +import static io.appium.java_client.MobileCommand.TERMINATE_APP; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; -@SuppressWarnings("rawtypes") -public interface InteractsWithApps extends ExecutesMethod { +@SuppressWarnings({"rawtypes", "unchecked"}) +public interface InteractsWithApps extends ExecutesMethod, CanRememberExtensionPresence { /** * Install an app on the mobile device. @@ -57,12 +60,20 @@ default void installApp(String appPath) { * the particular platform. */ default void installApp(String appPath, @Nullable BaseInstallApplicationOptions options) { - String[] parameters = options == null ? new String[]{"appPath"} : - new String[]{"appPath", "options"}; - Object[] values = options == null ? new Object[]{appPath} : - new Object[]{appPath, options.build()}; - CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(INSTALL_APP, prepareArguments(parameters, values))); + final String extName = "mobile: installApp"; + try { + var args = new HashMap(); + args.put("app", appPath); + args.put("appPath", appPath); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); + } catch (UnsupportedCommandException | InvalidArgumentException e) { + // TODO: Remove the fallback + var args = new HashMap(); + args.put("appPath", appPath); + ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); + CommandExecutionHelper.execute(markExtensionAbsence(extName), Map.entry(INSTALL_APP, args)); + } } /** @@ -72,20 +83,46 @@ default void installApp(String appPath, @Nullable BaseInstallApplicationOptions * @return True if app is installed, false otherwise. */ default boolean isAppInstalled(String bundleId) { - return CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(IS_APP_INSTALLED, prepareArguments("bundleId", bundleId))); + final String extName = "mobile: isAppInstalled"; + try { + return requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "bundleId", bundleId, + "appId", bundleId + )) + ); + } catch (UnsupportedCommandException | InvalidArgumentException e) { + // TODO: Remove the fallback + return requireNonNull( + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(IS_APP_INSTALLED, Map.of("bundleId", bundleId)) + ) + ); + } } /** - * Runs the current app as a background app for the time + * Runs the current app in the background for the time * requested. This is a synchronous method, it blocks while the * application is in background. * - * @param duration The time to run App in background. Minimum time resolution is one millisecond. - * Passing zero or a negative value will switch to Home screen and return immediately. + * @param duration The time to run App in background. Minimum time resolution unit is one millisecond. + * Passing a negative value will switch to Home screen and return immediately. */ default void runAppInBackground(Duration duration) { - execute(RUN_APP_IN_BACKGROUND, ImmutableMap.of("seconds", duration.toMillis() / 1000.0)); + final String extName = "mobile: backgroundApp"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "seconds", duration.toMillis() / 1000.0 + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(RUN_APP_IN_BACKGROUND, Map.of("seconds", duration.toMillis() / 1000.0)) + ); + } } /** @@ -107,12 +144,28 @@ default boolean removeApp(String bundleId) { * @return true if the uninstall was successful. */ default boolean removeApp(String bundleId, @Nullable BaseRemoveApplicationOptions options) { - String[] parameters = options == null ? new String[]{"bundleId"} : - new String[]{"bundleId", "options"}; - Object[] values = options == null ? new Object[]{bundleId} : - new Object[]{bundleId, options.build()}; - return CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(REMOVE_APP, prepareArguments(parameters, values))); + final String extName = "mobile: removeApp"; + try { + var args = new HashMap(); + args.put("bundleId", bundleId); + args.put("appId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + return requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args) + ); + } catch (UnsupportedCommandException | InvalidArgumentException e) { + // TODO: Remove the fallback + var args = new HashMap(); + args.put("bundleId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); + //noinspection RedundantCast + return requireNonNull( + (Boolean) CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(REMOVE_APP, args) + ) + ); + } } /** @@ -134,12 +187,20 @@ default void activateApp(String bundleId) { * particular platform. */ default void activateApp(String bundleId, @Nullable BaseActivateApplicationOptions options) { - String[] parameters = options == null ? new String[]{"bundleId"} : - new String[]{"bundleId", "options"}; - Object[] values = options == null ? new Object[]{bundleId} : - new Object[]{bundleId, options.build()}; - CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(ACTIVATE_APP, prepareArguments(parameters, values))); + final String extName = "mobile: activateApp"; + try { + var args = new HashMap(); + args.put("bundleId", bundleId); + args.put("appId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); + } catch (UnsupportedCommandException | InvalidArgumentException e) { + // TODO: Remove the fallback + var args = new HashMap(); + args.put("bundleId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); + CommandExecutionHelper.execute(markExtensionAbsence(extName), Map.entry(ACTIVATE_APP, args)); + } } /** @@ -149,8 +210,30 @@ default void activateApp(String bundleId, @Nullable BaseActivateApplicationOptio * @return one of possible {@link ApplicationState} values, */ default ApplicationState queryAppState(String bundleId) { - return ApplicationState.ofCode(CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(QUERY_APP_STATE, ImmutableMap.of("bundleId", bundleId)))); + final String extName = "mobile: queryAppState"; + try { + return ApplicationState.ofCode( + requireNonNull( + CommandExecutionHelper.executeScript( + assertExtensionExists(extName), + extName, Map.of( + "bundleId", bundleId, + "appId", bundleId + ) + ) + ) + ); + } catch (UnsupportedCommandException | InvalidArgumentException e) { + // TODO: Remove the fallback + return ApplicationState.ofCode( + requireNonNull( + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(QUERY_APP_STATE, Map.of("bundleId", bundleId)) + ) + ) + ); + } } /** @@ -172,11 +255,26 @@ default boolean terminateApp(String bundleId) { * @return true if the app was running before and has been successfully stopped. */ default boolean terminateApp(String bundleId, @Nullable BaseTerminateApplicationOptions options) { - String[] parameters = options == null ? new String[]{"bundleId"} : - new String[]{"bundleId", "options"}; - Object[] values = options == null ? new Object[]{bundleId} : - new Object[]{bundleId, options.build()}; - return CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(TERMINATE_APP, prepareArguments(parameters, values))); + final String extName = "mobile: terminateApp"; + try { + var args = new HashMap(); + args.put("bundleId", bundleId); + args.put("appId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + return requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args) + ); + } catch (UnsupportedCommandException | InvalidArgumentException e) { + // TODO: Remove the fallback + var args = new HashMap(); + args.put("bundleId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); + //noinspection RedundantCast + return requireNonNull( + (Boolean) CommandExecutionHelper.execute( + markExtensionAbsence(extName), Map.entry(TERMINATE_APP, args) + ) + ); + } } } diff --git a/src/main/java/io/appium/java_client/Location.java b/src/main/java/io/appium/java_client/Location.java new file mode 100644 index 000000000..322665a42 --- /dev/null +++ b/src/main/java/io/appium/java_client/Location.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; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.jspecify.annotations.Nullable; + +/** + * Represents the physical location. + */ +@Getter +@ToString +@EqualsAndHashCode +public class Location { + private final double latitude; + private final double longitude; + @Nullable private final Double altitude; + + /** + * Create {@link Location} with latitude, longitude and altitude values. + * + * @param latitude latitude value. + * @param longitude longitude value. + * @param altitude altitude value (can be null). + */ + public Location(double latitude, double longitude, @Nullable Double altitude) { + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + } + + /** + * Create {@link Location} with latitude and longitude values. + * + * @param latitude latitude value. + * @param longitude longitude value. + */ + public Location(double latitude, double longitude) { + this(latitude, longitude, null); + } +} diff --git a/src/main/java/io/appium/java_client/LocksDevice.java b/src/main/java/io/appium/java_client/LocksDevice.java index 944624002..bd818b21b 100644 --- a/src/main/java/io/appium/java_client/LocksDevice.java +++ b/src/main/java/io/appium/java_client/LocksDevice.java @@ -16,13 +16,17 @@ package io.appium.java_client; +import org.openqa.selenium.UnsupportedCommandException; + +import java.time.Duration; +import java.util.Map; + import static io.appium.java_client.MobileCommand.getIsDeviceLockedCommand; import static io.appium.java_client.MobileCommand.lockDeviceCommand; import static io.appium.java_client.MobileCommand.unlockDeviceCommand; +import static java.util.Objects.requireNonNull; -import java.time.Duration; - -public interface LocksDevice extends ExecutesMethod { +public interface LocksDevice extends ExecutesMethod, CanRememberExtensionPresence { /** * This method locks a device. It will return silently if the device @@ -41,7 +45,15 @@ default void lockDevice() { * A negative/zero value will lock the device and return immediately. */ default void lockDevice(Duration duration) { - CommandExecutionHelper.execute(this, lockDeviceCommand(duration)); + final String extName = "mobile: lock"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "seconds", duration.getSeconds() + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), lockDeviceCommand(duration)); + } } /** @@ -49,7 +61,17 @@ default void lockDevice(Duration duration) { * is not locked. */ default void unlockDevice() { - CommandExecutionHelper.execute(this, unlockDeviceCommand()); + final String extName = "mobile: unlock"; + try { + //noinspection ConstantConditions + if (!(Boolean) CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: isLocked")) { + return; + } + CommandExecutionHelper.executeScript(this, extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), unlockDeviceCommand()); + } } /** @@ -58,6 +80,16 @@ default void unlockDevice() { * @return true if the device is locked or false otherwise. */ default boolean isDeviceLocked() { - return CommandExecutionHelper.execute(this, getIsDeviceLockedCommand()); + final String extName = "mobile: isLocked"; + try { + return requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName) + ); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return requireNonNull( + CommandExecutionHelper.execute(markExtensionAbsence(extName), getIsDeviceLockedCommand()) + ); + } } } diff --git a/src/main/java/io/appium/java_client/LogsEvents.java b/src/main/java/io/appium/java_client/LogsEvents.java index 7844b56a6..6ae80bc0c 100644 --- a/src/main/java/io/appium/java_client/LogsEvents.java +++ b/src/main/java/io/appium/java_client/LogsEvents.java @@ -16,19 +16,19 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.GET_EVENTS; -import static io.appium.java_client.MobileCommand.LOG_EVENT; - -import com.google.common.collect.ImmutableMap; import io.appium.java_client.serverevents.CommandEvent; import io.appium.java_client.serverevents.CustomEvent; -import io.appium.java_client.serverevents.TimedEvent; import io.appium.java_client.serverevents.ServerEvents; +import io.appium.java_client.serverevents.TimedEvent; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.remote.Response; + import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.openqa.selenium.json.Json; -import org.openqa.selenium.remote.Response; + +import static io.appium.java_client.MobileCommand.GET_EVENTS; +import static io.appium.java_client.MobileCommand.LOG_EVENT; public interface LogsEvents extends ExecutesMethod { @@ -40,7 +40,7 @@ public interface LogsEvents extends ExecutesMethod { * @throws org.openqa.selenium.WebDriverException if there was a failure while executing the script */ default void logEvent(CustomEvent event) { - execute(LOG_EVENT, ImmutableMap.of("vendor", event.getVendor(), "event", event.getEventName())); + execute(LOG_EVENT, Map.of("vendor", event.getVendor(), "event", event.getEventName())); } /** @@ -62,8 +62,8 @@ default ServerEvents getEvents() { .stream() .map((Map cmd) -> new CommandEvent( (String) cmd.get("cmd"), - ((Long) cmd.get("startTime")), - ((Long) cmd.get("endTime")) + (Long) cmd.get("startTime"), + (Long) cmd.get("endTime") )) .collect(Collectors.toList()); diff --git a/src/main/java/io/appium/java_client/MobileBy.java b/src/main/java/io/appium/java_client/MobileBy.java deleted file mode 100644 index 65f382ec7..000000000 --- a/src/main/java/io/appium/java_client/MobileBy.java +++ /dev/null @@ -1,340 +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; - -import java.io.Serializable; - -import org.openqa.selenium.By; - -/** - * Appium locating strategies. - * - * @deprecated Use {@link AppiumBy} instead. - */ -@SuppressWarnings("serial") -@Deprecated -public abstract class MobileBy extends AppiumBy { - - protected MobileBy(String selector, String locatorString, String locatorName) { - super(selector, locatorString, locatorName); - } - - /** - * Refer to https://developer.android.com/training/testing/ui-automator - * @deprecated Use {@link AppiumBy#androidUIAutomator(String)} instead. - * @param uiautomatorText is Android UIAutomator string - * @return an instance of {@link ByAndroidUIAutomator} - */ - @Deprecated - public static By AndroidUIAutomator(final String uiautomatorText) { - return new ByAndroidUIAutomator(uiautomatorText); - } - - /** - * About Android accessibility - * https://developer.android.com/intl/ru/training/accessibility/accessible-app.html - * About iOS accessibility - * https://developer.apple.com/library/ios/documentation/UIKit/Reference/ - * UIAccessibilityIdentification_Protocol/index.html - * @deprecated Use {@link AppiumBy#accessibilityId(String)} instead. - * @param accessibilityId id is a convenient UI automation accessibility Id. - * @return an instance of {@link ByAndroidUIAutomator} - */ - @Deprecated - public static By AccessibilityId(final String accessibilityId) { - return new ByAccessibilityId(accessibilityId); - } - - /** - * This locator strategy is available in XCUITest Driver mode. - * @deprecated Use {@link AppiumBy#iOSClassChain(String)} instead. - * @param iOSClassChainString is a valid class chain locator string. - * See - * the documentation for more details - * @return an instance of {@link ByIosClassChain} - */ - @Deprecated - public static By iOSClassChain(final String iOSClassChainString) { - return new ByIosClassChain(iOSClassChainString); - } - - /** - * This locator strategy is only available in Espresso Driver mode. - * @deprecated Use {@link AppiumBy#androidDataMatcher(String)} instead. - * @param dataMatcherString is a valid json string detailing hamcrest matcher for Espresso onData(). - * See - * the documentation for more details - * @return an instance of {@link ByAndroidDataMatcher} - */ - @Deprecated - public static By androidDataMatcher(final String dataMatcherString) { - return new ByAndroidDataMatcher(dataMatcherString); - } - - /** - * This locator strategy is only available in Espresso Driver mode. - * @deprecated Use {@link AppiumBy#androidViewMatcher(String)} instead. - * @param viewMatcherString is a valid json string detailing hamcrest matcher for Espresso onView(). - * See - * the documentation for more details - * @return an instance of {@link ByAndroidViewMatcher} - */ - @Deprecated - public static By androidViewMatcher(final String viewMatcherString) { - return new ByAndroidViewMatcher(viewMatcherString); - } - - /** - * This locator strategy is available in XCUITest Driver mode. - * @deprecated Use {@link AppiumBy#iOSNsPredicateString(String)} instead. - * @param iOSNsPredicateString is an iOS NsPredicate String - * @return an instance of {@link ByIosNsPredicate} - */ - @Deprecated - public static By iOSNsPredicateString(final String iOSNsPredicateString) { - return new ByIosNsPredicate(iOSNsPredicateString); - } - - /** - * The Windows UIAutomation selector. - * @deprecated Not supported on the server side. - * @param windowsAutomation The element name in the Windows UIAutomation selector - * @return an instance of {@link MobileBy.ByWindowsAutomation} - */ - @Deprecated - public static By windowsAutomation(final String windowsAutomation) { - return new ByWindowsAutomation(windowsAutomation); - } - - /** - * This locator strategy is available in Espresso Driver mode. - * @deprecated Use {@link AppiumBy#androidViewTag(String)} instead. - * @since Appium 1.8.2 beta - * @param tag is an view tag string - * @return an instance of {@link ByAndroidViewTag} - */ - @Deprecated - public static By AndroidViewTag(final String tag) { - return new ByAndroidViewTag(tag); - } - - /** - * This locator strategy is available only if OpenCV libraries and - * NodeJS bindings are installed on the server machine. - * - * @deprecated Use {@link AppiumBy#image(String)} instead. - * @see - * The documentation on Image Comparison Features - * @see - * The settings available for lookup fine-tuning - * @since Appium 1.8.2 - * @param b64Template base64-encoded template image string. Supported image formats are the same - * as for OpenCV library. - * @return an instance of {@link ByImage} - */ - @Deprecated - public static By image(final String b64Template) { - return new ByImage(b64Template); - } - - /** - * This type of locator requires the use of the 'customFindModules' capability and a - * separately-installed element finding plugin. - * - * @deprecated Use {@link AppiumBy#custom(String)} instead. - * @param selector selector to pass to the custom element finding plugin - * @return an instance of {@link ByCustom} - * @since Appium 1.9.2 - */ - @Deprecated - public static By custom(final String selector) { - return new ByCustom(selector); - } - - /** - * Refer to https://developer.android.com/training/testing/ui-automator - * - * @deprecated Use {@link AppiumBy.ByAndroidUIAutomator} instead. - */ - @Deprecated - public static class ByAndroidUIAutomator extends AppiumBy.ByAndroidUIAutomator { - - public ByAndroidUIAutomator(String uiautomatorText) { - super(uiautomatorText); - } - - @Override public String toString() { - return "By.AndroidUIAutomator: " + getRemoteParameters().value(); - } - } - - /** - * About Android accessibility - * https://developer.android.com/intl/ru/training/accessibility/accessible-app.html - * About iOS accessibility - * https://developer.apple.com/library/ios/documentation/UIKit/Reference/ - * UIAccessibilityIdentification_Protocol/index.html - * @deprecated Use {@link AppiumBy.ByAccessibilityId} instead. - */ - @Deprecated - public static class ByAccessibilityId extends AppiumBy.ByAccessibilityId { - - public ByAccessibilityId(String accessibilityId) { - super(accessibilityId); - } - - @Override public String toString() { - return "By.AccessibilityId: " + getRemoteParameters().value(); - } - } - - /** - * This locator strategy is available in XCUITest Driver mode. - * See - * the documentation for more details - * @deprecated Use {@link AppiumBy.ByIosClassChain} instead. - */ - @Deprecated - public static class ByIosClassChain extends AppiumBy.ByIosClassChain { - - protected ByIosClassChain(String locatorString) { - super(locatorString); - } - - @Override public String toString() { - return "By.IosClassChain: " + getRemoteParameters().value(); - } - } - - /** - * This locator strategy is only available in Espresso Driver mode. - * See - * the documentation for more details - * @deprecated Use {@link AppiumBy.ByAndroidDataMatcher} instead. - */ - @Deprecated - public static class ByAndroidDataMatcher extends AppiumBy.ByAndroidDataMatcher { - - protected ByAndroidDataMatcher(String locatorString) { - super(locatorString); - } - - @Override public String toString() { - return "By.AndroidDataMatcher: " + getRemoteParameters().value(); - } - } - - /** - * This locator strategy is only available in Espresso Driver mode. - * See - * the documentation for more details - * @deprecated Use {@link AppiumBy.ByAndroidViewMatcher} instead. - */ - @Deprecated - public static class ByAndroidViewMatcher extends AppiumBy.ByAndroidViewMatcher { - - protected ByAndroidViewMatcher(String locatorString) { - super(locatorString); - } - - @Override public String toString() { - return "By.AndroidViewMatcher: " + getRemoteParameters().value(); - } - } - - /** - * This locator strategy is available in XCUITest Driver mode. - * @deprecated Use {@link AppiumBy.ByIosNsPredicate} instead. - */ - @Deprecated - public static class ByIosNsPredicate extends AppiumBy.ByIosNsPredicate { - - protected ByIosNsPredicate(String locatorString) { - super(locatorString); - } - - @Override public String toString() { - return "By.IosNsPredicate: " + getRemoteParameters().value(); - } - } - - /** - * The Windows UIAutomation selector. - * @deprecated Not supported on the server side. - */ - @Deprecated - public static class ByWindowsAutomation extends MobileBy implements Serializable { - - protected ByWindowsAutomation(String locatorString) { - super("-windows uiautomation", locatorString, "windowsAutomation"); - } - - @Override public String toString() { - return "By.windowsAutomation: " + getRemoteParameters().value(); - } - } - - /** - * This locator strategy is available only if OpenCV libraries and - * NodeJS bindings are installed on the server machine. - * @deprecated Use {@link AppiumBy.ByImage} instead. - */ - @Deprecated - public static class ByImage extends AppiumBy.ByImage { - - protected ByImage(String b64Template) { - super(b64Template); - } - - @Override public String toString() { - return "By.Image: " + getRemoteParameters().value(); - } - } - - /** - * This type of locator requires the use of the 'customFindModules' capability and a - * separately-installed element finding plugin. - * @deprecated Use {@link AppiumBy.ByCustom} instead. - */ - @Deprecated - public static class ByCustom extends AppiumBy.ByCustom { - - protected ByCustom(String selector) { - super(selector); - } - - @Override public String toString() { - return "By.Custom: " + getRemoteParameters().value(); - } - } - - /** - * This locator strategy is available in Espresso Driver mode. - * @deprecated Use {@link AppiumBy.ByAndroidViewTag} instead. - */ - @Deprecated - public static class ByAndroidViewTag extends AppiumBy.ByAndroidViewTag { - - public ByAndroidViewTag(String tag) { - super(tag); - } - - @Override public String toString() { - return "By.AndroidViewTag: " + getRemoteParameters().value(); - } - } -} diff --git a/src/main/java/io/appium/java_client/MobileCommand.java b/src/main/java/io/appium/java_client/MobileCommand.java index 4d20164ad..b4df90047 100644 --- a/src/main/java/io/appium/java_client/MobileCommand.java +++ b/src/main/java/io/appium/java_client/MobileCommand.java @@ -17,105 +17,180 @@ package io.appium.java_client; import com.google.common.collect.ImmutableMap; - import io.appium.java_client.imagecomparison.BaseComparisonOptions; import io.appium.java_client.imagecomparison.ComparisonMode; import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; -import org.apache.commons.lang3.StringUtils; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.http.HttpMethod; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.AbstractMap; +import java.util.Collections; import java.util.HashMap; import java.util.Map; -import javax.annotation.Nullable; +import java.util.Optional; + +import static com.google.common.base.Strings.isNullOrEmpty; /** * The repository of mobile commands defined in the Mobile JSON * wire protocol. + * Most of these commands are platform-specific obsolete things and should eventually be replaced with + * calls to corresponding `mobile:` extensions, so we don't abuse non-w3c APIs */ +@SuppressWarnings({"checkstyle:HideUtilityClassConstructor", "checkstyle:ConstantName"}) public class MobileCommand { //General + @Deprecated protected static final String RESET; + @Deprecated protected static final String GET_STRINGS; + @Deprecated public static final String SET_VALUE; + @Deprecated protected static final String PULL_FILE; + @Deprecated protected static final String PULL_FOLDER; + @Deprecated public static final String RUN_APP_IN_BACKGROUND; + @Deprecated protected static final String PERFORM_TOUCH_ACTION; + @Deprecated protected static final String PERFORM_MULTI_TOUCH; + @Deprecated public static final String LAUNCH_APP; + @Deprecated public static final String CLOSE_APP; + @Deprecated protected static final String GET_DEVICE_TIME; + @Deprecated protected static final String GET_SESSION; protected static final String LOG_EVENT; protected static final String GET_EVENTS; //region Applications Management + @Deprecated protected static final String IS_APP_INSTALLED; + @Deprecated protected static final String INSTALL_APP; + @Deprecated protected static final String ACTIVATE_APP; + @Deprecated protected static final String QUERY_APP_STATE; + @Deprecated protected static final String TERMINATE_APP; + @Deprecated protected static final String REMOVE_APP; //endregion //region Clipboard + @Deprecated public static final String GET_CLIPBOARD; + @Deprecated public static final String SET_CLIPBOARD; //endregion + @Deprecated protected static final String GET_PERFORMANCE_DATA; + @Deprecated protected static final String GET_SUPPORTED_PERFORMANCE_DATA_TYPES; + @Deprecated public static final String START_RECORDING_SCREEN; + @Deprecated public static final String STOP_RECORDING_SCREEN; + @Deprecated protected static final String HIDE_KEYBOARD; + @Deprecated protected static final String LOCK; //iOS + @Deprecated protected static final String SHAKE; + @Deprecated protected static final String TOUCH_ID; + @Deprecated protected static final String TOUCH_ID_ENROLLMENT; //Android - protected static final String CURRENT_ACTIVITY; + @Deprecated + public static final String CURRENT_ACTIVITY; + @Deprecated protected static final String END_TEST_COVERAGE; + @Deprecated protected static final String GET_DISPLAY_DENSITY; + @Deprecated protected static final String GET_NETWORK_CONNECTION; + @Deprecated protected static final String GET_SYSTEM_BARS; + @Deprecated protected static final String IS_KEYBOARD_SHOWN; + @Deprecated protected static final String IS_LOCKED; + @Deprecated public static final String LONG_PRESS_KEY_CODE; + @Deprecated protected static final String FINGER_PRINT; + @Deprecated protected static final String OPEN_NOTIFICATIONS; + @Deprecated public static final String PRESS_KEY_CODE; + @Deprecated protected static final String PUSH_FILE; + @Deprecated protected static final String SET_NETWORK_CONNECTION; + @Deprecated protected static final String START_ACTIVITY; + @Deprecated protected static final String TOGGLE_LOCATION_SERVICES; + @Deprecated protected static final String UNLOCK; + @Deprecated public static final String REPLACE_VALUE; protected static final String GET_SETTINGS; + @Deprecated protected static final String SET_SETTINGS; - protected static final String GET_CURRENT_PACKAGE; - protected static final String SEND_SMS; - protected static final String GSM_CALL; - protected static final String GSM_SIGNAL; - protected static final String GSM_VOICE; - protected static final String NETWORK_SPEED; - protected static final String POWER_CAPACITY; - protected static final String POWER_AC_STATE; + @Deprecated + public static final String GET_CURRENT_PACKAGE; + @Deprecated + public static final String SEND_SMS; + @Deprecated + public static final String GSM_CALL; + @Deprecated + public static final String GSM_SIGNAL; + @Deprecated + public static final String GSM_VOICE; + @Deprecated + public static final String NETWORK_SPEED; + @Deprecated + public static final String POWER_CAPACITY; + @Deprecated + public static final String POWER_AC_STATE; + @Deprecated protected static final String TOGGLE_WIFI; + @Deprecated protected static final String TOGGLE_AIRPLANE_MODE; + @Deprecated protected static final String TOGGLE_DATA; protected static final String COMPARE_IMAGES; protected static final String EXECUTE_DRIVER_SCRIPT; + @Deprecated protected static final String GET_ALLSESSION; protected static final String EXECUTE_GOOGLE_CDP_COMMAND; + public static final String GET_SCREEN_ORIENTATION = "getScreenOrientation"; + public static final String SET_SCREEN_ORIENTATION = "setScreenOrientation"; + public static final String GET_SCREEN_ROTATION = "getScreenRotation"; + public static final String SET_SCREEN_ROTATION = "setScreenRotation"; + + public static final String GET_CONTEXT_HANDLES = "getContextHandles"; + public static final String GET_CURRENT_CONTEXT_HANDLE = "getCurrentContextHandle"; + public static final String SWITCH_TO_CONTEXT = "switchToContext"; + + public static final String GET_LOCATION = "getLocation"; + public static final String SET_LOCATION = "setLocation"; + public static final Map commandRepository; static { @@ -285,6 +360,18 @@ public class MobileCommand { commandRepository.put(EXECUTE_DRIVER_SCRIPT, postC("/session/:sessionId/appium/execute_driver")); commandRepository.put(GET_ALLSESSION, getC("/sessions")); commandRepository.put(EXECUTE_GOOGLE_CDP_COMMAND, postC("/session/:sessionId/goog/cdp/execute")); + + commandRepository.put(GET_SCREEN_ORIENTATION, getC("/session/:sessionId/orientation")); + commandRepository.put(SET_SCREEN_ORIENTATION, postC("/session/:sessionId/orientation")); + commandRepository.put(GET_SCREEN_ROTATION, getC("/session/:sessionId/rotation")); + commandRepository.put(SET_SCREEN_ROTATION, postC("/session/:sessionId/rotation")); + + commandRepository.put(GET_CONTEXT_HANDLES, getC("/session/:sessionId/contexts")); + commandRepository.put(GET_CURRENT_CONTEXT_HANDLE, getC("/session/:sessionId/context")); + commandRepository.put(SWITCH_TO_CONTEXT, postC("/session/:sessionId/context")); + + commandRepository.put(GET_LOCATION, getC("/session/:sessionId/location")); + commandRepository.put(SET_LOCATION, postC("/session/:sessionId/location")); } /** @@ -318,35 +405,34 @@ public static AppiumCommandInfo deleteC(String url) { } /** - * This method forms a {@link java.util.Map} of parameters for the - * keyboard hiding. + * This method forms a {@link Map} of parameters for the keyboard hiding. * * @param keyName The button pressed by the mobile driver to attempt hiding the * keyboard. - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> hideKeyboardCommand(String keyName) { - return new AbstractMap.SimpleEntry<>( - HIDE_KEYBOARD, prepareArguments("keyName", keyName)); + return Map.entry(HIDE_KEYBOARD, Map.of("keyName", keyName)); } /** - * This method forms a {@link java.util.Map} of parameters for the - * keyboard hiding. + * This method forms a {@link Map} of parameters for the keyboard hiding. * * @param strategy HideKeyboardStrategy. * @param keyName a String, representing the text displayed on the button of the * keyboard you want to press. For example: "Done". - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> hideKeyboardCommand(String strategy, String keyName) { - String[] parameters = new String[]{"strategy", "key"}; - Object[] values = new Object[]{strategy, keyName}; - return new AbstractMap.SimpleEntry<>( - HIDE_KEYBOARD, prepareArguments(parameters, values)); + return Map.entry(HIDE_KEYBOARD, Map.of( + "strategy", strategy, + "key", keyName + )); } /** @@ -355,7 +441,9 @@ public static AppiumCommandInfo deleteC(String url) { * @param param is a parameter name. * @param value is the parameter value. * @return built {@link ImmutableMap}. + * @deprecated Use {@link Map#of(Object, Object)} */ + @Deprecated public static ImmutableMap prepareArguments(String param, Object value) { ImmutableMap.Builder builder = ImmutableMap.builder(); @@ -369,12 +457,14 @@ public static ImmutableMap prepareArguments(String param, * @param params is the array with parameter names. * @param values is the array with parameter values. * @return built {@link ImmutableMap}. + * @deprecated Use {@link Map#of(Object, Object, Object, Object)} */ + @Deprecated public static ImmutableMap prepareArguments(String[] params, Object[] values) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (int i = 0; i < params.length; i++) { - if (!StringUtils.isBlank(params[i]) && (values[i] != null)) { + if (!isNullOrEmpty(params[i]) && values[i] != null) { builder.put(params[i], values[i]); } } @@ -382,139 +472,135 @@ public static ImmutableMap prepareArguments(String[] params, } /** - * This method forms a {@link java.util.Map} of parameters for the - * key event invocation. + * This method forms a {@link Map} of parameters for the key event invocation. * * @param key code for the key pressed on the device. - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> pressKeyCodeCommand(int key) { - return new AbstractMap.SimpleEntry<>( - PRESS_KEY_CODE, prepareArguments("keycode", key)); + return Map.entry(PRESS_KEY_CODE, Map.of("keycode", key)); } /** - * This method forms a {@link java.util.Map} of parameters for the - * key event invocation. + * This method forms a {@link Map} of parameters for the key event invocation. * * @param key code for the key pressed on the Android device. * @param metastate metastate for the keypress. - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> pressKeyCodeCommand(int key, Integer metastate) { - String[] parameters = new String[]{"keycode", "metastate"}; - Object[] values = new Object[]{key, metastate}; - return new AbstractMap.SimpleEntry<>( - PRESS_KEY_CODE, prepareArguments(parameters, values)); + return Map.entry(PRESS_KEY_CODE, Map.of( + "keycode", key, + "metastate", metastate + )); } /** - * This method forms a {@link java.util.Map} of parameters for the - * long key event invocation. + * This method forms a {@link Map} of parameters for the long key event invocation. * * @param key code for the long key pressed on the device. - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> longPressKeyCodeCommand(int key) { - return new AbstractMap.SimpleEntry<>( - LONG_PRESS_KEY_CODE, prepareArguments("keycode", key)); + return Map.entry(LONG_PRESS_KEY_CODE, Map.of("keycode", key)); } /** - * This method forms a {@link java.util.Map} of parameters for the - * long key event invocation. + * This method forms a {@link Map} of parameters for the long key event invocation. * * @param key code for the long key pressed on the Android device. * @param metastate metastate for the long key press. - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> longPressKeyCodeCommand(int key, Integer metastate) { - String[] parameters = new String[]{"keycode", "metastate"}; - Object[] values = new Object[]{key, metastate}; - return new AbstractMap.SimpleEntry<>( - LONG_PRESS_KEY_CODE, prepareArguments(parameters, values)); + return Map.entry(LONG_PRESS_KEY_CODE, Map.of( + "keycode", key, + "metastate", metastate + )); } /** - * This method forms a {@link java.util.Map} of parameters for the - * device locking. + * This method forms a {@link Map} of parameters for the device locking. * * @param duration for how long to lock the screen for. Minimum time resolution is one second - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> lockDeviceCommand(Duration duration) { - return new AbstractMap.SimpleEntry<>( - LOCK, prepareArguments("seconds", duration.getSeconds())); + return Map.entry(LOCK, Map.of("seconds", duration.getSeconds())); } /** - * This method forms a {@link java.util.Map} of parameters for the - * device unlocking. + * This method forms a {@link Map} of parameters for the device unlocking. * - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> unlockDeviceCommand() { - return new AbstractMap.SimpleEntry<>(UNLOCK, ImmutableMap.of()); + return Map.entry(UNLOCK, Map.of()); } /** - * This method forms a {@link java.util.Map} of parameters for the - * device locked query. + * This method forms a {@link Map} of parameters for the device locked query. * - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> getIsDeviceLockedCommand() { - return new AbstractMap.SimpleEntry<>(IS_LOCKED, ImmutableMap.of()); + return Map.entry(IS_LOCKED, Map.of()); } public static Map.Entry> getSettingsCommand() { - return new AbstractMap.SimpleEntry<>(GET_SETTINGS, ImmutableMap.of()); + return Map.entry(GET_SETTINGS, Map.of()); } public static Map.Entry> setSettingsCommand(String setting, Object value) { - return setSettingsCommand(prepareArguments(setting, value)); + return setSettingsCommand(Map.of(setting, value)); } public static Map.Entry> setSettingsCommand(Map settings) { - return new AbstractMap.SimpleEntry<>(SET_SETTINGS, prepareArguments("settings", settings)); + return Map.entry(SET_SETTINGS, Map.of("settings", settings)); } /** - * This method forms a {@link java.util.Map} of parameters for the - * file pushing. + * This method forms a {@link Map} of parameters for the file pushing. * * @param remotePath Path to file to write data to on remote device * @param base64Data Base64 encoded byte array of data to write to remote device - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> pushFileCommand(String remotePath, byte[] base64Data) { - String[] parameters = new String[]{"path", "data"}; - Object[] values = new Object[]{remotePath, new String(base64Data, StandardCharsets.UTF_8)}; - return new AbstractMap.SimpleEntry<>(PUSH_FILE, prepareArguments(parameters, values)); + return Map.entry(PUSH_FILE, Map.of( + "path", remotePath, + "data", new String(base64Data, StandardCharsets.UTF_8) + )); } public static Map.Entry> startRecordingScreenCommand(BaseStartScreenRecordingOptions opts) { - return new AbstractMap.SimpleEntry<>(START_RECORDING_SCREEN, - prepareArguments("options", opts.build())); + return Map.entry(START_RECORDING_SCREEN, Map.of("options", opts.build())); } public static Map.Entry> stopRecordingScreenCommand(BaseStopScreenRecordingOptions opts) { - return new AbstractMap.SimpleEntry<>(STOP_RECORDING_SCREEN, - prepareArguments("options", opts.build())); + return Map.entry(STOP_RECORDING_SCREEN, Map.of("options", opts.build())); } /** - * Forms a {@link java.util.Map} of parameters for images comparison. + * Forms a {@link Map} of parameters for images comparison. * * @param mode one of possible comparison modes * @param img1Data base64-encoded data of the first image @@ -525,23 +611,22 @@ public static ImmutableMap prepareArguments(String[] params, public static Map.Entry> compareImagesCommand(ComparisonMode mode, byte[] img1Data, byte[] img2Data, @Nullable BaseComparisonOptions options) { - String[] parameters = options == null - ? new String[]{"mode", "firstImage", "secondImage"} - : new String[]{"mode", "firstImage", "secondImage", "options"}; - Object[] values = options == null - ? new Object[]{mode.toString(), new String(img1Data, StandardCharsets.UTF_8), - new String(img2Data, StandardCharsets.UTF_8)} - : new Object[]{mode.toString(), new String(img1Data, StandardCharsets.UTF_8), - new String(img2Data, StandardCharsets.UTF_8), options.build()}; - return new AbstractMap.SimpleEntry<>(COMPARE_IMAGES, prepareArguments(parameters, values)); + var args = new HashMap(); + args.put("mode", mode.toString()); + args.put("firstImage", new String(img1Data, StandardCharsets.UTF_8)); + args.put("secondImage", new String(img2Data, StandardCharsets.UTF_8)); + Optional.ofNullable(options).ifPresent(opts -> args.put("options", options.build())); + return Map.entry(COMPARE_IMAGES, Collections.unmodifiableMap(args)); } /** * This method forms a {@link Map} of parameters for the checking of the keyboard state (is it shown or not). * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> isKeyboardShownCommand() { - return new AbstractMap.SimpleEntry<>(IS_KEYBOARD_SHOWN, ImmutableMap.of()); + return Map.entry(IS_KEYBOARD_SHOWN, Map.of()); } } diff --git a/src/main/java/io/appium/java_client/MultiTouchAction.java b/src/main/java/io/appium/java_client/MultiTouchAction.java index ac66594dc..d82b47b1f 100644 --- a/src/main/java/io/appium/java_client/MultiTouchAction.java +++ b/src/main/java/io/appium/java_client/MultiTouchAction.java @@ -16,15 +16,13 @@ package io.appium.java_client; -import static com.google.common.base.Preconditions.checkArgument; -import static java.util.stream.Collectors.toList; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - +import java.util.ArrayList; import java.util.List; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.stream.Collectors.toList; + /** * Used for Webdriver 3 multi-touch gestures * See the Webriver 3 spec https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html @@ -54,12 +52,12 @@ @Deprecated public class MultiTouchAction implements PerformsActions { - private ImmutableList.Builder actions; + private List actions; private PerformsTouchActions performsTouchActions; public MultiTouchAction(PerformsTouchActions performsTouchActions) { this.performsTouchActions = performsTouchActions; - actions = ImmutableList.builder(); + actions = new ArrayList<>(); } /** @@ -77,22 +75,20 @@ public MultiTouchAction add(TouchAction action) { * Perform the multi-touch action on the mobile performsTouchActions. */ public MultiTouchAction perform() { - List touchActions = actions.build(); - checkArgument(touchActions.size() > 0, + checkArgument(!actions.isEmpty(), "MultiTouch action must have at least one TouchAction added before it can be performed"); - if (touchActions.size() > 1) { + if (actions.size() > 1) { performsTouchActions.performMultiTouchAction(this); return this; } //android doesn't like having multi-touch actions with only a single TouchAction... - performsTouchActions.performTouchAction(touchActions.get(0)); + performsTouchActions.performTouchAction(actions.get(0)); return this; } protected Map> getParameters() { - ImmutableList touchActions = actions.build(); - return ImmutableMap.of("actions", - touchActions.stream().map(touchAction -> - touchAction.getParameters().get("actions")).collect(toList())); + return Map.of("actions", + actions.stream().map(touchAction -> touchAction.getParameters().get("actions")).collect(toList()) + ); } /** @@ -101,7 +97,7 @@ protected Map> getParameters() { * @return this MultiTouchAction, for possible segmented-touches. */ protected MultiTouchAction clearActions() { - actions = ImmutableList.builder(); + actions = new ArrayList<>(); return this; } } diff --git a/src/main/java/io/appium/java_client/PerformsTouchActions.java b/src/main/java/io/appium/java_client/PerformsTouchActions.java index 0ac7776d6..539a38a52 100644 --- a/src/main/java/io/appium/java_client/PerformsTouchActions.java +++ b/src/main/java/io/appium/java_client/PerformsTouchActions.java @@ -16,12 +16,12 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.PERFORM_MULTI_TOUCH; -import static io.appium.java_client.MobileCommand.PERFORM_TOUCH_ACTION; - import java.util.List; import java.util.Map; +import static io.appium.java_client.MobileCommand.PERFORM_MULTI_TOUCH; +import static io.appium.java_client.MobileCommand.PERFORM_TOUCH_ACTION; + /** * Touch actions are deprecated. * Please use W3C Actions instead or the corresponding diff --git a/src/main/java/io/appium/java_client/PullsFiles.java b/src/main/java/io/appium/java_client/PullsFiles.java index 590e4aaa0..3c1e7ccff 100644 --- a/src/main/java/io/appium/java_client/PullsFiles.java +++ b/src/main/java/io/appium/java_client/PullsFiles.java @@ -16,17 +16,17 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.PULL_FILE; -import static io.appium.java_client.MobileCommand.PULL_FOLDER; - -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.remote.Response; +import org.openqa.selenium.UnsupportedCommandException; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.PULL_FILE; +import static io.appium.java_client.MobileCommand.PULL_FOLDER; +import static java.util.Objects.requireNonNull; -public interface PullsFiles extends ExecutesMethod { +public interface PullsFiles extends ExecutesMethod, CanRememberExtensionPresence { /** * Pull a file from the remote system. @@ -34,15 +34,29 @@ public interface PullsFiles extends ExecutesMethod { * built with debuggable flag enabled in order to get access to its container * on the internal file system. * - * @param remotePath If the path starts with @applicationId// prefix, then the file - * will be pulled from the root of the corresponding application container. - * Otherwise, the root folder is considered as / on Android and - * on iOS it is a media folder root (real devices only). + * @param remotePath Path to file to read data from the remote device. + * Check the documentation on `mobile: pullFile` + * extension for more details on possible values + * for different platforms. * @return A byte array of Base64 encoded data. */ default byte[] pullFile(String remotePath) { - Response response = execute(PULL_FILE, ImmutableMap.of("path", remotePath)); - String base64String = response.getValue().toString(); + final String extName = "mobile: pullFile"; + String base64String; + try { + base64String = requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, + Map.of("remotePath", remotePath) + ) + ); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + base64String = requireNonNull( + CommandExecutionHelper.execute(markExtensionAbsence(extName), + Map.entry(PULL_FILE, Map.of("path", remotePath)) + ) + ); + } return Base64.getDecoder().decode(base64String.getBytes(StandardCharsets.UTF_8)); } @@ -52,15 +66,29 @@ default byte[] pullFile(String remotePath) { * built with debuggable flag enabled in order to get access to its container * on the internal file system. * - * @param remotePath If the path starts with @applicationId/ prefix, then the folder - * will be pulled from the root of the corresponding application container. - * Otherwise, the root folder is considered as / on Android and - * on iOS it is a media folder root (real devices only). + * @param remotePath Path to a folder to read data from the remote device. + * Check the documentation on `mobile: pullFolder` + * extension for more details on possible values + * for different platforms. * @return A byte array of Base64 encoded zip archive data. */ default byte[] pullFolder(String remotePath) { - Response response = execute(PULL_FOLDER, ImmutableMap.of("path", remotePath)); - String base64String = response.getValue().toString(); + final String extName = "mobile: pullFolder"; + String base64String; + try { + base64String = requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, + Map.of("remotePath", remotePath) + ) + ); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + base64String = requireNonNull( + CommandExecutionHelper.execute(markExtensionAbsence(extName), + Map.entry(PULL_FOLDER, Map.of("path", remotePath)) + ) + ); + } return Base64.getDecoder().decode(base64String.getBytes(StandardCharsets.UTF_8)); } diff --git a/src/main/java/io/appium/java_client/PushesFiles.java b/src/main/java/io/appium/java_client/PushesFiles.java index d6da2ca82..a30bf0d3b 100644 --- a/src/main/java/io/appium/java_client/PushesFiles.java +++ b/src/main/java/io/appium/java_client/PushesFiles.java @@ -16,48 +16,50 @@ package io.appium.java_client; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.MobileCommand.pushFileCommand; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; +import org.openqa.selenium.UnsupportedCommandException; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.pushFileCommand; -public interface PushesFiles extends ExecutesMethod { +public interface PushesFiles extends ExecutesMethod, CanRememberExtensionPresence { /** - * Saves base64 encoded data as a media file on the remote system. + * Saves base64-encoded data as a file on the remote system. * - * @param remotePath Path to file to write data to on remote device - * Only the filename part matters there on Simulator, so the remote end - * can figure out which type of media data it is and save - * it into a proper folder on the target device. Check - * 'xcrun simctl addmedia' output to get more details on - * supported media types. - * If the path starts with @applicationId/ prefix, then the file - * will be pushed to the root of the corresponding application container. + * @param remotePath Path to file to write data to on remote device. + * Check the documentation on `mobile: pushFile` + * extension for more details on possible values + * for different platforms. * @param base64Data Base64 encoded byte array of media file data to write to remote device */ default void pushFile(String remotePath, byte[] base64Data) { - CommandExecutionHelper.execute(this, pushFileCommand(remotePath, base64Data)); + final String extName = "mobile: pushFile"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "remotePath", remotePath, + "payload", new String(base64Data, StandardCharsets.UTF_8) + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), pushFileCommand(remotePath, base64Data)); + } } /** - * Saves base64 encoded data as a media file on the remote system. + * Sends the file to the remote device. * * @param remotePath See the documentation on {@link #pushFile(String, byte[])} * @param file Is an existing local file to be written to the remote device - * @throws IOException when there are problems with a file or current file system + * @throws IOException when there are problems with a file on current file system */ default void pushFile(String remotePath, File file) throws IOException { - checkNotNull(file, "A reference to file should not be NULL"); - if (!file.exists()) { - throw new IOException(String.format("The given file %s doesn't exist", - file.getAbsolutePath())); - } - pushFile(remotePath, Base64.encodeBase64(FileUtils.readFileToByteArray(file))); + pushFile(remotePath, Base64.getEncoder().encode(Files.readAllBytes(file.toPath()))); } } diff --git a/src/main/java/io/appium/java_client/ScreenshotState.java b/src/main/java/io/appium/java_client/ScreenshotState.java index 5645b79e2..f51add334 100644 --- a/src/main/java/io/appium/java_client/ScreenshotState.java +++ b/src/main/java/io/appium/java_client/ScreenshotState.java @@ -21,9 +21,7 @@ import lombok.Setter; import lombok.experimental.Accessors; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - +import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -33,7 +31,8 @@ import java.util.function.Function; import java.util.function.Supplier; -import javax.imageio.ImageIO; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; @Accessors(chain = true) public class ScreenshotState { @@ -82,7 +81,7 @@ public class ScreenshotState { * @param stateProvider lambda function, which returns a screenshot for further comparison */ public ScreenshotState(ComparesImages comparator, Supplier stateProvider) { - this.comparator = checkNotNull(comparator); + this.comparator = requireNonNull(comparator); this.stateProvider = stateProvider; } @@ -111,7 +110,7 @@ public ScreenshotState remember() { * @return self instance for chaining */ public ScreenshotState remember(BufferedImage customInitialState) { - this.previousScreenshot = checkNotNull(customInitialState); + this.previousScreenshot = requireNonNull(customInitialState); return this; } @@ -176,7 +175,7 @@ private ScreenshotState checkState(Function checkerFunc, Durati * @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet */ public ScreenshotState verifyChanged(Duration timeout, double minScore) { - return checkState((x) -> x < minScore, timeout); + return checkState(x -> x < minScore, timeout); } /** @@ -191,7 +190,7 @@ public ScreenshotState verifyChanged(Duration timeout, double minScore) { * @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet */ public ScreenshotState verifyNotChanged(Duration timeout, double minScore) { - return checkState((x) -> x >= minScore, timeout); + return checkState(x -> x >= minScore, timeout); } /** diff --git a/src/main/java/io/appium/java_client/Setting.java b/src/main/java/io/appium/java_client/Setting.java index b5b84ca16..cc1f44cd1 100644 --- a/src/main/java/io/appium/java_client/Setting.java +++ b/src/main/java/io/appium/java_client/Setting.java @@ -65,6 +65,7 @@ public enum Setting { this.name = name; } + @Override public String toString() { return this.name; } diff --git a/src/main/java/io/appium/java_client/SupportsLegacyAppManagement.java b/src/main/java/io/appium/java_client/SupportsLegacyAppManagement.java deleted file mode 100644 index 7be14ec6d..000000000 --- a/src/main/java/io/appium/java_client/SupportsLegacyAppManagement.java +++ /dev/null @@ -1,59 +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; - -import static io.appium.java_client.MobileCommand.CLOSE_APP; -import static io.appium.java_client.MobileCommand.LAUNCH_APP; -import static io.appium.java_client.MobileCommand.RESET; - -@Deprecated -public interface SupportsLegacyAppManagement extends ExecutesMethod { - /** - * Launches the app, which was provided in the capabilities at session creation, - * and (re)starts the session. - * - * @deprecated This method is deprecated and will be removed. - * See https://github.com/appium/appium/issues/15807 - */ - @Deprecated - default void launchApp() { - execute(LAUNCH_APP); - } - - /** - * Resets the currently running app together with the session. - * - * @deprecated This method is deprecated and will be removed. - * See https://github.com/appium/appium/issues/15807 - */ - @Deprecated - default void resetApp() { - execute(RESET); - } - - /** - * Close the app which was provided in the capabilities at session creation - * and quits the session. - * - * @deprecated This method is deprecated and will be removed. - * See https://github.com/appium/appium/issues/15807 - */ - @Deprecated - default void closeApp() { - execute(CLOSE_APP); - } -} diff --git a/src/main/java/io/appium/java_client/TouchAction.java b/src/main/java/io/appium/java_client/TouchAction.java index 866e87140..6f6621f0b 100644 --- a/src/main/java/io/appium/java_client/TouchAction.java +++ b/src/main/java/io/appium/java_client/TouchAction.java @@ -16,13 +16,6 @@ package io.appium.java_client; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableList.builder; -import static java.util.stream.Collectors.toList; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.touch.ActionOptions; import io.appium.java_client.touch.LongPressOptions; import io.appium.java_client.touch.TapOptions; @@ -30,9 +23,15 @@ import io.appium.java_client.touch.offset.ElementOption; import io.appium.java_client.touch.offset.PointOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; + /** * Used for Webdriver 3 touch actions * See the Webriver 3 spec @@ -55,12 +54,12 @@ @Deprecated public class TouchAction> implements PerformsActions { - protected ImmutableList.Builder parameterBuilder; + protected List parameters; private PerformsTouchActions performsTouchActions; public TouchAction(PerformsTouchActions performsTouchActions) { - this.performsTouchActions = checkNotNull(performsTouchActions); - parameterBuilder = builder(); + this.performsTouchActions = requireNonNull(performsTouchActions); + parameters = new ArrayList<>(); } /** @@ -70,7 +69,7 @@ public TouchAction(PerformsTouchActions performsTouchActions) { * @return this TouchAction, for chaining. */ public T press(PointOption pressOptions) { - parameterBuilder.add(new ActionParameter("press", pressOptions)); + parameters.add(new ActionParameter("press", pressOptions)); //noinspection unchecked return (T) this; } @@ -81,8 +80,7 @@ public T press(PointOption pressOptions) { * @return this TouchAction, for chaining. */ public T release() { - ActionParameter action = new ActionParameter("release"); - parameterBuilder.add(action); + parameters.add(new ActionParameter("release")); //noinspection unchecked return (T) this; } @@ -99,8 +97,7 @@ public T release() { * @return this TouchAction, for chaining. */ public T moveTo(PointOption moveToOptions) { - ActionParameter action = new ActionParameter("moveTo", moveToOptions); - parameterBuilder.add(action); + parameters.add(new ActionParameter("moveTo", moveToOptions)); return (T) this; } @@ -111,8 +108,7 @@ public T moveTo(PointOption moveToOptions) { * @return this TouchAction, for chaining. */ public T tap(TapOptions tapOptions) { - ActionParameter action = new ActionParameter("tap", tapOptions); - parameterBuilder.add(action); + parameters.add(new ActionParameter("tap", tapOptions)); return (T) this; } @@ -123,8 +119,7 @@ public T tap(TapOptions tapOptions) { * @return this TouchAction, for chaining. */ public T tap(PointOption tapOptions) { - ActionParameter action = new ActionParameter("tap", tapOptions); - parameterBuilder.add(action); + parameters.add(new ActionParameter("tap", tapOptions)); return (T) this; } @@ -134,8 +129,7 @@ public T tap(PointOption tapOptions) { * @return this TouchAction, for chaining. */ public T waitAction() { - ActionParameter action = new ActionParameter("wait"); - parameterBuilder.add(action); + parameters.add(new ActionParameter("wait")); //noinspection unchecked return (T) this; } @@ -147,8 +141,7 @@ public T waitAction() { * @return this TouchAction, for chaining. */ public T waitAction(WaitOptions waitOptions) { - ActionParameter action = new ActionParameter("wait", waitOptions); - parameterBuilder.add(action); + parameters.add(new ActionParameter("wait", waitOptions)); //noinspection unchecked return (T) this; } @@ -160,8 +153,7 @@ public T waitAction(WaitOptions waitOptions) { * @return this TouchAction, for chaining. */ public T longPress(LongPressOptions longPressOptions) { - ActionParameter action = new ActionParameter("longPress", longPressOptions); - parameterBuilder.add(action); + parameters.add(new ActionParameter("longPress", longPressOptions)); //noinspection unchecked return (T) this; } @@ -173,8 +165,7 @@ public T longPress(LongPressOptions longPressOptions) { * @return this TouchAction, for chaining. */ public T longPress(PointOption longPressOptions) { - ActionParameter action = new ActionParameter("longPress", longPressOptions); - parameterBuilder.add(action); + parameters.add(new ActionParameter("longPress", longPressOptions)); //noinspection unchecked return (T) this; } @@ -183,8 +174,7 @@ public T longPress(PointOption longPressOptions) { * Cancel this action, if it was partially completed by the performsTouchActions. */ public void cancel() { - ActionParameter action = new ActionParameter("cancel"); - parameterBuilder.add(action); + parameters.add(new ActionParameter("cancel")); this.perform(); } @@ -205,9 +195,9 @@ public T perform() { * @return A map of parameters for this touch action to pass as part of mjsonwp. */ protected Map> getParameters() { - List actionList = parameterBuilder.build(); - return ImmutableMap.of("actions", actionList.stream() - .map(ActionParameter::getParameterMap).collect(toList())); + return Map.of("actions", + parameters.stream().map(ActionParameter::getParameterMap).collect(toList()) + ); } /** @@ -216,7 +206,7 @@ protected Map> getParameters() { * @return this TouchAction, for possible segmented-touches. */ protected T clearParameters() { - parameterBuilder = builder(); + parameters = new ArrayList<>(); //noinspection unchecked return (T) this; } @@ -225,26 +215,26 @@ protected T clearParameters() { * Just holds values to eventually return the parameters required for the mjsonwp. */ protected class ActionParameter { - private String actionName; - private ImmutableMap.Builder optionsBuilder; + private final String actionName; + private final Map options; public ActionParameter(String actionName) { this.actionName = actionName; - optionsBuilder = ImmutableMap.builder(); + options = new HashMap<>(); } public ActionParameter(String actionName, ActionOptions opts) { - checkNotNull(opts); - this.actionName = actionName; - optionsBuilder = ImmutableMap.builder(); + this(actionName); + requireNonNull(opts); //noinspection unchecked - optionsBuilder.putAll(opts.build()); + options.putAll(opts.build()); } - public ImmutableMap getParameterMap() { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("action", actionName).put("options", optionsBuilder.build()); - return builder.build(); + public Map getParameterMap() { + return Map.of( + "action", actionName, + "options", Collections.unmodifiableMap(options) + ); } } } diff --git a/src/main/java/io/appium/java_client/android/Activity.java b/src/main/java/io/appium/java_client/android/Activity.java index 41a17dc8c..34821f8d4 100644 --- a/src/main/java/io/appium/java_client/android/Activity.java +++ b/src/main/java/io/appium/java_client/android/Activity.java @@ -4,7 +4,7 @@ import lombok.experimental.Accessors; import static com.google.common.base.Preconditions.checkArgument; -import static org.apache.commons.lang3.StringUtils.isBlank; +import static com.google.common.base.Strings.isNullOrEmpty; /** * This is a simple POJO class to support the {@link StartsActivity}. @@ -29,9 +29,9 @@ public class Activity { * @param appActivity The value for the app activity. */ public Activity(String appPackage, String appActivity) { - checkArgument(!isBlank(appPackage), + checkArgument(!isNullOrEmpty(appPackage), "App package should be defined as not empty or null string"); - checkArgument(!isBlank(appActivity), + checkArgument(!isNullOrEmpty(appActivity), "App activity should be defined as not empty or null string"); this.appPackage = appPackage; this.appActivity = appActivity; 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 76810cac3..aa1d30159 100644 --- a/src/main/java/io/appium/java_client/android/AndroidDriver.java +++ b/src/main/java/io/appium/java_client/android/AndroidDriver.java @@ -16,13 +16,7 @@ package io.appium.java_client.android; -import static io.appium.java_client.android.AndroidMobileCommandHelper.endTestCoverageCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.openNotificationsCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleLocationServicesCommand; -import static org.openqa.selenium.remote.DriverCommand.EXECUTE_SCRIPT; - -import com.google.common.collect.ImmutableMap; - +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecuteCDPCommand; @@ -31,11 +25,10 @@ import io.appium.java_client.HasOnScreenKeyboard; import io.appium.java_client.HidesKeyboard; import io.appium.java_client.InteractsWithApps; -import io.appium.java_client.PullsFiles; import io.appium.java_client.LocksDevice; import io.appium.java_client.PerformsTouchActions; +import io.appium.java_client.PullsFiles; import io.appium.java_client.PushesFiles; -import io.appium.java_client.SupportsLegacyAppManagement; import io.appium.java_client.android.connection.HasNetworkConnection; import io.appium.java_client.android.geolocation.SupportsExtendedGeolocationCommands; import io.appium.java_client.android.nativekey.PressesKey; @@ -50,13 +43,10 @@ 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; import java.net.URL; -import java.util.Collections; -import java.util.Map; /** * Android driver implementation. @@ -71,7 +61,6 @@ public class AndroidDriver extends AppiumDriver implements HasDeviceTime, PullsFiles, InteractsWithApps, - SupportsLegacyAppManagement, HasAppStrings, HasNetworkConnection, PushesFiles, @@ -90,6 +79,8 @@ public class AndroidDriver extends AppiumDriver implements HasBattery, ExecuteCDPCommand, CanReplaceElementValue, + SupportsGpsStateManagement, + HasNotifications, SupportsExtendedGeolocationCommands { private static final String ANDROID_PLATFORM = Platform.ANDROID.name(); @@ -203,55 +194,66 @@ public AndroidDriver(HttpClient.Factory httpClientFactory, Capabilities capabili * */ public AndroidDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformName(capabilities, ANDROID_PLATFORM)); + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformName(capabilities, + ANDROID_PLATFORM)); } /** - * Creates a new instance based on {@code capabilities}. + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * UiAutomator2Options options = new UiAutomator2Options();
+     * AndroidDriver driver = new AndroidDriver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} * @param capabilities take a look at {@link Capabilities} + * */ - public AndroidDriver(Capabilities capabilities) { - super(ensurePlatformName(capabilities, ANDROID_PLATFORM)); + public AndroidDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * Get test-coverage data. + * Creates a new instance based on {@code capabilities}. * - * @param intent intent to broadcast. - * @param path path to .ec file. + * @param capabilities take a look at {@link Capabilities} */ - public void endTestCoverage(String intent, String path) { - CommandExecutionHelper.execute(this, endTestCoverageCommand(intent, path)); + public AndroidDriver(Capabilities capabilities) { + super(ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * Open the notification shade, on Android devices. + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! This API is supposed to be used for **debugging purposes only**. + * + * @param remoteSessionAddress The address of the **running** session including the session identifier. + * @param automationName The name of the target automation. */ - public void openNotifications() { - CommandExecutionHelper.execute(this, openNotificationsCommand()); - } - - public void toggleLocationServices() { - CommandExecutionHelper.execute(this, toggleLocationServicesCommand()); + public AndroidDriver(URL remoteSessionAddress, String automationName) { + super(remoteSessionAddress, ANDROID_PLATFORM, automationName); } - @SuppressWarnings("unchecked") @Override public AndroidBatteryInfo getBatteryInfo() { - return new AndroidBatteryInfo((Map) execute(EXECUTE_SCRIPT, ImmutableMap.of( - "script", "mobile: batteryInfo", "args", Collections.emptyList())).getValue()); - } - - @Override - public RemoteLocationContext getLocationContext() { - return locationContext; + return new AndroidBatteryInfo(CommandExecutionHelper.executeScript(this, "mobile: batteryInfo")); } @Override public synchronized StringWebSocketClient getLogcatClient() { if (logcatClient == null) { - logcatClient = new StringWebSocketClient(); + logcatClient = new StringWebSocketClient(getHttpClient()); } return logcatClient; } 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 867e96fdd..cacc04137 100644 --- a/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java +++ b/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java @@ -16,18 +16,13 @@ package io.appium.java_client.android; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.MobileCommand; - -import org.apache.commons.lang3.StringUtils; import org.openqa.selenium.remote.RemoteWebElement; -import java.util.AbstractMap; import java.util.Map; +import static java.util.Locale.ROOT; + /** * This util class helps to prepare parameters of Android-specific JSONWP * commands. @@ -39,8 +34,9 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> currentActivityCommand() { - return new AbstractMap.SimpleEntry<>(CURRENT_ACTIVITY, ImmutableMap.of()); + return Map.entry(CURRENT_ACTIVITY, Map.of()); } /** @@ -48,23 +44,9 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> currentPackageCommand() { - return new AbstractMap.SimpleEntry<>(GET_CURRENT_PACKAGE, ImmutableMap.of()); - } - - /** - * This method forms a {@link Map} of parameters for the ending of the test coverage. - * - * @param intent intent to broadcast. - * @param path path to .ec file. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - public static Map.Entry> endTestCoverageCommand(String intent, - String path) { - String[] parameters = new String[] {"intent", "path"}; - Object[] values = new Object[] {intent, path}; - return new AbstractMap.SimpleEntry<>( - END_TEST_COVERAGE, prepareArguments(parameters, values)); + return Map.entry(GET_CURRENT_PACKAGE, Map.of()); } /** @@ -75,7 +57,7 @@ public class AndroidMobileCommandHelper extends MobileCommand { * */ public static Map.Entry> getSupportedPerformanceDataTypesCommand() { - return new AbstractMap.SimpleEntry<>(GET_SUPPORTED_PERFORMANCE_DATA_TYPES, ImmutableMap.of()); + return Map.entry(GET_SUPPORTED_PERFORMANCE_DATA_TYPES, Map.of()); } /** @@ -108,51 +90,52 @@ public class AndroidMobileCommandHelper extends MobileCommand { */ public static Map.Entry> getPerformanceDataCommand( String packageName, String dataType, int dataReadTimeout) { - String[] parameters = new String[] {"packageName", "dataType", "dataReadTimeout"}; - Object[] values = new Object[] {packageName, dataType, dataReadTimeout}; - return new AbstractMap.SimpleEntry<>( - GET_PERFORMANCE_DATA, prepareArguments(parameters, values)); + return Map.entry(GET_PERFORMANCE_DATA, Map.of( + "packageName", packageName, + "dataType", dataType, + "dataReadTimeout", dataReadTimeout + )); } /** - * This method forms a {@link Map} of parameters to - * Retrieve the display density of the Android device. + * This method forms a {@link Map} of parameters to retrieve the display density of the Android device. * - * @return a key-value pair. The key is the command name. The value is a - * {@link Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> getDisplayDensityCommand() { - return new AbstractMap.SimpleEntry<>(GET_DISPLAY_DENSITY, ImmutableMap.of()); + return Map.entry(GET_DISPLAY_DENSITY, Map.of()); } /** * This method forms a {@link Map} of parameters for the getting of a network connection value. * - * @return a key-value pair. The key is the command name. The value is a - * {@link Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> getNetworkConnectionCommand() { - return new AbstractMap.SimpleEntry<>(GET_NETWORK_CONNECTION, ImmutableMap.of()); + return Map.entry(GET_NETWORK_CONNECTION, Map.of()); } /** - * This method forms a {@link Map} of parameters to - * Retrieve visibility and bounds information of the status and navigation bars. + * This method forms a {@link Map} of parameters to retrieve visibility and bounds information of the status and + * navigation bars. * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> getSystemBarsCommand() { - return new AbstractMap.SimpleEntry<>(GET_SYSTEM_BARS, ImmutableMap.of()); + return Map.entry(GET_SYSTEM_BARS, Map.of()); } /** - * This method forms a {@link java.util.Map} of parameters for the - * finger print authentication invocation. + * This method forms a {@link Map} of parameters for the finger print authentication invocation. * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> isLockedCommand() { - return new AbstractMap.SimpleEntry<>(IS_LOCKED, ImmutableMap.of()); + return Map.entry(IS_LOCKED, Map.of()); } /** @@ -161,9 +144,9 @@ public class AndroidMobileCommandHelper extends MobileCommand { * @param fingerPrintId finger prints stored in Android Keystore system (from 1 to 10) * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> fingerPrintCommand(int fingerPrintId) { - return new AbstractMap.SimpleEntry<>(FINGER_PRINT, - prepareArguments("fingerprintId", fingerPrintId)); + return Map.entry(FINGER_PRINT, Map.of("fingerprintId", fingerPrintId)); } /** @@ -171,8 +154,9 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> openNotificationsCommand() { - return new AbstractMap.SimpleEntry<>(OPEN_NOTIFICATIONS, ImmutableMap.of()); + return Map.entry(OPEN_NOTIFICATIONS, Map.of()); } /** @@ -181,58 +165,12 @@ public class AndroidMobileCommandHelper extends MobileCommand { * @param bitMask The bitmask of the desired connection * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> setConnectionCommand(long bitMask) { - String[] parameters = new String[] {"name", "parameters"}; - Object[] values = new Object[] {"network_connection", ImmutableMap.of("type", bitMask)}; - return new AbstractMap.SimpleEntry<>( - SET_NETWORK_CONNECTION, prepareArguments(parameters, values)); - } - - /** - * This method forms a {@link Map} of parameters for the activity starting. - * - * @param appPackage The package containing the activity. [Required] - * @param appActivity The activity to start. [Required] - * @param appWaitPackage Automation will begin after this package starts. [Optional] - * @param appWaitActivity Automation will begin after this activity starts. [Optional] - * @param intentAction Intent action which will be used to start activity [Optional] - * @param intentCategory Intent category which will be used to start activity [Optional] - * @param intentFlags Flags that will be used to start activity [Optional] - * @param optionalIntentArguments Additional intent arguments that will be used to - * start activity [Optional] - * @param stopApp Stop app on reset or not - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @throws IllegalArgumentException when any required argument is empty - */ - public static Map.Entry> startActivityCommand(String appPackage, - String appActivity, String appWaitPackage, String appWaitActivity, - String intentAction, String intentCategory, String intentFlags, - String optionalIntentArguments, boolean stopApp) throws IllegalArgumentException { - - checkArgument((!StringUtils.isBlank(appPackage) - && !StringUtils.isBlank(appActivity)), - String.format("'%s' and '%s' are required.", "appPackage", "appActivity")); - - String targetWaitPackage = !StringUtils.isBlank(appWaitPackage) ? appWaitPackage : ""; - String targetWaitActivity = !StringUtils.isBlank(appWaitActivity) ? appWaitActivity : ""; - String targetIntentAction = !StringUtils.isBlank(intentAction) ? intentAction : ""; - String targetIntentCategory = !StringUtils.isBlank(intentCategory) ? intentCategory : ""; - String targetIntentFlags = !StringUtils.isBlank(intentFlags) ? intentFlags : ""; - String targetOptionalIntentArguments = !StringUtils.isBlank(optionalIntentArguments) - ? optionalIntentArguments : ""; - - ImmutableMap parameters = ImmutableMap - .builder().put("appPackage", appPackage) - .put("appActivity", appActivity) - .put("appWaitPackage", targetWaitPackage) - .put("appWaitActivity", targetWaitActivity) - .put("dontStopAppOnReset", !stopApp) - .put("intentAction", targetIntentAction) - .put("intentCategory", targetIntentCategory) - .put("intentFlags", targetIntentFlags) - .put("optionalIntentArguments", targetOptionalIntentArguments) - .build(); - return new AbstractMap.SimpleEntry<>(START_ACTIVITY, parameters); + return Map.entry(SET_NETWORK_CONNECTION, Map.of( + "name", "network_connection", + "parameters", Map.of("type", bitMask) + )); } /** @@ -240,17 +178,19 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> toggleLocationServicesCommand() { - return new AbstractMap.SimpleEntry<>(TOGGLE_LOCATION_SERVICES, ImmutableMap.of()); + return Map.entry(TOGGLE_LOCATION_SERVICES, Map.of()); } /** - * This method forms a {@link java.util.Map} of parameters for the element. + * This method forms a {@link Map} of parameters for the element. * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> unlockCommand() { - return new AbstractMap.SimpleEntry<>(UNLOCK, ImmutableMap.of()); + return Map.entry(UNLOCK, Map.of()); } @@ -262,14 +202,13 @@ public class AndroidMobileCommandHelper extends MobileCommand { * @param value a new value * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ - public static Map.Entry> replaceElementValueCommand( + @Deprecated + public static Map.Entry> replaceElementValueCommand( RemoteWebElement remoteWebElement, String value) { - String[] parameters = new String[] {"id", "value"}; - Object[] values = - new Object[] {remoteWebElement.getId(), value}; - - return new AbstractMap.SimpleEntry<>( - REPLACE_VALUE, prepareArguments(parameters, values)); + return Map.entry(REPLACE_VALUE, Map.of( + "id", remoteWebElement.getId(), + "value", value + )); } /** @@ -281,14 +220,13 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> sendSMSCommand( String phoneNumber, String message) { - ImmutableMap parameters = ImmutableMap - .builder().put("phoneNumber", phoneNumber) - .put("message", message) - .build(); - - return new AbstractMap.SimpleEntry<>(SEND_SMS, parameters); + return Map.entry(SEND_SMS, Map.of( + "phoneNumber", phoneNumber, + "message", message + )); } /** @@ -300,11 +238,13 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> gsmCallCommand( String phoneNumber, GsmCallActions gsmCallActions) { - String[] parameters = new String[] {"phoneNumber", "action"}; - Object[] values = new Object[]{phoneNumber, gsmCallActions.name().toLowerCase()}; - return new AbstractMap.SimpleEntry<>(GSM_CALL, prepareArguments(parameters, values)); + return Map.entry(GSM_CALL, Map.of( + "phoneNumber", phoneNumber, + "action", gsmCallActions.name().toLowerCase(ROOT) + )); } /** @@ -315,13 +255,14 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> gsmSignalStrengthCommand( GsmSignalStrength gsmSignalStrength) { - return new AbstractMap.SimpleEntry<>(GSM_SIGNAL, - prepareArguments( + return Map.entry(GSM_SIGNAL, + Map.of( // https://github.com/appium/appium/issues/12234 - new String[] {"signalStrengh", "signalStrength" }, - new Object[] {gsmSignalStrength.ordinal(), gsmSignalStrength.ordinal()} + "signalStrengh", gsmSignalStrength.ordinal(), + "signalStrength", gsmSignalStrength.ordinal() )); } @@ -333,10 +274,10 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> gsmVoiceCommand( GsmVoiceState gsmVoiceState) { - return new AbstractMap.SimpleEntry<>(GSM_VOICE, - prepareArguments("state", gsmVoiceState.name().toLowerCase())); + return Map.entry(GSM_VOICE, Map.of("state", gsmVoiceState.name().toLowerCase(ROOT))); } /** @@ -347,10 +288,10 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> networkSpeedCommand( NetworkSpeed networkSpeed) { - return new AbstractMap.SimpleEntry<>(NETWORK_SPEED, - prepareArguments("netspeed", networkSpeed.name().toLowerCase())); + return Map.entry(NETWORK_SPEED, Map.of("netspeed", networkSpeed.name().toLowerCase(ROOT))); } /** @@ -361,10 +302,10 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> powerCapacityCommand( int percent) { - return new AbstractMap.SimpleEntry<>(POWER_CAPACITY, - prepareArguments("percent", percent)); + return Map.entry(POWER_CAPACITY, Map.of("percent", percent)); } /** @@ -375,10 +316,10 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> powerACCommand( PowerACState powerACState) { - return new AbstractMap.SimpleEntry<>(POWER_AC_STATE, - prepareArguments("state", powerACState.name().toLowerCase())); + return Map.entry(POWER_AC_STATE, Map.of("state", powerACState.name().toLowerCase(ROOT))); } /** @@ -386,8 +327,9 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> toggleWifiCommand() { - return new AbstractMap.SimpleEntry<>(TOGGLE_WIFI, ImmutableMap.of()); + return Map.entry(TOGGLE_WIFI, Map.of()); } /** @@ -395,8 +337,9 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> toggleAirplaneCommand() { - return new AbstractMap.SimpleEntry<>(TOGGLE_AIRPLANE_MODE, ImmutableMap.of()); + return Map.entry(TOGGLE_AIRPLANE_MODE, Map.of()); } /** @@ -404,7 +347,8 @@ public class AndroidMobileCommandHelper extends MobileCommand { * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ + @Deprecated public static Map.Entry> toggleDataCommand() { - return new AbstractMap.SimpleEntry<>(TOGGLE_DATA, ImmutableMap.of()); + return Map.entry(TOGGLE_DATA, Map.of()); } } diff --git a/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java index c8378a86b..dd6ee2b2f 100644 --- a/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java +++ b/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java @@ -16,16 +16,16 @@ package io.appium.java_client.android; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; import io.appium.java_client.screenrecording.ScreenRecordingUploadOptions; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static java.util.Optional.ofNullable; + public class AndroidStartScreenRecordingOptions extends BaseStartScreenRecordingOptions { private Integer bitRate; @@ -106,11 +106,10 @@ public AndroidStartScreenRecordingOptions withTimeLimit(Duration timeLimit) { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(bitRate).map(x -> builder.put("bitRate", x)); - ofNullable(videoSize).map(x -> builder.put("videoSize", x)); - ofNullable(isBugReportEnabled).map(x -> builder.put("bugReport", x)); - return builder.build(); + var map = new HashMap<>(super.build()); + ofNullable(bitRate).ifPresent(x -> map.put("bitRate", x)); + ofNullable(videoSize).ifPresent(x -> map.put("videoSize", x)); + ofNullable(isBugReportEnabled).ifPresent(x -> map.put("bugReport", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java b/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java index 717d5150d..611fb30ed 100644 --- a/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java +++ b/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java @@ -1,11 +1,15 @@ package io.appium.java_client.android; -import static io.appium.java_client.android.AndroidMobileCommandHelper.fingerPrintCommand; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import java.util.Map; + +import static io.appium.java_client.android.AndroidMobileCommandHelper.fingerPrintCommand; -public interface AuthenticatesByFinger extends ExecutesMethod { +public interface AuthenticatesByFinger extends ExecutesMethod, CanRememberExtensionPresence { /** * Authenticate users by using their finger print scans on supported emulators. @@ -13,6 +17,14 @@ public interface AuthenticatesByFinger extends ExecutesMethod { * @param fingerPrintId finger prints stored in Android Keystore system (from 1 to 10) */ default void fingerPrint(int fingerPrintId) { - CommandExecutionHelper.execute(this, fingerPrintCommand(fingerPrintId)); + final String extName = "mobile: fingerprint"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "fingerprintId", fingerPrintId + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), fingerPrintCommand(fingerPrintId)); + } } } diff --git a/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java b/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java index 72a170f32..3c42f5c35 100644 --- a/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java +++ b/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java @@ -1,21 +1,42 @@ package io.appium.java_client.android; -import com.google.common.collect.ImmutableMap; +import io.appium.java_client.CanRememberExtensionPresence; +import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; import io.appium.java_client.MobileCommand; +import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.remote.RemoteWebElement; -public interface CanReplaceElementValue extends ExecutesMethod { +import java.util.Map; + +public interface CanReplaceElementValue extends ExecutesMethod, CanRememberExtensionPresence { /** - * Replaces element value with the given one. + * Sends a text to the given element by replacing its previous content. * * @param element The destination element. - * @param value The value to set. + * @param value The text to enter. It could also contain Unicode characters. + * If the text ends with `\\n` (the backslash must be escaped, so the + * char is NOT translated into `0x0A`) then the Enter key press is going to + * be emulated after it is entered (the `\\n` substring itself will be cut + * off from the typed text). */ default void replaceElementValue(RemoteWebElement element, String value) { - this.execute(MobileCommand.REPLACE_VALUE, ImmutableMap.of( - "id", element.getId(), - "value", value - )); + final String extName = "mobile: replaceElementValue"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "elementId", element.getId(), + "text", value + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(MobileCommand.REPLACE_VALUE, Map.of( + "id", element.getId(), + "text", value, + "value", value + )) + ); + } } } 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 063f689cc..8b7018e76 100644 --- a/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java +++ b/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java @@ -16,17 +16,17 @@ package io.appium.java_client.android; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.MobileCommand.SET_CLIPBOARD; -import static io.appium.java_client.MobileCommand.prepareArguments; - import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.clipboard.ClipboardContentType; import io.appium.java_client.clipboard.HasClipboard; import java.nio.charset.StandardCharsets; -import java.util.AbstractMap; import java.util.Base64; +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 { /** @@ -37,11 +37,13 @@ public interface HasAndroidClipboard extends HasClipboard { * @param base64Content base64-encoded content to be set. */ default void setClipboard(String label, ClipboardContentType contentType, byte[] base64Content) { - String[] parameters = new String[]{"content", "contentType", "label"}; - Object[] values = new Object[]{new String(checkNotNull(base64Content), StandardCharsets.UTF_8), - contentType.name().toLowerCase(), checkNotNull(label)}; - CommandExecutionHelper.execute(this, new AbstractMap.SimpleEntry<>(SET_CLIPBOARD, - prepareArguments(parameters, values))); + CommandExecutionHelper.execute(this, Map.entry(SET_CLIPBOARD, + Map.of( + "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), + "contentType", contentType.name().toLowerCase(ROOT), + "label", requireNonNull(label) + ) + )); } /** diff --git a/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java b/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java index 7984a0b43..3e230b3a3 100644 --- a/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java +++ b/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java @@ -1,14 +1,16 @@ package io.appium.java_client.android; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getDisplayDensityCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getSystemBarsCommand; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; import java.util.Map; -public interface HasAndroidDeviceDetails extends ExecutesMethod { +import static io.appium.java_client.android.AndroidMobileCommandHelper.getDisplayDensityCommand; +import static io.appium.java_client.android.AndroidMobileCommandHelper.getSystemBarsCommand; + +public interface HasAndroidDeviceDetails extends ExecutesMethod, CanRememberExtensionPresence { /** Retrieve the display density of the Android device. @@ -16,7 +18,13 @@ public interface HasAndroidDeviceDetails extends ExecutesMethod { @return The density value in dpi */ default Long getDisplayDensity() { - return CommandExecutionHelper.execute(this, getDisplayDensityCommand()); + final String extName = "mobile: getDisplayDensity"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute(markExtensionAbsence(extName), getDisplayDensityCommand()); + } } /** @@ -25,7 +33,13 @@ default Long getDisplayDensity() { @return The map where keys are bar types and values are mappings of bar properties. */ default Map> getSystemBars() { - return CommandExecutionHelper.execute(this, getSystemBarsCommand()); + final String extName = "mobile: getSystemBars"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute(markExtensionAbsence(extName), getSystemBarsCommand()); + } } } diff --git a/src/main/java/io/appium/java_client/android/HasNotifications.java b/src/main/java/io/appium/java_client/android/HasNotifications.java new file mode 100644 index 000000000..0b7f7365b --- /dev/null +++ b/src/main/java/io/appium/java_client/android/HasNotifications.java @@ -0,0 +1,24 @@ +package io.appium.java_client.android; + +import io.appium.java_client.CanRememberExtensionPresence; +import io.appium.java_client.CommandExecutionHelper; +import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import static io.appium.java_client.android.AndroidMobileCommandHelper.openNotificationsCommand; + +public interface HasNotifications extends ExecutesMethod, CanRememberExtensionPresence { + + /** + * Opens notification drawer on the device under test. + */ + default void openNotifications() { + final String extName = "mobile: openNotifications"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), openNotificationsCommand()); + } + } +} diff --git a/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java b/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java index f64f87332..9a175d14c 100644 --- a/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java +++ b/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java @@ -1,14 +1,17 @@ package io.appium.java_client.android; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getPerformanceDataCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getSupportedPerformanceDataTypesCommand; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; import java.util.List; +import java.util.Map; + +import static io.appium.java_client.android.AndroidMobileCommandHelper.getPerformanceDataCommand; +import static io.appium.java_client.android.AndroidMobileCommandHelper.getSupportedPerformanceDataTypesCommand; -public interface HasSupportedPerformanceDataType extends ExecutesMethod { +public interface HasSupportedPerformanceDataType extends ExecutesMethod, CanRememberExtensionPresence { /** * returns the information type of the system state which is supported to read @@ -18,7 +21,15 @@ public interface HasSupportedPerformanceDataType extends ExecutesMethod { * */ default List getSupportedPerformanceDataTypes() { - return CommandExecutionHelper.execute(this, getSupportedPerformanceDataTypesCommand()); + final String extName = "mobile: getPerformanceDataTypes"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute( + markExtensionAbsence(extName), getSupportedPerformanceDataTypesCommand() + ); + } } /** @@ -50,7 +61,17 @@ default List getSupportedPerformanceDataTypes() { * in case of cpu info : [[user, kernel], [0.9, 1.3]] */ default List> getPerformanceData(String packageName, String dataType, int dataReadTimeout) { - return CommandExecutionHelper.execute(this, - getPerformanceDataCommand(packageName, dataType, dataReadTimeout)); + final String extName = "mobile: getPerformanceData"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "packageName", packageName, + "dataType", dataType + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute( + markExtensionAbsence(extName), getPerformanceDataCommand(packageName, dataType, dataReadTimeout) + ); + } } } diff --git a/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java b/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java index 4fffed837..d5051da0e 100644 --- a/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java +++ b/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java @@ -16,20 +16,19 @@ package io.appium.java_client.android; -import static io.appium.java_client.service.local.AppiumServiceBuilder.DEFAULT_APPIUM_PORT; -import static org.openqa.selenium.remote.DriverCommand.EXECUTE_SCRIPT; - -import com.google.common.collect.ImmutableMap; - +import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; import io.appium.java_client.ws.StringWebSocketClient; +import org.openqa.selenium.remote.HttpCommandExecutor; import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.SessionId; import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; +import java.net.URL; import java.util.function.Consumer; +import static io.appium.java_client.service.local.AppiumServiceBuilder.DEFAULT_APPIUM_PORT; + public interface ListensToLogcatMessages extends ExecutesMethod { StringWebSocketClient getLogcatClient(); @@ -39,7 +38,7 @@ public interface ListensToLogcatMessages extends ExecutesMethod { * is assigned to the default port (4723). */ default void startLogcatBroadcast() { - startLogcatBroadcast("localhost", DEFAULT_APPIUM_PORT); + startLogcatBroadcast("127.0.0.1"); } /** @@ -59,16 +58,13 @@ default void startLogcatBroadcast(String host) { * @param port the port of the host where Appium server is running */ default void startLogcatBroadcast(String host, int port) { - execute(EXECUTE_SCRIPT, ImmutableMap.of("script", "mobile: startLogsBroadcast", - "args", Collections.emptyList())); - final URI endpointUri; - try { - endpointUri = new URI(String.format("ws://%s:%s/ws/session/%s/appium/device/logcat", - host, port, ((RemoteWebDriver) this).getSessionId())); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - getLogcatClient().connect(endpointUri); + var remoteWebDriver = (RemoteWebDriver) this; + URL serverUrl = ((HttpCommandExecutor) remoteWebDriver.getCommandExecutor()).getAddressOfRemoteServer(); + var scheme = "https".equals(serverUrl.getProtocol()) ? "wss" : "ws"; + CommandExecutionHelper.executeScript(this, "mobile: startLogsBroadcast"); + SessionId sessionId = remoteWebDriver.getSessionId(); + var endpoint = String.format("%s://%s:%s/ws/session/%s/appium/device/logcat", scheme, host, port, sessionId); + getLogcatClient().connect(URI.create(endpoint)); } /** @@ -133,7 +129,6 @@ default void removeAllLogcatListeners() { */ default void stopLogcatBroadcast() { removeAllLogcatListeners(); - execute(EXECUTE_SCRIPT, ImmutableMap.of("script", "mobile: stopLogsBroadcast", - "args", Collections.emptyList())); + CommandExecutionHelper.executeScript(this, "mobile: stopLogsBroadcast"); } } 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 bd21d9d86..23b0ad7a9 100644 --- a/src/main/java/io/appium/java_client/android/StartsActivity.java +++ b/src/main/java/io/appium/java_client/android/StartsActivity.java @@ -16,46 +16,35 @@ package io.appium.java_client.android; -import static io.appium.java_client.android.AndroidMobileCommandHelper.currentActivityCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.currentPackageCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.startActivityCommand; - +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; -public interface StartsActivity extends ExecutesMethod { - /** - * This method should start arbitrary activity during a test. If the activity belongs to - * another application, that application is started and the activity is opened. - *

- * Usage: - *

- *
-     *     {@code
-     *     Activity activity = new Activity("app package goes here", "app activity goes here");
-     *     activity.setWaitAppPackage("app wait package goes here");
-     *     activity.setWaitAppActivity("app wait activity goes here");
-     *     driver.startActivity(activity);
-     *     }
-     * 
- * - * @param activity The {@link Activity} object - */ - default void startActivity(Activity activity) { - CommandExecutionHelper.execute(this, - startActivityCommand(activity.getAppPackage(), activity.getAppActivity(), - activity.getAppWaitPackage(), activity.getAppWaitActivity(), - activity.getIntentAction(), activity.getIntentCategory(), activity.getIntentFlags(), - activity.getOptionalIntentArguments(), activity.isStopApp())); - } +import java.util.Map; + +import static io.appium.java_client.MobileCommand.CURRENT_ACTIVITY; +import static io.appium.java_client.MobileCommand.GET_CURRENT_PACKAGE; +public interface StartsActivity extends ExecutesMethod, CanRememberExtensionPresence { /** * Get the current activity being run on the mobile device. * * @return a current activity being run on the mobile device. */ + @Nullable default String currentActivity() { - return CommandExecutionHelper.execute(this, currentActivityCommand()); + final String extName = "mobile: getCurrentActivity"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(CURRENT_ACTIVITY, Map.of()) + ); + } } /** @@ -63,7 +52,17 @@ default String currentActivity() { * * @return a current package being run on the mobile device. */ + @Nullable default String getCurrentPackage() { - return CommandExecutionHelper.execute(this, currentPackageCommand()); + final String extName = "mobile: getCurrentPackage"; + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(GET_CURRENT_PACKAGE, Map.of()) + ); + } } } diff --git a/src/main/java/io/appium/java_client/android/SupportsGpsStateManagement.java b/src/main/java/io/appium/java_client/android/SupportsGpsStateManagement.java new file mode 100644 index 000000000..5c16bb293 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/SupportsGpsStateManagement.java @@ -0,0 +1,37 @@ +package io.appium.java_client.android; + +import io.appium.java_client.CanRememberExtensionPresence; +import io.appium.java_client.CommandExecutionHelper; +import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleLocationServicesCommand; +import static java.util.Objects.requireNonNull; + +public interface SupportsGpsStateManagement extends ExecutesMethod, CanRememberExtensionPresence { + + /** + * Toggles GPS service state. + * This method only works reliably since API 31 (Android 12). + */ + default void toggleLocationServices() { + final String extName = "mobile: toggleGps"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleLocationServicesCommand()); + } + } + + /** + * Check GPS service state. + * + * @return true if GPS service is enabled. + */ + default boolean isLocationServicesEnabled() { + return requireNonNull( + CommandExecutionHelper.executeScript(this, "mobile: isGpsEnabled") + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java b/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java index df63b6e1a..2992e5847 100644 --- a/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java +++ b/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java @@ -1,33 +1,72 @@ package io.appium.java_client.android; +import io.appium.java_client.CanRememberExtensionPresence; +import io.appium.java_client.CommandExecutionHelper; +import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import java.util.Map; + import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleAirplaneCommand; import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleDataCommand; import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleWifiCommand; +import static java.util.Objects.requireNonNull; -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; - -public interface SupportsNetworkStateManagement extends ExecutesMethod { +public interface SupportsNetworkStateManagement extends ExecutesMethod, CanRememberExtensionPresence { /** * Toggles Wifi on and off. */ default void toggleWifi() { - CommandExecutionHelper.execute(this, toggleWifiCommand()); + final String extName = "mobile: setConnectivity"; + try { + Map result = requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: getConnectivity") + ); + CommandExecutionHelper.executeScript(this, extName, Map.of( + "wifi", !((Boolean) result.get("wifi")) + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleWifiCommand()); + } } /** - * Toggle Airplane mode and this works on OS 6.0 and lesser - * and does not work on OS 7.0 and greater + * Toggle Airplane mode and this works on Android versions below + * 6 and above 10. */ default void toggleAirplaneMode() { - CommandExecutionHelper.execute(this, toggleAirplaneCommand()); + final String extName = "mobile: setConnectivity"; + try { + Map result = requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: getConnectivity") + ); + CommandExecutionHelper.executeScript(this, extName, Map.of( + "airplaneMode", !((Boolean) result.get("airplaneMode")) + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleAirplaneCommand()); + } } /** - * Toggle Mobile Data and this works on Emulator and rooted device. + * Toggle Mobile Data and this works on Emulators and real devices + * running Android version above 10. */ default void toggleData() { - CommandExecutionHelper.execute(this, toggleDataCommand()); + final String extName = "mobile: setConnectivity"; + try { + Map result = requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: getConnectivity") + ); + CommandExecutionHelper.executeScript(this, extName, Map.of( + "data", !((Boolean) result.get("data")) + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleDataCommand()); + } } } 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 9da6dfaec..bb618b8cd 100644 --- a/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java +++ b/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java @@ -1,17 +1,22 @@ package io.appium.java_client.android; -import static io.appium.java_client.android.AndroidMobileCommandHelper.gsmCallCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.gsmSignalStrengthCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.gsmVoiceCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.networkSpeedCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.powerACCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.powerCapacityCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.sendSMSCommand; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import java.util.Map; + +import static io.appium.java_client.MobileCommand.GSM_CALL; +import static io.appium.java_client.MobileCommand.GSM_SIGNAL; +import static io.appium.java_client.MobileCommand.GSM_VOICE; +import static io.appium.java_client.MobileCommand.NETWORK_SPEED; +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 { +public interface SupportsSpecialEmulatorCommands extends ExecutesMethod, CanRememberExtensionPresence { /** * Emulate send SMS event on the connected emulator. @@ -20,17 +25,47 @@ public interface SupportsSpecialEmulatorCommands extends ExecutesMethod { * @param message The message content. */ default void sendSMS(String phoneNumber, String message) { - CommandExecutionHelper.execute(this, sendSMSCommand(phoneNumber, message)); + final String extName = "mobile: sendSms"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "phoneNumber", phoneNumber, + "message", message + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(SEND_SMS, Map.of( + "phoneNumber", phoneNumber, + "message", message + )) + ); + } } /** * Emulate GSM call event on the connected emulator. * * @param phoneNumber The phone number of the caller. - * @param gsmCallActions One of available {@link GsmCallActions} values. + * @param gsmCallAction One of available {@link GsmCallActions} values. */ - default void makeGsmCall(String phoneNumber, GsmCallActions gsmCallActions) { - CommandExecutionHelper.execute(this, gsmCallCommand(phoneNumber, gsmCallActions)); + default void makeGsmCall(String phoneNumber, GsmCallActions gsmCallAction) { + final String extName = "mobile: gsmCall"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "phoneNumber", phoneNumber, + "action", gsmCallAction.toString().toLowerCase(ROOT) + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(GSM_CALL, Map.of( + "phoneNumber", phoneNumber, + "action", gsmCallAction.toString().toLowerCase(ROOT) + )) + ); + } } /** @@ -39,7 +74,21 @@ default void makeGsmCall(String phoneNumber, GsmCallActions gsmCallActions) { * @param gsmSignalStrength One of available {@link GsmSignalStrength} values. */ default void setGsmSignalStrength(GsmSignalStrength gsmSignalStrength) { - CommandExecutionHelper.execute(this, gsmSignalStrengthCommand(gsmSignalStrength)); + final String extName = "mobile: gsmSignal"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "strength", gsmSignalStrength.ordinal() + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(GSM_SIGNAL, Map.of( + "signalStrengh", gsmSignalStrength.ordinal(), + "signalStrength", gsmSignalStrength.ordinal() + )) + ); + } } /** @@ -48,7 +97,20 @@ default void setGsmSignalStrength(GsmSignalStrength gsmSignalStrength) { * @param gsmVoiceState One of available {@link GsmVoiceState} values. */ default void setGsmVoice(GsmVoiceState gsmVoiceState) { - CommandExecutionHelper.execute(this, gsmVoiceCommand(gsmVoiceState)); + final String extName = "mobile: gsmVoice"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "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(ROOT) + )) + ); + } } /** @@ -57,7 +119,20 @@ default void setGsmVoice(GsmVoiceState gsmVoiceState) { * @param networkSpeed One of available {@link NetworkSpeed} values. */ default void setNetworkSpeed(NetworkSpeed networkSpeed) { - CommandExecutionHelper.execute(this, networkSpeedCommand(networkSpeed)); + final String extName = "mobile: networkSpeed"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "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(ROOT) + )) + ); + } } /** @@ -66,7 +141,20 @@ default void setNetworkSpeed(NetworkSpeed networkSpeed) { * @param percent Percentage value in range [0, 100]. */ default void setPowerCapacity(int percent) { - CommandExecutionHelper.execute(this, powerCapacityCommand(percent)); + final String extName = "mobile: powerCapacity"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "percent", percent + )); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(POWER_CAPACITY, Map.of( + "percent", percent + )) + ); + } } /** @@ -75,7 +163,20 @@ default void setPowerCapacity(int percent) { * @param powerACState One of available {@link PowerACState} values. */ default void setPowerAC(PowerACState powerACState) { - CommandExecutionHelper.execute(this, powerACCommand(powerACState)); + final String extName = "mobile: powerAC"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "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(ROOT) + )) + ); + } } } diff --git a/src/main/java/io/appium/java_client/android/appmanagement/AndroidInstallApplicationOptions.java b/src/main/java/io/appium/java_client/android/appmanagement/AndroidInstallApplicationOptions.java index a6eb8e088..216641b84 100644 --- a/src/main/java/io/appium/java_client/android/appmanagement/AndroidInstallApplicationOptions.java +++ b/src/main/java/io/appium/java_client/android/appmanagement/AndroidInstallApplicationOptions.java @@ -16,17 +16,17 @@ package io.appium.java_client.android.appmanagement; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.appmanagement.BaseInstallApplicationOptions; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + public class AndroidInstallApplicationOptions extends BaseInstallApplicationOptions { private Boolean replace; @@ -65,7 +65,7 @@ public AndroidInstallApplicationOptions withReplaceDisabled() { * @return self instance for chaining. */ public AndroidInstallApplicationOptions withTimeout(Duration timeout) { - checkArgument(!checkNotNull(timeout).isNegative(), "The timeout value cannot be negative"); + checkArgument(!requireNonNull(timeout).isNegative(), "The timeout value cannot be negative"); this.timeout = timeout; return this; } @@ -139,12 +139,12 @@ public AndroidInstallApplicationOptions withGrantPermissionsDisabled() { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - ofNullable(replace).map(x -> builder.put("replace", x)); - ofNullable(timeout).map(x -> builder.put("timeout", x.toMillis())); - ofNullable(allowTestPackages).map(x -> builder.put("allowTestPackages", x)); - ofNullable(useSdcard).map(x -> builder.put("useSdcard", x)); - ofNullable(grantPermissions).map(x -> builder.put("grantPermissions", x)); - return builder.build(); + var map = new HashMap(); + ofNullable(replace).ifPresent(x -> map.put("replace", x)); + ofNullable(timeout).ifPresent(x -> map.put("timeout", x.toMillis())); + ofNullable(allowTestPackages).ifPresent(x -> map.put("allowTestPackages", x)); + ofNullable(useSdcard).ifPresent(x -> map.put("useSdcard", x)); + ofNullable(grantPermissions).ifPresent(x -> map.put("grantPermissions", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/android/appmanagement/AndroidRemoveApplicationOptions.java b/src/main/java/io/appium/java_client/android/appmanagement/AndroidRemoveApplicationOptions.java index c09253c51..fe68a0073 100644 --- a/src/main/java/io/appium/java_client/android/appmanagement/AndroidRemoveApplicationOptions.java +++ b/src/main/java/io/appium/java_client/android/appmanagement/AndroidRemoveApplicationOptions.java @@ -16,17 +16,17 @@ package io.appium.java_client.android.appmanagement; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.appmanagement.BaseRemoveApplicationOptions; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + public class AndroidRemoveApplicationOptions extends BaseRemoveApplicationOptions { private Duration timeout; @@ -40,7 +40,7 @@ public class AndroidRemoveApplicationOptions extends * @return self instance for chaining. */ public AndroidRemoveApplicationOptions withTimeout(Duration timeout) { - checkArgument(!checkNotNull(timeout).isNegative(), + checkArgument(!requireNonNull(timeout).isNegative(), "The timeout value cannot be negative"); this.timeout = timeout; return this; @@ -69,9 +69,9 @@ public AndroidRemoveApplicationOptions withKeepDataDisabled() { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - ofNullable(timeout).map(x -> builder.put("timeout", x.toMillis())); - ofNullable(keepData).map(x -> builder.put("keepData", x)); - return builder.build(); + var map = new HashMap(); + ofNullable(timeout).ifPresent(x -> map.put("timeout", x.toMillis())); + ofNullable(keepData).ifPresent(x -> map.put("keepData", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/android/appmanagement/AndroidTerminateApplicationOptions.java b/src/main/java/io/appium/java_client/android/appmanagement/AndroidTerminateApplicationOptions.java index 6ad6c83f5..b683c5a8f 100644 --- a/src/main/java/io/appium/java_client/android/appmanagement/AndroidTerminateApplicationOptions.java +++ b/src/main/java/io/appium/java_client/android/appmanagement/AndroidTerminateApplicationOptions.java @@ -16,17 +16,17 @@ package io.appium.java_client.android.appmanagement; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.appmanagement.BaseTerminateApplicationOptions; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + public class AndroidTerminateApplicationOptions extends BaseTerminateApplicationOptions { private Duration timeout; @@ -39,15 +39,15 @@ public class AndroidTerminateApplicationOptions extends * @return self instance for chaining. */ public AndroidTerminateApplicationOptions withTimeout(Duration timeout) { - checkArgument(!checkNotNull(timeout).isNegative(), "The timeout value cannot be negative"); + checkArgument(!requireNonNull(timeout).isNegative(), "The timeout value cannot be negative"); this.timeout = timeout; return this; } @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - ofNullable(timeout).map(x -> builder.put("timeout", x.toMillis())); - return builder.build(); + var map = new HashMap(); + ofNullable(timeout).ifPresent(x -> map.put("timeout", x.toMillis())); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java b/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java index c17d450a1..a00693af3 100644 --- a/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java +++ b/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java @@ -16,13 +16,18 @@ package io.appium.java_client.android.connection; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getNetworkConnectionCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.setConnectionCommand; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import java.util.Map; + +import static io.appium.java_client.android.AndroidMobileCommandHelper.getNetworkConnectionCommand; +import static io.appium.java_client.android.AndroidMobileCommandHelper.setConnectionCommand; +import static java.util.Objects.requireNonNull; -public interface HasNetworkConnection extends ExecutesMethod { +public interface HasNetworkConnection extends ExecutesMethod, CanRememberExtensionPresence { /** * Set the network connection of the device. @@ -31,8 +36,25 @@ public interface HasNetworkConnection extends ExecutesMethod { * @return Connection object, which represents the resulting state */ default ConnectionState setConnection(ConnectionState connection) { - return new ConnectionState(CommandExecutionHelper.execute(this, - setConnectionCommand(connection.getBitMask()))); + final String extName = "mobile: setConnectivity"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( + "wifi", connection.isWiFiEnabled(), + "data", connection.isDataEnabled(), + "airplaneMode", connection.isAirplaneModeEnabled() + )); + return getConnection(); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return new ConnectionState( + requireNonNull( + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + setConnectionCommand(connection.getBitMask()) + ) + ) + ); + } } /** @@ -41,6 +63,26 @@ default ConnectionState setConnection(ConnectionState connection) { * @return Connection object, which lets you to inspect the current status */ default ConnectionState getConnection() { - return new ConnectionState(CommandExecutionHelper.execute(this, getNetworkConnectionCommand())); + final String extName = "mobile: getConnectivity"; + try { + Map result = requireNonNull( + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName) + ); + return new ConnectionState( + ((boolean) result.get("wifi") ? ConnectionState.WIFI_MASK : 0) + | ((boolean) result.get("data") ? ConnectionState.DATA_MASK : 0) + | ((boolean) result.get("airplaneMode") ? ConnectionState.AIRPLANE_MODE_MASK : 0) + ); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return new ConnectionState( + requireNonNull( + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + getNetworkConnectionCommand() + ) + ) + ); + } } } diff --git a/src/main/java/io/appium/java_client/android/geolocation/AndroidGeoLocation.java b/src/main/java/io/appium/java_client/android/geolocation/AndroidGeoLocation.java index f04a41fe2..37d878642 100644 --- a/src/main/java/io/appium/java_client/android/geolocation/AndroidGeoLocation.java +++ b/src/main/java/io/appium/java_client/android/geolocation/AndroidGeoLocation.java @@ -16,8 +16,8 @@ package io.appium.java_client.android.geolocation; -import com.google.common.collect.ImmutableMap; - +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import static java.util.Optional.ofNullable; @@ -110,16 +110,16 @@ public AndroidGeoLocation withSpeed(double speed) { * @return Parameters mapping */ public Map build() { - ImmutableMap.Builder builder = ImmutableMap.builder(); - ofNullable(longitude).map(x -> builder.put("longitude", x)) - .orElseThrow(() -> new IllegalArgumentException( - "A valid 'longitude' must be provided")); - ofNullable(latitude).map(x -> builder.put("latitude", x)) - .orElseThrow(() -> new IllegalArgumentException( - "A valid 'latitude' must be provided")); - ofNullable(altitude).map(x -> builder.put("altitude", x)); - ofNullable(satellites).map(x -> builder.put("satellites", x)); - ofNullable(speed).map(x -> builder.put("speed", x)); - return builder.build(); + var map = new HashMap(); + ofNullable(longitude).ifPresentOrElse(x -> map.put("longitude", x), () -> { + throw new IllegalArgumentException("A valid 'longitude' must be provided"); + }); + ofNullable(latitude).ifPresentOrElse(x -> map.put("latitude", x), () -> { + throw new IllegalArgumentException("A valid 'latitude' must be provided"); + }); + ofNullable(altitude).ifPresent(x -> map.put("altitude", x)); + ofNullable(satellites).ifPresent(x -> map.put("satellites", x)); + ofNullable(speed).ifPresent(x -> map.put("speed", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.java b/src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.java index 3587ad07e..0472a5bab 100644 --- a/src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.java +++ b/src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.java @@ -16,12 +16,11 @@ package io.appium.java_client.android.geolocation; -import com.google.common.collect.ImmutableMap; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.remote.DriverCommand; +import io.appium.java_client.MobileCommand; -import java.util.AbstractMap; +import java.util.Map; public interface SupportsExtendedGeolocationCommands extends ExecutesMethod { @@ -32,9 +31,8 @@ public interface SupportsExtendedGeolocationCommands extends ExecutesMethod { * @param location The location object to set. */ default void setLocation(AndroidGeoLocation location) { - ImmutableMap parameters = ImmutableMap - .of("location", location.build()); - CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(DriverCommand.SET_LOCATION, parameters)); + CommandExecutionHelper.execute(this, Map.entry(MobileCommand.SET_LOCATION, + Map.of("location", location.build()) + )); } } 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/KeyEvent.java b/src/main/java/io/appium/java_client/android/nativekey/KeyEvent.java index a8538b629..984c34cbf 100644 --- a/src/main/java/io/appium/java_client/android/nativekey/KeyEvent.java +++ b/src/main/java/io/appium/java_client/android/nativekey/KeyEvent.java @@ -16,12 +16,12 @@ package io.appium.java_client.android.nativekey; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static java.util.Optional.ofNullable; + public class KeyEvent { private Integer keyCode; private Integer metaState; @@ -82,13 +82,13 @@ public KeyEvent withFlag(KeyEventFlag keyEventFlag) { * @throws IllegalStateException if key code is not set */ public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - final int keyCode = ofNullable(this.keyCode) - .orElseThrow(() -> new IllegalStateException("The key code must be set")); - builder.put("keycode", keyCode); - ofNullable(this.metaState).ifPresent(x -> builder.put("metastate", x)); - ofNullable(this.flags).ifPresent(x -> builder.put("flags", x)); - return builder.build(); + var map = new HashMap(); + ofNullable(this.keyCode).ifPresentOrElse(x -> map.put("keycode", x), () -> { + throw new IllegalStateException("The key code must be set"); + }); + ofNullable(this.metaState).ifPresent(x -> map.put("metastate", x)); + ofNullable(this.flags).ifPresent(x -> map.put("flags", x)); + return Collections.unmodifiableMap(map); } } 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/nativekey/PressesKey.java b/src/main/java/io/appium/java_client/android/nativekey/PressesKey.java index b4c39767c..af633a1d9 100644 --- a/src/main/java/io/appium/java_client/android/nativekey/PressesKey.java +++ b/src/main/java/io/appium/java_client/android/nativekey/PressesKey.java @@ -16,15 +16,18 @@ package io.appium.java_client.android.nativekey; -import static io.appium.java_client.MobileCommand.LONG_PRESS_KEY_CODE; -import static io.appium.java_client.MobileCommand.PRESS_KEY_CODE; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; -import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.LONG_PRESS_KEY_CODE; +import static io.appium.java_client.MobileCommand.PRESS_KEY_CODE; -public interface PressesKey extends ExecutesMethod { +public interface PressesKey extends ExecutesMethod, CanRememberExtensionPresence { /** * Send a key event to the device under test. @@ -32,8 +35,16 @@ public interface PressesKey extends ExecutesMethod { * @param keyEvent The generated native key event */ default void pressKey(KeyEvent keyEvent) { - CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(PRESS_KEY_CODE, keyEvent.build())); + final String extName = "mobile: pressKey"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, keyEvent.build()); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(PRESS_KEY_CODE, keyEvent.build()) + ); + } } /** @@ -42,7 +53,17 @@ default void pressKey(KeyEvent keyEvent) { * @param keyEvent The generated native key event */ default void longPressKey(KeyEvent keyEvent) { - CommandExecutionHelper.execute(this, - new AbstractMap.SimpleEntry<>(LONG_PRESS_KEY_CODE, keyEvent.build())); + final String extName = "mobile: pressKey"; + try { + var args = new HashMap<>(keyEvent.build()); + args.put("isLongPress", true); + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute( + markExtensionAbsence(extName), + Map.entry(LONG_PRESS_KEY_CODE, keyEvent.build()) + ); + } } } 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 0baebf02f..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 @@ -32,13 +32,12 @@ import io.appium.java_client.android.options.app.SupportsAllowTestPackagesOption; import io.appium.java_client.android.options.app.SupportsAndroidInstallTimeoutOption; import io.appium.java_client.android.options.app.SupportsAppActivityOption; -import io.appium.java_client.android.options.app.SupportsIntentOptionsOption; -import io.appium.java_client.android.options.app.SupportsAppWaitDurationOption; import io.appium.java_client.android.options.app.SupportsAppPackageOption; import io.appium.java_client.android.options.app.SupportsAppWaitActivityOption; +import io.appium.java_client.android.options.app.SupportsAppWaitDurationOption; import io.appium.java_client.android.options.app.SupportsAppWaitPackageOption; import io.appium.java_client.android.options.app.SupportsAutoGrantPermissionsOption; -import io.appium.java_client.android.options.app.SupportsEnforceAppInstallOption; +import io.appium.java_client.android.options.app.SupportsIntentOptionsOption; import io.appium.java_client.android.options.app.SupportsRemoteAppsCacheLimitOption; import io.appium.java_client.android.options.app.SupportsUninstallOtherPackagesOption; import io.appium.java_client.android.options.avd.SupportsAvdArgsOption; @@ -89,6 +88,7 @@ import io.appium.java_client.remote.options.SupportsAppOption; import io.appium.java_client.remote.options.SupportsAutoWebViewOption; import io.appium.java_client.remote.options.SupportsDeviceNameOption; +import io.appium.java_client.remote.options.SupportsEnforceAppInstallOption; import io.appium.java_client.remote.options.SupportsIsHeadlessOption; import io.appium.java_client.remote.options.SupportsLanguageOption; import io.appium.java_client.remote.options.SupportsLocaleOption; @@ -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 14eddf9eb..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 @@ -31,13 +31,12 @@ import io.appium.java_client.android.options.app.SupportsAllowTestPackagesOption; import io.appium.java_client.android.options.app.SupportsAndroidInstallTimeoutOption; import io.appium.java_client.android.options.app.SupportsAppActivityOption; -import io.appium.java_client.android.options.app.SupportsAppWaitDurationOption; import io.appium.java_client.android.options.app.SupportsAppPackageOption; import io.appium.java_client.android.options.app.SupportsAppWaitActivityOption; +import io.appium.java_client.android.options.app.SupportsAppWaitDurationOption; import io.appium.java_client.android.options.app.SupportsAppWaitForLaunchOption; import io.appium.java_client.android.options.app.SupportsAppWaitPackageOption; import io.appium.java_client.android.options.app.SupportsAutoGrantPermissionsOption; -import io.appium.java_client.android.options.app.SupportsEnforceAppInstallOption; import io.appium.java_client.android.options.app.SupportsIntentActionOption; import io.appium.java_client.android.options.app.SupportsIntentCategoryOption; import io.appium.java_client.android.options.app.SupportsIntentFlagsOption; @@ -50,7 +49,6 @@ import io.appium.java_client.android.options.avd.SupportsAvdOption; import io.appium.java_client.android.options.avd.SupportsAvdReadyTimeoutOption; import io.appium.java_client.android.options.avd.SupportsGpsEnabledOption; -import io.appium.java_client.remote.options.SupportsIsHeadlessOption; import io.appium.java_client.android.options.avd.SupportsNetworkSpeedOption; import io.appium.java_client.android.options.context.SupportsAutoWebviewTimeoutOption; import io.appium.java_client.android.options.context.SupportsChromeLoggingPrefsOption; @@ -78,7 +76,6 @@ import io.appium.java_client.android.options.mjpeg.SupportsMjpegScreenshotUrlOption; import io.appium.java_client.android.options.mjpeg.SupportsMjpegServerPortOption; import io.appium.java_client.android.options.other.SupportsDisableSuppressAccessibilityServiceOption; -import io.appium.java_client.remote.options.SupportsSkipLogCaptureOption; import io.appium.java_client.android.options.other.SupportsUserProfileOption; import io.appium.java_client.android.options.server.SupportsDisableWindowAnimationOption; import io.appium.java_client.android.options.server.SupportsSkipDeviceInitializationOption; @@ -97,17 +94,23 @@ import io.appium.java_client.remote.options.SupportsClearSystemFilesOption; import io.appium.java_client.remote.options.SupportsDeviceNameOption; import io.appium.java_client.remote.options.SupportsEnablePerformanceLoggingOption; +import io.appium.java_client.remote.options.SupportsEnforceAppInstallOption; +import io.appium.java_client.remote.options.SupportsIsHeadlessOption; import io.appium.java_client.remote.options.SupportsLanguageOption; import io.appium.java_client.remote.options.SupportsLocaleOption; import io.appium.java_client.remote.options.SupportsOrientationOption; import io.appium.java_client.remote.options.SupportsOtherAppsOption; +import io.appium.java_client.remote.options.SupportsSkipLogCaptureOption; import io.appium.java_client.remote.options.SupportsUdidOption; import org.openqa.selenium.Capabilities; 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/android/options/app/IntentOptions.java b/src/main/java/io/appium/java_client/android/options/app/IntentOptions.java index e651a4b91..67f09f2b5 100644 --- a/src/main/java/io/appium/java_client/android/options/app/IntentOptions.java +++ b/src/main/java/io/appium/java_client/android/options/app/IntentOptions.java @@ -261,7 +261,7 @@ public IntentOptions withEi(Map ei) { private Map convertMapValues(Map map, Function converter) { return map.entrySet().stream() .collect(Collectors.toMap( - Map.Entry::getKey, (entry) -> converter.apply(String.valueOf(entry.getValue()))) + Map.Entry::getKey, entry -> converter.apply(String.valueOf(entry.getValue()))) ); } @@ -272,7 +272,7 @@ private Map convertMapValues(Map map, Function> getEi() { Optional> value = getOptionValue("ei"); - return value.map((v) -> convertMapValues(v, Integer::parseInt)); + return value.map(v -> convertMapValues(v, Integer::parseInt)); } /** @@ -292,7 +292,7 @@ public IntentOptions withEl(Map el) { */ public Optional> getEl() { Optional> value = getOptionValue("el"); - return value.map((v) -> convertMapValues(v, Long::parseLong)); + return value.map(v -> convertMapValues(v, Long::parseLong)); } /** @@ -312,7 +312,7 @@ public IntentOptions withEf(Map ef) { */ public Optional> getEf() { Optional> value = getOptionValue("ef"); - return value.map((v) -> convertMapValues(v, Float::parseFloat)); + return value.map(v -> convertMapValues(v, Float::parseFloat)); } /** @@ -356,7 +356,7 @@ public Optional> getEcn() { private static Map mergeValues(Map map) { return map.entrySet().stream() .collect( - Collectors.toMap(Map.Entry::getKey, (entry) -> ((List) entry.getValue()).stream() + Collectors.toMap(Map.Entry::getKey, entry -> ((List) entry.getValue()).stream() .map(String::valueOf) .collect(Collectors.joining(","))) ); diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsActivityOptionsOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsActivityOptionsOption.java index 393ee51c5..59d5fe520 100644 --- a/src/main/java/io/appium/java_client/android/options/app/SupportsActivityOptionsOption.java +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsActivityOptionsOption.java @@ -48,6 +48,6 @@ default T setActivityOptions(ActivityOptions options) { default Optional getActivityOptions() { //noinspection unchecked return Optional.ofNullable(getCapability(ACTIVITY_OPTIONS_OPTION)) - .map((v) -> new ActivityOptions((Map) v)); + .map(v -> new ActivityOptions((Map) v)); } } diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAndroidInstallTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAndroidInstallTimeoutOption.java index 40a588c2e..ead32780b 100644 --- a/src/main/java/io/appium/java_client/android/options/app/SupportsAndroidInstallTimeoutOption.java +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAndroidInstallTimeoutOption.java @@ -24,8 +24,6 @@ import java.time.Duration; import java.util.Optional; -import static io.appium.java_client.internal.CapabilityHelpers.toDuration; - public interface SupportsAndroidInstallTimeoutOption> extends Capabilities, CanSetCapability { String ANDROID_INSTALL_TIMEOUT_OPTION = "androidInstallTimeout"; diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsIntentOptionsOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentOptionsOption.java index 91c2b49a2..3c0fab894 100644 --- a/src/main/java/io/appium/java_client/android/options/app/SupportsIntentOptionsOption.java +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentOptionsOption.java @@ -48,6 +48,6 @@ default T setIntentOptions(IntentOptions options) { default Optional getIntentOptions() { //noinspection unchecked return Optional.ofNullable(getCapability(INTENT_OPTIONS_OPTION)) - .map((v) -> new IntentOptions((Map) v)); + .map(v -> new IntentOptions((Map) v)); } } diff --git a/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdArgsOption.java b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdArgsOption.java index 7843393e4..bca9866c0 100644 --- a/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdArgsOption.java +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdArgsOption.java @@ -56,7 +56,7 @@ default T setAvdArgs(String args) { default Optional, String>> getAvdArgs() { //noinspection unchecked return Optional.ofNullable(getCapability(AVD_ARGS_OPTION)) - .map((v) -> v instanceof List + .map(v -> v instanceof List ? Either.left((List) v) : Either.right(String.valueOf(v)) ); diff --git a/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdEnvOption.java b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdEnvOption.java index e6d083aee..9fadb6532 100644 --- a/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdEnvOption.java +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdEnvOption.java @@ -45,6 +45,6 @@ default T setAvdEnv(Map env) { default Optional> getAvdEnv() { //noinspection unchecked return Optional.ofNullable(getCapability(AVD_ENV_OPTION)) - .map((v) -> (Map) v); + .map(v -> (Map) v); } } diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsShowChromedriverLogOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsShowChromedriverLogOption.java index d201069e5..ef2b6f301 100644 --- a/src/main/java/io/appium/java_client/android/options/context/SupportsShowChromedriverLogOption.java +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsShowChromedriverLogOption.java @@ -45,7 +45,7 @@ default T showChromedriverLog() { * @param value Whether to forward chromedriver output to the Appium server log. * @return self instance for chaining. */ - default T setDhowChromedriverLog(boolean value) { + default T setShowChromedriverLog(boolean value) { return amend(SHOW_CHROMEDRIVER_LOG_OPTION, value); } @@ -54,7 +54,7 @@ default T setDhowChromedriverLog(boolean value) { * * @return True or false. */ - default Optional doesDhowChromedriverLog() { + default Optional doesShowChromedriverLog() { return Optional.ofNullable( toSafeBoolean(getCapability(SHOW_CHROMEDRIVER_LOG_OPTION)) ); diff --git a/src/main/java/io/appium/java_client/android/options/localization/SupportsAppLocaleOption.java b/src/main/java/io/appium/java_client/android/options/localization/SupportsAppLocaleOption.java index 40447d749..d8fafba02 100644 --- a/src/main/java/io/appium/java_client/android/options/localization/SupportsAppLocaleOption.java +++ b/src/main/java/io/appium/java_client/android/options/localization/SupportsAppLocaleOption.java @@ -51,6 +51,6 @@ default T setAppLocale(AppLocale locale) { default Optional getAppLocale() { //noinspection unchecked return Optional.ofNullable(getCapability(APP_LOCALE_OPTION)) - .map((v) -> new AppLocale((Map) v)); + .map(v -> new AppLocale((Map) v)); } } diff --git a/src/main/java/io/appium/java_client/android/options/server/EspressoBuildConfig.java b/src/main/java/io/appium/java_client/android/options/server/EspressoBuildConfig.java index 2044f0bd1..4cbc571da 100644 --- a/src/main/java/io/appium/java_client/android/options/server/EspressoBuildConfig.java +++ b/src/main/java/io/appium/java_client/android/options/server/EspressoBuildConfig.java @@ -42,7 +42,7 @@ private EspressoBuildConfig assignToolsVersionsField(String name, Object value) Optional> toolsVersionsOptional = getOptionValue(TOOLS_VERSION); Map toolsVersions = toolsVersionsOptional.orElseGet(HashMap::new); toolsVersions.put(name, value); - if (!toolsVersionsOptional.isPresent()) { + if (toolsVersionsOptional.isEmpty()) { assignOptionValue(TOOLS_VERSION, toolsVersions); } return this; @@ -51,7 +51,7 @@ private EspressoBuildConfig assignToolsVersionsField(String name, Object value) private Optional getToolsVersionsFieldValue(String name) { Optional> toolsVersionsOptional = getOptionValue(TOOLS_VERSION); //noinspection unchecked - return toolsVersionsOptional.map((v) -> (R) v.getOrDefault(name, null)); + return toolsVersionsOptional.map(v -> (R) v.getOrDefault(name, null)); } /** diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoBuildConfigOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoBuildConfigOption.java index 93eb19831..a916ec35b 100644 --- a/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoBuildConfigOption.java +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoBuildConfigOption.java @@ -61,7 +61,7 @@ default T setEspressoBuildConfig(EspressoBuildConfig config) { default Optional> getEspressoBuildConfig() { return Optional.ofNullable(getCapability(ESPRESSO_BUILD_CONFIG_OPTION)) .map(String::valueOf) - .map((v) -> v.trim().startsWith("{") + .map(v -> v.trim().startsWith("{") ? Either.left(new EspressoBuildConfig(v)) : Either.right(v) ); diff --git a/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java b/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java new file mode 100644 index 000000000..d7e34242e --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java @@ -0,0 +1,141 @@ +/* + * 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.chromium; + +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.service.local.AppiumServiceBuilder; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.HttpClient; + +import java.net.URL; + +/** + * 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. + *
+ * + *

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

+ */ +public class ChromiumDriver extends AppiumDriver { + private static final String AUTOMATION_NAME = AutomationName.CHROMIUM; + + public ChromiumDriver(HttpCommandExecutor executor, Capabilities capabilities) { + super(executor, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(service, httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(builder, httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + /** + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! 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. + */ + public ChromiumDriver(URL remoteSessionAddress, String platformName) { + super(remoteSessionAddress, platformName, AUTOMATION_NAME); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * ClientConfig clientConfig = ClientConfig.defaultConfig()
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * ChromiumOptions options = new ChromiumOptions();
+     * ChromiumDriver driver = new ChromiumDriver(clientConfig, options);
+     *
+     * 
+ * + * @param clientConfig take a look at {@link ClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public ChromiumDriver(ClientConfig clientConfig, Capabilities capabilities) { + super(AppiumClientConfig.fromClientConfig(clientConfig), ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * ChromiumOptions options = new ChromiumOptions();
+     * ChromiumDriver driver = new ChromiumDriver(options, appiumClientConfig);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public ChromiumDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public ChromiumDriver(Capabilities capabilities) { + super(ensureAutomationName(capabilities, AUTOMATION_NAME)); + } +} 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 new file mode 100644 index 000000000..2f25eeff4 --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/ChromiumOptions.java @@ -0,0 +1,58 @@ +/* + * 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.chromium.options; + +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsBrowserNameOption; +import org.openqa.selenium.Capabilities; + +import java.util.Map; + +/** + * Options class that sets options for Chromium when testing websites. + *
+ * @see appium-chromium-driver usage section + */ +public class ChromiumOptions extends BaseOptions implements + SupportsBrowserNameOption, + SupportsChromeDrivePortOption, + SupportsExecutableOption, + SupportsExecutableDirOption, + SupportsVerboseOption, + SupportsLogPathOption, + SupportsBuildCheckOption, + SupportsAutodownloadOption, + SupportsUseSystemExecutableOption { + public ChromiumOptions() { + setCommonOptions(); + } + + public ChromiumOptions(Capabilities source) { + super(source); + setCommonOptions(); + } + + public ChromiumOptions(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setAutomationName(AutomationName.CHROMIUM); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsAutodownloadOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsAutodownloadOption.java new file mode 100644 index 000000000..a1cefdffe --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsAutodownloadOption.java @@ -0,0 +1,51 @@ +/* + * 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.chromium.options; + +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 SupportsAutodownloadOption> extends + Capabilities, CanSetCapability { + String AUTODOWNLOAD_ENABLED = "autodownloadEnabled"; + + /** + * Set to false for disabling automatic downloading of Chrome drivers. + * Unless disable build check preference has been user-set, the capability + * is present because the default value is true. + * + * @param autodownloadEnabled flag. + * @return self instance for chaining. + */ + default T setAutodownloadEnabled(boolean autodownloadEnabled) { + return amend(AUTODOWNLOAD_ENABLED, autodownloadEnabled); + } + + /** + * Get the auto download flag. + * + * @return auto download flag. + */ + default Optional isAutodownloadEnabled() { + return Optional.ofNullable(toSafeBoolean(getCapability(AUTODOWNLOAD_ENABLED))); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsBuildCheckOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsBuildCheckOption.java new file mode 100644 index 000000000..204967bca --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsBuildCheckOption.java @@ -0,0 +1,51 @@ +/* + * 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.chromium.options; + +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 SupportsBuildCheckOption> extends + Capabilities, CanSetCapability { + String DISABLE_BUILD_CHECK = "disableBuildCheck"; + + /** + * Set to true to add the --disable-build-check flag when starting WebDriver. + * Unless disable build check preference has been user-set, the capability + * is not present because the default value is false. + * + * @param buildCheckDisabled flag for --disable-build-check. + * @return self instance for chaining. + */ + default T setBuildCheckDisabled(boolean buildCheckDisabled) { + return amend(DISABLE_BUILD_CHECK, buildCheckDisabled); + } + + /** + * Get disable build check flag. + * + * @return disable build check flag. + */ + default Optional isBuildCheckDisabled() { + return Optional.ofNullable(toSafeBoolean(getCapability(DISABLE_BUILD_CHECK))); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsChromeDrivePortOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsChromeDrivePortOption.java new file mode 100644 index 000000000..68cace279 --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsChromeDrivePortOption.java @@ -0,0 +1,51 @@ +/* + * 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.chromium.options; + +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.toInteger; + +public interface SupportsChromeDrivePortOption> extends + Capabilities, CanSetCapability { + String CHROME_DRIVER_PORT = "chromedriverPort"; + + /** + * The port to start WebDriver processes on. Unless the chrome drive port preference + * has been user-set, it will listen on port 9515, which is the default + * value for this capability. + * + * @param port port number in range 0..65535. + * @return self instance for chaining. + */ + default T setChromeDriverPort(int port) { + return amend(CHROME_DRIVER_PORT, port); + } + + /** + * Get the number of the port for the chrome driver to listen on. + * + * @return Chrome driver port value. + */ + default Optional getChromeDriverPort() { + return Optional.ofNullable(toInteger(getCapability(CHROME_DRIVER_PORT))); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsExecutableDirOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsExecutableDirOption.java new file mode 100644 index 000000000..c525ab7ad --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsExecutableDirOption.java @@ -0,0 +1,49 @@ +/* + * 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.chromium.options; + +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 SupportsExecutableDirOption> extends + Capabilities, CanSetCapability { + String EXECUTABLE_DIR = "executableDir"; + + /** + * A directory within which is found any number of WebDriver binaries. + * If set, the driver will search this directory for WebDrivers of the + * appropriate version to use for your browser. + * + * @param directory of WebDriver binaries. + * @return self instance for chaining. + */ + default T setExecutableDir(String directory) { + return amend(EXECUTABLE_DIR, directory); + } + + /** + * Get a directory within which is found any number of WebDriver binaries. + * + * @return executable directory of a Driver binary. + */ + default Optional getExecutableDir() { + return Optional.ofNullable((String) getCapability(EXECUTABLE_DIR)); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsExecutableOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsExecutableOption.java new file mode 100644 index 000000000..84e730e62 --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsExecutableOption.java @@ -0,0 +1,48 @@ +/* + * 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.chromium.options; + +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 SupportsExecutableOption> extends + Capabilities, CanSetCapability { + String EXECUTABLE = "executable"; + + /** + * The absolute path to a WebDriver binary executable. + * If set, the driver will use that path instead of its own WebDriver. + * + * @param path absolute of a WebDriver. + * @return self instance for chaining. + */ + default T setExecutable(String path) { + return amend(EXECUTABLE, path); + } + + /** + * Get the absolute path to a WebDriver binary executable. + * + * @return executable absolute path. + */ + default Optional getExecutable() { + return Optional.ofNullable((String) getCapability(EXECUTABLE)); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsLogPathOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsLogPathOption.java new file mode 100644 index 000000000..cf1b8713d --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsLogPathOption.java @@ -0,0 +1,48 @@ +/* + * 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.chromium.options; + +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 SupportsLogPathOption> extends + Capabilities, CanSetCapability { + String LOG_PATH = "logPath"; + + /** + * If set, the path to use with the --log-path parameter directing + * WebDriver to write its log to that path. + * + * @param logPath where to write the logs. + * @return self instance for chaining. + */ + default T setLogPath(String logPath) { + return amend(LOG_PATH, logPath); + } + + /** + * Get the log path where the WebDrive writes the logs. + * + * @return the log path. + */ + default Optional getLogPath() { + return Optional.ofNullable((String) getCapability(LOG_PATH)); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsUseSystemExecutableOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsUseSystemExecutableOption.java new file mode 100644 index 000000000..6d51b332b --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsUseSystemExecutableOption.java @@ -0,0 +1,53 @@ +/* + * 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.chromium.options; + +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 SupportsUseSystemExecutableOption> extends + Capabilities, CanSetCapability { + String USE_SYSTEM_EXECUTABLE = "useSystemExecutable"; + + /** + * Set to true to use the version of WebDriver bundled with this driver, + * rather than attempting to download a new one based on the version of the + * browser under test. + * Unless disable build check preference has been user-set, the capability + * is not present because the default value is false. + * + * @param useSystemExecutable flag. + * @return self instance for chaining. + */ + default T setUseSystemExecutable(boolean useSystemExecutable) { + return amend(USE_SYSTEM_EXECUTABLE, useSystemExecutable); + } + + /** + * Get the use system executable flag. + * + * @return use system executable flag. + */ + default Optional isUseSystemExecutable() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_SYSTEM_EXECUTABLE))); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/options/SupportsVerboseOption.java b/src/main/java/io/appium/java_client/chromium/options/SupportsVerboseOption.java new file mode 100644 index 000000000..14aa571d2 --- /dev/null +++ b/src/main/java/io/appium/java_client/chromium/options/SupportsVerboseOption.java @@ -0,0 +1,51 @@ +/* + * 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.chromium.options; + +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 SupportsVerboseOption> extends + Capabilities, CanSetCapability { + String VERBOSE = "verbose"; + + /** + * Set to true to add the --verbose flag when starting WebDriver. + * Unless the verbose preference has been user-set, the capability + * is not present because the default value is false. + * + * @param verbose flag for --verbose. + * @return self instance for chaining. + */ + default T setVerbose(boolean verbose) { + return amend(VERBOSE, verbose); + } + + /** + * Get the verbose flag. + * + * @return verbose flag. + */ + default Optional isVerbose() { + return Optional.ofNullable(toSafeBoolean(getCapability(VERBOSE))); + } +} 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 ca813c4e0..ae507f940 100644 --- a/src/main/java/io/appium/java_client/clipboard/HasClipboard.java +++ b/src/main/java/io/appium/java_client/clipboard/HasClipboard.java @@ -16,31 +16,39 @@ package io.appium.java_client.clipboard; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.MobileCommand.GET_CLIPBOARD; -import static io.appium.java_client.MobileCommand.SET_CLIPBOARD; -import static io.appium.java_client.MobileCommand.prepareArguments; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; import java.nio.charset.StandardCharsets; -import java.util.AbstractMap; import java.util.Base64; +import java.util.Map; -public interface HasClipboard extends ExecutesMethod { +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 { /** * Set the content of device's clipboard. * - * @param contentType one of supported content types. + * @param contentType one of supported content types. * @param base64Content base64-encoded content to be set. */ default void setClipboard(ClipboardContentType contentType, byte[] base64Content) { - String[] parameters = new String[]{"content", "contentType"}; - Object[] values = new Object[]{new String(checkNotNull(base64Content), StandardCharsets.UTF_8), - contentType.name().toLowerCase()}; - CommandExecutionHelper.execute(this, new AbstractMap.SimpleEntry<>(SET_CLIPBOARD, - prepareArguments(parameters, values))); + final String extName = "mobile: setClipboard"; + var args = Map.of( + "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), + "contentType", contentType.name().toLowerCase(ROOT) + ); + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(this, Map.entry(SET_CLIPBOARD, args)); + } } /** @@ -50,8 +58,14 @@ default void setClipboard(ClipboardContentType contentType, byte[] base64Content * @return the actual content of the clipboard as base64-encoded string or an empty string if the clipboard is empty */ default String getClipboard(ClipboardContentType contentType) { - return CommandExecutionHelper.execute(this, new AbstractMap.SimpleEntry<>(GET_CLIPBOARD, - prepareArguments("contentType", contentType.name().toLowerCase()))); + final String extName = "mobile: getClipboard"; + var args = Map.of("contentType", contentType.name().toLowerCase(ROOT)); + try { + return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + return CommandExecutionHelper.execute(this, Map.entry(GET_CLIPBOARD, args)); + } } /** 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 15d7ddf36..7eac89083 100644 --- a/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java +++ b/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java @@ -16,11 +16,12 @@ package io.appium.java_client.driverscripts; -import com.google.common.collect.ImmutableMap; - +import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Locale.ROOT; +import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; @@ -35,7 +36,7 @@ public class ScriptOptions { * @return self instance for chaining */ public ScriptOptions withScriptType(ScriptType type) { - this.scriptType = checkNotNull(type); + this.scriptType = requireNonNull(type); return this; } @@ -58,9 +59,9 @@ public ScriptOptions withTimeout(long timeoutMs) { * @return The map containing the provided options */ public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - ofNullable(scriptType).map(x -> builder.put("type", x.name().toLowerCase())); - ofNullable(timeoutMs).map(x -> builder.put("timeout", x)); - return builder.build(); + var map = new HashMap(); + 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/CanExecuteFlutterScripts.java b/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java new file mode 100644 index 000000000..e844388be --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/CanExecuteFlutterScripts.java @@ -0,0 +1,33 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.flutter.commands.FlutterCommandParameter; +import org.openqa.selenium.JavascriptExecutor; + +import java.util.Map; + +public interface CanExecuteFlutterScripts extends JavascriptExecutor { + + /** + * Executes a Flutter-specific script using JavascriptExecutor. + * + * @param scriptName The name of the Flutter script to execute. + * @param parameter The parameters for the Flutter command. + * @return The result of executing the script. + */ + default Object executeFlutterCommand(String scriptName, FlutterCommandParameter parameter) { + return executeFlutterCommand(scriptName, parameter.toJson()); + } + + /** + * Executes a Flutter-specific script using JavascriptExecutor. + * + * @param scriptName The name of the Flutter script to execute. + * @param args The args for the Flutter command in Map format. + * @return The result of executing the script. + */ + default Object executeFlutterCommand(String scriptName, Map args) { + String commandName = String.format("flutter: %s", scriptName); + return executeScript(commandName, args); + } + +} diff --git a/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java b/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java new file mode 100644 index 000000000..2e5a83430 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/FlutterDriverOptions.java @@ -0,0 +1,56 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.android.options.UiAutomator2Options; +import io.appium.java_client.flutter.options.SupportsFlutterElementWaitTimeoutOption; +import io.appium.java_client.flutter.options.SupportsFlutterEnableMockCamera; +import io.appium.java_client.flutter.options.SupportsFlutterServerLaunchTimeoutOption; +import io.appium.java_client.flutter.options.SupportsFlutterSystemPortOption; +import io.appium.java_client.ios.options.XCUITestOptions; +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.options.BaseOptions; +import org.openqa.selenium.Capabilities; + +import java.util.Map; + +/** + * Provides options specific to the Appium Flutter Integration Driver. + * + *

For more details, refer to the + * capabilities documentation

+ */ +public class FlutterDriverOptions extends BaseOptions implements + SupportsFlutterSystemPortOption, + SupportsFlutterServerLaunchTimeoutOption, + SupportsFlutterElementWaitTimeoutOption, + SupportsFlutterEnableMockCamera { + + public FlutterDriverOptions() { + setDefaultOptions(); + } + + public FlutterDriverOptions(Capabilities source) { + super(source); + setDefaultOptions(); + } + + public FlutterDriverOptions(Map source) { + super(source); + setDefaultOptions(); + } + + public FlutterDriverOptions setUiAutomator2Options(UiAutomator2Options uiAutomator2Options) { + return setDefaultOptions(merge(uiAutomator2Options)); + } + + public FlutterDriverOptions setXCUITestOptions(XCUITestOptions xcuiTestOptions) { + return setDefaultOptions(merge(xcuiTestOptions)); + } + + private void setDefaultOptions() { + setDefaultOptions(this); + } + + private FlutterDriverOptions setDefaultOptions(FlutterDriverOptions flutterDriverOptions) { + return flutterDriverOptions.setAutomationName(AutomationName.FLUTTER_INTEGRATION); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java b/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java new file mode 100644 index 000000000..1d11378e2 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/FlutterIntegrationTestDriver.java @@ -0,0 +1,24 @@ +package io.appium.java_client.flutter; + +import org.openqa.selenium.WebDriver; + +/** + * The {@code FlutterDriver} interface represents a driver that controls interactions with + * 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.

+ * + * @see WebDriver + * @see SupportsGestureOnFlutterElements + * @see SupportsScrollingOfFlutterElements + * @see SupportsWaitingForFlutterElements + */ +public interface FlutterIntegrationTestDriver extends + WebDriver, + SupportsGestureOnFlutterElements, + SupportsScrollingOfFlutterElements, + SupportsWaitingForFlutterElements, + SupportsFlutterCameraMocking { +} diff --git a/src/main/java/io/appium/java_client/flutter/SupportsFlutterCameraMocking.java b/src/main/java/io/appium/java_client/flutter/SupportsFlutterCameraMocking.java new file mode 100644 index 000000000..6ffad1089 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/SupportsFlutterCameraMocking.java @@ -0,0 +1,47 @@ +package io.appium.java_client.flutter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Map; + +/** + * This interface extends {@link CanExecuteFlutterScripts} and provides methods + * to support mocking of camera inputs in Flutter applications. + */ +public interface SupportsFlutterCameraMocking extends CanExecuteFlutterScripts { + + /** + * Injects a mock image into the Flutter application using the provided file. + * + * @param image the image file to be mocked (must be in PNG format) + * @return an {@code String} representing a unique id of the injected image + * @throws IOException if an I/O error occurs while reading the image file + */ + default String injectMockImage(File image) throws IOException { + String base64EncodedImage = Base64.getEncoder().encodeToString(Files.readAllBytes(image.toPath())); + return injectMockImage(base64EncodedImage); + } + + /** + * Injects a mock image into the Flutter application using the provided Base64-encoded image string. + * + * @param base64Image the Base64-encoded string representation of the image (must be in PNG format) + * @return an {@code String} representing the result of the injection operation + */ + default String injectMockImage(String base64Image) { + return (String) executeFlutterCommand("injectImage", Map.of( + "base64Image", base64Image + )); + } + + /** + * Activates the injected image identified by the specified image ID in the Flutter application. + * + * @param imageId the ID of the injected image to activate + */ + default void activateInjectedImage(String imageId) { + executeFlutterCommand("activateInjectedImage", Map.of("imageId", imageId)); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/SupportsGestureOnFlutterElements.java b/src/main/java/io/appium/java_client/flutter/SupportsGestureOnFlutterElements.java new file mode 100644 index 000000000..7e80e8a97 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/SupportsGestureOnFlutterElements.java @@ -0,0 +1,35 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.flutter.commands.DoubleClickParameter; +import io.appium.java_client.flutter.commands.DragAndDropParameter; +import io.appium.java_client.flutter.commands.LongPressParameter; + +public interface SupportsGestureOnFlutterElements extends CanExecuteFlutterScripts { + + /** + * Performs a double click action on an element. + * + * @param parameter The parameters for double-clicking, specifying element details. + */ + default void performDoubleClick(DoubleClickParameter parameter) { + executeFlutterCommand("doubleClick", parameter); + } + + /** + * Performs a long press action on an element. + * + * @param parameter The parameters for long pressing, specifying element details. + */ + default void performLongPress(LongPressParameter parameter) { + executeFlutterCommand("longPress", parameter); + } + + /** + * Performs a drag-and-drop action between two elements. + * + * @param parameter The parameters for drag-and-drop, specifying source and target elements. + */ + default void performDragAndDrop(DragAndDropParameter parameter) { + executeFlutterCommand("dragAndDrop", parameter); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/SupportsScrollingOfFlutterElements.java b/src/main/java/io/appium/java_client/flutter/SupportsScrollingOfFlutterElements.java new file mode 100644 index 000000000..25a734cf7 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/SupportsScrollingOfFlutterElements.java @@ -0,0 +1,17 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.flutter.commands.ScrollParameter; +import org.openqa.selenium.WebElement; + +public interface SupportsScrollingOfFlutterElements extends CanExecuteFlutterScripts { + + /** + * Scrolls to make an element visible on the screen. + * + * @param parameter The parameters for scrolling, specifying element details. + * @return The WebElement that was scrolled to. + */ + default WebElement scrollTillVisible(ScrollParameter parameter) { + return (WebElement) executeFlutterCommand("scrollTillVisible", parameter); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/SupportsWaitingForFlutterElements.java b/src/main/java/io/appium/java_client/flutter/SupportsWaitingForFlutterElements.java new file mode 100644 index 000000000..521f75cc8 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/SupportsWaitingForFlutterElements.java @@ -0,0 +1,25 @@ +package io.appium.java_client.flutter; + +import io.appium.java_client.flutter.commands.WaitParameter; + +public interface SupportsWaitingForFlutterElements extends CanExecuteFlutterScripts { + + /** + * Waits for an element to become visible on the screen. + * + * @param parameter The parameters for waiting, specifying timeout and element details. + */ + default void waitForVisible(WaitParameter parameter) { + executeFlutterCommand("waitForVisible", parameter); + } + + /** + * Waits for an element to become absent on the screen. + * + * @param parameter The parameters for waiting, specifying timeout and element details. + */ + default void waitForInVisible(WaitParameter parameter) { + executeFlutterCommand("waitForAbsent", parameter); + } + +} diff --git a/src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java b/src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java new file mode 100644 index 000000000..8bbf45cbf --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/android/FlutterAndroidDriver.java @@ -0,0 +1,69 @@ +package io.appium.java_client.flutter.android; + +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.android.AndroidDriver; +import io.appium.java_client.flutter.FlutterIntegrationTestDriver; +import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.service.local.AppiumServiceBuilder; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.HttpClient; + +import java.net.URL; + +/** + * Custom AndroidDriver implementation with additional Flutter-specific capabilities. + */ +public class FlutterAndroidDriver extends AndroidDriver implements FlutterIntegrationTestDriver { + + public FlutterAndroidDriver(HttpCommandExecutor executor, Capabilities capabilities) { + super(executor, capabilities); + } + + public FlutterAndroidDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, capabilities); + } + + public FlutterAndroidDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, capabilities); + } + + public FlutterAndroidDriver( + AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(service, httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, capabilities); + } + + public FlutterAndroidDriver( + AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(builder, httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, capabilities); + } + + public FlutterAndroidDriver(ClientConfig clientConfig, Capabilities capabilities) { + super(clientConfig, capabilities); + } + + public FlutterAndroidDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, capabilities); + } + + public FlutterAndroidDriver(Capabilities capabilities) { + super(capabilities); + } + + public FlutterAndroidDriver(URL remoteSessionAddress, String automationName) { + super(remoteSessionAddress, automationName); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/DoubleClickParameter.java b/src/main/java/io/appium/java_client/flutter/commands/DoubleClickParameter.java new file mode 100644 index 000000000..859f26057 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/DoubleClickParameter.java @@ -0,0 +1,34 @@ +package io.appium.java_client.flutter.commands; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.internal.Require; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Accessors(chain = true) +@Setter +@Getter +public class DoubleClickParameter extends FlutterCommandParameter { + private WebElement element; + private Point offset; + + + @Override + public Map toJson() { + Require.precondition(element != null || offset != null, + "Must supply a valid element or offset to perform flutter gesture event"); + + Map params = new HashMap<>(); + Optional.ofNullable(element).ifPresent(element -> params.put("origin", element)); + Optional.ofNullable(offset).ifPresent(offset -> + params.put("offset", Map.of("x", offset.getX(), "y", offset.getY()))); + return Collections.unmodifiableMap(params); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/DragAndDropParameter.java b/src/main/java/io/appium/java_client/flutter/commands/DragAndDropParameter.java new file mode 100644 index 000000000..14bc04cbf --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/DragAndDropParameter.java @@ -0,0 +1,35 @@ +package io.appium.java_client.flutter.commands; + +import lombok.Getter; +import lombok.experimental.Accessors; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.internal.Require; + +import java.util.Map; + +@Accessors(chain = true) +@Getter +public class DragAndDropParameter extends FlutterCommandParameter { + private final WebElement source; + private final WebElement target; + + /** + * Constructs a new instance of {@code DragAndDropParameter} with the given source and target {@link WebElement}s. + * Throws an {@link IllegalArgumentException} if either {@code source} or {@code target} is {@code null}. + * + * @param source The source {@link WebElement} from which the drag operation starts. + * @param target The target {@link WebElement} where the drag operation ends. + * @throws IllegalArgumentException if {@code source} or {@code target} is {@code null}. + */ + public DragAndDropParameter(WebElement source, WebElement target) { + Require.precondition(source != null && target != null, + "Must supply valid source and target element to perform drag and drop event"); + this.source = source; + this.target = target; + } + + @Override + public Map toJson() { + return Map.of("source", source, "target", target); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/FlutterCommandParameter.java b/src/main/java/io/appium/java_client/flutter/commands/FlutterCommandParameter.java new file mode 100644 index 000000000..ddd2d74f6 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/FlutterCommandParameter.java @@ -0,0 +1,25 @@ +package io.appium.java_client.flutter.commands; + +import io.appium.java_client.AppiumBy; +import org.openqa.selenium.By; + +import java.util.Map; + +public abstract class FlutterCommandParameter { + + /** + * Parses an Appium Flutter locator into a Map representation suitable for Flutter Integration Driver. + * + * @param by The FlutterBy instance representing the locator to parse. + * @return A Map containing the parsed locator information with keys using and value. + */ + protected static Map parseFlutterLocator(AppiumBy.FlutterBy by) { + By.Remotable.Parameters parameters = by.getRemoteParameters(); + return Map.of( + "using", parameters.using(), + "value", parameters.value() + ); + } + + public abstract Map toJson(); +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/LongPressParameter.java b/src/main/java/io/appium/java_client/flutter/commands/LongPressParameter.java new file mode 100644 index 000000000..36f80772d --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/LongPressParameter.java @@ -0,0 +1,33 @@ +package io.appium.java_client.flutter.commands; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.internal.Require; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Accessors(chain = true) +@Setter +@Getter +public class LongPressParameter extends FlutterCommandParameter { + private WebElement element; + private Point offset; + + @Override + public Map toJson() { + Require.precondition(element != null || offset != null, + "Must supply a valid element or offset to perform flutter gesture event"); + + Map params = new HashMap<>(); + Optional.ofNullable(element).ifPresent(element -> params.put("origin", element)); + Optional.ofNullable(offset).ifPresent(offset -> + params.put("offset", Map.of("x", offset.getX(), "y", offset.getY()))); + return Collections.unmodifiableMap(params); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/ScrollParameter.java b/src/main/java/io/appium/java_client/flutter/commands/ScrollParameter.java new file mode 100644 index 000000000..d2a2674c7 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/ScrollParameter.java @@ -0,0 +1,86 @@ +package io.appium.java_client.flutter.commands; + +import io.appium.java_client.AppiumBy; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.openqa.selenium.internal.Require; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Accessors(chain = true) +@Getter +@Setter +public class ScrollParameter extends FlutterCommandParameter { + private AppiumBy.FlutterBy scrollTo; + private AppiumBy.FlutterBy scrollView; + private ScrollDirection scrollDirection; + private Integer delta; + private Integer maxScrolls; + private Integer settleBetweenScrollsTimeout; + private Duration dragDuration; + + private ScrollParameter() { + } + + /** + * Constructs a new ScrollOptions object with the given parameters. + * + * @param scrollTo the locator used for scrolling to a specific element + */ + public ScrollParameter(AppiumBy.FlutterBy scrollTo) { + this(scrollTo, ScrollDirection.DOWN); + } + + /** + * Constructs a new ScrollOptions object with the given parameters. + * + * @param scrollTo the locator used for scrolling to a specific element + * @param scrollDirection the direction in which to scroll (e.g., ScrollDirection.DOWN) + * @throws IllegalArgumentException if scrollTo is null + */ + public ScrollParameter(AppiumBy.FlutterBy scrollTo, ScrollDirection scrollDirection) { + Require.precondition(scrollTo != null, "Must supply a valid locator for scrollTo"); + this.scrollTo = scrollTo; + this.scrollDirection = scrollDirection; + } + + @Override + public Map toJson() { + Map params = new HashMap<>(); + + params.put("finder", parseFlutterLocator(scrollTo)); + Optional.ofNullable(scrollView) + .ifPresent(scrollView -> params.put("scrollView", parseFlutterLocator(scrollView))); + Optional.ofNullable(delta) + .ifPresent(delta -> params.put("delta", delta)); + Optional.ofNullable(maxScrolls) + .ifPresent(maxScrolls -> params.put("maxScrolls", maxScrolls)); + Optional.ofNullable(settleBetweenScrollsTimeout) + .ifPresent(timeout -> params.put("settleBetweenScrollsTimeout", settleBetweenScrollsTimeout)); + Optional.ofNullable(scrollDirection) + .ifPresent(direction -> params.put("scrollDirection", direction.getDirection())); + Optional.ofNullable(dragDuration) + .ifPresent(direction -> params.put("dragDuration", dragDuration.getSeconds())); + + return Collections.unmodifiableMap(params); + } + + @Getter + public static enum ScrollDirection { + UP("up"), + RIGHT("right"), + DOWN("down"), + LEFT("left"); + + private final String direction; + + ScrollDirection(String direction) { + this.direction = direction; + } + } +} diff --git a/src/main/java/io/appium/java_client/flutter/commands/WaitParameter.java b/src/main/java/io/appium/java_client/flutter/commands/WaitParameter.java new file mode 100644 index 000000000..d9f057032 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/commands/WaitParameter.java @@ -0,0 +1,38 @@ +package io.appium.java_client.flutter.commands; + +import io.appium.java_client.AppiumBy; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.internal.Require; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Accessors(chain = true) +@Getter +@Setter +public class WaitParameter extends FlutterCommandParameter { + private WebElement element; + private AppiumBy.FlutterBy locator; + private Duration timeout; + + @Override + public Map toJson() { + Require.precondition(element != null || locator != null, + "Must supply a valid element or locator to wait for"); + Map params = new HashMap<>(); + Optional.ofNullable(element) + .ifPresent(element -> params.put("element", element)); + Optional.ofNullable(locator) + .ifPresent(locator -> params.put("locator", parseFlutterLocator(locator))); + Optional.ofNullable(timeout) + .ifPresent(timeout -> params.put("timeout", timeout)); + + return Collections.unmodifiableMap(params); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/ios/FlutterIOSDriver.java b/src/main/java/io/appium/java_client/flutter/ios/FlutterIOSDriver.java new file mode 100644 index 000000000..2d8c9c991 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/ios/FlutterIOSDriver.java @@ -0,0 +1,69 @@ +package io.appium.java_client.flutter.ios; + +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.flutter.FlutterIntegrationTestDriver; +import io.appium.java_client.ios.IOSDriver; +import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.service.local.AppiumServiceBuilder; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.HttpClient; + +import java.net.URL; + +/** + * Custom IOSDriver implementation with additional Flutter-specific capabilities. + */ +public class FlutterIOSDriver extends IOSDriver implements FlutterIntegrationTestDriver { + + public FlutterIOSDriver(HttpCommandExecutor executor, Capabilities capabilities) { + super(executor, capabilities); + } + + public FlutterIOSDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, capabilities); + } + + public FlutterIOSDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, capabilities); + } + + public FlutterIOSDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, capabilities); + } + + public FlutterIOSDriver( + AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(service, httpClientFactory, capabilities); + } + + public FlutterIOSDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, capabilities); + } + + public FlutterIOSDriver( + AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(builder, httpClientFactory, capabilities); + } + + public FlutterIOSDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, capabilities); + } + + public FlutterIOSDriver(ClientConfig clientConfig, Capabilities capabilities) { + super(clientConfig, capabilities); + } + + public FlutterIOSDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, capabilities); + } + + public FlutterIOSDriver(URL remoteSessionAddress) { + super(remoteSessionAddress); + } + + public FlutterIOSDriver(Capabilities capabilities) { + super(capabilities); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterElementWaitTimeoutOption.java b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterElementWaitTimeoutOption.java new file mode 100644 index 000000000..794c955d4 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterElementWaitTimeoutOption.java @@ -0,0 +1,36 @@ +package io.appium.java_client.flutter.options; + +import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.time.Duration; +import java.util.Optional; + +public interface SupportsFlutterElementWaitTimeoutOption> extends + Capabilities, CanSetCapability { + String FLUTTER_ELEMENT_WAIT_TIMEOUT_OPTION = "flutterElementWaitTimeout"; + + /** + * Sets the Flutter element wait timeout. + * Defaults to 5 seconds. + * + * @param timeout The duration to wait for Flutter elements during findElement method + * @return self instance for chaining. + */ + default T setFlutterElementWaitTimeout(Duration timeout) { + return amend(FLUTTER_ELEMENT_WAIT_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Retrieves the current Flutter element wait timeout if set. + * + * @return An {@link Optional} containing the duration of the Flutter element wait timeout, or empty if not set. + */ + default Optional getFlutterElementWaitTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(FLUTTER_ELEMENT_WAIT_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterEnableMockCamera.java b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterEnableMockCamera.java new file mode 100644 index 000000000..baffaf96d --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterEnableMockCamera.java @@ -0,0 +1,33 @@ +package io.appium.java_client.flutter.options; + +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 SupportsFlutterEnableMockCamera> extends + Capabilities, CanSetCapability { + String FLUTTER_ENABLE_MOCK_CAMERA_OPTION = "flutterEnableMockCamera"; + + /** + * Sets the 'flutterEnableMockCamera' capability to the specified value. + * + * @param value the value to set for the 'flutterEnableMockCamera' capability + * @return an instance of type {@code T} with the updated capability set + */ + default T setFlutterEnableMockCamera(boolean value) { + return amend(FLUTTER_ENABLE_MOCK_CAMERA_OPTION, value); + } + + /** + * Retrieves the current value of the 'flutterEnableMockCamera' capability, if available. + * + * @return an {@code Optional} containing the current value of the capability, + */ + default Optional doesFlutterEnableMockCamera() { + return Optional.ofNullable(toSafeBoolean(getCapability(FLUTTER_ENABLE_MOCK_CAMERA_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterServerLaunchTimeoutOption.java b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterServerLaunchTimeoutOption.java new file mode 100644 index 000000000..52b51a8eb --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterServerLaunchTimeoutOption.java @@ -0,0 +1,36 @@ +package io.appium.java_client.flutter.options; + +import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.time.Duration; +import java.util.Optional; + +public interface SupportsFlutterServerLaunchTimeoutOption> extends + Capabilities, CanSetCapability { + String FLUTTER_SERVER_LAUNCH_TIMEOUT_OPTION = "flutterServerLaunchTimeout"; + + /** + * Timeout to wait for FlutterServer to be pingable, + * e.g. finishes building. Defaults to 60000ms. + * + * @param timeout Timeout to wait until FlutterServer is listening. + * @return self instance for chaining. + */ + default T setFlutterServerLaunchTimeout(Duration timeout) { + return amend(FLUTTER_SERVER_LAUNCH_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the maximum timeout to wait until FlutterServer is listening. + * + * @return Timeout value. + */ + default Optional getFlutterServerLaunchTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(FLUTTER_SERVER_LAUNCH_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterSystemPortOption.java b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterSystemPortOption.java new file mode 100644 index 000000000..3f25ccec3 --- /dev/null +++ b/src/main/java/io/appium/java_client/flutter/options/SupportsFlutterSystemPortOption.java @@ -0,0 +1,33 @@ +package io.appium.java_client.flutter.options; + +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.toInteger; + +public interface SupportsFlutterSystemPortOption> extends + Capabilities, CanSetCapability { + String FLUTTER_SYSTEM_PORT_OPTION = "flutterSystemPort"; + + /** + * Set the port where Flutter server starts. + * + * @param flutterSystemPort is the port number + * @return self instance for chaining. + */ + default T setFlutterSystemPort(int flutterSystemPort) { + return amend(FLUTTER_SYSTEM_PORT_OPTION, flutterSystemPort); + } + + /** + * Get the number of the port Flutter server starts on the system. + * + * @return Port number + */ + default Optional getFlutterSystemPort() { + return Optional.ofNullable(toInteger(getCapability(FLUTTER_SYSTEM_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/functions/ActionSupplier.java b/src/main/java/io/appium/java_client/functions/ActionSupplier.java index 9554bf6ff..67ea8b888 100644 --- a/src/main/java/io/appium/java_client/functions/ActionSupplier.java +++ b/src/main/java/io/appium/java_client/functions/ActionSupplier.java @@ -20,6 +20,12 @@ import java.util.function.Supplier; +/** + * Represents a supplier of actions. + * + * @deprecated Use {@link Supplier} instead + */ +@Deprecated @FunctionalInterface public interface ActionSupplier> extends Supplier { } diff --git a/src/main/java/io/appium/java_client/functions/AppiumFunction.java b/src/main/java/io/appium/java_client/functions/AppiumFunction.java index de9069d37..e23dcb298 100644 --- a/src/main/java/io/appium/java_client/functions/AppiumFunction.java +++ b/src/main/java/io/appium/java_client/functions/AppiumFunction.java @@ -28,9 +28,11 @@ * * @param The input type * @param The return type + * @deprecated Use {@link java.util.function.Function} instead */ +@Deprecated @FunctionalInterface -public interface AppiumFunction extends Function, java.util.function.Function { +public interface AppiumFunction extends Function, java.util.function.Function { @Override default AppiumFunction compose(java.util.function.Function before) { Objects.requireNonNull(before); diff --git a/src/main/java/io/appium/java_client/functions/ExpectedCondition.java b/src/main/java/io/appium/java_client/functions/ExpectedCondition.java index 885952525..926577c53 100644 --- a/src/main/java/io/appium/java_client/functions/ExpectedCondition.java +++ b/src/main/java/io/appium/java_client/functions/ExpectedCondition.java @@ -23,7 +23,9 @@ * with {@link java.util.function.Function}. * * @param The return type + * @deprecated Use {@link org.openqa.selenium.support.ui.ExpectedCondition} instead */ +@Deprecated @FunctionalInterface public interface ExpectedCondition extends org.openqa.selenium.support.ui.ExpectedCondition, AppiumFunction { diff --git a/src/main/java/io/appium/java_client/gecko/GeckoDriver.java b/src/main/java/io/appium/java_client/gecko/GeckoDriver.java index 43d2072c8..07cb9e3e7 100644 --- a/src/main/java/io/appium/java_client/gecko/GeckoDriver.java +++ b/src/main/java/io/appium/java_client/gecko/GeckoDriver.java @@ -16,6 +16,7 @@ package io.appium.java_client.gecko; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.remote.AutomationName; import io.appium.java_client.service.local.AppiumDriverLocalService; @@ -74,6 +75,20 @@ public GeckoDriver(HttpClient.Factory httpClientFactory, Capabilities capabiliti super(httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); } + /** + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! 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. + */ + public GeckoDriver(URL remoteSessionAddress, String platformName) { + super(remoteSessionAddress, platformName, AUTOMATION_NAME); + } + /** * Creates a new instance based on the given ClientConfig and {@code capabilities}. * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. @@ -94,7 +109,31 @@ public GeckoDriver(HttpClient.Factory httpClientFactory, Capabilities capabiliti * */ public GeckoDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensureAutomationName(capabilities, AUTOMATION_NAME)); + super(AppiumClientConfig.fromClientConfig(clientConfig), ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * GeckoOptions options = new GeckoOptions();
+     * GeckoDriver driver = new GeckoDriver(options, appiumClientConfig);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public GeckoDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensureAutomationName(capabilities, AUTOMATION_NAME)); } public GeckoDriver(Capabilities capabilities) { 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/BaseComparisonOptions.java b/src/main/java/io/appium/java_client/imagecomparison/BaseComparisonOptions.java index 5489b9686..bd20f884a 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/BaseComparisonOptions.java +++ b/src/main/java/io/appium/java_client/imagecomparison/BaseComparisonOptions.java @@ -16,12 +16,12 @@ package io.appium.java_client.imagecomparison; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static java.util.Optional.ofNullable; + public abstract class BaseComparisonOptions> { private Boolean visualize; @@ -45,8 +45,8 @@ public T withEnabledVisualization() { * @return comparison options mapping. */ public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - ofNullable(visualize).map(x -> builder.put("visualize", x)); - return builder.build(); + var map = new HashMap(); + ofNullable(visualize).ifPresent(x -> map.put("visualize", x)); + return Collections.unmodifiableMap(map); } } 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 1dcfa3e68..e73be71c8 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java +++ b/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java @@ -16,10 +16,6 @@ package io.appium.java_client.imagecomparison; -import lombok.AccessLevel; -import lombok.Getter; -import org.apache.commons.codec.binary.Base64; - import org.openqa.selenium.Point; import org.openqa.selenium.Rectangle; @@ -28,25 +24,31 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Map; public abstract class ComparisonResult { private static final String VISUALIZATION = "visualization"; - @Getter(AccessLevel.PROTECTED) private final Map commandResult; + protected final Object commandResult; - public ComparisonResult(Map commandResult) { + public ComparisonResult(Object commandResult) { this.commandResult = commandResult; } + protected Map getResultAsMap() { + //noinspection unchecked + return (Map) commandResult; + } + /** - * 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 */ protected void verifyPropertyPresence(String propertyName) { - if (!commandResult.containsKey(propertyName)) { + if (!getResultAsMap().containsKey(propertyName)) { throw new IllegalStateException( String.format("There is no '%s' attribute in the resulting command output %s. " + "Did you set the options properly?", propertyName, commandResult)); @@ -60,17 +62,17 @@ protected void verifyPropertyPresence(String propertyName) { */ public byte[] getVisualization() { verifyPropertyPresence(VISUALIZATION); - return ((String) getCommandResult().get(VISUALIZATION)).getBytes(StandardCharsets.UTF_8); + return ((String) getResultAsMap().get(VISUALIZATION)).getBytes(StandardCharsets.UTF_8); } /** * Stores visualization image into the given file. * - * @param destination file to save image. + * @param destination File path to save the image to. * @throws IOException On file system I/O error. */ public void storeVisualization(File destination) throws IOException { - final byte[] data = Base64.decodeBase64(getVisualization()); + final byte[] data = Base64.getDecoder().decode(getVisualization()); try (OutputStream stream = new FileOutputStream(destination)) { stream.write(data); } diff --git a/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingOptions.java b/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingOptions.java index d4c89161f..3fd56517c 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingOptions.java +++ b/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingOptions.java @@ -16,13 +16,13 @@ package io.appium.java_client.imagecomparison; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import static com.google.common.base.Preconditions.checkArgument; import static java.util.Optional.ofNullable; -import com.google.common.collect.ImmutableMap; - -import java.util.Map; - public class FeaturesMatchingOptions extends BaseComparisonOptions { private String detectorName; private String matchFunc; @@ -68,11 +68,10 @@ public FeaturesMatchingOptions withGoodMatchesFactor(int factor) { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(detectorName).map(x -> builder.put("detectorName", x)); - ofNullable(matchFunc).map(x -> builder.put("matchFunc", x)); - ofNullable(goodMatchesFactor).map(x -> builder.put("goodMatchesFactor", x)); - return builder.build(); + var map = new HashMap<>(super.build()); + ofNullable(detectorName).ifPresent(x -> map.put("detectorName", x)); + ofNullable(matchFunc).ifPresent(x -> map.put("matchFunc", x)); + ofNullable(goodMatchesFactor).ifPresent(x -> map.put("goodMatchesFactor", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingResult.java b/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingResult.java index 2ba90c7dd..0a983e50a 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingResult.java +++ b/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingResult.java @@ -43,7 +43,7 @@ public FeaturesMatchingResult(Map input) { */ public int getCount() { verifyPropertyPresence(COUNT); - return ((Long) getCommandResult().get(COUNT)).intValue(); + return ((Long) getResultAsMap().get(COUNT)).intValue(); } /** @@ -56,7 +56,7 @@ public int getCount() { */ public int getTotalCount() { verifyPropertyPresence(TOTAL_COUNT); - return ((Long) getCommandResult().get(TOTAL_COUNT)).intValue(); + return ((Long) getResultAsMap().get(TOTAL_COUNT)).intValue(); } /** @@ -67,7 +67,7 @@ public int getTotalCount() { public List getPoints1() { verifyPropertyPresence(POINTS1); //noinspection unchecked - return ((List>) getCommandResult().get(POINTS1)).stream() + return ((List>) getResultAsMap().get(POINTS1)).stream() .map(ComparisonResult::mapToPoint) .collect(Collectors.toList()); } @@ -80,7 +80,7 @@ public List getPoints1() { public Rectangle getRect1() { verifyPropertyPresence(RECT1); //noinspection unchecked - return mapToRect((Map) getCommandResult().get(RECT1)); + return mapToRect((Map) getResultAsMap().get(RECT1)); } /** @@ -91,7 +91,7 @@ public Rectangle getRect1() { public List getPoints2() { verifyPropertyPresence(POINTS2); //noinspection unchecked - return ((List>) getCommandResult().get(POINTS2)).stream() + return ((List>) getResultAsMap().get(POINTS2)).stream() .map(ComparisonResult::mapToPoint) .collect(Collectors.toList()); } @@ -104,6 +104,6 @@ public List getPoints2() { public Rectangle getRect2() { verifyPropertyPresence(RECT2); //noinspection unchecked - return mapToRect((Map) getCommandResult().get(RECT2)); + return mapToRect((Map) getResultAsMap().get(RECT2)); } } diff --git a/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingOptions.java b/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingOptions.java index d4a7810e6..314a237dc 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingOptions.java +++ b/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingOptions.java @@ -16,12 +16,12 @@ package io.appium.java_client.imagecomparison; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static java.util.Optional.ofNullable; + public class OccurrenceMatchingOptions extends BaseComparisonOptions { private Double threshold; private Boolean multiple; @@ -66,11 +66,10 @@ public OccurrenceMatchingOptions withMatchNeighbourThreshold(int threshold) { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(threshold).map(x -> builder.put("threshold", x)); - ofNullable(matchNeighbourThreshold).map(x -> builder.put("matchNeighbourThreshold", x)); - ofNullable(multiple).map(x -> builder.put("multiple", x)); - return builder.build(); + var map = new HashMap<>(super.build()); + ofNullable(threshold).ifPresent(x -> map.put("threshold", x)); + ofNullable(matchNeighbourThreshold).ifPresent(x -> map.put("matchNeighbourThreshold", x)); + ofNullable(multiple).ifPresent(x -> map.put("multiple", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingResult.java b/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingResult.java index 256a1636a..7b0266f23 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingResult.java +++ b/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingResult.java @@ -18,34 +18,131 @@ import org.openqa.selenium.Rectangle; +import java.io.File; +import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class OccurrenceMatchingResult extends ComparisonResult { private static final String RECT = "rect"; - private static final String MULTIPLE = "multiple"; + private static final String SCORE = "score"; - private final boolean isAtRoot; + private final boolean hasMultiple; - public OccurrenceMatchingResult(Map input) { - this(input, true); + public OccurrenceMatchingResult(Object input) { + super(input); + hasMultiple = input instanceof List; } - private OccurrenceMatchingResult(Map input, boolean isAtRoot) { - super(input); - this.isAtRoot = isAtRoot; + /** + * Check whether the current instance contains multiple matches. + * + * @return True or false. + */ + public boolean hasMultiple() { + return hasMultiple; } /** - * Returns rectangle of partial image occurrence. + * Returns rectangle of the partial image occurrence. * * @return The region of the partial image occurrence on the full image. */ public Rectangle getRect() { + if (hasMultiple) { + return getRect(0); + } verifyPropertyPresence(RECT); //noinspection unchecked - return mapToRect((Map) getCommandResult().get(RECT)); + return mapToRect((Map) getResultAsMap().get(RECT)); + } + + /** + * Returns rectangle of the partial image occurrence for the given match index. + * + * @param matchIndex Match index. + * @return Matching rectangle. + * @throws IllegalStateException If the current instance does not represent multiple matches. + */ + public Rectangle getRect(int matchIndex) { + return getMatch(matchIndex).getRect(); + } + + /** + * Returns the score of the partial image occurrence. + * + * @return Matching score in range 0..1. + */ + public double getScore() { + if (hasMultiple) { + return getScore(0); + } + verifyPropertyPresence(SCORE); + var value = getResultAsMap().get(SCORE); + if (value instanceof Long) { + return ((Long) value).doubleValue(); + } + return (Double) value; + } + + /** + * Returns the score of the partial image occurrence for the given match index. + * + * @param matchIndex Match index. + * @return Matching score in range 0..1. + * @throws IllegalStateException If the current instance does not represent multiple matches. + */ + public double getScore(int matchIndex) { + return getMatch(matchIndex).getScore(); + } + + /** + * Returns the visualization of the matching result. + * + * @return The visualization of the matching result represented as base64-encoded PNG image. + */ + @Override + public byte[] getVisualization() { + return hasMultiple ? getVisualization(0) : super.getVisualization(); + } + + /** + * Returns the visualization of the partial image occurrence for the given match index. + * + * @param matchIndex Match index. + * @return The visualization of the matching result represented as base64-encoded PNG image. + * @throws IllegalStateException If the current instance does not represent multiple matches. + */ + public byte[] getVisualization(int matchIndex) { + return getMatch(matchIndex).getVisualization(); + } + + /** + * Stores visualization image into the given file. + * + * @param destination File path to save the image to. + * @throws IOException On file system I/O error. + */ + @Override + public void storeVisualization(File destination) throws IOException { + if (hasMultiple) { + getMatch(0).storeVisualization(destination); + } else { + super.storeVisualization(destination); + } + } + + /** + * Stores visualization image into the given file. + * + * @param matchIndex Match index. + * @param destination File path to save the image to. + * @throws IOException On file system I/O error. + * @throws IllegalStateException If the current instance does not represent multiple matches. + */ + public void storeVisualization(int matchIndex, File destination) throws IOException { + getMatch(matchIndex).storeVisualization(destination); } /** @@ -54,18 +151,37 @@ public Rectangle getRect() { * * @since Appium 1.21.0 * @return The list containing properties of each single match or an empty list. - * @throws IllegalStateException If the accessor is called on a non-root match instance. + * @throws IllegalStateException If the current instance does not represent multiple matches. */ public List getMultiple() { - if (!isAtRoot) { - throw new IllegalStateException("Only the root match could contain multiple submatches"); - } - verifyPropertyPresence(MULTIPLE); + return getMultipleMatches(false); + } + private List getMultipleMatches(boolean throwIfEmpty) { + if (!hasMultiple) { + throw new IllegalStateException(String.format( + "This %s does not represent multiple matches. Did you set options properly?", + getClass().getSimpleName() + )); + } //noinspection unchecked - List> multiple = (List>) getCommandResult().get(MULTIPLE); - return multiple.stream() - .map((m) -> new OccurrenceMatchingResult(m, false)) + var matches = ((List>) commandResult).stream() + .map(OccurrenceMatchingResult::new) .collect(Collectors.toList()); + if (matches.isEmpty() && throwIfEmpty) { + throw new IllegalStateException("Zero matches have been found. Try the lookup with different options."); + } + return matches; + } + + private OccurrenceMatchingResult getMatch(int index) { + var matches = getMultipleMatches(true); + if (index < 0 || index >= matches.size()) { + throw new IndexOutOfBoundsException(String.format( + "The match #%s does not exist. The total number of found matches is %s", + index, matches.size() + )); + } + return matches.get(index); } } diff --git a/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingResult.java b/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingResult.java index 50c388ead..0806e7b53 100644 --- a/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingResult.java +++ b/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingResult.java @@ -33,10 +33,9 @@ public SimilarityMatchingResult(Map input) { */ public double getScore() { verifyPropertyPresence(SCORE); - //noinspection unchecked - if (getCommandResult().get(SCORE) instanceof Long) { - return ((Long) getCommandResult().get(SCORE)).doubleValue(); + if (getResultAsMap().get(SCORE) instanceof Long) { + return ((Long) getResultAsMap().get(SCORE)).doubleValue(); } - return (double) getCommandResult().get(SCORE); + return (double) getResultAsMap().get(SCORE); } } 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 995385b4b..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; @@ -29,6 +29,9 @@ public class CapabilityHelpers { public static final String APPIUM_PREFIX = "appium:"; + private CapabilityHelpers() { + } + /** * Helper that is used for capability values retrieval. * Supports both prefixed W3C and "classic" capability names. @@ -159,8 +162,8 @@ public static Duration toDuration(Object value, * Converts generic capability value to a url. * * @param value The capability value. - * @throws IllegalArgumentException If the given value cannot be parsed to a valid url. * @return null is the passed value is null otherwise the converted value. + * @throws IllegalArgumentException If the given value cannot be parsed to a valid url. */ @Nullable public static URL toUrl(Object value) { diff --git a/src/main/java/io/appium/java_client/internal/Config.java b/src/main/java/io/appium/java_client/internal/Config.java index fd9ef73c1..4413f28ab 100644 --- a/src/main/java/io/appium/java_client/internal/Config.java +++ b/src/main/java/io/appium/java_client/internal/Config.java @@ -11,7 +11,7 @@ public class Config { private static Config mainInstance = null; private static final String MAIN_CONFIG = "main.properties"; - private static final Map cache = new ConcurrentHashMap<>(); + private static final Map CACHE = new ConcurrentHashMap<>(); private final String configName; /** @@ -58,7 +58,7 @@ public T getValue(String key, Class valueType) { * @throws ClassCastException if the retrieved value cannot be cast to `valueType` type */ public Optional getOptionalValue(String key, Class valueType) { - final Properties cachedProps = cache.computeIfAbsent(configName, (k) -> { + final Properties cachedProps = CACHE.computeIfAbsent(configName, k -> { try (InputStream configFileStream = getClass().getClassLoader().getResourceAsStream(configName)) { final Properties p = new Properties(); p.load(configFileStream); diff --git a/src/main/java/io/appium/java_client/internal/ReflectionHelpers.java b/src/main/java/io/appium/java_client/internal/ReflectionHelpers.java new file mode 100644 index 000000000..dd131fc65 --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/ReflectionHelpers.java @@ -0,0 +1,47 @@ +/* + * 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.internal; + +import org.openqa.selenium.WebDriverException; + +import java.lang.reflect.Field; + +public class ReflectionHelpers { + + private ReflectionHelpers() { + } + + /** + * Sets the given value to a private instance field. + * + * @param cls The target class or a superclass. + * @param target Target instance. + * @param fieldName Target field name. + * @param newValue The value to be set. + * @return The same instance for chaining. + */ + public static T setPrivateFieldValue(Class cls, T target, String fieldName, Object newValue) { + try { + final Field f = cls.getDeclaredField(fieldName); + f.setAccessible(true); + f.set(target, newValue); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new WebDriverException(e); + } + return target; + } +} diff --git a/src/main/java/io/appium/java_client/internal/SessionHelpers.java b/src/main/java/io/appium/java_client/internal/SessionHelpers.java new file mode 100644 index 000000000..51371dbd1 --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/SessionHelpers.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.internal; + +import lombok.Data; +import org.openqa.selenium.InvalidArgumentException; +import org.openqa.selenium.WebDriverException; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SessionHelpers { + private static final Pattern SESSION = Pattern.compile("/session/([^/]+)"); + + private SessionHelpers() { + } + + @Data public static class SessionAddress { + private final URL serverUrl; + private final String id; + } + + /** + * Parses the address of a running remote session. + * + * @param address The address string containing /session/id suffix. + * @return Parsed address object. + * @throws InvalidArgumentException If no session identifier could be parsed. + */ + public static SessionAddress parseSessionAddress(URL address) { + String addressString = address.toString(); + Matcher matcher = SESSION.matcher(addressString); + if (!matcher.find()) { + throw new InvalidArgumentException( + String.format("The server URL '%s' must include /session/ suffix", addressString) + ); + } + try { + return new SessionAddress( + new URL(addressString.replace(matcher.group(), "")), matcher.group(1) + ); + } catch (MalformedURLException 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 new file mode 100644 index 000000000..b075c9b6b --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/filters/AppiumIdempotencyFilter.java @@ -0,0 +1,39 @@ +/* + * 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.internal.filters; + +import org.openqa.selenium.remote.http.Filter; +import org.openqa.selenium.remote.http.HttpHandler; +import org.openqa.selenium.remote.http.HttpMethod; + +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 + private static final String IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key"; + + @Override + public HttpHandler apply(HttpHandler next) { + return req -> { + if (req.getMethod() == HttpMethod.POST && req.getUri().endsWith("/session")) { + 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 new file mode 100644 index 000000000..030666ab6 --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/filters/AppiumUserAgentFilter.java @@ -0,0 +1,91 @@ +/* + * 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.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 static java.util.Locale.ROOT; + +/** + * Manage Appium Client configurations. + */ + +public class AppiumUserAgentFilter implements Filter { + + public static final String VERSION_KEY = "appiumClient.version"; + + private static final String USER_AGENT_PREFIX = "appium/"; + + /** + * A default User Agent name for Appium Java client. + * e.g. appium/8.2.0 (selenium/4.5.0 (java mac)) + */ + public static final String USER_AGENT = buildUserAgentHeaderValue(AddSeleniumUserAgent.USER_AGENT); + + private static String buildUserAgentHeaderValue(@NonNull String previousUA) { + return String.format("%s%s (%s)", + USER_AGENT_PREFIX, Config.main().getValue(VERSION_KEY, String.class), previousUA); + } + + /** + * Returns true if the given User Agent includes "appium/", which + * implies the User Agent already has the Appium UA by this method. + * The matching is case-insensitive. + * @param userAgent the User Agent in the request headers. + * @return whether the given User Agent includes Appium UA + * like by this filter. + */ + private static boolean containsAppiumName(@Nullable String userAgent) { + return userAgent != null && userAgent.toLowerCase(ROOT).contains(USER_AGENT_PREFIX.toLowerCase(ROOT)); + } + + /** + * Returns the User Agent. If the given UA already has + * {@link USER_AGENT_PREFIX}, it returns the UA. + * IF the given UA does not have {@link USER_AGENT_PREFIX}, + * it returns UA with the Appium prefix. + * @param userAgent the User Agent in the request headers. + * @return the User Agent for the request + */ + public static String buildUserAgent(@Nullable String userAgent) { + if (userAgent == null) { + return USER_AGENT; + } + + if (containsAppiumName(userAgent)) { + return userAgent; + } + + return buildUserAgentHeaderValue(userAgent); + } + + @Override + public HttpHandler apply(HttpHandler next) { + return req -> { + var originalUserAgentHeader = req.getHeader(HttpHeader.UserAgent.getName()); + var newUserAgentHeader = buildUserAgent(originalUserAgentHeader); + req.setHeader(HttpHeader.UserAgent.getName(), newUserAgentHeader); + return next.execute(req); + }; + } +} diff --git a/src/main/java/io/appium/java_client/ios/HasIOSClipboard.java b/src/main/java/io/appium/java_client/ios/HasIOSClipboard.java index 538c8cee3..32f4c9df4 100644 --- a/src/main/java/io/appium/java_client/ios/HasIOSClipboard.java +++ b/src/main/java/io/appium/java_client/ios/HasIOSClipboard.java @@ -16,11 +16,10 @@ package io.appium.java_client.ios; -import static com.google.common.base.Preconditions.checkNotNull; - import io.appium.java_client.clipboard.ClipboardContentType; import io.appium.java_client.clipboard.HasClipboard; +import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -29,7 +28,8 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Base64; -import javax.imageio.ImageIO; + +import static java.util.Objects.requireNonNull; public interface HasIOSClipboard extends HasClipboard { /** @@ -40,7 +40,7 @@ public interface HasIOSClipboard extends HasClipboard { */ default void setClipboardImage(BufferedImage img) throws IOException { try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) { - ImageIO.write(checkNotNull(img), "png", os); + ImageIO.write(requireNonNull(img), "png", os); setClipboard(ClipboardContentType.IMAGE, Base64 .getMimeEncoder() .encode(os.toByteArray())); @@ -68,7 +68,7 @@ default BufferedImage getClipboardImage() throws IOException { default void setClipboardUrl(URL url) { setClipboard(ClipboardContentType.URL, Base64 .getMimeEncoder() - .encode(checkNotNull(url).toString().getBytes(StandardCharsets.UTF_8))); + .encode(requireNonNull(url).toString().getBytes(StandardCharsets.UTF_8))); } /** 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 18ecf3065..0fd5cbf20 100644 --- a/src/main/java/io/appium/java_client/ios/IOSDriver.java +++ b/src/main/java/io/appium/java_client/ios/IOSDriver.java @@ -16,24 +16,21 @@ package io.appium.java_client.ios; -import static io.appium.java_client.MobileCommand.prepareArguments; -import static org.openqa.selenium.remote.DriverCommand.EXECUTE_SCRIPT; - -import com.google.common.collect.ImmutableMap; - +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; +import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.HasAppStrings; import io.appium.java_client.HasDeviceTime; import io.appium.java_client.HasOnScreenKeyboard; import io.appium.java_client.HidesKeyboard; import io.appium.java_client.HidesKeyboardWithKeyName; import io.appium.java_client.InteractsWithApps; -import io.appium.java_client.PullsFiles; import io.appium.java_client.LocksDevice; import io.appium.java_client.PerformsTouchActions; +import io.appium.java_client.PullsFiles; import io.appium.java_client.PushesFiles; -import io.appium.java_client.SupportsLegacyAppManagement; import io.appium.java_client.battery.HasBattery; +import io.appium.java_client.remote.AutomationName; import io.appium.java_client.remote.SupportsContextSwitching; import io.appium.java_client.remote.SupportsLocation; import io.appium.java_client.remote.SupportsRotation; @@ -47,12 +44,10 @@ 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; import java.net.URL; -import java.util.Collections; import java.util.Map; /** @@ -66,7 +61,6 @@ public class IOSDriver extends AppiumDriver implements HasDeviceTime, PullsFiles, InteractsWithApps, - SupportsLegacyAppManagement, HasAppStrings, PerformsTouchActions, HidesKeyboardWithKeyName, @@ -192,7 +186,45 @@ public IOSDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities * */ public IOSDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformName(capabilities, PLATFORM_NAME)); + super(AppiumClientConfig.fromClientConfig(clientConfig), + ensurePlatformName(capabilities, PLATFORM_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * XCUITestOptions options = new XCUITestOptions();
+     * IOSDriver driver = new IOSDriver(options, appiumClientConfig);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public IOSDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformName(capabilities, PLATFORM_NAME)); + } + + /** + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! This API is supposed to be used for **debugging purposes only**. + * + * @param remoteSessionAddress The address of the **running** session including the session identifier. + */ + public IOSDriver(URL remoteSessionAddress) { + super(remoteSessionAddress, PLATFORM_NAME, AutomationName.IOS_XCUI_TEST); } /** @@ -208,11 +240,9 @@ public IOSDriver(Capabilities capabilities) { return new InnerTargetLocator(); } - @SuppressWarnings("unchecked") @Override public IOSBatteryInfo getBatteryInfo() { - return new IOSBatteryInfo((Map) execute(EXECUTE_SCRIPT, ImmutableMap.of( - "script", "mobile: batteryInfo", "args", Collections.emptyList())).getValue()); + return new IOSBatteryInfo(CommandExecutionHelper.executeScript(this, "mobile: batteryInfo")); } private class InnerTargetLocator extends RemoteTargetLocator { @@ -243,20 +273,15 @@ class IOSAlert implements Alert { } @Override public void sendKeys(String keysToSend) { - execute(DriverCommand.SET_ALERT_VALUE, prepareArguments("value", keysToSend)); + execute(DriverCommand.SET_ALERT_VALUE, Map.of("value", keysToSend)); } } - @Override - public RemoteLocationContext getLocationContext() { - return locationContext; - } - @Override public synchronized StringWebSocketClient getSyslogClient() { if (syslogClient == null) { - syslogClient = new StringWebSocketClient(); + syslogClient = new StringWebSocketClient(getHttpClient()); } return syslogClient; } diff --git a/src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java b/src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java index d7f6af6b1..ebdddaedc 100644 --- a/src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java +++ b/src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java @@ -16,33 +16,34 @@ package io.appium.java_client.ios; -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.MobileCommand; -import java.util.AbstractMap; import java.util.Map; +@Deprecated public class IOSMobileCommandHelper extends MobileCommand { /** * This method forms a {@link Map} of parameters for the device shaking. * * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated this helper is deprecated and will be removed in future versions. */ - public static Map.Entry> shakeCommand() { - return new AbstractMap.SimpleEntry<>(SHAKE, ImmutableMap.of()); + @Deprecated + public static Map.Entry> shakeCommand() { + return Map.entry(SHAKE, Map.of()); } - + /** * This method forms a {@link Map} of parameters for the touchId simulator. - * + * * @param match If true, simulates a successful fingerprint scan. If false, simulates a failed fingerprint scan. * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated this helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> touchIdCommand(boolean match) { - return new AbstractMap.SimpleEntry<>( - TOUCH_ID, prepareArguments("match", match)); + return Map.entry(TOUCH_ID, Map.of("match", match)); } /** @@ -51,9 +52,10 @@ public class IOSMobileCommandHelper extends MobileCommand { * * @param enabled Whether to enable or disable Touch ID Enrollment for Simulator. * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + * @deprecated this helper is deprecated and will be removed in future versions. */ + @Deprecated public static Map.Entry> toggleTouchIdEnrollmentCommand(boolean enabled) { - return new AbstractMap.SimpleEntry<>( - TOUCH_ID_ENROLLMENT, prepareArguments("enabled", enabled)); + return Map.entry(TOUCH_ID_ENROLLMENT, Map.of("enabled", enabled)); } } 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 d9b918977..5c56cd9a5 100644 --- a/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java +++ b/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java @@ -16,17 +16,18 @@ package io.appium.java_client.ios; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; import io.appium.java_client.screenrecording.ScreenRecordingUploadOptions; import java.time.Duration; +import java.util.Collections; +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; + public class IOSStartScreenRecordingOptions extends BaseStartScreenRecordingOptions { private String videoType; @@ -57,7 +58,7 @@ public IOSStartScreenRecordingOptions withUploadOptions(ScreenRecordingUploadOpt * @return self instance for chaining. */ public IOSStartScreenRecordingOptions withVideoType(String videoType) { - this.videoType = checkNotNull(videoType); + this.videoType = requireNonNull(videoType); return this; } @@ -73,7 +74,7 @@ public enum VideoQuality { * @return self instance for chaining. */ public IOSStartScreenRecordingOptions withVideoQuality(VideoQuality videoQuality) { - this.videoQuality = checkNotNull(videoQuality).name().toLowerCase(); + this.videoQuality = requireNonNull(videoQuality).name().toLowerCase(ROOT); return this; } @@ -99,7 +100,7 @@ public IOSStartScreenRecordingOptions withFps(int fps) { * @return self instance for chaining. */ public IOSStartScreenRecordingOptions withVideoScale(String videoScale) { - this.videoScale = checkNotNull(videoScale); + this.videoScale = requireNonNull(videoScale); return this; } @@ -134,13 +135,12 @@ public IOSStartScreenRecordingOptions withVideoFilters(String filters) { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(videoType).map(x -> builder.put("videoType", x)); - ofNullable(videoQuality).map(x -> builder.put("videoQuality", x)); - ofNullable(videoScale).map(x -> builder.put("videoScale", x)); - ofNullable(videoFilters).map(x -> builder.put("videoFilters", x)); - ofNullable(fps).map(x -> builder.put("videoFps", x)); - return builder.build(); + var map = new HashMap<>(super.build()); + ofNullable(videoType).map(x -> map.put("videoType", x)); + ofNullable(videoQuality).map(x -> map.put("videoQuality", x)); + ofNullable(videoScale).map(x -> map.put("videoScale", x)); + ofNullable(videoFilters).map(x -> map.put("videoFilters", x)); + ofNullable(fps).map(x -> map.put("videoFps", x)); + return Collections.unmodifiableMap(map); } } 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 01a98f9e5..aca87955d 100644 --- a/src/main/java/io/appium/java_client/ios/IOSTouchAction.java +++ b/src/main/java/io/appium/java_client/ios/IOSTouchAction.java @@ -48,9 +48,7 @@ public IOSTouchAction(PerformsTouchActions performsTouchActions) { * @return self-reference */ public IOSTouchAction doubleTap(PointOption doubleTapOption) { - ActionParameter action = new ActionParameter("doubleTap", - doubleTapOption); - parameterBuilder.add(action); + parameters.add(new ActionParameter("doubleTap", doubleTapOption)); return this; } @@ -61,7 +59,7 @@ public IOSTouchAction doubleTap(PointOption doubleTapOption) { * @return this TouchAction, for chaining. */ public IOSTouchAction press(IOSPressOptions pressOptions) { - parameterBuilder.add(new ActionParameter("press", pressOptions)); + parameters.add(new ActionParameter("press", pressOptions)); return this; } } diff --git a/src/main/java/io/appium/java_client/ios/ListensToSyslogMessages.java b/src/main/java/io/appium/java_client/ios/ListensToSyslogMessages.java index 6bb15821c..98a75158a 100644 --- a/src/main/java/io/appium/java_client/ios/ListensToSyslogMessages.java +++ b/src/main/java/io/appium/java_client/ios/ListensToSyslogMessages.java @@ -16,20 +16,19 @@ package io.appium.java_client.ios; -import static io.appium.java_client.service.local.AppiumServiceBuilder.DEFAULT_APPIUM_PORT; -import static org.openqa.selenium.remote.DriverCommand.EXECUTE_SCRIPT; - -import com.google.common.collect.ImmutableMap; - +import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; import io.appium.java_client.ws.StringWebSocketClient; +import org.openqa.selenium.remote.HttpCommandExecutor; import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.SessionId; import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; +import java.net.URL; import java.util.function.Consumer; +import static io.appium.java_client.service.local.AppiumServiceBuilder.DEFAULT_APPIUM_PORT; + public interface ListensToSyslogMessages extends ExecutesMethod { StringWebSocketClient getSyslogClient(); @@ -40,7 +39,7 @@ public interface ListensToSyslogMessages extends ExecutesMethod { * is assigned to the default port (4723). */ default void startSyslogBroadcast() { - startSyslogBroadcast("localhost", DEFAULT_APPIUM_PORT); + startSyslogBroadcast("localhost"); } /** @@ -60,16 +59,13 @@ default void startSyslogBroadcast(String host) { * @param port the port of the host where Appium server is running */ default void startSyslogBroadcast(String host, int port) { - execute(EXECUTE_SCRIPT, ImmutableMap.of("script", "mobile: startLogsBroadcast", - "args", Collections.emptyList())); - final URI endpointUri; - try { - endpointUri = new URI(String.format("ws://%s:%s/ws/session/%s/appium/device/syslog", - host, port, ((RemoteWebDriver) this).getSessionId())); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - getSyslogClient().connect(endpointUri); + var remoteWebDriver = (RemoteWebDriver) this; + URL serverUrl = ((HttpCommandExecutor) remoteWebDriver.getCommandExecutor()).getAddressOfRemoteServer(); + var scheme = "https".equals(serverUrl.getProtocol()) ? "wss" : "ws"; + CommandExecutionHelper.executeScript(this, "mobile: startLogsBroadcast"); + SessionId sessionId = remoteWebDriver.getSessionId(); + var endpoint = String.format("%s://%s:%s/ws/session/%s/appium/device/syslog", scheme, host, port, sessionId); + getSyslogClient().connect(URI.create(endpoint)); } /** @@ -133,7 +129,6 @@ default void removeAllSyslogListeners() { * Stops syslog messages broadcast via web socket. */ default void stopSyslogBroadcast() { - execute(EXECUTE_SCRIPT, ImmutableMap.of("script", "mobile: stopLogsBroadcast", - "args", Collections.emptyList())); + CommandExecutionHelper.executeScript(this, "mobile: stopLogsBroadcast"); } } diff --git a/src/main/java/io/appium/java_client/ios/PerformsTouchID.java b/src/main/java/io/appium/java_client/ios/PerformsTouchID.java index 90e088e93..5829808bd 100644 --- a/src/main/java/io/appium/java_client/ios/PerformsTouchID.java +++ b/src/main/java/io/appium/java_client/ios/PerformsTouchID.java @@ -16,34 +16,37 @@ package io.appium.java_client.ios; -import static io.appium.java_client.ios.IOSMobileCommandHelper.toggleTouchIdEnrollmentCommand; -import static io.appium.java_client.ios.IOSMobileCommandHelper.touchIdCommand; - import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import java.util.Map; + public interface PerformsTouchID extends ExecutesMethod { /** - * Simulate touchId event. + * Simulate touchId event on iOS Simulator. Check the documentation on 'mobile: sendBiometricMatch' + * extension for more details. * * @param match If true, simulates a successful fingerprint scan. If false, simulates a failed fingerprint scan. */ default void performTouchID(boolean match) { - CommandExecutionHelper.execute(this, touchIdCommand(match)); + CommandExecutionHelper.executeScript(this, "mobile: sendBiometricMatch", Map.of( + "type", "touchId", + "match", match + )); } /** - * Enrolls touchId in iOS Simulators. This call will only work if Appium process or its - * parent application (e.g. Terminal.app or Appium.app) has - * access to Mac OS accessibility in System Preferences > - * Security & Privacy > Privacy > Accessibility list. + * Enrolls touchId in iOS Simulator. Check the documentation on 'mobile: enrollBiometric' + * extension for more details. * * @param enabled Whether to enable or disable Touch ID Enrollment. The actual state of the feature * will only be changed if the current value is different from the previous one. * Multiple calls of the method with the same argument value have no effect. */ default void toggleTouchIDEnrollment(boolean enabled) { - CommandExecutionHelper.execute(this, toggleTouchIdEnrollmentCommand(enabled)); + CommandExecutionHelper.executeScript(this, "mobile: enrollBiometric", Map.of( + "isEnabled", enabled + )); } } diff --git a/src/main/java/io/appium/java_client/ios/ShakesDevice.java b/src/main/java/io/appium/java_client/ios/ShakesDevice.java index 208f05bb1..57302ef8a 100644 --- a/src/main/java/io/appium/java_client/ios/ShakesDevice.java +++ b/src/main/java/io/appium/java_client/ios/ShakesDevice.java @@ -16,17 +16,25 @@ package io.appium.java_client.ios; -import static io.appium.java_client.ios.IOSMobileCommandHelper.shakeCommand; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import static io.appium.java_client.ios.IOSMobileCommandHelper.shakeCommand; -public interface ShakesDevice extends ExecutesMethod { +public interface ShakesDevice extends ExecutesMethod, CanRememberExtensionPresence { /** - * Simulate shaking the device. + * Simulate shaking the Simulator. This API does not work for real devices. */ default void shake() { - CommandExecutionHelper.execute(this, shakeCommand()); + final String extName = "mobile: shake"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), shakeCommand()); + } } } 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 2eb61386d..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; @@ -106,6 +108,7 @@ import io.appium.java_client.remote.options.SupportsClearSystemFilesOption; import io.appium.java_client.remote.options.SupportsDeviceNameOption; import io.appium.java_client.remote.options.SupportsEnablePerformanceLoggingOption; +import io.appium.java_client.remote.options.SupportsEnforceAppInstallOption; import io.appium.java_client.remote.options.SupportsIsHeadlessOption; import io.appium.java_client.remote.options.SupportsLanguageOption; import io.appium.java_client.remote.options.SupportsLocaleOption; @@ -118,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 @@ -136,6 +142,7 @@ public class XCUITestOptions extends BaseOptions implements SupportsOtherAppsOption, SupportsAppPushTimeoutOption, SupportsAppInstallStrategyOption, + SupportsEnforceAppInstallOption, // WebDriverAgent options: https://github.com/appium/appium-xcuitest-driver#webdriveragent SupportsXcodeCertificateOptions, SupportsKeychainOptions, @@ -151,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/other/SupportsCommandTimeoutsOption.java b/src/main/java/io/appium/java_client/ios/options/other/SupportsCommandTimeoutsOption.java index 0afd1a9a8..d19e6272f 100644 --- a/src/main/java/io/appium/java_client/ios/options/other/SupportsCommandTimeoutsOption.java +++ b/src/main/java/io/appium/java_client/ios/options/other/SupportsCommandTimeoutsOption.java @@ -62,7 +62,7 @@ default T setCommandTimeouts(Duration timeout) { default Optional> getCommandTimeouts() { return Optional.ofNullable(getCapability(COMMAND_TIMEOUTS_OPTION)) .map(String::valueOf) - .map((v) -> v.trim().startsWith("{") + .map(v -> v.trim().startsWith("{") ? Either.left(new CommandTimeouts(v)) : Either.right(toDuration(v)) ); diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCustomSslCertOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCustomSslCertOption.java index 93611eec1..af501a76e 100644 --- a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCustomSslCertOption.java +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCustomSslCertOption.java @@ -42,7 +42,7 @@ default T setCustomSSLCert(String cert) { * * @return Certificate content. */ - default Optional setCustomSSLCert() { + default Optional getCustomSSLCert() { return Optional.ofNullable((String) getCapability(CUSTOM_SSLCERT_OPTION)); } } diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsPermissionsOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsPermissionsOption.java index e3a4b36c2..3f55a335c 100644 --- a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsPermissionsOption.java +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsPermissionsOption.java @@ -44,6 +44,6 @@ default T setPermissions(Permissions permissions) { */ default Optional getPermissions() { return Optional.ofNullable(getCapability(PERMISSIONS_OPTION)) - .map((v) -> new Permissions(String.valueOf(v))); + .map(v -> new Permissions(String.valueOf(v))); } } 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 7f99af2e0..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/simulator/SupportsSimulatorTracePointerOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorTracePointerOption.java index a0341725a..d7a1d115e 100644 --- a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorTracePointerOption.java +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorTracePointerOption.java @@ -55,7 +55,7 @@ default T setSimulatorTracePointer(boolean value) { * * @return True or false. */ - default Optional doesSimulatorTracePointerd() { + default Optional doesSimulatorTracePointer() { return Optional.ofNullable(toSafeBoolean(getCapability(SIMULATOR_TRACE_POINTER_OPTION))); } } diff --git a/src/main/java/io/appium/java_client/ios/options/wda/ProcessArguments.java b/src/main/java/io/appium/java_client/ios/options/wda/ProcessArguments.java index c5ed74d22..08b06f9f8 100644 --- a/src/main/java/io/appium/java_client/ios/options/wda/ProcessArguments.java +++ b/src/main/java/io/appium/java_client/ios/options/wda/ProcessArguments.java @@ -49,8 +49,8 @@ public ProcessArguments(Map env) { */ public Map toMap() { Map result = new HashMap<>(); - Optional.ofNullable(args).ifPresent((v) -> result.put("args", v)); - Optional.ofNullable(env).ifPresent((v) -> result.put("env", v)); + Optional.ofNullable(args).ifPresent(v -> result.put("args", v)); + Optional.ofNullable(env).ifPresent(v -> result.put("env", v)); return Collections.unmodifiableMap(result); } } 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/ios/options/wda/SupportsWaitForIdleTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWaitForIdleTimeoutOption.java index 0d9ffda35..f9dd2401a 100644 --- a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWaitForIdleTimeoutOption.java +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWaitForIdleTimeoutOption.java @@ -53,6 +53,6 @@ default T setWaitForIdleTimeout(Duration timeout) { default Optional getWaitForIdleTimeout() { return Optional.ofNullable(getCapability(WAIT_FOR_IDLE_TIMEOUT_OPTION)) .map(CapabilityHelpers::toDouble) - .map((d) -> toDuration((long) (d * 1000.0))); + .map(d -> toDuration((long) (d * 1000.0))); } } diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaEventloopIdleDelayOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaEventloopIdleDelayOption.java index 4d35bb5c4..3d48703b5 100644 --- a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaEventloopIdleDelayOption.java +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaEventloopIdleDelayOption.java @@ -54,6 +54,6 @@ default T setWdaEventloopIdleDelay(Duration duration) { default Optional getWdaEventloopIdleDelay() { return Optional.ofNullable(getCapability(WDA_EVENTLOOP_IDLE_DELAY_OPTION)) .map(CapabilityHelpers::toDouble) - .map((d) -> toDuration((long) (d * 1000.0))); + .map(d -> toDuration((long) (d * 1000.0))); } } diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsXcodeCertificateOptions.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsXcodeCertificateOptions.java index fcecbab8c..45d437195 100644 --- a/src/main/java/io/appium/java_client/ios/options/wda/SupportsXcodeCertificateOptions.java +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsXcodeCertificateOptions.java @@ -51,6 +51,6 @@ default Optional getXcodeCertificate() { String orgId = (String) getCapability(XCODE_ORG_ID_OPTION); String signingId = (String) getCapability(XCODE_SIGNING_ID_OPTION); return Optional.ofNullable(orgId) - .map((x) -> new XcodeCertificate(orgId, signingId)); + .map(x -> new XcodeCertificate(orgId, signingId)); } } diff --git a/src/main/java/io/appium/java_client/ios/touch/IOSPressOptions.java b/src/main/java/io/appium/java_client/ios/touch/IOSPressOptions.java index 0720bd325..a6b8cdf7f 100644 --- a/src/main/java/io/appium/java_client/ios/touch/IOSPressOptions.java +++ b/src/main/java/io/appium/java_client/ios/touch/IOSPressOptions.java @@ -16,12 +16,12 @@ package io.appium.java_client.ios.touch; -import static java.util.Optional.ofNullable; - import io.appium.java_client.touch.offset.AbstractOptionCombinedWithPosition; import java.util.Map; +import static java.util.Optional.ofNullable; + public class IOSPressOptions extends AbstractOptionCombinedWithPosition { private Double pressure = null; diff --git a/src/main/java/io/appium/java_client/mac/Mac2Driver.java b/src/main/java/io/appium/java_client/mac/Mac2Driver.java index b76de5943..46911c314 100644 --- a/src/main/java/io/appium/java_client/mac/Mac2Driver.java +++ b/src/main/java/io/appium/java_client/mac/Mac2Driver.java @@ -16,6 +16,7 @@ package io.appium.java_client.mac; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.PerformsTouchActions; import io.appium.java_client.remote.AutomationName; @@ -85,6 +86,19 @@ public Mac2Driver(HttpClient.Factory httpClientFactory, Capabilities capabilitie capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } + /** + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! This API is supposed to be used for **debugging purposes only**. + * + * @param remoteSessionAddress The address of the **running** session including the session identifier. + */ + public Mac2Driver(URL remoteSessionAddress) { + super(remoteSessionAddress, PLATFORM_NAME, AUTOMATION_NAME); + } + /** * Creates a new instance based on the given ClientConfig and {@code capabilities}. * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. @@ -105,7 +119,32 @@ public Mac2Driver(HttpClient.Factory httpClientFactory, Capabilities capabilitie * */ public Mac2Driver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformAndAutomationNames( + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * Mac2Options options = new Mac2Options();
+     * Mac2Driver driver = new Mac2Driver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public Mac2Driver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformAndAutomationNames( capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } diff --git a/src/main/java/io/appium/java_client/mac/Mac2StartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/mac/Mac2StartScreenRecordingOptions.java index 45c573126..342598b62 100644 --- a/src/main/java/io/appium/java_client/mac/Mac2StartScreenRecordingOptions.java +++ b/src/main/java/io/appium/java_client/mac/Mac2StartScreenRecordingOptions.java @@ -16,10 +16,11 @@ package io.appium.java_client.mac; -import com.google.common.collect.ImmutableMap; import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import static java.util.Optional.ofNullable; @@ -139,14 +140,13 @@ public Mac2StartScreenRecordingOptions withTimeLimit(Duration timeLimit) { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(fps).map(x -> builder.put("fps", x)); - ofNullable(preset).map(x -> builder.put("preset", x)); - ofNullable(videoFilter).map(x -> builder.put("videoFilter", x)); - ofNullable(captureClicks).map(x -> builder.put("captureClicks", x)); - ofNullable(captureCursor).map(x -> builder.put("captureCursor", x)); - ofNullable(deviceId).map(x -> builder.put("deviceId", x)); - return builder.build(); + var map = new HashMap<>(super.build()); + ofNullable(fps).map(x -> map.put("fps", x)); + ofNullable(preset).map(x -> map.put("preset", x)); + ofNullable(videoFilter).map(x -> map.put("videoFilter", x)); + ofNullable(captureClicks).map(x -> map.put("captureClicks", x)); + ofNullable(captureCursor).map(x -> map.put("captureCursor", x)); + ofNullable(deviceId).map(x -> map.put("deviceId", x)); + return Collections.unmodifiableMap(map); } } 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 37e31575a..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, @@ -83,7 +86,7 @@ public Mac2Options setPrerun(AppleScriptData script) { public Optional getPrerun() { //noinspection unchecked return Optional.ofNullable(getCapability(PRERUN_OPTION)) - .map((v) -> new AppleScriptData((Map) v)); + .map(v -> new AppleScriptData((Map) v)); } /** @@ -108,6 +111,6 @@ public Mac2Options setPostrun(AppleScriptData script) { public Optional getPostrun() { //noinspection unchecked return Optional.ofNullable(getCapability(POSTRUN_OPTION)) - .map((v) -> new AppleScriptData((Map) v)); + .map(v -> new AppleScriptData((Map) v)); } } diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java index 69fe8c1e0..63deca31f 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java @@ -16,14 +16,14 @@ package io.appium.java_client.pagefactory; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Used to mark a field on a Page/Screen Object to indicate that lookup should use a * series of {@link AndroidBy} tags diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java index 25b37c0b6..aa245d971 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java @@ -16,14 +16,14 @@ package io.appium.java_client.pagefactory; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Used to mark a field on a Page Object to indicate an alternative mechanism for locating the diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindByAllSet.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindByAllSet.java index ab6afda28..fed03d755 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindByAllSet.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindByAllSet.java @@ -1,12 +1,12 @@ package io.appium.java_client.pagefactory; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - /** * Defines set of chained/possible locators. Each one locator * should be defined with {@link AndroidFindAll} diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindByChainSet.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindByChainSet.java index 365146650..3fda1f27a 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindByChainSet.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindByChainSet.java @@ -1,12 +1,12 @@ package io.appium.java_client.pagefactory; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - /** * Defines set of chained/possible locators. Each one locator * should be defined with {@link io.appium.java_client.pagefactory.AndroidFindBys} diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBySet.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBySet.java index 6e35ebccd..9b1f62519 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBySet.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBySet.java @@ -16,13 +16,13 @@ package io.appium.java_client.pagefactory; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - /** * Defines set of chained/possible locators. Each one locator * should be defined with {@link io.appium.java_client.pagefactory.AndroidFindBy} diff --git a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBys.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBys.java index eabfeb7c2..db278a9ce 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidFindBys.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBys.java @@ -16,14 +16,14 @@ package io.appium.java_client.pagefactory; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Used to mark a field on a Page Object to indicate that lookup should use * a series of {@link io.appium.java_client.pagefactory.AndroidBy} tags. diff --git a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java index b8b0fbe8f..9e148a2c7 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java @@ -16,15 +16,8 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; -import static io.appium.java_client.pagefactory.ThrowableUtil.isInvalidSelectorRootCause; -import static io.appium.java_client.pagefactory.ThrowableUtil.isStaleElementReferenceException; -import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType; -import static java.lang.String.format; - import io.appium.java_client.pagefactory.bys.ContentMappedBy; import io.appium.java_client.pagefactory.locator.CacheableLocator; - import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; @@ -34,23 +27,57 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.FluentWait; +import java.lang.ref.WeakReference; import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; +import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; +import static io.appium.java_client.pagefactory.ThrowableUtil.isInvalidSelectorRootCause; +import static io.appium.java_client.pagefactory.ThrowableUtil.isStaleElementReferenceException; +import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType; +import static java.lang.String.format; + class AppiumElementLocator implements CacheableLocator { - private static final String exceptionMessageIfElementNotFound = "Can't locate an element by this strategy: %s"; + private static final String EXCEPTION_MESSAGE_IF_ELEMENT_NOT_FOUND = "Can't locate an element by this strategy: %s"; private final boolean shouldCache; private final By by; private final Duration duration; + private final WeakReference searchContextReference; private final SearchContext searchContext; + private WebElement cachedElement; private List cachedElementList; + /** + * Creates a new mobile element locator. It instantiates {@link WebElement} + * using @AndroidFindBy (-s), @iOSFindBy (-s) and @FindBy (-s) annotation + * sets + * + * @param searchContextReference The context reference to use when finding the element + * @param by a By locator strategy + * @param shouldCache is the flag that signalizes that elements which + * are found once should be cached + * @param duration timeout parameter for the element to be found + */ + AppiumElementLocator( + WeakReference searchContextReference, + By by, + boolean shouldCache, + Duration duration + ) { + this.searchContextReference = searchContextReference; + this.searchContext = null; + this.shouldCache = shouldCache; + this.duration = duration; + this.by = by; + } + /** * Creates a new mobile element locator. It instantiates {@link WebElement} * using @AndroidFindBy (-s), @iOSFindBy (-s) and @FindBy (-s) annotation @@ -62,15 +89,25 @@ class AppiumElementLocator implements CacheableLocator { * are found once should be cached * @param duration timeout parameter for the element to be found */ - - public AppiumElementLocator(SearchContext searchContext, By by, boolean shouldCache, - Duration duration) { + public AppiumElementLocator( + SearchContext searchContext, + By by, + boolean shouldCache, + Duration duration + ) { + this.searchContextReference = null; this.searchContext = searchContext; this.shouldCache = shouldCache; this.duration = duration; this.by = by; } + private Optional getSearchContext() { + return searchContext == null + ? Optional.ofNullable(searchContextReference).map(WeakReference::get) + : Optional.of(searchContext); + } + /** * This methods makes sets some settings of the {@link By} according to * the given instance of {@link SearchContext}. If there is some {@link ContentMappedBy} @@ -86,8 +123,7 @@ private static By getBy(By currentBy, SearchContext currentContent) { return currentBy; } - return ContentMappedBy.class.cast(currentBy) - .useContent(getCurrentContentType(currentContent)); + return ((ContentMappedBy) currentBy).useContent(getCurrentContentType(currentContent)); } private T waitFor(Supplier supplier) { @@ -99,8 +135,7 @@ private T waitFor(Supplier supplier) { return wait.until(function); } catch (TimeoutException e) { if (function.foundStaleElementReferenceException != null) { - throw StaleElementReferenceException - .class.cast(function.foundStaleElementReferenceException); + throw (StaleElementReferenceException) function.foundStaleElementReferenceException; } throw e; } @@ -114,16 +149,21 @@ public WebElement findElement() { return cachedElement; } + SearchContext searchContext = getSearchContext() + .orElseThrow(() -> new IllegalStateException( + String.format("The element %s is not locatable anymore " + + "because its context has been garbage collected", by) + )); + By bySearching = getBy(this.by, searchContext); try { - WebElement result = waitFor(() -> - searchContext.findElement(bySearching)); + WebElement result = waitFor(() -> searchContext.findElement(bySearching)); if (shouldCache) { cachedElement = result; } return result; } catch (TimeoutException | StaleElementReferenceException e) { - throw new NoSuchElementException(format(exceptionMessageIfElementNotFound, bySearching.toString()), e); + throw new NoSuchElementException(format(EXCEPTION_MESSAGE_IF_ELEMENT_NOT_FOUND, bySearching.toString()), e); } } @@ -135,12 +175,17 @@ public List findElements() { return cachedElementList; } + SearchContext searchContext = getSearchContext() + .orElseThrow(() -> new IllegalStateException( + String.format("Elements %s are not locatable anymore " + + "because their context has been garbage collected", by) + )); + List result; try { result = waitFor(() -> { - List list = searchContext - .findElements(getBy(by, searchContext)); - return list.size() > 0 ? list : null; + List list = searchContext.findElements(getBy(by, searchContext)); + return list.isEmpty() ? null : list; }); } catch (TimeoutException | StaleElementReferenceException e) { result = new ArrayList<>(); @@ -172,30 +217,22 @@ public T apply(Supplier supplier) { return supplier.get(); } catch (Throwable e) { boolean isRootCauseStaleElementReferenceException = false; - Throwable shouldBeThrown; boolean isRootCauseInvalidSelector = isInvalidSelectorRootCause(e); - if (!isRootCauseInvalidSelector) { isRootCauseStaleElementReferenceException = isStaleElementReferenceException(e); } - if (isRootCauseStaleElementReferenceException) { foundStaleElementReferenceException = extractReadableException(e); } + if (isRootCauseInvalidSelector || isRootCauseStaleElementReferenceException) { + return null; + } - if (!isRootCauseInvalidSelector & !isRootCauseStaleElementReferenceException) { - shouldBeThrown = extractReadableException(e); - if (shouldBeThrown != null) { - if (NoSuchElementException.class.equals(shouldBeThrown.getClass())) { - throw NoSuchElementException.class.cast(shouldBeThrown); - } else { - throw new WebDriverException(shouldBeThrown); - } - } else { - throw new WebDriverException(e); - } + Throwable excToThrow = extractReadableException(e); + if (excToThrow instanceof WebDriverException) { + throw (WebDriverException) excToThrow; } else { - return null; + throw new WebDriverException(excToThrow); } } } 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 179ecbcc4..f423d1dca 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java @@ -16,22 +16,23 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.WithTimeout.DurationBuilder.build; -import static java.util.Optional.ofNullable; - 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.openqa.selenium.By; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.SearchContext; +import java.lang.ref.WeakReference; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.time.Duration; -import javax.annotation.Nullable; + +import static io.appium.java_client.pagefactory.WithTimeout.DurationBuilder.build; +import static java.util.Optional.ofNullable; public class AppiumElementLocatorFactory implements CacheableElementLocatorFactory { private final SearchContext searchContext; + private final WeakReference searchContextReference; private final Duration duration; private final AppiumByBuilder builder; @@ -39,21 +40,47 @@ public class AppiumElementLocatorFactory implements CacheableElementLocatorFacto * Creates a new mobile element locator factory. * * @param searchContext The context to use when finding the element - * @param duration timeout parameters for the elements to be found - * @param builder is handler of Appium-specific page object annotations + * @param duration timeout parameters for the elements to be found + * @param builder is handler of Appium-specific page object annotations */ - public AppiumElementLocatorFactory(SearchContext searchContext, Duration duration, - AppiumByBuilder builder) { + public AppiumElementLocatorFactory( + SearchContext searchContext, + Duration duration, + AppiumByBuilder builder + ) { this.searchContext = searchContext; + this.searchContextReference = null; this.duration = duration; this.builder = builder; } - public @Nullable CacheableLocator createLocator(Field field) { + /** + * Creates a new mobile element locator factory. + * + * @param searchContextReference The context reference to use when finding the element + * @param duration timeout parameters for the elements to be found + * @param builder is handler of Appium-specific page object annotations + */ + AppiumElementLocatorFactory( + WeakReference searchContextReference, + Duration duration, + AppiumByBuilder builder + ) { + this.searchContextReference = searchContextReference; + this.searchContext = null; + this.duration = duration; + this.builder = builder; + } + + @Nullable + @Override + public CacheableLocator createLocator(Field field) { return this.createLocator((AnnotatedElement) field); } - @Override public @Nullable CacheableLocator createLocator(AnnotatedElement annotatedElement) { + @Nullable + @Override + public CacheableLocator createLocator(AnnotatedElement annotatedElement) { Duration customDuration; if (annotatedElement.isAnnotationPresent(WithTimeout.class)) { WithTimeout withTimeout = annotatedElement.getAnnotation(WithTimeout.class); @@ -61,14 +88,19 @@ public AppiumElementLocatorFactory(SearchContext searchContext, Duration duratio } else { customDuration = duration; } - builder.setAnnotated(annotatedElement); - By byResult = builder.buildBy(); - - return ofNullable(byResult) - .map(by -> new AppiumElementLocator(searchContext, by, builder.isLookupCached(), customDuration)) - .orElse(null); + try { + return ofNullable(builder.buildBy()) + .map(by -> { + var isLookupCached = builder.isLookupCached(); + return searchContextReference != null + ? new AppiumElementLocator(searchContextReference, by, isLookupCached, customDuration) + : new AppiumElementLocator(searchContext, by, isLookupCached, customDuration); + }) + .orElse(null); + } finally { + // unleak element reference after the locator is built + builder.setAnnotated(null); + } } - - } 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 ea3b35e5a..792932cd4 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumFieldDecorator.java @@ -16,17 +16,11 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedProxy; -import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.unpackWebDriverFromSearchContext; -import static java.time.Duration.ofSeconds; - -import com.google.common.collect.ImmutableList; - import io.appium.java_client.internal.CapabilityHelpers; import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.locator.CacheableLocator; -import io.appium.java_client.remote.MobileCapabilityType; -import org.openqa.selenium.Capabilities; +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; @@ -35,8 +29,10 @@ import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.support.pagefactory.DefaultFieldDecorator; import org.openqa.selenium.support.pagefactory.ElementLocator; +import org.openqa.selenium.support.pagefactory.ElementLocatorFactory; import org.openqa.selenium.support.pagefactory.FieldDecorator; +import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; @@ -49,6 +45,12 @@ import java.util.List; import java.util.Map; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedProxy; +import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.unpackObjectFromSearchContext; +import static io.appium.java_client.remote.options.SupportsAutomationNameOption.AUTOMATION_NAME_OPTION; +import static java.time.Duration.ofSeconds; + /** * Default decorator for use with PageFactory. Will decorate 1) all of the * WebElement fields and 2) List of WebElement that have @@ -60,11 +62,13 @@ */ public class AppiumFieldDecorator implements FieldDecorator { - private static final List> availableElementClasses = ImmutableList.of(WebElement.class, - RemoteWebElement.class); + private static final List> AVAILABLE_ELEMENT_CLASSES = List.of( + WebElement.class, + RemoteWebElement.class + ); public static final Duration DEFAULT_WAITING_TIMEOUT = ofSeconds(1); - private final WebDriver webDriver; - private final DefaultFieldDecorator defaultElementFieldDecoracor; + private final WeakReference webDriverReference; + private final DefaultFieldDecorator defaultElementFieldDecorator; private final AppiumElementLocatorFactory widgetLocatorFactory; private final String platform; private final String automation; @@ -79,31 +83,84 @@ public class AppiumFieldDecorator implements FieldDecorator { * @param duration is a desired duration of the waiting for an element presence. */ public AppiumFieldDecorator(SearchContext context, Duration duration) { - this.webDriver = unpackWebDriverFromSearchContext(context); + this.webDriverReference = requireWebDriverReference(context); + this.platform = readStringCapability(context, CapabilityType.PLATFORM_NAME); + this.automation = readStringCapability(context, AUTOMATION_NAME_OPTION); + this.duration = duration; - if (this.webDriver instanceof HasCapabilities) { - Capabilities caps = ((HasCapabilities) this.webDriver).getCapabilities(); - this.platform = CapabilityHelpers.getCapability(caps, CapabilityType.PLATFORM_NAME, String.class); - this.automation = CapabilityHelpers.getCapability(caps, MobileCapabilityType.AUTOMATION_NAME, String.class); - } else { - this.platform = null; - this.automation = null; - } + defaultElementFieldDecorator = createFieldDecorator(new AppiumElementLocatorFactory( + context, duration, new DefaultElementByBuilder(platform, automation) + )); + widgetLocatorFactory = new AppiumElementLocatorFactory( + context, duration, new WidgetByBuilder(platform, automation) + ); + } + public AppiumFieldDecorator(SearchContext context) { + this(context, DEFAULT_WAITING_TIMEOUT); + } + + /** + * Creates field decorator based on {@link SearchContext} and timeout {@code duration}. + * + * @param contextReference reference to {@link SearchContext} + * It may be the instance of {@link WebDriver} or {@link WebElement} or + * {@link Widget} or some other user's extension/implementation. + * @param duration is a desired duration of the waiting for an element presence. + */ + AppiumFieldDecorator(WeakReference contextReference, Duration duration) { + var cr = contextReference.get(); + this.webDriverReference = requireWebDriverReference(cr); + this.platform = readStringCapability(cr, CapabilityType.PLATFORM_NAME); + this.automation = readStringCapability(cr, AUTOMATION_NAME_OPTION); this.duration = duration; - defaultElementFieldDecoracor = new DefaultFieldDecorator( - new AppiumElementLocatorFactory(context, duration, - new DefaultElementByBuilder(platform, automation))) { + defaultElementFieldDecorator = createFieldDecorator(new AppiumElementLocatorFactory( + contextReference, duration, new DefaultElementByBuilder(platform, automation) + )); + widgetLocatorFactory = new AppiumElementLocatorFactory( + contextReference, duration, new WidgetByBuilder(platform, automation) + ); + } + + @NonNull + private static WeakReference requireWebDriverReference(SearchContext searchContext) { + var wd = unpackObjectFromSearchContext( + checkNotNull(searchContext, "The provided search context cannot be null"), + WebDriver.class + ); + return wd.map(WeakReference::new) + .orElseThrow(() -> new IllegalArgumentException( + String.format( + "No driver implementing %s interface could be extracted from the %s instance. " + + "Is the provided search context valid?", + WebDriver.class.getName(), searchContext.getClass().getName() + ) + )); + } + + @Nullable + private String readStringCapability(SearchContext searchContext, String capName) { + if (searchContext == null) { + return null; + } + return unpackObjectFromSearchContext(searchContext, HasCapabilities.class) + .map(HasCapabilities::getCapabilities) + .map(caps -> CapabilityHelpers.getCapability(caps, capName, String.class)) + .orElse(null); + } + + private DefaultFieldDecorator createFieldDecorator(ElementLocatorFactory factory) { + return new DefaultFieldDecorator(factory) { @Override protected WebElement proxyForLocator(ClassLoader ignored, ElementLocator locator) { return proxyForAnElement(locator); } @Override - @SuppressWarnings("unchecked") protected List proxyForListLocator(ClassLoader ignored, ElementLocator locator) { ElementListInterceptor elementInterceptor = new ElementListInterceptor(locator); + //noinspection unchecked return getEnhancedProxy(ArrayList.class, elementInterceptor); } @@ -122,18 +179,10 @@ protected boolean isDecoratableList(Field field) { List bounds = (listType instanceof TypeVariable) ? Arrays.asList(((TypeVariable) listType).getBounds()) : Collections.emptyList(); - - return availableElementClasses.stream() - .anyMatch((webElClass) -> webElClass.equals(listType) || bounds.contains(webElClass)); + return AVAILABLE_ELEMENT_CLASSES.stream() + .anyMatch(webElClass -> webElClass.equals(listType) || bounds.contains(webElClass)); } }; - - widgetLocatorFactory = - new AppiumElementLocatorFactory(context, duration, new WidgetByBuilder(platform, automation)); - } - - public AppiumFieldDecorator(SearchContext context) { - this(context, DEFAULT_WAITING_TIMEOUT); } /** @@ -144,15 +193,11 @@ public AppiumFieldDecorator(SearchContext context) { * @return a field value or null. */ public Object decorate(ClassLoader ignored, Field field) { - Object result = defaultElementFieldDecoracor.decorate(ignored, field); - if (result != null) { - return result; - } - - return decorateWidget(field); + Object result = decorateWidget(field); + return result == null ? defaultElementFieldDecorator.decorate(ignored, field) : result; } - @SuppressWarnings("unchecked") + @Nullable private Object decorateWidget(Field field) { Class type = field.getType(); if (!Widget.class.isAssignableFrom(type) && !List.class.isAssignableFrom(type)) { @@ -178,34 +223,38 @@ private Object decorateWidget(Field field) { if (!Widget.class.isAssignableFrom((Class) listType)) { return null; } + //noinspection unchecked widgetType = (Class) listType; } else { return null; } } else { + //noinspection unchecked widgetType = (Class) field.getType(); } CacheableLocator locator = widgetLocatorFactory.createLocator(field); - Map> map = - OverrideWidgetReader.read(widgetType, field, platform); + Map> map = OverrideWidgetReader.read(widgetType, field, platform); if (isAlist) { - return getEnhancedProxy(ArrayList.class, - new WidgetListInterceptor(locator, webDriver, map, widgetType, - duration)); + return getEnhancedProxy( + ArrayList.class, + new WidgetListInterceptor(locator, webDriverReference, map, widgetType, duration) + ); } - Constructor constructor = - WidgetConstructorUtil.findConvenientConstructor(widgetType); - return getEnhancedProxy(widgetType, new Class[]{constructor.getParameterTypes()[0]}, + Constructor constructor = WidgetConstructorUtil.findConvenientConstructor(widgetType); + return getEnhancedProxy( + widgetType, + new Class[]{constructor.getParameterTypes()[0]}, new Object[]{proxyForAnElement(locator)}, - new WidgetInterceptor(locator, webDriver, null, map, duration)); + new WidgetInterceptor(locator, webDriverReference, null, map, duration) + ); } private WebElement proxyForAnElement(ElementLocator locator) { - ElementInterceptor elementInterceptor = new ElementInterceptor(locator, webDriver); + ElementInterceptor elementInterceptor = new ElementInterceptor(locator, webDriverReference); return getEnhancedProxy(RemoteWebElement.class, elementInterceptor); } } diff --git a/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java b/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java index 23445fcf8..ab4e29274 100644 --- a/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java +++ b/src/main/java/io/appium/java_client/pagefactory/DefaultElementByBuilder.java @@ -16,10 +16,6 @@ package io.appium.java_client.pagefactory; -import static java.lang.Integer.signum; -import static java.util.Arrays.asList; -import static java.util.Optional.ofNullable; - import io.appium.java_client.pagefactory.bys.ContentMappedBy; import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.bys.builder.AppiumByBuilder; @@ -46,6 +42,10 @@ import java.util.Map; import java.util.Optional; +import static java.lang.Integer.signum; +import static java.util.Arrays.asList; +import static java.util.Optional.ofNullable; + public class DefaultElementByBuilder extends AppiumByBuilder { private static final String PRIORITY = "priority"; @@ -175,18 +175,13 @@ protected By buildMobileNativeBy() { getBys(iOSXCUITFindBy.class, iOSXCUITFindBys.class, iOSXCUITFindAll.class)); } - if (isWindows()) { - return buildMobileBy(howToUseLocatorsOptional.map(HowToUseLocators::windowsAutomation).orElse(null), - getBys(WindowsFindBy.class, WindowsFindBys.class, WindowsFindAll.class)); - } - return null; } @Override public boolean isLookupCached() { AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated(); - return (annotatedElement.getAnnotation(CacheLookup.class) != null); + return annotatedElement.getAnnotation(CacheLookup.class) != null; } private By returnMappedBy(By byDefault, By nativeAppBy) { @@ -206,15 +201,13 @@ public By buildBy() { String idOrName = ((Field) annotatedElementContainer.getAnnotated()).getName(); if (defaultBy == null && mobileNativeBy == null) { - defaultBy = - new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); + defaultBy = new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); mobileNativeBy = new By.ById(idOrName); return returnMappedBy(defaultBy, mobileNativeBy); } if (defaultBy == null) { - defaultBy = - new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); + defaultBy = new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); return returnMappedBy(defaultBy, mobileNativeBy); } diff --git a/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java index 35a703e8a..82b61990b 100644 --- a/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java @@ -16,25 +16,27 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; - import io.appium.java_client.pagefactory.interceptors.InterceptorOfASingleElement; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.pagefactory.ElementLocator; +import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; + /** * Intercepts requests to {@link WebElement}. */ -class ElementInterceptor extends InterceptorOfASingleElement { +public class ElementInterceptor extends InterceptorOfASingleElement { - ElementInterceptor(ElementLocator locator, WebDriver driver) { + public ElementInterceptor(ElementLocator locator, WeakReference driver) { super(locator, driver); } - @Override protected Object getObject(WebElement element, Method method, Object[] args) + @Override + protected Object getObject(WebElement element, Method method, Object[] args) throws Throwable { try { return method.invoke(element, args); diff --git a/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java index 5dd385b29..77e68a329 100644 --- a/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java @@ -16,8 +16,6 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; - import io.appium.java_client.pagefactory.interceptors.InterceptorOfAListOfElements; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.pagefactory.ElementLocator; @@ -25,17 +23,19 @@ import java.lang.reflect.Method; import java.util.List; +import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; + /** * Intercepts requests to the list of {@link WebElement}. */ -class ElementListInterceptor extends InterceptorOfAListOfElements { +public class ElementListInterceptor extends InterceptorOfAListOfElements { - ElementListInterceptor(ElementLocator locator) { + public ElementListInterceptor(ElementLocator locator) { super(locator); } - @Override protected Object getObject(List elements, Method method, Object[] args) - throws Throwable { + @Override + protected Object getObject(List elements, Method method, Object[] args) throws Throwable { try { return method.invoke(elements, args); } catch (Throwable t) { diff --git a/src/main/java/io/appium/java_client/pagefactory/HowToUseLocators.java b/src/main/java/io/appium/java_client/pagefactory/HowToUseLocators.java index 88a9abb7d..cdeb9da1e 100644 --- a/src/main/java/io/appium/java_client/pagefactory/HowToUseLocators.java +++ b/src/main/java/io/appium/java_client/pagefactory/HowToUseLocators.java @@ -29,27 +29,17 @@ * or the searching by all possible locators. * * @return the strategy which defines how to use locators which are described by the - * {@link AndroidFindBy} annotation + * {@link AndroidFindBy} annotation */ LocatorGroupStrategy androidAutomation() default LocatorGroupStrategy.CHAIN; - /** - * The strategy which defines how to use locators which are described by the - * {@link WindowsFindBy} annotation. These annotations can define the chained searching - * or the searching by all possible locators. - * - * @return the strategy which defines how to use locators which are described by the - * {@link WindowsFindBy} annotation - */ - LocatorGroupStrategy windowsAutomation() default LocatorGroupStrategy.CHAIN; - /** * The strategy which defines how to use locators which are described by the * {@link iOSXCUITFindBy} annotation. These annotations can define the chained searching * or the searching by all possible locators. * * @return the strategy which defines how to use locators which are described by the - * {@link iOSXCUITFindBy} annotation + * {@link iOSXCUITFindBy} annotation */ LocatorGroupStrategy iOSXCUITAutomation() default LocatorGroupStrategy.CHAIN; } 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 4060e4fae..09984729c 100644 --- a/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java +++ b/src/main/java/io/appium/java_client/pagefactory/OverrideWidgetReader.java @@ -16,13 +16,7 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.WidgetConstructorUtil.findConvenientConstructor; -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 io.appium.java_client.pagefactory.bys.ContentType; -import io.appium.java_client.remote.AutomationName; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; @@ -30,6 +24,12 @@ import java.util.HashMap; import java.util.Map; +import static io.appium.java_client.pagefactory.WidgetConstructorUtil.findConvenientConstructor; +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; private static final String HTML = "html"; @@ -37,17 +37,20 @@ class OverrideWidgetReader { private static final String IOS_XCUIT_AUTOMATION = "iOSXCUITAutomation"; private static final String WINDOWS_AUTOMATION = "windowsAutomation"; + private OverrideWidgetReader() { + } + @SuppressWarnings("unchecked") private static Class getConvenientClass(Class declaredClass, - AnnotatedElement annotatedElement, String method) { + AnnotatedElement annotatedElement, String method) { Class convenientClass; OverrideWidget overrideWidget = annotatedElement.getAnnotation(OverrideWidget.class); try { if (overrideWidget == null || (convenientClass = - (Class) OverrideWidget.class - .getDeclaredMethod(method).invoke(overrideWidget)) - .equals(EMPTY)) { + (Class) OverrideWidget.class + .getDeclaredMethod(method).invoke(overrideWidget)) + .equals(EMPTY)) { convenientClass = declaredClass; } } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { @@ -56,9 +59,9 @@ private static Class getConvenientClass(Class getConvenientClass(Class getDefaultOrHTMLWidgetClass( - Class declaredClass, AnnotatedElement annotatedElement) { + Class declaredClass, AnnotatedElement annotatedElement) { return getConvenientClass(declaredClass, annotatedElement, HTML); } static Class getMobileNativeWidgetClass(Class declaredClass, - AnnotatedElement annotatedElement, String platform) { - String transformedPlatform = String.valueOf(platform).toUpperCase().trim(); + AnnotatedElement annotatedElement, String platform) { + String transformedPlatform = String.valueOf(platform).toUpperCase(ROOT).trim(); if (ANDROID.equalsIgnoreCase(transformedPlatform)) { return getConvenientClass(declaredClass, annotatedElement, ANDROID_UI_AUTOMATOR); @@ -90,26 +93,26 @@ static Class getMobileNativeWidgetClass(Class getConstructorOfADefaultOrHTMLWidget( - Class declaredClass, AnnotatedElement annotatedElement) { + Class declaredClass, AnnotatedElement annotatedElement) { Class clazz = - getDefaultOrHTMLWidgetClass(declaredClass, annotatedElement); + getDefaultOrHTMLWidgetClass(declaredClass, annotatedElement); return findConvenientConstructor(clazz); } private static Constructor getConstructorOfAMobileNativeWidgets( - Class declaredClass, AnnotatedElement annotatedElement, String platform) { + Class declaredClass, AnnotatedElement annotatedElement, String platform) { Class clazz = - getMobileNativeWidgetClass(declaredClass, annotatedElement, platform); + getMobileNativeWidgetClass(declaredClass, annotatedElement, platform); return findConvenientConstructor(clazz); } protected static Map> read( - Class declaredClass, AnnotatedElement annotatedElement, String platform) { + Class declaredClass, AnnotatedElement annotatedElement, String platform) { Map> result = new HashMap<>(); result.put(ContentType.HTML_OR_DEFAULT, - getConstructorOfADefaultOrHTMLWidget(declaredClass, annotatedElement)); + getConstructorOfADefaultOrHTMLWidget(declaredClass, annotatedElement)); result.put(ContentType.NATIVE_MOBILE_SPECIFIC, - getConstructorOfAMobileNativeWidgets(declaredClass, annotatedElement, platform)); + getConstructorOfAMobileNativeWidgets(declaredClass, annotatedElement, platform)); return result; } } diff --git a/src/main/java/io/appium/java_client/pagefactory/ThrowableUtil.java b/src/main/java/io/appium/java_client/pagefactory/ThrowableUtil.java index ca65e9e24..af09676f7 100644 --- a/src/main/java/io/appium/java_client/pagefactory/ThrowableUtil.java +++ b/src/main/java/io/appium/java_client/pagefactory/ThrowableUtil.java @@ -24,6 +24,9 @@ class ThrowableUtil { private static final String INVALID_SELECTOR_PATTERN = "Invalid locator strategy:"; + private ThrowableUtil() { + } + protected static boolean isInvalidSelectorRootCause(Throwable e) { if (e == null) { return false; @@ -33,8 +36,8 @@ protected static boolean isInvalidSelectorRootCause(Throwable e) { return true; } - if (String.valueOf(e.getMessage()).contains(INVALID_SELECTOR_PATTERN) || String - .valueOf(e.getMessage()).contains("Locator Strategy \\w+ is not supported")) { + if (String.valueOf(e.getMessage()).contains(INVALID_SELECTOR_PATTERN) + || String.valueOf(e.getMessage()).contains("Locator Strategy \\w+ is not supported")) { return true; } @@ -54,8 +57,8 @@ protected static boolean isStaleElementReferenceException(Throwable e) { } protected static Throwable extractReadableException(Throwable e) { - if (!RuntimeException.class.equals(e.getClass()) && !InvocationTargetException.class - .equals(e.getClass())) { + if (!RuntimeException.class.equals(e.getClass()) + && !InvocationTargetException.class.equals(e.getClass())) { return e; } diff --git a/src/main/java/io/appium/java_client/pagefactory/Widget.java b/src/main/java/io/appium/java_client/pagefactory/Widget.java index fe8a7f9e3..1b2fdaebe 100644 --- a/src/main/java/io/appium/java_client/pagefactory/Widget.java +++ b/src/main/java/io/appium/java_client/pagefactory/Widget.java @@ -16,8 +16,6 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.unpackWebDriverFromSearchContext; - import org.openqa.selenium.By; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebDriver; @@ -27,6 +25,8 @@ import java.util.List; +import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.unpackWebDriverFromSearchContext; + /** * It is the Appium-specific extension of the Page Object design pattern. It allows user * to create objects which typify some element with nested sub-elements. Also it allows to diff --git a/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java b/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java index 4f509598b..b87996357 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetByBuilder.java @@ -16,10 +16,6 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.OverrideWidgetReader.getDefaultOrHTMLWidgetClass; -import static io.appium.java_client.pagefactory.OverrideWidgetReader.getMobileNativeWidgetClass; -import static java.util.Optional.ofNullable; - import org.openqa.selenium.By; import java.lang.reflect.AnnotatedElement; @@ -28,6 +24,10 @@ import java.lang.reflect.Type; import java.util.List; +import static io.appium.java_client.pagefactory.OverrideWidgetReader.getDefaultOrHTMLWidgetClass; +import static io.appium.java_client.pagefactory.OverrideWidgetReader.getMobileNativeWidgetClass; +import static java.util.Optional.ofNullable; + public class WidgetByBuilder extends DefaultElementByBuilder { public WidgetByBuilder(String platform, String automation) { @@ -51,7 +51,7 @@ private static Class getClassFromAListField(Field field) { @SuppressWarnings("unchecked") private By getByFromDeclaredClass(WhatIsNeeded whatIsNeeded) { AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated(); - Field field = Field.class.cast(annotatedElement); + Field field = (Field) annotatedElement; Class declaredClass; By result = null; 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 747a9eadd..46d946628 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetInterceptor.java @@ -16,63 +16,71 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; -import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType; - 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 net.sf.cglib.proxy.MethodProxy; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.PageFactory; +import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Callable; + +import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; +import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType; +import static java.util.Optional.ofNullable; -class WidgetInterceptor extends InterceptorOfASingleElement { +public class WidgetInterceptor extends InterceptorOfASingleElement { private final Map> instantiationMap; private final Map cachedInstances = new HashMap<>(); private final Duration duration; - private WebElement cachedElement; + private WeakReference cachedElementReference; - WidgetInterceptor(CacheableLocator locator, WebDriver driver, WebElement cachedElement, - Map> instantiationMap, - Duration duration) { - super(locator, driver); - this.cachedElement = cachedElement; + /** + * Proxy interceptor class for widgets. + */ + public WidgetInterceptor( + @Nullable CacheableLocator locator, + WeakReference driverReference, + @Nullable WeakReference cachedElementReference, + Map> instantiationMap, + Duration duration + ) { + super(locator, driverReference); + this.cachedElementReference = cachedElementReference; this.instantiationMap = instantiationMap; this.duration = duration; } - - @Override protected Object getObject(WebElement element, Method method, Object[] args) - throws Throwable { + @Override + protected Object getObject(WebElement element, Method method, Object[] args) throws Throwable { ContentType type = getCurrentContentType(element); - if (cachedElement == null - || (locator != null && !((CacheableLocator) locator) - .isLookUpCached()) - || cachedInstances.size() == 0) { - cachedElement = element; + WebElement cachedElement = cachedElementReference == null ? null : cachedElementReference.get(); + if (cachedElement == null || !cachedInstances.containsKey(type) + || locator != null && !((CacheableLocator) locator).isLookUpCached() + ) { + cachedElementReference = new WeakReference<>(element); Constructor constructor = instantiationMap.get(type); Class clazz = constructor.getDeclaringClass(); - int modifiers = clazz.getModifiers(); - if (Modifier.isAbstract(modifiers)) { - throw new InstantiationException(clazz.getName() - + " is abstract so " - + "it can't be instantiated"); + if (Modifier.isAbstract(clazz.getModifiers())) { + throw new InstantiationException( + String.format("%s is abstract so it cannot be instantiated", clazz.getName()) + ); } - Widget widget = constructor.newInstance(cachedElement); + Widget widget = constructor.newInstance(element); cachedInstances.put(type, widget); - PageFactory.initElements(new AppiumFieldDecorator(widget, duration), widget); + PageFactory.initElements(new AppiumFieldDecorator(new WeakReference<>(widget), duration), widget); } try { method.setAccessible(true); @@ -82,11 +90,11 @@ class WidgetInterceptor extends InterceptorOfASingleElement { } } - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) - throws Throwable { - if (locator != null) { - return super.intercept(obj, method, args, proxy); - } - return getObject(cachedElement, method, args); + @Override + public Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { + WebElement element = ofNullable(cachedElementReference).map(WeakReference::get).orElse(null); + return locator == null && element != null + ? getObject(element, method, args) + : super.call(obj, method, args, original); } } 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 580de7fb2..bb4bb1889 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/WidgetListInterceptor.java @@ -16,36 +16,46 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; -import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType; -import static java.util.Optional.ofNullable; - 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 io.appium.java_client.pagefactory.utils.ProxyFactory; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; -class WidgetListInterceptor extends InterceptorOfAListOfElements { +import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; +import static io.appium.java_client.pagefactory.utils.ProxyFactory.getEnhancedProxy; +import static io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType; +import static java.util.Optional.ofNullable; +public class WidgetListInterceptor extends InterceptorOfAListOfElements { private final Map> instantiationMap; private final List cachedWidgets = new ArrayList<>(); private final Class declaredType; private final Duration duration; - private final WebDriver driver; - private List cachedElements; + private final WeakReference driver; + private final List> cachedElementReferences = new ArrayList<>(); - WidgetListInterceptor(CacheableLocator locator, WebDriver driver, - Map> instantiationMap, - Class declaredType, Duration duration) { + /** + * Proxy interceptor class for lists of widgets. + */ + public WidgetListInterceptor( + @Nullable CacheableLocator locator, + WeakReference driver, + Map> instantiationMap, + Class declaredType, + Duration duration + ) { super(locator); this.instantiationMap = instantiationMap; this.declaredType = declaredType; @@ -53,22 +63,29 @@ class WidgetListInterceptor extends InterceptorOfAListOfElements { this.driver = driver; } - - @Override protected Object getObject(List elements, Method method, Object[] args) - throws Throwable { - if (cachedElements == null || (locator != null && !((CacheableLocator) locator) - .isLookUpCached())) { - cachedElements = elements; + @Override + protected Object getObject(List elements, Method method, Object[] args) throws Throwable { + List cachedElements = cachedElementReferences.stream() + .map(WeakReference::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (cachedElements.size() != cachedWidgets.size() + || locator != null && !((CacheableLocator) locator).isLookUpCached()) { cachedWidgets.clear(); + cachedElementReferences.clear(); ContentType type = null; - for (WebElement element : cachedElements) { + for (WebElement element : elements) { type = ofNullable(type).orElseGet(() -> getCurrentContentType(element)); - Class[] params = - new Class[] {instantiationMap.get(type).getParameterTypes()[0]}; - cachedWidgets.add(ProxyFactory - .getEnhancedProxy(declaredType, params, new Object[] {element}, - new WidgetInterceptor(null, driver, element, instantiationMap, duration))); + Class[] params = new Class[] {instantiationMap.get(type).getParameterTypes()[0]}; + WeakReference elementReference = new WeakReference<>(element); + cachedWidgets.add( + getEnhancedProxy( + declaredType, params, new Object[] {element}, + new WidgetInterceptor(null, driver, elementReference, instantiationMap, duration) + ) + ); + cachedElementReferences.add(elementReference); } } try { diff --git a/src/main/java/io/appium/java_client/pagefactory/WindowsBy.java b/src/main/java/io/appium/java_client/pagefactory/WindowsBy.java deleted file mode 100644 index e953db2fc..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsBy.java +++ /dev/null @@ -1,72 +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.pagefactory; - -/** - * Used to build a complex Windows automation locator. - */ -public @interface WindowsBy { - - /** - * It is a Windows automator string. - * - * @return a Windows automator string - */ - String windowsAutomation() default ""; - - /** - * It an UI automation accessibility Id which is a convenient to Windows. - * - * @return an UI automation accessibility Id - */ - String accessibility() default ""; - - /** - * It is an id of the target element. - * - * @return an id of the target element - */ - String id() default ""; - - /** - * It is a className of the target element. - * - * @return a className of the target element - */ - String className() default ""; - - /** - * It is a desired element tag. - * - * @return a desired element tag - */ - String tagName() default ""; - - /** - * It is a xpath to the target element. - * - * @return a xpath to the target element - */ - String xpath() default ""; - - /** - * Priority of the searching. Higher number means lower priority. - * - * @return priority of the searching - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/WindowsFindAll.java b/src/main/java/io/appium/java_client/pagefactory/WindowsFindAll.java deleted file mode 100644 index 03be2f654..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindAll.java +++ /dev/null @@ -1,49 +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.pagefactory; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Used to mark a field on a Page/Screen Object to indicate that lookup should use a series - * of {@link WindowsBy} tags - * It will then search for all elements that match any criteria. Note that elements - * are not guaranteed to be in document order. - */ -@Retention(RUNTIME) @Target({FIELD, TYPE}) -@Repeatable(WindowsFindByAllSet.class) -public @interface WindowsFindAll { - /** - * It is a set of {@link WindowsBy} strategies which may be used to find the target element. - * - * @return a collection of strategies which may be used to find the target element - */ - WindowsBy[] value(); - - /** - * Priority of the searching. Higher number means lower priority. - * - * @return priority of the searching - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBy.java b/src/main/java/io/appium/java_client/pagefactory/WindowsFindBy.java deleted file mode 100644 index 9ba86d6c3..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBy.java +++ /dev/null @@ -1,86 +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.pagefactory; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Used to mark a field on a Page Object to indicate an alternative mechanism for locating the - * element or a list of elements. Used in conjunction with - * {@link org.openqa.selenium.support.PageFactory} - * this allows users to quickly and easily create PageObjects. - * using Windows automation selectors, accessibility, id, name, class name, tag and xpath - */ -@Retention(RUNTIME) @Target({FIELD, TYPE}) -@Repeatable(WindowsFindBySet.class) -public @interface WindowsFindBy { - - /** - * It is a Windows automator string. - * - * @return a Windows automator string - */ - String windowsAutomation() default ""; - - /** - * It an UI automation accessibility Id which is a convenient to Windows. - * - * @return an UI automation accessibility Id - */ - String accessibility() default ""; - - /** - * It is an id of the target element. - * - * @return an id of the target element - */ - String id() default ""; - - /** - * It is a className of the target element. - * - * @return a className of the target element - */ - String className() default ""; - - /** - * It is a desired element tag. - * - * @return a desired element tag - */ - String tagName() default ""; - - /** - * It is a xpath to the target element. - * - * @return a xpath to the target element - */ - String xpath() default ""; - - /** - * Priority of the searching. Higher number means lower priority. - * - * @return priority of the searching - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/WindowsFindByAllSet.java b/src/main/java/io/appium/java_client/pagefactory/WindowsFindByAllSet.java deleted file mode 100644 index 9a1e2f3cd..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindByAllSet.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.appium.java_client.pagefactory; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Defines set of chained/possible locators. Each one locator - * should be defined with {@link WindowsFindAll} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface WindowsFindByAllSet { - /** - * An array of which builds a sequence of the chained searching for elements or a set of possible locators. - * - * @return an array of {@link WindowsFindAll} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - WindowsFindAll[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/WindowsFindByChainSet.java b/src/main/java/io/appium/java_client/pagefactory/WindowsFindByChainSet.java deleted file mode 100644 index 583d0f5d5..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindByChainSet.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.appium.java_client.pagefactory; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Defines set of chained/possible locators. Each one locator - * should be defined with {@link WindowsFindBys} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface WindowsFindByChainSet { - /** - * An array of which builds a sequence of the chained searching for elements or a set of possible locators. - * - * @return an array of {@link WindowsFindBys} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - WindowsFindBys[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBySet.java b/src/main/java/io/appium/java_client/pagefactory/WindowsFindBySet.java deleted file mode 100644 index 5efd79322..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBySet.java +++ /dev/null @@ -1,40 +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.pagefactory; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Defines set of chained/possible locators. Each one locator - * should be defined with {@link WindowsFindBy} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface WindowsFindBySet { - /** - * An array ofwhich builds a sequence of the chained searching for elements or a set of possible locators. - * - * @return an array of {@link WindowsFindBy} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - WindowsFindBy[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBys.java b/src/main/java/io/appium/java_client/pagefactory/WindowsFindBys.java deleted file mode 100644 index 605986de3..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBys.java +++ /dev/null @@ -1,47 +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.pagefactory; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Used to mark a field on a Page Object to indicate that lookup should use - * a series of {@link WindowsBy} tags. - */ -@Retention(RUNTIME) @Target({FIELD, TYPE}) -@Repeatable(WindowsFindByChainSet.class) -public @interface WindowsFindBys { - /** - * It is a set of {@link WindowsBy} strategies which build the chain of the searching for the target element. - * - * @return a collection of strategies which build the chain of the searching for the target element - */ - WindowsBy[] value(); - - /** - * Priority of the searching. Higher number means lower priority. - * - * @return priority of the searching - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/WithTimeout.java b/src/main/java/io/appium/java_client/pagefactory/WithTimeout.java index 0b95a50c1..0b04dadf2 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WithTimeout.java +++ b/src/main/java/io/appium/java_client/pagefactory/WithTimeout.java @@ -44,6 +44,9 @@ ChronoUnit chronoUnit(); class DurationBuilder { + private DurationBuilder() { + } + static Duration build(WithTimeout withTimeout) { return Duration.of(withTimeout.time(), withTimeout.chronoUnit()); } 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 1f995bbe3..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 @@ -1,32 +1,34 @@ /* -* 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. -*/ + * 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.pagefactory.bys; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.pagefactory.bys.ContentType.NATIVE_MOBILE_SPECIFIC; - +import lombok.EqualsAndHashCode; +import org.jspecify.annotations.NonNull; import org.openqa.selenium.By; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; import java.util.List; import java.util.Map; -import javax.annotation.Nonnull; +import static io.appium.java_client.pagefactory.bys.ContentType.NATIVE_MOBILE_SPECIFIC; +import static java.util.Objects.requireNonNull; + +@EqualsAndHashCode(callSuper = true) public class ContentMappedBy extends By { private final Map map; private ContentType currentContent = NATIVE_MOBILE_SPECIFIC; @@ -37,24 +39,28 @@ public ContentMappedBy(Map map) { /** * This method sets required content type for the further searching. + * * @param type required content type {@link ContentType} * @return self-reference. */ - public By useContent(@Nonnull ContentType type) { - checkNotNull(type); + public By useContent(@NonNull ContentType type) { + requireNonNull(type); currentContent = type; return this; } - @Override public WebElement findElement(SearchContext context) { + @Override + public WebElement findElement(SearchContext context) { return context.findElement(map.get(currentContent)); } - @Override public List findElements(SearchContext context) { + @Override + public List findElements(SearchContext context) { return context.findElements(map.get(currentContent)); } - @Override public String toString() { + @Override + public String toString() { return map.get(currentContent).toString(); } } 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 423645881..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,17 +16,11 @@ package io.appium.java_client.pagefactory.bys.builder; -import static io.appium.java_client.remote.AutomationName.IOS_XCUI_TEST; -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.TVOS; -import static io.appium.java_client.remote.MobilePlatform.WINDOWS; - +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; @@ -38,11 +32,17 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static io.appium.java_client.remote.AutomationName.IOS_XCUI_TEST; +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.TVOS; +import static io.appium.java_client.remote.MobilePlatform.WINDOWS; + /** - * It is the basic handler of Appium-specific page object annotations + * It is the basic handler of Appium-specific page object annotations. * About the Page Object design pattern please read these documents: - * - https://code.google.com/p/selenium/wiki/PageObjects - * - https://code.google.com/p/selenium/wiki/PageFactory + * - Selenium Page Object models + * - Selenium Page Factory */ public abstract class AppiumByBuilder extends AbstractAnnotations { protected static final Class[] DEFAULT_ANNOTATION_METHOD_ARGUMENTS = new Class[]{}; @@ -76,7 +76,7 @@ private static Method[] prepareAnnotationMethods(Class ann List targetAnnotationMethodNamesList = getMethodNames(annotation.getDeclaredMethods()); targetAnnotationMethodNamesList.removeAll(METHODS_TO_BE_EXCLUDED_WHEN_ANNOTATION_IS_READ); return targetAnnotationMethodNamesList.stream() - .map((methodName) -> { + .map(methodName -> { try { return annotation.getMethod(methodName, DEFAULT_ANNOTATION_METHOD_ARGUMENTS); } catch (NoSuchMethodException | SecurityException e) { @@ -87,13 +87,13 @@ private static Method[] prepareAnnotationMethods(Class ann private static String getFilledValue(Annotation mobileBy) { return Stream.of(prepareAnnotationMethods(mobileBy.getClass())) - .filter((method) -> String.class == method.getReturnType()) - .filter((method) -> { + .filter(method -> String.class == method.getReturnType()) + .filter(method -> { try { Object strategyParameter = method.invoke(mobileBy); return strategyParameter != null && !String.valueOf(strategyParameter).isEmpty(); } catch (IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { + | InvocationTargetException e) { throw new RuntimeException(e); } }) @@ -107,9 +107,9 @@ private static String getFilledValue(Annotation mobileBy) { private static By getMobileBy(Annotation annotation, String valueName) { return Stream.of(Strategies.values()) - .filter((strategy) -> strategy.returnValueName().equals(valueName)) + .filter(strategy -> strategy.returnValueName().equals(valueName)) .findFirst() - .map((strategy) -> strategy.getBy(annotation)) + .map(strategy -> strategy.getBy(annotation)) .orElseThrow(() -> new IllegalArgumentException( String.format("@%s: There is an unknown strategy %s", annotation.getClass().getSimpleName(), valueName) @@ -118,14 +118,14 @@ private static By getMobileBy(Annotation annotation, String valueName) { private static T getComplexMobileBy(Annotation[] annotations, Class requiredByClass) { By[] byArray = Stream.of(annotations) - .map((annotation) -> getMobileBy(annotation, getFilledValue(annotation))) + .map(annotation -> getMobileBy(annotation, getFilledValue(annotation))) .toArray(By[]::new); try { Constructor c = requiredByClass.getConstructor(By[].class); Object[] values = new Object[]{byArray}; return c.newInstance(values); } catch (InvocationTargetException | NoSuchMethodException | InstantiationException - | IllegalAccessException e) { + | IllegalAccessException e) { throw new RuntimeException(e); } } diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByAll.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByAll.java deleted file mode 100644 index 59015ede7..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByAll.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.appium.java_client.pagefactory.bys.builder; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import org.openqa.selenium.By; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebElement; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -/** - * Mechanism used to locate elements within a document using a series of lookups. This class will - * find all DOM elements that matches any of the locators in sequence, e.g. - * - *
- * driver.findElements(new ByAll(by1, by2))
- * 
- * - * will find all elements that match by1 and then all elements that match by2. - * This means that the list of elements returned may not be in document order. - * - * @deprecated Use {@link org.openqa.selenium.support.pagefactory.ByAll} - */ -@Deprecated -public class ByAll extends org.openqa.selenium.support.pagefactory.ByAll { - - private final List bys; - - private Function> getSearchingFunction(By by) { - return input -> { - try { - return Optional.of(input.findElement(by)); - } catch (NoSuchElementException e) { - return Optional.empty(); - } - }; - } - - /** - * Finds all elements that matches any of the locators in sequence. - * - * @param bys is a set of {@link By} which forms the all possible searching. - */ - public ByAll(By[] bys) { - super(bys); - checkNotNull(bys); - - this.bys = Arrays.asList(bys); - - checkArgument(!this.bys.isEmpty(), "By array should not be empty"); - } - - @Override - public WebElement findElement(SearchContext context) { - return bys.stream() - .map(by -> getSearchingFunction(by).apply(context)) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow(() -> new NoSuchElementException("Cannot locate an element using " + toString())); - } -} diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java index f8d2240cb..b92d2eb10 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java @@ -16,9 +16,6 @@ package io.appium.java_client.pagefactory.bys.builder; -import static com.google.common.base.Preconditions.checkNotNull; - -import io.appium.java_client.functions.AppiumFunction; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; @@ -27,12 +24,15 @@ import org.openqa.selenium.support.ui.FluentWait; import java.util.Optional; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; public class ByChained extends org.openqa.selenium.support.pagefactory.ByChained { private final By[] bys; - private static AppiumFunction getSearchingFunction(By by) { + private static Function getSearchingFunction(By by) { return input -> { try { if (input == null) { @@ -51,8 +51,7 @@ private static AppiumFunction getSearchingFunction(By * @param bys is a set of {@link By} which forms the chain of the searching. */ public ByChained(By[] bys) { - super(bys); - checkNotNull(bys); + super(requireNonNull(bys)); if (bys.length == 0) { throw new IllegalArgumentException("By array should not be empty"); } @@ -61,18 +60,16 @@ public ByChained(By[] bys) { @Override public WebElement findElement(SearchContext context) { - AppiumFunction searchingFunction = null; - + Function searchingFunction = null; for (By by: bys) { - searchingFunction = Optional.ofNullable(searchingFunction != null - ? searchingFunction.andThen(getSearchingFunction(by)) : null).orElse(getSearchingFunction(by)); + searchingFunction = Optional.ofNullable(searchingFunction) + .map(sf -> sf.andThen(getSearchingFunction(by))) + .orElseGet(() -> getSearchingFunction(by)); } - - FluentWait waiting = new FluentWait<>(context); + requireNonNull(searchingFunction); try { - checkNotNull(searchingFunction); - return waiting.until(searchingFunction); + return new FluentWait<>(context).until(searchingFunction); } catch (TimeoutException e) { throw new NoSuchElementException("Cannot locate an element using " + this); } diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java index db06bdbc1..590db8278 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/Strategies.java @@ -16,6 +16,11 @@ package io.appium.java_client.pagefactory.bys.builder; +import io.appium.java_client.AppiumBy; +import io.appium.java_client.pagefactory.AndroidBy; +import io.appium.java_client.pagefactory.AndroidFindBy; +import org.openqa.selenium.By; + import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -23,13 +28,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.openqa.selenium.By; - -import io.appium.java_client.AppiumBy; -import io.appium.java_client.MobileBy; -import io.appium.java_client.pagefactory.AndroidBy; -import io.appium.java_client.pagefactory.AndroidFindBy; - enum Strategies { BYUIAUTOMATOR("uiAutomator") { @Override By getBy(Annotation annotation) { @@ -41,7 +39,7 @@ enum Strategies { return super.getBy(annotation); } }, - BYACCESSABILITY("accessibility") { + BYACCESSIBILITY("accessibility") { @Override By getBy(Annotation annotation) { return AppiumBy.accessibilityId(getValue(annotation, this)); } @@ -84,16 +82,6 @@ enum Strategies { .partialLinkText(getValue(annotation, this)); } }, - /** - * The Windows UIAutomation strategy. - * @deprecated Not supported on the server side. - */ - @Deprecated - BYWINDOWSAUTOMATION("windowsAutomation") { - @Override By getBy(Annotation annotation) { - return MobileBy.windowsAutomation(getValue(annotation, this)); - } - }, BY_CLASS_CHAIN("iOSClassChain") { @Override By getBy(Annotation annotation) { return AppiumBy @@ -126,7 +114,7 @@ enum Strategies { } static List strategiesNames() { - return Stream.of(values()).map((s) -> s.valueName).collect(Collectors.toList()); + return Stream.of(values()).map(s -> s.valueName).collect(Collectors.toList()); } private static String getValue(Annotation annotation, Strategies strategy) { diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindAll.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindAll.java index c388f47d2..94a2241c6 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindAll.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindAll.java @@ -16,14 +16,14 @@ package io.appium.java_client.pagefactory; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Used to mark a field on a Page/Screen Object to indicate that lookup should use a series * of {@link io.appium.java_client.pagefactory.iOSXCUITBy} tags. diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java index 5194e4094..dbc6d23c0 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java @@ -16,14 +16,14 @@ package io.appium.java_client.pagefactory; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + @Retention(RUNTIME) @Target({FIELD, TYPE}) @Repeatable(iOSXCUITFindBySet.class) public @interface iOSXCUITFindBy { diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByAllSet.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByAllSet.java index 0bb769ea7..240efa73d 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByAllSet.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByAllSet.java @@ -1,12 +1,12 @@ package io.appium.java_client.pagefactory; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - /** * Defines set of chained/possible locators. Each one locator * should be defined with {@link io.appium.java_client.pagefactory.iOSXCUITFindAll} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByChainSet.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByChainSet.java index 2b5fc28de..fcc1a9e87 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByChainSet.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByChainSet.java @@ -1,12 +1,12 @@ package io.appium.java_client.pagefactory; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - /** * Defines set of chained/possible locators. Each one locator * should be defined with {@link io.appium.java_client.pagefactory.iOSXCUITFindBys} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBySet.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBySet.java index f1fea5a89..ce7464d2a 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBySet.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBySet.java @@ -16,13 +16,13 @@ package io.appium.java_client.pagefactory; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - @Retention(RUNTIME) @Target({FIELD, TYPE}) public @interface iOSXCUITFindBySet { /** diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBys.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBys.java index 78a0eba3a..ec8424569 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBys.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBys.java @@ -16,14 +16,14 @@ package io.appium.java_client.pagefactory; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + /** * Used to mark a field on a Page Object to indicate that lookup should use * a series of {@link io.appium.java_client.pagefactory.iOSXCUITBy} tags. 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 8b96c6c68..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 @@ -16,36 +16,34 @@ package io.appium.java_client.pagefactory.interceptors; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; +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 java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Callable; -public abstract class InterceptorOfAListOfElements implements MethodInterceptor { +public abstract class InterceptorOfAListOfElements implements MethodCallListener { protected final ElementLocator locator; - public InterceptorOfAListOfElements(ElementLocator locator) { + public InterceptorOfAListOfElements(@Nullable ElementLocator locator) { this.locator = locator; } - protected abstract Object getObject(List elements, Method method, Object[] args) - throws InvocationTargetException, IllegalAccessException, InstantiationException, Throwable; + protected abstract Object getObject( + List elements, Method method, Object[] args + ) throws Throwable; - /** - * Look at {@link MethodInterceptor#intercept(Object, Method, Object[], MethodProxy)}. - */ - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) - throws Throwable { - if (Object.class.equals(method.getDeclaringClass())) { - return proxy.invokeSuper(obj, args); + @Override + public Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { + if (locator == null || Object.class == method.getDeclaringClass()) { + return original.call(); } - List realElements = new ArrayList<>(locator.findElements()); + final var realElements = new ArrayList<>(locator.findElements()); return getObject(realElements, method, args); } } 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 d06555eaf..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 @@ -16,47 +16,65 @@ package io.appium.java_client.pagefactory.interceptors; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; +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 java.lang.ref.WeakReference; import java.lang.reflect.Method; +import java.util.Objects; +import java.util.concurrent.Callable; -public abstract class InterceptorOfASingleElement implements MethodInterceptor { +public abstract class InterceptorOfASingleElement implements MethodCallListener { protected final ElementLocator locator; - protected final WebDriver driver; + private final WeakReference driverReference; - public InterceptorOfASingleElement(ElementLocator locator, WebDriver driver) { + public InterceptorOfASingleElement( + @Nullable ElementLocator locator, + WeakReference driverReference + ) { this.locator = locator; - this.driver = driver; + this.driverReference = driverReference; } - protected abstract Object getObject(WebElement element, Method method, Object[] args) - throws Throwable; + protected abstract Object getObject(WebElement element, Method method, Object[] args) throws Throwable; - /** - * Look at {@link MethodInterceptor#intercept(Object, Method, Object[], MethodProxy)}. - */ - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) - throws Throwable { + private static boolean areElementsEqual(Object we1, Object we2) { + if (!(we1 instanceof RemoteWebElement) || !(we2 instanceof RemoteWebElement)) { + return false; + } + + return we1 == we2 + || (Objects.equals(((RemoteWebElement) we1).getId(), ((RemoteWebElement) we2).getId())); + } + + @Override + public Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { + if (locator == null) { + return original.call(); + } if (method.getName().equals("toString") && args.length == 0) { return locator.toString(); } - if (Object.class.equals(method.getDeclaringClass())) { - return proxy.invokeSuper(obj, args); + if (Object.class == method.getDeclaringClass()) { + return original.call(); } - if (WrapsDriver.class.isAssignableFrom(method.getDeclaringClass()) && method.getName() - .equals("getWrappedDriver")) { - return driver; + if (WrapsDriver.class.isAssignableFrom(method.getDeclaringClass()) + && method.getName().equals("getWrappedDriver")) { + return driverReference.get(); } WebElement realElement = locator.findElement(); + if ("equals".equals(method.getName()) && args.length == 1) { + return areElementsEqual(realElement, args[0]); + } return getObject(realElement, method, args); } } diff --git a/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java b/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java index 390ebcd92..9e33276e5 100644 --- a/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/utils/ProxyFactory.java @@ -16,43 +16,88 @@ package io.appium.java_client.pagefactory.utils; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.MethodInterceptor; +import io.appium.java_client.proxy.MethodCallListener; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +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.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.not; /** * Original class is a super class of a * proxy object here. */ public final class ProxyFactory { + private static final Set NON_PROXYABLE_METHODS = setWithout( + OBJECT_METHOD_NAMES, "toString", "equals", "hashCode" + ); - private ProxyFactory() { - super(); + @SafeVarargs + private static Set setWithout(@SuppressWarnings("SameParameterValue") Set source, T... items) { + Set result = new HashSet<>(source); + Arrays.asList(items).forEach(result::remove); + return Collections.unmodifiableSet(result); } - public static T getEnhancedProxy(Class requiredClazz, MethodInterceptor interceptor) { - return getEnhancedProxy(requiredClazz, new Class[] {}, new Object[] {}, interceptor); + @SafeVarargs + private static Set setWith(@SuppressWarnings("SameParameterValue") Set source, T... items) { + Set result = new HashSet<>(source); + result.addAll(List.of(items)); + return Collections.unmodifiableSet(result); + } + + private ProxyFactory() { } /** - * It returns some proxies created by CGLIB. + * Creates a proxy instance for the given class with an empty constructor. * * @param The proxy object class. * @param requiredClazz is a {@link java.lang.Class} whose instance should be created + * @param listener is the instance of a method listener class + * @return a proxied instance of the desired class + */ + public static T getEnhancedProxy(Class requiredClazz, MethodCallListener listener) { + return getEnhancedProxy(requiredClazz, new Class[] {}, new Object[] {}, listener); + } + + /** + * Creates a proxy instance for the given class. + * + * @param The proxy object class. + * @param cls is a {@link java.lang.Class} whose instance should be created * @param params is an array of @link java.lang.Class}. It should be convenient to * parameter types of some declared constructor which belongs to desired * class. * @param values is an array of @link java.lang.Object}. It should be convenient to * parameter types of some declared constructor which belongs to desired * class. - * @param interceptor is the instance of {@link net.sf.cglib.proxy.MethodInterceptor} + * @param listener is the instance of a method listener class * @return a proxied instance of the desired class */ - @SuppressWarnings("unchecked") - public static T getEnhancedProxy(Class requiredClazz, Class[] params, Object[] values, - MethodInterceptor interceptor) { - Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(requiredClazz); - enhancer.setCallback(interceptor); - return (T) enhancer.create(params, values); + public static T getEnhancedProxy( + Class cls, Class[] params, Object[] values, MethodCallListener listener + ) { + ElementMatcher extraMatcher = not( + namedOneOf(NON_PROXYABLE_METHODS.toArray(new String[0])) + ).and( + not(isAbstract()) + ); + return createProxy( + cls, + values, + params, + Collections.singletonList(listener), + extraMatcher + ); } } 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 8aa5f7634..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 @@ -16,53 +16,74 @@ package io.appium.java_client.pagefactory.utils; -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.Optional.ofNullable; -import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; - 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 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() { + } /** - * This method extract an instance of {@link WebDriver} from the given {@link SearchContext}. + * This method extracts an instance of the given interface from the given {@link SearchContext}. + * It is expected that the {@link SearchContext} itself or the object it wraps implements it. + * * @param searchContext is an instance of {@link SearchContext}. It may be the instance of * {@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 WrapsDriver} or {@link WrapsElement} - * @return the instance of {@link WebDriver}. - * Note: if the given {@link SearchContext} is not - * {@link WebDriver} and it doesn't implement - * {@link WrapsDriver} or {@link WrapsElement} then this method returns null. - * + * @param cls interface whose instance is going to be extracted. + * @return Either an instance of the given interface or Optional.empty(). */ - public static WebDriver unpackWebDriverFromSearchContext(SearchContext searchContext) { - if (searchContext instanceof WebDriver) { - return (WebDriver) searchContext; + public static Optional unpackObjectFromSearchContext(@Nullable SearchContext searchContext, Class cls) { + if (searchContext == null) { + return Optional.empty(); } + if (cls.isAssignableFrom(searchContext.getClass())) { + return Optional.of(cls.cast(searchContext)); + } if (searchContext instanceof WrapsDriver) { - return unpackWebDriverFromSearchContext( - ((WrapsDriver) searchContext).getWrappedDriver()); + return unpackObjectFromSearchContext(((WrapsDriver) searchContext).getWrappedDriver(), cls); } - // Search context it is not only WebDriver. WebElement is search context too. // RemoteWebElement implements WrapsDriver if (searchContext instanceof WrapsElement) { - return unpackWebDriverFromSearchContext( - ((WrapsElement) searchContext).getWrappedElement()); + return unpackObjectFromSearchContext(((WrapsElement) searchContext).getWrappedElement(), cls); } - return null; + return Optional.empty(); + } + + /** + * This method extract an instance of {@link WebDriver} from the given {@link SearchContext}. + * @param searchContext is an instance of {@link SearchContext}. It may be the instance of + * {@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 WrapsDriver} or {@link WrapsElement} + * @return the instance of {@link WebDriver}. + * Note: if the given {@link SearchContext} is not + * {@link WebDriver} and it doesn't implement + * {@link WrapsDriver} or {@link WrapsElement} then this method returns null. + * + */ + @Nullable + public static WebDriver unpackWebDriverFromSearchContext(@Nullable SearchContext searchContext) { + return unpackObjectFromSearchContext(searchContext, WebDriver.class).orElse(null); } /** @@ -72,28 +93,26 @@ public static WebDriver unpackWebDriverFromSearchContext(SearchContext searchCon * {@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) { - return ofNullable(unpackWebDriverFromSearchContext(context)).map(driver -> { - if (driver instanceof HasBrowserCheck && !((HasBrowserCheck) driver).isBrowser()) { - return NATIVE_MOBILE_SPECIFIC; - } + var browserCheckHolder = unpackObjectFromSearchContext(context, HasBrowserCheck.class); + if (browserCheckHolder.filter(hbc -> !hbc.isBrowser()).isPresent()) { + return NATIVE_MOBILE_SPECIFIC; + } - if (ContextAware.class.isAssignableFrom(driver.getClass())) { //it is desktop browser - ContextAware contextAware = (ContextAware) driver; - String currentContext = contextAware.getContext(); - if (containsIgnoreCase(currentContext, NATIVE_APP_PATTERN)) { - return NATIVE_MOBILE_SPECIFIC; - } - } + var contextAware = unpackObjectFromSearchContext(context, SupportsContextSwitching.class); + if (contextAware.map(SupportsContextSwitching::getContext) + .filter(c -> c.toUpperCase(ROOT).contains(NATIVE_CONTEXT)).isPresent()) { + return NATIVE_MOBILE_SPECIFIC; + } - return HTML_OR_DEFAULT; - }).orElse(HTML_OR_DEFAULT); + return HTML_OR_DEFAULT; } } 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/HasMethodCallListeners.java b/src/main/java/io/appium/java_client/proxy/HasMethodCallListeners.java new file mode 100644 index 000000000..b5807f71b --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/HasMethodCallListeners.java @@ -0,0 +1,35 @@ +/* + * 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; + +public interface HasMethodCallListeners { + /** + * The setter is dynamically created by ByteBuddy to store + * method call listeners on the instrumented proxy instance. + * + * @param methodCallListeners Array of method call listeners assigned to the proxy instance. + */ + void setMethodCallListeners(MethodCallListener[] methodCallListeners); + + /** + * The getter is dynamically created by ByteBuddy to access + * method call listeners on the instrumented proxy instance. + * + * @return Array of method call listeners assigned the proxy instance. + */ + MethodCallListener[] getMethodCallListeners(); +} diff --git a/src/main/java/io/appium/java_client/proxy/Helpers.java b/src/main/java/io/appium/java_client/proxy/Helpers.java new file mode 100644 index 000000000..e420d494e --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/Helpers.java @@ -0,0 +1,231 @@ +/* + * 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 com.google.common.base.Preconditions; +import lombok.Value; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.matcher.ElementMatchers; +import org.jspecify.annotations.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.WeakHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +public class Helpers { + public static final Set OBJECT_METHOD_NAMES = Stream.of(Object.class.getMethods()) + .map(Method::getName) + .collect(Collectors.toSet()); + + // Each proxy class created by ByteBuddy gets automatically cached by the + // given class loader. It is important to have this cache here in order to improve + // 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 Map> CACHED_PROXY_CLASSES = + Collections.synchronizedMap(new WeakHashMap<>()); + + private Helpers() { + } + + /** + * Creates a transparent proxy instance for the given class. + * It is possible to provide one or more method execution listeners + * or replace particular method calls completely. Callbacks + * defined in these listeners are going to be called when any + * **public** method of the given class is invoked. Overridden callbacks + * are expected to be skipped if they throw + * {@link io.appium.java_client.proxy.NotImplementedException}. + * + * @param cls the class to which the proxy should be created. + * Must not be an interface. + * @param constructorArgs Array of constructor arguments. Could be an + * empty array if the class provides a constructor without arguments. + * @param constructorArgTypes Array of constructor argument types. Must + * represent types of constructorArgs. + * @param listeners One or more method invocation listeners. + * @param Any class derived from Object + * @return Proxy instance + */ + public static T createProxy( + Class cls, + Object[] constructorArgs, + Class[] constructorArgTypes, + Collection listeners + ) { + ElementMatcher extraMatcher = ElementMatchers.not(namedOneOf( + OBJECT_METHOD_NAMES.toArray(new String[0]) + )); + return createProxy(cls, constructorArgs, constructorArgTypes, listeners, extraMatcher); + } + + /** + * Creates a transparent proxy instance for the given class. + * It is possible to provide one or more method execution listeners + * or replace particular method calls completely. Callbacks + * defined in these listeners are going to be called when any + * **public** method of the given class is invoked. Overridden callbacks + * are expected to be skipped if they throw + * {@link io.appium.java_client.proxy.NotImplementedException}. + * !!! This API is designed for private usage. + * + * @param cls The class to which the proxy should be created. + * Must not be an interface. + * @param constructorArgs Array of constructor arguments. Could be an + * empty array if the class provides a constructor without arguments. + * @param constructorArgTypes Array of constructor argument types. Must + * represent types of constructorArgs. + * @param listeners One or more method invocation listeners. + * @param extraMethodMatcher Optional additional method proxy conditions + * @param Any class derived from Object + * @return Proxy instance + */ + public static T createProxy( + Class cls, + Object[] constructorArgs, + Class[] constructorArgTypes, + Collection listeners, + @Nullable ElementMatcher extraMethodMatcher + ) { + var signature = ProxyClassSignature.of(cls, constructorArgTypes, extraMethodMatcher); + var proxyClass = CACHED_PROXY_CLASSES.computeIfAbsent(signature, k -> { + Preconditions.checkArgument(constructorArgs.length == constructorArgTypes.length, + String.format( + "Constructor arguments array length %d must be equal to the types array length %d", + constructorArgs.length, constructorArgTypes.length + ) + ); + Preconditions.checkArgument(!listeners.isEmpty(), "The collection of listeners must not be empty"); + requireNonNull(cls, "Class must not be null"); + Preconditions.checkArgument(!cls.isInterface(), "Class must not be an interface"); + + ElementMatcher.Junction matcher = ElementMatchers.isPublic(); + //noinspection resource + return new ByteBuddy() + .subclass(cls) + .method(extraMethodMatcher == null ? matcher : matcher.and(extraMethodMatcher)) + .intercept(MethodDelegation.to(Interceptor.class)) + // https://github.com/raphw/byte-buddy/blob/2caef35c172897cbdd21d163c55305a64649ce41/byte-buddy-dep/src/test/java/net/bytebuddy/ByteBuddyTutorialExamplesTest.java#L346 + .defineField("methodCallListeners", MethodCallListener[].class, Visibility.PRIVATE) + .implement(HasMethodCallListeners.class).intercept(FieldAccessor.ofBeanProperty()) + .make() + .load(Helpers.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) + .getLoaded() + .asSubclass(cls); + }); + + 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); + } + } + + /** + * Creates a transparent proxy instance for the given class. + * It is possible to provide one or more method execution listeners + * or replace particular method calls completely. Callbacks + * defined in these listeners are going to be called when any + * **public** method of the given class is invoked. Overridden callbacks + * are expected to be skipped if they throw NotImplementedException. + * + * @param cls the class to which the proxy should be created. + * Must not be an interface. Must expose a constructor + * without arguments. + * @param listeners One or more method invocation listeners. + * @param Any class derived from Object + * @return Proxy instance + */ + public static T createProxy(Class cls, Collection listeners) { + return createProxy(cls, new Object[]{}, new Class[]{}, listeners); + } + + /** + * Creates a transparent proxy instance for the given class. + * It is possible to provide one or more method execution listeners + * or replace particular method calls completely. Callbacks + * defined in these listeners are going to be called when any + * **public** method of the given class is invoked. Overridden callbacks + * are expected to be skipped if they throw NotImplementedException. + * + * @param cls the class to which the proxy should be created. + * Must not be an interface. Must expose a constructor + * without arguments. + * @param listener Method invocation listener. + * @param Any class derived from Object + * @return Proxy instance + */ + public static T createProxy(Class cls, MethodCallListener listener) { + return createProxy(cls, new Object[]{}, new Class[]{}, Collections.singletonList(listener)); + } + + /** + * Creates a transparent proxy instance for the given class. + * It is possible to provide one or more method execution listeners + * or replace particular method calls completely. Callbacks + * defined in these listeners are going to be called when any + * **public** method of the given class is invoked. Overridden callbacks + * are expected to be skipped if they throw NotImplementedException. + * + * @param cls the class to which the proxy should be created. + * Must not be an interface. + * @param constructorArgs Array of constructor arguments. Could be an + * empty array if the class provides a constructor without arguments. + * @param constructorArgTypes Array of constructor argument types. Must + * represent types of constructorArgs. + * @param listener Method invocation listener. + * @param Any class derived from Object + * @return Proxy instance + */ + public static T createProxy( + Class cls, + Object[] constructorArgs, + Class[] constructorArgTypes, + MethodCallListener listener + ) { + return createProxy(cls, constructorArgs, constructorArgTypes, Collections.singletonList(listener)); + } + + @Value(staticConstructor = "of") + private static class ProxyClassSignature { + Class cls; + Class[] constructorArgTypes; + ElementMatcher extraMethodMatcher; + } +} diff --git a/src/main/java/io/appium/java_client/proxy/Interceptor.java b/src/main/java/io/appium/java_client/proxy/Interceptor.java new file mode 100644 index 000000000..f4ece1668 --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/Interceptor.java @@ -0,0 +1,128 @@ +/* + * 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.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import net.bytebuddy.implementation.bind.annotation.This; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +import static io.appium.java_client.proxy.MethodCallListener.UNSET; + +public class Interceptor { + private static final Logger LOGGER = LoggerFactory.getLogger(Interceptor.class); + + private Interceptor() { + } + + /** + * A magic method used to wrap public method calls in classes + * patched by ByteBuddy and acting as proxies. The performance + * of this method is mission-critical as it gets called upon + * every invocation of any method of the proxied class. + * + * @param self The reference to the original instance. + * @param method The reference to the original method. + * @param args The reference to method args. + * @param callable The reference to the non-patched callable to avoid call recursion. + * @return Either the original method result or the patched one. + */ + @SuppressWarnings("unused") + @RuntimeType + public static Object intercept( + @This Object self, + @Origin Method method, + @AllArguments Object[] args, + @SuperCall Callable callable + ) throws Throwable { + var listeners = ((HasMethodCallListeners) self).getMethodCallListeners(); + if (listeners == null || listeners.length == 0) { + return callable.call(); + } + + for (var listener : listeners) { + try { + listener.beforeCall(self, method, args); + } catch (NotImplementedException e) { + // ignore + } catch (Exception e) { + LOGGER.atError().log("Got an unexpected error in beforeCall listener of {}.{} method", + self.getClass().getName(), method.getName(), e + ); + } + } + + Object result = UNSET; + for (var listener : listeners) { + try { + result = listener.call(self, method, args, callable); + if (result != UNSET) { + break; + } + } catch (NotImplementedException e) { + // ignore + } catch (Exception e) { + try { + result = listener.onError(self, method, args, e); + if (result != UNSET) { + return result; + } + } catch (NotImplementedException ignore) { + // ignore + } + throw e; + } + } + if (UNSET == result) { + try { + result = callable.call(); + } catch (Exception e) { + for (var listener : listeners) { + try { + result = listener.onError(self, method, args, e); + if (result != UNSET) { + return result; + } + } catch (NotImplementedException ignore) { + // ignore + } + } + throw e; + } + } + + final Object endResult = result == UNSET ? null : result; + for (var listener : listeners) { + try { + listener.afterCall(self, method, args, endResult); + } catch (NotImplementedException e) { + // ignore + } catch (Exception e) { + LOGGER.atError().log("Got an unexpected error in afterCall listener of {}.{} method", + self.getClass().getName(), method.getName(), e + ); + } + } + return endResult; + } +} diff --git a/src/main/java/io/appium/java_client/proxy/MethodCallListener.java b/src/main/java/io/appium/java_client/proxy/MethodCallListener.java new file mode 100644 index 000000000..7dfb5b299 --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/MethodCallListener.java @@ -0,0 +1,82 @@ +/* + * 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 java.lang.reflect.Method; +import java.util.UUID; +import java.util.concurrent.Callable; + +public interface MethodCallListener { + UUID UNSET = UUID.randomUUID(); + + /** + * The callback to be invoked before any public method of the proxy is called. + * The implementation is not expected to throw any exceptions. If a + * runtime exception is thrown then it is going to be silently logged. + * + * @param obj The proxy instance + * @param method Method to be called + * @param args Array of method arguments + */ + default void beforeCall(Object obj, Method method, Object[] args) { + } + + /** + * Override this callback in order to change/customize the behavior + * of a single or multiple methods. The original method result + * will be replaced with the result returned by this callback. + * Also, any exception thrown by it will replace original method(s) + * exception. + * + * @param obj The proxy instance + * @param method Method to be replaced + * @param args Array of method arguments + * @param original The reference to the original method in case it is necessary to instrument its result. + * @return The type of the returned result should be castable to the returned type of the original method. + */ + default Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { + return UNSET; + } + + /** + * The callback to be invoked after any public method of the proxy is called. + * The implementation is not expected to throw any exceptions. If a + * runtime exception is thrown then it is going to be silently logged. + * + * @param obj The proxy instance + * @param method Method to be called + * @param args Array of method arguments + */ + default void afterCall(Object obj, Method method, Object[] args, Object result) { + } + + /** + * The callback to be invoked if a public method or its + * {@link #call(Object, Method, Object[], Callable) Call} replacement throws an exception. + * + * @param obj The proxy instance + * @param method Method to be called + * @param args Array of method arguments + * @param e Exception instance thrown by the original method invocation. + * @return You could either (re)throw the exception in this callback or + * overwrite the behavior and return a result from it. It is expected that the + * type of the returned argument could be cast to the returned type of the original method. + */ + default Object onError(Object obj, Method method, Object[] args, Throwable e) throws Throwable { + return UNSET; + } +} diff --git a/src/main/java/io/appium/java_client/proxy/NotImplementedException.java b/src/main/java/io/appium/java_client/proxy/NotImplementedException.java new file mode 100644 index 000000000..861c114c8 --- /dev/null +++ b/src/main/java/io/appium/java_client/proxy/NotImplementedException.java @@ -0,0 +1,20 @@ +/* + * 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; + +public class NotImplementedException extends RuntimeException { +} 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/AndroidMobileCapabilityType.java b/src/main/java/io/appium/java_client/remote/AndroidMobileCapabilityType.java deleted file mode 100644 index 9ec293fa5..000000000 --- a/src/main/java/io/appium/java_client/remote/AndroidMobileCapabilityType.java +++ /dev/null @@ -1,492 +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.remote; - -import org.openqa.selenium.remote.CapabilityType; - -/** - * The list of Android-specific capabilities.
- * Read:
- * - * https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md#android-only - */ -public interface AndroidMobileCapabilityType extends CapabilityType { - - /** - * Activity name for the Android activity you want to launch from your package. - * This often needs to be preceded by a {@code .} (e.g., {@code .MainActivity} - * instead of {@code MainActivity}). By default this capability is received from the package - * manifest (action: android.intent.action.MAIN , category: android.intent.category.LAUNCHER) - */ - String APP_ACTIVITY = "appActivity"; - - /** - * Java package of the Android app you want to run. By default this capability is received - * from the package manifest ({@literal @}package attribute value) - */ - String APP_PACKAGE = "appPackage"; - - /** - * Activity name/names, comma separated, for the Android activity you want to wait for. - * By default the value of this capability is the same as for {@code appActivity}. - * You must set it to the very first focused application activity name in case it is different - * from the one which is set as {@code appActivity} if your capability has {@code appActivity} - * and {@code appPackage}. You can also use wildcards ({@code *}). - */ - String APP_WAIT_ACTIVITY = "appWaitActivity"; - - /** - * Java package of the Android app you want to wait for. - * By default the value of this capability is the same as for {@code appActivity} - */ - String APP_WAIT_PACKAGE = "appWaitPackage"; - - /** - * Timeout in milliseconds used to wait for the appWaitActivity to launch (default 20000). - * @since 1.6.0 - */ - String APP_WAIT_DURATION = "appWaitDuration"; - - /** - * Timeout in seconds while waiting for device to become ready. - */ - String DEVICE_READY_TIMEOUT = "deviceReadyTimeout"; - - /** - * Allow to install a test package which has {@code android:testOnly="true"} in the manifest. - * {@code false} by default - */ - String ALLOW_TEST_PACKAGES = "allowTestPackages"; - - /** - * Fully qualified instrumentation class. Passed to -w in adb shell - * am instrument -e coverage true -w. - */ - String ANDROID_COVERAGE = "androidCoverage"; - - /** - * A broadcast action implemented by yourself which is used to dump coverage into file system. - * Passed to -a in adb shell am broadcast -a - */ - String ANDROID_COVERAGE_END_INTENT = "androidCoverageEndIntent"; - - /** - * (Chrome and webview only) Enable Chromedriver's performance logging (default false). - * - * @deprecated move to {@link MobileCapabilityType#ENABLE_PERFORMANCE_LOGGING} - */ - @Deprecated - String ENABLE_PERFORMANCE_LOGGING = "enablePerformanceLogging"; - - /** - * Timeout in seconds used to wait for a device to become ready after booting. - */ - String ANDROID_DEVICE_READY_TIMEOUT = "androidDeviceReadyTimeout"; - - /** - * Port used to connect to the ADB server (default 5037). - */ - String ADB_PORT = "adbPort"; - - /** - * Devtools socket name. Needed only when tested app is a Chromium embedding browser. - * The socket is open by the browser and Chromedriver connects to it as a devtools client. - */ - String ANDROID_DEVICE_SOCKET = "androidDeviceSocket"; - - /** - * Timeout in milliseconds used to wait for an apk to install to the device. Defaults to `90000`. - * @since 1.6.0 - */ - String ANDROID_INSTALL_TIMEOUT = "androidInstallTimeout"; - - /** - * The name of the directory on the device in which the apk will be push before install. - * Defaults to {@code /data/local/tmp} - * @since 1.6.5 - */ - String ANDROID_INSTALL_PATH = "androidInstallPath"; - - /** - * Name of avd to launch. - */ - String AVD = "avd"; - - /** - * How long to wait in milliseconds for an avd to launch and connect to - * ADB (default 120000). - * @since 0.18.0 - */ - String AVD_LAUNCH_TIMEOUT = "avdLaunchTimeout"; - - /** - * How long to wait in milliseconds for an avd to finish its - * boot animations (default 120000). - * @since 0.18.0 - */ - String AVD_READY_TIMEOUT = "avdReadyTimeout"; - - /** - * Additional emulator arguments used when launching an avd. - */ - String AVD_ARGS = "avdArgs"; - - /** - * Use a custom keystore to sign apks, default false. - */ - String USE_KEYSTORE = "useKeystore"; - - /** - * Path to custom keystore, default ~/.android/debug.keystore. - */ - String KEYSTORE_PATH = "keystorePath"; - - /** - * Password for custom keystore. - */ - String KEYSTORE_PASSWORD = "keystorePassword"; - - /** - * Alias for key. - */ - String KEY_ALIAS = "keyAlias"; - - /** - * Password for key. - */ - String KEY_PASSWORD = "keyPassword"; - - /** - * The absolute local path to webdriver executable (if Chromium embedder provides - * its own webdriver, it should be used instead of original chromedriver - * bundled with Appium). - */ - String CHROMEDRIVER_EXECUTABLE = "chromedriverExecutable"; - - /** - * An array of arguments to be passed to the chromedriver binary when it's run by Appium. - * By default no CLI args are added beyond what Appium uses internally (such as {@code --url-base}, {@code --port}, - * {@code --adb-port}, and {@code --log-path}. - * @since 1.12.0 - */ - String CHROMEDRIVER_ARGS = "chromedriverArgs"; - - /** - * The absolute path to a directory to look for Chromedriver executables in, for automatic discovery of compatible - * Chromedrivers. Ignored if {@code chromedriverUseSystemExecutable} is {@code true} - * @since 1.8.0 - */ - String CHROMEDRIVER_EXECUTABLE_DIR = "chromedriverExecutableDir"; - - /** - * The absolute path to a file which maps Chromedriver versions to the minimum Chrome that it supports. - * Ignored if {@code chromedriverUseSystemExecutable} is {@code true} - * @since 1.8.0 - */ - String CHROMEDRIVER_CHROME_MAPPING_FILE = "chromedriverChromeMappingFile"; - - /** - * If true, bypasses automatic Chromedriver configuration and uses the version that comes downloaded with Appium. - * Ignored if {@code chromedriverExecutable} is set. Defaults to {@code false} - * @since 1.9.0 - */ - String CHROMEDRIVER_USE_SYSTEM_EXECUTABLE = "chromedriverUseSystemExecutable"; - - /** - * Numeric port to start Chromedriver on. Note that use of this capability is discouraged as it will cause undefined - * behavior in case there are multiple webviews present. By default Appium will find a free port. - */ - String CHROMEDRIVER_PORT = "chromedriverPort"; - - /** - * A list of valid ports for Appium to use for communication with Chromedrivers. This capability supports multiple - * webview scenarios. The form of this capability is an array of numeric ports, where array items can themselves be - * arrays of length 2, where the first element is the start of an inclusive range and the second is the end. - * By default, Appium will use any free port. - * @since 1.13.0 - */ - String CHROMEDRIVER_PORTS = "chromedriverPorts"; - - /** - * Sets the chromedriver flag {@code --disable-build-check} for Chrome webview tests. - * @since 1.11.0 - */ - String CHROMEDRIVER_DISABLE_BUILD_CHECK = "chromedriverDisableBuildCheck"; - - /** - * Amount of time to wait for Webview context to become active, in ms. Defaults to 2000. - * @since 1.5.2 - */ - String AUTO_WEBVIEW_TIMEOUT = "autoWebviewTimeout"; - - /** - * Intent action which will be used to start activity - * (default android.intent.action.MAIN). - */ - String INTENT_ACTION = "intentAction"; - - /** - * Intent category which will be used to start - * activity (default android.intent.category.LAUNCHER). - */ - String INTENT_CATEGORY = "intentCategory"; - - /** - * Flags that will be used to start activity (default 0x10200000). - */ - String INTENT_FLAGS = "intentFlags"; - - /** - * Additional intent arguments that will be used to start activity. See - * - * Intent arguments. - */ - String OPTIONAL_INTENT_ARGUMENTS = "optionalIntentArguments"; - - /** - * Doesn't stop the process of the app under test, before starting the app using adb. - * If the app under test is created by another anchor app, setting this false, - * allows the process of the anchor app to be still alive, during the start of - * the test app using adb. In other words, with dontStopAppOnReset set to true, - * we will not include the -S flag in the adb shell am start call. - * With this capability omitted or set to false, we include the -S flag. Default false - * @since 1.4.0 - */ - String DONT_STOP_APP_ON_RESET = "dontStopAppOnReset"; - - /** - * Enable Unicode input, default false. - * @since 1.2.0 - */ - String UNICODE_KEYBOARD = "unicodeKeyboard"; - - /** - * Reset keyboard to its original state, after running Unicode tests with - * unicodeKeyboard capability. Ignored if used alone. Default false - */ - String RESET_KEYBOARD = "resetKeyboard"; - - /** - * Skip checking and signing of app with debug keys, will work only with - * UiAutomator and not with selendroid, default false. - * @since 1.2.2 - */ - String NO_SIGN = "noSign"; - - /** - * Calls the setCompressedLayoutHierarchy() uiautomator function. - * This capability can speed up test execution, since Accessibility commands will run - * faster ignoring some elements. The ignored elements will not be findable, - * which is why this capability has also been implemented as a toggle-able - * setting as well as a capability. Defaults to false. - */ - String IGNORE_UNIMPORTANT_VIEWS = "ignoreUnimportantViews"; - - /** - * Disables android watchers that watch for application not responding and application crash, - * this will reduce cpu usage on android device/emulator. This capability will work only with - * UiAutomator and not with selendroid, default false. - * @since 1.4.0 - */ - String DISABLE_ANDROID_WATCHERS = "disableAndroidWatchers"; - - /** - * Allows passing chromeOptions capability for ChromeDriver. - * For more information see - * - * chromeOptions. - */ - String CHROME_OPTIONS = "chromeOptions"; - - /** - * Kill ChromeDriver session when moving to a non-ChromeDriver webview. - * Defaults to false - */ - String RECREATE_CHROME_DRIVER_SESSIONS = "recreateChromeDriverSessions"; - - /** - * In a web context, use native (adb) method for taking a screenshot, rather than proxying - * to ChromeDriver, default false. - * @since 1.5.3 - */ - String NATIVE_WEB_SCREENSHOT = "nativeWebScreenshot"; - - /** - * The name of the directory on the device in which the screenshot will be put. - * Defaults to /data/local/tmp. - * @since 1.6.0 - */ - String ANDROID_SCREENSHOT_PATH = "androidScreenshotPath"; - - /** - * Set the network speed emulation. Specify the maximum network upload and download speeds. Defaults to {@code full} - */ - String NETWORK_SPEED = "networkSpeed"; - - /** - * Toggle gps location provider for emulators before starting the session. By default the emulator will have this - * option enabled or not according to how it has been provisioned. - */ - String GPS_ENABLED = "gpsEnabled"; - - /** - * Set this capability to {@code true} to run the Emulator headless when device display is not needed to be visible. - * {@code false} is the default value. isHeadless is also support for iOS, check XCUITest-specific capabilities. - */ - String IS_HEADLESS = "isHeadless"; - - /** - * Timeout in milliseconds used to wait for adb command execution. Defaults to {@code 20000} - */ - String ADB_EXEC_TIMEOUT = "adbExecTimeout"; - - /** - * Sets the locale script. - * @since 1.10.0 - */ - String LOCALE_SCRIPT = "localeScript"; - - /** - * Skip device initialization which includes i.a.: installation and running of Settings app or setting of - * permissions. Can be used to improve startup performance when the device was already used for automation and - * it's prepared for the next automation. Defaults to {@code false} - * @since 1.11.0 - */ - String SKIP_DEVICE_INITIALIZATION = "skipDeviceInitialization"; - - /** - * Have Appium automatically determine which permissions your app requires and - * grant them to the app on install. Defaults to {@code false}. If noReset is {@code true}, this capability doesn't - * work. - */ - String AUTO_GRANT_PERMISSIONS = "autoGrantPermissions"; - - /** - * Allow for correct handling of orientation on landscape-oriented devices. - * Set to {@code true} to basically flip the meaning of {@code PORTRAIT} and {@code LANDSCAPE}. - * Defaults to {@code false}. - * @since 1.6.4 - */ - String ANDROID_NATURAL_ORIENTATION = "androidNaturalOrientation"; - - /** - * {@code systemPort} used to connect to - * appium-uiautomator2-server or - * appium-espresso-driver. - * The default is {@code 8200} in general and selects one port from {@code 8200} to {@code 8299} - * for appium-uiautomator2-server, it is {@code 8300} from {@code 8300} to {@code 8399} for - * appium-espresso-driver. When you run tests in parallel, you must adjust the port to avoid conflicts. Read - * - * Parallel Testing Setup Guide for more details. - */ - String SYSTEM_PORT = "systemPort"; - - /** - * Optional remote ADB server host. - * @since 1.7.0 - */ - String REMOTE_ADB_HOST = "remoteAdbHost"; - - /** - * Skips unlock during session creation. Defaults to {@code false} - */ - String SKIP_UNLOCK = "skipUnlock"; - - /** - * Unlock the target device with particular lock pattern instead of just waking up the device with a helper app. - * It works with {@code unlockKey} capability. Defaults to undefined. {@code fingerprint} is available only for - * Android 6.0+ and emulators. - * Read unlock doc in - * android driver. - */ - String UNLOCK_TYPE = "unlockType"; - - /** - * A key pattern to unlock used by {@code unlockType}. - */ - String UNLOCK_KEY = "unlockKey"; - - /** - * Initializing the app under test automatically. - * Appium does not launch the app under test if this is {@code false}. Defaults to {@code true} - */ - String AUTO_LAUNCH = "autoLaunch"; - - /** - * Skips to start capturing logcat. It might improve performance such as network. - * Log related commands will not work. Defaults to {@code false}. - * @since 1.12.0 - */ - String SKIP_LOGCAT_CAPTURE = "skipLogcatCapture"; - - /** - * A package, list of packages or * to uninstall package/s before installing apks for test. - * {@code '*'} uninstall all of thrid-party packages except for packages which is necessary for Appium to test such - * as {@code io.appium.settings} or {@code io.appium.uiautomator2.server} since Appium already contains the logic to - * manage them. - * @since 1.12.0 - */ - String UNINSTALL_OTHER_PACKAGES = "uninstallOtherPackages"; - - /** - * Set device animation scale zero if the value is {@code true}. After session is complete, Appium restores the - * animation scale to it's original value. Defaults to {@code false} - * @since 1.9.0 - */ - String DISABLE_WINDOW_ANIMATION = "disableWindowAnimation"; - - /** - * Specify the Android build-tools version to be something different than the default, which is to use the most - * recent version. It is helpful to use a non-default version if your environment uses alpha/beta build tools. - * @since 1.14.0 - */ - String BUILD_TOOLS_VERSION = "buildToolsVersion"; - - /** - * By default application installation is skipped if newer or the same version of this app is already present on - * the device under test. Setting this option to {@code true} will enforce Appium to always install the current - * application build independently of the currently installed version of it. Defaults to {@code false}. - * @since 1.16.0 - */ - String ENFORCE_APP_INSTALL = "enforceAppInstall"; - - /** - * Whether or not Appium should augment its webview detection with page detection, guaranteeing that any - * webview contexts which show up in the context list have active pages. This can prevent an error if a - * context is selected where Chromedriver cannot find any pages. Defaults to {@code false}. - * @since 1.15.0 - */ - String ENSURE_WEBVIEWS_HAVE_PAGES = "ensureWebviewsHavePages"; - - /** - * To support the `ensureWebviewsHavePages` feature, it is necessary to open a TCP port for communication with - * the webview on the device under test. This capability allows overriding of the default port of {@code 9222}, - * in case multiple sessions are running simultaneously (to avoid port clash), or in case the default port - * is not appropriate for your system. - * @since 1.15.0 - */ - String WEBVIEW_DEVTOOLS_PORT = "webviewDevtoolsPort"; - - /** - * Set the maximum number of remote cached apks which are pushed to the device-under-test's - * local storage. Caching apks remotely speeds up the execution of sequential test cases, when using the - * same set of apks, by avoiding the need to be push an apk to the remote file system every time a - * reinstall is needed. Set this capability to {@code 0} to disable caching. Defaults to {@code 10}. - * @since 1.14.0 - */ - String REMOTE_APPS_CACHE_LIMIT = "remoteAppsCacheLimit"; -} 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 01c551fb4..ad6bb36c3 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java +++ b/src/main/java/io/appium/java_client/remote/AppiumCommandExecutor.java @@ -16,17 +16,14 @@ package io.appium.java_client.remote; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Throwables.throwIfUnchecked; -import static java.util.Optional.ofNullable; -import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION; - -import com.google.common.base.Supplier; import com.google.common.base.Throwables; - +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.internal.ReflectionHelpers; +import lombok.Getter; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.Command; import org.openqa.selenium.remote.CommandCodec; import org.openqa.selenium.remote.CommandExecutor; @@ -38,113 +35,131 @@ import org.openqa.selenium.remote.Response; import org.openqa.selenium.remote.ResponseCodec; import org.openqa.selenium.remote.codec.w3c.W3CHttpCommandCodec; -import org.openqa.selenium.remote.http.ClientConfig; 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 java.io.IOException; -import java.lang.reflect.Field; import java.net.ConnectException; +import java.net.MalformedURLException; import java.net.URL; -import java.time.Duration; import java.util.Map; import java.util.Optional; -import java.util.UUID; +import static com.google.common.base.Throwables.throwIfUnchecked; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION; + +@NullMarked public class AppiumCommandExecutor extends HttpCommandExecutor { - // https://github.com/appium/appium-base-driver/pull/400 - private static final String IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key"; - private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(10); private final Optional serviceOptional; + @Getter + private final AppiumClientConfig appiumClientConfig; - private AppiumCommandExecutor(Map additionalCommands, DriverService service, - URL addressOfRemoteServer, - HttpClient.Factory httpClientFactory, - ClientConfig clientConfig) { + /** + * Create an AppiumCommandExecutor instance. + * + * @param additionalCommands is the map of Appium commands + * @param service take a look at {@link DriverService} + * @param httpClientFactory take a look at {@link Factory} + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + */ + public AppiumCommandExecutor( + Map additionalCommands, + @Nullable DriverService service, + @Nullable Factory httpClientFactory, + AppiumClientConfig appiumClientConfig) { super(additionalCommands, - ofNullable(clientConfig).orElse( - ClientConfig.defaultConfig() - .baseUrl(Require.nonNull("Server URL", ofNullable(service) - .map(DriverService::getUrl) - .orElse(addressOfRemoteServer))) - .readTimeout(DEFAULT_READ_TIMEOUT) - ), + appiumClientConfig, ofNullable(httpClientFactory).orElseGet(HttpCommandExecutor::getDefaultClientFactory) ); serviceOptional = ofNullable(service); + + this.appiumClientConfig = appiumClientConfig; } public AppiumCommandExecutor(Map additionalCommands, DriverService service, - HttpClient.Factory httpClientFactory) { - this(additionalCommands, checkNotNull(service), null, httpClientFactory, null); + @Nullable Factory httpClientFactory) { + this(additionalCommands, requireNonNull(service), httpClientFactory, + AppiumClientConfig.defaultConfig().baseUrl(requireNonNull(service).getUrl())); } - public AppiumCommandExecutor(Map additionalCommands, - URL addressOfRemoteServer, HttpClient.Factory httpClientFactory) { - this(additionalCommands, null, checkNotNull(addressOfRemoteServer), httpClientFactory, null); + public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer, + @Nullable Factory httpClientFactory) { + this(additionalCommands, null, httpClientFactory, + AppiumClientConfig.defaultConfig().baseUrl(requireNonNull(addressOfRemoteServer))); } - public AppiumCommandExecutor(Map additionalCommands, ClientConfig clientConfig) { - this(additionalCommands, null, checkNotNull(clientConfig.baseUrl()), null, clientConfig); + public AppiumCommandExecutor(Map additionalCommands, AppiumClientConfig appiumClientConfig) { + this(additionalCommands, null, null, appiumClientConfig); } - public AppiumCommandExecutor(Map additionalCommands, - URL addressOfRemoteServer) { - this(additionalCommands, addressOfRemoteServer, HttpClient.Factory.createDefault()); + public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer) { + this(additionalCommands, null, HttpClient.Factory.createDefault(), + AppiumClientConfig.defaultConfig().baseUrl(requireNonNull(addressOfRemoteServer))); } - public AppiumCommandExecutor(Map additionalCommands, - DriverService service) { - this(additionalCommands, service, HttpClient.Factory.createDefault()); + public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer, + AppiumClientConfig appiumClientConfig) { + this(additionalCommands, null, HttpClient.Factory.createDefault(), + appiumClientConfig.baseUrl(requireNonNull(addressOfRemoteServer))); } - @SuppressWarnings("SameParameterValue") - protected B getPrivateFieldValue( - Class cls, String fieldName, Class fieldType) { - try { - final Field f = cls.getDeclaredField(fieldName); - f.setAccessible(true); - return fieldType.cast(f.get(this)); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new WebDriverException(e); - } + public AppiumCommandExecutor(Map additionalCommands, DriverService service) { + this(additionalCommands, service, HttpClient.Factory.createDefault(), + AppiumClientConfig.defaultConfig().baseUrl(service.getUrl())); } + public AppiumCommandExecutor(Map additionalCommands, + DriverService service, AppiumClientConfig appiumClientConfig) { + this(additionalCommands, service, HttpClient.Factory.createDefault(), appiumClientConfig); + } + + @Deprecated @SuppressWarnings("SameParameterValue") protected void setPrivateFieldValue( Class cls, String fieldName, Object newValue) { - try { - final Field f = cls.getDeclaredField(fieldName); - f.setAccessible(true); - f.set(this, newValue); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new WebDriverException(e); - } + 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; } - protected void setCommandCodec(CommandCodec newCodec) { - setPrivateFieldValue(HttpCommandExecutor.class, "commandCodec", newCodec); + public void setCommandCodec(CommandCodec newCodec) { + this.commandCodec = newCodec; } - protected void setResponseCodec(ResponseCodec codec) { - setPrivateFieldValue(HttpCommandExecutor.class, "responseCodec", codec); + public void setResponseCodec(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. + */ + protected void overrideServerUrl(URL serverUrl) { + HttpClient newClient = getHttpClientFactory().createClient(appiumClientConfig.baseUrl(serverUrl)); + setPrivateFieldValue(HttpCommandExecutor.class, "client", newClient); } private Response createSession(Command command) throws IOException { @@ -152,12 +167,7 @@ private Response createSession(Command command) throws IOException { throw new SessionNotCreatedException("Session already exists"); } - ProtocolHandshake.Result result = new AppiumProtocolHandshake().createSession( - getClient().with((httpHandler) -> (req) -> { - req.setHeader(IDEMPOTENCY_KEY_HEADER, UUID.randomUUID().toString().toLowerCase()); - return httpHandler.execute(req); - }), command - ); + var result = new ProtocolHandshake().createSession(getClient(), command); Dialect dialect = result.getDialect(); if (!(dialect.getCommandCodec() instanceof W3CHttpCommandCodec)) { throw new SessionNotCreatedException("Only W3C sessions are supported. " @@ -166,11 +176,47 @@ private Response createSession(Command command) throws IOException { setCommandCodec(new AppiumW3CHttpCommandCodec()); refreshAdditionalCommands(); setResponseCodec(dialect.getResponseCodec()); - return result.createResponse(); + Response response = result.createResponse(); + if (appiumClientConfig.isDirectConnectEnabled()) { + setDirectConnect(response); + } + + return response; } public void refreshAdditionalCommands() { - getAdditionalCommands().forEach(this::defineCommand); + getAdditionalCommands().forEach(super::defineCommand); + } + + public void defineCommand(String commandName, CommandInfo info) { + super.defineCommand(commandName, info); + } + + @SuppressWarnings("unchecked") + private void setDirectConnect(Response response) throws SessionNotCreatedException { + Map responseValue = (Map) response.getValue(); + + DirectConnect directConnect = new DirectConnect(responseValue); + + if (!directConnect.isValid()) { + return; + } + + if (!directConnect.getProtocol().equals("https")) { + throw new SessionNotCreatedException( + String.format("The given protocol '%s' as the direct connection url returned by " + + "the remote server is not accurate. Only 'https' is supported.", + directConnect.getProtocol())); + } + + URL newUrl; + try { + newUrl = directConnect.getUrl(); + } catch (MalformedURLException e) { + throw new SessionNotCreatedException(e.getMessage()); + } + + overrideServerUrl(newUrl); } @Override @@ -197,8 +243,7 @@ public Response execute(Command command) throws WebDriverException { } return new WebDriverException("The appium server has accidentally died!", rootCause); - }).orElseGet((Supplier) () -> - new WebDriverException(rootCause.getMessage(), rootCause)); + }).orElseGet(() -> new WebDriverException(rootCause.getMessage(), rootCause)); } throwIfUnchecked(t); throw new WebDriverException(t); diff --git a/src/main/java/io/appium/java_client/remote/AppiumNewSessionCommandPayload.java b/src/main/java/io/appium/java_client/remote/AppiumNewSessionCommandPayload.java index a57170d6c..31635dabb 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumNewSessionCommandPayload.java +++ b/src/main/java/io/appium/java_client/remote/AppiumNewSessionCommandPayload.java @@ -16,17 +16,23 @@ package io.appium.java_client.remote; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import io.appium.java_client.remote.options.BaseOptions; import org.openqa.selenium.Capabilities; import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.CommandPayload; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION; +/** + * This class is deprecated and will be removed. + * + * @deprecated Use CommandPayload instead. + */ +@Deprecated public class AppiumNewSessionCommandPayload extends CommandPayload { /** * Appends "appium:" prefix to all non-prefixed non-standard capabilities. @@ -37,7 +43,7 @@ public class AppiumNewSessionCommandPayload extends CommandPayload { private static Map makeW3CSafe(Capabilities possiblyInvalidCapabilities) { return Require.nonNull("Capabilities", possiblyInvalidCapabilities) .asMap().entrySet().stream() - .collect(ImmutableMap.toImmutableMap( + .collect(Collectors.toUnmodifiableMap( entry -> BaseOptions.toW3cName(entry.getKey()), Map.Entry::getValue )); @@ -50,8 +56,8 @@ private static Map makeW3CSafe(Capabilities possiblyInvalidCapab * @param capabilities User-provided capabilities. */ public AppiumNewSessionCommandPayload(Capabilities capabilities) { - super(NEW_SESSION, ImmutableMap.of( - "capabilities", ImmutableSet.of(makeW3CSafe(capabilities)), + super(NEW_SESSION, Map.of( + "capabilities", Set.of(makeW3CSafe(capabilities)), "desiredCapabilities", capabilities )); } diff --git a/src/main/java/io/appium/java_client/remote/AppiumProtocolHandshake.java b/src/main/java/io/appium/java_client/remote/AppiumProtocolHandshake.java index 98b128554..ef2f659da 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumProtocolHandshake.java +++ b/src/main/java/io/appium/java_client/remote/AppiumProtocolHandshake.java @@ -16,121 +16,13 @@ package io.appium.java_client.remote; -import com.google.common.io.CountingOutputStream; -import com.google.common.io.FileBackedOutputStream; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.ImmutableCapabilities; -import org.openqa.selenium.SessionNotCreatedException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.internal.Either; -import org.openqa.selenium.json.Json; -import org.openqa.selenium.json.JsonOutput; -import org.openqa.selenium.remote.Command; -import org.openqa.selenium.remote.NewSessionPayload; import org.openqa.selenium.remote.ProtocolHandshake; -import org.openqa.selenium.remote.http.HttpHandler; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import static java.nio.charset.StandardCharsets.UTF_8; - -@SuppressWarnings("UnstableApiUsage") +/** + * This class is deprecated and should be removed. + * + * @deprecated Use ProtocolHandshake instead. + */ +@Deprecated public class AppiumProtocolHandshake extends ProtocolHandshake { - private static void writeJsonPayload(NewSessionPayload srcPayload, Appendable destination) { - try (JsonOutput json = new Json().newOutput(destination)) { - json.beginObject(); - - json.name("capabilities"); - json.beginObject(); - - json.name("firstMatch"); - json.beginArray(); - json.beginObject(); - json.endObject(); - json.endArray(); - - json.name("alwaysMatch"); - try { - Method getW3CMethod = NewSessionPayload.class.getDeclaredMethod("getW3C"); - getW3CMethod.setAccessible(true); - //noinspection unchecked - ((Stream>) getW3CMethod.invoke(srcPayload)) - .findFirst() - .map(json::write) - .orElseGet(() -> { - json.beginObject(); - json.endObject(); - return null; - }); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw new WebDriverException(e); - } - - json.endObject(); // Close "capabilities" object - - try { - Method writeMetaDataMethod = NewSessionPayload.class.getDeclaredMethod( - "writeMetaData", JsonOutput.class); - writeMetaDataMethod.setAccessible(true); - writeMetaDataMethod.invoke(srcPayload, json); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw new WebDriverException(e); - } - - json.endObject(); - } - } - - @Override - public Result createSession(HttpHandler client, Command command) throws IOException { - //noinspection unchecked - Capabilities desired = ((Set>) command.getParameters().get("capabilities")) - .stream() - .findAny() - .map(ImmutableCapabilities::new) - .orElseGet(ImmutableCapabilities::new); - try (NewSessionPayload payload = NewSessionPayload.create(desired)) { - Either result = createSession(client, payload); - if (result.isRight()) { - return result.right(); - } - throw result.left(); - } - } - - @Override - public Either createSession( - HttpHandler client, NewSessionPayload payload) throws IOException { - int threshold = (int) Math.min(Runtime.getRuntime().freeMemory() / 10, Integer.MAX_VALUE); - FileBackedOutputStream os = new FileBackedOutputStream(threshold); - - try (CountingOutputStream counter = new CountingOutputStream(os); - Writer writer = new OutputStreamWriter(counter, UTF_8)) { - writeJsonPayload(payload, writer); - - try (InputStream rawIn = os.asByteSource().openBufferedStream(); - BufferedInputStream contentStream = new BufferedInputStream(rawIn)) { - Method createSessionMethod = ProtocolHandshake.class.getDeclaredMethod("createSession", - HttpHandler.class, InputStream.class, long.class); - createSessionMethod.setAccessible(true); - //noinspection unchecked - return (Either) createSessionMethod.invoke( - this, client, contentStream, counter.getCount() - ); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw new WebDriverException(e); - } - } finally { - os.reset(); - } - } } diff --git a/src/main/java/io/appium/java_client/remote/AppiumW3CHttpCommandCodec.java b/src/main/java/io/appium/java_client/remote/AppiumW3CHttpCommandCodec.java index cd2d6991c..1fc6943a3 100644 --- a/src/main/java/io/appium/java_client/remote/AppiumW3CHttpCommandCodec.java +++ b/src/main/java/io/appium/java_client/remote/AppiumW3CHttpCommandCodec.java @@ -16,6 +16,10 @@ package io.appium.java_client.remote; +import org.openqa.selenium.remote.codec.w3c.W3CHttpCommandCodec; + +import java.util.Map; + import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_ATTRIBUTE; import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_LOCATION; import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW; @@ -26,10 +30,6 @@ import static org.openqa.selenium.remote.DriverCommand.SET_TIMEOUT; import static org.openqa.selenium.remote.DriverCommand.SUBMIT_ELEMENT; -import org.openqa.selenium.remote.codec.w3c.W3CHttpCommandCodec; - -import java.util.Map; - public class AppiumW3CHttpCommandCodec extends W3CHttpCommandCodec { /** * This class overrides the built-in Selenium W3C commands codec, diff --git a/src/main/java/io/appium/java_client/remote/AutomationName.java b/src/main/java/io/appium/java_client/remote/AutomationName.java index e38474dbc..e941d516b 100644 --- a/src/main/java/io/appium/java_client/remote/AutomationName.java +++ b/src/main/java/io/appium/java_client/remote/AutomationName.java @@ -18,8 +18,6 @@ public interface AutomationName { // Officially supported drivers - @Deprecated - String APPIUM = "Appium"; // https://github.com/appium/appium-xcuitest-driver String IOS_XCUI_TEST = "XCuiTest"; // https://github.com/appium/appium-uiautomator2-driver @@ -34,8 +32,12 @@ public interface AutomationName { String SAFARI = "Safari"; // https://github.com/appium/appium-geckodriver String GECKO = "Gecko"; + // https://github.com/appium/appium-chromium-driver + String CHROMIUM = "Chromium"; // Third-party drivers // https://github.com/YOU-i-Labs/appium-youiengine-driver String YOUI_ENGINE = "youiengine"; + //https://github.com/AppiumTestDistribution/appium-flutter-integration-driver + String FLUTTER_INTEGRATION = "FlutterIntegration"; } diff --git a/src/main/java/io/appium/java_client/remote/DirectConnect.java b/src/main/java/io/appium/java_client/remote/DirectConnect.java new file mode 100644 index 000000000..fb1a05c51 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/DirectConnect.java @@ -0,0 +1,91 @@ +/* + * 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; + +import lombok.AccessLevel; +import lombok.Getter; +import org.jspecify.annotations.Nullable; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; + +public class DirectConnect { + private static final String DIRECT_CONNECT_PROTOCOL = "directConnectProtocol"; + private static final String DIRECT_CONNECT_PATH = "directConnectPath"; + private static final String DIRECT_CONNECT_HOST = "directConnectHost"; + private static final String DIRECT_CONNECT_PORT = "directConnectPort"; + + @Getter(AccessLevel.PUBLIC) private final String protocol; + + @Getter(AccessLevel.PUBLIC) private final String path; + + @Getter(AccessLevel.PUBLIC) private final String host; + + @Getter(AccessLevel.PUBLIC) private final String port; + + /** + * Create a DirectConnect instance. + * @param responseValue is the response body + */ + public DirectConnect(Map responseValue) { + this.protocol = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_PROTOCOL); + this.path = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_PATH); + this.host = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_HOST); + this.port = this.getDirectConnectValue(responseValue, DIRECT_CONNECT_PORT); + } + + @Nullable + private String getDirectConnectValue(Map responseValue, String key) { + Object directConnectPath = responseValue.get(APPIUM_PREFIX + key); + if (directConnectPath != null) { + return String.valueOf(directConnectPath); + } + directConnectPath = responseValue.get(key); + return directConnectPath == null ? null : String.valueOf(directConnectPath); + } + + /** + * Returns true if the {@link DirectConnect} instance member has nonnull values. + * @return true if each connection information has a nonnull value + */ + public boolean isValid() { + return Stream.of(this.protocol, this.path, this.host, this.port).noneMatch(Objects::isNull); + } + + /** + * Returns a URL instance built with members in the DirectConnect instance. + * @return A URL object + * @throws MalformedURLException if the built url was invalid + */ + public URL getUrl() throws MalformedURLException { + String newUrlCandidate = String.format("%s://%s:%s%s", this.protocol, this.host, this.port, this.path); + + try { + return new URL(newUrlCandidate); + } catch (MalformedURLException e) { + throw new MalformedURLException( + String.format("The remote server returned an invalid value to build the direct connect URL: %s", + newUrlCandidate) + ); + } + } +} diff --git a/src/main/java/io/appium/java_client/remote/IOSMobileCapabilityType.java b/src/main/java/io/appium/java_client/remote/IOSMobileCapabilityType.java deleted file mode 100644 index 410e551a9..000000000 --- a/src/main/java/io/appium/java_client/remote/IOSMobileCapabilityType.java +++ /dev/null @@ -1,385 +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.remote; - -import org.openqa.selenium.remote.CapabilityType; - -/** - * The list of iOS-specific capabilities.
- * Read:
- * - * https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md#ios-only - *
- * and
- * - * https://github.com/appium/appium-xcuitest-driver#desired-capabilities - */ -public interface IOSMobileCapabilityType extends CapabilityType { - - /** - * (Sim-only) Calendar format to set for the iOS Simulator. - */ - String CALENDAR_FORMAT = "calendarFormat"; - - /** - * Bundle ID of the app under test. Useful for starting an app on a real device - * or for using other caps which require the bundle ID during test startup. - * To run a test on a real device using the bundle ID, - * you may omit the 'app' capability, but you must provide 'udid'. - */ - String BUNDLE_ID = "bundleId"; - - /** - * (Sim-only) Force location services to be either on or off. - * Default is to keep current sim setting. - */ - String LOCATION_SERVICES_ENABLED = "locationServicesEnabled"; - - /** - * (Sim-only) Set location services to be authorized or not authorized for app via plist, - * so that location services alert doesn't pop up. Default is to keep current sim - * setting. Note that if you use this setting you MUST also use the bundleId - * capability to send in your app's bundle ID. - */ - String LOCATION_SERVICES_AUTHORIZED = "locationServicesAuthorized"; - - /** - * Accept all iOS alerts automatically if they pop up. - * This includes privacy access permission alerts - * (e.g., location, contacts, photos). Default is false. - */ - String AUTO_ACCEPT_ALERTS = "autoAcceptAlerts"; - - /** - * Dismiss all iOS alerts automatically if they pop up. - * This includes privacy access permission alerts (e.g., - * location, contacts, photos). Default is false. - */ - String AUTO_DISMISS_ALERTS = "autoDismissAlerts"; - - /** - * Use native intruments lib (ie disable instruments-without-delay). - */ - String NATIVE_INSTRUMENTS_LIB = "nativeInstrumentsLib"; - - /** - * Enable "real", non-javascript-based web taps in Safari. - * Default: false. - * Warning: depending on viewport size/ratio this might not accurately tap an element. - */ - String NATIVE_WEB_TAP = "nativeWebTap"; - - /** - * (Sim-only) (>= 8.1) Initial safari url, default is a local welcome page. - */ - String SAFARI_INITIAL_URL = "safariInitialUrl"; - - /** - * (Sim-only) Allow javascript to open new windows in Safari. Default keeps current sim - * setting. - */ - String SAFARI_ALLOW_POPUPS = "safariAllowPopups"; - - /** - * (Sim-only) Prevent Safari from showing a fraudulent website warning. - * Default keeps current sim setting. - */ - String SAFARI_IGNORE_FRAUD_WARNING = "safariIgnoreFraudWarning"; - - /** - * (Sim-only) Whether Safari should allow links to open in new windows. - * Default keeps current sim setting. - */ - String SAFARI_OPEN_LINKS_IN_BACKGROUND = "safariOpenLinksInBackground"; - - /** - * (Sim-only) Whether to keep keychains (Library/Keychains) when appium - * session is started/finished. - */ - String KEEP_KEY_CHAINS = "keepKeyChains"; - - /** - * Where to look for localizable strings. Default en.lproj. - */ - String LOCALIZABLE_STRINGS_DIR = "localizableStringsDir"; - - /** - * Arguments to pass to the AUT using instruments. - */ - String PROCESS_ARGUMENTS = "processArguments"; - - /** - * The delay, in ms, between keystrokes sent to an element when typing. - */ - String INTER_KEY_DELAY = "interKeyDelay"; - - /** - * Whether to show any logs captured from a device in the appium logs. Default false. - */ - String SHOW_IOS_LOG = "showIOSLog"; - - /** - * strategy to use to type test into a test field. Simulator default: oneByOne. - * Real device default: grouped. - */ - String SEND_KEY_STRATEGY = "sendKeyStrategy"; - - /** - * Max timeout in sec to wait for a screenshot to be generated. default: 10. - */ - String SCREENSHOT_WAIT_TIMEOUT = "screenshotWaitTimeout"; - - /** - * The ios automation script used to determined if the app has been launched, - * by default the system wait for the page source not to be empty. - * The result must be a boolean. - */ - String WAIT_FOR_APP_SCRIPT = "waitForAppScript"; - - /** - * Number of times to send connection message to remote debugger, to get webview. - * Default: 8. - */ - String WEBVIEW_CONNECT_RETRIES = "webviewConnectRetries"; - - /** - * The display name of the application under test. Used to automate backgrounding - * the app in iOS 9+. - */ - String APP_NAME = "appName"; - - /** - * (Sim only) Add an SSL certificate to IOS Simulator. - */ - String CUSTOM_SSL_CERT = "customSSLCert"; - - /** - * The desired capability to specify a length for tapping, if the regular - * tap is too long for the app under test. The XCUITest specific capability. - */ - String TAP_WITH_SHORT_PRESS_DURATION = "tapWithShortPressDuration"; - - /** - * Simulator scale factor. - * This is useful to have if the default resolution of simulated device is - * greater than the actual display resolution. So you can scale the simulator - * to see the whole device screen without scrolling. - * This capability only works below Xcode9. - */ - String SCALE_FACTOR = "scaleFactor"; - - /** - * This value if specified, will be used to forward traffic from Mac - * host to real ios devices over USB. Default value is same as port - * number used by WDA on device. - * eg: 8100 - */ - String WDA_LOCAL_PORT = "wdaLocalPort"; - - /** - * Whether to display the output of the Xcode command - * used to run the tests.If this is true, - * there will be lots of extra logging at startup. Defaults to false - */ - String SHOW_XCODE_LOG = "showXcodeLog"; - - /** - * Time in milliseconds to pause between installing the application - * and starting WebDriverAgent on the device. Used particularly for larger applications. - * Defaults to 0 - */ - String IOS_INSTALL_PAUSE = "iosInstallPause"; - - /** - * Full path to an optional Xcode configuration file that - * specifies the code signing identity - * and team for running the WebDriverAgent on the real device. - * e.g., /path/to/myconfig.xcconfig - */ - String XCODE_CONFIG_FILE = "xcodeConfigFile"; - - /** - * Password for unlocking keychain specified in keychainPath. - */ - String KEYCHAIN_PASSWORD = "keychainPassword"; - - /** - * Skips the build phase of running the WDA app. - * Building is then the responsibility of the user. - * Only works for Xcode 8+. Defaults to false - */ - String USE_PREBUILT_WDA = "usePrebuiltWDA"; - - /** - * Sets read only permissons to Attachments subfolder of WebDriverAgent - * root inside Xcode's DerivedData. - * This is necessary to prevent XCTest framework from - * creating tons of unnecessary screenshots and logs, - * which are impossible to shutdown using programming - * interfaces provided by Apple - * - * @deprecated This capability was deleted at Appium 1.14.0 - */ - @Deprecated - String PREVENT_WDAATTACHMENTS = "preventWDAAttachments"; - - /** - * Appium will connect to an existing WebDriverAgent, - * instance at this URL instead of starting a new one. - * eg : http://localhost:8100 - */ - String WEB_DRIVER_AGENT_URL = "webDriverAgentUrl"; - - /** - * Full path to the private development key exported - * from the system keychain. Used in conjunction - * with keychainPassword when testing on real devices. - * e.g., /path/to/MyPrivateKey.p12 - */ - String KEYCHAIN_PATH = "keychainPath"; - - /** - * If {@code true}, forces uninstall of any existing WebDriverAgent app on device. - * Set it to {@code true} if you want to apply different startup options for WebDriverAgent for each session. - * Although, it is only guaranteed to work stable on Simulator. Real devices require WebDriverAgent - * client to run for as long as possible without reinstall/restart to avoid issues like - * - * https://github.com/facebook/WebDriverAgent/issues/507. - * The {@code false} value (the default behaviour since driver version 2.35.0) will try to detect currently - * running WDA listener executed by previous testing session(s) and reuse it if possible, which is - * highly recommended for real device testing and to speed up suites of multiple tests in general. - * A new WDA session will be triggered at the default URL (http://localhost:8100) if WDA is not - * listening and {@code webDriverAgentUrl} capability is not set. The negative/unset value of {@code useNewWDA} - * capability has no effect prior to xcuitest driver version 2.35.0. - */ - String USE_NEW_WDA = "useNewWDA"; - - /** - * Time, in ms, to wait for WebDriverAgent to be pingable. Defaults to 60000ms. - */ - String WDA_LAUNCH_TIMEOUT = "wdaLaunchTimeout"; - - /** - * Timeout, in ms, for waiting for a response from WebDriverAgent. Defaults to 240000ms. - */ - String WDA_CONNECTION_TIMEOUT = "wdaConnectionTimeout"; - - /** - * Apple developer team identifier string. - * Must be used in conjunction with xcodeSigningId to take effect. - * e.g., JWL241K123 - */ - String XCODE_ORG_ID = "xcodeOrgId"; - - /** - * String representing a signing certificate. - * Must be used in conjunction with xcodeOrgId. - * This is usually just iPhone Developer, so the default (if not included) is iPhone Developer - */ - String XCODE_SIGNING_ID = "xcodeSigningId"; - - /** - * Bundle id to update WDA to before building and launching on real devices. - * This bundle id must be associated with a valid provisioning profile. - * e.g., io.appium.WebDriverAgentRunner. - */ - String UPDATE_WDA_BUNDLEID = "updatedWDABundleId"; - - /** - * Whether to perform reset on test session finish (false) or not (true). - * Keeping this variable set to true and Simulator running - * (the default behaviour since version 1.6.4) may significantly shorten the - * duration of test session initialization. - * Defaults to true. - */ - String RESET_ON_SESSION_START_ONLY = "resetOnSessionStartOnly"; - - /** - * Custom timeout(s) in milliseconds for WDA backend commands execution. - * This might be useful if WDA backend freezes unexpectedly or requires - * too much time to fail and blocks automated test execution. - * The value is expected to be of type string and can either contain - * max milliseconds to wait for each WDA command to be executed before - * terminating the session forcefully or a valid JSON string, - * where keys are internal Appium command names (you can find these in logs, - * look for "Executing command 'command_name'" records) and values are - * timeouts in milliseconds. You can also set the 'default' key to assign - * the timeout for all other commands not explicitly enumerated as JSON keys. - */ - String COMMAND_TIMEOUTS = "commandTimeouts"; - - /** - * Number of times to try to build and launch WebDriverAgent onto the device. - * Defaults to 2. - */ - String WDA_STARTUP_RETRIES = "wdaStartupRetries"; - - /** - * Time, in ms, to wait between tries to build and launch WebDriverAgent. - * Defaults to 10000ms. - */ - String WDA_STARTUP_RETRY_INTERVAL = "wdaStartupRetryInterval"; - - /** - * Set this option to true in order to enable hardware keyboard in Simulator. - * It is set to false by default, because this helps to workaround some XCTest bugs. - */ - String CONNECT_HARDWARE_KEYBOARD = "connectHardwareKeyboard"; - - /** - * Maximum frequency of keystrokes for typing and clear. - * If your tests are failing because of typing errors, you may want to adjust this. - * Defaults to 60 keystrokes per minute. - */ - String MAX_TYPING_FREQUENCY = "maxTypingFrequency"; - - /** - * Use native methods for determining visibility of elements. - * In some cases this takes a long time. - * Setting this capability to false will cause the system to use the position - * and size of elements to make sure they are visible on the screen. - * This can, however, lead to false results in some situations. - * Defaults to false, except iOS 9.3, where it defaults to true. - */ - String SIMPLE_ISVISIBLE_CHECK = "simpleIsVisibleCheck"; - - /** - * Use SSL to download dependencies for WebDriverAgent. Defaults to false. - */ - String USE_CARTHAGE_SSL = "useCarthageSsl"; - - /** - * Use default proxy for test management within WebDriverAgent. - * Setting this to false sometimes helps with socket hangup problems. - * Defaults to true. - */ - String SHOULD_USE_SINGLETON_TESTMANAGER = "shouldUseSingletonTestManager"; - - /** - * Set this to true if you want to start ios_webkit_debug proxy server - * automatically for accessing webviews on iOS. - * The capatibility only works for real device automation. - * Defaults to false. - */ - String START_IWDP = "startIWDP"; - - /** - * Enrolls simulator for touch id. Defaults to false. - */ - String ALLOW_TOUCHID_ENROLL = "allowTouchIdEnroll"; - -} diff --git a/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java b/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java deleted file mode 100644 index f1aba4b9c..000000000 --- a/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java +++ /dev/null @@ -1,139 +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.remote; - -import org.openqa.selenium.remote.CapabilityType; - -/** - * The list of common capabilities.
- * Read:
- * - * https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md#general-capabilities - */ -public interface MobileCapabilityType extends CapabilityType { - - /** - * Which automation engine to use. - */ - String AUTOMATION_NAME = "automationName"; - - /** - * Mobile OS version. - */ - String PLATFORM_VERSION = "platformVersion"; - - /** - * The kind of mobile device or emulator to use. - */ - String DEVICE_NAME = "deviceName"; - - /** - * How long (in seconds) Appium will wait for a new command from the - * client before assuming the client quit and ending the session. - */ - String NEW_COMMAND_TIMEOUT = "newCommandTimeout"; - - /** - * The absolute local path or remote http URL to a {@code .ipa} file (IOS), - * {@code .app} folder (IOS Simulator), {@code .apk} file (Android) or {@code .apks} file (Android App Bundle), - * or a {@code .zip} file containing one of these (for .app, the .app folder must be the root of the zip file). - * Appium will attempt to install this app binary on the appropriate device first. - * Note that this capability is not required for Android if you specify {@code appPackage} - * and {@code appActivity} capabilities (see below). Incompatible with {@code browserName}. See - * - * here - * about {@code .apks} file. - */ - String APP = "app"; - - /** - * Unique device identifier of the connected physical device. - */ - String UDID = "udid"; - - - /** - * Language to set for iOS (XCUITest driver only) and Android. - */ - String LANGUAGE = "language"; - - /** - * Locale to set for iOS (XCUITest driver only) and Android. - * {@code fr_CA} format for iOS. {@code CA} format (country name abbreviation) for Android - */ - String LOCALE = "locale"; - - /** - * (Sim/Emu-only) start in a certain orientation. - */ - String ORIENTATION = "orientation"; - - /** - * Move directly into Webview context. Default false. - */ - String AUTO_WEBVIEW = "autoWebview"; - - /** - * Don't reset app state before this session. See - * - * here - * for more detail. - */ - String NO_RESET = "noReset"; - - /** - * Perform a complete reset. See - * - * here - * for more detail. - */ - String FULL_RESET = "fullReset"; - - /** - * The desired capability which specifies whether to delete any generated files at - * the end of a session (see iOS and Android entries for particulars). - */ - String CLEAR_SYSTEM_FILES = "clearSystemFiles"; - - /** - * Enable or disable the reporting of the timings for various Appium-internal events - * (e.g., the start and end of each command, etc.). Defaults to {@code false}. - * To enable, use {@code true}. The timings are then reported as {@code events} property on response - * to querying the current session. See the - * - * event timing docs for the the structure of this response. - */ - String EVENT_TIMINGS = "eventTimings"; - - /** - * (Web and webview only) Enable ChromeDriver's (on Android) - * or Safari's (on iOS) performance logging (default {@code false}). - */ - String ENABLE_PERFORMANCE_LOGGING = "enablePerformanceLogging"; - - - /** - * App or list of apps (as a JSON array) to install prior to running tests. Note that it will not work with - * automationName of Espresso and iOS real devices. - */ - String OTHER_APPS = "otherApps"; - - /** - * When a find operation fails, print the current page source. Defaults to false. - */ - String PRINT_PAGE_SOURCE_ON_FIND_FAILURE = "printPageSourceOnFindFailure"; -} diff --git a/src/main/java/io/appium/java_client/remote/MobileOptions.java b/src/main/java/io/appium/java_client/remote/MobileOptions.java deleted file mode 100644 index 98bd098b7..000000000 --- a/src/main/java/io/appium/java_client/remote/MobileOptions.java +++ /dev/null @@ -1,512 +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.remote; - -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.MutableCapabilities; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.remote.CapabilityType; - -import java.net.URL; -import java.time.Duration; - -/** - * Use the specific options class for your driver, - * for example XCUITestOptions or UiAutomator2Options. - * - * @param The child class for a proper chaining. - */ -@Deprecated -public class MobileOptions> extends MutableCapabilities { - - /** - * Creates new instance with no preset capabilities. - */ - public MobileOptions() { - } - - /** - * Creates new instance with provided capabilities capabilities. - * - * @param source is Capabilities instance to merge into new instance - */ - public MobileOptions(Capabilities source) { - super(source); - } - - /** - * Set the kind of mobile device or emulator to use. - * - * @param platform the kind of mobile device or emulator to use. - * @return this MobileOptions, for chaining. - * @see org.openqa.selenium.remote.CapabilityType#PLATFORM_NAME - */ - public T setPlatformName(String platform) { - return amend(CapabilityType.PLATFORM_NAME, platform); - } - - /** - * Set the absolute local path for the location of the App. - * The or remote http URL to a {@code .ipa} file (IOS), - * - * @param path is a String representing the location of the App - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#APP - */ - public T setApp(String path) { - return amend(MobileCapabilityType.APP, path); - } - - /** - * Set the remote http URL for the location of the App. - * - * @param url is the URL representing the location of the App - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#APP - */ - public T setApp(URL url) { - return setApp(url.toString()); - } - - /** - * Get the app location. - * - * @return String representing app location - * @see MobileCapabilityType#APP - */ - public String getApp() { - return (String) getCapability(MobileCapabilityType.APP); - } - - /** - * Set the automation engine to use. - * - * @param name is the name of the automation engine - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#AUTOMATION_NAME - */ - public T setAutomationName(String name) { - return amend(MobileCapabilityType.AUTOMATION_NAME, name); - } - - /** - * Get the automation engine to use. - * - * @return String representing the name of the automation engine - * @see MobileCapabilityType#AUTOMATION_NAME - */ - public String getAutomationName() { - return (String) getCapability(MobileCapabilityType.AUTOMATION_NAME); - } - - /** - * Set the app to move directly into Webview context. - * - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#AUTO_WEBVIEW - */ - public T setAutoWebview() { - return setAutoWebview(true); - } - - /** - * Set whether the app moves directly into Webview context. - * - * @param bool is whether the app moves directly into Webview context. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#AUTO_WEBVIEW - */ - public T setAutoWebview(boolean bool) { - return amend(MobileCapabilityType.AUTO_WEBVIEW, bool); - } - - /** - * Get whether the app moves directly into Webview context. - * - * @return true if app moves directly into Webview context. - * @see MobileCapabilityType#AUTO_WEBVIEW - */ - public boolean doesAutoWebview() { - return (boolean) getCapability(MobileCapabilityType.AUTO_WEBVIEW); - } - - /** - * Set the app to delete any generated files at the end of a session. - * - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#CLEAR_SYSTEM_FILES - */ - public T setClearSystemFiles() { - return setClearSystemFiles(true); - } - - /** - * Set whether the app deletes generated files at the end of a session. - * - * @param bool is whether the app deletes generated files at the end of a session. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#CLEAR_SYSTEM_FILES - */ - public T setClearSystemFiles(boolean bool) { - return amend(MobileCapabilityType.CLEAR_SYSTEM_FILES, bool); - } - - /** - * Get whether the app deletes generated files at the end of a session. - * - * @return true if the app deletes generated files at the end of a session. - * @see MobileCapabilityType#CLEAR_SYSTEM_FILES - */ - public boolean doesClearSystemFiles() { - return (boolean) getCapability(MobileCapabilityType.CLEAR_SYSTEM_FILES); - } - - /** - * Set the name of the device. - * - * @param deviceName is the name of the device. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#DEVICE_NAME - */ - public T setDeviceName(String deviceName) { - return amend(MobileCapabilityType.DEVICE_NAME, deviceName); - } - - /** - * Get the name of the device. - * - * @return String representing the name of the device. - * @see MobileCapabilityType#DEVICE_NAME - */ - public String getDeviceName() { - return (String) getCapability(MobileCapabilityType.DEVICE_NAME); - } - - /** - * Set the app to enable performance logging. - * - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#ENABLE_PERFORMANCE_LOGGING - */ - public T setEnablePerformanceLogging() { - return setEnablePerformanceLogging(true); - } - - /** - * Set whether the app logs performance. - * - * @param bool is whether the app logs performance. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#ENABLE_PERFORMANCE_LOGGING - */ - public T setEnablePerformanceLogging(boolean bool) { - return amend(MobileCapabilityType.ENABLE_PERFORMANCE_LOGGING, bool); - } - - /** - * Get the app logs performance. - * - * @return true if the app logs performance. - * @see MobileCapabilityType#ENABLE_PERFORMANCE_LOGGING - */ - public boolean isEnablePerformanceLogging() { - return (boolean) getCapability(MobileCapabilityType.ENABLE_PERFORMANCE_LOGGING); - } - - /** - * Set the app to report the timings for various Appium-internal events. - * - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#EVENT_TIMINGS - */ - public T setEventTimings() { - return setEventTimings(true); - } - - /** - * Set whether the app reports the timings for various Appium-internal events. - * - * @param bool is whether the app enables event timings. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#EVENT_TIMINGS - */ - public T setEventTimings(boolean bool) { - return amend(MobileCapabilityType.EVENT_TIMINGS, bool); - } - - /** - * Get whether the app reports the timings for various Appium-internal events. - * - * @return true if the app reports event timings. - * @see MobileCapabilityType#EVENT_TIMINGS - */ - public boolean doesEventTimings() { - return (boolean) getCapability(MobileCapabilityType.EVENT_TIMINGS); - } - - /** - * Set the app to do a full reset. - * - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#FULL_RESET - */ - public T setFullReset() { - return setFullReset(true); - } - - /** - * Set whether the app does a full reset. - * - * @param bool is whether the app does a full reset. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#FULL_RESET - */ - public T setFullReset(boolean bool) { - return amend(MobileCapabilityType.FULL_RESET, bool); - } - - /** - * Get whether the app does a full reset. - * - * @return true if the app does a full reset. - * @see MobileCapabilityType#FULL_RESET - */ - public boolean doesFullReset() { - return (boolean) getCapability(MobileCapabilityType.FULL_RESET); - } - - /** - * Set language abbreviation for use in session. - * - * @param language is the language abbreviation. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#LANGUAGE - */ - public T setLanguage(String language) { - return amend(MobileCapabilityType.LANGUAGE, language); - } - - /** - * Get language abbreviation for use in session. - * - * @return String representing the language abbreviation. - * @see MobileCapabilityType#LANGUAGE - */ - public String getLanguage() { - return (String) getCapability(MobileCapabilityType.LANGUAGE); - } - - /** - * Set locale abbreviation for use in session. - * - * @param locale is the locale abbreviation. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#LOCALE - */ - public T setLocale(String locale) { - return amend(MobileCapabilityType.LOCALE, locale); - } - - /** - * Get locale abbreviation for use in session. - * - * @return String representing the locale abbreviation. - * @see MobileCapabilityType#LOCALE - */ - public String getLocale() { - return (String) getCapability(MobileCapabilityType.LOCALE); - } - - /** - * Set the timeout for new commands. - * - * @param duration is the allowed time before seeing a new command. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#NEW_COMMAND_TIMEOUT - */ - public T setNewCommandTimeout(Duration duration) { - return amend(MobileCapabilityType.NEW_COMMAND_TIMEOUT, duration.getSeconds()); - } - - /** - * Get the timeout for new commands. - * - * @return allowed time before seeing a new command. - * @see MobileCapabilityType#NEW_COMMAND_TIMEOUT - */ - public Duration getNewCommandTimeout() { - Object duration = getCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT); - return Duration.ofSeconds(Long.parseLong("" + duration)); - } - - /** - * Set the app not to do a reset. - * - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#NO_RESET - */ - public T setNoReset() { - return setNoReset(true); - } - - /** - * Set whether the app does not do a reset. - * - * @param bool is whether the app does not do a reset. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#NO_RESET - */ - public T setNoReset(boolean bool) { - return amend(MobileCapabilityType.NO_RESET, bool); - } - - /** - * Get whether the app does not do a reset. - * - * @return true if the app does not do a reset. - * @see MobileCapabilityType#NO_RESET - */ - public boolean doesNoReset() { - return (boolean) getCapability(MobileCapabilityType.NO_RESET); - } - - /** - * Set the orientation of the screen. - * - * @param orientation is the screen orientation. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#ORIENTATION - */ - public T setOrientation(ScreenOrientation orientation) { - return amend(MobileCapabilityType.ORIENTATION, orientation); - } - - /** - * Get the orientation of the screen. - * - * @return ScreenOrientation of the app. - * @see MobileCapabilityType#ORIENTATION - */ - public ScreenOrientation getOrientation() { - return (ScreenOrientation) getCapability(MobileCapabilityType.ORIENTATION); - } - - /** - * Set the location of the app(s) to install before running a test. - * - * @param apps is the apps to install. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#OTHER_APPS - */ - public T setOtherApps(String apps) { - return amend(MobileCapabilityType.OTHER_APPS, apps); - } - - /** - * Get the list of apps to install before running a test. - * - * @return String of apps to install. - * @see MobileCapabilityType#OTHER_APPS - */ - public String getOtherApps() { - return (String) getCapability(MobileCapabilityType.OTHER_APPS); - } - - /** - * Set the version of the platform. - * - * @param version is the platform version. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#PLATFORM_VERSION - */ - public T setPlatformVersion(String version) { - return amend(MobileCapabilityType.PLATFORM_VERSION, version); - } - - /** - * Get the version of the platform. - * - * @return String representing the platform version. - * @see MobileCapabilityType#PLATFORM_VERSION - */ - public String getPlatformVersion() { - return (String) getCapability(MobileCapabilityType.PLATFORM_VERSION); - } - - /** - * Set the app to print page source when a find operation fails. - * - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#PRINT_PAGE_SOURCE_ON_FIND_FAILURE - */ - public T setPrintPageSourceOnFindFailure() { - return setPrintPageSourceOnFindFailure(true); - } - - /** - * Set whether the app to print page source when a find operation fails. - * - * @param bool is whether to print page source. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#PRINT_PAGE_SOURCE_ON_FIND_FAILURE - */ - public T setPrintPageSourceOnFindFailure(boolean bool) { - return amend(MobileCapabilityType.PRINT_PAGE_SOURCE_ON_FIND_FAILURE, bool); - } - - /** - * Get whether the app to print page source when a find operation fails. - * - * @return true if app prints page source. - * @see MobileCapabilityType#PRINT_PAGE_SOURCE_ON_FIND_FAILURE - */ - public boolean doesPrintPageSourceOnFindFailure() { - return (boolean) getCapability(MobileCapabilityType.PRINT_PAGE_SOURCE_ON_FIND_FAILURE); - } - - /** - * Set the id of the device. - * - * @param id is the unique device identifier. - * @return this MobileOptions, for chaining. - * @see MobileCapabilityType#UDID - */ - public T setUdid(String id) { - return amend(MobileCapabilityType.UDID, id); - } - - /** - * Get the id of the device. - * - * @return String representing the unique device identifier. - * @see MobileCapabilityType#UDID - */ - public String getUdid() { - return (String) getCapability(MobileCapabilityType.UDID); - } - - @Override - public T merge(Capabilities extraCapabilities) { - super.merge(extraCapabilities); - return (T) this; - } - - protected T amend(String optionName, Object value) { - setCapability(optionName, value); - return (T) this; - } -} 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 7f5f79956..c576583dc 100644 --- a/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java +++ b/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java @@ -16,23 +16,22 @@ package io.appium.java_client.remote; -import com.google.common.collect.ImmutableMap; 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.DriverCommand; import org.openqa.selenium.remote.Response; -import javax.annotation.Nullable; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.Objects.requireNonNull; -public interface SupportsContextSwitching extends WebDriver, ContextAware, ExecutesMethod { +public interface SupportsContextSwitching extends WebDriver, ExecutesMethod { /** * Switches to the given context. * @@ -40,9 +39,9 @@ public interface SupportsContextSwitching extends WebDriver, ContextAware, Execu * @return self instance for chaining. */ default WebDriver context(String name) { - checkNotNull(name, "Must supply a context name"); + requireNonNull(name, "Must supply a context name"); try { - execute(DriverCommand.SWITCH_TO_CONTEXT, ImmutableMap.of("name", name)); + execute(MobileCommand.SWITCH_TO_CONTEXT, Map.of("name", name)); return this; } catch (WebDriverException e) { throw new NoSuchContextException(e.getMessage(), e); @@ -55,7 +54,7 @@ default WebDriver context(String name) { * @return List list of context names. */ default Set getContextHandles() { - Response response = execute(DriverCommand.GET_CONTEXT_HANDLES, ImmutableMap.of()); + Response response = execute(MobileCommand.GET_CONTEXT_HANDLES, Map.of()); Object value = response.getValue(); try { //noinspection unchecked @@ -75,7 +74,7 @@ default Set getContextHandles() { @Nullable default String getContext() { String contextName = - String.valueOf(execute(DriverCommand.GET_CURRENT_CONTEXT_HANDLE).getValue()); + String.valueOf(execute(MobileCommand.GET_CURRENT_CONTEXT_HANDLE).getValue()); return "null".equalsIgnoreCase(contextName) ? null : contextName; } } 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 b2211dfda..c19dcc96c 100644 --- a/src/main/java/io/appium/java_client/remote/SupportsLocation.java +++ b/src/main/java/io/appium/java_client/remote/SupportsLocation.java @@ -16,19 +16,44 @@ package io.appium.java_client.remote; +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.html5.Location; -import org.openqa.selenium.html5.LocationContext; -import org.openqa.selenium.remote.html5.RemoteLocationContext; +import org.openqa.selenium.WebDriverException; -public interface SupportsLocation extends WebDriver, LocationContext { - public RemoteLocationContext getLocationContext(); +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; - default Location location() { - return getLocationContext().location(); +public interface SupportsLocation extends WebDriver, ExecutesMethod { + + /** + * Gets the current device's geolocation coordinates. + * + * @return A {@link Location} containing the location information. Throws {@link WebDriverException} if the + * location is not available. + */ + default Location getLocation() { + Map result = CommandExecutionHelper.execute(this, MobileCommand.GET_LOCATION); + return new Location( + result.get("latitude").doubleValue(), + result.get("longitude").doubleValue(), + Optional.ofNullable(result.get("altitude")).map(Number::doubleValue).orElse(null) + ); } + /** + * Sets the current device's geolocation coordinates. + * + * @param location A {@link Location} containing the new location information. + */ default void setLocation(Location location) { - getLocationContext().setLocation(location); + var locationParameters = new HashMap(); + locationParameters.put("latitude", location.getLatitude()); + locationParameters.put("longitude", location.getLongitude()); + Optional.ofNullable(location.getAltitude()).ifPresent(altitude -> locationParameters.put("altitude", altitude)); + execute(MobileCommand.SET_LOCATION, Map.of("location", locationParameters)); } } 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 8ac22a707..eb8a52b44 100644 --- a/src/main/java/io/appium/java_client/remote/SupportsRotation.java +++ b/src/main/java/io/appium/java_client/remote/SupportsRotation.java @@ -16,36 +16,36 @@ package io.appium.java_client.remote; -import com.google.common.collect.ImmutableMap; import io.appium.java_client.ExecutesMethod; +import io.appium.java_client.MobileCommand; import org.openqa.selenium.DeviceRotation; -import org.openqa.selenium.Rotatable; import org.openqa.selenium.ScreenOrientation; import org.openqa.selenium.WebDriver; -import org.openqa.selenium.remote.DriverCommand; import org.openqa.selenium.remote.Response; import java.util.Map; -public interface SupportsRotation extends WebDriver, ExecutesMethod, Rotatable { +import static java.util.Locale.ROOT; + +public interface SupportsRotation extends WebDriver, ExecutesMethod { /** * Get device rotation. * * @return The rotation value. */ default DeviceRotation rotation() { - Response response = execute(DriverCommand.GET_SCREEN_ROTATION); + Response response = execute(MobileCommand.GET_SCREEN_ROTATION); //noinspection unchecked return new DeviceRotation((Map) response.getValue()); } default void rotate(DeviceRotation rotation) { - execute(DriverCommand.SET_SCREEN_ROTATION, rotation.parameters()); + execute(MobileCommand.SET_SCREEN_ROTATION, rotation.parameters()); } default void rotate(ScreenOrientation orientation) { - execute(DriverCommand.SET_SCREEN_ORIENTATION, - ImmutableMap.of("orientation", orientation.value().toUpperCase())); + execute(MobileCommand.SET_SCREEN_ORIENTATION, + Map.of("orientation", orientation.value().toUpperCase(ROOT))); } /** @@ -54,8 +54,8 @@ default void rotate(ScreenOrientation orientation) { * @return The orientation value. */ default ScreenOrientation getOrientation() { - Response response = execute(DriverCommand.GET_SCREEN_ORIENTATION); + 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/YouiEngineCapabilityType.java b/src/main/java/io/appium/java_client/remote/YouiEngineCapabilityType.java deleted file mode 100644 index 80301762d..000000000 --- a/src/main/java/io/appium/java_client/remote/YouiEngineCapabilityType.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.appium.java_client.remote; - -import org.openqa.selenium.remote.CapabilityType; - -/** - * The list of youiengine-specific capabilities. - */ -public interface YouiEngineCapabilityType extends CapabilityType { - /** - * IP address of the app to execute commands against. - */ - String APP_ADDRESS = "youiEngineAppAddress"; -} diff --git a/src/main/java/io/appium/java_client/remote/options/BaseMapOptionData.java b/src/main/java/io/appium/java_client/remote/options/BaseMapOptionData.java index a7b75e3d7..dc5ada3a5 100644 --- a/src/main/java/io/appium/java_client/remote/options/BaseMapOptionData.java +++ b/src/main/java/io/appium/java_client/remote/options/BaseMapOptionData.java @@ -26,7 +26,7 @@ public abstract class BaseMapOptionData> { private Map options; - private static final Gson gson = new Gson(); + private static final Gson GSON = new Gson(); public BaseMapOptionData() { } @@ -37,7 +37,7 @@ public BaseMapOptionData(Map options) { public BaseMapOptionData(String json) { //noinspection unchecked - this((Map) gson.fromJson(json, Map.class)); + this((Map) GSON.fromJson(json, Map.class)); } /** @@ -78,11 +78,11 @@ public Map toMap() { } public JsonObject toJson() { - return gson.toJsonTree(toMap()).getAsJsonObject(); + return GSON.toJsonTree(toMap()).getAsJsonObject(); } @Override public String toString() { - return gson.toJson(toMap()); + return GSON.toJson(toMap()); } } 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 7ef52b7e4..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. @@ -96,7 +97,7 @@ public Platform getPlatformName() { } try { - return Platform.fromString((String.valueOf(cap))); + return Platform.fromString(String.valueOf(cap)); } catch (WebDriverException e) { return null; } @@ -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/SupportsAutoWebViewOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsAutoWebViewOption.java index 8a5d7e9fd..3cd2d7e93 100644 --- a/src/main/java/io/appium/java_client/remote/options/SupportsAutoWebViewOption.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsAutoWebViewOption.java @@ -24,7 +24,7 @@ public interface SupportsAutoWebViewOption> extends Capabilities, CanSetCapability { - String AUTO_WEB_VIEW_OPTION = "autoWebView"; + String AUTO_WEB_VIEW_OPTION = "autoWebview"; /** * Set the app to move directly into Webview context. diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsBrowserVersionOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsBrowserVersionOption.java index 4eece6cdb..0e940b5d6 100644 --- a/src/main/java/io/appium/java_client/remote/options/SupportsBrowserVersionOption.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsBrowserVersionOption.java @@ -18,8 +18,6 @@ import org.openqa.selenium.Capabilities; -import java.util.Optional; - public interface SupportsBrowserVersionOption> extends Capabilities, CanSetCapability { String BROWSER_VERSION_OPTION = "browserVersion"; diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsEnforceAppInstallOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsEnforceAppInstallOption.java similarity index 91% rename from src/main/java/io/appium/java_client/android/options/app/SupportsEnforceAppInstallOption.java rename to src/main/java/io/appium/java_client/remote/options/SupportsEnforceAppInstallOption.java index 3acc25875..5e343938c 100644 --- a/src/main/java/io/appium/java_client/android/options/app/SupportsEnforceAppInstallOption.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsEnforceAppInstallOption.java @@ -14,10 +14,8 @@ * limitations under the License. */ -package io.appium.java_client.android.options.app; +package io.appium.java_client.remote.options; -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; @@ -40,7 +38,7 @@ default T enforceAppInstall() { /** * Allows setting whether the application under test is always reinstalled even - * if a newer version of it already exists on the device under test. false by default. + * if a newer version of it already exists on the device under test. False (Android), true (iOS) by default. * * @param value True to allow test packages installation. * @return self instance for chaining. 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 3bc8b16e0..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"; @@ -42,9 +44,9 @@ default T setOrientation(ScreenOrientation orientation) { */ default Optional getOrientation() { return Optional.ofNullable(getCapability(ORIENTATION_OPTION)) - .map((v) -> v instanceof ScreenOrientation + .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/SupportsProxyOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsProxyOption.java index cb3de55e7..d69be4d2b 100644 --- a/src/main/java/io/appium/java_client/remote/options/SupportsProxyOption.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsProxyOption.java @@ -45,7 +45,7 @@ default T setProxy(Proxy proxy) { default Optional getProxy() { return Optional.ofNullable(getCapability(PROXY_OPTION)) .map(String::valueOf) - .map((v) -> new Gson().fromJson(v, Map.class)) + .map(v -> new Gson().fromJson(v, Map.class)) .map(Proxy::new); } } 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 0068dfe42..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("_", " "); } /** @@ -38,7 +40,7 @@ public String toString() { */ public static UnhandledPromptBehavior fromString(String value) { return Arrays.stream(values()) - .filter((v) -> v.toString().equals(value)) + .filter(v -> v.toString().equals(value)) .findFirst() .orElseThrow(() -> new IllegalArgumentException( String.format("Unhandled prompt behavior '%s' is not supported. " 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/SafariDriver.java b/src/main/java/io/appium/java_client/safari/SafariDriver.java index 0b8a8400d..f8f10bb01 100644 --- a/src/main/java/io/appium/java_client/safari/SafariDriver.java +++ b/src/main/java/io/appium/java_client/safari/SafariDriver.java @@ -16,6 +16,7 @@ package io.appium.java_client.safari; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.remote.AutomationName; import io.appium.java_client.service.local.AppiumDriverLocalService; @@ -82,6 +83,19 @@ public SafariDriver(HttpClient.Factory httpClientFactory, Capabilities capabilit capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } + /** + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! This API is supposed to be used for **debugging purposes only**. + * + * @param remoteSessionAddress The address of the **running** session including the session identifier. + */ + public SafariDriver(URL remoteSessionAddress) { + super(remoteSessionAddress, PLATFORM_NAME, AUTOMATION_NAME); + } + /** * Creates a new instance based on the given ClientConfig and {@code capabilities}. * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. @@ -102,7 +116,32 @@ public SafariDriver(HttpClient.Factory httpClientFactory, Capabilities capabilit * */ public SafariDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformAndAutomationNames( + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * SafariOptions options = new SafariOptions();
+     * SafariDriver driver = new SafariDriver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public SafariDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformAndAutomationNames( capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } 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/screenrecording/BaseScreenRecordingOptions.java b/src/main/java/io/appium/java_client/screenrecording/BaseScreenRecordingOptions.java index 127cc29a9..fd75dc2d6 100644 --- a/src/main/java/io/appium/java_client/screenrecording/BaseScreenRecordingOptions.java +++ b/src/main/java/io/appium/java_client/screenrecording/BaseScreenRecordingOptions.java @@ -16,13 +16,11 @@ package io.appium.java_client.screenrecording; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - import java.util.Map; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + public abstract class BaseScreenRecordingOptions> { private ScreenRecordingUploadOptions uploadOptions; @@ -34,7 +32,7 @@ public abstract class BaseScreenRecordingOptions build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - //noinspection unchecked - ofNullable(uploadOptions).map(x -> builder.putAll(x.build())); - return builder.build(); + return ofNullable(uploadOptions).map(ScreenRecordingUploadOptions::build).orElseGet(Map::of); } } diff --git a/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.java index 206cc1a6c..55716b622 100644 --- a/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.java +++ b/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.java @@ -16,14 +16,14 @@ package io.appium.java_client.screenrecording; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + public abstract class BaseStartScreenRecordingOptions> extends BaseScreenRecordingOptions> { private Boolean forceRestart; @@ -36,7 +36,7 @@ public abstract class BaseStartScreenRecordingOptions build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(timeLimit).map(x -> builder.put("timeLimit", x.getSeconds())); - ofNullable(forceRestart).map(x -> builder.put("forceRestart", x)); - return builder.build(); + var map = new HashMap<>(super.build()); + ofNullable(timeLimit).map(x -> map.put("timeLimit", x.getSeconds())); + ofNullable(forceRestart).map(x -> map.put("forceRestart", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java b/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java index cea74c04c..9743edb0a 100644 --- a/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java +++ b/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java @@ -16,14 +16,14 @@ package io.appium.java_client.screenrecording; +import io.appium.java_client.CommandExecutionHelper; +import io.appium.java_client.ExecutesMethod; + import static io.appium.java_client.MobileCommand.START_RECORDING_SCREEN; import static io.appium.java_client.MobileCommand.STOP_RECORDING_SCREEN; import static io.appium.java_client.MobileCommand.startRecordingScreenCommand; import static io.appium.java_client.MobileCommand.stopRecordingScreenCommand; -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; - public interface CanRecordScreen extends ExecutesMethod { /** diff --git a/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java b/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java index 424bfeddd..e018b47ea 100644 --- a/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java +++ b/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java @@ -16,13 +16,13 @@ package io.appium.java_client.screenrecording; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + public class ScreenRecordingUploadOptions { private String remotePath; private String user; @@ -43,7 +43,7 @@ public static ScreenRecordingUploadOptions uploadOptions() { * @return self instance for chaining. */ public ScreenRecordingUploadOptions withRemotePath(String remotePath) { - this.remotePath = checkNotNull(remotePath); + this.remotePath = requireNonNull(remotePath); return this; } @@ -56,8 +56,8 @@ public ScreenRecordingUploadOptions withRemotePath(String remotePath) { * @return self instance for chaining. */ public ScreenRecordingUploadOptions withAuthCredentials(String user, String pass) { - this.user = checkNotNull(user); - this.pass = checkNotNull(pass); + this.user = requireNonNull(user); + this.pass = requireNonNull(pass); return this; } @@ -73,7 +73,7 @@ public enum RequestMethod { * @return self instance for chaining. */ public ScreenRecordingUploadOptions withHttpMethod(RequestMethod method) { - this.method = checkNotNull(method).name(); + this.method = requireNonNull(method).name(); return this; } @@ -87,7 +87,7 @@ public ScreenRecordingUploadOptions withHttpMethod(RequestMethod method) { * @return self instance for chaining. */ public ScreenRecordingUploadOptions withFileFieldName(String fileFieldName) { - this.fileFieldName = checkNotNull(fileFieldName); + this.fileFieldName = requireNonNull(fileFieldName); return this; } @@ -100,7 +100,7 @@ public ScreenRecordingUploadOptions withFileFieldName(String fileFieldName) { * @return self instance for chaining. */ public ScreenRecordingUploadOptions withFormFields(Map formFields) { - this.formFields = checkNotNull(formFields); + this.formFields = requireNonNull(formFields); return this; } @@ -112,7 +112,7 @@ public ScreenRecordingUploadOptions withFormFields(Map formField * @return self instance for chaining. */ public ScreenRecordingUploadOptions withHeaders(Map headers) { - this.headers = checkNotNull(headers); + this.headers = requireNonNull(headers); return this; } @@ -123,14 +123,14 @@ public ScreenRecordingUploadOptions withHeaders(Map headers) { * @return arguments mapping. */ public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - ofNullable(remotePath).map(x -> builder.put("remotePath", x)); - ofNullable(user).map(x -> builder.put("user", x)); - ofNullable(pass).map(x -> builder.put("pass", x)); - ofNullable(method).map(x -> builder.put("method", x)); - ofNullable(fileFieldName).map(x -> builder.put("fileFieldName", x)); - ofNullable(formFields).map(x -> builder.put("formFields", x)); - ofNullable(headers).map(x -> builder.put("headers", x)); - return builder.build(); + var map = new HashMap(); + ofNullable(remotePath).map(x -> map.put("remotePath", x)); + ofNullable(user).map(x -> map.put("user", x)); + ofNullable(pass).map(x -> map.put("pass", x)); + ofNullable(method).map(x -> map.put("method", x)); + ofNullable(fileFieldName).map(x -> map.put("fileFieldName", x)); + ofNullable(formFields).map(x -> map.put("formFields", x)); + ofNullable(headers).map(x -> map.put("headers", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/serverevents/ServerEvents.java b/src/main/java/io/appium/java_client/serverevents/ServerEvents.java index 901241ce5..624dd1707 100644 --- a/src/main/java/io/appium/java_client/serverevents/ServerEvents.java +++ b/src/main/java/io/appium/java_client/serverevents/ServerEvents.java @@ -1,10 +1,11 @@ package io.appium.java_client.serverevents; +import lombok.Data; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import lombok.Data; @Data public class ServerEvents { @@ -13,7 +14,7 @@ public class ServerEvents { public final List events; public final String jsonData; - public void save(Path output) throws IOException { + public void save(Path output) throws IOException { Files.write(output, this.jsonData.getBytes()); } } \ No newline at end of file diff --git a/src/main/java/io/appium/java_client/serverevents/TimedEvent.java b/src/main/java/io/appium/java_client/serverevents/TimedEvent.java index dca5f1218..999ecbd39 100644 --- a/src/main/java/io/appium/java_client/serverevents/TimedEvent.java +++ b/src/main/java/io/appium/java_client/serverevents/TimedEvent.java @@ -1,8 +1,9 @@ package io.appium.java_client.serverevents; -import java.util.List; import lombok.Data; +import java.util.List; + @Data public class TimedEvent { public final String name; 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 d4cedc033..70c9f024a 100644 --- a/src/main/java/io/appium/java_client/service/local/AppiumDriverLocalService.java +++ b/src/main/java/io/appium/java_client/service/local/AppiumDriverLocalService.java @@ -16,18 +16,10 @@ package io.appium.java_client.service.local; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.service.local.AppiumServiceBuilder.BROADCAST_IP_ADDRESS; -import static org.slf4j.event.Level.DEBUG; -import static org.slf4j.event.Level.INFO; - -import com.google.common.annotations.VisibleForTesting; - +import lombok.Getter; import lombok.SneakyThrows; -import org.apache.commons.lang3.StringUtils; - -import org.openqa.selenium.net.UrlChecker; -import org.openqa.selenium.os.CommandLine; +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; @@ -37,31 +29,35 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.lang.reflect.Field; -import java.net.MalformedURLException; -import java.net.URI; import java.net.URL; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.Nullable; +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; +import static org.slf4j.event.Level.INFO; public final class AppiumDriverLocalService extends DriverService { private static final String URL_MASK = "http://%s:%d/"; private static final Logger LOG = LoggerFactory.getLogger(AppiumDriverLocalService.class); - private static final Pattern LOG_MESSAGE_PATTERN = Pattern.compile("^(.*)\\R"); private static final Pattern LOGGER_CONTEXT_PATTERN = Pattern.compile("^(\\[debug\\] )?\\[(.+?)\\]"); private static final String APPIUM_SERVICE_SLF4J_LOGGER_PREFIX = "appium.service"; private static final Duration DESTROY_TIMEOUT = Duration.ofSeconds(60); + private static final Duration IS_RUNNING_PING_TIMEOUT = Duration.ofMillis(1500); private final File nodeJSExec; private final List nodeJSArgs; @@ -69,10 +65,12 @@ public final class AppiumDriverLocalService extends DriverService { private final Duration startupTimeout; private final ReentrantLock lock = new ReentrantLock(true); //uses "fair" thread ordering policy private final ListOutputStream stream = new ListOutputStream().add(System.out); + private final AppiumServerAvailabilityChecker availabilityChecker = new AppiumServerAvailabilityChecker(); private final URL url; + @Getter private String basePath; - private CommandLine process = null; + private ExternalProcess process = null; AppiumDriverLocalService(String ipAddress, File nodeJSExec, int nodeJSPort, Duration startupTimeout, @@ -99,10 +97,6 @@ public AppiumDriverLocalService withBasePath(String basePath) { return this; } - public String getBasePath() { - return this.basePath; - } - @SneakyThrows private static URL addSuffix(URL url, String suffix) { return url.toURI().resolve("." + (suffix.startsWith("/") ? suffix : "/" + suffix)).toURL(); @@ -111,7 +105,7 @@ private static URL addSuffix(URL url, String suffix) { @SneakyThrows @SuppressWarnings("SameParameterValue") private static URL replaceHost(URL source, String oldHost, String newHost) { - return new URL(source.toString().replace(oldHost, newHost)); + return new URL(source.toString().replaceFirst(oldHost, newHost)); } /** @@ -128,40 +122,48 @@ public URL getUrl() { public boolean isRunning() { lock.lock(); try { - if (process == null) { - return false; - } - - if (!process.isRunning()) { + if (process == null || !process.isAlive()) { return false; } try { - ping(Duration.ofMillis(1500)); - return true; - } catch (UrlChecker.TimeoutException e) { + return ping(IS_RUNNING_PING_TIMEOUT); + } catch (AppiumServerAvailabilityChecker.ConnectionTimeout + | AppiumServerAvailabilityChecker.ConnectionError e) { return false; - } catch (MalformedURLException e) { - throw new AppiumServerHasNotBeenStartedLocallyException(e.getMessage(), e); + } catch (InterruptedException e) { + throw new RuntimeException(e); } } finally { lock.unlock(); } + } + private boolean ping(Duration timeout) throws InterruptedException { + var baseURL = fixBroadcastAddresses(getUrl()); + var statusUrl = addSuffix(baseURL, "/status"); + return availabilityChecker.waitUntilAvailable(statusUrl, timeout); } - private void ping(Duration timeout) throws UrlChecker.TimeoutException, MalformedURLException { - // The operating system might block direct access to the universal broadcast IP address - URL status = addSuffix(replaceHost(getUrl(), BROADCAST_IP_ADDRESS, "127.0.0.1"), "/status"); - new UrlChecker().waitUntilAvailable(timeout.toMillis(), TimeUnit.MILLISECONDS, status); + private URL fixBroadcastAddresses(URL url) { + var host = url.getHost(); + // The operating system will block direct access to the universal broadcast IP address + if (host.equals(BROADCAST_IP4_ADDRESS)) { + return replaceHost(url, BROADCAST_IP4_ADDRESS, "127.0.0.1"); + } + if (host.equals(BROADCAST_IP6_ADDRESS)) { + return replaceHost(url, BROADCAST_IP6_ADDRESS, "::1"); + } + return url; } /** * Starts the defined appium server. * - * @throws AppiumServerHasNotBeenStartedLocallyException If an error occurs while spawning the child process. + * @throws AppiumServerHasNotBeenStartedLocallyException If an error occurs on Appium server startup. * @see #stop() */ + @Override public void start() throws AppiumServerHasNotBeenStartedLocallyException { lock.lock(); try { @@ -170,31 +172,75 @@ public void start() throws AppiumServerHasNotBeenStartedLocallyException { } try { - process = new CommandLine(this.nodeJSExec.getCanonicalPath(), - nodeJSArgs.toArray(new String[]{})); - process.setEnvironmentVariables(nodeJSEnvironment); - process.copyOutputTo(stream); - process.executeAsync(); + var processBuilder = ExternalProcess.builder() + .command(this.nodeJSExec.getCanonicalPath(), nodeJSArgs) + .copyOutputTo(stream); + nodeJSEnvironment.forEach(processBuilder::environment); + process = processBuilder.start(); + } catch (IOException e) { + throw new AppiumServerHasNotBeenStartedLocallyException(e); + } + + var didPingSucceed = false; + try { ping(startupTimeout); - } catch (Throwable e) { - destroyProcess(); - String msgTxt = "The local appium server has not been started. " - + "The given Node.js executable: " + this.nodeJSExec.getAbsolutePath() - + " Arguments: " + nodeJSArgs.toString() + " " + "\n"; - if (process != null) { - String processStream = process.getStdOut(); - if (!StringUtils.isBlank(processStream)) { - msgTxt = msgTxt + "Process output: " + processStream + "\n"; - } + didPingSucceed = true; + } catch (AppiumServerAvailabilityChecker.ConnectionTimeout + | AppiumServerAvailabilityChecker.ConnectionError e) { + var errorLines = new ArrayList<>(generateDetailedErrorMessagePrefix(e)); + errorLines.addAll(retrieveServerDebugInfo()); + throw new AppiumServerHasNotBeenStartedLocallyException( + String.join("\n", errorLines), e + ); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + if (!didPingSucceed) { + destroyProcess(); } - - throw new AppiumServerHasNotBeenStartedLocallyException(msgTxt, e); } } finally { lock.unlock(); } } + private List generateDetailedErrorMessagePrefix(RuntimeException e) { + var errorLines = new ArrayList(); + if (e instanceof AppiumServerAvailabilityChecker.ConnectionTimeout) { + errorLines.add(String.format( + "Appium HTTP server is not listening at %s after %s ms timeout. " + + "Consider increasing the server startup timeout value and " + + "check the server log for possible error messages occurrences.", getUrl(), + ((AppiumServerAvailabilityChecker.ConnectionTimeout) e).getTimeout().toMillis() + )); + } else if (e instanceof AppiumServerAvailabilityChecker.ConnectionError) { + var connectionError = (AppiumServerAvailabilityChecker.ConnectionError) e; + var statusCode = connectionError.getResponseCode(); + var statusUrl = connectionError.getStatusUrl(); + var payload = connectionError.getPayload(); + errorLines.add(String.format( + "Appium HTTP server has started and is listening although we were " + + "unable to get an OK response from %s. Make sure both the client " + + "and the server use the same base path '%s' and check the server log for possible " + + "error messages occurrences.", statusUrl, Optional.ofNullable(basePath).orElse("/") + )); + errorLines.add(String.format("Response status code: %s", statusCode)); + payload.ifPresent(p -> errorLines.add(String.format("Response payload: %s", p))); + } + return errorLines; + } + + private List retrieveServerDebugInfo() { + var result = new ArrayList(); + result.add(String.format("Node.js executable path: %s", nodeJSExec.getAbsolutePath())); + result.add(String.format("Arguments: %s", nodeJSArgs)); + ofNullable(process) + .map(ExternalProcess::getOutput) + .filter(o -> !isNullOrEmpty(o)) + .ifPresent(o -> result.add(String.format("Server log: %s", o))); + return result; + } + /** * Stops this service is it is currently running. This method will attempt to block until the * server has been fully shutdown. @@ -214,47 +260,16 @@ public void stop() { } } - /** - * Destroys the service if it is running. - * - * @param timeout The maximum time to wait before the process will be force-killed. - * @return The exit code of the process or zero if the process was not running. - */ - private int destroyProcess(Duration timeout) { - if (!process.isRunning()) { - return 0; - } - - // This all magic is necessary, because Selenium does not publicly expose - // process killing timeouts. By default a process is killed forcibly if - // it does not exit after two seconds, which is in most cases not enough for - // Appium - try { - Field processField = process.getClass().getDeclaredField("process"); - processField.setAccessible(true); - Object osProcess = processField.get(process); - Field watchdogField = osProcess.getClass().getDeclaredField("executeWatchdog"); - watchdogField.setAccessible(true); - Object watchdog = watchdogField.get(osProcess); - Field nativeProcessField = watchdog.getClass().getDeclaredField("process"); - nativeProcessField.setAccessible(true); - Process nativeProcess = (Process) nativeProcessField.get(watchdog); - nativeProcess.destroy(); - nativeProcess.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS); - } catch (Exception e) { - LOG.warn("No explicit timeout could be applied to the process termination", e); - } - - return process.destroy(); - } - /** * Destroys the service. - * This methods waits up to `DESTROY_TIMEOUT` seconds for the Appium service + * This method waits up to `DESTROY_TIMEOUT` seconds for the Appium service * to exit gracefully. */ private void destroyProcess() { - destroyProcess(DESTROY_TIMEOUT); + if (process == null || !process.isAlive()) { + return; + } + process.shutdown(DESTROY_TIMEOUT); } /** @@ -264,11 +279,7 @@ private void destroyProcess() { */ @Nullable public String getStdOut() { - if (process != null) { - return process.getStdOut(); - } - - return null; + return ofNullable(process).map(ExternalProcess::getOutput).orElse(null); } /** @@ -278,7 +289,7 @@ public String getStdOut() { * that is ready to accept server output */ public void addOutPutStream(OutputStream outputStream) { - checkNotNull(outputStream, "outputStream parameter is NULL!"); + requireNonNull(outputStream, "outputStream parameter is NULL!"); stream.add(outputStream); } @@ -289,9 +300,9 @@ public void addOutPutStream(OutputStream outputStream) { * that are ready to accept server output */ public void addOutPutStreams(List outputStreams) { - checkNotNull(outputStreams, "outputStreams parameter is NULL!"); - for (OutputStream stream : outputStreams) { - addOutPutStream(stream); + requireNonNull(outputStreams, "outputStreams parameter is NULL!"); + for (OutputStream outputStream : outputStreams) { + addOutPutStream(outputStream); } } @@ -301,7 +312,7 @@ public void addOutPutStreams(List outputStreams) { * @return the outputStream has been removed if it is present */ public Optional removeOutPutStream(OutputStream outputStream) { - checkNotNull(outputStream, "outputStream parameter is NULL!"); + requireNonNull(outputStream, "outputStream parameter is NULL!"); return stream.remove(outputStream); } @@ -386,19 +397,18 @@ public void enableDefaultSlf4jLoggingOfOutputData() { * available. */ public void addSlf4jLogMessageConsumer(BiConsumer slf4jLogMessageConsumer) { - checkNotNull(slf4jLogMessageConsumer, "slf4jLogMessageConsumer parameter is NULL!"); + requireNonNull(slf4jLogMessageConsumer, "slf4jLogMessageConsumer parameter is NULL!"); addLogMessageConsumer(logMessage -> { slf4jLogMessageConsumer.accept(logMessage, parseSlf4jContextFromLogMessage(logMessage)); }); } - @VisibleForTesting - static Slf4jLogMessageContext parseSlf4jContextFromLogMessage(String logMessage) { + private static Slf4jLogMessageContext parseSlf4jContextFromLogMessage(String logMessage) { Matcher m = LOGGER_CONTEXT_PATTERN.matcher(logMessage); 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; } @@ -420,7 +430,7 @@ static Slf4jLogMessageContext parseSlf4jContextFromLogMessage(String logMessage) * @param consumer Consumer block to be executed when a log message is available. */ public void addLogMessageConsumer(Consumer consumer) { - checkNotNull(consumer, "consumer parameter is NULL!"); + requireNonNull(consumer, "consumer parameter is NULL!"); addOutPutStream(new OutputStream() { private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/src/main/java/io/appium/java_client/service/local/AppiumServerAvailabilityChecker.java b/src/main/java/io/appium/java_client/service/local/AppiumServerAvailabilityChecker.java new file mode 100644 index 000000000..2876c3707 --- /dev/null +++ b/src/main/java/io/appium/java_client/service/local/AppiumServerAvailabilityChecker.java @@ -0,0 +1,158 @@ +/* + * 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.service.local; + +import lombok.Getter; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.time.Duration; +import java.time.Instant; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +public class AppiumServerAvailabilityChecker { + private static final Duration CONNECT_TIMEOUT = Duration.ofMillis(500); + private static final Duration READ_TIMEOUT = Duration.ofSeconds(1); + private static final Duration MAX_POLL_INTERVAL = Duration.ofMillis(320); + private static final Duration MIN_POLL_INTERVAL = Duration.ofMillis(10); + + /** + * Verifies a possibility of establishing a connection + * to a running Appium server. + * + * @param serverStatusUrl The URL of /status endpoint. + * @param timeout Wait timeout. If the server responds with non-200 error + * code then we are not going to retry, but throw an exception + * immediately. + * @return true in case of success + * @throws InterruptedException If the API is interrupted + * @throws ConnectionTimeout If it is not possible to successfully open + * an HTTP connection to the server's /status endpoint. + * @throws ConnectionError If an HTTP connection was opened successfully, + * but non-200 error code was received. + */ + public boolean waitUntilAvailable(URL serverStatusUrl, Duration timeout) throws InterruptedException { + var interval = MIN_POLL_INTERVAL; + var start = Instant.now(); + IOException lastError = null; + while (Duration.between(start, Instant.now()).compareTo(timeout) <= 0) { + HttpURLConnection connection = null; + try { + connection = connectToUrl(serverStatusUrl); + return checkResponse(connection); + } catch (IOException e) { + lastError = e; + } finally { + Optional.ofNullable(connection).ifPresent(HttpURLConnection::disconnect); + } + //noinspection BusyWait + Thread.sleep(interval.toMillis()); + interval = interval.compareTo(MAX_POLL_INTERVAL) >= 0 ? interval : interval.multipliedBy(2); + } + throw new ConnectionTimeout(timeout, lastError); + } + + private HttpURLConnection connectToUrl(URL url) throws IOException { + var connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout((int) CONNECT_TIMEOUT.toMillis()); + connection.setReadTimeout((int) READ_TIMEOUT.toMillis()); + connection.connect(); + return connection; + } + + private boolean checkResponse(HttpURLConnection connection) throws IOException { + var responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + return true; + } + var is = responseCode < HttpURLConnection.HTTP_BAD_REQUEST + ? connection.getInputStream() + : connection.getErrorStream(); + throw new ConnectionError(connection.getURL(), responseCode, is); + } + + @Getter + public static class ConnectionError extends RuntimeException { + private static final int MAX_PAYLOAD_LEN = 1024; + + private final URL statusUrl; + private final int responseCode; + private final Optional payload; + + /** + * Thrown on server connection errors. + * + * @param statusUrl Appium server status URL. + * @param responseCode The response code received from the URL above. + * @param body The response body stream received from the URL above. + */ + public ConnectionError(URL statusUrl, int responseCode, InputStream body) { + super(ConnectionError.class.getSimpleName()); + this.statusUrl = statusUrl; + this.responseCode = responseCode; + this.payload = readResponseStreamSafely(body); + } + + private static Optional readResponseStreamSafely(InputStream is) { + try (var br = new BufferedReader(new InputStreamReader(is))) { + var result = new LinkedList(); + String currentLine; + var payloadSize = 0L; + while ((currentLine = br.readLine()) != null) { + result.addFirst(currentLine); + payloadSize += currentLine.length(); + while (payloadSize > MAX_PAYLOAD_LEN && result.size() > 1) { + payloadSize -= result.removeLast().length(); + } + } + var s = abbreviate(result); + return s.isEmpty() ? Optional.empty() : Optional.of(s); + } catch (IOException e) { + return Optional.empty(); + } + } + + private static String abbreviate(List filo) { + var result = String.join("\n", filo).trim(); + return result.length() > MAX_PAYLOAD_LEN + ? "…" + result.substring(0, MAX_PAYLOAD_LEN) + : result; + } + } + + @Getter + public static class ConnectionTimeout extends RuntimeException { + private final Duration timeout; + + /** + * Thrown on server timeout errors. + * + * @param timeout Timeout value. + * @param cause Timeout cause. + */ + public ConnectionTimeout(Duration timeout, Throwable cause) { + super(ConnectionTimeout.class.getSimpleName(), cause); + this.timeout = timeout; + } + } +} diff --git a/src/main/java/io/appium/java_client/service/local/AppiumServerHasNotBeenStartedLocallyException.java b/src/main/java/io/appium/java_client/service/local/AppiumServerHasNotBeenStartedLocallyException.java index 664e6a602..9c0afb248 100644 --- a/src/main/java/io/appium/java_client/service/local/AppiumServerHasNotBeenStartedLocallyException.java +++ b/src/main/java/io/appium/java_client/service/local/AppiumServerHasNotBeenStartedLocallyException.java @@ -16,16 +16,16 @@ package io.appium.java_client.service.local; - public class AppiumServerHasNotBeenStartedLocallyException extends RuntimeException { + public AppiumServerHasNotBeenStartedLocallyException(String message, Throwable cause) { + super(message, cause); + } - private static final long serialVersionUID = 1L; - - public AppiumServerHasNotBeenStartedLocallyException(String messege, Throwable t) { - super(messege, t); + public AppiumServerHasNotBeenStartedLocallyException(String message) { + super(message); } - public AppiumServerHasNotBeenStartedLocallyException(String messege) { - super(messege); + public AppiumServerHasNotBeenStartedLocallyException(Throwable cause) { + super(cause); } } 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 e0ce11520..b22c93937 100644 --- a/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java +++ b/src/main/java/io/appium/java_client/service/local/AppiumServiceBuilder.java @@ -16,47 +16,43 @@ package io.appium.java_client.service.local; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; - -import com.google.common.collect.ImmutableList; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import io.appium.java_client.remote.AndroidMobileCapabilityType; +import io.appium.java_client.android.options.context.SupportsChromedriverExecutableOption; +import io.appium.java_client.android.options.signing.SupportsKeystoreOptions; import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.remote.MobileCapabilityType; +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.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.SystemUtils; -import org.apache.commons.validator.routines.InetAddressValidator; +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; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; +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; + public final class AppiumServiceBuilder extends DriverService.Builder { @@ -74,22 +70,26 @@ public final class AppiumServiceBuilder */ private static final String NODE_PATH = "NODE_BINARY_PATH"; - public static final String BROADCAST_IP_ADDRESS = "0.0.0.0"; + public static final String BROADCAST_IP4_ADDRESS = "0.0.0.0"; + public static final String BROADCAST_IP6_ADDRESS = "::"; private static final Path APPIUM_PATH_SUFFIX = Paths.get("appium", "build", "lib", "main.js"); public static final int DEFAULT_APPIUM_PORT = 4723; private final Map serverArguments = new HashMap<>(); private File appiumJS; private File node; - private String ipAddress = BROADCAST_IP_ADDRESS; + private String ipAddress = BROADCAST_IP4_ADDRESS; private Capabilities capabilities; private boolean autoQuoteCapabilitiesOnWindows = false; - private static final Function APPIUM_JS_NOT_EXIST_ERROR = (fullPath) -> String.format( + private static final Function APPIUM_JS_NOT_EXIST_ERROR = fullPath -> String.format( "The main Appium script does not exist at '%s'", fullPath.getAbsolutePath()); - private static final Function NODE_JS_NOT_EXIST_ERROR = (fullPath) -> + private static final Function NODE_JS_NOT_EXIST_ERROR = fullPath -> String.format("The main NodeJS executable does not exist at '%s'", fullPath.getAbsolutePath()); - private static final List PATH_CAPABILITIES = ImmutableList.of(AndroidMobileCapabilityType.KEYSTORE_PATH, - AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE, MobileCapabilityType.APP); + private static final List PATH_CAPABILITIES = List.of( + SupportsChromedriverExecutableOption.CHROMEDRIVER_EXECUTABLE_OPTION, + SupportsKeystoreOptions.KEYSTORE_PATH_OPTION, + SupportsAppOption.APP_OPTION + ); public AppiumServiceBuilder() { usingPort(DEFAULT_APPIUM_PORT); @@ -144,14 +144,14 @@ private static File findNpm() { private static File findMainScript() { File npm = findNpm(); - List cmdLine = SystemUtils.IS_OS_WINDOWS + 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"); ProcessBuilder pb = new ProcessBuilder(cmdLine); String nodeModulesRoot; try { - nodeModulesRoot = IOUtils.toString(pb.start().getInputStream(), StandardCharsets.UTF_8).trim(); + nodeModulesRoot = new String(pb.start().getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim(); } catch (IOException e) { throw new InvalidServerInstanceException( "Cannot retrieve the path to the folder where NodeJS modules are located", e); @@ -163,7 +163,6 @@ private static File findMainScript() { return mainAppiumJs; } - @Override protected File findDefaultExecutable() { if (this.node != null) { validatePath(this.node.getAbsolutePath(), NODE_JS_NOT_EXIST_ERROR.apply(this.node)); @@ -230,9 +229,11 @@ public AppiumServiceBuilder withArgument(ServerArgument argument, String value) } private static String sanitizeBasePath(String basePath) { - basePath = checkNotNull(basePath).trim(); - checkArgument(!basePath.isEmpty(), - "Given base path is not valid - blank or empty values are not allowed for base path"); + basePath = requireNonNull(basePath).trim(); + checkArgument( + !basePath.isEmpty(), + "Given base path is not valid - blank or empty values are not allowed for base path" + ); return basePath.endsWith("/") ? basePath : basePath + "/"; } @@ -283,10 +284,10 @@ public AppiumServiceBuilder withIPAddress(String ipAddress) { @Nullable private static File loadPathFromEnv(String envVarName) { String fullPath = System.getProperty(envVarName); - if (StringUtils.isBlank(fullPath)) { + if (isNullOrEmpty(fullPath)) { fullPath = System.getenv(envVarName); } - return StringUtils.isBlank(fullPath) ? null : new File(fullPath); + return isNullOrEmpty(fullPath) ? null : new File(fullPath); } private void loadPathToMainScript() { @@ -356,22 +357,15 @@ private String capabilitiesToCmdlineArg() { } @Override - protected ImmutableList createArgs() { + protected List createArgs() { List argList = new ArrayList<>(); loadPathToMainScript(); argList.add(appiumJS.getAbsolutePath()); argList.add("--port"); argList.add(String.valueOf(getPort())); - if (StringUtils.isBlank(ipAddress)) { - ipAddress = BROADCAST_IP_ADDRESS; - } else { - InetAddressValidator validator = InetAddressValidator.getInstance(); - if (!validator.isValid(ipAddress) && !validator.isValidInet4Address(ipAddress) - && !validator.isValidInet6Address(ipAddress)) { - throw new IllegalArgumentException( - "The invalid IP address " + ipAddress + " is defined"); - } + if (isNullOrEmpty(ipAddress)) { + ipAddress = BROADCAST_IP4_ADDRESS; } argList.add("--address"); argList.add(ipAddress); @@ -386,12 +380,12 @@ protected ImmutableList createArgs() { for (Map.Entry entry : entries) { String argument = entry.getKey(); String value = entry.getValue(); - if (StringUtils.isBlank(argument) || value == null) { + if (isNullOrEmpty(argument) || value == null) { continue; } argList.add(argument); - if (!StringUtils.isBlank(value)) { + if (!isNullOrEmpty(value)) { argList.add(value); } } @@ -401,7 +395,14 @@ protected ImmutableList createArgs() { argList.add(capabilitiesToCmdlineArg()); } - return new ImmutableList.Builder().addAll(argList).build(); + return Collections.unmodifiableList(argList); + } + + @Override + protected void loadSystemProperties() { + if (this.exe == null) { + usingDriverExecutable(findDefaultExecutable()); + } } /** 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 89820f70a..7173963ad 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 @@ -53,12 +53,14 @@ Optional remove(OutputStream stream) { } } + @Override public void flush() throws IOException { for (OutputStream stream : streams) { stream.flush(); } } + @Override public void close() throws IOException { for (OutputStream stream : streams) { stream.close(); diff --git a/src/main/java/io/appium/java_client/service/local/flags/GeneralServerFlag.java b/src/main/java/io/appium/java_client/service/local/flags/GeneralServerFlag.java index 19b69c38c..8bf2b9679 100644 --- a/src/main/java/io/appium/java_client/service/local/flags/GeneralServerFlag.java +++ b/src/main/java/io/appium/java_client/service/local/flags/GeneralServerFlag.java @@ -38,15 +38,6 @@ public enum GeneralServerFlag implements ServerArgument { * Enables session override (clobbering). Default: false */ SESSION_OVERRIDE("--session-override"), - /** - * Pre-launch the application before allowing the first session - * (Requires –app and, for Android, –app-pkg and –app-activity). - * Default: false - * - * @deprecated This argument has been removed from Appium 2.0 - */ - @Deprecated - PRE_LAUNCH("--pre-launch"), /** * The message log level to be shown. * Sample: --log-level debug diff --git a/src/main/java/io/appium/java_client/touch/ActionOptions.java b/src/main/java/io/appium/java_client/touch/ActionOptions.java index 2673142e4..2514a92a5 100644 --- a/src/main/java/io/appium/java_client/touch/ActionOptions.java +++ b/src/main/java/io/appium/java_client/touch/ActionOptions.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Map; +@Deprecated public abstract class ActionOptions> { /** * This method is automatically called before building diff --git a/src/main/java/io/appium/java_client/touch/LongPressOptions.java b/src/main/java/io/appium/java_client/touch/LongPressOptions.java index 198f476b5..56d2334fb 100644 --- a/src/main/java/io/appium/java_client/touch/LongPressOptions.java +++ b/src/main/java/io/appium/java_client/touch/LongPressOptions.java @@ -16,15 +16,16 @@ package io.appium.java_client.touch; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - import io.appium.java_client.touch.offset.AbstractOptionCombinedWithPosition; import java.time.Duration; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + +@Deprecated public class LongPressOptions extends AbstractOptionCombinedWithPosition { protected Duration duration = null; @@ -45,7 +46,7 @@ public static LongPressOptions longPressOptions() { * @return this instance for chaining. */ public LongPressOptions withDuration(Duration duration) { - checkNotNull(duration); + requireNonNull(duration); checkArgument(duration.toMillis() >= 0, "Duration value should be greater or equal to zero"); this.duration = duration; diff --git a/src/main/java/io/appium/java_client/touch/TapOptions.java b/src/main/java/io/appium/java_client/touch/TapOptions.java index 7c4a1e1a0..7dee99fae 100644 --- a/src/main/java/io/appium/java_client/touch/TapOptions.java +++ b/src/main/java/io/appium/java_client/touch/TapOptions.java @@ -16,13 +16,14 @@ package io.appium.java_client.touch; -import static com.google.common.base.Preconditions.checkArgument; -import static java.util.Optional.ofNullable; - import io.appium.java_client.touch.offset.AbstractOptionCombinedWithPosition; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Optional.ofNullable; + +@Deprecated public class TapOptions extends AbstractOptionCombinedWithPosition { private Integer tapsCount = null; diff --git a/src/main/java/io/appium/java_client/touch/WaitOptions.java b/src/main/java/io/appium/java_client/touch/WaitOptions.java index 422f0b052..11eb0ccdc 100644 --- a/src/main/java/io/appium/java_client/touch/WaitOptions.java +++ b/src/main/java/io/appium/java_client/touch/WaitOptions.java @@ -16,13 +16,14 @@ package io.appium.java_client.touch; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.time.Duration.ofMillis; - import java.time.Duration; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.time.Duration.ofMillis; +import static java.util.Objects.requireNonNull; + +@Deprecated public class WaitOptions extends ActionOptions { protected Duration duration = ofMillis(0); @@ -44,7 +45,7 @@ public static WaitOptions waitOptions(Duration duration) { * @return this instance for chaining. */ public WaitOptions withDuration(Duration duration) { - checkNotNull(duration); + requireNonNull(duration); checkArgument(duration.toMillis() >= 0, "Duration value should be greater or equal to zero"); this.duration = duration; diff --git a/src/main/java/io/appium/java_client/touch/offset/AbstractOptionCombinedWithPosition.java b/src/main/java/io/appium/java_client/touch/offset/AbstractOptionCombinedWithPosition.java index a497ab4a8..194228eea 100644 --- a/src/main/java/io/appium/java_client/touch/offset/AbstractOptionCombinedWithPosition.java +++ b/src/main/java/io/appium/java_client/touch/offset/AbstractOptionCombinedWithPosition.java @@ -1,11 +1,12 @@ package io.appium.java_client.touch.offset; -import static java.util.Optional.ofNullable; - import io.appium.java_client.touch.ActionOptions; import java.util.Map; +import static java.util.Optional.ofNullable; + +@Deprecated public abstract class AbstractOptionCombinedWithPosition> extends ActionOptions> { private ActionOptions positionOption; diff --git a/src/main/java/io/appium/java_client/touch/offset/ElementOption.java b/src/main/java/io/appium/java_client/touch/offset/ElementOption.java index 775b067d1..ac5d577a7 100644 --- a/src/main/java/io/appium/java_client/touch/offset/ElementOption.java +++ b/src/main/java/io/appium/java_client/touch/offset/ElementOption.java @@ -1,9 +1,5 @@ package io.appium.java_client.touch.offset; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Optional.ofNullable; - import org.openqa.selenium.Point; import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.RemoteWebElement; @@ -11,6 +7,11 @@ import java.util.HashMap; import java.util.Map; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + +@Deprecated public class ElementOption extends PointOption { private String elementId; @@ -82,7 +83,7 @@ public ElementOption withCoordinates(int xOffset, int yOffset) { * @return self-reference */ public ElementOption withElement(WebElement element) { - checkNotNull(element); + requireNonNull(element); checkArgument(true, "Element should be an instance of the class which " + "extends org.openqa.selenium.remote.RemoteWebElement", element instanceof RemoteWebElement); diff --git a/src/main/java/io/appium/java_client/touch/offset/PointOption.java b/src/main/java/io/appium/java_client/touch/offset/PointOption.java index 1cccd2486..a45d59f9c 100644 --- a/src/main/java/io/appium/java_client/touch/offset/PointOption.java +++ b/src/main/java/io/appium/java_client/touch/offset/PointOption.java @@ -1,12 +1,13 @@ package io.appium.java_client.touch.offset; -import static java.util.Optional.ofNullable; - import io.appium.java_client.touch.ActionOptions; import org.openqa.selenium.Point; import java.util.Map; +import static java.util.Optional.ofNullable; + +@Deprecated public class PointOption> extends ActionOptions { protected Point coordinates; diff --git a/src/main/java/io/appium/java_client/windows/WindowsDriver.java b/src/main/java/io/appium/java_client/windows/WindowsDriver.java index 82af6e02d..7c6e68a7a 100644 --- a/src/main/java/io/appium/java_client/windows/WindowsDriver.java +++ b/src/main/java/io/appium/java_client/windows/WindowsDriver.java @@ -16,6 +16,7 @@ package io.appium.java_client.windows; +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.MobileCommand; import io.appium.java_client.PerformsTouchActions; @@ -80,6 +81,10 @@ public WindowsDriver(HttpClient.Factory httpClientFactory, Capabilities capabili capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } + public WindowsDriver(URL remoteSessionAddress) { + super(remoteSessionAddress, PLATFORM_NAME, AUTOMATION_NAME); + } + /** * Creates a new instance based on the given ClientConfig and {@code capabilities}. * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. @@ -100,7 +105,32 @@ public WindowsDriver(HttpClient.Factory httpClientFactory, Capabilities capabili * */ public WindowsDriver(ClientConfig clientConfig, Capabilities capabilities) { - super(clientConfig, ensurePlatformAndAutomationNames( + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * WindowsOptions options = new WindowsOptions();
+     * WindowsDriver driver = new WindowsDriver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public WindowsDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformAndAutomationNames( capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } diff --git a/src/main/java/io/appium/java_client/windows/WindowsStartScreenRecordingOptions.java b/src/main/java/io/appium/java_client/windows/WindowsStartScreenRecordingOptions.java index ff90a08f2..8f5d5bc72 100644 --- a/src/main/java/io/appium/java_client/windows/WindowsStartScreenRecordingOptions.java +++ b/src/main/java/io/appium/java_client/windows/WindowsStartScreenRecordingOptions.java @@ -16,10 +16,11 @@ package io.appium.java_client.windows; -import com.google.common.collect.ImmutableMap; import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import static java.util.Optional.ofNullable; @@ -138,14 +139,13 @@ public WindowsStartScreenRecordingOptions withTimeLimit(Duration timeLimit) { @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(fps).map(x -> builder.put("fps", x)); - ofNullable(preset).map(x -> builder.put("preset", x)); - ofNullable(videoFilter).map(x -> builder.put("videoFilter", x)); - ofNullable(captureClicks).map(x -> builder.put("captureClicks", x)); - ofNullable(captureCursor).map(x -> builder.put("captureCursor", x)); - ofNullable(audioInput).map(x -> builder.put("audioInput", x)); - return builder.build(); + var map = new HashMap<>(super.build()); + ofNullable(fps).map(x -> map.put("fps", x)); + ofNullable(preset).map(x -> map.put("preset", x)); + ofNullable(videoFilter).map(x -> map.put("videoFilter", x)); + ofNullable(captureClicks).map(x -> map.put("captureClicks", x)); + ofNullable(captureCursor).map(x -> map.put("captureCursor", x)); + ofNullable(audioInput).map(x -> map.put("audioInput", x)); + return Collections.unmodifiableMap(map); } } diff --git a/src/main/java/io/appium/java_client/windows/options/SupportsAppArgumentsOption.java b/src/main/java/io/appium/java_client/windows/options/SupportsAppArgumentsOption.java index e5a8f55c3..1b4465a88 100644 --- a/src/main/java/io/appium/java_client/windows/options/SupportsAppArgumentsOption.java +++ b/src/main/java/io/appium/java_client/windows/options/SupportsAppArgumentsOption.java @@ -20,7 +20,6 @@ import io.appium.java_client.remote.options.CanSetCapability; import org.openqa.selenium.Capabilities; -import java.net.URL; import java.util.Optional; public interface SupportsAppArgumentsOption> extends 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 8a1f0eb8f..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, @@ -65,7 +65,7 @@ private void setCommonOptions() { * each key must be a valid PowerShell script or command to be * executed prior to the WinAppDriver session startup. * See - * https://github.com/appium/appium-windows-driver#power-shell-commands-execution + * https://github.com/appium/appium-windows-driver#power-shell-commands-execution * for more details. * * @param script E.g. {script: 'Get-Process outlook -ErrorAction SilentlyContinue'}. @@ -83,7 +83,7 @@ public WindowsOptions setPrerun(PowerShellData script) { public Optional getPrerun() { //noinspection unchecked return Optional.ofNullable(getCapability(PRERUN_OPTION)) - .map((v) -> new PowerShellData((Map) v)); + .map(v -> new PowerShellData((Map) v)); } /** @@ -91,7 +91,7 @@ public Optional getPrerun() { * each key must be a valid PowerShell script or command to be * executed after an WinAppDriver session is finished. * See - * https://github.com/appium/appium-windows-driver#power-shell-commands-execution + * https://github.com/appium/appium-windows-driver#power-shell-commands-execution * for more details. * * @param script E.g. {script: 'Get-Process outlook -ErrorAction SilentlyContinue'}. @@ -109,6 +109,6 @@ public WindowsOptions setPostrun(PowerShellData script) { public Optional getPostrun() { //noinspection unchecked return Optional.ofNullable(getCapability(POSTRUN_OPTION)) - .map((v) -> new PowerShellData((Map) v)); + .map(v -> new PowerShellData((Map) v)); } } 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 6a3148aa2..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,20 +16,19 @@ package io.appium.java_client.ws; -import java.net.URI; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -import javax.annotation.Nullable; - -import org.openqa.selenium.remote.http.ClientConfig; +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 java.lang.ref.WeakReference; +import java.net.URI; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + public class StringWebSocketClient implements WebSocket.Listener, CanHandleMessages, CanHandleErrors, CanHandleConnects, CanHandleDisconnects { private final List> messageHandlers = new CopyOnWriteArrayList<>(); @@ -39,6 +38,12 @@ public class StringWebSocketClient implements WebSocket.Listener, private volatile boolean isListening = false; + private final WeakReference httpClient; + + public StringWebSocketClient(HttpClient httpClient) { + this.httpClient = new WeakReference<>(httpClient); + } + private URI endpoint; private void setEndpoint(URI endpoint) { @@ -65,26 +70,31 @@ public void connect(URI endpoint) { return; } - ClientConfig clientConfig = ClientConfig.defaultConfig() - .readTimeout(Duration.ZERO) - .baseUri(endpoint); // To avoid NPE in org.openqa.selenium.remote.http.netty.NettyMessages (line 78) - HttpClient client = HttpClient.Factory.createDefault().createClient(clientConfig); HttpRequest request = new HttpRequest(HttpMethod.GET, endpoint.toString()); - client.openSocket(request, this); + Objects.requireNonNull(httpClient.get()).openSocket(request, this); onOpen(); setEndpoint(endpoint); } + /** + * The callback method invoked on websocket opening. + */ public void onOpen() { - getConnectionHandlers().forEach(Runnable::run); - isListening = true; + try { + getConnectionHandlers().forEach(Runnable::run); + } finally { + isListening = true; + } } @Override public void onClose(int code, String reason) { - getDisconnectionHandlers().forEach(Runnable::run); - isListening = false; + try { + getDisconnectionHandlers().forEach(Runnable::run); + } finally { + isListening = false; + } } @Override diff --git a/src/main/resources/main.properties b/src/main/resources/main.properties index a4236a9fe..9875b0c49 100644 --- a/src/main/resources/main.properties +++ b/src/main/resources/main.properties @@ -1 +1,2 @@ selenium.version=@selenium.version@ +appiumClient.version=@appiumClient.version@ diff --git a/src/test/java/io/appium/java_client/TestResources.java b/src/test/java/io/appium/java_client/TestResources.java deleted file mode 100644 index 9d188fb58..000000000 --- a/src/test/java/io/appium/java_client/TestResources.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.appium.java_client; - -import java.nio.file.Path; - -import static io.appium.java_client.TestUtils.resourcePathToLocalPath; - -public class TestResources { - public static Path apiDemosApk() { - return resourcePathToLocalPath("apps/ApiDemos-debug.apk"); - } - - public static Path testAppZip() { - return resourcePathToLocalPath("apps/TestApp.app.zip"); - } - - public static Path uiCatalogAppZip() { - return resourcePathToLocalPath("apps/UICatalog.app.zip"); - } - - public static Path vodQaAppZip() { - return resourcePathToLocalPath("apps/vodqa.zip"); - } - - public static Path intentExampleApk() { - return resourcePathToLocalPath("apps/IntentExample.apk"); - } - - public static Path helloAppiumHtml() { - return resourcePathToLocalPath("html/hello appium - saved page.htm"); - } -} diff --git a/src/test/java/io/appium/java_client/android/AndroidAbilityToUseSupplierTest.java b/src/test/java/io/appium/java_client/android/AndroidAbilityToUseSupplierTest.java deleted file mode 100644 index 38fa40b93..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidAbilityToUseSupplierTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.appium.java_client.android; - -import static io.appium.java_client.TestUtils.getCenter; -import static io.appium.java_client.touch.WaitOptions.waitOptions; -import static io.appium.java_client.touch.offset.ElementOption.element; -import static java.time.Duration.ofSeconds; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import io.appium.java_client.AppiumBy; -import io.appium.java_client.functions.ActionSupplier; -import io.appium.java_client.touch.offset.ElementOption; -import org.junit.jupiter.api.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.Point; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public class AndroidAbilityToUseSupplierTest extends BaseAndroidTest { - - private final ActionSupplier horizontalSwipe = () -> { - driver.findElement(By.id("io.appium.android.apis:id/gallery")); - - WebElement gallery = driver.findElement(By.id("io.appium.android.apis:id/gallery")); - List images = gallery.findElements(AppiumBy.className("android.widget.ImageView")); - Point location = gallery.getLocation(); - Point center = getCenter(gallery, location); - - ElementOption pressOption = element(images.get(2),-10,center.y - location.y); - ElementOption moveOption = element(gallery, 10,center.y - location.y); - - return new AndroidTouchAction(driver) - .press(pressOption) - .waitAction(waitOptions(ofSeconds(2))) - .moveTo(moveOption) - .release(); - }; - - private final ActionSupplier verticalSwiping = () -> - new AndroidTouchAction(driver) - .press(element(driver.findElement(AppiumBy.accessibilityId("Gallery")))) - - .waitAction(waitOptions(ofSeconds(2))) - - .moveTo(element(driver.findElement(AppiumBy.accessibilityId("Auto Complete")))) - .release(); - - @Test public void horizontalSwipingWithSupplier() { - Activity activity = new Activity("io.appium.android.apis", ".view.Gallery1"); - driver.startActivity(activity); - WebElement gallery = driver.findElement(By.id("io.appium.android.apis:id/gallery")); - List images = gallery.findElements(AppiumBy.className("android.widget.ImageView")); - int originalImageCount = images.size(); - - horizontalSwipe.get().perform(); - - assertNotEquals(originalImageCount, - gallery.findElements(AppiumBy.className("android.widget.ImageView")).size()); - } - - @Test public void verticalSwipingWithSupplier() throws Exception { - driver.resetApp(); - driver.findElement(AppiumBy.accessibilityId("Views")).click(); - - Point originalLocation = driver.findElement(AppiumBy.accessibilityId("Gallery")).getLocation(); - verticalSwiping.get().perform(); - Thread.sleep(5000); - assertNotEquals(originalLocation, driver.findElement(AppiumBy.accessibilityId("Gallery")).getLocation()); - } -} diff --git a/src/test/java/io/appium/java_client/android/AndroidActivityTest.java b/src/test/java/io/appium/java_client/android/AndroidActivityTest.java deleted file mode 100644 index 86377ac11..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidActivityTest.java +++ /dev/null @@ -1,74 +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.android; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.appium.java_client.android.nativekey.AndroidKey; -import io.appium.java_client.android.nativekey.KeyEvent; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class AndroidActivityTest extends BaseAndroidTest { - - @BeforeEach public void setUp() { - Activity activity = new Activity("io.appium.android.apis", ".ApiDemos"); - driver.startActivity(activity); - } - - @Test public void startActivityInThisAppTestCase() { - Activity activity = new Activity("io.appium.android.apis", - ".accessibility.AccessibilityNodeProviderActivity"); - driver.startActivity(activity); - assertEquals(driver.currentActivity(), - ".accessibility.AccessibilityNodeProviderActivity"); - } - - @Test public void startActivityWithWaitingAppTestCase() { - final Activity activity = new Activity("io.appium.android.apis", - ".accessibility.AccessibilityNodeProviderActivity") - .setAppWaitPackage("io.appium.android.apis") - .setAppWaitActivity(".accessibility.AccessibilityNodeProviderActivity"); - driver.startActivity(activity); - assertEquals(driver.currentActivity(), - ".accessibility.AccessibilityNodeProviderActivity"); - } - - @Test public void startActivityInNewAppTestCase() { - Activity activity = new Activity("com.android.settings", ".Settings"); - driver.startActivity(activity); - assertEquals(driver.currentActivity(), ".Settings"); - driver.pressKey(new KeyEvent(AndroidKey.BACK)); - assertEquals(driver.currentActivity(), ".ApiDemos"); - } - - @Test public void startActivityInNewAppTestCaseWithoutClosingApp() { - Activity activity = new Activity("io.appium.android.apis", - ".accessibility.AccessibilityNodeProviderActivity"); - driver.startActivity(activity); - assertEquals(driver.currentActivity(), ".accessibility.AccessibilityNodeProviderActivity"); - - Activity newActivity = new Activity("com.android.settings", ".Settings") - .setAppWaitPackage("com.android.settings") - .setAppWaitActivity(".Settings") - .setStopApp(false); - driver.startActivity(newActivity); - assertEquals(driver.currentActivity(), ".Settings"); - driver.pressKey(new KeyEvent(AndroidKey.BACK)); - assertEquals(driver.currentActivity(), ".accessibility.AccessibilityNodeProviderActivity"); - } -} diff --git a/src/test/java/io/appium/java_client/android/AndroidTouchTest.java b/src/test/java/io/appium/java_client/android/AndroidTouchTest.java deleted file mode 100644 index 958da3140..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidTouchTest.java +++ /dev/null @@ -1,202 +0,0 @@ -package io.appium.java_client.android; - -import static io.appium.java_client.TestUtils.getCenter; -import static io.appium.java_client.touch.LongPressOptions.longPressOptions; -import static io.appium.java_client.touch.TapOptions.tapOptions; -import static io.appium.java_client.touch.WaitOptions.waitOptions; -import static io.appium.java_client.touch.offset.ElementOption.element; -import static io.appium.java_client.touch.offset.PointOption.point; -import static java.time.Duration.ofSeconds; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import io.appium.java_client.AppiumBy; -import io.appium.java_client.MultiTouchAction; -import io.appium.java_client.TouchAction; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.Point; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public class AndroidTouchTest extends BaseAndroidTest { - - @BeforeEach - public void setUp() { - driver.resetApp(); - } - - @Test public void dragNDropByElementTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.DragAndDropDemo"); - driver.startActivity(activity); - WebElement dragDot1 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_1")); - WebElement dragDot3 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_3")); - - WebElement dragText = driver.findElement(By.id("io.appium.android.apis:id/drag_text")); - assertEquals("Drag text not empty", "", dragText.getText()); - - TouchAction dragNDrop = new TouchAction(driver) - .longPress(element(dragDot1)) - .moveTo(element(dragDot3)) - .release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void dragNDropByElementAndDurationTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.DragAndDropDemo"); - driver.startActivity(activity); - WebElement dragDot1 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_1")); - WebElement dragDot3 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_3")); - - WebElement dragText = driver.findElement(By.id("io.appium.android.apis:id/drag_text")); - assertEquals("Drag text not empty", "", dragText.getText()); - - TouchAction dragNDrop = new TouchAction(driver) - .longPress(longPressOptions() - .withElement(element(dragDot1)) - .withDuration(ofSeconds(2))) - .moveTo(element(dragDot3)) - .release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void dragNDropByCoordinatesTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.DragAndDropDemo"); - driver.startActivity(activity); - WebElement dragDot1 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_1")); - WebElement dragDot3 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_3")); - - WebElement dragText = driver.findElement(By.id("io.appium.android.apis:id/drag_text")); - assertEquals("Drag text not empty", "", dragText.getText()); - - Point center1 = getCenter(dragDot1); - Point center2 = getCenter(dragDot3); - - TouchAction dragNDrop = new TouchAction(driver) - .longPress(point(center1.x, center1.y)) - .moveTo(point(center2.x, center2.y)) - .release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void dragNDropByCoordinatesAndDurationTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.DragAndDropDemo"); - driver.startActivity(activity); - WebElement dragDot1 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_1")); - WebElement dragDot3 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_3")); - - WebElement dragText = driver.findElement(By.id("io.appium.android.apis:id/drag_text")); - assertEquals("Drag text not empty", "", dragText.getText()); - - Point center1 = getCenter(dragDot1); - Point center2 = getCenter(dragDot3); - - TouchAction dragNDrop = new TouchAction(driver) - .longPress(longPressOptions() - .withPosition(point(center1.x, center1.y)) - .withDuration(ofSeconds(2))) - .moveTo(point(center2.x, center2.y)) - .release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void pressByCoordinatesTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.Buttons1"); - driver.startActivity(activity); - Point point = driver.findElement(By.id("io.appium.android.apis:id/button_toggle")).getLocation(); - new TouchAction(driver) - .press(point(point.x + 20, point.y + 30)) - .waitAction(waitOptions(ofSeconds(1))) - .release() - .perform(); - assertEquals("ON", driver.findElement(By.id("io.appium.android.apis:id/button_toggle")).getText()); - } - - @Test public void pressByElementTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.Buttons1"); - driver.startActivity(activity); - new TouchAction(driver) - .press(element(driver.findElement(By.id("io.appium.android.apis:id/button_toggle")))) - .waitAction(waitOptions(ofSeconds(1))) - .release() - .perform(); - assertEquals("ON", driver.findElement(By.id("io.appium.android.apis:id/button_toggle")).getText()); - } - - @Test public void tapActionTestByElement() throws Exception { - Activity activity = new Activity("io.appium.android.apis", ".view.ChronometerDemo"); - driver.startActivity(activity); - WebElement chronometer = driver.findElement(By.id("io.appium.android.apis:id/chronometer")); - - TouchAction startStop = new TouchAction(driver) - .tap(tapOptions().withElement(element(driver.findElement(By.id("io.appium.android.apis:id/start"))))) - .waitAction(waitOptions(ofSeconds(2))) - .tap(tapOptions().withElement(element(driver.findElement(By.id("io.appium.android.apis:id/stop"))))); - - startStop.perform(); - - String time = chronometer.getText(); - assertNotEquals(time, "Initial format: 00:00"); - Thread.sleep(2500); - assertEquals(time, chronometer.getText()); - } - - @Test public void tapActionTestByCoordinates() throws Exception { - Activity activity = new Activity("io.appium.android.apis", ".view.ChronometerDemo"); - driver.startActivity(activity); - WebElement chronometer = driver.findElement(By.id("io.appium.android.apis:id/chronometer")); - - Point center1 = getCenter(driver.findElement(By.id("io.appium.android.apis:id/start"))); - - TouchAction startStop = new TouchAction(driver) - .tap(point(center1.x, center1.y)) - .tap(element(driver.findElement(By.id("io.appium.android.apis:id/stop")), 5, 5)); - startStop.perform(); - - String time = chronometer.getText(); - assertNotEquals(time, "Initial format: 00:00"); - Thread.sleep(2500); - assertEquals(time, chronometer.getText()); - } - - @Test public void horizontalSwipingTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.Gallery1"); - driver.startActivity(activity); - - WebElement gallery = driver.findElement(By.id("io.appium.android.apis:id/gallery")); - List images = gallery.findElements(AppiumBy.className("android.widget.ImageView")); - int originalImageCount = images.size(); - Point location = gallery.getLocation(); - Point center = getCenter(gallery); - - TouchAction swipe = new TouchAction(driver) - .press(element(images.get(2),-10, center.y - location.y)) - .waitAction(waitOptions(ofSeconds(2))) - .moveTo(element(gallery,10,center.y - location.y)) - .release(); - swipe.perform(); - assertNotEquals(originalImageCount, - gallery.findElements(AppiumBy.className("android.widget.ImageView")).size()); - } - - @Test public void multiTouchTest() { - Activity activity = new Activity("io.appium.android.apis", ".view.Buttons1"); - driver.startActivity(activity); - TouchAction press = new TouchAction(driver) - .press(element(driver.findElement(By.id("io.appium.android.apis:id/button_toggle")))) - .waitAction(waitOptions(ofSeconds(1))) - .release(); - new MultiTouchAction(driver) - .add(press) - .perform(); - assertEquals("ON", driver.findElement(By.id("io.appium.android.apis:id/button_toggle")).getText()); - } - -} diff --git a/src/test/java/io/appium/java_client/android/IntentTest.java b/src/test/java/io/appium/java_client/android/IntentTest.java deleted file mode 100644 index a25e6a0bf..000000000 --- a/src/test/java/io/appium/java_client/android/IntentTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.appium.java_client.android; - -import static io.appium.java_client.TestResources.intentExampleApk; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.appium.java_client.android.options.UiAutomator2Options; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.openqa.selenium.By; - -import java.util.function.Predicate; - -public class IntentTest { - private static AppiumDriverLocalService service; - protected static AndroidDriver driver; - - /** - * initialization. - */ - @BeforeAll public static void beforeClass() { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new RuntimeException("An appium server node is not started!"); - } - - UiAutomator2Options options = new UiAutomator2Options() - .setDeviceName("Android Emulator") - .setApp(intentExampleApk().toAbsolutePath().toString()); - driver = new AndroidDriver(service.getUrl(), options); - } - - /** - * finishing. - */ - @AfterAll public static void afterClass() { - if (driver != null) { - driver.quit(); - } - if (service != null) { - service.stop(); - } - } - - - @Test public void startActivityWithIntent() { - Predicate predicate = driver -> { - Activity activity = new Activity("com.android.mms", - ".ui.ComposeMessageActivity") - .setIntentAction("android.intent.action.SEND") - .setIntentCategory("android.intent.category.DEFAULT") - .setIntentFlags("0x4000000") - .setOptionalIntentArguments("-d \"TestIntent\" -t \"text/plain\""); - driver.startActivity(activity); - return true; - }; - assertTrue(predicate.test(driver)); - - } - - @Test public void startActivityWithDefaultIntentAndDefaultCategoryWithOptionalArgs() { - final Activity activity = new Activity("com.prgguru.android", ".GreetingActivity") - .setIntentAction("android.intent.action.MAIN") - .setIntentCategory("android.intent.category.DEFAULT") - .setIntentFlags("0x4000000") - .setOptionalIntentArguments("--es \"USERNAME\" \"AppiumIntentTest\" -t \"text/plain\""); - driver.startActivity(activity); - assertEquals(driver.findElement(By.id("com.prgguru.android:id/textView1")).getText(), - "Welcome AppiumIntentTest"); - } -} diff --git a/src/test/java/io/appium/java_client/android/geolocation/AndroidGeoLocationTest.java b/src/test/java/io/appium/java_client/android/geolocation/AndroidGeoLocationTest.java new file mode 100644 index 000000000..47746c42a --- /dev/null +++ b/src/test/java/io/appium/java_client/android/geolocation/AndroidGeoLocationTest.java @@ -0,0 +1,59 @@ +package io.appium.java_client.android.geolocation; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class AndroidGeoLocationTest { + + @Test + void shouldThrowExceptionWhenLatitudeIsNotSet() { + var androidGeoLocation = new AndroidGeoLocation().withLongitude(24.105078); + + var exception = assertThrows(IllegalArgumentException.class, androidGeoLocation::build); + + assertEquals("A valid 'latitude' must be provided", exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenLongitudeIsNotSet() { + var androidGeoLocation = new AndroidGeoLocation().withLatitude(56.946285); + + var exception = assertThrows(IllegalArgumentException.class, androidGeoLocation::build); + + assertEquals("A valid 'longitude' must be provided", exception.getMessage()); + } + + @Test + void shodBuildMinimalParameters() { + var androidGeoLocation = new AndroidGeoLocation() + .withLongitude(24.105078) + .withLatitude(56.946285); + + assertParameters(androidGeoLocation.build(), 24.105078, 56.946285, null, null, null); + } + + @Test + void shodBuildFullParameters() { + var androidGeoLocation = new AndroidGeoLocation() + .withLongitude(24.105078) + .withLatitude(56.946285) + .withAltitude(7) + .withSpeed(1.5) + .withSatellites(12); + + assertParameters(androidGeoLocation.build(), 24.105078, 56.946285, 7.0, 1.5, 12); + } + + private static void assertParameters(Map actualParams, double longitude, double latitude, + Double altitude, Double speed, Integer satellites) { + assertEquals(longitude, actualParams.get("longitude")); + assertEquals(latitude, actualParams.get("latitude")); + assertEquals(altitude, actualParams.get("altitude")); + assertEquals(speed, actualParams.get("speed")); + assertEquals(satellites, actualParams.get("satellites")); + } +} diff --git a/src/test/java/io/appium/java_client/android/nativekey/KeyEventTest.java b/src/test/java/io/appium/java_client/android/nativekey/KeyEventTest.java new file mode 100644 index 000000000..707da9bfa --- /dev/null +++ b/src/test/java/io/appium/java_client/android/nativekey/KeyEventTest.java @@ -0,0 +1,48 @@ +package io.appium.java_client.android.nativekey; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class KeyEventTest { + + @Test + void shouldThrowExceptionWhenKeyCodeIsNotSet() { + var keyEvent = new KeyEvent(); + + var exception = assertThrows(IllegalStateException.class, keyEvent::build); + + assertEquals("The key code must be set", exception.getMessage()); + } + + @Test + void shouldBuildMinimalParameters() { + var keyEvent = new KeyEvent().withKey(AndroidKey.A); + + Map params = keyEvent.build(); + + assertParameters(params, AndroidKey.A, null, null); + } + + @Test + void shouldBuildFullParameters() { + var keyEvent = new KeyEvent() + .withKey(AndroidKey.A) + .withMetaModifier(KeyEventMetaModifier.CAP_LOCKED) + .withFlag(KeyEventFlag.KEEP_TOUCH_MODE); + + Map params = keyEvent.build(); + + assertParameters(params, AndroidKey.A, KeyEventMetaModifier.CAP_LOCKED.getValue(), + KeyEventFlag.KEEP_TOUCH_MODE.getValue()); + } + + private static void assertParameters(Map params, AndroidKey key, Integer metastate, Integer flags) { + assertEquals(key.getCode(), params.get("keycode")); + assertEquals(metastate, params.get("metastate")); + assertEquals(flags, params.get("flags")); + } +} diff --git a/src/test/java/io/appium/java_client/drivers/options/OptionsBuildingTest.java b/src/test/java/io/appium/java_client/drivers/options/OptionsBuildingTest.java index ab10f1ec1..4ab700ca3 100644 --- a/src/test/java/io/appium/java_client/drivers/options/OptionsBuildingTest.java +++ b/src/test/java/io/appium/java_client/drivers/options/OptionsBuildingTest.java @@ -16,13 +16,12 @@ package io.appium.java_client.drivers.options; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import io.appium.java_client.android.options.EspressoOptions; import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.android.options.localization.AppLocale; import io.appium.java_client.android.options.server.EspressoBuildConfig; import io.appium.java_client.android.options.signing.KeystoreConfig; +import io.appium.java_client.chromium.options.ChromiumOptions; import io.appium.java_client.gecko.options.GeckoOptions; import io.appium.java_client.gecko.options.Verbosity; import io.appium.java_client.ios.options.XCUITestOptions; @@ -41,6 +40,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; +import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -59,14 +60,14 @@ public void canBuildXcuiTestOptions() throws MalformedURLException { .setWdaBaseUrl("http://localhost:8000") .setPermissions(new Permissions() .withAppPermissions("com.apple.MobileSafari", - ImmutableMap.of("calendar", "YES"))) + Map.of("calendar", "YES"))) .setSafariSocketChunkSize(10) .setCommandTimeouts(new CommandTimeouts() .withCommandTimeout("yolo", Duration.ofSeconds(1))); assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); assertEquals(new URL("http://localhost:8000"), options.getWdaBaseUrl().orElse(null)); assertNotNull(options.getPermissions() - .map((v) -> v.getAppPermissions("com.apple.MobileSafari")) + .map(v -> v.getAppPermissions("com.apple.MobileSafari")) .orElse(null)); assertEquals(10L, (long) options.getSafariSocketChunkSize().orElse(0)); assertEquals(1L, options.getCommandTimeouts().orElse(null).left() @@ -104,15 +105,15 @@ public void canBuildEspressoOptions() { .withLanguage("zh") .withVariant("hans")) .setEspressoBuildConfig(new EspressoBuildConfig() - .withAdditionalAppDependencies(ImmutableList.of( + .withAdditionalAppDependencies(List.of( "com.dep1:1.2.3", "com.dep2:1.2.3" )) - ); + ); assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); assertEquals("CN", options.getAppLocale().orElse(null).getCountry().orElse(null)); assertEquals(2, options.getEspressoBuildConfig().orElse(null) - .left().getAdditionalAppDependencies().orElse(null).size()); + .left().getAdditionalAppDependencies().orElse(null).size()); assertTrue(options.doesForceEspressoRebuild().orElse(false)); } @@ -153,8 +154,8 @@ public void canBuildGeckoOptions() { assertEquals(AutomationName.GECKO, options.getAutomationName().orElse(null)); options.setNewCommandTimeout(Duration.ofSeconds(10)) .setVerbosity(Verbosity.TRACE) - .setMozFirefoxOptions(ImmutableMap.of( - "profile", "yolo" + .setMozFirefoxOptions(Map.of( + "profile", "yolo" )); assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); assertEquals(Verbosity.TRACE, options.getVerbosity().orElse(null)); @@ -172,7 +173,7 @@ public void canBuildSafariOptions() { .setWebkitWebrtc(new WebrtcData() .withDisableIceCandidateFiltering(true) .withDisableInsecureMediaCapture(true) - ); + ); assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); assertTrue(options.doesSafariUseSimulator().orElse(false)); assertTrue(options.getWebkitWebrtc().orElse(null) @@ -180,4 +181,33 @@ public void canBuildSafariOptions() { assertTrue(options.getWebkitWebrtc().orElse(null) .doesDisableInsecureMediaCapture().orElse(false)); } + + @Test + public void canBuildChromiumOptions() { + // Given + // When + ChromiumOptions options = new ChromiumOptions(); + + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .setPlatformName(Platform.MAC.name()) + .withBrowserName("Chrome") + .setAutodownloadEnabled(true) + .setBuildCheckDisabled(true) + .setChromeDriverPort(5485) + .setExecutable("/absolute/executable/path") + .setLogPath("/wonderful/log/path") + .setVerbose(true); + + // Then + assertEquals(AutomationName.CHROMIUM, options.getAutomationName().orElse(null)); + assertEquals("Chrome", options.getBrowserName()); + assertTrue(options.isAutodownloadEnabled().orElse(null)); + assertTrue(options.isBuildCheckDisabled().orElse(null)); + assertEquals(5485, options.getChromeDriverPort().orElse(null)); + assertFalse(options.getExecutableDir().isPresent()); + assertEquals("/absolute/executable/path", options.getExecutable().orElse(null)); + assertEquals("/wonderful/log/path", options.getLogPath().orElse(null)); + assertFalse(options.isUseSystemExecutable().isPresent()); + assertTrue(options.isVerbose().orElse(null)); + } } 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 5316f56e4..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 @@ -16,19 +16,14 @@ package io.appium.java_client.events.stubs; -import com.google.common.collect.ImmutableList; 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.DeviceRotation; import org.openqa.selenium.HasCapabilities; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.OutputType; -import org.openqa.selenium.Rotatable; -import org.openqa.selenium.ScreenOrientation; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; @@ -43,39 +38,12 @@ import java.util.Map; import java.util.Set; -public class EmptyWebDriver implements WebDriver, ContextAware, Rotatable, - JavascriptExecutor, HasCapabilities, TakesScreenshot { +public class EmptyWebDriver implements WebDriver, JavascriptExecutor, HasCapabilities, TakesScreenshot { public EmptyWebDriver() { } private static List createStubList() { - return ImmutableList.of(new StubWebElement(), new StubWebElement()); - } - - public WebDriver context(String name) { - return null; - } - - public Set getContextHandles() { - return null; - } - - public String getContext() { - return ""; - } - - public void rotate(ScreenOrientation orientation) { - } - - public void rotate(DeviceRotation rotation) { - } - - public ScreenOrientation getOrientation() { - return null; - } - - public DeviceRotation rotation() { - return null; + return List.of(new StubWebElement(), new StubWebElement()); } public void get(String url) { @@ -202,10 +170,6 @@ public Timeouts timeouts() { return null; } - public ImeHandler ime() { - return null; - } - public Window window() { return new StubWindow(); } diff --git a/src/test/java/io/appium/java_client/events/stubs/StubWebElement.java b/src/test/java/io/appium/java_client/events/stubs/StubWebElement.java index 4be313211..a84708083 100644 --- a/src/test/java/io/appium/java_client/events/stubs/StubWebElement.java +++ b/src/test/java/io/appium/java_client/events/stubs/StubWebElement.java @@ -16,9 +16,6 @@ package io.appium.java_client.events.stubs; -import com.google.common.collect.ImmutableList; -import java.util.ArrayList; -import java.util.List; import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.OutputType; @@ -27,12 +24,15 @@ import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; +import java.util.ArrayList; +import java.util.List; + public class StubWebElement implements WebElement { public StubWebElement() { } private static List createStubSubElementList() { - return new ArrayList<>(ImmutableList.of(new StubWebElement(), new StubWebElement())); + return new ArrayList<>(List.of(new StubWebElement(), new StubWebElement())); } public void click() { diff --git a/src/test/java/io/appium/java_client/internal/AppiumUserAgentFilterTest.java b/src/test/java/io/appium/java_client/internal/AppiumUserAgentFilterTest.java new file mode 100644 index 000000000..32c5c2276 --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/AppiumUserAgentFilterTest.java @@ -0,0 +1,60 @@ +package io.appium.java_client.internal; + +import io.appium.java_client.internal.filters.AppiumUserAgentFilter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AppiumUserAgentFilterTest { + @Test + void validateUserAgent() { + assertTrue(AppiumUserAgentFilter.USER_AGENT.startsWith("appium/")); + } + + @ParameterizedTest + @ValueSource(strings = { + "appium/8.2.0 (selenium/4.5.0 (java mac))", + "APPIUM/8.2.0 (selenium/4.5.0 (java mac))", + "something (Appium/8.2.0 (selenium/4.5.0 (java mac)))", + "something (appium/8.2.0 (selenium/4.5.0 (java mac)))" + }) + void validUserAgentIfContainsAppiumName(String userAgent) { + assertEquals(AppiumUserAgentFilter.buildUserAgent(userAgent), userAgent); + } + + @Test + void validBuildUserAgentNoUA() { + assertEquals(AppiumUserAgentFilter.buildUserAgent(null), AppiumUserAgentFilter.USER_AGENT); + } + + @Test + void validBuildUserAgentNoAppium1() { + String ua = AppiumUserAgentFilter.buildUserAgent("selenium/4.5.0 (java mac)"); + assertTrue(ua.startsWith("appium/")); + assertTrue(ua.endsWith("selenium/4.5.0 (java mac))")); + } + + @Test + void validBuildUserAgentNoAppium2() { + String ua = AppiumUserAgentFilter.buildUserAgent("customSelenium/4.5.0 (java mac)"); + assertTrue(ua.startsWith("appium/")); + assertTrue(ua.endsWith("customSelenium/4.5.0 (java mac))")); + } + + @Test + void validBuildUserAgentAlreadyHasAppium1() { + // Won't modify since the UA already has appium prefix + String ua = AppiumUserAgentFilter.buildUserAgent("appium/8.1.0 (selenium/4.5.0 (java mac))"); + assertEquals("appium/8.1.0 (selenium/4.5.0 (java mac))", ua); + } + + @Test + void validBuildUserAgentAlreadyHasAppium2() { + // Won't modify since the UA already has appium prefix + String ua = AppiumUserAgentFilter.buildUserAgent("something (appium/8.1.0 (selenium/4.5.0 (java mac)))"); + assertEquals("something (appium/8.1.0 (selenium/4.5.0 (java mac)))", ua); + } +} diff --git a/src/test/java/io/appium/java_client/internal/ConfigTest.java b/src/test/java/io/appium/java_client/internal/ConfigTest.java index cd2bd390c..f509a4d23 100644 --- a/src/test/java/io/appium/java_client/internal/ConfigTest.java +++ b/src/test/java/io/appium/java_client/internal/ConfigTest.java @@ -1,35 +1,41 @@ package io.appium.java_client.internal; +import io.appium.java_client.internal.filters.AppiumUserAgentFilter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; +class ConfigTest { + private static final String SELENIUM_EXISTING_KEY = "selenium.version"; -public class ConfigTest { - private static final String EXISTING_KEY = "selenium.version"; private static final String MISSING_KEY = "bla"; - @Test - public void verifyGettingExistingValue() { - assertThat(Config.main().getValue(EXISTING_KEY, String.class).length(), greaterThan(0)); - assertTrue(Config.main().getOptionalValue(EXISTING_KEY, String.class).isPresent()); + @ParameterizedTest + @ValueSource(strings = {SELENIUM_EXISTING_KEY, AppiumUserAgentFilter.VERSION_KEY}) + void verifyGettingExistingValue(String key) { + assertThat(Config.main().getValue(key, String.class).length(), greaterThan(0)); + assertTrue(Config.main().getOptionalValue(key, String.class).isPresent()); } @Test - public void verifyGettingNonExistingValue() { + void verifyGettingNonExistingValue() { assertThrows(IllegalArgumentException.class, () -> Config.main().getValue(MISSING_KEY, String.class)); } - @Test - public void verifyGettingExistingValueWithWrongClass() { - assertThrows(ClassCastException.class, () -> Config.main().getValue(EXISTING_KEY, Integer.class)); + @ParameterizedTest + @ValueSource(strings = {SELENIUM_EXISTING_KEY, AppiumUserAgentFilter.VERSION_KEY}) + void verifyGettingExistingValueWithWrongClass(String key) { + assertThrows(ClassCastException.class, () -> Config.main().getValue(key, Integer.class)); } @Test - public void verifyGettingNonExistingOptionalValue() { + void verifyGettingNonExistingOptionalValue() { assertFalse(Config.main().getOptionalValue(MISSING_KEY, String.class).isPresent()); } } diff --git a/src/test/java/io/appium/java_client/internal/DirectConnectTest.java b/src/test/java/io/appium/java_client/internal/DirectConnectTest.java new file mode 100644 index 000000000..8255c3c5d --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/DirectConnectTest.java @@ -0,0 +1,59 @@ +package io.appium.java_client.internal; + +import io.appium.java_client.remote.DirectConnect; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DirectConnectTest { + + @Test + void hasValidDirectConnectValuesWithoutAppiumPrefix() throws MalformedURLException { + Map responseValue = new HashMap<>(); + responseValue.put("directConnectProtocol", "https"); + responseValue.put("directConnectPath", "/path/to"); + responseValue.put("directConnectHost", "host"); + responseValue.put("directConnectPort", "8080"); + DirectConnect directConnect = new DirectConnect(responseValue); + assertTrue(directConnect.isValid()); + assertEquals(directConnect.getUrl().toString(), "https://host:8080/path/to"); + } + + @Test + void hasValidDirectConnectValuesWithAppiumPrefix() throws MalformedURLException { + Map responseValue = new HashMap<>(); + responseValue.put("appium:directConnectProtocol", "https"); + responseValue.put("appium:directConnectPath", "/path/to"); + responseValue.put("appium:directConnectHost", "host"); + responseValue.put("appium:directConnectPort", "8080"); + DirectConnect directConnect = new DirectConnect(responseValue); + assertTrue(directConnect.isValid()); + assertEquals(directConnect.getUrl().toString(), "https://host:8080/path/to"); + } + + @Test + void hasValidDirectConnectStringPort() { + Map responseValue = new HashMap<>(); + responseValue.put("appium:directConnectProtocol", "https"); + responseValue.put("appium:directConnectPath", "/path/to"); + responseValue.put("appium:directConnectHost", "host"); + responseValue.put("appium:directConnectPort", "port"); + DirectConnect directConnect = new DirectConnect(responseValue); + assertTrue(directConnect.isValid()); + assertThrowsExactly(MalformedURLException.class, directConnect::getUrl); + } + + @Test + void hasInvalidDirectConnect() { + Map responseValue = new HashMap<>(); + DirectConnect directConnect = new DirectConnect(responseValue); + assertFalse(directConnect.isValid()); + } +} diff --git a/src/test/java/io/appium/java_client/internal/SessionConnectTest.java b/src/test/java/io/appium/java_client/internal/SessionConnectTest.java new file mode 100644 index 000000000..a97653882 --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/SessionConnectTest.java @@ -0,0 +1,38 @@ +/* + * 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.internal; + +import io.appium.java_client.ios.IOSDriver; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.WebDriverException; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SessionConnectTest { + + @Test + void canConnectToASession() throws MalformedURLException { + IOSDriver driver = new IOSDriver(new URL("http://localhost:4723/session/1234")); + assertEquals(driver.getSessionId().toString(), "1234"); + assertThrows(WebDriverException.class, driver::quit); + } + +} diff --git a/src/test/java/io/appium/java_client/ios/AppIOSTest.java b/src/test/java/io/appium/java_client/ios/AppIOSTest.java deleted file mode 100644 index b049a15bb..000000000 --- a/src/test/java/io/appium/java_client/ios/AppIOSTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.appium.java_client.ios; - -import io.appium.java_client.ios.options.XCUITestOptions; -import org.junit.jupiter.api.BeforeAll; -import org.openqa.selenium.SessionNotCreatedException; - -import java.time.Duration; - -import static io.appium.java_client.TestResources.testAppZip; - -public class AppIOSTest extends BaseIOSTest { - - public static final String BUNDLE_ID = "io.appium.TestApp"; - - @BeforeAll - public static void beforeClass() throws Exception { - startAppiumServer(); - - XCUITestOptions options = new XCUITestOptions() - .setPlatformVersion(PLATFORM_VERSION) - .setDeviceName(DEVICE_NAME) - .setCommandTimeouts(Duration.ofSeconds(240)) - .setApp(testAppZip().toAbsolutePath().toString()) - .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT); - try { - driver = new IOSDriver(service.getUrl(), options); - } catch (SessionNotCreatedException e) { - options.useNewWDA(); - driver = new IOSDriver(service.getUrl(), options); - } - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSElementTest.java b/src/test/java/io/appium/java_client/ios/IOSElementTest.java deleted file mode 100644 index 18db02e2b..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSElementTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.appium.java_client.ios; - -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 org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.WebDriverWait; - -import io.appium.java_client.AppiumBy; - -import java.time.Duration; - -@TestMethodOrder(MethodOrderer.MethodName.class) -public class IOSElementTest extends AppIOSTest { - - @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)); - - WebElement slider = wait.until( - driver1 -> driver1.findElement(AppiumBy.className("XCUIElementTypeSlider"))); - slider.sendKeys("0%"); - assertEquals("0%", slider.getAttribute("value")); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSSearchingTest.java b/src/test/java/io/appium/java_client/ios/IOSSearchingTest.java deleted file mode 100644 index 30213f480..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSSearchingTest.java +++ /dev/null @@ -1,53 +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 static org.junit.jupiter.api.Assertions.assertNotEquals; - -import org.junit.jupiter.api.Test; - -import io.appium.java_client.AppiumBy; - -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 findByByIosPredicatesTest() { - assertNotEquals(driver - .findElement(AppiumBy.iOSNsPredicateString("name like 'Answer'")) - .getText(), null); - assertNotEquals(driver - .findElements(AppiumBy.iOSNsPredicateString("name like 'Answer'")) - .size(), 0); - } - - @Test public void findByByIosClassChainTest() { - assertNotEquals(driver - .findElement(AppiumBy.iOSClassChain("**/XCUIElementTypeButton")) - .getText(), null); - assertNotEquals(driver - .findElements(AppiumBy.iOSClassChain("**/XCUIElementTypeButton")) - .size(), 0); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSTouchTest.java b/src/test/java/io/appium/java_client/ios/IOSTouchTest.java deleted file mode 100644 index 46a4d0474..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSTouchTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.appium.java_client.ios; - -import static io.appium.java_client.ios.touch.IOSPressOptions.iosPressOptions; -import static io.appium.java_client.touch.TapOptions.tapOptions; -import static io.appium.java_client.touch.WaitOptions.waitOptions; -import static io.appium.java_client.touch.offset.ElementOption.element; -import static java.time.Duration.ofMillis; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; - -import io.appium.java_client.AppiumBy; -import io.appium.java_client.MultiTouchAction; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.openqa.selenium.By; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.time.Duration; - -@TestMethodOrder(MethodOrderer.MethodName.class) -public class IOSTouchTest extends AppIOSTest { - - @Test - public void tapTest() { - WebElement intA = driver.findElement(By.id("IntegerA")); - WebElement intB = driver.findElement(By.id("IntegerB")); - intA.clear(); - intB.clear(); - intA.sendKeys("2"); - intB.sendKeys("4"); - - WebElement e = driver.findElement(AppiumBy.accessibilityId("ComputeSumButton")); - new IOSTouchAction(driver).tap(tapOptions().withElement(element(e))).perform(); - assertEquals(driver.findElement(By.xpath("//*[@name = \"Answer\"]")).getText(), "6"); - } - - @Test - public void touchWithPressureTest() { - WebElement intA = driver.findElement(By.id("IntegerA")); - WebElement intB = driver.findElement(By.id("IntegerB")); - intA.clear(); - intB.clear(); - intA.sendKeys("2"); - intB.sendKeys("4"); - - WebElement e = driver.findElement(AppiumBy.accessibilityId("ComputeSumButton")); - new IOSTouchAction(driver) - .press(iosPressOptions() - .withElement(element(e)) - .withPressure(1)) - .waitAction(waitOptions(ofMillis(100))) - .release() - .perform(); - assertEquals(driver.findElement(By.xpath("//*[@name = \"Answer\"]")).getText(), "6"); - } - - @Test public void multiTouchTest() { - WebElement e = driver.findElement(AppiumBy.accessibilityId("ComputeSumButton")); - WebElement e2 = driver.findElement(AppiumBy.accessibilityId("show alert")); - - IOSTouchAction tap1 = new IOSTouchAction(driver).tap(tapOptions().withElement(element(e))); - IOSTouchAction tap2 = new IOSTouchAction(driver).tap(tapOptions().withElement(element(e2))); - - new MultiTouchAction(driver).add(tap1).add(tap2).perform(); - - WebDriverWait waiting = new WebDriverWait(driver, Duration.ofSeconds(10)); - assertNotNull(waiting.until(alertIsPresent())); - driver.switchTo().alert().accept(); - } - - @Test public void doubleTapTest() { - WebElement firstField = driver.findElement(By.id("IntegerA")); - firstField.sendKeys("2"); - - IOSTouchAction iosTouchAction = new IOSTouchAction(driver); - iosTouchAction.doubleTap(element(firstField)); - WebElement editingMenu = driver.findElement(AppiumBy.className("XCUIElementTypeTextField")); - assertNotNull(editingMenu); - } -} 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 c2cb1b6f4..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,30 +16,35 @@ package io.appium.java_client.pagefactory_tests; -import static io.appium.java_client.TestResources.helloAppiumHtml; -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.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - 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; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBys; import org.openqa.selenium.support.PageFactory; import java.util.List; +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; + public class DesktopBrowserCompatibilityTest { + private static final String HELLO_APPIUM_HTML = + TestUtils.resourcePathToAbsolutePath("html/hello appium - saved page.htm").toUri().toString(); @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) @AndroidFindBy(className = "someClass") @@ -58,14 +63,15 @@ public class DesktopBrowserCompatibilityTest { } @Test public void chromeTest() { - WebDriver driver = new ChromeDriver(); + WebDriver driver = new ChromeDriver(new ChromeOptions().addArguments("--headless=new")); try { PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(15)), this); - driver.get(helloAppiumHtml().toUri().toString()); + driver.get(HELLO_APPIUM_HTML); assertNotEquals(0, foundLinks.size()); 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/TimeoutTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java index cfba2ba36..32d23c874 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java @@ -16,18 +16,6 @@ package io.appium.java_client.pagefactory_tests; -import static io.appium.java_client.pagefactory.AppiumFieldDecorator.DEFAULT_WAITING_TIMEOUT; -import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; -import static java.lang.Math.abs; -import static java.lang.String.format; -import static java.lang.System.currentTimeMillis; -import static java.time.Duration.ofSeconds; -import static java.time.temporal.ChronoUnit.SECONDS; -import static org.apache.commons.lang3.time.DurationFormatUtils.formatDuration; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.lessThanOrEqualTo; -import static org.openqa.selenium.support.PageFactory.initElements; - import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.WithTimeout; import org.junit.jupiter.api.AfterEach; @@ -37,12 +25,25 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.support.FindAll; import org.openqa.selenium.support.FindBy; import java.time.Duration; import java.util.List; +import static io.appium.java_client.pagefactory.AppiumFieldDecorator.DEFAULT_WAITING_TIMEOUT; +import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; +import static java.lang.Math.abs; +import static java.lang.String.format; +import static java.lang.System.currentTimeMillis; +import static java.time.Duration.ofSeconds; +import static java.time.temporal.ChronoUnit.SECONDS; +import static org.apache.commons.lang3.time.DurationFormatUtils.formatDuration; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.openqa.selenium.support.PageFactory.initElements; + public class TimeoutTest { private static final long ACCEPTABLE_TIME_DIFF_MS = 1500; @@ -50,13 +51,13 @@ public class TimeoutTest { private WebDriver driver; @FindAll({ - @FindBy(className = "ClassWhichDoesNotExist"), - @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) + @FindBy(className = "ClassWhichDoesNotExist"), + @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) private List stubElements; @WithTimeout(time = 5, chronoUnit = SECONDS) @FindAll({@FindBy(className = "ClassWhichDoesNotExist"), - @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) + @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) private List stubElements2; private Duration timeOutDuration; @@ -69,7 +70,7 @@ private static long getPerformanceDiff(long expectedMs, Runnable runnable) { long startMark = currentTimeMillis(); runnable.run(); long endMark = currentTimeMillis(); - return abs(expectedMs - (endMark - startMark)); + return abs(expectedMs - (endMark - startMark)); } private static String assertionMessage(Duration expectedDuration) { @@ -86,7 +87,7 @@ public static void beforeAll() { * The setting up. */ @BeforeEach public void setUp() { - driver = new ChromeDriver(); + driver = new ChromeDriver(new ChromeOptions().addArguments("--headless=new")); timeOutDuration = DEFAULT_WAITING_TIMEOUT; initElements(new AppiumFieldDecorator(driver, timeOutDuration), this); } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java deleted file mode 100644 index 418f35e71..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java +++ /dev/null @@ -1,149 +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.pagefactory_tests; - -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; - -import io.appium.java_client.ios.AppIOSTest; -import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import io.appium.java_client.pagefactory.HowToUseLocators; -import io.appium.java_client.pagefactory.iOSXCUITFindBy; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; - -import java.util.List; - -@TestMethodOrder(MethodOrderer.MethodName.class) -public class XCUITModeTest extends AppIOSTest { - - private boolean populated = false; - - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "label contains 'Compute'") - @iOSXCUITFindBy(className = "XCUIElementTypeButton") - private WebElement computeButton; - - @HowToUseLocators(iOSXCUITAutomation = CHAIN) - @iOSXCUITFindBy(iOSNsPredicate = "name like 'Answer'") - private WebElement answer; - - @iOSXCUITFindBy(iOSNsPredicate = "name = 'IntegerA'") - private WebElement textField1; - - @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(id = "locationStatus") - private WebElement locationStatus; - - @HowToUseLocators(iOSXCUITAutomation = CHAIN) - @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'contact'") - private WebElement contactAlert; - - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'location'") - private WebElement locationAlert; - - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeTextField[2]") - private WebElement secondTextField; - - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton[-1]") - private WebElement lastButton; - - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton") - private List allButtons; - - /** - * The setting up. - */ - @BeforeEach public void setUp() { - if (!populated) { - PageFactory.initElements(new AppiumFieldDecorator(driver), this); - } - - 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 pageObjectChainingTest() { - assertTrue(contactAlert.isDisplayed()); - } - - @Test public void findElementByIdTest() { - assertTrue(locationStatus.isDisplayed()); - } - - @Test public void nativeSelectorTest() { - assertTrue(locationAlert.isDisplayed()); - } - - @Test public void findElementByClassChain() { - assertThat(secondTextField.getAttribute("name"), equalTo("IntegerB")); - } - - @Test public void findElementByClassChainWithNegativeIndex() { - assertThat(lastButton.getAttribute("name"), equalTo("Check calendar authorized")); - } - - @Test public void findMultipleElementsByClassChain() { - assertThat(allButtons.size(), is(greaterThan(1))); - } - - @Test public void findElementByXUISelectorTest() { - assertNotNull(gesture.getText()); - } - - @Test public void setValueTest() { - textField1.sendKeys("2"); - textField2.sendKeys("4"); - driver.hideKeyboard(); - computeButton.click(); - assertEquals("6", answer.getText()); - } -} 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 bea245a99..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 @@ -1,13 +1,5 @@ package io.appium.java_client.pagefactory_tests.widget.tests; -import static com.google.common.collect.ImmutableList.of; -import static io.appium.java_client.remote.AutomationName.ANDROID_UIAUTOMATOR2; -import static io.appium.java_client.remote.AutomationName.IOS_XCUI_TEST; -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 org.apache.commons.lang3.StringUtils.EMPTY; - import io.appium.java_client.HasBrowserCheck; import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; @@ -19,12 +11,20 @@ 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; import java.util.Set; import java.util.concurrent.TimeUnit; +import static io.appium.java_client.remote.AutomationName.ANDROID_UIAUTOMATOR2; +import static io.appium.java_client.remote.AutomationName.IOS_XCUI_TEST; +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 org.apache.commons.lang3.StringUtils.EMPTY; + public abstract class AbstractStubWebDriver implements WebDriver, HasBrowserCheck, @@ -61,7 +61,7 @@ public String getTitle() { @Override public List findElements(By by) { - return of(new StubWebElement(this, by), new StubWebElement(this, by)); + return List.of(new StubWebElement(this, by), new StubWebElement(this, by)); } @Override @@ -156,26 +156,73 @@ 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; } - }; - } - @Override - public ImeHandler ime() { - return null; + public Timeouts pageLoadTimeout(Duration duration) { + return this; + } + }; } @Override 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 e81022a57..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,13 +1,17 @@ package io.appium.java_client.pagefactory_tests.widget.tests; -import com.google.common.collect.ImmutableList; - 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); } @@ -17,11 +21,86 @@ public T getSubWidget() { } public List getSubWidgets() { - return ImmutableList.of(); + return List.of(); } @Override 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/StubWebElement.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/StubWebElement.java index cffb170fd..94fd5a8db 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/StubWebElement.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/StubWebElement.java @@ -1,8 +1,5 @@ package io.appium.java_client.pagefactory_tests.widget.tests; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.ImmutableList.of; - import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.OutputType; @@ -15,13 +12,15 @@ import java.util.List; +import static java.util.Objects.requireNonNull; + public class StubWebElement implements WebElement, WrapsDriver { private final WebDriver driver; private final By by; public StubWebElement(WebDriver driver, By by) { - this.driver = checkNotNull(driver); - this.by = checkNotNull(by); + this.driver = requireNonNull(driver); + this.by = requireNonNull(by); } @Override @@ -70,8 +69,8 @@ public String getText() { } @Override - public List findElements(By by) { - return of(new StubWebElement(driver, by), new StubWebElement(driver, by)); + public List findElements(By by) { + return List.of(new StubWebElement(driver, by), new StubWebElement(driver, by)); } @Override diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java index 0420d44f6..2f8e2d60d 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/WidgetTest.java @@ -1,11 +1,5 @@ package io.appium.java_client.pagefactory_tests.widget.tests; -import static java.util.stream.Collectors.toList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsString; -import static org.openqa.selenium.support.PageFactory.initElements; - import io.appium.java_client.pagefactory.AppiumFieldDecorator; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; @@ -13,6 +7,12 @@ import java.util.List; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.openqa.selenium.support.PageFactory.initElements; + public abstract class WidgetTest { protected final AbstractApp app; @@ -37,7 +37,6 @@ protected WidgetTest(AbstractApp app, WebDriver driver) { protected static void checkThatLocatorsAreCreatedCorrectly(DefaultStubWidget single, List multiple, By rootLocator, By subLocator) { - assertThat(single.toString(), containsString(rootLocator.toString())); assertThat(multiple.stream().map(DefaultStubWidget::toString).collect(toList()), contains(containsString(rootLocator.toString()), diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AndroidWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AndroidWidgetTest.java index a3eee515a..2e0f1e0ae 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AndroidWidgetTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AndroidWidgetTest.java @@ -1,16 +1,16 @@ package io.appium.java_client.pagefactory_tests.widget.tests.android; +import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; +import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; +import io.appium.java_client.pagefactory_tests.widget.tests.WidgetTest; +import org.junit.jupiter.api.Test; + import static io.appium.java_client.AppiumBy.androidUIAutomator; import static io.appium.java_client.pagefactory_tests.widget.tests.android.AndroidApp.ANDROID_DEFAULT_WIDGET_LOCATOR; import static io.appium.java_client.pagefactory_tests.widget.tests.android.AndroidApp.ANDROID_EXTERNALLY_DEFINED_WIDGET_LOCATOR; import static io.appium.java_client.pagefactory_tests.widget.tests.android.AnnotatedAndroidWidget.ANDROID_ROOT_WIDGET_LOCATOR; import static io.appium.java_client.pagefactory_tests.widget.tests.android.DefaultAndroidWidget.ANDROID_SUB_WIDGET_LOCATOR; -import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; -import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; -import io.appium.java_client.pagefactory_tests.widget.tests.WidgetTest; -import org.junit.jupiter.api.Test; - public class AndroidWidgetTest extends WidgetTest { public AndroidWidgetTest() { 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 8c93632ec..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 @@ -1,26 +1,28 @@ package io.appium.java_client.pagefactory_tests.widget.tests.combined; -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.openqa.selenium.support.PageFactory.initElements; - import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.OverrideWidget; import io.appium.java_client.pagefactory_tests.widget.tests.AbstractApp; 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 io.appium.java_client.pagefactory_tests.widget.tests.windows.DefaultWindowsWidget; +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; +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.junit.jupiter.params.provider.Arguments.arguments; +import static org.openqa.selenium.support.PageFactory.initElements; + @SuppressWarnings({"unused", "unchecked"}) public class CombinedAppTest { @@ -31,33 +33,35 @@ public class CombinedAppTest { */ public static Stream data() { return Stream.of( - Arguments.of(new CombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), - Arguments.of(new CombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), + arguments(new CombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), + arguments(new CombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), DefaultIosXCUITWidget.class), - Arguments.of(new CombinedApp(), new AbstractStubWebDriver.StubWindowsDriver(), DefaultWindowsWidget.class), - Arguments.of(new CombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), - Arguments.of(new CombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), + arguments(new CombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), + arguments(new CombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), DefaultFindByWidget.class), - Arguments.of(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), - Arguments.of(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), DefaultStubWidget.class), - Arguments.of(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubWindowsDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubWindowsDriver(), DefaultStubWidget.class), - Arguments.of(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), - Arguments.of(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), DefaultFindByWidget.class) ); } @ParameterizedTest @MethodSource("data") - public void checkThatWidgetsAreCreatedCorrectly(AbstractApp app, WebDriver driver, Class widgetClass) { + void checkThatWidgetsAreCreatedCorrectly(AbstractApp app, WebDriver driver, + Class widgetClass) { initElements(new AppiumFieldDecorator(driver), app); 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()) @@ -70,14 +74,12 @@ public static class CombinedApp implements AbstractApp { @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class) + iOSXCUITAutomation = DefaultIosXCUITWidget.class) private DefaultStubWidget singleWidget; @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class) + iOSXCUITAutomation = DefaultIosXCUITWidget.class) private List multipleWidget; @Override 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 1d686c58e..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 @@ -1,32 +1,40 @@ package io.appium.java_client.pagefactory_tests.widget.tests.combined; -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.openqa.selenium.support.PageFactory.initElements; - import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.OverrideWidget; import io.appium.java_client.pagefactory_tests.widget.tests.AbstractApp; 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 io.appium.java_client.pagefactory_tests.widget.tests.windows.DefaultWindowsWidget; 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.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. * @@ -38,8 +46,6 @@ public static Stream data() { new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), Arguments.of(new AppWithCombinedWidgets(), new AbstractStubWebDriver.StubIOSXCUITDriver(), DefaultIosXCUITWidget.class), - Arguments.of(new AppWithCombinedWidgets(), - new AbstractStubWebDriver.StubWindowsDriver(), DefaultWindowsWidget.class), Arguments.of(new AppWithCombinedWidgets(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), Arguments.of(new AppWithCombinedWidgets(), @@ -59,7 +65,8 @@ public static Stream data() { @ParameterizedTest @MethodSource("data") - public void checkThatWidgetsAreCreatedCorrectly(AbstractApp app, WebDriver driver, Class widgetClass) { + 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(), @@ -79,15 +86,13 @@ public static class CombinedWidget extends DefaultStubWidget { @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class + iOSXCUITAutomation = DefaultIosXCUITWidget.class ) private DefaultStubWidget singleWidget; @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class + iOSXCUITAutomation = DefaultIosXCUITWidget.class ) private List multipleWidget; @@ -166,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/pagefactory_tests/widget/tests/ios/XCUITWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/XCUITWidgetTest.java index 6a9bb0d53..edaa699f7 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/XCUITWidgetTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/XCUITWidgetTest.java @@ -1,16 +1,16 @@ package io.appium.java_client.pagefactory_tests.widget.tests.ios; +import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; +import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; +import io.appium.java_client.pagefactory_tests.widget.tests.WidgetTest; +import org.junit.jupiter.api.Test; + import static io.appium.java_client.AppiumBy.iOSNsPredicateString; import static io.appium.java_client.pagefactory_tests.widget.tests.combined.DefaultIosXCUITWidget.XCUIT_SUB_WIDGET_LOCATOR; import static io.appium.java_client.pagefactory_tests.widget.tests.ios.AnnotatedIosWidget.XCUIT_ROOT_WIDGET_LOCATOR; import static io.appium.java_client.pagefactory_tests.widget.tests.ios.IosApp.IOS_XCUIT_WIDGET_LOCATOR; import static io.appium.java_client.pagefactory_tests.widget.tests.ios.IosApp.XCUIT_EXTERNALLY_DEFINED_WIDGET_LOCATOR; -import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; -import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; -import io.appium.java_client.pagefactory_tests.widget.tests.WidgetTest; -import org.junit.jupiter.api.Test; - public class XCUITWidgetTest extends WidgetTest { public XCUITWidgetTest() { diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/AnnotatedWindowsWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/AnnotatedWindowsWidget.java deleted file mode 100644 index 733d0db95..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/AnnotatedWindowsWidget.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.windows; - -import io.appium.java_client.pagefactory.WindowsFindBy; -import io.appium.java_client.pagefactory.iOSXCUITFindBy; -import org.openqa.selenium.WebElement; - -@WindowsFindBy(windowsAutomation = "SOME_ROOT_LOCATOR") -@iOSXCUITFindBy(iOSNsPredicate = "XCUIT_SOME_ROOT_LOCATOR") -public class AnnotatedWindowsWidget extends DefaultWindowsWidget { - public static String WINDOWS_ROOT_WIDGET_LOCATOR = "SOME_ROOT_LOCATOR"; - - protected AnnotatedWindowsWidget(WebElement element) { - super(element); - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/DefaultWindowsWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/DefaultWindowsWidget.java deleted file mode 100644 index ab7b81a41..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/DefaultWindowsWidget.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.windows; - -import io.appium.java_client.pagefactory.WindowsFindBy; -import io.appium.java_client.pagefactory.iOSXCUITFindBy; -import io.appium.java_client.pagefactory_tests.widget.tests.DefaultStubWidget; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public class DefaultWindowsWidget extends DefaultStubWidget { - - public static String WINDOWS_SUB_WIDGET_LOCATOR = "SOME_SUB_LOCATOR"; - - @WindowsFindBy(windowsAutomation = "SOME_SUB_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "XCUIT_SOME_SUB_LOCATOR") - private DefaultWindowsWidget singleWidget; - - @WindowsFindBy(windowsAutomation = "SOME_SUB_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "XCUIT_SOME_SUB_LOCATOR") - private List multipleWidgets; - - protected DefaultWindowsWidget(WebElement element) { - super(element); - } - - @Override - public DefaultWindowsWidget getSubWidget() { - return singleWidget; - } - - @Override - public List getSubWidgets() { - return multipleWidgets; - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/ExtendedWindowsWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/ExtendedWindowsWidget.java deleted file mode 100644 index 14cc95f65..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/ExtendedWindowsWidget.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.windows; - -import org.openqa.selenium.WebElement; - -public class ExtendedWindowsWidget extends AnnotatedWindowsWidget { - protected ExtendedWindowsWidget(WebElement element) { - super(element); - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsApp.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsApp.java deleted file mode 100644 index 07eb5784d..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsApp.java +++ /dev/null @@ -1,125 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.windows; - -import io.appium.java_client.AppiumBy; -import io.appium.java_client.pagefactory.WindowsFindBy; -import io.appium.java_client.pagefactory.iOSXCUITFindBy; -import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; - -import java.util.List; - -public class WindowsApp implements ExtendedApp { - - public static String WINDOWS_DEFAULT_WIDGET_LOCATOR = "SOME_WINDOWS_DEFAULT_LOCATOR"; - - public static String WINDOWS_EXTERNALLY_DEFINED_WIDGET_LOCATOR = "WINDOWS_EXTERNALLY_DEFINED_WIDGET_LOCATOR"; - - @WindowsFindBy(windowsAutomation = "SOME_WINDOWS_DEFAULT_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_DEFAULT_LOCATOR") - private DefaultWindowsWidget singleIosWidget; - - @WindowsFindBy(windowsAutomation = "SOME_WINDOWS_DEFAULT_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_DEFAULT_LOCATOR") - private List multipleIosWidgets; - - /** - * This class is annotated by {@link WindowsFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. - * This field was added to check that locator is created correctly according to current platform. - * It is expected that the root element and sub-elements are found using - * {@link AppiumBy#windowsAutomation(String)} - */ - private AnnotatedWindowsWidget singleAnnotatedIosWidget; - - /** - * This class is annotated by {@link WindowsFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. - * This field was added to check that locator is created correctly according to current platform. - * It is expected that the root element and sub-elements are found using - * {@link AppiumBy#windowsAutomation(String)}. - */ - private List multipleIosIosWidgets; - - /** - * This class is not annotated by {@link WindowsFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. - * But the superclass is annotated by these annotations. This field was added to check that locator is - * created correctly according to current platform. - * It is expected that the root element and sub-elements are found using - * {@link AppiumBy#windowsAutomation(String)}. - */ - private ExtendedWindowsWidget singleExtendedIosWidget; - - /** - * This class is not annotated by {@link WindowsFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. - * But the superclass is annotated by these annotations. This field was added to check that locator is - * created correctly according to current platform. - * It is expected that the root element and sub-elements are found using - * {@link AppiumBy#windowsAutomation(String)}. - */ - private List multipleExtendedIosWidgets; - - /** - * This class is not annotated by {@link WindowsFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. - * But the superclass is annotated by these annotations. This field was added to check that locator is - * created correctly according to current platform. - * It is expected that the root element and sub-elements are found using - * {@link AppiumBy#windowsAutomation(String)}. - */ - @WindowsFindBy(windowsAutomation = "WINDOWS_EXTERNALLY_DEFINED_WIDGET_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_EXTERNALLY_DEFINED_LOCATOR") - private ExtendedWindowsWidget singleOverriddenIosWidget; - - /** - * This class is not annotated by {@link WindowsFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. - * But the superclass is annotated by these annotations. This field was added to check that locator is - * created correctly according to current platform. - * It is expected that the root element and sub-elements are found using - * {@link AppiumBy#windowsAutomation(String)}. - */ - @WindowsFindBy(windowsAutomation = "WINDOWS_EXTERNALLY_DEFINED_WIDGET_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_EXTERNALLY_DEFINED_LOCATOR") - private List multipleOverriddenIosWidgets; - - @Override - public DefaultWindowsWidget getWidget() { - return singleIosWidget; - } - - @Override - public List getWidgets() { - return multipleIosWidgets; - } - - @Override - public DefaultWindowsWidget getAnnotatedWidget() { - return singleAnnotatedIosWidget; - } - - @Override - public List getAnnotatedWidgets() { - return multipleIosIosWidgets; - } - - @Override - public DefaultWindowsWidget getExtendedWidget() { - return singleExtendedIosWidget; - } - - @Override - public List getExtendedWidgets() { - return multipleExtendedIosWidgets; - } - - @Override - public DefaultWindowsWidget getExtendedWidgetWithOverriddenLocators() { - return singleOverriddenIosWidget; - } - - @Override - public List getExtendedWidgetsWithOverriddenLocators() { - return multipleOverriddenIosWidgets; - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsWidgetTest.java deleted file mode 100644 index d5990b7e5..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsWidgetTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.windows; - -import static io.appium.java_client.MobileBy.windowsAutomation; -import static io.appium.java_client.pagefactory_tests.widget.tests.windows.AnnotatedWindowsWidget.WINDOWS_ROOT_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.windows.DefaultWindowsWidget.WINDOWS_SUB_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.windows.WindowsApp.WINDOWS_DEFAULT_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.windows.WindowsApp.WINDOWS_EXTERNALLY_DEFINED_WIDGET_LOCATOR; - -import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; -import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; -import io.appium.java_client.pagefactory_tests.widget.tests.WidgetTest; -import org.junit.jupiter.api.Test; - -public class WindowsWidgetTest extends WidgetTest { - - public WindowsWidgetTest() { - super(new WindowsApp(), new AbstractStubWebDriver.StubWindowsDriver()); - } - - @Test - @Override - public void checkThatWidgetsAreCreatedCorrectly() { - checkThatLocatorsAreCreatedCorrectly(app.getWidget(), app.getWidgets(), - windowsAutomation(WINDOWS_DEFAULT_WIDGET_LOCATOR), windowsAutomation(WINDOWS_SUB_WIDGET_LOCATOR)); - } - - @Test - @Override - public void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getAnnotatedWidget(), - ((ExtendedApp) app).getAnnotatedWidgets(), - windowsAutomation(WINDOWS_ROOT_WIDGET_LOCATOR), windowsAutomation(WINDOWS_SUB_WIDGET_LOCATOR)); - } - - @Test - @Override - public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidget(), - ((ExtendedApp) app).getExtendedWidgets(), - windowsAutomation(WINDOWS_ROOT_WIDGET_LOCATOR), windowsAutomation(WINDOWS_SUB_WIDGET_LOCATOR)); - } - - @Test - @Override - public void checkCaseWhenBothWidgetFieldAndClassHaveDeclaredAnnotations() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidgetWithOverriddenLocators(), - ((ExtendedApp) app).getExtendedWidgetsWithOverriddenLocators(), - windowsAutomation(WINDOWS_EXTERNALLY_DEFINED_WIDGET_LOCATOR), - windowsAutomation(WINDOWS_SUB_WIDGET_LOCATOR)); - } -} 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 new file mode 100644 index 000000000..af0ca78d9 --- /dev/null +++ b/src/test/java/io/appium/java_client/proxy/ProxyHelpersTest.java @@ -0,0 +1,216 @@ +/* + * 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 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; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ProxyHelpersTest { + + public static class FakeIOSDriver extends IOSDriver { + public FakeIOSDriver(URL url, Capabilities caps) { + super(url, 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 + void shouldFireBeforeAndAfterEvents() { + final StringBuilder acc = new StringBuilder(); + MethodCallListener listener = new MethodCallListener() { + @Override + public void beforeCall(Object target, Method method, Object[] args) { + acc.append("beforeCall ").append(method.getName()).append("\n"); + // should be ignored + throw new IllegalStateException(); + } + + @Override + public void afterCall(Object target, Method method, Object[] args, Object result) { + acc.append("afterCall ").append(method.getName()).append("\n"); + // should be ignored + throw new IllegalStateException(); + } + }; + RemoteWebDriver driver = createProxy(RemoteWebDriver.class, Collections.singletonList(listener)); + + assertThrows( + UnreachableBrowserException.class, + () -> driver.get("http://example.com/") + ); + + assertThat(acc.toString().trim(), is(equalTo( + String.join("\n", + "beforeCall get", + "beforeCall getSessionId", + "afterCall getSessionId", + "beforeCall getCapabilities", + "afterCall getCapabilities", + "beforeCall getCapabilities", + "afterCall getCapabilities") + ))); + } + + @Test + void shouldFireErrorEvents() { + MethodCallListener listener = new MethodCallListener() { + @Override + public Object onError(Object obj, Method method, Object[] args, Throwable e) { + throw new IllegalStateException(); + } + }; + RemoteWebDriver driver = createProxy(RemoteWebDriver.class, Collections.singletonList(listener)); + assertThrows( + IllegalStateException.class, + () -> driver.get("http://example.com/") + ); + } + + @Test + void shouldFireCallEvents() throws MalformedURLException { + final StringBuilder acc = new StringBuilder(); + MethodCallListener listener = new MethodCallListener() { + @Override + public Object call(Object obj, Method method, Object[] args, Callable original) { + acc.append("onCall ").append(method.getName()).append("\n"); + throw new IllegalStateException(); + } + + @Override + public Object onError(Object obj, Method method, Object[] args, Throwable e) throws Throwable { + acc.append("onError ").append(method.getName()).append("\n"); + throw e; + } + }; + FakeIOSDriver driver = createProxy( + FakeIOSDriver.class, + new Object[] {new URL("http://localhost:4723/"), new XCUITestOptions()}, + new Class[] {URL.class, Capabilities.class}, + listener + ); + + assertThrows( + IllegalStateException.class, + () -> driver.get("http://example.com/") + ); + + assertThat(acc.toString().trim(), is(equalTo( + String.join("\n", + "onCall get", + "onError get") + ))); + } + + + @Test + void shouldFireEventsForAllWebDriverCommands() throws MalformedURLException { + final StringBuilder acc = new StringBuilder(); + + var remoteWebElementListener = new ElementAwareWebDriverListener() { + @Override + public void beforeCall(Object target, Method method, Object[] args) { + acc.append("beforeCall ").append(method.getName()).append("\n"); + } + }; + + FakeIOSDriver driver = createProxy( + FakeIOSDriver.class, + new Object[] {new URL("http://localhost:4723/"), new XCUITestOptions()}, + new Class[] {URL.class, Capabilities.class}, + remoteWebElementListener + ); + + WebElement element = driver.findElement(By.id("button")); + + assertThrows( + NoSuchSessionException.class, + element::click + ); + + List elements = driver.findElements(By.id("button")); + + assertThrows( + NoSuchSessionException.class, + () -> elements.get(1).isSelected() + ); + + assertThat(acc.toString().trim(), is(equalTo( + String.join("\n", + "beforeCall findElement", + "beforeCall click", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities", + "beforeCall findElements", + "beforeCall isSelected", + "beforeCall getSessionId", + "beforeCall getCapabilities", + "beforeCall getCapabilities" + ) + ))); + } +} diff --git a/src/test/java/io/appium/java_client/remote/AppiumCommandExecutorTest.java b/src/test/java/io/appium/java_client/remote/AppiumCommandExecutorTest.java new file mode 100644 index 000000000..38b1b6459 --- /dev/null +++ b/src/test/java/io/appium/java_client/remote/AppiumCommandExecutorTest.java @@ -0,0 +1,41 @@ +package io.appium.java_client.remote; + +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.MobileCommand; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class AppiumCommandExecutorTest { + private static final String APPIUM_URL = "https://appium.example.com"; + + private AppiumCommandExecutor createExecutor() { + URL baseUrl; + try { + baseUrl = new URL(APPIUM_URL); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + AppiumClientConfig clientConfig = AppiumClientConfig.defaultConfig().baseUrl(baseUrl); + return new AppiumCommandExecutor(MobileCommand.commandRepository, clientConfig); + } + + @Test + void getAdditionalCommands() { + assertNotNull(createExecutor().getAdditionalCommands()); + } + + @Test + void getHttpClientFactory() { + assertNotNull(createExecutor().getHttpClientFactory()); + } + + @Test + void overrideServerUrl() { + assertDoesNotThrow(() -> createExecutor().overrideServerUrl(new URL("https://direct.example.com"))); + } +} diff --git a/src/test/java/io/appium/java_client/remote/MobileOptionsTest.java b/src/test/java/io/appium/java_client/remote/MobileOptionsTest.java deleted file mode 100644 index 13faeb583..000000000 --- a/src/test/java/io/appium/java_client/remote/MobileOptionsTest.java +++ /dev/null @@ -1,110 +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.remote; - -import org.junit.jupiter.api.Test; -import org.openqa.selenium.MutableCapabilities; -import org.openqa.selenium.ScreenOrientation; - -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Duration; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class MobileOptionsTest { - private MobileOptions mobileOptions = new MobileOptions<>(); - - @Test - public void acceptsExistingCapabilities() { - MutableCapabilities capabilities = new MutableCapabilities(); - capabilities.setCapability("deviceName", "Pixel"); - capabilities.setCapability("platformVersion", "10"); - capabilities.setCapability("newCommandTimeout", 60); - - mobileOptions = new MobileOptions<>(capabilities); - - assertEquals("Pixel", mobileOptions.getDeviceName()); - assertEquals("10", mobileOptions.getPlatformVersion()); - assertEquals(Duration.ofSeconds(60), mobileOptions.getNewCommandTimeout()); - } - - @Test - public void acceptsMobileCapabilities() throws MalformedURLException { - mobileOptions.setApp(new URL("http://example.com/myapp.apk")) - .setAutomationName(AutomationName.ANDROID_UIAUTOMATOR2) - .setPlatformVersion("10") - .setDeviceName("Pixel") - .setOtherApps("/path/to/app.apk") - .setLocale("fr_CA") - .setUdid("1ae203187fc012g") - .setOrientation(ScreenOrientation.LANDSCAPE) - .setNewCommandTimeout(Duration.ofSeconds(60)) - .setLanguage("fr"); - - assertEquals("http://example.com/myapp.apk", mobileOptions.getApp()); - assertEquals(AutomationName.ANDROID_UIAUTOMATOR2, mobileOptions.getAutomationName()); - assertEquals("10", mobileOptions.getPlatformVersion()); - assertEquals("Pixel", mobileOptions.getDeviceName()); - assertEquals("/path/to/app.apk", mobileOptions.getOtherApps()); - assertEquals("fr_CA", mobileOptions.getLocale()); - assertEquals("1ae203187fc012g", mobileOptions.getUdid()); - assertEquals(ScreenOrientation.LANDSCAPE, mobileOptions.getOrientation()); - assertEquals(Duration.ofSeconds(60), mobileOptions.getNewCommandTimeout()); - assertEquals("fr", mobileOptions.getLanguage()); - } - - @Test - public void acceptsMobileBooleanCapabilityDefaults() { - mobileOptions.setClearSystemFiles() - .setAutoWebview() - .setEnablePerformanceLogging() - .setEventTimings() - .setAutoWebview() - .setFullReset() - .setPrintPageSourceOnFindFailure(); - - assertTrue(mobileOptions.doesClearSystemFiles()); - assertTrue(mobileOptions.doesAutoWebview()); - assertTrue(mobileOptions.isEnablePerformanceLogging()); - assertTrue(mobileOptions.doesEventTimings()); - assertTrue(mobileOptions.doesAutoWebview()); - assertTrue(mobileOptions.doesFullReset()); - assertTrue(mobileOptions.doesPrintPageSourceOnFindFailure()); - } - - @Test - public void setsMobileBooleanCapabilities() { - mobileOptions.setClearSystemFiles(false) - .setAutoWebview(false) - .setEnablePerformanceLogging(false) - .setEventTimings(false) - .setAutoWebview(false) - .setFullReset(false) - .setPrintPageSourceOnFindFailure(false); - - assertFalse(mobileOptions.doesClearSystemFiles()); - assertFalse(mobileOptions.doesAutoWebview()); - assertFalse(mobileOptions.isEnablePerformanceLogging()); - assertFalse(mobileOptions.doesEventTimings()); - assertFalse(mobileOptions.doesAutoWebview()); - assertFalse(mobileOptions.doesFullReset()); - assertFalse(mobileOptions.doesPrintPageSourceOnFindFailure()); - } -} 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/touch/DummyElement.java b/src/test/java/io/appium/java_client/touch/DummyElement.java deleted file mode 100644 index 62e1fbb12..000000000 --- a/src/test/java/io/appium/java_client/touch/DummyElement.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.appium.java_client.touch; - -import org.openqa.selenium.By; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.Point; -import org.openqa.selenium.Rectangle; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.RemoteWebElement; - -import java.util.List; - -public class DummyElement extends RemoteWebElement { - @Override - public void click() { - // dummy - } - - @Override - public void submit() { - // dummy - } - - @Override - public void sendKeys(CharSequence... charSequences) { - // dummy - } - - @Override - public void clear() { - // dummy - } - - @Override - public String getTagName() { - return ""; - } - - @Override - public String getAttribute(String s) { - return ""; - } - - @Override - public boolean isSelected() { - return false; - } - - @Override - public boolean isEnabled() { - return false; - } - - @Override - public String getText() { - return ""; - } - - @Override - public List findElements(By by) { - return null; - } - - @Override - public WebElement findElement(By by) { - return null; - } - - @Override - public boolean isDisplayed() { - return false; - } - - @Override - public Point getLocation() { - return null; - } - - @Override - public Dimension getSize() { - return null; - } - - @Override - public Rectangle getRect() { - return null; - } - - @Override - public String getCssValue(String s) { - return ""; - } - - @Override - public X getScreenshotAs(OutputType outputType) { - return null; - } - - @Override - public String getId() { - return "123"; - } -} diff --git a/src/test/java/io/appium/java_client/touch/FailsWithMatcher.java b/src/test/java/io/appium/java_client/touch/FailsWithMatcher.java deleted file mode 100644 index f4ac4bec8..000000000 --- a/src/test/java/io/appium/java_client/touch/FailsWithMatcher.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.appium.java_client.touch; - -import static org.hamcrest.core.AllOf.allOf; -import static org.hamcrest.core.IsInstanceOf.instanceOf; - -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; - -public final class FailsWithMatcher extends TypeSafeMatcher { - - private final Matcher matcher; - - private FailsWithMatcher(final Matcher matcher) { - this.matcher = matcher; - } - - public static Matcher failsWith( - final Class throwableType) { - return new FailsWithMatcher<>(instanceOf(throwableType)); - } - - public static Matcher failsWith( - final Class throwableType, final Matcher throwableMatcher) { - return new FailsWithMatcher(allOf(instanceOf(throwableType), throwableMatcher)); - } - - @Override - protected boolean matchesSafely(final Runnable runnable) { - try { - runnable.run(); - return false; - } catch (final Throwable ex) { - return matcher.matches(ex); - } - } - - @Override - public void describeTo(final Description description) { - description.appendText("fails with ").appendDescriptionOf(matcher); - } - -} diff --git a/src/test/java/io/appium/java_client/touch/TouchOptionsTests.java b/src/test/java/io/appium/java_client/touch/TouchOptionsTests.java deleted file mode 100644 index b08249962..000000000 --- a/src/test/java/io/appium/java_client/touch/TouchOptionsTests.java +++ /dev/null @@ -1,96 +0,0 @@ -package io.appium.java_client.touch; - -import static io.appium.java_client.touch.FailsWithMatcher.failsWith; -import static io.appium.java_client.touch.LongPressOptions.longPressOptions; -import static io.appium.java_client.touch.TapOptions.tapOptions; -import static io.appium.java_client.touch.WaitOptions.waitOptions; -import static io.appium.java_client.touch.offset.ElementOption.element; -import static io.appium.java_client.touch.offset.PointOption.point; -import static java.time.Duration.ofMillis; -import static java.time.Duration.ofSeconds; -import static org.hamcrest.CoreMatchers.everyItem; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.in; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import io.appium.java_client.touch.offset.ElementOption; -import io.appium.java_client.touch.offset.PointOption; -import org.junit.jupiter.api.Test; -import org.openqa.selenium.Point; -import org.openqa.selenium.remote.RemoteWebElement; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class TouchOptionsTests { - private static final RemoteWebElement DUMMY_ELEMENT = new DummyElement(); - - @Test - public void invalidEmptyPointOptionsShouldFailOnBuild() { - assertThrows(IllegalArgumentException.class, - () -> new PointOption<>().build()); - } - - @Test - public void invalidEmptyElementOptionsShouldFailOnBuild() { - assertThrows(IllegalArgumentException.class, - () -> new ElementOption().build()); - } - - @Test - public void invalidOptionsArgumentsShouldFailOnAltering() { - final List invalidOptions = new ArrayList<>(); - invalidOptions.add(() -> waitOptions(ofMillis(-1))); - invalidOptions.add(() -> new ElementOption().withCoordinates(new Point(0, 0)).withElement(null)); - invalidOptions.add(() -> new WaitOptions().withDuration(null)); - invalidOptions.add(() -> tapOptions().withTapsCount(-1)); - invalidOptions.add(() -> longPressOptions().withDuration(null)); - invalidOptions.add(() -> longPressOptions().withDuration(ofMillis(-1))); - for (Runnable item : invalidOptions) { - assertThat(item, failsWith(RuntimeException.class)); - } - } - - @Test - public void longPressOptionsShouldBuildProperly() { - final Map actualOpts = longPressOptions() - .withElement(element(DUMMY_ELEMENT).withCoordinates(0, 0)) - .withDuration(ofMillis(1)) - .build(); - final Map expectedOpts = new HashMap<>(); - expectedOpts.put("element", DUMMY_ELEMENT.getId()); - expectedOpts.put("x", 0); - expectedOpts.put("y", 0); - expectedOpts.put("duration", 1L); - assertThat(actualOpts.entrySet(), everyItem(is(in(expectedOpts.entrySet())))); - assertThat(expectedOpts.entrySet(), everyItem(is(in(actualOpts.entrySet())))); - } - - @Test - public void tapOptionsShouldBuildProperly() { - final Map actualOpts = tapOptions() - .withPosition(point(new Point(0, 0))) - .withTapsCount(2) - .build(); - final Map expectedOpts = new HashMap<>(); - expectedOpts.put("x", 0); - expectedOpts.put("y", 0); - expectedOpts.put("count", 2); - assertThat(actualOpts.entrySet(), everyItem(is(in(expectedOpts.entrySet())))); - assertThat(expectedOpts.entrySet(), everyItem(is(in(actualOpts.entrySet())))); - } - - @Test - public void waitOptionsShouldBuildProperly() { - final Map actualOpts = new WaitOptions() - .withDuration(ofSeconds(1)) - .build(); - final Map expectedOpts = new HashMap<>(); - expectedOpts.put("ms", 1000L); - assertThat(actualOpts.entrySet(), everyItem(is(in(expectedOpts.entrySet())))); - assertThat(expectedOpts.entrySet(), everyItem(is(in(actualOpts.entrySet())))); - } -} 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 63% rename from src/test/java/io/appium/java_client/TestUtils.java rename to src/test/java/io/appium/java_client/utils/TestUtils.java index f2ed2792e..8aa90892c 100644 --- a/src/test/java/io/appium/java_client/TestUtils.java +++ b/src/test/java/io/appium/java_client/utils/TestUtils.java @@ -1,24 +1,47 @@ -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.io.IOException; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; +import java.net.URISyntaxException; import java.net.URL; import java.net.UnknownHostException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; 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() { + } + public static String getLocalIp4Address() throws SocketException, UnknownHostException { // https://stackoverflow.com/questions/9481865/getting-the-ip-address-of-the-current-machine-using-java try (final DatagramSocket socket = new DatagramSocket()) { @@ -27,19 +50,15 @@ public static String getLocalIp4Address() throws SocketException, UnknownHostExc } } - public static Path resourcePathToLocalPath(String resourcePath) { + public static Path resourcePathToAbsolutePath(String resourcePath) { URL url = ClassLoader.getSystemResource(resourcePath); if (url == null) { throw new IllegalArgumentException(String.format("Cannot find the '%s' resource", resourcePath)); } - return Paths.get(url.getPath()); - } - - public static String resourceAsString(String resourcePath) { try { - return new String(Files.readAllBytes(resourcePathToLocalPath(resourcePath))); - } catch (IOException e) { - throw new RuntimeException(e); + return Paths.get(url.toURI()).toAbsolutePath(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); } } @@ -80,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; + } } diff --git a/src/test/resources/META-INF/services/io.appium.java_client.events.api.Listener b/src/test/resources/META-INF/services/io.appium.java_client.events.api.Listener deleted file mode 100644 index 6faceb0d0..000000000 --- a/src/test/resources/META-INF/services/io.appium.java_client.events.api.Listener +++ /dev/null @@ -1,10 +0,0 @@ -io.appium.java_client.events.listeners.AlertListener -io.appium.java_client.events.listeners.RotationListener -io.appium.java_client.events.listeners.WindowListener -io.appium.java_client.events.listeners.ContextListener -io.appium.java_client.events.listeners.ElementListener -io.appium.java_client.events.listeners.ExceptionListener -io.appium.java_client.events.listeners.JavaScriptListener -io.appium.java_client.events.listeners.NavigationListener -io.appium.java_client.events.listeners.SearchingListener -io.appium.java_client.events.listeners.AppiumListener diff --git a/src/test/resources/apps/ApiDemos-debug.apk b/src/test/resources/apps/ApiDemos-debug.apk deleted file mode 100644 index 62a1fd607..000000000 Binary files a/src/test/resources/apps/ApiDemos-debug.apk and /dev/null differ diff --git a/src/test/resources/apps/IntentExample.apk b/src/test/resources/apps/IntentExample.apk deleted file mode 100644 index 196ea9094..000000000 Binary files a/src/test/resources/apps/IntentExample.apk and /dev/null differ diff --git a/src/test/resources/apps/TestApp.app.zip b/src/test/resources/apps/TestApp.app.zip deleted file mode 100644 index 9bce35781..000000000 Binary files a/src/test/resources/apps/TestApp.app.zip and /dev/null differ diff --git a/src/test/resources/apps/UICatalog.app.zip b/src/test/resources/apps/UICatalog.app.zip deleted file mode 100644 index e483caad5..000000000 Binary files a/src/test/resources/apps/UICatalog.app.zip and /dev/null differ diff --git a/src/test/resources/apps/vodqa.zip b/src/test/resources/apps/vodqa.zip deleted file mode 100644 index 74b980dec..000000000 Binary files a/src/test/resources/apps/vodqa.zip and /dev/null differ