diff --git a/.dependabot/config.yml b/.dependabot/config.yml deleted file mode 100644 index 7abbcde03..000000000 --- a/.dependabot/config.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 1 -update_configs: - - package_manager: "java:gradle" - directory: "/" - update_schedule: "weekly" - 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/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..c082a4d00 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: +- package-ecosystem: gradle + directory: "/" + schedule: + interval: weekly + time: "11:00" + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: "11:00" + open-pull-requests-limit: 10 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 405a2b306..000000000 --- a/.github/workflows/gradle-wrapper-validation.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Validate Gradle Wrapper" -on: [push, pull_request] - -jobs: - validation: - name: "Validation" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml deleted file mode 100644 index 04241b76f..000000000 --- a/.github/workflows/gradle.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Appium Java Client CI - -on: [push, pull_request] - -jobs: - build: - - runs-on: macOS-latest - - steps: - - uses: actions/checkout@v1 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: Build with Gradle - run: ./gradlew clean build -x signMavenJavaPublication -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 15900ab6e..4d235791a 100644 --- a/README.md +++ b/README.md @@ -1,29 +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) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/f365c5e9458b42bf8a5b1d928d7e4f48)](https://www.codacy.com/app/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. -## How to install latest java client Beta/Snapshots +## v7 to v8 Migration -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) +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. -### Maven +## Add Appium java client to your test framework - - Add the following to pom.xml: +### Stable + +#### Maven + +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 @@ -34,662 +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 -*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 f2d020e67..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,60 +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-10.15' - -variables: - ANDROID_EMU_NAME: test - ANDROID_EMU_ABI: x86 - ANDROID_EMU_TARGET: android-27 - ANDROID_EMU_TAG: google_apis - XCODE_VERSION: 11.5 - IOS_PLATFORM_VERSION: 13.5 - IOS_DEVICE_NAME: iPhone X - -jobs: -- job: E2E_Tests - timeoutInMinutes: '60' - steps: - - task: NodeTool@0 - inputs: - versionSpec: '12.x' - - - script: | - echo "Configuring Environment" - 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 > /dev/null 2>&1 & - $ANDROID_HOME/platform-tools/adb wait-for-device - while [[ $? -ne 0 ]]; do sleep 1; $ANDROID_HOME/platform-tools/adb shell pm list packages; done; - $ANDROID_HOME/platform-tools/adb devices - echo "Emulator started" - - sudo xcode-select -s /Applications/Xcode_$(XCODE_VERSION).app/Contents/Developer - xcrun simctl list - - npm config delete prefix - npm config set prefix $NVM_DIR/versions/node/`node --version` - node --version - - npm install -g appium@beta - appium --version - - java -version - - - task: Gradle@2 - inputs: - gradleWrapperFile: 'gradlew' - gradleOptions: '-Xmx3072m' - javaHomeOption: 'JDKVersion' - jdkVersionOption: '1.8' - jdkArchitectureOption: 'x64' - publishJUnitResults: true - tasks: 'build' - options: 'xcuiTest uiAutomationTest -x checkstyleTest -x test -x signMavenJavaPublication' diff --git a/build.gradle b/build.gradle index a26153854..60340fbfe 100644 --- a/build.gradle +++ b/build.gradle @@ -1,127 +1,109 @@ -apply plugin: 'java' -apply plugin: 'idea' -apply plugin: 'maven-publish' -apply plugin: 'eclipse' -apply plugin: 'jacoco' -apply plugin: 'checkstyle' -apply plugin: 'signing' - import org.apache.tools.ant.filters.* -repositories { - jcenter() - mavenCentral() +plugins { + id 'java-library' + id 'idea' + id 'eclipse' + id 'maven-publish' + id 'jacoco' + id 'signing' + id 'org.owasp.dependencycheck' version '12.2.0' + id 'com.gradleup.shadow' version '9.3.1' + id 'org.jreleaser' version '1.21.0' } -buildscript { - repositories { - jcenter() - mavenCentral() - } - dependencies { - classpath 'org.owasp:dependency-check-gradle:5.3.2.1' - classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0' - } +ext { + seleniumVersion = project.property('selenium.version') + appiumClientVersion = project.property('appiumClient.version') + slf4jVersion = '2.0.17' } -apply plugin: 'org.owasp.dependencycheck' -apply plugin: 'com.github.johnrengelman.shadow' +group = 'io.appium' +version = appiumClientVersion -configurations { - ecj - lombok -} +repositories { + mavenCentral() -dependencies { - ecj 'org.eclipse.jdt:ecj:3.21.0' - lombok 'org.projectlombok:lombok:1.18.12' + 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() } -compileJava { - def ecjJar = configurations.ecj.singleFile - def lombokjar = configurations.lombok.singleFile - - options.fork = true - options.fork executable: 'java', jvmArgs: [ '-javaagent:'+lombokjar.path+'=ECJ', '-jar', ecjJar.path, '-cp', lombokjar.path] - options.define compilerArgs: [ - '-encoding', 'utf-8', - // https://www.ibm.com/support/knowledgecenter/SS8PJ7_9.7.0/org.eclipse.jdt.doc.user/tasks/task-using_batch_compiler.htm - '-warn:-unused,-unchecked,-raw,-serial,-suppress', - ] -} - dependencies { - compileOnly('org.projectlombok:lombok:1.18.12') - annotationProcessor('org.projectlombok:lombok:1.18.12') - compile ("org.seleniumhq.selenium:selenium-java:${project.property('selenium.version')}") { - force = true + compileOnly 'org.projectlombok:lombok:1.18.42' + annotationProcessor 'org.projectlombok:lombok:1.18.42' - exclude group: 'com.google.code.gson' - exclude module: 'htmlunit-driver' - exclude group: 'net.sourceforge.htmlunit' - - } - compile ("org.seleniumhq.selenium:selenium-support:${project.property('selenium.version')}") { - force = true - } - compile ("org.seleniumhq.selenium:selenium-api:${project.property('selenium.version')}") { - force = true - } - compile 'com.google.code.gson:gson:2.8.6' - compile 'org.apache.httpcomponents:httpclient:4.5.12' - compile 'cglib:cglib:3.3.0' - compile 'commons-validator:commons-validator:1.6' - compile 'org.apache.commons:commons-lang3:3.10' - compile 'commons-io:commons-io:2.7' - compile 'org.springframework:spring-context:5.2.7.RELEASE' - compile 'org.aspectj:aspectjweaver:1.9.5' - compile 'org.slf4j:slf4j-api:1.7.30' - - testCompile 'junit:junit:4.13' - testCompile 'org.hamcrest:hamcrest:2.2' - testCompile (group: 'io.github.bonigarcia', name: 'webdrivermanager', version: '4.0.0') { - exclude group: 'org.seleniumhq.selenium' + 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}" + } + } + api('org.seleniumhq.selenium:selenium-remote-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.enabled true - html.destination file("${buildDir}/Reports/jacoco") + html.required = true + 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') @@ -132,10 +114,8 @@ publishing { mavenJava(MavenPublication) { groupId = 'io.appium' artifactId = 'java-client' - version = '7.3.0' + version = appiumClientVersion from components.java - artifact sourcesJar - artifact javadocJar pom { name = 'java-client' description = 'Java client for Appium Mobile Webdriver' @@ -164,6 +144,11 @@ publishing { url = 'https://github.com/mykola-mokhnach' id = 'mykola-mokhnach' } + developer { + name = 'Valery Yatsynovich' + url = 'https://github.com/valfirst' + id = 'valfirst' + } } licenses { license { @@ -183,58 +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 = '5.4' + gradleVersion = '9.1.0' distributionType = Wrapper.DistributionType.ALL } processResources { filter ReplaceTokens, tokens: [ - 'selenium.version': project.property('selenium.version') + 'selenium.version' : seleniumVersion, + 'appiumClient.version': appiumClientVersion ] } -task xcuiTest( type: Test ) { - useJUnit() - testLogging.showStandardStreams = true - testLogging.exceptionFormat = 'full' - filter { - includeTestsMatching '*.appium.element.generation.ios.*' - includeTestsMatching '*.appium.AppiumFluentWaitTest' - includeTestsMatching 'io.appium.java_client.ios.*' - includeTestsMatching '*.pagefactory_tests.XCUITModeTest' - includeTestsMatching '*.pagefactory_tests.widget.tests.combined.*' - includeTestsMatching '*.pagefactory_tests.widget.tests.ios.*' - includeTestsMatching '*.StartingAppLocallyTest.startingIOSAppWithCapabilitiesAndServiceTest' - includeTestsMatching '*.StartingAppLocallyTest.startingIOSAppWithCapabilitiesAndFlagsOnServerSideTest' - exclude '**/UIAutomationTest.class' - 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' + } + } + } + } + + 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') + } + + targets.configureEach { + testTask.configure { + shouldRunAfter(test) + filter { + exclude '**/IOSScreenRecordTest.class' + exclude '**/ImagesComparisonTest.class' + exclude '**/IOSNativeWebTapSettingTest.class' + } + } + } + } -task uiAutomationTest( type: Test ) { - useJUnit() - testLogging.showStandardStreams = true - testLogging.exceptionFormat = 'full' - filter { - includeTestsMatching '*.SettingTest' - includeTestsMatching 'io.appium.java_client.android.ClipboardTest' - includeTestsMatching '*.AndroidAppStringsTest' + 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 a354af7f1..000000000 --- a/docs/Installing-the-project.md +++ /dev/null @@ -1,83 +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 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/Note-for-developers.md b/docs/Note-for-developers.md index 586df8f5e..d6182196d 100644 --- a/docs/Note-for-developers.md +++ b/docs/Note-for-developers.md @@ -12,28 +12,7 @@ This is the Gradle project. Be sure that: -- The `JAVA_HOME` environmental contains a path to JDK > 7 - -- If non built-in gradle distribution is used then its version should be > 2.1 - -## Compiler - -This project is compiled in some not common way. We use `ecj` Eclipse Java Compiler. Below is the sample how to define this compiler by IDE. -![eclipse compiler](https://cloud.githubusercontent.com/assets/4927589/14228367/6fce184e-f91b-11e5-837c-2673446d24ea.png) - -## JDK - -Please check following settings: - -![](https://cloud.githubusercontent.com/assets/4927589/18324490/7ffd3ba4-7545-11e6-9f22-eb028737283c.png) - -![](https://cloud.githubusercontent.com/assets/4927589/18324593/f5254e3a-7545-11e6-85c5-e4c491ee268d.png) - -![](https://cloud.githubusercontent.com/assets/4927589/18324648/3f4635a6-7546-11e6-966c-2949059968ac.png) - -![](https://cloud.githubusercontent.com/assets/4927589/18324760/cbca4aee-7546-11e6-8cfb-e86d8018be6a.png) - -![](https://cloud.githubusercontent.com/assets/4927589/18324835/2e3bfc04-7547-11e6-8f5e-981aea8f1771.png) +- The `JAVA_HOME` environmental contains a path to JDK 1.8+ ## Coding Standards 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 1dcbc1024..ff77c1247 100644 --- a/docs/The-event_firing.md +++ b/docs/The-event_firing.md @@ -1,151 +1,213 @@ -since 4.1.0 +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 -frameworks. - - -# The API +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 +frameworks. The feature has been introduced first since Selenium API v4. -The API was designed the way which allows end user to select events (searching, navigation, exception throwing etc.) which should be listened to. It contains -the following list of interfaces (new items may be added further): +# The API -- `io.appium.java_client.events.api.Listener` is the basic interface -- `io.appium.java_client.events.api.general.AlertEventListener` is for the listening to alerts -- `io.appium.java_client.events.api.general.ElementEventListener` is for the listening to actions related to elements -- `io.appium.java_client.events.api.general.JavaScriptEventListener` is for the listening to java script executing -- `io.appium.java_client.events.api.general.ListensToException` is for the listening to exceptions which are thrown -- `io.appium.java_client.events.api.general.NavigationEventListener` is for the listening to events related to navigation -- `io.appium.java_client.events.api.general.SearchingEventListener` is for the listening to events related to the searching. -- `io.appium.java_client.events.api.general.WindowEventListener` is for the listening to actions on a window -- `io.appium.java_client.events.api.mobile.ContextEventListener` is for the listening to the switching to mobile context -- `io.appium.java_client.events.api.mobile.RotationEventListener` is for the listening to screen rotation -- `io.appium.java_client.events.api.general.AppiumWebDriverEventListener` was added to provide the compatibility with -user's implementation of `org.openqa.selenium.support.events.WebDriverEventListener`. Also it extends some interfaces above. - -# Briefly about the engine. +There are two main entities used to implement events firing logic: +- [org.openqa.selenium.support.events.EventFiringDecorator](https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/support/events/EventFiringDecorator.java) class +- [org.openqa.selenium.support.events.WebDriverListener](https://github.com/SeleniumHQ/selenium/blob/trunk/java/src/org/openqa/selenium/support/events/WebDriverListener.java) interface -This is pretty similar solution as the `org.openqa.selenium.support.events.EventFiringWebDriver` of the Selenium project. You -can read about this thing there [The blog post](https://seleniumworks.blogspot.com/2014/02/eventfiringwebdriver.html). +## WebDriverListener -Here we were trying to improve existing drawbacks and restrictions using: +Classes that implement this interface are intended to be used with EventFiringDecorator. +This interface provides empty default implementation for all methods that do nothing. +You could easily extend that interface to add more methods that you'd like to listen to. +The strategy to add new/custom event listeners is the following. Let say there is a public `setOrientation` +method in the target WebDriver instance. Then you'd have to add `beforeSetOrientation` and/or +`afterSetOrientation` methods to your WebDriverListener descendant accepting single argument +of `WebDriver` type. If the target method accepts one or more arguments then these arguments +should also be added to the event listeners in the same order they are accepted by the original method, +but the very first argument should still be the firing WebDriver instance. -- API splitting, see above. +_Important_: Make sure that your implementation of WebDriverListener class is public +and that event listener methods are also public. -- the binding of some [Spring framework engines](https://spring.io/projects/spring-framework) with [AspectJ](https://en.wikipedia.org/wiki/AspectJ). +## EventFiringDecorator -# How to use +This decorator creates a wrapper around an arbitrary WebDriver instance that notifies +registered listeners about events happening in this WebDriver and derived objects, +such as WebElements and Alert. +Listeners should implement WebDriverListener. It supports three types of events: +- "before"-event: a method is about to be called; +- "after"-event: a method was called successfully and returned some result; +- "error"-event: a method was called and thrown an exception. -It is easy. +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 -import io.appium.java_client.events.api.general.AlertEventListener; - -public class AlertListener implements AlertEventListener { -... -} - -... -import io.appium.java_client.events.api.general.ElementEventListener; - -public class ElementListener implements ElementEventListener { -... -} - -//and so on -... -import io.appium.java_client.events.EventFiringWebDriverFactory; -import io.appium.java_client.events.api.Listener; -... - -AndroidDriver driver = new AndroidDriver(parameters); -driver = EventFiringWebDriverFactory.getEventFiringWebDriver(driver, new AlertListener(), - new ElementListener()); - -//or -AndroidDriver driver2 = new AndroidDriver(parameters); -List listeners = new ArrayList<>(); -listeners.add(new AlertListener()); -listeners.add(new ElementListener()); -driver = EventFiringWebDriverFactory.getEventFiringWebDriver(driver2, listeners); +WebDriver original = new AndroidDriver(); +// it is expected that MyListener class implements WebDriverListener +// interface or its descendant +WebDriverListener listener = new MyListener(); +WebDriver decorated = new EventFiringDecorator(listener).decorate(original); +// the next call is going to fire: +// - beforeAnyCall +// - beforeAnyWebDriverCall +// - beforeGet +// - afterGet +// - afterAnyWebDriverCall +// - afterAnyCall +// events in the listener instence (in this order) +decorated.get("http://example.com/"); +// the next call is going to fire: +// - beforeAnyCall +// - beforeAnyWebDriverCall +// - beforeFindElement +// - afterFindElement +// - afterAnyWebDriverCall +// - afterAnyCall +// events in the listener instence (in this order) +WebElement header = decorated.findElement(By.tagName("h1")); +// if an error happens during any of these calls the the onError event is fired ``` -## What if there are listeners which used everywhere by default. - -In order to avoid the repeating actions an end user is free to do these things: - -- create folders `/META-INF/services` and put the file `io.appium.java_client.events.api.Listener` there. Please read about -[SPI](https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html). - -![image](https://cloud.githubusercontent.com/assets/4927589/16731325/24eab680-4780-11e6-8551-a3c72d4b9c38.png) +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 +method called in a class or in any class. To subscribe to a "specific" event a listener should +implement a method with a name derived from the target method to be watched. The listener methods +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. -- define the list of default listeners at the `io.appium.java_client.events.api.Listener` +## createProxy API (since Java Client 8.3.0) -![image](https://cloud.githubusercontent.com/assets/4927589/16731509/2734a4e0-4781-11e6-81cb-ab64a5924c35.png) - -And then it is enough +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 - -//and so on -... -import io.appium.java_client.events.EventFiringWebDriverFactory; -... - -AndroidDriver driver = new AndroidDriver(parameters); -driver = EventFiringWebDriverFactory.getEventFiringWebDriver(driver); +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); ``` -If there are listeners defined externally when this collection is merged with default set of listeners. +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: -# How to reuse customized WebDriverEventListener +```java +import io.appium.java_client.proxy.MethodCallListener; +import io.appium.java_client.proxy.NotImplementedException; -If an end user has their own `org.openqa.selenium.support.events.WebDriverEventListener` implementation then in order to -make it compatible with this engine it is enough to do the following. +import static io.appium.java_client.proxy.Helpers.createProxy; +// ... -```java -import org.openqa.selenium.support.events.WebDriverEventListener; -import io.appium.java_client.events.api.general.AppiumWebDriverEventListener; +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"); + } -public class UsersWebDriverEventListener implements WebDriverEventListener, AppiumWebDriverEventListener { -... -} + @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" + ) +); ``` -or just +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. -```java -import io.appium.java_client.events.api.general.AppiumWebDriverEventListener; +#### ElementAwareWebDriverListener -public class UsersWebDriverEventListener implements AppiumWebDriverEventListener { -... -} -``` -# Also +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) -As soon as Appium java client has *Java 8-style* API (methods with default implementation) there was provided the ability to get objects created by these interfaces (anonymous types) listenable. Also there is an option to make some objects (some single element that has been found, for example) listenable too. +- Any elements returned by the driver (e.g., click, isSelected on a WebElement) ```java -import static io.appium.java_client.events.EventFiringObjectFactory.getEventFiringObject; -... +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; -AppiumDriver appiumDriver = new AppiumDriver(parameters); -FindsByAndroidUIAutomator findsByAndroidUIAutomator = - new FindsByAndroidUIAutomator() { - @Override - public AndroidElement findElement(String by, String using) { - return appiumDriver.findElement(String by, String using); - } +// ... + +final StringBuilder acc = new StringBuilder(); +var listener = new ElementAwareWebDriverListener() { @Override - public List findElements(String by, String using) { - return appiumDriver.findElements(by, using); + public void beforeCall(Object target, Method method, Object[] args) { + acc.append("beforeCall ").append(method.getName()).append("\n"); } }; -findsByAndroidUIAutomator = - getEventFiringObject(findsByAndroidUIAutomator, appiumDriver, listeners); +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 new file mode 100644 index 000000000..b3be7def0 --- /dev/null +++ b/docs/v7-to-v8-migration-guide.md @@ -0,0 +1,129 @@ +This is the list of main changes between major versions 7 and 8 of Appium +java client. This list should help you to successfully migrate your +existing automated tests codebase. + + +## Strict W3C specification compatibility + +- Java client now supports Selenium 4, which also means it is +*strictly* W3C compliant. Old JWP-based servers are not supported +anymore, and it won't be possible to use the new client version +with them. Capabilities that enforce the usage of JWP protocol +on Appium drivers don't have any effect anymore. +- The recommended way to provide capabilities for driver creation is +to use specific option builders inherited from +[BaseOptions class](https://github.com/appium/java-client/blob/master/src/main/java/io/appium/java_client/remote/options/BaseOptions.java). +For example +[XCUITestOptions](https://github.com/appium/java-client/blob/master/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java) +to create a XCUITest driver instance or +[UiAutomator2Options](https://github.com/appium/java-client/blob/master/src/main/java/io/appium/java_client/android/options/UiAutomator2Options.java) +to create an UiAutomator2 driver instance. +If there is no driver-specific options class for your driver then either use +`BaseOptions` builder as the base class to define your capabilities or request +driver developers to add one. _Do not_ use `DesiredCapabilities` class for this purpose in W3C context. +Check [unit tests](https://github.com/appium/java-client/blob/master/src/test/java/io/appium/java_client/drivers/options/OptionsBuildingTest.java) +for more examples on how to build driver options. + +## Elements lookup + +- All `findBy*` shortcut methods were removed. Consider using +`findElement[s](By. or AppiumBy.)` instead. +- `MobileBy` class has been deprecated. Consider using +[AppiumBy](https://github.com/appium/java-client/blob/master/src/main/java/io/appium/java_client/AppiumBy.java) +instead. +- All locator names in `AppiumBy` have been aligned to follow the common +(camelCase) naming strategy, e.g. `MobileBy.AccessibilityId` was changed +to `AppiumBy.accessibilityId`. +- The changes made in Selenium 4 broke `class name` selector strategy in Appium. +`AppiumBy.className` should be used instead of Selenium's `By.className` now. + +## Time + +- All methods that use TimeUnit class or where the time is passed as +a simple numeric value were replaced with their alternatives using +[java.time.Duration](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html) +class. + +## Events + +- The current event firing mechanism that Appium java client uses +has been deprecated in favour of the one that Selenium 4 provides +natively. Read [The-event_firing.md](The-event_firing.md) for more +details on how to use it. + +## AppiumDriver + +- All `AppiumDriver` descendants and the base class itself are not generic +anymore and work with `WebElement` interface only. +- The base Appium driver does not extend `ContextAware`, `Rotatable` and other +mobile-specific interfaces. Instead, it only has the very basic set of methods. +Mobile specific extensions have been respectively moved to `IOSDriver` and +`AndroidDriver`. +- Removed the obsolete `HasSessionDetails` extensions as it was using legacy +JWP calls to retrieve session details. +- `DefaultGenericMobileDriver` class has been removed. Now `AppiumDriver` is +inherited directly from Selenium's `RemoteWebDriver`. + +## MobileElement + +- `DefaultGenericMobileElement` class has been removed completely together +with its descendants (`MobileElement`, `IOSElement`, `AndroidElement` etc.). +Use `WebElement` instead. +- Due to the above change the page factory is now only creating elements +that are instantiated from `RemoteWebElement` and implement `WebElement` interface. +- If you used some special methods that `MobileElement` or its descendants provided +then change these: + - `replaceValue` has been moved to the corresponding `AndroidDriver` + instance and is called now `replaceElementValue` + - use `sendKeys` method of `WebElement` interface instead of `setValue`. + +## Touch Actions + +- The `TouchAction` and `MultiTouchAction` classes have been deprecated. +The support of these actions will be removed from future Appium versions. +Please use [W3C Actions](https://w3c.github.io/webdriver/#actions) instead +or the corresponding extension methods for the driver (if available). +Check + - https://www.youtube.com/watch?v=oAJ7jwMNFVU + - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + - 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. + +## resetApp/launchApp/closeApp + +- AppiumDriver methods `resetApp`, `launchApp` and `closeApp` have been deprecated as +they are going to be removed from future Appium versions. Check +https://github.com/appium/appium/issues/15807 for more details. + +## AppiumDriverLocalService + +- The default URL the server is listening on has been changed, and it +does not contain the `/wd/hub` suffix anymore (e.g. `http://0.0.0.0:4723/wd/hub` +became `http://0.0.0.0:4723/`). This has been done in order +to align the actual behavior with Appium v2. If you still would like to use +v8 of the Java client with Appium v1.2x, where the server URL contains the `/wd/hub` suffix +by default, then consider providing `--base-path` setting explicitly while +building `AppiumServiceBuilder` instance (e.g. `.withArgument(GeneralServerFlag.BASEPATH, "/wd/hub/")`). +Older versions of Appium server (v1.19 and older) won't work with `AppiumDriverLocalService`, +because they don't allow provisioning of base path in form of a command line argument. 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 9bd535700..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=3.141.59 +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 f3d88b1c2..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 4e1cc9db6..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-6.1.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 2fe81a7d9..adff685a0 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or 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,80 +15,114 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$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"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,87 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the 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. + JAVACMD=java + 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +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 fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a141..e509b2dd8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @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 +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,10 +27,14 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +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 @@ -51,48 +57,35 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +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 -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :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 %CMD_LINE_ARGS% +"%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 -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal 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 91% 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 87eafb978..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,9 +16,9 @@ package io.appium.java_client.android; -import static org.junit.Assert.assertNotEquals; +import org.junit.jupiter.api.Test; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertNotEquals; public class AndroidAppStringsTest extends BaseAndroidTest { 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 88% 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 f1eb1043a..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,16 +16,16 @@ package io.appium.java_client.android; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - import io.appium.java_client.android.connection.ConnectionState; import io.appium.java_client.android.connection.ConnectionStateBuilder; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; +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; -@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@TestMethodOrder(MethodOrderer.MethodName.class) public class AndroidConnectionTest extends BaseAndroidTest { @Test 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 65% 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 60e423fe8..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,22 +16,23 @@ package io.appium.java_client.android; -import static org.junit.Assert.assertEquals; - import io.appium.java_client.NoSuchContextException; -import org.junit.BeforeClass; -import org.junit.Test; +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 { - @BeforeClass public static void beforeClass2() throws Exception { - Activity activity = new Activity("io.appium.android.apis", ".view.WebView1"); - driver.startActivity(activity); + @BeforeAll public static void beforeClass2() throws Exception { + startActivity(".view.WebView1"); Thread.sleep(20000); } @Test public void testGetContext() { - assertEquals("NATIVE_APP", driver.getContext()); + assertEquals(NATIVE_CONTEXT, driver.getContext()); } @Test public void testGetContextHandles() { @@ -42,13 +43,12 @@ 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(expected = NoSuchContextException.class) public void testContextError() { - driver.context("Planet of the Ape-ium"); - assertEquals("Planet of the Ape-ium", driver.getContext()); + @Test public void testContextError() { + assertThrows(NoSuchContextException.class, () -> driver.context("Planet of the Ape-ium")); } } 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 66% 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 5e75653e6..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,31 +16,33 @@ package io.appium.java_client.android; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import io.appium.java_client.MobileBy; -import org.junit.Test; +import io.appium.java_client.AppiumBy; +import org.junit.jupiter.api.Test; import org.openqa.selenium.json.Json; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; -import static org.junit.Assert.assertNotNull; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; public class AndroidDataMatcherTest extends BaseEspressoTest { @Test public void testFindByDataMatcher() { - final WebDriverWait wait = new WebDriverWait(driver, 10); + final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions - .elementToBeClickable(MobileBy.AccessibilityId("Graphics"))); - driver.findElement(MobileBy.AccessibilityId("Graphics")).click(); + .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 - .presenceOfElementLocated(MobileBy.androidDataMatcher(selector)))); + .presenceOfElementLocated(AppiumBy.androidDataMatcher(selector)))); } } 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 83% 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 1f3c0ae00..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,30 +16,29 @@ package io.appium.java_client.android; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.lessThan; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.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.Test; +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 { @@ -150,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); @@ -193,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); } @@ -221,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 @@ -238,31 +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", ""); - } - - @Test - public void getDeviceUDIDTest() { - String deviceSerial = driver.getSessionDetail("deviceUDID").toString(); - assertNotNull(deviceSerial); - } - - @Test - public void getSessionMapData() { - Map map = (Map) driver.getSessionDetail("desired"); - assertNotEquals(map.size(), 0); + byte[] data = driver.pullFile("/data/system/users/userlist.xml"); + assert data.length > 0; } @Test @@ -274,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"); @@ -289,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(); @@ -314,10 +288,4 @@ public void getPerformanceDataTest() { public void getCurrentPackageTest() { assertEquals(APP_ID, driver.getCurrentPackage()); } - - @Test public void validateAllSessions() { - List> jsonMap = driver.getAllSessionDetails(); - assertNotNull(jsonMap); - } - } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidElementTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidElementTest.java new file mode 100644 index 000000000..44c8473d6 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidElementTest.java @@ -0,0 +1,88 @@ +/* + * 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 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 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() { + startActivity(".ApiDemos"); + } + + + @Test public void findByAccessibilityIdTest() { + assertNotEquals(driver.findElement(By.id("android:id/content")) + .findElement(AppiumBy.accessibilityId("Graphics")).getText(), null); + assertEquals(driver.findElement(By.id("android:id/content")) + .findElements(AppiumBy.accessibilityId("Graphics")).size(), 1); + } + + @Test public void findByAndroidUIAutomatorTest() { + assertNotEquals(driver.findElement(By.id("android:id/content")) + .findElement(AppiumBy + .androidUIAutomator("new UiSelector().clickable(true)")).getText(), null); + assertNotEquals(driver.findElement(By.id("android:id/content")) + .findElements(AppiumBy + .androidUIAutomator("new UiSelector().clickable(true)")).size(), 0); + assertNotEquals(driver.findElement(By.id("android:id/content")) + .findElements(AppiumBy + .androidUIAutomator("new UiSelector().clickable(true)")).size(), 1); + } + + @Test public void replaceValueTest() { + String originalValue = "original value"; + + startActivity(".view.Controls1"); + WebElement editElement = driver + .findElement(AppiumBy.androidUIAutomator("resourceId(\"io.appium.android.apis:id/edit\")")); + editElement.sendKeys(originalValue); + assertEquals(originalValue, editElement.getText()); + String replacedValue = "replaced value"; + driver.replaceElementValue((RemoteWebElement) editElement, replacedValue); + assertEquals(replacedValue, editElement.getText()); + } + + @Test public void scrollingToSubElement() { + driver.findElement(AppiumBy.accessibilityId("Views")).click(); + WebElement list = driver.findElement(By.id("android:id/list")); + WebElement radioGroup = list + .findElement(AppiumBy + .androidUIAutomator("new UiScrollable(new UiSelector()).scrollIntoView(" + + "new UiSelector().text(\"Radio Group\"));")); + assertNotNull(radioGroup.getLocation()); + } + + @Test public void setValueTest() { + String value = "new value"; + + startActivity(".view.Controls1"); + WebElement editElement = driver + .findElement(AppiumBy.androidUIAutomator("resourceId(\"io.appium.android.apis:id/edit\")")); + editElement.sendKeys(value); + assertEquals(value, editElement.getText()); + } +} 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 86% 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 e86a58c05..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,17 +1,10 @@ package io.appium.java_client.android; -import static java.time.Duration.ofMillis; -import static java.time.Duration.ofSeconds; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.StringContains.containsString; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - import io.appium.java_client.functions.AppiumFunction; import io.appium.java_client.functions.ExpectedCondition; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriver; @@ -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 -> { @@ -65,18 +66,17 @@ public class AndroidFunctionTest extends BaseAndroidTest { return null; }; - @BeforeClass + @BeforeAll public static void startWebViewActivity() { if (driver != null) { - Activity activity = new Activity("io.appium.android.apis", ".view.WebView1"); - driver.startActivity(activity); + startActivity(".view.WebView1"); } } - @Before + @BeforeEach public void setUp() { - driver.context("NATIVE_APP"); + driver.context(NATIVE_CONTEXT); } @Test @@ -127,17 +127,18 @@ public void complexWaitingTestWithPreCondition() { assertThat("There should be 3 calls", calls.size(), is(3)); } - @Test(expected = TimeoutException.class) + @Test public void nullPointerExceptionSafetyTestWithPrecondition() { Wait wait = new FluentWait<>(Pattern.compile("Fake_context")) .withTimeout(ofSeconds(30)).pollingEvery(ofMillis(500)); - assertTrue(wait.until(searchingFunction.compose(contextFunction)).size() > 0); + assertThrows(TimeoutException.class, () -> wait.until(searchingFunction.compose(contextFunction))); } - @Test(expected = TimeoutException.class) + @Test public void nullPointerExceptionSafetyTestWithPostConditions() { Wait wait = new FluentWait<>(Pattern.compile("Fake_context")) .withTimeout(ofSeconds(30)).pollingEvery(ofMillis(500)); - assertTrue(wait.until(contextFunction.andThen(searchingFunction).andThen(filteringFunction)).size() > 0); + assertThrows(TimeoutException.class, + () -> wait.until(contextFunction.andThen(searchingFunction).andThen(filteringFunction))); } } 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 76% 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 5cfdee5c2..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,14 +1,14 @@ package io.appium.java_client.android; -import static org.junit.Assert.assertTrue; - import org.apache.commons.lang3.time.DurationFormatUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.time.Duration; 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); @@ -25,9 +25,9 @@ public void verifyLogcatListenerCanBeAssigned() { messageSemaphore.acquire(); // This is needed for pushing some internal log messages driver.runAppInBackground(Duration.ofSeconds(1)); - assertTrue(String.format("Didn't receive any log message after %s timeout", - DurationFormatUtils.formatDuration(timeout.toMillis(), "H:mm:ss", true)), - messageSemaphore.tryAcquire(timeout.toMillis(), TimeUnit.MILLISECONDS)); + assertTrue(messageSemaphore.tryAcquire(timeout.toMillis(), TimeUnit.MILLISECONDS), + String.format("Didn't receive any log message after %s timeout", + DurationFormatUtils.formatDuration(timeout.toMillis(), "H:mm:ss", true))); } catch (InterruptedException e) { throw new IllegalStateException(e); } finally { 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 78% 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 be34c67b9..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 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; -import org.junit.Before; -import org.junit.Test; -import org.openqa.selenium.WebDriverException; - -import java.time.Duration; - public class AndroidScreenRecordTest extends BaseAndroidTest { - @Before + @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/e2eAndroidTest/java/io/appium/java_client/android/AndroidSearchingTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidSearchingTest.java new file mode 100644 index 000000000..fb9275943 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidSearchingTest.java @@ -0,0 +1,67 @@ +/* + * 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 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() { + startActivity(".ApiDemos"); + } + + @Test public void findByAccessibilityIdTest() { + assertNotEquals(driver.findElement(AppiumBy.accessibilityId("Graphics")).getText(), null); + assertEquals(driver.findElements(AppiumBy.accessibilityId("Graphics")).size(), 1); + } + + @Test public void findByAndroidUIAutomatorTest() { + assertNotEquals(driver + .findElement(AppiumBy + .androidUIAutomator("new UiSelector().clickable(true)")).getText(), null); + assertNotEquals(driver + .findElements(AppiumBy + .androidUIAutomator("new UiSelector().clickable(true)")).size(), 0); + assertNotEquals(driver + .findElements(AppiumBy + .androidUIAutomator("new UiSelector().clickable(true)")).size(), 1); + } + + @Test public void findByXPathTest() { + By byXPath = By.xpath("//android.widget.TextView[contains(@text, 'Animat')]"); + assertNotNull(driver.findElement(byXPath).getText()); + assertEquals(driver.findElements(byXPath).size(), 1); + } + + @Test public void findScrollable() { + driver.findElement(AppiumBy.accessibilityId("Views")).click(); + WebElement radioGroup = driver + .findElement(AppiumBy.androidUIAutomator("new UiScrollable(new UiSelector()" + + ".resourceId(\"android:id/list\")).scrollIntoView(" + + "new UiSelector().text(\"Radio Group\"));")); + assertNotNull(radioGroup.getLocation()); + } +} 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 72% 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 e8253074e..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,27 +16,29 @@ package io.appium.java_client.android; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import io.appium.java_client.MobileBy; -import org.junit.Test; +import io.appium.java_client.AppiumBy; +import org.junit.jupiter.api.Test; import org.openqa.selenium.json.Json; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; -import static org.junit.Assert.assertNotNull; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; 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, 10); + final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); assertNotNull(wait.until(ExpectedConditions - .presenceOfElementLocated(MobileBy.androidViewMatcher(selector)))); + .presenceOfElementLocated(AppiumBy.androidViewMatcher(selector)))); } } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java new file mode 100644 index 000000000..1325a0f85 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseAndroidTest.java @@ -0,0 +1,74 @@ +/* + * 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 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 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; + + private static AppiumDriverLocalService service; + protected static AndroidDriver driver; + + /** + * initialization. + */ + @BeforeAll public static void beforeClass() { + service = new AppiumServiceBuilder() + .withIPAddress("127.0.0.1") + .usingPort(PORT) + .build(); + service.start(); + + UiAutomator2Options options = new UiAutomator2Options() + .setDeviceName("Android Emulator") + .enableBiDi() + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) + .eventTimings(); + driver = new AndroidDriver(service.getUrl(), options); + } + + /** + * finishing. + */ + @AfterAll public static void afterClass() { + if (driver != null) { + driver.quit(); + } + if (service != null) { + 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 60% 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 cbea1e064..2245b1be3 100644 --- a/src/test/java/io/appium/java_client/android/BaseEspressoTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java @@ -16,25 +16,23 @@ package io.appium.java_client.android; -import io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobileCapabilityType; +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 org.junit.AfterClass; -import org.junit.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -import static io.appium.java_client.TestResources.apiDemosApk; +import io.appium.java_client.utils.TestUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") public class BaseEspressoTest { private static AppiumDriverLocalService service; - protected static AndroidDriver driver; + protected static AndroidDriver driver; /** * initialization. */ - @BeforeClass public static void beforeClass() { + @BeforeAll public static void beforeClass() { service = AppiumDriverLocalService.buildDefaultService(); service.start(); @@ -43,18 +41,17 @@ public class BaseEspressoTest { "An appium server node is not started!"); } - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.ESPRESSO); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, apiDemosApk().toAbsolutePath().toString()); - capabilities.setCapability("eventTimings", true); - driver = new AndroidDriver<>(service.getUrl(), capabilities); + EspressoOptions options = new EspressoOptions() + .setDeviceName("Android Emulator") + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) + .eventTimings(); + driver = new AndroidDriver(service.getUrl(), options); } /** * finishing. */ - @AfterClass public static void afterClass() { + @AfterAll public static void afterClass() { if (driver != null) { driver.quit(); } 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 97% 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 a48ecc092..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.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 73% 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 0e58ae3b6..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.Assert.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import org.junit.Before; -import org.junit.Test; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; public class ClipboardTest extends BaseAndroidTest { - @Before public void setUp() { - driver.resetApp(); + @BeforeEach public void setUp() { + 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/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java new file mode 100644 index 000000000..1e0bff096 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteCDPCommandTest.java @@ -0,0 +1,94 @@ +/* + * 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 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.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.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import java.util.HashMap; +import java.util.Map; + +import static java.time.Duration.ofSeconds; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ExecuteCDPCommandTest { + + private WebDriver driver; + + private AppiumDriverLocalService service; + + @FindBy(name = "q") + private WebElement searchTextField; + + + /** + * The setting up. + */ + @BeforeEach + public void setUp() { + service = AppiumDriverLocalService.buildDefaultService(); + service.start(); + + 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); + } + + /** + * finishing. + */ + @AfterEach + public void tearDown() { + if (driver != null) { + driver.quit(); + } + + if (service != null) { + service.stop(); + } + } + + @Test + public void testExecuteCDPCommandWithoutParam() { + driver.get("https://www.google.com"); + searchTextField.sendKeys("Hello"); + Map cookies = ((AndroidDriver) driver).executeCdpCommand("Page.getCookies"); + assertNotNull(cookies); + } + + @Test + public void testExecuteCDPCommandWithParams() { + Map params = new HashMap(); + params.put("latitude", 13.0827); + params.put("longitude", 80.2707); + params.put("accuracy", 1); + ((AndroidDriver) driver).executeCdpCommand("Emulation.setGeolocationOverride", params); + driver.get("https://www.google.com"); + } + +} 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 92% rename from src/test/java/io/appium/java_client/android/ExecuteDriverScriptTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteDriverScriptTest.java index 5b8da2938..d8bf5a65d 100644 --- a/src/test/java/io/appium/java_client/android/ExecuteDriverScriptTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteDriverScriptTest.java @@ -19,16 +19,16 @@ import io.appium.java_client.driverscripts.ScriptOptions; import io.appium.java_client.driverscripts.ScriptType; import io.appium.java_client.driverscripts.ScriptValue; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.List; import java.util.Map; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class ExecuteDriverScriptTest extends BaseAndroidTest { 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 64% 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 712c384f8..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,38 +16,39 @@ package io.appium.java_client.android; -import static io.appium.java_client.MobileBy.AndroidUIAutomator; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.openqa.selenium.By.id; - -import io.appium.java_client.remote.MobileCapabilityType; +import io.appium.java_client.android.options.UiAutomator2Options; import io.appium.java_client.service.local.AppiumDriverLocalService; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.WebElement; + +import java.time.Duration; + +import static io.appium.java_client.AppiumBy.androidUIAutomator; public class FingerPrintTest { private static AppiumDriverLocalService service; - private static AndroidDriver driver; + private static AndroidDriver driver; private static void initDriver() { - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability("appPackage", "com.android.settings"); - capabilities.setCapability("appActivity", "Settings"); - driver = new AndroidDriver<>(service.getUrl(), capabilities); - driver.manage().timeouts().implicitlyWait(15, SECONDS); + UiAutomator2Options options = new UiAutomator2Options() + .setDeviceName("Android Emulator") + .setAppPackage("com.android.settings") + .setAppActivity("Settings"); + driver = new AndroidDriver(service.getUrl(), options); + driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15)); } /** * initialization. */ - @BeforeClass public static void beforeClass() { + @BeforeAll public static void beforeClass() { service = AppiumDriverLocalService.buildDefaultService(); service.start(); @@ -59,38 +60,37 @@ private static void initDriver() { /** * finishing. */ - @AfterClass public static void afterClass() { + @AfterAll public static void afterClass() { if (service != null) { service.stop(); } } - private AndroidElement findElementByText(String text) { - return driver.findElements(id("android:id/title")).stream().filter(androidElement -> + private WebElement findElementByText(String text) { + return driver.findElements(By.id("android:id/title")).stream().filter(androidElement -> text.equals(androidElement.getText())).findFirst() .orElseThrow(() -> new NoSuchElementException(String.format("There is no element with the text '%s'", text))); } private void clickNext() { - driver.findElementById("com.android.settings:id/next_button").click(); + driver.findElement(By.id("com.android.settings:id/next_button")).click(); } private void clickFingerPrintNext() { - driver.findElementById("com.android.settings:id/fingerprint_next_button").click(); + driver.findElement(By.id("com.android.settings:id/fingerprint_next_button")).click(); } private void clickOKInPopup() { - driver.findElementById("android:id/button1").click(); + driver.findElement(By.id("android:id/button1")).click(); } private void enterPasswordAndContinue() { - driver.findElementById("com.android.settings:id/password_entry") - .sendKeys("1234\n"); + driver.findElement(By.id("com.android.settings:id/password_entry")).sendKeys("1234\n"); } private void clickOnSecurity() { - driver.findElement(AndroidUIAutomator("new UiScrollable(new UiSelector()" + driver.findElement(androidUIAutomator("new UiScrollable(new UiSelector()" + ".scrollable(true)).scrollIntoView(" + "new UiSelector().text(\"Security & location\"));")).click(); } @@ -98,7 +98,7 @@ private void clickOnSecurity() { /** * enable system security which is required for finger print activation. */ - @Before + @BeforeEach public void before() { initDriver(); clickOnSecurity(); @@ -123,14 +123,14 @@ public void fingerPrintTest() { try { clickNext(); } catch (Exception e) { - Assert.fail("fingerprint command fail to execute"); + Assertions.fail("fingerprint command fail to execute"); } } /** * disabling pin lock mode. */ - @After + @AfterEach public void after() { driver.quit(); 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 85% 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 388b32cf1..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.Matchers.greaterThan; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; - 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.Test; +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 92% 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 9a6b8ed1e..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,25 +16,24 @@ package io.appium.java_client.android; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.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; import io.appium.java_client.android.nativekey.KeyEventMetaModifier; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +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"); - @Before + @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 86% 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 ab4b0350a..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,15 +16,15 @@ package io.appium.java_client.android; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.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.Test; +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 { @@ -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/e2eAndroidTest/java/io/appium/java_client/android/OpenNotificationsTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/OpenNotificationsTest.java new file mode 100644 index 000000000..08bddc736 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/OpenNotificationsTest.java @@ -0,0 +1,29 @@ +package io.appium.java_client.android; + +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.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(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 78% rename from src/test/java/io/appium/java_client/android/SettingTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/SettingTest.java index 559c1ba69..380efa1cb 100644 --- a/src/test/java/io/appium/java_client/android/SettingTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/SettingTest.java @@ -1,11 +1,14 @@ package io.appium.java_client.android; import io.appium.java_client.Setting; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.time.Duration; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class SettingTest extends BaseAndroidTest { @@ -103,6 +106,24 @@ public class SettingTest extends BaseAndroidTest { .get("shouldUseCompactResponses")); } + @Test public void setMultipleSettings() { + EnumMap enumSettings = new EnumMap<>(Setting.class); + enumSettings.put(Setting.IGNORE_UNIMPORTANT_VIEWS, true); + enumSettings.put(Setting.ELEMENT_RESPONSE_ATTRIBUTES, "type,label"); + driver.setSettings(enumSettings); + Map actual = driver.getSettings(); + assertEquals(true, actual.get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); + assertEquals("type,label", actual.get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + + Map mapSettings = new HashMap<>(); + mapSettings.put(Setting.IGNORE_UNIMPORTANT_VIEWS.toString(), false); + mapSettings.put(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString(), ""); + driver.setSettings(mapSettings); + actual = driver.getSettings(); + assertEquals(false, actual.get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); + assertEquals("", actual.get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + } + private void assertJSONElementContains(Setting setting, long value) { assertEquals(driver.getSettings().get(setting.toString()), value); } 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 64% 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 19df3618a..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,31 +1,32 @@ package io.appium.java_client.android; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import io.appium.java_client.MobileBy; -import io.appium.java_client.MobileElement; -import org.junit.After; -import org.junit.Ignore; -import org.junit.Test; +import io.appium.java_client.AppiumBy; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.DeviceRotation; +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class UIAutomator2Test extends BaseAndroidTest { - @After + @AfterEach public void afterMethod() { driver.rotate(new DeviceRotation(0, 0, 0)); } @Test public void testLandscapeRightRotation() { - new WebDriverWait(driver, 20).until(ExpectedConditions - .elementToBeClickable(driver.findElementById("android:id/content") - .findElement(MobileBy.AccessibilityId("Graphics")))); + new WebDriverWait(driver, Duration.ofSeconds(20)).until(ExpectedConditions + .elementToBeClickable(driver.findElement(By.id("android:id/content")) + .findElement(AppiumBy.accessibilityId("Graphics")))); DeviceRotation landscapeRightRotation = new DeviceRotation(0, 0, 90); driver.rotate(landscapeRightRotation); assertEquals(driver.rotation(), landscapeRightRotation); @@ -33,9 +34,9 @@ public void testLandscapeRightRotation() { @Test public void testLandscapeLeftRotation() { - new WebDriverWait(driver, 20).until(ExpectedConditions - .elementToBeClickable(driver.findElementById("android:id/content") - .findElement(MobileBy.AccessibilityId("Graphics")))); + new WebDriverWait(driver, Duration.ofSeconds(20)).until(ExpectedConditions + .elementToBeClickable(driver.findElement(By.id("android:id/content")) + .findElement(AppiumBy.accessibilityId("Graphics")))); DeviceRotation landscapeLeftRotation = new DeviceRotation(0, 0, 270); driver.rotate(landscapeLeftRotation); assertEquals(driver.rotation(), landscapeLeftRotation); @@ -43,9 +44,9 @@ public void testLandscapeLeftRotation() { @Test public void testPortraitUpsideDown() { - new WebDriverWait(driver, 20).until(ExpectedConditions - .elementToBeClickable(driver.findElementById("android:id/content") - .findElement(MobileBy.AccessibilityId("Graphics")))); + new WebDriverWait(driver, Duration.ofSeconds(20)).until(ExpectedConditions + .elementToBeClickable(driver.findElement(By.id("android:id/content")) + .findElement(AppiumBy.accessibilityId("Graphics")))); DeviceRotation landscapeRightRotation = new DeviceRotation(0, 0, 180); driver.rotate(landscapeRightRotation); assertEquals(driver.rotation(), landscapeRightRotation); @@ -54,15 +55,15 @@ public void testPortraitUpsideDown() { /** * ignoring. */ - @Ignore + @Disabled + @Test public void testToastMSGIsDisplayed() { - final WebDriverWait wait = new WebDriverWait(driver, 30); - Activity activity = new Activity("io.appium.android.apis", ".view.PopupMenu1"); - driver.startActivity(activity); + final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); + startActivity(".view.PopupMenu1"); - wait.until(ExpectedConditions.presenceOfElementLocated(MobileBy - .AccessibilityId("Make a Popup!"))); - MobileElement popUpElement = driver.findElement(MobileBy.AccessibilityId("Make a Popup!")); + wait.until(ExpectedConditions.presenceOfElementLocated(AppiumBy + .accessibilityId("Make a Popup!"))); + WebElement popUpElement = driver.findElement(AppiumBy.accessibilityId("Make a Popup!")); wait.until(ExpectedConditions.elementToBeClickable(popUpElement)).click(); wait.until(ExpectedConditions.visibilityOfElementLocated( By.xpath(".//*[@text='Search']"))).click(); 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 74% 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 99121e990..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,26 +16,16 @@ 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 junit.framework.TestCase.assertNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.android.AndroidElement; - 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; import io.appium.java_client.pagefactory.AndroidFindBys; import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.HowToUseLocators; -import org.junit.Before; -import org.junit.Test; +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; import org.openqa.selenium.WrapsDriver; @@ -45,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; @@ -64,10 +65,10 @@ public class AndroidPageObjectTest extends BaseAndroidTest { private List androidUIAutomatorViews; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") - private List mobileElementViews; + private List mobileElementViews; @FindBy(className = "android.widget.TextView") - private List mobiletextVieWs; + private List mobiletextVieWs; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") private List remoteElementViews; @@ -94,10 +95,10 @@ public class AndroidPageObjectTest extends BaseAndroidTest { private WebElement androidUIAutomatorView; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") - private MobileElement mobileElementView; + private WebElement mobileElementView; @FindBy(className = "android.widget.TextView") - private MobileElement mobiletextVieW; + private WebElement mobiletextVieW; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") private RemoteWebElement remotetextVieW; @@ -114,12 +115,12 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")") @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")") @AndroidFindBy(id = "android:id/text1") - private AndroidElement androidElementView; + private WebElement androidElementView; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")") @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")") @AndroidFindBy(id = "android:id/text1") - private List androidElementViews; + private List androidElementViews; @HowToUseLocators(androidAutomation = ALL_POSSIBLE) @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/Fakecontent\")") @@ -152,9 +153,13 @@ 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 MobileElement cached; + private WebElement cached; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")") @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")") @@ -170,61 +175,61 @@ 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 AndroidElement androidElementViewFoundByMixedSearching; + 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; + 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\")")}) - private AndroidElement androidElementViewFoundByMixedSearching2; + @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\")")}) - private List androidElementsViewFoundByMixedSearching2; + @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 AndroidElement androidElementViewFoundByMixedSearching3; + private WebElement androidElementViewFoundByMixedSearching3; @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 List androidElementsViewFoundByMixedSearching3; + private List androidElementsViewFoundByMixedSearching3; /** * The setting up. */ - @Before public void setUp() { + @BeforeEach public void setUp() { if (!populated) { //This time out is set because test can be run on slow Android SDK emulator PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(5)), this); @@ -326,9 +331,9 @@ public class AndroidPageObjectTest extends BaseAndroidTest { assertNotEquals(null, textAndroidId.getAttribute("text")); } - @Test(expected = NoSuchElementException.class) public void checkThatTestWillNotBeFailedBecauseOfInvalidFindBy() { - assertNotNull( - elementWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy.getAttribute("text")); + @Test public void checkThatTestWillNotBeFailedBecauseOfInvalidFindBy() { + assertThrows(NoSuchElementException.class, + () -> elementWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy.getAttribute("text")); } @Test public void checkThatTestWillNotBeFailedBecauseOfInvalidFindByList() { @@ -336,8 +341,8 @@ public class AndroidPageObjectTest extends BaseAndroidTest { } @Test public void checkThatClassObjectMethodsDoNotInvokeTheSearching() { - assertTrue(AndroidElement.class.isAssignableFrom(fakeElement.getClass())); - assertNotEquals(AndroidElement.class, fakeElement.getClass()); + assertTrue(WebElement.class.isAssignableFrom(fakeElement.getClass())); + assertNotEquals(WebElement.class, fakeElement.getClass()); assertEquals(driver, ((WrapsDriver) fakeElement).getWrappedDriver()); } @@ -346,13 +351,27 @@ public class AndroidPageObjectTest extends BaseAndroidTest { assertNotEquals(ArrayList.class, fakeElements.getClass()); } - @Test public void checkCached() { - assertEquals(cached.getId(), cached.getId()); + @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(expected = NoSuchElementException.class) + @Test public void checkThatElementSearchingThrowsExpectedExceptionIfChainedLocatorIsInvalid() { - assertNotNull(elementFoundByInvalidChainedSelector.getAttribute("text")); + assertThrows(NoSuchElementException.class, () -> elementFoundByInvalidChainedSelector.getAttribute("text")); } @Test public void checkThatListSearchingWorksIfChainedLocatorIsInvalid() { @@ -367,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 74% 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 4be99b958..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,21 +16,18 @@ 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; 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.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.FindBys; @@ -38,13 +35,16 @@ import java.util.List; +import static java.time.Duration.ofSeconds; + public class MobileBrowserCompatibilityTest { private WebDriver driver; private AppiumDriverLocalService service; - @AndroidFindBy(className = "someClass") @AndroidFindBy(xpath = "//someTag") + @AndroidFindBy(className = "someClass") + @AndroidFindBy(xpath = "//someTag") private RemoteWebElement btnG; //this element should be found by id = 'btnG' or name = 'btnG' @FindBy(name = "q") @@ -53,20 +53,20 @@ public class MobileBrowserCompatibilityTest { @AndroidFindBy(className = "someClass") @FindBys({@FindBy(className = "r"), @FindBy(tagName = "a")}) - private List - foundLinks; + private List foundLinks; /** * The setting up. */ - @Before public void setUp() { + @BeforeEach + 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.BROWSER); - driver = new AndroidDriver(service.getUrl(), capabilities); + UiAutomator2Options options = new UiAutomator2Options() + .withBrowserName(MobileBrowserType.BROWSER) + .setDeviceName("Android Emulator"); + driver = new AndroidDriver(service.getUrl(), options); //This time out is set because test can be run on slow Android SDK emulator PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(5)), this); } @@ -74,7 +74,8 @@ public class MobileBrowserCompatibilityTest { /** * finishing. */ - @After public void tearDown() { + @AfterEach + public void tearDown() { if (driver != null) { driver.quit(); } @@ -84,12 +85,13 @@ public class MobileBrowserCompatibilityTest { } } - @Test public void test() { + @Test + public void test() { driver.get("https://www.google.com"); searchTextField.sendKeys("Hello"); btnG.click(); - Assert.assertNotEquals(0, foundLinks.size()); + Assertions.assertNotEquals(0, foundLinks.size()); } } 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 55% 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 05a9109f9..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,48 +1,45 @@ package io.appium.java_client.service.local; -import static io.appium.java_client.TestResources.apiDemosApk; -import static io.appium.java_client.TestUtils.getLocalIp4Address; -import static io.appium.java_client.remote.AndroidMobileCapabilityType.APP_ACTIVITY; -import static io.appium.java_client.remote.AndroidMobileCapabilityType.APP_PACKAGE; -import static io.appium.java_client.remote.AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE; -import static io.appium.java_client.remote.MobileCapabilityType.APP; -import static io.appium.java_client.remote.MobileCapabilityType.FULL_RESET; -import static io.appium.java_client.remote.MobileCapabilityType.NEW_COMMAND_TIMEOUT; -import static io.appium.java_client.remote.MobileCapabilityType.PLATFORM_NAME; +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_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.PRE_LAUNCH; 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; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -import com.google.common.collect.ImmutableMap; -import io.github.bonigarcia.wdm.WebDriverManager; -import org.junit.After; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.remote.DesiredCapabilities; +import static org.junit.jupiter.api.Assertions.assertEquals; +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 java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -public class ServerBuilderTest { +@SuppressWarnings("ResultOfMethodCallIgnored") +class ServerBuilderTest { /** * It may be impossible to find the path to the instance of appium server due to different circumstance. @@ -51,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; @@ -69,14 +62,14 @@ public class ServerBuilderTest { /** * initialization. */ - @BeforeClass + @BeforeAll public static void beforeClass() throws Exception { testIP = getLocalIp4Address(); chromeManager = chromedriver(); chromeManager.setup(); } - @After + @AfterEach public void tearDown() throws Exception { ofNullable(service).ifPresent(AppiumDriverLocalService::stop); @@ -95,7 +88,7 @@ public void tearDown() throws Exception { } @Test - public void checkAbilityToAddLogMessageConsumer() { + void checkAbilityToAddLogMessageConsumer() { List log = new ArrayList<>(); service = buildDefaultService(); service.clearOutPutStreams(); @@ -105,101 +98,93 @@ 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) - .withArgument(PRE_LAUNCH) .build(); service.start(); assertTrue(service.isRunning()); } @Test - public void checkAbilityToStartServiceUsingCapabilities() { - DesiredCapabilities caps = new DesiredCapabilities(); - caps.setCapability(PLATFORM_NAME, "Android"); - caps.setCapability(FULL_RESET, true); - caps.setCapability(NEW_COMMAND_TIMEOUT, 60); - caps.setCapability(APP_PACKAGE, "io.appium.android.apis"); - caps.setCapability(APP_ACTIVITY, ".view.WebView1"); - caps.setCapability(APP, apiDemosApk().toAbsolutePath().toString()); - caps.setCapability(CHROMEDRIVER_EXECUTABLE, chromeManager.getBinaryPath()); - - service = new AppiumServiceBuilder().withCapabilities(caps).build(); + void checkAbilityToStartServiceUsingCapabilities() { + UiAutomator2Options options = new UiAutomator2Options() + .fullReset() + .setNewCommandTimeout(Duration.ofSeconds(60)) + .setAppPackage("io.appium.android.apis") + .setAppActivity(".view.WebView1") + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) + .setChromedriverExecutable(chromeManager.getDownloadedDriverPath()); + + service = new AppiumServiceBuilder().withCapabilities(options).build(); service.start(); assertTrue(service.isRunning()); } @Test - public void checkAbilityToStartServiceUsingCapabilitiesAndFlags() { - File app = ROOT_TEST_PATH.resolve("ApiDemos-debug.apk").toFile(); - - DesiredCapabilities caps = new DesiredCapabilities(); - caps.setCapability(PLATFORM_NAME, "Android"); - caps.setCapability(FULL_RESET, true); - caps.setCapability(NEW_COMMAND_TIMEOUT, 60); - caps.setCapability(APP_PACKAGE, "io.appium.android.apis"); - caps.setCapability(APP_ACTIVITY, ".view.WebView1"); - caps.setCapability(APP, app.getAbsolutePath()); - caps.setCapability("winPath", "C:\\selenium\\app.apk"); - caps.setCapability("unixPath", "/selenium/app.apk"); - caps.setCapability("quotes", "\"'"); - caps.setCapability("goog:chromeOptions", - ImmutableMap.of("env", ImmutableMap.of("test", "value"), "val2", 0)); - caps.setCapability(CHROMEDRIVER_EXECUTABLE, chromeManager.getBinaryPath()); + void checkAbilityToStartServiceUsingCapabilitiesAndFlags() { + + UiAutomator2Options options = new UiAutomator2Options() + .fullReset() + .setNewCommandTimeout(Duration.ofSeconds(60)) + .setAppPackage("io.appium.android.apis") + .setAppActivity(".view.WebView1") + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL) + .setChromedriverExecutable(chromeManager.getDownloadedDriverPath()) + .amend("winPath", "C:\\selenium\\app.apk") + .amend("unixPath", "/selenium/app.apk") + .amend("quotes", "\"'") + .setChromeOptions( + Map.of("env", Map.of("test", "value"), "val2", 0) + ); service = new AppiumServiceBuilder() .withArgument(CALLBACK_ADDRESS, testIP) .withArgument(SESSION_OVERRIDE) - .withArgument(PRE_LAUNCH) - .withCapabilities(caps).build(); + .withCapabilities(options).build(); service.start(); assertTrue(service.isRunning()); } @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(); @@ -207,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); @@ -219,7 +204,7 @@ public void checkAbilityToChangeOutputStreamAfterTheServiceIsStarted() throws Ex } @Test - public void checkAbilityToShutDownService() { + void checkAbilityToShutDownService() { service = buildDefaultService(); service.start(); service.stop(); @@ -227,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(), @@ -241,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(); @@ -251,9 +236,9 @@ public void checkAbilityToStartServiceWithLogFile() throws Exception { } @Test - public void checkAbilityToStartServiceWithPortUsingFlag() { + void checkAbilityToStartServiceWithPortUsingFlag() { String port = "8996"; - String expectedUrl = String.format("http://0.0.0.0:%s/wd/hub", port); + String expectedUrl = String.format("http://0.0.0.0:%s/", port); service = new AppiumServiceBuilder() .withArgument(() -> "--port", port) @@ -264,9 +249,9 @@ public void checkAbilityToStartServiceWithPortUsingFlag() { } @Test - public void checkAbilityToStartServiceWithPortUsingShortFlag() { + void checkAbilityToStartServiceWithPortUsingShortFlag() { String port = "8996"; - String expectedUrl = String.format("http://0.0.0.0:%s/wd/hub", port); + String expectedUrl = String.format("http://0.0.0.0:%s/", port); service = new AppiumServiceBuilder() .withArgument(() -> "-p", port) @@ -277,8 +262,8 @@ public void checkAbilityToStartServiceWithPortUsingShortFlag() { } @Test - public void checkAbilityToStartServiceWithIpUsingFlag() { - String expectedUrl = String.format("http://%s:4723/wd/hub", testIP); + void checkAbilityToStartServiceWithIpUsingFlag() { + String expectedUrl = String.format("http://%s:4723/", testIP); service = new AppiumServiceBuilder() .withArgument(() -> "--address", testIP) @@ -289,8 +274,8 @@ public void checkAbilityToStartServiceWithIpUsingFlag() { } @Test - public void checkAbilityToStartServiceWithIpUsingShortFlag() { - String expectedUrl = String.format("http://%s:4723/wd/hub", testIP); + void checkAbilityToStartServiceWithIpUsingShortFlag() { + String expectedUrl = String.format("http://%s:4723/", testIP); service = new AppiumServiceBuilder() .withArgument(() -> "-a", testIP) @@ -301,7 +286,7 @@ public void checkAbilityToStartServiceWithIpUsingShortFlag() { } @Test - public void checkAbilityToStartServiceWithLogFileUsingFlag() { + void checkAbilityToStartServiceWithLogFileUsingFlag() { testLogFile = new File("Log2.txt"); service = new AppiumServiceBuilder() @@ -312,7 +297,7 @@ public void checkAbilityToStartServiceWithLogFileUsingFlag() { } @Test - public void checkAbilityToStartServiceWithLogFileUsingShortFlag() { + void checkAbilityToStartServiceWithLogFileUsingShortFlag() { testLogFile = new File("Log3.txt"); service = new AppiumServiceBuilder() @@ -321,4 +306,39 @@ public void checkAbilityToStartServiceWithLogFileUsingShortFlag() { service.start(); assertTrue(testLogFile.exists()); } + + @Test + 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 + 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 + void checkAbilityToValidateBasePathForEmptyBasePath() { + assertThrows(IllegalArgumentException.class, () -> new AppiumServiceBuilder().withArgument(BASEPATH, "")); + } + + @Test + void checkAbilityToValidateBasePathForBlankBasePath() { + assertThrows(IllegalArgumentException.class, () -> new AppiumServiceBuilder().withArgument(BASEPATH, " ")); + } + + @Test + void checkAbilityToValidateBasePathForNullBasePath() { + assertThrows(NullPointerException.class, () -> new AppiumServiceBuilder().withArgument(BASEPATH, null)); + } } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java new file mode 100644 index 000000000..131610d35 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/StartingAppLocallyAndroidTest.java @@ -0,0 +1,118 @@ +/* + * 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 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.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.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; + +class StartingAppLocallyAndroidTest { + + @Test + void startingAndroidAppWithCapabilitiesOnlyTest() { + AndroidDriver driver = new AndroidDriver(new UiAutomator2Options() + .setDeviceName("Android Emulator") + .autoGrantPermissions() + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL)); + try { + Capabilities caps = driver.getCapabilities(); + + assertTrue(MobilePlatform.ANDROID.equalsIgnoreCase( + String.valueOf(caps.getCapability(PLATFORM_NAME))) + ); + 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 + void startingAndroidAppWithCapabilitiesAndServiceTest() { + AppiumServiceBuilder builder = new AppiumServiceBuilder() + .withArgument(GeneralServerFlag.SESSION_OVERRIDE) + .withArgument(GeneralServerFlag.STRICT_CAPS); + + AndroidDriver driver = new AndroidDriver(builder, new UiAutomator2Options() + .setDeviceName("Android Emulator") + .autoGrantPermissions() + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL)); + try { + Capabilities caps = driver.getCapabilities(); + + assertTrue(MobilePlatform.ANDROID.equalsIgnoreCase( + String.valueOf(caps.getCapability(PLATFORM_NAME))) + ); + assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); + } finally { + driver.quit(); + } + } + + @Test + void startingAndroidAppWithCapabilitiesAndFlagsOnServerSideTest() { + UiAutomator2Options serverOptions = new UiAutomator2Options() + .setDeviceName("Android Emulator") + .fullReset() + .autoGrantPermissions() + .setNewCommandTimeout(Duration.ofSeconds(60)) + .setApp(TestUtils.ANDROID_APIDEMOS_APK_URL); + + WebDriverManager chromeManager = chromedriver(); + chromeManager.setup(); + serverOptions.setChromedriverExecutable(chromeManager.getDownloadedDriverPath()); + + AppiumServiceBuilder builder = new AppiumServiceBuilder() + .withArgument(GeneralServerFlag.SESSION_OVERRIDE) + .withArgument(GeneralServerFlag.STRICT_CAPS) + .withCapabilities(serverOptions); + + UiAutomator2Options clientOptions = new UiAutomator2Options() + .setAppPackage("io.appium.android.apis") + .setAppActivity(".view.WebView1"); + + AndroidDriver driver = new AndroidDriver(builder, clientOptions); + try { + Capabilities caps = driver.getCapabilities(); + + assertTrue(MobilePlatform.ANDROID.equalsIgnoreCase( + String.valueOf(caps.getCapability(PLATFORM_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 93% 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 fa1ba1d5c..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 static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import org.junit.jupiter.api.Test; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.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; -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 50% 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 45e7a8e2e..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,40 +18,47 @@ import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; -import org.junit.AfterClass; +import io.appium.java_client.service.local.flags.GeneralServerFlag; +import org.junit.jupiter.api.AfterAll; -import java.net.SocketException; -import java.net.UnknownHostException; - -import static io.appium.java_client.TestUtils.getLocalIp4Address; +import java.time.Duration; +import java.util.Optional; +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") public class BaseIOSTest { protected static AppiumDriverLocalService service; - protected static IOSDriver driver; + 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 X"; - public static final String PLATFORM_VERSION = System.getenv("IOS_PLATFORM_VERSION") != null - ? System.getenv("IOS_PLATFORM_VERSION") : "11.4"; + 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. * - * @return ip of a local host - * @throws UnknownHostException when it is impossible to get ip address of a local host + * @return service instance */ - public static String startAppiumServer() throws UnknownHostException, SocketException { - service = new AppiumServiceBuilder().usingPort(PORT).build(); + 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 getLocalIp4Address(); + return service; } /** * finishing. */ - @AfterClass public static void afterClass() { + @AfterAll public static void afterClass() { if (driver != null) { driver.quit(); } 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 57% 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 cc87a97ee..8c54b30c4 100644 --- a/src/test/java/io/appium/java_client/ios/BaseIOSWebViewTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java @@ -16,53 +16,41 @@ package io.appium.java_client.ios; -import io.appium.java_client.remote.IOSMobileCapabilityType; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; +import io.appium.java_client.ios.options.XCUITestOptions; +import org.junit.jupiter.api.BeforeAll; import org.openqa.selenium.SessionNotCreatedException; -import org.openqa.selenium.remote.DesiredCapabilities; import java.io.IOException; -import java.net.MalformedURLException; 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); - @BeforeClass + @BeforeAll public static void beforeClass() throws IOException { - final String ip = startAppiumServer(); + startAppiumServer(); - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!"); + XCUITestOptions options = new XCUITestOptions() + .setPlatformVersion(PLATFORM_VERSION) + .setDeviceName(DEVICE_NAME) + .setWdaLaunchTimeout(WDA_LAUNCH_TIMEOUT) + .setCommandTimeouts(Duration.ofSeconds(240)) + .setApp(new URL(IOS_SIM_VODQA_RELEASE_URL)); + if (PREBUILT_WDA_PATH != null) { + options.usePreinstalledWda().setPrebuiltWdaPath(PREBUILT_WDA_PATH); } - - final DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, PLATFORM_VERSION); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, DEVICE_NAME); - capabilities.setCapability("commandTimeouts", "120000"); - capabilities.setCapability(MobileCapabilityType.APP, vodQaAppZip().toAbsolutePath().toString()); - Supplier> createDriver = () -> { - try { - return new IOSDriver<>(new URL("http://" + ip + ":" + PORT + "/wd/hub"), capabilities); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - }; + Supplier createDriver = () -> new IOSDriver(service.getUrl(), options); try { driver = createDriver.get(); } catch (SessionNotCreatedException e) { // Sometimes WDA session creation freezes unexpectedly on CI: // https://dev.azure.com/srinivasansekar/java-client/_build/results?buildId=356&view=ms.vss-test-web.build-test-results-tab - capabilities.setCapability("useNewWDA", true); + options.useNewWDA(); driver = createDriver.get(); } } @@ -76,6 +64,7 @@ protected void findAndSwitchToWebView() throws InterruptedException { return; } } + //noinspection BusyWait Thread.sleep(WEB_VIEW_DETECT_INTERVAL.toMillis()); } throw new IllegalStateException(String.format("No web views have been detected within %sms timeout", diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java new file mode 100644 index 000000000..7468e89e9 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseSafariTest.java @@ -0,0 +1,42 @@ +/* + * 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 io.appium.java_client.remote.MobileBrowserType; +import org.junit.jupiter.api.BeforeAll; + +import java.time.Duration; + +public class BaseSafariTest extends BaseIOSTest { + private static final Duration WEBVIEW_CONNECT_TIMEOUT = Duration.ofSeconds(30); + + @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 85% 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 ac7618fd8..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.Assert.assertEquals; +import org.junit.jupiter.api.Test; -import org.junit.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 81% 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 e5a5033bb..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,53 +16,29 @@ package io.appium.java_client.ios; -import static junit.framework.TestCase.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; - -import io.appium.java_client.MobileBy; import org.apache.commons.lang3.StringUtils; -import org.junit.After; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.support.ui.WebDriverWait; +import java.time.Duration; import java.util.function.Supplier; -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class IOSAlertTest extends AppIOSTest { +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; - private static final long ALERT_TIMEOUT_SECONDS = 5; +@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 final WebDriverWait waiter = new WebDriverWait(driver, ALERT_TIMEOUT_SECONDS); - 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(MobileBy.AccessibilityId(iOSAutomationText)).click(); - } catch (WebDriverException e) { - // ignore - } - try { - waiter.until(alertIsPresent()); - return; - } catch (TimeoutException e) { - retry++; - if (retry >= CLICK_RETRIES) { - throw e; - } - } - } - } - - @After + @AfterEach public void afterEach() { try { driver.switchTo().alert().accept(); @@ -96,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 53% 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 369ea3292..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 static org.hamcrest.core.StringContains.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - import io.appium.java_client.NoSuchContextException; -import org.junit.Test; +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; 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(expected = NoSuchContextException.class) public void testContextError() { - driver.context("Planet of the Ape-ium"); - assertEquals("Planet of the Ape-ium", driver.getContext()); + @Test public void testContextError() { + // this test is not stable in the CI env due to simulator slowness + Assumptions.assumeFalse(TestUtils.isCiEnv()); + + assertThrows(NoSuchContextException.class, () -> driver.context("Planet of the Ape-ium")); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java new file mode 100644 index 000000000..c729d5b93 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSDriverTest.java @@ -0,0 +1,171 @@ +/* + * 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.Location; +import io.appium.java_client.appmanagement.ApplicationState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.ScreenOrientation; +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 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 + public void setupEach() { + if (driver.queryAppState(BUNDLE_ID).ordinal() < ApplicationState.RUNNING_IN_FOREGROUND.ordinal()) { + driver.activateApp(BUNDLE_ID); + } + } + + @Test + public void addCustomCommandTest() { + driver.addCommand(HttpMethod.GET, "/appium/sessions", "getSessions"); + final Response getSessions = driver.execute("getSessions"); + assertNotNull(getSessions.getSessionId()); + } + + @Test + public void addCustomCommandWithSessionIdTest() { + driver.addCommand(HttpMethod.GET, "/session/" + driver.getSessionId() + "/appium/settings", + "getSessionSettings"); + final Response getSessionSettings = driver.execute("getSessionSettings"); + assertNotNull(getSessionSettings.getSessionId()); + } + + @Test + public void addCustomCommandWithElementIdTest() { + var usernameEdit = driver.findElement(USERNAME_EDIT_PREDICATE); + driver.addCommand(HttpMethod.POST, + String.format("/session/%s/appium/element/%s/value", driver.getSessionId(), + ((RemoteWebElement) usernameEdit).getId()), "setNewValue"); + final Response setNewValue = driver.execute("setNewValue", + Map.of("id", ((RemoteWebElement) usernameEdit).getId(), "text", "foo")); + assertNotNull(setNewValue.getSessionId()); + } + + @Test + public void getDeviceTimeTest() { + String time = driver.getDeviceTime(); + assertFalse(time.isEmpty()); + } + + @Test public void resetTest() { + 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.0); + try { + driver.setLocation(location); + } catch (Exception e) { + fail("Not able to set location"); + } + } + + @Test public void orientationTest() { + 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() { + try { + driver.lockDevice(); + assertTrue(driver.isDeviceLocked()); + } finally { + driver.unlockDevice(); + assertFalse(driver.isDeviceLocked()); + } + } + + @Test public void pullFileTest() { + byte[] data = driver.pullFile(String.format("@%s/VodQAReactNative", BUNDLE_ID)); + assertThat(data.length, greaterThan(0)); + } + + @Test public void keyboardTest() { + driver.findElement(USERNAME_EDIT_PREDICATE).click(); + assertTrue(driver.isKeyboardShown()); + } + + @Test + public void putAppIntoBackgroundAndRestoreTest() { + final long msStarted = System.currentTimeMillis(); + driver.runAppInBackground(Duration.ofSeconds(4)); + assertThat(System.currentTimeMillis() - msStarted, greaterThan(3000L)); + } + + @Test + public void applicationsManagementTest() { + driver.runAppInBackground(Duration.ofSeconds(-1)); + waitUntilTrue( + () -> driver.queryAppState(BUNDLE_ID).ordinal() < ApplicationState.RUNNING_IN_FOREGROUND.ordinal(), + Duration.ofSeconds(10), Duration.ofSeconds(1)); + driver.activateApp(BUNDLE_ID); + waitUntilTrue( + () -> driver.queryAppState(BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND, + Duration.ofSeconds(10), Duration.ofSeconds(1)); + } + + 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 63% 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 de8bcbb36..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,33 +1,37 @@ package io.appium.java_client.ios; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; +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 public void nativeWebTapSettingTest() { + @Test + public void nativeWebTapSettingTest() { assertTrue(driver.isBrowser()); driver.get("https://saucelabs.com/test/guinea-pig"); // do a click with nativeWebTap turned on, and assert we get to the right page driver.nativeWebTap(true); - WebElement el = driver.findElementById("i am a link"); + WebElement el = driver.findElement(By.id("i am a link")); el.click(); - assertTrue(new WebDriverWait(driver, 30) + assertTrue(new WebDriverWait(driver, Duration.ofSeconds(30)) .until(ExpectedConditions.titleIs("I am another page title - Sauce Labs"))); driver.navigate().back(); // now do a click with it turned off and assert the same behavior - assertTrue(new WebDriverWait(driver, 30) + assertTrue(new WebDriverWait(driver, Duration.ofSeconds(30)) .until(ExpectedConditions.titleIs("I am a page title - Sauce Labs"))); driver.nativeWebTap(false); - el = driver.findElementById("i am a link"); + el = driver.findElement(By.id("i am a link")); el.click(); - assertTrue(new WebDriverWait(driver, 30) + assertTrue(new WebDriverWait(driver, Duration.ofSeconds(30)) .until(ExpectedConditions.titleIs("I am another page title - Sauce Labs"))); } } 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 95% 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 5657b1ff2..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.Test; - -import java.time.Duration; - public class IOSScreenRecordTest extends AppIOSTest { @Test diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java new file mode 100644 index 000000000..e3fa303e6 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.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.ios; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class IOSSearchingTest extends AppIOSTest { + @Test + public void findByAccessibilityIdTest() { + assertNotNull(driver.findElement(LOGIN_LINK_ID).getText()); + assertNotEquals(0, driver.findElements(LOGIN_LINK_ID).size()); + } + + @Test + public void findByByIosPredicatesTest() { + assertNotNull(driver.findElement(USERNAME_EDIT_PREDICATE).getText()); + assertNotEquals(0, driver.findElements(USERNAME_EDIT_PREDICATE).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 76% 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 ce627e8f9..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,14 +1,14 @@ package io.appium.java_client.ios; -import static org.junit.Assert.assertTrue; - import org.apache.commons.lang3.time.DurationFormatUtils; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.time.Duration; 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); @@ -25,9 +25,9 @@ public void verifySyslogListenerCanBeAssigned() { messageSemaphore.acquire(); // This is needed for pushing some internal log messages driver.runAppInBackground(Duration.ofSeconds(1)); - assertTrue(String.format("Didn't receive any log message after %s timeout", - DurationFormatUtils.formatDuration(timeout.toMillis(), "H:mm:ss", true)), - messageSemaphore.tryAcquire(timeout.toMillis(), TimeUnit.MILLISECONDS)); + assertTrue(messageSemaphore.tryAcquire(timeout.toMillis(), TimeUnit.MILLISECONDS), + String.format("Didn't receive any log message after %s timeout", + DurationFormatUtils.formatDuration(timeout.toMillis(), "H:mm:ss", true))); } catch (InterruptedException e) { throw new IllegalStateException(e); } finally { diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java new file mode 100644 index 000000000..1895e3517 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSWebViewTest.java @@ -0,0 +1,34 @@ +package io.appium.java_client.ios; + +import io.appium.java_client.AppiumBy; +import io.appium.java_client.utils.TestUtils; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.support.ui.ExpectedConditions; +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(); + new WebDriverWait(driver, LOOKUP_TIMEOUT) + .until(ExpectedConditions.presenceOfElementLocated(AppiumBy.accessibilityId("webView"))) + .click(); + new WebDriverWait(driver, LOOKUP_TIMEOUT) + .until(ExpectedConditions.presenceOfElementLocated(AppiumBy.accessibilityId("Webview"))); + findAndSwitchToWebView(); + assertTrue(driver.findElement(By.partialLinkText("login")).isDisplayed()); + } +} 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 85% 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 b3bbcf0c2..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.Matchers.greaterThan; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; - 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.Test; +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 88% 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 7e65b12b7..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,15 +16,15 @@ package io.appium.java_client.ios; -import static org.junit.Assert.assertEquals; - -import org.junit.After; -import org.junit.Test; +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 { - @After public void afterMethod() { + @AfterEach public void afterMethod() { driver.rotate(new DeviceRotation(0, 0, 0)); } 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 73% 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 d48d2c64d..647b93e2d 100644 --- a/src/test/java/io/appium/java_client/ios/SettingTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/SettingTest.java @@ -18,10 +18,13 @@ import io.appium.java_client.Setting; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.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 { @@ -34,10 +37,10 @@ public class SettingTest extends AppIOSTest { } @Test public void testSetElementResponseAttributes() { - assertEquals("type,label", driver.getSettings() + assertEquals("", driver.getSettings() .get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); - driver.setElementResponseAttributes("name"); - assertEquals("name", driver.getSettings() + driver.setElementResponseAttributes("type,label"); + assertEquals("type,label", driver.getSettings() .get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); } @@ -94,5 +97,21 @@ public class SettingTest extends AppIOSTest { .get("shouldUseCompactResponses")); } - + @Test public void setMultipleSettings() { + EnumMap enumSettings = new EnumMap<>(Setting.class); + enumSettings.put(Setting.IGNORE_UNIMPORTANT_VIEWS, true); + enumSettings.put(Setting.ELEMENT_RESPONSE_ATTRIBUTES, "type,label"); + driver.setSettings(enumSettings); + Map actual = driver.getSettings(); + assertEquals(true, actual.get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); + assertEquals("type,label", actual.get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + + Map mapSettings = new HashMap<>(); + mapSettings.put(Setting.IGNORE_UNIMPORTANT_VIEWS.toString(), false); + mapSettings.put(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString(), ""); + driver.setSettings(mapSettings); + actual = driver.getSettings(); + assertEquals(false, actual.get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); + assertEquals("", actual.get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + } } 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/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java b/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java new file mode 100644 index 000000000..256c43835 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/service/local/StartingAppLocallyIosTest.java @@ -0,0 +1,116 @@ +/* + * 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 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.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; + +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 + 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(appUrl) + .setWdaLaunchTimeout(BaseIOSTest.WDA_LAUNCH_TIMEOUT); + IOSDriver driver = new IOSDriver(options); + try { + XCUITestOptions caps = new XCUITestOptions(driver.getCapabilities()); + + assertEquals(AutomationName.IOS_XCUI_TEST, caps.getAutomationName().orElse(null)); + assertEquals(Platform.IOS, caps.getPlatformName()); + assertNotNull(caps.getDeviceName().orElse(null)); + assertEquals(BaseIOSTest.PLATFORM_VERSION, caps.getPlatformVersion().orElse(null)); + assertEquals(appUrl.toString(), caps.getApp().orElse(null)); + } finally { + driver.quit(); + } + } + + @Test + 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(appUrl) + .setWdaLaunchTimeout(BaseIOSTest.WDA_LAUNCH_TIMEOUT); + + AppiumServiceBuilder builder = new AppiumServiceBuilder() + .withArgument(GeneralServerFlag.SESSION_OVERRIDE) + .withArgument(GeneralServerFlag.STRICT_CAPS) + .withTimeout(BaseIOSTest.SERVER_START_TIMEOUT); + + IOSDriver driver = new IOSDriver(builder, options); + try { + Capabilities caps = driver.getCapabilities(); + assertTrue(Objects.requireNonNull(caps.getCapability(PLATFORM_NAME)) + .toString().equalsIgnoreCase(MobilePlatform.IOS)); + assertNotNull(caps.getCapability(DEVICE_NAME_OPTION)); + } finally { + driver.quit(); + } + } + + @Test + 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(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); + try { + XCUITestOptions caps = new XCUITestOptions(driver.getCapabilities()); + assertEquals(Platform.IOS, caps.getPlatformName()); + assertNotNull(caps.getDeviceName().orElse(null)); + assertFalse(driver.isBrowser()); + } finally { + driver.quit(); + } + } +} diff --git a/src/main/java/io/appium/java_client/AppiumBy.java b/src/main/java/io/appium/java_client/AppiumBy.java new file mode 100644 index 000000000..1c24b29c1 --- /dev/null +++ b/src/main/java/io/appium/java_client/AppiumBy.java @@ -0,0 +1,460 @@ +/* + * 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 com.google.common.base.Preconditions; +import lombok.EqualsAndHashCode; +import lombok.Getter; +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; + private final String locatorName; + + protected AppiumBy(String selector, String locatorString, String locatorName) { + 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) { + return context.findElements(this); + } + + @Override + public WebElement findElement(SearchContext context) { + return context.findElement(this); + } + + @Override + public String toString() { + return String.format("%s.%s: %s", AppiumBy.class.getSimpleName(), locatorName, remoteParameters.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 + * + * @param accessibilityId id is a convenient UI automation accessibility Id. + * @return an instance of {@link AppiumBy.ByAndroidUIAutomator} + */ + public static By accessibilityId(final String accessibilityId) { + return new ByAccessibilityId(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 + * @return an instance of {@link AppiumBy.ByAndroidDataMatcher} + */ + public static By androidDataMatcher(final String dataMatcherString) { + return new ByAndroidDataMatcher(dataMatcherString); + } + + /** + * Refer to UI Automator . + * + * @param uiautomatorText is Android UIAutomator string + * @return an instance of {@link ByAndroidUIAutomator} + */ + public static By androidUIAutomator(final String uiautomatorText) { + return new ByAndroidUIAutomator(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 + * @return an instance of {@link AppiumBy.ByAndroidViewMatcher} + */ + public static By androidViewMatcher(final String viewMatcherString) { + return new ByAndroidViewMatcher(viewMatcherString); + } + + /** + * This locator strategy is available in Espresso Driver mode. + * + * @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); + } + + /** + * 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} + */ + public static By className(final String selector) { + return new ByClassName(selector); + } + + /** + * For IOS the element name. + * For Android it is the resource identifier. + * + * @param selector element id + * @return an instance of {@link ById} + */ + public static By id(final String selector) { + return new ById(selector); + } + + /** + * For IOS the element name. + * For Android it is the resource identifier. + * + * @param selector element id + * @return an instance of {@link ByName} + */ + public static By name(final String selector) { + return new ByName(selector); + } + + /** + * This type of locator requires the use of the 'customFindModules' capability and a + * separately-installed element finding plugin. + * + * @param selector selector to pass to the custom element finding plugin + * @return an instance of {@link ByCustom} + * @since Appium 1.9.2 + */ + public static By custom(final String selector) { + return new ByCustom(selector); + } + + /** + * This locator strategy is available only if OpenCV libraries and + * Node.js bindings are installed on the server machine. + * + * @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); + } + + /** + * This locator strategy is available in XCUITest Driver mode. + * + * @param iOSClassChainString is a valid class chain locator string. + * See + * the documentation for more details + * @return an instance of {@link AppiumBy.ByIosClassChain} + */ + public static By iOSClassChain(final String iOSClassChainString) { + return new ByIosClassChain(iOSClassChainString); + } + + /** + * This locator strategy is available in XCUITest Driver mode. + * + * @param iOSNsPredicateString is an iOS NsPredicate String + * @return an instance of {@link AppiumBy.ByIosNsPredicate} + */ + 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"); + } + } + + public static class ByAndroidDataMatcher extends AppiumBy implements Serializable { + protected ByAndroidDataMatcher(String locatorString) { + super("-android datamatcher", locatorString, "androidDataMatcher"); + } + } + + public static class ByAndroidUIAutomator extends AppiumBy implements Serializable { + public ByAndroidUIAutomator(String uiautomatorText) { + super("-android uiautomator", uiautomatorText, "androidUIAutomator"); + } + } + + public static class ByAndroidViewMatcher extends AppiumBy implements Serializable { + protected ByAndroidViewMatcher(String locatorString) { + super("-android viewmatcher", locatorString, "androidViewMatcher"); + } + } + + public static class ByAndroidViewTag extends AppiumBy implements Serializable { + public ByAndroidViewTag(String tag) { + super("-android viewtag", tag, "androidViewTag"); + } + } + + public static class ById extends AppiumBy implements Serializable { + protected ById(String selector) { + super("id", selector, "id"); + } + } + + public static class ByName extends AppiumBy implements Serializable { + protected ByName(String selector) { + super("name", selector, "name"); + } + } + + public static class ByClassName extends AppiumBy implements Serializable { + protected ByClassName(String selector) { + super("class name", selector, "className"); + } + } + + public static class ByCustom extends AppiumBy implements Serializable { + protected ByCustom(String selector) { + super("-custom", selector, "custom"); + } + } + + public static class ByImage extends AppiumBy implements Serializable { + protected ByImage(String b64Template) { + super("-image", b64Template, "image"); + } + } + + public static class ByIosClassChain extends AppiumBy implements Serializable { + protected ByIosClassChain(String locatorString) { + super("-ios class chain", locatorString, "iOSClassChain"); + } + } + + public static class ByIosNsPredicate extends AppiumBy implements Serializable { + 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 32785d1eb..7fa2b3629 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -16,66 +16,75 @@ package io.appium.java_client; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.remote.MobileCapabilityType.PLATFORM_NAME; -import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; -import static org.apache.commons.lang3.StringUtils.isBlank; - -import com.google.common.collect.ImmutableMap; - import io.appium.java_client.internal.CapabilityHelpers; -import io.appium.java_client.internal.JsonToMobileElementConverter; +import io.appium.java_client.internal.SessionHelpers; import io.appium.java_client.remote.AppiumCommandExecutor; -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 org.openqa.selenium.By; +import lombok.Getter; +import org.jspecify.annotations.NonNull; import org.openqa.selenium.Capabilities; -import org.openqa.selenium.DeviceRotation; -import org.openqa.selenium.MutableCapabilities; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; +import org.openqa.selenium.ImmutableCapabilities; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.SessionNotCreatedException; +import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.html5.Location; - +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.DesiredCapabilities; +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.codec.w3c.W3CHttpResponseCodec; import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.http.HttpMethod; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; -import java.util.LinkedHashSet; -import java.util.List; +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. - * - * @param the required type of class which implement {@link WebElement}. - * Instances of the defined type will be returned via findElement* and findElements* - * Warning (!!!). Allowed types: - * {@link WebElement}, {@link org.openqa.selenium.remote.RemoteWebElement}, - * {@link MobileElement} and its subclasses that designed - * specifically for each target mobile OS (still Android and iOS) */ -@SuppressWarnings("unchecked") -public class AppiumDriver - extends DefaultGenericMobileDriver implements ComparesImages, FindsByImage, FindsByCustom, - ExecutesDriverScript, LogsEvents, HasSettings { - - private static final ErrorHandler errorHandler = new ErrorHandler(new ErrorCodesMobile(), true); +public class AppiumDriver extends RemoteWebDriver implements + ExecutesMethod, + ComparesImages, + ExecutesDriverScript, + LogsEvents, + HasBrowserCheck, + CanRememberExtensionPresence, + HasSettings, + HasBiDi { + + private static final ErrorHandler ERROR_HANDLER = new ErrorHandler(new ErrorCodesMobile(), true); // frequently used command parameters - private URL remoteAddress; - private RemoteLocationContext locationContext; - private ExecuteMethod executeMethod; + @Getter + private final URL remoteAddress; + 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}. @@ -88,261 +97,331 @@ public class AppiumDriver 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(); - this.setElementConverter(new JsonToMobileElementConverter(this)); } - public AppiumDriver(URL remoteAddress, Capabilities desiredCapabilities) { + public AppiumDriver(AppiumClientConfig clientConfig, Capabilities capabilities) { + this(new AppiumCommandExecutor(MobileCommand.commandRepository, clientConfig), capabilities); + } + + public AppiumDriver(URL remoteAddress, Capabilities capabilities) { this(new AppiumCommandExecutor(MobileCommand.commandRepository, remoteAddress), - desiredCapabilities); + capabilities); } public AppiumDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { + Capabilities capabilities) { this(new AppiumCommandExecutor(MobileCommand.commandRepository, remoteAddress, - httpClientFactory), desiredCapabilities); + httpClientFactory), capabilities); } - public AppiumDriver(AppiumDriverLocalService service, Capabilities desiredCapabilities) { + public AppiumDriver(AppiumDriverLocalService service, Capabilities capabilities) { this(new AppiumCommandExecutor(MobileCommand.commandRepository, service), - desiredCapabilities); + capabilities); } public AppiumDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { + Capabilities capabilities) { this(new AppiumCommandExecutor(MobileCommand.commandRepository, service, httpClientFactory), - desiredCapabilities); + capabilities); } - public AppiumDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) { - this(builder.build(), desiredCapabilities); + public AppiumDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + this(builder.build(), capabilities); } public AppiumDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - this(builder.build(), httpClientFactory, desiredCapabilities); + Capabilities capabilities) { + this(builder.build(), httpClientFactory, capabilities); } - public AppiumDriver(HttpClient.Factory httpClientFactory, Capabilities desiredCapabilities) { + public AppiumDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { this(AppiumDriverLocalService.buildDefaultService(), httpClientFactory, - desiredCapabilities); + capabilities); } - public AppiumDriver(Capabilities desiredCapabilities) { - this(AppiumDriverLocalService.buildDefaultService(), desiredCapabilities); + public AppiumDriver(Capabilities capabilities) { + this(AppiumDriverLocalService.buildDefaultService(), capabilities); } /** - * Changes platform name and returns new 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 newPlatform a {@link MobileCapabilityType#PLATFORM_NAME} value which has - * to be set up - * @return {@link Capabilities} with changed mobile platform value - * @deprecated Please use {@link #updateDefaultPlatformName(Capabilities, String)} instead + * @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. */ - @Deprecated - protected static Capabilities substituteMobilePlatform(Capabilities originalCapabilities, - String newPlatform) { - DesiredCapabilities dc = new DesiredCapabilities(originalCapabilities); - dc.setCapability(PLATFORM_NAME, newPlatform); - return dc; + 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(); + + setSessionId(sessionAddress.getId()); + } + + @Override + public ExecuteMethod getExecuteMethod() { + return executeMethod; } /** - * Changes platform name if it is not set and returns new capabilities. + * This method is used to get build version status of running Appium server. * - * @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 mobile platform name value or the original capabilities + * @return map containing version details */ - protected static Capabilities updateDefaultPlatformName(Capabilities originalCapabilities, - String defaultName) { - if (originalCapabilities.getCapability(PLATFORM_NAME) == null) { - DesiredCapabilities dc = new DesiredCapabilities(originalCapabilities); - dc.setCapability(PLATFORM_NAME, defaultName); - return dc; - } - return originalCapabilities; + public Map getStatus() { + //noinspection unchecked + return (Map) execute(DriverCommand.STATUS).getValue(); } - @Override - public List findElements(By by) { - return super.findElements(by); + /** + * This method is used to add custom appium commands in Appium 2.0. + * + * @param httpMethod the available {@link HttpMethod}. + * @param url The url to URL template as https://www.w3.org/TR/webdriver/#endpoints. + * @param methodName The name of custom appium command. + */ + public void addCommand(HttpMethod httpMethod, String url, String methodName) { + CommandInfo commandInfo; + switch (httpMethod) { + case GET: + commandInfo = MobileCommand.getC(url); + break; + case POST: + commandInfo = MobileCommand.postC(url); + break; + case DELETE: + 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()).defineCommand(methodName, commandInfo); } @Override - public List findElements(String by, String using) { - return super.findElements(by, using); + public Response execute(String driverCommand, Map parameters) { + return super.execute(driverCommand, parameters); } @Override - public List findElementsById(String id) { - return super.findElementsById(id); - } - - public List findElementsByLinkText(String using) { - return super.findElementsByLinkText(using); + public Response execute(String command) { + return super.execute(command, Collections.emptyMap()); } - public List findElementsByPartialLinkText(String using) { - return super.findElementsByPartialLinkText(using); - } - - public List findElementsByTagName(String using) { - return super.findElementsByTagName(using); - } - - public List findElementsByName(String using) { - return super.findElementsByName(using); - } - - public List findElementsByClassName(String using) { - return super.findElementsByClassName(using); - } + @Override + 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); + } - public List findElementsByCssSelector(String using) { - return super.findElementsByCssSelector(using); + @Override + public X convertFromPngBytes(byte[] png) { + return outputType.convertFromPngBytes(png); + } + }); } - public List findElementsByXPath(String using) { - return super.findElementsByXPath(using); + @Override + public AppiumDriver assertExtensionExists(String extName) { + if (absentExtensionNames.contains(extName)) { + throw new UnsupportedCommandException(); + } + return this; } @Override - public List findElementsByAccessibilityId(String using) { - return super.findElementsByAccessibilityId(using); + public AppiumDriver markExtensionAbsence(String extName) { + absentExtensionNames.add(extName); + return this; } @Override - public ExecuteMethod getExecuteMethod() { - return executeMethod; + public Optional maybeGetBiDi() { + return Optional.ofNullable(this.biDi); } @Override - public WebDriver context(String name) { - checkNotNull(name, "Must supply a context name"); - try { - execute(DriverCommand.SWITCH_TO_CONTEXT, ImmutableMap.of("name", name)); - return this; - } catch (WebDriverException e) { - throw new NoSuchContextException(e.getMessage(), e); + @NonNull + public BiDi getBiDi() { + var webSocketUrl = ((BaseOptions) this.capabilities).getWebSocketUrl().orElseThrow( + () -> { + var suffix = wasBiDiRequested + ? "Do both the server and the driver declare BiDi support?" + : String.format("Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL); + return new BiDiException(String.format( + "BiDi is not enabled for this driver session. %s", suffix + )); + } + ); + if (this.biDiUri == null) { + throw new BiDiException( + String.format( + "BiDi is not enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", + SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl + ) + ); + } + if (this.biDi == null) { + // This should not happen + throw new IllegalStateException(); } + return this.biDi; } - @Override - public Set getContextHandles() { - Response response = execute(DriverCommand.GET_CONTEXT_HANDLES); - Object value = response.getValue(); - try { - List returnedValues = (List) value; - return new LinkedHashSet<>(returnedValues); - } catch (ClassCastException ex) { - throw new WebDriverException( - "Returned value cannot be converted to List: " + value, ex); - } + protected HttpClient getHttpClient() { + return ((HttpCommandExecutor) getCommandExecutor()).client; } @Override - public String getContext() { - String contextName = - String.valueOf(execute(DriverCommand.GET_CURRENT_CONTEXT_HANDLE).getValue()); - if ("null".equalsIgnoreCase(contextName)) { - return null; + protected void startSession(Capabilities requestCapabilities) { + var response = Optional.ofNullable( + execute(DriverCommand.NEW_SESSION(singleton(requestCapabilities))) + ).orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a null response." + )); + + var rawResponseCapabilities = Optional.ofNullable(response.getValue()) + .map(value -> { + if (!(value instanceof Map)) { + throw new SessionNotCreatedException(String.format( + "The underlying command executor returned a response " + + "with a non well formed payload: %s", response) + ); + } + //noinspection unchecked + return (Map) value; + }) + .orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a response without payload: " + response) + ); + + // TODO: remove this workaround for Selenium API enforcing some legacy capability values in major version + rawResponseCapabilities.remove("platform"); + if (rawResponseCapabilities.containsKey(CapabilityType.BROWSER_NAME) + && isNullOrEmpty((String) rawResponseCapabilities.get(CapabilityType.BROWSER_NAME))) { + rawResponseCapabilities.remove(CapabilityType.BROWSER_NAME); + } + this.capabilities = new BaseOptions<>(rawResponseCapabilities); + this.wasBiDiRequested = Boolean.TRUE.equals( + requestCapabilities.getCapability(SupportsWebSocketUrlOption.WEB_SOCKET_URL) + ); + if (wasBiDiRequested) { + this.initBiDi((BaseOptions) capabilities); } - return contextName; + setSessionId(response.getSessionId()); } /** - * This method is used to get build version status of running Appium server. + * Changes platform name if it is not set and returns merged capabilities. * - * @return map containing version details + * @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 */ - public Map getStatus() { - return (Map) execute(DriverCommand.STATUS).getValue(); + protected static Capabilities ensurePlatformName( + Capabilities originalCapabilities, String defaultName) { + return originalCapabilities.getPlatformName() == null + ? originalCapabilities.merge(new ImmutableCapabilities(PLATFORM_NAME, defaultName)) + : originalCapabilities; } - @Override - public DeviceRotation rotation() { - Response response = execute(DriverCommand.GET_SCREEN_ROTATION); - DeviceRotation deviceRotation = - new DeviceRotation((Map) response.getValue()); - if (deviceRotation.getX() < 0 || deviceRotation.getY() < 0 || deviceRotation.getZ() < 0) { - throw new WebDriverException("Unexpected orientation returned: " + deviceRotation); - } - return deviceRotation; - } - - @Override - public void rotate(DeviceRotation rotation) { - execute(DriverCommand.SET_SCREEN_ROTATION, rotation.parameters()); - } - - - @Override - public void rotate(ScreenOrientation orientation) { - execute(DriverCommand.SET_SCREEN_ORIENTATION, - ImmutableMap.of("orientation", orientation.value().toUpperCase())); - } - - @Override - public ScreenOrientation getOrientation() { - Response response = execute(DriverCommand.GET_SCREEN_ORIENTATION); - String orientation = response.getValue().toString().toLowerCase(); - if (orientation.equals(ScreenOrientation.LANDSCAPE.value())) { - return ScreenOrientation.LANDSCAPE; - } else if (orientation.equals(ScreenOrientation.PORTRAIT.value())) { - return ScreenOrientation.PORTRAIT; - } else { - throw new WebDriverException("Unexpected orientation returned: " + orientation); + /** + * 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; } - @Override - public Location location() { - return locationContext.location(); - } - - @Override - public void setLocation(Location location) { - locationContext.setLocation(location); - } - - public URL getRemoteAddress() { - return remoteAddress; + /** + * Changes platform and automation names if they are not set + * and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultPlatformName a platformName value which has to be set up + * @param defaultAutomationName The default automation name to set up for this class + * @return {@link Capabilities} with changed platform/automation name value or the original capabilities + */ + protected static Capabilities ensurePlatformAndAutomationNames( + Capabilities originalCapabilities, String defaultPlatformName, String defaultAutomationName) { + Capabilities capsWithPlatformFixed = ensurePlatformName(originalCapabilities, defaultPlatformName); + return ensureAutomationName(capsWithPlatformFixed, defaultAutomationName); } - @Override - public boolean isBrowser() { - String browserName = CapabilityHelpers.getCapability(getCapabilities(), - CapabilityType.BROWSER_NAME, String.class); - if (!isBlank(browserName)) { - try { - return (boolean) executeScript("return !!window.navigator;"); - } catch (WebDriverException ign) { - // ignore - } + private void initBiDi(BaseOptions responseCaps) { + var webSocketUrl = responseCaps.getWebSocketUrl(); + if (webSocketUrl.isEmpty()) { + return; } + URISyntaxException uriSyntaxError = null; try { - return !containsIgnoreCase(getContext(), "NATIVE_APP"); - } catch (WebDriverException e) { - return false; + this.biDiUri = new URI(String.valueOf(webSocketUrl.get())); + } catch (URISyntaxException e) { + uriSyntaxError = e; } - } - - @Override - protected void startSession(Capabilities capabilities) { - super.startSession(capabilities); - // The RemoteWebDriver implementation overrides platformName - // so we need to restore it back to the original value - Object originalPlatformName = capabilities.getCapability(PLATFORM_NAME); - Capabilities originalCaps = super.getCapabilities(); - if (originalPlatformName != null && originalCaps instanceof MutableCapabilities) { - ((MutableCapabilities) super.getCapabilities()).setCapability(PLATFORM_NAME, - originalPlatformName); + 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 3bab7980c..34a848f79 100644 --- a/src/main/java/io/appium/java_client/AppiumExecutionMethod.java +++ b/src/main/java/io/appium/java_client/AppiumExecutionMethod.java @@ -16,17 +16,15 @@ package io.appium.java_client; -import com.google.common.collect.ImmutableMap; - import org.openqa.selenium.remote.ExecuteMethod; import org.openqa.selenium.remote.Response; import java.util.Map; public class AppiumExecutionMethod implements ExecuteMethod { - private final AppiumDriver driver; + private final AppiumDriver driver; - public AppiumExecutionMethod(AppiumDriver driver) { + public AppiumExecutionMethod(AppiumDriver driver) { this.driver = driver; } @@ -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/DefaultGenericMobileDriver.java b/src/main/java/io/appium/java_client/DefaultGenericMobileDriver.java deleted file mode 100644 index 0ca7a1dcc..000000000 --- a/src/main/java/io/appium/java_client/DefaultGenericMobileDriver.java +++ /dev/null @@ -1,164 +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 com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.By; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.CommandExecutor; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.remote.Response; - -import java.util.List; -import java.util.Map; - -@SuppressWarnings({"unchecked", "rawtypes"}) -abstract class DefaultGenericMobileDriver extends RemoteWebDriver - implements MobileDriver { - - public DefaultGenericMobileDriver(CommandExecutor executor, Capabilities desiredCapabilities) { - super(executor, desiredCapabilities); - } - - @Override public Response execute(String driverCommand, Map parameters) { - return super.execute(driverCommand, parameters); - } - - @Override public Response execute(String command) { - return super.execute(command, ImmutableMap.of()); - } - - @Override public List findElements(By by) { - return super.findElements(by); - } - - @Override public List findElements(String by, String using) { - return super.findElements(by, using); - } - - @Override public T findElement(By by) { - return (T) super.findElement(by); - } - - @Override public T findElement(String by, String using) { - return (T) super.findElement(by, using); - } - - @Override public List findElementsById(String id) { - return super.findElementsById(id); - } - - @Override public T findElementById(String id) { - return (T) super.findElementById(id); - } - - /** - * Finds a single element by link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public T findElementByLinkText(String using) throws WebDriverException { - return (T) super.findElementByLinkText(using); - } - - /** - * Finds many elements by link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByLinkText(String using) throws WebDriverException { - return super.findElementsByLinkText(using); - } - - /** - * Finds a single element by partial link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public T findElementByPartialLinkText(String using) throws WebDriverException { - return (T) super.findElementByPartialLinkText(using); - } - - /** - * Finds many elements by partial link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByPartialLinkText(String using) throws WebDriverException { - return super.findElementsByPartialLinkText(using); - } - - public T findElementByTagName(String using) { - return (T) super.findElementByTagName(using); - } - - public List findElementsByTagName(String using) { - return super.findElementsByTagName(using); - } - - public T findElementByName(String using) { - return (T) super.findElementByName(using); - } - - public List findElementsByName(String using) { - return super.findElementsByName(using); - } - - public T findElementByClassName(String using) { - return (T) super.findElementByClassName(using); - } - - public List findElementsByClassName(String using) { - return super.findElementsByClassName(using); - } - - /** - * Finds a single element by CSS selector. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public T findElementByCssSelector(String using) throws WebDriverException { - return (T) super.findElementByCssSelector(using); - } - - /** - * Finds many elements by CSS selector. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByCssSelector(String using) throws WebDriverException { - return super.findElementsByCssSelector(using); - } - - public T findElementByXPath(String using) { - return (T) super.findElementByXPath(using); - } - - public List findElementsByXPath(String using) { - return super.findElementsByXPath(using); - } - - @Override - public String toString() { - Capabilities capabilities = getCapabilities(); - return String.format("%s, Capabilities: %s", getClass().getCanonicalName(), - capabilities != null ? capabilities.asMap().toString() : "null"); - } -} diff --git a/src/main/java/io/appium/java_client/DefaultGenericMobileElement.java b/src/main/java/io/appium/java_client/DefaultGenericMobileElement.java deleted file mode 100644 index 2d8154880..000000000 --- a/src/main/java/io/appium/java_client/DefaultGenericMobileElement.java +++ /dev/null @@ -1,179 +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 com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.internal.FindsByClassName; -import org.openqa.selenium.internal.FindsByCssSelector; -import org.openqa.selenium.internal.FindsById; -import org.openqa.selenium.internal.FindsByLinkText; -import org.openqa.selenium.internal.FindsByName; -import org.openqa.selenium.internal.FindsByTagName; -import org.openqa.selenium.internal.FindsByXPath; -import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.remote.Response; - -import java.util.List; -import java.util.Map; - -@SuppressWarnings({"unchecked", "rawtypes"}) -abstract class DefaultGenericMobileElement extends RemoteWebElement - implements FindsByClassName, - FindsByCssSelector, FindsById, - FindsByLinkText, FindsByName, FindsByTagName, FindsByXPath, FindsByFluentSelector, FindsByAccessibilityId, - ExecutesMethod { - - @Override public Response execute(String driverCommand, Map parameters) { - return super.execute(driverCommand, parameters); - } - - @Override public Response execute(String command) { - return super.execute(command, ImmutableMap.of()); - } - - @Override public List findElements(By by) { - return super.findElements(by); - } - - @Override public List findElements(String by, String using) { - return super.findElements(by, using); - } - - @Override public T findElement(By by) { - return (T) super.findElement(by); - } - - @Override public T findElement(String by, String using) { - return (T) super.findElement(by, using); - } - - @Override public List findElementsById(String id) { - return super.findElementsById(id); - } - - @Override public T findElementById(String id) { - return (T) super.findElementById(id); - } - - /** - * Finds a single element by link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public T findElementByLinkText(String using) throws WebDriverException { - return (T) super.findElementByLinkText(using); - } - - /** - * Finds many elements by link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByLinkText(String using) throws WebDriverException { - return super.findElementsByLinkText(using); - } - - /** - * Finds a single element by partial link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public T findElementByPartialLinkText(String using) throws WebDriverException { - return (T) super.findElementByPartialLinkText(using); - } - - /** - * Finds many elements by partial link text. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByPartialLinkText(String using) throws WebDriverException { - return super.findElementsByPartialLinkText(using); - } - - public T findElementByTagName(String using) { - return (T) super.findElementByTagName(using); - } - - public List findElementsByTagName(String using) { - return super.findElementsByTagName(using); - } - - public T findElementByName(String using) { - return (T) super.findElementByName(using); - } - - public List findElementsByName(String using) { - return super.findElementsByName(using); - } - - public T findElementByClassName(String using) { - return (T) super.findElementByClassName(using); - } - - public List findElementsByClassName(String using) { - return super.findElementsByClassName(using); - } - - /** - * Finds a single element by CSS selector. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public T findElementByCssSelector(String using) throws WebDriverException { - return (T) super.findElementByCssSelector(using); - } - - /** - * Finds many elements by CSS selector. - * - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByCssSelector(String using) throws WebDriverException { - return super.findElementsByCssSelector(using); - } - - public T findElementByXPath(String using) { - return (T) super.findElementByXPath(using); - } - - public List findElementsByXPath(String using) { - return super.findElementsByXPath(using); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException because it may not work against native app UI. - */ - public void submit() throws WebDriverException { - super.submit(); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException because it may not work against native app UI. - */ - public String getCssValue(String propertyName) throws WebDriverException { - return super.getCssValue(propertyName); - } -} 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 new file mode 100644 index 000000000..7114da787 --- /dev/null +++ b/src/main/java/io/appium/java_client/ExecuteCDPCommand.java @@ -0,0 +1,60 @@ +/* + * 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 org.jspecify.annotations.Nullable; +import org.openqa.selenium.remote.Response; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.EXECUTE_GOOGLE_CDP_COMMAND; +import static java.util.Objects.requireNonNull; + +public interface ExecuteCDPCommand extends ExecutesMethod { + + /** + * Allows to execute ChromeDevProtocol commands against Android Chrome browser session. + * + * @param command Command to execute against the browser (For Ref : https://chromedevtools.github.io/devtools-protocol/) + * @param params additional parameters required to execute the command + * @return Value (Output of the command execution) + * @throws org.openqa.selenium.WebDriverException if there was a failure while executing the command + * @since Appium 1.18 + */ + default Map executeCdpCommand(String command, @Nullable Map params) { + Map data = new HashMap<>(); + data.put("cmd", requireNonNull(command)); + data.put("params", params == null ? Collections.emptyMap() : params); + Response response = execute(EXECUTE_GOOGLE_CDP_COMMAND, data); + //noinspection unchecked + return Collections.unmodifiableMap((Map) response.getValue()); + } + + /** + * Allows to execute ChromeDevProtocol commands against Android Chrome browser session without parameters. + * + * @param command Command to execute against the browser (For Ref : https://chromedevtools.github.io/devtools-protocol/) + * @return Value (Output of the command execution) + * @throws org.openqa.selenium.WebDriverException if there was a failure while executing the command + * @since Appium 1.18 + */ + default Map executeCdpCommand(String command) { + return executeCdpCommand(command, null); + } +} 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/ExecutesMethod.java b/src/main/java/io/appium/java_client/ExecutesMethod.java index 1f49276bc..f0bd981ca 100644 --- a/src/main/java/io/appium/java_client/ExecutesMethod.java +++ b/src/main/java/io/appium/java_client/ExecutesMethod.java @@ -22,18 +22,18 @@ public interface ExecutesMethod { /** - * Executes JSONWP command and returns a response. + * Executes the given command and returns a response. * - * @param driverCommand a JSONWP command + * @param driverCommand a command to execute * @param parameters map of command parameters * @return a result response */ Response execute(String driverCommand, Map parameters); /** - * Executes JSONWP command and returns a response. + * Executes the given command and returns a response. * - * @param driverCommand a JSONWP command + * @param driverCommand a command to execute * @return a result response */ Response execute(String driverCommand); diff --git a/src/main/java/io/appium/java_client/FindsByAccessibilityId.java b/src/main/java/io/appium/java_client/FindsByAccessibilityId.java deleted file mode 100644 index 4d79a33a4..000000000 --- a/src/main/java/io/appium/java_client/FindsByAccessibilityId.java +++ /dev/null @@ -1,52 +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 org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByAccessibilityId extends FindsByFluentSelector { - /** - * Method performs the searching for a single element by accessibility ID selector - * and value of the given selector. - * - * @param using an accessibility ID selector - * @return The first element that matches the given selector - * - * @throws WebDriverException This method is not applicable with browser/webview UI. - * @throws NoSuchElementException when no one element is found - */ - default T findElementByAccessibilityId(String using) { - return findElement(MobileSelector.ACCESSIBILITY.toString(), using); - } - - /** - * Method performs the searching for a list of elements by accessibility ID selector - * and value of the given selector. - * - * @param using an accessibility ID selector - * @return a list of elements that match the given selector - * - * @throws WebDriverException This method is not applicable with browser/webview UI. - */ - default List findElementsByAccessibilityId(String using) { - return findElements(MobileSelector.ACCESSIBILITY.toString(), using); - } -} diff --git a/src/main/java/io/appium/java_client/FindsByAndroidUIAutomator.java b/src/main/java/io/appium/java_client/FindsByAndroidUIAutomator.java deleted file mode 100644 index 50c7bafba..000000000 --- a/src/main/java/io/appium/java_client/FindsByAndroidUIAutomator.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; - -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByAndroidUIAutomator extends FindsByFluentSelector { - - /** - * Method performs the searching for a single element by Android UIAutomator selector - * and value of the given selector. - * - * @param using an Android UIAutomator selector - * @return The first element that matches the given selector - * - * @throws WebDriverException This method is not applicable with browser/webview UI. - * @throws NoSuchElementException when no one element is found - */ - default T findElementByAndroidUIAutomator(String using) { - return findElement(MobileSelector.ANDROID_UI_AUTOMATOR.toString(), using); - } - - /** - * Method performs the searching for a list of elements by Android UIAutomator selector - * and value of the given selector. - * - * @param using an Android UIAutomator selector - * @return a list of elements that match the given selector - * - * @throws WebDriverException This method is not applicable with browser/webview UI. - */ - default List findElementsByAndroidUIAutomator(String using) { - return findElements(MobileSelector.ANDROID_UI_AUTOMATOR.toString(), using); - } -} diff --git a/src/main/java/io/appium/java_client/FindsByAndroidViewMatcher.java b/src/main/java/io/appium/java_client/FindsByAndroidViewMatcher.java deleted file mode 100644 index 1370cf3ae..000000000 --- a/src/main/java/io/appium/java_client/FindsByAndroidViewMatcher.java +++ /dev/null @@ -1,32 +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 org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByAndroidViewMatcher extends FindsByFluentSelector { - - default T findElementByAndroidViewMatcher(String using) { - return findElement(MobileSelector.ANDROID_VIEW_MATCHER.toString(), using); - } - - default List findElementsByAndroidViewMatcher(String using) { - return findElements(MobileSelector.ANDROID_VIEW_MATCHER.toString(), using); - } -} diff --git a/src/main/java/io/appium/java_client/FindsByAndroidViewTag.java b/src/main/java/io/appium/java_client/FindsByAndroidViewTag.java deleted file mode 100644 index b1db5c432..000000000 --- a/src/main/java/io/appium/java_client/FindsByAndroidViewTag.java +++ /dev/null @@ -1,52 +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 org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByAndroidViewTag extends FindsByFluentSelector { - /** - * Method performs the searching for a single element by view tag selector - * and value of the given selector. - * - * @param using an view tag selector - * @return The first element that matches the given selector - * - * @throws WebDriverException This method is not applicable with browser/webview UI. - * @throws NoSuchElementException when no one element is found - */ - default T findElementByAndroidViewTag(String using) { - return findElement(MobileSelector.ANDROID_VIEWTAG.toString(), using); - } - - /** - * Method performs the searching for a list of elements by view tag selector - * and value of the given selector. - * - * @param using an view tag selector - * @return a list of elements that match the given selector - * - * @throws WebDriverException This method is not applicable with browser/webview UI. - */ - default List findElementsByAndroidViewTag(String using) { - return findElements(MobileSelector.ANDROID_VIEWTAG.toString(), using); - } -} diff --git a/src/main/java/io/appium/java_client/FindsByCustom.java b/src/main/java/io/appium/java_client/FindsByCustom.java deleted file mode 100644 index f908fc424..000000000 --- a/src/main/java/io/appium/java_client/FindsByCustom.java +++ /dev/null @@ -1,55 +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 org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByCustom extends FindsByFluentSelector { - /** - * Performs the lookup for a single element by sending a selector to a custom element finding - * plugin. This type of locator requires the use of the 'customFindModules' capability and a - * separately-installed element finding plugin. - * - * @param selector selector to pass to the custom element finding plugin - * @return The first element that matches the given selector - * @see - * The documentation on custom element finding plugins and their use - * @throws NoSuchElementException when no element is found - * @since Appium 1.9.2 - */ - default T findElementByCustom(String selector) { - return findElement(MobileSelector.CUSTOM.toString(), selector); - } - - /** - * Performs the lookup for a single element by sending a selector to a custom element finding - * plugin. This type of locator requires the use of the 'customFindModules' capability and a - * separately-installed element finding plugin. - * - * @param selector selector to pass to the custom element finding plugin - * @return a list of elements that match the given selector or an empty list - * @see - * The documentation on custom element finding plugins and their use - * @since Appium 1.9.2 - */ - default List findElementsByCustom(String selector) { - return findElements(MobileSelector.CUSTOM.toString(), selector); - } -} \ No newline at end of file diff --git a/src/main/java/io/appium/java_client/FindsByFluentSelector.java b/src/main/java/io/appium/java_client/FindsByFluentSelector.java deleted file mode 100644 index f545f47be..000000000 --- a/src/main/java/io/appium/java_client/FindsByFluentSelector.java +++ /dev/null @@ -1,51 +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 org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByFluentSelector { - - /** - * Method performs the searching for a single element by some selector defined by string - * and value of the given selector. - * - * @param by is a string selector - * @param using is a value of the given selector - * @return the first found element - * - * @throws org.openqa.selenium.WebDriverException when current session doesn't - * support the given selector or when value of the selector is not consistent. - * @throws org.openqa.selenium.NoSuchElementException when no one element is found - */ - T findElement(String by, String using); - - /** - * Method performs the searching for a list of elements by some selector defined by string - * and value of the given selector. - * - * @param by is a string selector - * @param using is a value of the given selector - * @return a list of elements - * - * @throws org.openqa.selenium.WebDriverException when current session doesn't support - * the given selector or when value of the selector is not consistent. - */ - List findElements(String by, String using); -} diff --git a/src/main/java/io/appium/java_client/FindsByImage.java b/src/main/java/io/appium/java_client/FindsByImage.java deleted file mode 100644 index 76de64fd1..000000000 --- a/src/main/java/io/appium/java_client/FindsByImage.java +++ /dev/null @@ -1,63 +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 org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByImage extends FindsByFluentSelector { - /** - * Performs the lookup for a single element by matching its image template - * to the current full screen shot. This type of locator requires OpenCV libraries - * and bindings for NodeJS to be installed on the server machine. Lookup options - * fine-tuning might be done via {@link HasSettings#setSetting(Setting, Object)}. - * - * @param b64Template base64-encoded template image string. Supported image formats are the same - * as for OpenCV library. - * @return The first element that matches the given selector - * @throws NoSuchElementException when no element is found - * @see - * The documentation on Image Comparison Features - * @see - * The settings available for lookup fine-tuning - * @since Appium 1.8.2 - */ - default T findElementByImage(String b64Template) { - return findElement(MobileSelector.IMAGE.toString(), b64Template); - } - - /** - * Performs the lookup for a list of elements by matching them to image template - * in the current full screen shot. This type of locator requires OpenCV libraries - * and bindings for NodeJS to be installed on the server machine. Lookup options - * fine-tuning might be done via {@link HasSettings#setSetting(Setting, Object)}. - * - * @param b64Template base64-encoded template image string. Supported image formats are the same - * as for OpenCV library. - * @return a list of elements that match the given selector or an empty list - * @see - * The documentation on Image Comparison Features - * @see - * The settings available for lookup fine-tuning - * @since Appium 1.8.2 - */ - default List findElementsByImage(String b64Template) { - return findElements(MobileSelector.IMAGE.toString(), b64Template); - } -} diff --git a/src/main/java/io/appium/java_client/FindsByWindowsAutomation.java b/src/main/java/io/appium/java_client/FindsByWindowsAutomation.java deleted file mode 100644 index 4416eb63f..000000000 --- a/src/main/java/io/appium/java_client/FindsByWindowsAutomation.java +++ /dev/null @@ -1,50 +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 org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByWindowsAutomation extends FindsByFluentSelector { - - /** - * Finds the first of elements that match the Windows UIAutomation selector supplied. - * - * @param selector a Windows UIAutomation selector - * @return The first element that matches the given selector - * @throws WebDriverException This method is not applicable with browser/webview UI. - * @throws NoSuchElementException when no one element is found - */ - default T findElementByWindowsUIAutomation(String selector) { - return findElement(MobileSelector.WINDOWS_UI_AUTOMATION.toString(), selector); - } - - /** - * Finds a list of elements that match the Windows UIAutomation selector supplied. - * - * @param selector a Windows UIAutomation selector - * @return a list of elements that match the given selector - * @throws WebDriverException This method is not applicable with browser/webview UI. - */ - default List findElementsByWindowsUIAutomation(String selector) { - return findElements(MobileSelector.WINDOWS_UI_AUTOMATION.toString(), selector); - } -} 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 new file mode 100644 index 000000000..a75ffbfd4 --- /dev/null +++ b/src/main/java/io/appium/java_client/HasBrowserCheck.java @@ -0,0 +1,43 @@ +package io.appium.java_client; + +import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.remote.SupportsContextSwitching; +import org.openqa.selenium.HasCapabilities; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.remote.CapabilityType; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static java.util.Locale.ROOT; +import static java.util.Objects.requireNonNull; + +public interface HasBrowserCheck extends ExecutesMethod, HasCapabilities { + String NATIVE_CONTEXT = "NATIVE_APP"; + + /** + * Validates if the driver is currently in a web browser context. + * + * @return true or false. + */ + default boolean isBrowser() { + String browserName = CapabilityHelpers.getCapability(getCapabilities(), + CapabilityType.BROWSER_NAME, String.class); + if (!isNullOrEmpty(browserName)) { + try { + return requireNonNull( + CommandExecutionHelper.executeScript(this, "return !!window.navigator;") + ); + } catch (WebDriverException ign) { + // ignore + } + } + if (!(this instanceof SupportsContextSwitching)) { + return false; + } + try { + 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/HasSessionDetails.java b/src/main/java/io/appium/java_client/HasSessionDetails.java deleted file mode 100644 index 1ae3f9f80..000000000 --- a/src/main/java/io/appium/java_client/HasSessionDetails.java +++ /dev/null @@ -1,104 +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.GET_ALLSESSION; -import static io.appium.java_client.MobileCommand.GET_SESSION; -import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toMap; -import static org.apache.commons.lang3.StringUtils.isBlank; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.remote.Response; - -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; - -public interface HasSessionDetails extends ExecutesMethod { - /** - * The current session details. - * - * @return a map with values that hold session details. - */ - @SuppressWarnings("unchecked") - default Map getSessionDetails() { - Response response = execute(GET_SESSION); - Map resultMap = Map.class.cast(response.getValue()); - - //this filtering was added to clear returned result. - //results of further operations should be simply interpreted by users - return ImmutableMap.builder() - .putAll(resultMap.entrySet() - .stream().filter(entry -> { - String key = entry.getKey(); - Object value = entry.getValue(); - return !isBlank(key) - && value != null - && !isBlank(String.valueOf(value)); - }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue))).build(); - } - - default @Nullable Object getSessionDetail(String detail) { - return getSessionDetails().get(detail); - } - - /** - * The current mobile platform. - * - * @return name of the current mobile platform. - */ - default @Nullable String getPlatformName() { - Object platformName = ofNullable(getSessionDetail("platformName")) - .orElseGet(() -> getSessionDetail("platform")); - return ofNullable(platformName).map(String::valueOf).orElse(null); - } - - /** - * The current automation name. - * - * @return current automation name. - */ - default @Nullable String getAutomationName() { - return ofNullable(getSessionDetail("automationName")) - .map(String::valueOf).orElse(null); - } - - /** - * Checks if focus is on browser. - * - * @return is focus on browser or on native content. - */ - default boolean isBrowser() { - return ofNullable(getSessionDetail("browserName")) - .orElse(null) != null; - } - - /** - * Get All Sessions details. - * - * @return List of Map objects with All Session Details. - */ - @SuppressWarnings("unchecked") - default List> getAllSessionDetails() { - Response response = execute(GET_ALLSESSION); - List> resultSet = List.class.cast(response.getValue()); - return ImmutableList.>builder().addAll(resultSet).build(); - } -} diff --git a/src/main/java/io/appium/java_client/HasSettings.java b/src/main/java/io/appium/java_client/HasSettings.java index 2d6045846..73befa6f5 100644 --- a/src/main/java/io/appium/java_client/HasSettings.java +++ b/src/main/java/io/appium/java_client/HasSettings.java @@ -16,12 +16,15 @@ 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; import java.util.Map; +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 { @@ -52,6 +55,31 @@ default HasSettings setSetting(String settingName, Object value) { return this; } + /** + * Sets settings for this test session. + * + * @param settings a map with settings, where key is the setting name you wish to set and value is the value of + * the setting. + * @return Self instance for chaining. + */ + default HasSettings setSettings(EnumMap settings) { + Map convertedSettings = settings.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().toString(), Entry::getValue)); + return setSettings(convertedSettings); + } + + /** + * Sets settings for this test session. + * + * @param settings a map with settings, where key is the setting name you wish to set and value is the value of + * the setting. + * @return Self instance for chaining. + */ + default HasSettings setSettings(Map settings) { + CommandExecutionHelper.execute(this, setSettingsCommand(settings)); + return this; + } + /** * Get settings stored for this test session It's probably better to use a * convenience function, rather than use this function directly. Try finding 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 202360e22..0ca018abb 100644 --- a/src/main/java/io/appium/java_client/InteractsWithApps.java +++ b/src/main/java/io/appium/java_client/InteractsWithApps.java @@ -16,39 +16,32 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.ACTIVATE_APP; -import static io.appium.java_client.MobileCommand.CLOSE_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.LAUNCH_APP; -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.RESET; -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; -public interface InteractsWithApps extends ExecutesMethod { +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; - /** - * Launches the app, which was provided in the capabilities at session creation, - * and (re)starts the session. - */ - default void launchApp() { - execute(LAUNCH_APP); - } +@SuppressWarnings({"rawtypes", "unchecked"}) +public interface InteractsWithApps extends ExecutesMethod, CanRememberExtensionPresence { /** * Install an app on the mobile device. @@ -63,16 +56,24 @@ default void installApp(String appPath) { * Install an app on the mobile device. * * @param appPath path to app to install or a remote URL. - * @param options Set of the corresponding instllation options for + * @param options Set of the corresponding installation options for * 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)); + } } /** @@ -82,27 +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)) + ) + ); + } } /** - * Resets the currently running app together with the session. - */ - default void resetApp() { - execute(RESET); - } - - /** - * 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)) + ); + } } /** @@ -124,20 +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))); - } - - /** - * Close the app which was provided in the capabilities at session creation - * and quits the session. - */ - default void closeApp() { - execute(CLOSE_APP); + 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) + ) + ); + } } /** @@ -159,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)); + } } /** @@ -174,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)) + ) + ) + ); + } } /** @@ -197,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/InteractsWithFiles.java b/src/main/java/io/appium/java_client/InteractsWithFiles.java deleted file mode 100644 index d8b6af05a..000000000 --- a/src/main/java/io/appium/java_client/InteractsWithFiles.java +++ /dev/null @@ -1,83 +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.PULL_FILE; -import static io.appium.java_client.MobileCommand.PULL_FOLDER; - -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.remote.Response; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -public interface InteractsWithFiles extends ExecutesMethod { - - /** - * Pull a file from the simulator/device. - * On iOS the server should have ifuse - * libraries installed and configured properly for this feature to work - * on real devices. - * On Android the application under test should be - * built with debuggable flag enabled in order to get access to its container - * on the internal file system. - * - * @see iFuse GitHub page6 - * @see osxFuse FAQ - * @see 'Debug Your App' developer article - * - * @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). - * @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(); - - return Base64.getDecoder().decode(base64String.getBytes(StandardCharsets.UTF_8)); - } - - /** - * Pull a folder content from the simulator/device. - * On iOS the server should have ifuse - * libraries installed and configured properly for this feature to work - * on real devices. - * On Android the application under test should be - * built with debuggable flag enabled in order to get access to its container - * on the internal file system. - * - * @see iFuse GitHub page6 - * @see osxFuse FAQ - * @see 'Debug Your App' developer article - * - * @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). - * @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(); - - return Base64.getDecoder().decode(base64String.getBytes(StandardCharsets.UTF_8)); - } - -} 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 b8fd3d5db..000000000 --- a/src/main/java/io/appium/java_client/MobileBy.java +++ /dev/null @@ -1,772 +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 lombok.AccessLevel; -import lombok.Getter; -import org.apache.commons.lang3.StringUtils; -import org.openqa.selenium.By; -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.io.Serializable; -import java.util.List; - -@SuppressWarnings("serial") -public abstract class MobileBy extends By { - - private static final String ERROR_TEXT = "The class %s of the given context " - + "doesn't implement %s nor %s. Sorry. It is impossible to find something."; - - @Getter(AccessLevel.PROTECTED) private final String locatorString; - private final MobileSelector selector; - - private static IllegalArgumentException formIllegalArgumentException(Class givenClass, - Class class1, Class class2) { - return new IllegalArgumentException(String.format(ERROR_TEXT, givenClass.getCanonicalName(), - class1.getCanonicalName(), class2.getCanonicalName())); - } - - protected MobileBy(MobileSelector selector, String locatorString) { - if (StringUtils.isBlank(locatorString)) { - throw new IllegalArgumentException("Must supply a not empty locator value."); - } - this.locatorString = locatorString; - this.selector = selector; - } - - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - return (List) ((FindsByFluentSelector) context) - .findElements(selector.toString(), getLocatorString()); - } - - @Override public WebElement findElement(SearchContext context) { - return ((FindsByFluentSelector) context) - .findElement(selector.toString(), getLocatorString()); - } - - /** - * Refer to https://developer.android.com/training/testing/ui-automator - * @param uiautomatorText is Android UIAutomator string - * @return an instance of {@link io.appium.java_client.MobileBy.ByAndroidUIAutomator} - */ - 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 - * @param accessibilityId id is a convenient UI automation accessibility Id. - * @return an instance of {@link io.appium.java_client.MobileBy.ByAndroidUIAutomator} - */ - public static By AccessibilityId(final String accessibilityId) { - return new ByAccessibilityId(accessibilityId); - } - - /** - * This locator strategy is available in XCUITest Driver mode. - * @param iOSClassChainString is a valid class chain locator string. - * See - * the documentation for more details - * @return an instance of {@link io.appium.java_client.MobileBy.ByIosClassChain} - */ - public static By iOSClassChain(final String iOSClassChainString) { - return new ByIosClassChain(iOSClassChainString); - } - - /** - * 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 - * @return an instance of {@link io.appium.java_client.MobileBy.ByAndroidDataMatcher} - */ - public static By androidDataMatcher(final String dataMatcherString) { - return new ByAndroidDataMatcher(dataMatcherString); - } - - /** - * 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 - * @return an instance of {@link io.appium.java_client.MobileBy.ByAndroidViewMatcher} - */ - public static By androidViewMatcher(final String viewMatcherString) { - return new ByAndroidViewMatcher(viewMatcherString); - } - - /** - * This locator strategy is available in XCUITest Driver mode. - * @param iOSNsPredicateString is an an iOS NsPredicate String - * @return an instance of {@link io.appium.java_client.MobileBy.ByIosNsPredicate} - */ - public static By iOSNsPredicateString(final String iOSNsPredicateString) { - return new ByIosNsPredicate(iOSNsPredicateString); - } - - public static By windowsAutomation(final String windowsAutomation) { - return new ByWindowsAutomation(windowsAutomation); - } - - /** - * This locator strategy is available in Espresso Driver mode. - * @since Appium 1.8.2 beta - * @param tag is an view tag string - * @return an instance of {@link ByAndroidViewTag} - */ - 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. - * - * @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} - */ - 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. - * - * @param selector selector to pass to the custom element finding plugin - * @return an instance of {@link ByCustom} - * @since Appium 1.9.2 - */ - public static By custom(final String selector) { - return new ByCustom(selector); - } - - - public static class ByAndroidUIAutomator extends MobileBy implements Serializable { - - - public ByAndroidUIAutomator(String uiautomatorText) { - super(MobileSelector.ANDROID_UI_AUTOMATOR, uiautomatorText); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override - public List findElements(SearchContext context) throws WebDriverException, - IllegalArgumentException { - Class contextClass = context.getClass(); - - if (FindsByAndroidUIAutomator.class.isAssignableFrom(contextClass)) { - return FindsByAndroidUIAutomator.class.cast(context) - .findElementsByAndroidUIAutomator(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidUIAutomator.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) throws WebDriverException, - IllegalArgumentException { - Class contextClass = context.getClass(); - - if (FindsByAndroidUIAutomator.class.isAssignableFrom(contextClass)) { - return FindsByAndroidUIAutomator.class.cast(context) - .findElementByAndroidUIAutomator(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidUIAutomator.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.AndroidUIAutomator: " + getLocatorString(); - } - } - - - public static class ByAccessibilityId extends MobileBy implements Serializable { - - public ByAccessibilityId(String accessibilityId) { - super(MobileSelector.ACCESSIBILITY, accessibilityId); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override - public List findElements(SearchContext context) throws WebDriverException, - IllegalArgumentException { - Class contextClass = context.getClass(); - - if (FindsByAccessibilityId.class.isAssignableFrom(contextClass)) { - return FindsByAccessibilityId.class.cast(context) - .findElementsByAccessibilityId(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAccessibilityId.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) throws WebDriverException, - IllegalArgumentException { - Class contextClass = context.getClass(); - - if (FindsByAccessibilityId.class.isAssignableFrom(contextClass)) { - return FindsByAccessibilityId.class.cast(context) - .findElementByAccessibilityId(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAccessibilityId.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.AccessibilityId: " + getLocatorString(); - } - } - - public static class ByIosClassChain extends MobileBy implements Serializable { - - protected ByIosClassChain(String locatorString) { - super(MobileSelector.IOS_CLASS_CHAIN, locatorString); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByIosClassChain.class.isAssignableFrom(contextClass)) { - return FindsByIosClassChain.class.cast(context) - .findElementsByIosClassChain(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosClassChain.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByIosClassChain.class.isAssignableFrom(contextClass)) { - return FindsByIosClassChain.class.cast(context) - .findElementByIosClassChain(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosClassChain.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.IosClassChain: " + getLocatorString(); - } - } - - public static class ByAndroidDataMatcher extends MobileBy implements Serializable { - - protected ByAndroidDataMatcher(String locatorString) { - super(MobileSelector.ANDROID_DATA_MATCHER, locatorString); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByAndroidDataMatcher.class.isAssignableFrom(contextClass)) { - return FindsByAndroidDataMatcher.class.cast(context) - .findElementsByAndroidDataMatcher(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidDataMatcher.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByAndroidDataMatcher.class.isAssignableFrom(contextClass)) { - return FindsByAndroidDataMatcher.class.cast(context) - .findElementByAndroidDataMatcher(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidDataMatcher.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.FindsByAndroidDataMatcher: " + getLocatorString(); - } - } - - public static class ByAndroidViewMatcher extends MobileBy implements Serializable { - - protected ByAndroidViewMatcher(String locatorString) { - super(MobileSelector.ANDROID_VIEW_MATCHER, locatorString); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByAndroidViewMatcher.class.isAssignableFrom(contextClass)) { - return FindsByAndroidViewMatcher.class.cast(context) - .findElementsByAndroidViewMatcher(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidViewMatcher.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByAndroidViewMatcher.class.isAssignableFrom(contextClass)) { - return FindsByAndroidViewMatcher.class.cast(context) - .findElementByAndroidViewMatcher(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidViewMatcher.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.FindsByAndroidViewMatcher: " + getLocatorString(); - } - } - - public static class ByIosNsPredicate extends MobileBy implements Serializable { - - protected ByIosNsPredicate(String locatorString) { - super(MobileSelector.IOS_PREDICATE_STRING, locatorString); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByIosNSPredicate.class.isAssignableFrom(contextClass)) { - return FindsByIosNSPredicate.class.cast(context) - .findElementsByIosNsPredicate(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosNSPredicate.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByIosNSPredicate.class.isAssignableFrom(contextClass)) { - return FindsByIosNSPredicate.class.cast(context) - .findElementByIosNsPredicate(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosNSPredicate.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.IosNsPredicate: " + getLocatorString(); - } - } - - public static class ByWindowsAutomation extends MobileBy implements Serializable { - - protected ByWindowsAutomation(String locatorString) { - super(MobileSelector.WINDOWS_UI_AUTOMATION, locatorString); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByWindowsAutomation.class.isAssignableFrom(contextClass)) { - return FindsByWindowsAutomation.class.cast(context) - .findElementsByWindowsUIAutomation(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByWindowsAutomation.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByWindowsAutomation.class.isAssignableFrom(contextClass)) { - return FindsByWindowsAutomation.class.cast(context) - .findElementByWindowsUIAutomation(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosNSPredicate.class, - FindsByWindowsAutomation.class); - } - } - - public static class ByImage extends MobileBy implements Serializable { - - protected ByImage(String b64Template) { - super(MobileSelector.IMAGE, b64Template); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByImage.class.isAssignableFrom(contextClass)) { - return FindsByImage.class.cast(context).findElementsByImage(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByImage.class, FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByImage.class.isAssignableFrom(contextClass)) { - return FindsByImage.class.cast(context).findElementByImage(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByImage.class, FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.Image: " + getLocatorString(); - } - } - - public static class ByCustom extends MobileBy implements Serializable { - - protected ByCustom(String selector) { - super(MobileSelector.CUSTOM, selector); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override public List findElements(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByCustom.class.isAssignableFrom(contextClass)) { - return FindsByCustom.class.cast(context).findElementsByCustom(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByCustom.class, FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) { - Class contextClass = context.getClass(); - - if (FindsByCustom.class.isAssignableFrom(contextClass)) { - return FindsByCustom.class.cast(context).findElementByCustom(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByCustom.class, FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.Custom: " + getLocatorString(); - } - } - - public static class ByAndroidViewTag extends MobileBy implements Serializable { - - public ByAndroidViewTag(String tag) { - super(MobileSelector.ANDROID_VIEWTAG, tag); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @SuppressWarnings("unchecked") - @Override - public List findElements(SearchContext context) throws WebDriverException, - IllegalArgumentException { - Class contextClass = context.getClass(); - - if (FindsByAndroidViewTag.class.isAssignableFrom(contextClass)) { - return FindsByAndroidViewTag.class.cast(context) - .findElementsByAndroidViewTag(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidViewTag.class, - FindsByFluentSelector.class); - } - - /** - * {@inheritDoc} - * - * @throws WebDriverException when current session doesn't support the given selector or when - * value of the selector is not consistent. - * @throws IllegalArgumentException when it is impossible to find something on the given - * {@link SearchContext} instance - */ - @Override public WebElement findElement(SearchContext context) throws WebDriverException, - IllegalArgumentException { - Class contextClass = context.getClass(); - - if (FindsByAndroidViewTag.class.isAssignableFrom(contextClass)) { - return FindsByAndroidViewTag.class.cast(context) - .findElementByAndroidViewTag(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(contextClass)) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidViewTag.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.AndroidViewTag: " + getLocatorString(); - } - } -} - - diff --git a/src/main/java/io/appium/java_client/MobileCommand.java b/src/main/java/io/appium/java_client/MobileCommand.java index 58a3b6098..b4df90047 100644 --- a/src/main/java/io/appium/java_client/MobileCommand.java +++ b/src/main/java/io/appium/java_client/MobileCommand.java @@ -17,103 +17,179 @@ 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; - protected static final String SET_VALUE; + @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; - protected static final String LAUNCH_APP; - protected static final String CLOSE_APP; + @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; - protected static final String REPLACE_VALUE; + @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; @@ -192,6 +268,7 @@ public class MobileCommand { COMPARE_IMAGES = "compareImages"; EXECUTE_DRIVER_SCRIPT = "executeDriverScript"; GET_ALLSESSION = "getAllSessions"; + EXECUTE_GOOGLE_CDP_COMMAND = "executeCdp"; commandRepository = new HashMap<>(); commandRepository.put(RESET, postC("/session/:sessionId/appium/app/reset")); @@ -282,6 +359,19 @@ public class MobileCommand { commandRepository.put(COMPARE_IMAGES, postC("/session/:sessionId/appium/compare_images")); 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")); } /** @@ -315,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 + )); } /** @@ -352,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(); @@ -366,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]); } } @@ -379,136 +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 new AbstractMap.SimpleEntry<>(SET_SETTINGS, prepareArguments("settings", - prepareArguments(setting, value))); + return setSettingsCommand(Map.of(setting, value)); + } + + public static Map.Entry> setSettingsCommand(Map 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 @@ -519,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/MobileDriver.java b/src/main/java/io/appium/java_client/MobileDriver.java deleted file mode 100644 index e982d5419..000000000 --- a/src/main/java/io/appium/java_client/MobileDriver.java +++ /dev/null @@ -1,76 +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 org.openqa.selenium.By; -import org.openqa.selenium.ContextAware; -import org.openqa.selenium.Rotatable; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.html5.LocationContext; -import org.openqa.selenium.internal.FindsByClassName; -import org.openqa.selenium.internal.FindsByCssSelector; -import org.openqa.selenium.internal.FindsById; -import org.openqa.selenium.internal.FindsByLinkText; -import org.openqa.selenium.internal.FindsByName; -import org.openqa.selenium.internal.FindsByTagName; -import org.openqa.selenium.internal.FindsByXPath; - -import java.util.List; - -public interface MobileDriver extends WebDriver, PerformsTouchActions, ContextAware, Rotatable, - FindsByAccessibilityId, LocationContext, HidesKeyboard, HasDeviceTime, - InteractsWithFiles, InteractsWithApps, HasAppStrings, FindsByClassName, FindsByCssSelector, FindsById, - FindsByLinkText, FindsByName, FindsByTagName, FindsByXPath, FindsByFluentSelector, ExecutesMethod, - HasSessionDetails { - - List findElements(By by); - - T findElement(By by); - - T findElementByClassName(String className); - - List findElementsByClassName(String className); - - T findElementByCssSelector(String cssSelector); - - List findElementsByCssSelector(String cssSelector); - - T findElementById(String id); - - List findElementsById(String id); - - T findElementByLinkText(String linkText); - - List findElementsByLinkText(String linkText); - - T findElementByPartialLinkText(String partialLinkText); - - List findElementsByPartialLinkText(String partialLinkText); - - T findElementByName(String name); - - List findElementsByName(String name); - - T findElementByTagName(String tagName); - - List findElementsByTagName(String tagName); - - T findElementByXPath(String xPath); - - List findElementsByXPath(String xPath); -} diff --git a/src/main/java/io/appium/java_client/MobileElement.java b/src/main/java/io/appium/java_client/MobileElement.java deleted file mode 100644 index a8decf61d..000000000 --- a/src/main/java/io/appium/java_client/MobileElement.java +++ /dev/null @@ -1,99 +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 com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.By; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.Point; -import org.openqa.selenium.remote.FileDetector; - -import java.util.List; - -@SuppressWarnings({"unchecked"}) -public abstract class MobileElement - extends DefaultGenericMobileElement { - - protected FileDetector fileDetector; - - /** - * Method returns central coordinates of an element. - * @return The instance of the {@link org.openqa.selenium.Point} - */ - public Point getCenter() { - Point upperLeft = this.getLocation(); - Dimension dimensions = this.getSize(); - return new Point(upperLeft.getX() + dimensions.getWidth() / 2, - upperLeft.getY() + dimensions.getHeight() / 2); - } - - @Override public List findElements(By by) { - return super.findElements(by); - } - - @Override public List findElements(String by, String using) { - return super.findElements(by, using); - } - - @Override public List findElementsById(String id) { - return super.findElementsById(id); - } - - public List findElementsByLinkText(String using) { - return super.findElementsByLinkText(using); - } - - public List findElementsByPartialLinkText(String using) { - return super.findElementsByPartialLinkText(using); - } - - public List findElementsByTagName(String using) { - return super.findElementsByTagName(using); - } - - public List findElementsByName(String using) { - return super.findElementsByName(using); - } - - public List findElementsByClassName(String using) { - return super.findElementsByClassName(using); - } - - public List findElementsByCssSelector(String using) { - return super.findElementsByCssSelector(using); - } - - public List findElementsByXPath(String using) { - return super.findElementsByXPath(using); - } - - @Override public List findElementsByAccessibilityId(String using) { - return super.findElementsByAccessibilityId(using); - } - - /** - * This method sets the new value of the attribute "value". - * - * @param value is the new value which should be set - */ - public void setValue(String value) { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("id", id).put("value", value); - execute(MobileCommand.SET_VALUE, builder.build()); - } -} diff --git a/src/main/java/io/appium/java_client/MobileSelector.java b/src/main/java/io/appium/java_client/MobileSelector.java deleted file mode 100644 index 0fbe3284e..000000000 --- a/src/main/java/io/appium/java_client/MobileSelector.java +++ /dev/null @@ -1,41 +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; - -public enum MobileSelector { - ACCESSIBILITY("accessibility id"), - ANDROID_UI_AUTOMATOR("-android uiautomator"), - IOS_UI_AUTOMATION("-ios uiautomation"), - IOS_PREDICATE_STRING("-ios predicate string"), - IOS_CLASS_CHAIN("-ios class chain"), - WINDOWS_UI_AUTOMATION("-windows uiautomation"), - IMAGE("-image"), - ANDROID_VIEWTAG("-android viewtag"), - ANDROID_DATA_MATCHER("-android datamatcher"), - ANDROID_VIEW_MATCHER("-android viewmatcher"), - CUSTOM("-custom"); - - private final String selector; - - MobileSelector(String selector) { - this.selector = selector; - } - - @Override public String toString() { - return selector; - } -} diff --git a/src/main/java/io/appium/java_client/MultiTouchAction.java b/src/main/java/io/appium/java_client/MultiTouchAction.java index c7bdd6525..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 @@ -41,15 +39,25 @@ * "execution group", so these can be used to sync up complex actions. * Calling perform() sends the action command to the Mobile Driver. Otherwise, more and * more actions can be chained. + * + * @deprecated Touch actions are deprecated. + * Please use W3C Actions instead or the corresponding + * extension methods for the driver (if available). + * Check + * - https://www.youtube.com/watch?v=oAJ7jwMNFVU + * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * for more details. */ +@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<>(); } /** @@ -67,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()) + ); } /** @@ -91,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 fae9c2403..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,24 @@ 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 + * extension methods for the driver (if available). + * Check + * - https://www.youtube.com/watch?v=oAJ7jwMNFVU + * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * for more details. + */ +@Deprecated +@SuppressWarnings({"unchecked", "rawtypes"}) public interface PerformsTouchActions extends ExecutesMethod { /** * Performs a chain of touch actions, which together can be considered an @@ -36,6 +48,7 @@ public interface PerformsTouchActions extends ExecutesMethod { * touch actions to perform * @return the same touch action object */ + @Deprecated default TouchAction performTouchAction(TouchAction touchAction) { Map> parameters = touchAction.getParameters(); execute(PERFORM_TOUCH_ACTION, parameters); @@ -52,10 +65,12 @@ default TouchAction performTouchAction(TouchAction touchAction) { * is called. * * @param multiAction the MultiTouchAction object to perform. + * @return MultiTouchAction instance for chaining. */ - default void performMultiTouchAction(MultiTouchAction multiAction) { + @Deprecated + default MultiTouchAction performMultiTouchAction(MultiTouchAction multiAction) { Map> parameters = multiAction.getParameters(); execute(PERFORM_MULTI_TOUCH, parameters); - multiAction.clearActions(); + return multiAction.clearActions(); } } diff --git a/src/main/java/io/appium/java_client/PullsFiles.java b/src/main/java/io/appium/java_client/PullsFiles.java new file mode 100644 index 000000000..3c1e7ccff --- /dev/null +++ b/src/main/java/io/appium/java_client/PullsFiles.java @@ -0,0 +1,95 @@ +/* + * 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 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, CanRememberExtensionPresence { + + /** + * Pull a file from the remote system. + * On Android the application under test should be + * built with debuggable flag enabled in order to get access to its container + * on the internal file system. + * + * @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) { + 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)); + } + + /** + * Pull a folder content from the remote system. + * On Android the application under test should be + * built with debuggable flag enabled in order to get access to its container + * on the internal file system. + * + * @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) { + 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 new file mode 100644 index 000000000..a30bf0d3b --- /dev/null +++ b/src/main/java/io/appium/java_client/PushesFiles.java @@ -0,0 +1,65 @@ +/* + * 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 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, CanRememberExtensionPresence { + + /** + * Saves base64-encoded data as a file on the remote system. + * + * @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) { + 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)); + } + } + + /** + * 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 on current file system + */ + default void pushFile(String remotePath, File file) throws IOException { + 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 90197b4ce..cc1f44cd1 100644 --- a/src/main/java/io/appium/java_client/Setting.java +++ b/src/main/java/io/appium/java_client/Setting.java @@ -44,6 +44,7 @@ public enum Setting { MJPEG_SCALING_FACTOR("mjpegScalingFactor"), KEYBOARD_AUTOCORRECTION("keyboardAutocorrection"), KEYBOARD_PREDICTION("keyboardPrediction"), + BOUND_ELEMENTS_BY_INDEX("boundElementsByIndex"), // Android and iOS SHOULD_USE_COMPACT_RESPONSES("shouldUseCompactResponses"), ELEMENT_RESPONSE_ATTRIBUTES("elementResponseAttributes"), @@ -64,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/TouchAction.java b/src/main/java/io/appium/java_client/TouchAction.java index 63b889b20..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 @@ -42,15 +41,25 @@ * action.press(element).waitAction(300).moveTo(element1).release().perform(); * Calling perform() sends the action command to the Mobile Driver. Otherwise, * more and more actions can be chained. + * + * @deprecated Touch actions are deprecated. + * Please use W3C Actions instead or the corresponding + * extension methods for the driver (if available). + * Check + * - https://www.youtube.com/watch?v=oAJ7jwMNFVU + * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * for more details. */ +@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<>(); } /** @@ -60,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; } @@ -71,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; } @@ -89,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; } @@ -101,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; } @@ -113,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; } @@ -124,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; } @@ -137,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; } @@ -150,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; } @@ -163,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; } @@ -173,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(); } @@ -195,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()) + ); } /** @@ -206,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; } @@ -215,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 da957d746..34821f8d4 100644 --- a/src/main/java/io/appium/java_client/android/Activity.java +++ b/src/main/java/io/appium/java_client/android/Activity.java @@ -2,10 +2,9 @@ import lombok.Data; import lombok.experimental.Accessors; -import okhttp3.Interceptor; 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}. @@ -30,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 9fc18bd51..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,60 +16,73 @@ 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.FindsByAndroidDataMatcher; -import io.appium.java_client.FindsByAndroidViewMatcher; -import io.appium.java_client.FindsByAndroidUIAutomator; -import io.appium.java_client.FindsByAndroidViewTag; +import io.appium.java_client.ExecuteCDPCommand; +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.InteractsWithApps; 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.android.connection.HasNetworkConnection; +import io.appium.java_client.android.geolocation.SupportsExtendedGeolocationCommands; import io.appium.java_client.android.nativekey.PressesKey; import io.appium.java_client.battery.HasBattery; -import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.remote.SupportsContextSwitching; +import io.appium.java_client.remote.SupportsLocation; +import io.appium.java_client.remote.SupportsRotation; import io.appium.java_client.screenrecording.CanRecordScreen; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; import io.appium.java_client.ws.StringWebSocketClient; import org.openqa.selenium.Capabilities; -import org.openqa.selenium.WebElement; +import org.openqa.selenium.Platform; import org.openqa.selenium.remote.HttpCommandExecutor; +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. - * - * @param the required type of class which implement {@link org.openqa.selenium.WebElement}. - * Instances of the defined type will be returned via findElement* and findElements*. - * Warning (!!!). Allowed types: - * {@link org.openqa.selenium.WebElement} - * {@link org.openqa.selenium.remote.RemoteWebElement} - * {@link io.appium.java_client.MobileElement} - * {@link io.appium.java_client.android.AndroidElement} */ -public class AndroidDriver - extends AppiumDriver - implements PressesKey, HasNetworkConnection, PushesFiles, StartsActivity, - FindsByAndroidUIAutomator, FindsByAndroidViewTag, FindsByAndroidDataMatcher, - FindsByAndroidViewMatcher, LocksDevice, HasAndroidSettings, HasAndroidDeviceDetails, - HasSupportedPerformanceDataType, AuthenticatesByFinger, HasOnScreenKeyboard, - CanRecordScreen, SupportsSpecialEmulatorCommands, - SupportsNetworkStateManagement, ListensToLogcatMessages, HasAndroidClipboard, - HasBattery { - - private static final String ANDROID_PLATFORM = MobilePlatform.ANDROID; +public class AndroidDriver extends AppiumDriver implements + PressesKey, + SupportsRotation, + SupportsContextSwitching, + SupportsLocation, + PerformsTouchActions, + HidesKeyboard, + HasDeviceTime, + PullsFiles, + InteractsWithApps, + HasAppStrings, + HasNetworkConnection, + PushesFiles, + StartsActivity, + LocksDevice, + HasAndroidSettings, + HasAndroidDeviceDetails, + HasSupportedPerformanceDataType, + AuthenticatesByFinger, + HasOnScreenKeyboard, + CanRecordScreen, + SupportsSpecialEmulatorCommands, + SupportsNetworkStateManagement, + ListensToLogcatMessages, + HasAndroidClipboard, + HasBattery, + ExecuteCDPCommand, + CanReplaceElementValue, + SupportsGpsStateManagement, + HasNotifications, + SupportsExtendedGeolocationCommands { + private static final String ANDROID_PLATFORM = Platform.ANDROID.name(); private StringWebSocketClient logcatClient; @@ -82,17 +95,17 @@ public class AndroidDriver * @param capabilities take a look at {@link Capabilities} */ public AndroidDriver(HttpCommandExecutor executor, Capabilities capabilities) { - super(executor, updateDefaultPlatformName(capabilities, ANDROID_PLATFORM)); + super(executor, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** * Creates a new instance based on Appium server URL and {@code capabilities}. * * @param remoteAddress is the address of remotely/locally started Appium server - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(URL remoteAddress, Capabilities desiredCapabilities) { - super(remoteAddress, updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** @@ -100,22 +113,21 @@ public AndroidDriver(URL remoteAddress, Capabilities desiredCapabilities) { * * @param remoteAddress is the address of remotely/locally started Appium server * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(remoteAddress, httpClientFactory, - updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver( + URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** * Creates a new instance based on Appium driver local service and {@code capabilities}. * * @param service take a look at {@link AppiumDriverLocalService} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(AppiumDriverLocalService service, Capabilities desiredCapabilities) { - super(service, updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** @@ -123,22 +135,21 @@ public AndroidDriver(AppiumDriverLocalService service, Capabilities desiredCapab * * @param service take a look at {@link AppiumDriverLocalService} * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(service, httpClientFactory, - updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver( + AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** * Creates a new instance based on Appium service builder and {@code capabilities}. * * @param builder take a look at {@link AppiumServiceBuilder} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) { - super(builder, updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** @@ -146,65 +157,103 @@ public AndroidDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilit * * @param builder take a look at {@link AppiumServiceBuilder} * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ public AndroidDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(builder, httpClientFactory, - updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + Capabilities capabilities) { + super(builder, httpClientFactory, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** * Creates a new instance based on HTTP client factory and {@code capabilities}. * * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(HttpClient.Factory httpClientFactory, Capabilities desiredCapabilities) { - super(httpClientFactory, updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, 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: + * + *
+     *
+     * ClientConfig clientConfig = ClientConfig.defaultConfig()
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * UiAutomator2Options options = new UiAutomator2Options();
+     * AndroidDriver driver = new AndroidDriver(clientConfig, options);
+     *
+     * 
+ * + * @param clientConfig take a look at {@link ClientConfig} + * @param capabilities take a look at {@link Capabilities} * - * @param desiredCapabilities take a look at {@link Capabilities} */ - public AndroidDriver(Capabilities desiredCapabilities) { - super(updateDefaultPlatformName(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(ClientConfig clientConfig, Capabilities capabilities) { + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformName(capabilities, + ANDROID_PLATFORM)); } /** - * Get test-coverage data. + * 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} * - * @param intent intent to broadcast. - * @param path path to .ec file. */ - public void endTestCoverage(String intent, String path) { - CommandExecutionHelper.execute(this, endTestCoverageCommand(intent, path)); + public AndroidDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * Open the notification shade, on Android devices. + * Creates a new instance based on {@code capabilities}. + * + * @param capabilities take a look at {@link Capabilities} */ - public void openNotifications() { - CommandExecutionHelper.execute(this, openNotificationsCommand()); + public AndroidDriver(Capabilities capabilities) { + super(ensurePlatformName(capabilities, ANDROID_PLATFORM)); } - public void toggleLocationServices() { - CommandExecutionHelper.execute(this, toggleLocationServicesCommand()); + /** + * 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 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()); + 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/AndroidElement.java b/src/main/java/io/appium/java_client/android/AndroidElement.java deleted file mode 100644 index 95899e4db..000000000 --- a/src/main/java/io/appium/java_client/android/AndroidElement.java +++ /dev/null @@ -1,38 +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 io.appium.java_client.android.AndroidMobileCommandHelper.replaceElementValueCommand; - -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.FindsByAndroidDataMatcher; -import io.appium.java_client.FindsByAndroidViewMatcher; -import io.appium.java_client.FindsByAndroidUIAutomator; -import io.appium.java_client.FindsByAndroidViewTag; -import io.appium.java_client.MobileElement; - -public class AndroidElement extends MobileElement - implements FindsByAndroidUIAutomator, FindsByAndroidDataMatcher, - FindsByAndroidViewMatcher, FindsByAndroidViewTag { - /** - * This method replace current text value. - * @param value a new value - */ - public void replaceValue(String value) { - CommandExecutionHelper.execute(this, replaceElementValueCommand(this, value)); - } -} 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/AndroidTouchAction.java b/src/main/java/io/appium/java_client/android/AndroidTouchAction.java index e1af310df..0fa31cffa 100644 --- a/src/main/java/io/appium/java_client/android/AndroidTouchAction.java +++ b/src/main/java/io/appium/java_client/android/AndroidTouchAction.java @@ -20,6 +20,19 @@ import io.appium.java_client.TouchAction; +/** + * Android-specific touch action. + * + * @deprecated Touch actions are deprecated. + * Please use W3C Actions instead or the corresponding + * extension methods for the driver (if available). + * Check + * - https://www.youtube.com/watch?v=oAJ7jwMNFVU + * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * for more details. + */ +@Deprecated public class AndroidTouchAction extends TouchAction { public AndroidTouchAction(PerformsTouchActions performsTouchActions) { 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 new file mode 100644 index 000000000..3c42f5c35 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java @@ -0,0 +1,42 @@ +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 io.appium.java_client.MobileCommand; +import org.openqa.selenium.UnsupportedCommandException; +import org.openqa.selenium.remote.RemoteWebElement; + +import java.util.Map; + +public interface CanReplaceElementValue extends ExecutesMethod, CanRememberExtensionPresence { + /** + * Sends a text to the given element by replacing its previous content. + * + * @param element The destination element. + * @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) { + 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/HasAndroidSettings.java b/src/main/java/io/appium/java_client/android/HasAndroidSettings.java index 0c0e44f34..73900e050 100644 --- a/src/main/java/io/appium/java_client/android/HasAndroidSettings.java +++ b/src/main/java/io/appium/java_client/android/HasAndroidSettings.java @@ -21,7 +21,7 @@ import java.time.Duration; -interface HasAndroidSettings extends HasSettings { +public interface HasAndroidSettings extends HasSettings { /** * Set the `ignoreUnimportantViews` setting. *Android-only method*. * Sets whether Android devices should use `setCompressedLayoutHeirarchy()` 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/PushesFiles.java b/src/main/java/io/appium/java_client/android/PushesFiles.java deleted file mode 100644 index 6fdd73136..000000000 --- a/src/main/java/io/appium/java_client/android/PushesFiles.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 com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.MobileCommand.pushFileCommand; - -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; -import io.appium.java_client.InteractsWithFiles; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.io.IOException; - -public interface PushesFiles extends InteractsWithFiles, ExecutesMethod { - - /** - * Saves base64 encoded data as a file on the remote mobile device. - * The application under test should be - * built with debuggable flag enabled in order to get access to its container - * on the internal file system. - * - * @see 'Debug Your App' developer article - * - * @param remotePath Path to file to write data to on remote device - * If the path starts with @applicationId/ prefix, then the file - * will be pushed to the root of the corresponding application container. - * @param base64Data Base64 encoded byte array of data to write to remote device - */ - default void pushFile(String remotePath, byte[] base64Data) { - CommandExecutionHelper.execute(this, pushFileCommand(remotePath, base64Data)); - } - - /** - * Saves the given file to the remote mobile device. - * The application under test should be - * built with debugMode flag enabled in order to get access to its container - * on the internal file system. - * - * @see 'Debug Your App' developer article - * - * @param remotePath Path to file to write data to on remote device. - * If the path starts with @applicationId/ prefix, then the file - * will be pushed to the root of the corresponding application container. - * @param file is a file to write to remote device - * @throws IOException when there are problems with a file or 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("The given file " - + file.getAbsolutePath() + " doesn't exist"); - } - pushFile(remotePath, - Base64.encodeBase64(FileUtils.readFileToByteArray(file))); - } - -} 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 new file mode 100644 index 000000000..37d878642 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/geolocation/AndroidGeoLocation.java @@ -0,0 +1,125 @@ +/* + * 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.geolocation; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Optional.ofNullable; + +public class AndroidGeoLocation { + private Double longitude; + private Double latitude; + private Double altitude; + private Integer satellites; + private Double speed; + + /** + * Initializes AndroidLocation instance. + */ + public AndroidGeoLocation() { + + } + + /** + * Initializes AndroidLocation instance with longitude and latitude values. + * + * @param latitude latitude value + * @param longitude longitude value + */ + public AndroidGeoLocation(double latitude, double longitude) { + this.longitude = longitude; + this.latitude = latitude; + } + + /** + * Sets geo longitude value. This value is required to set. + * + * @param longitude geo longitude + * @return self instance for chaining + */ + public AndroidGeoLocation withLongitude(double longitude) { + this.longitude = longitude; + return this; + } + + /** + * Sets geo latitude value. This value is required to set. + * + * @param latitude geo latitude + * @return self instance for chaining + */ + public AndroidGeoLocation withLatitude(double latitude) { + this.latitude = latitude; + return this; + } + + /** + * Sets geo altitude value. + * + * @param altitude geo altitude + * @return self instance for chaining + */ + public AndroidGeoLocation withAltitude(double altitude) { + this.altitude = altitude; + return this; + } + + /** + * Sets the number of geo satellites being tracked. + * This number is respected on Emulators. + * + * @param satellites the count of satellites in range 1..12 + * @return self instance for chaining + */ + public AndroidGeoLocation withSatellites(int satellites) { + this.satellites = satellites; + return this; + } + + /** + * Sets the movement speed. It is measured in meters/second + * for real devices and in knots for emulators. + * + * @param speed the actual speed, which should be greater than zero + * @return self instance for chaining + */ + public AndroidGeoLocation withSpeed(double speed) { + this.speed = speed; + return this; + } + + /** + * Builds parameters map suitable for passing to the downstream API. + * + * @return Parameters mapping + */ + public Map 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 new file mode 100644 index 000000000..0472a5bab --- /dev/null +++ b/src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.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.android.geolocation; + +import io.appium.java_client.CommandExecutionHelper; +import io.appium.java_client.ExecutesMethod; +import io.appium.java_client.MobileCommand; + +import java.util.Map; + +public interface SupportsExtendedGeolocationCommands extends ExecutesMethod { + + /** + * Allows to set geo location with extended parameters + * available for Android platform. + * + * @param location The location object to set. + */ + default void setLocation(AndroidGeoLocation location) { + 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 new file mode 100644 index 000000000..da14a620e --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/EspressoOptions.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.android.options; + +import io.appium.java_client.android.options.adb.SupportsAdbExecTimeoutOption; +import io.appium.java_client.android.options.adb.SupportsAdbPortOption; +import io.appium.java_client.android.options.adb.SupportsAllowDelayAdbOption; +import io.appium.java_client.android.options.adb.SupportsBuildToolsVersionOption; +import io.appium.java_client.android.options.adb.SupportsClearDeviceLogsOnStartOption; +import io.appium.java_client.android.options.adb.SupportsIgnoreHiddenApiPolicyErrorOption; +import io.appium.java_client.android.options.adb.SupportsLogcatFilterSpecsOption; +import io.appium.java_client.android.options.adb.SupportsLogcatFormatOption; +import io.appium.java_client.android.options.adb.SupportsMockLocationAppOption; +import io.appium.java_client.android.options.adb.SupportsRemoteAdbHostOption; +import io.appium.java_client.android.options.adb.SupportsSkipLogcatCaptureOption; +import io.appium.java_client.android.options.adb.SupportsSuppressKillServerOption; +import io.appium.java_client.android.options.app.SupportsActivityOptionsOption; +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.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.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; +import io.appium.java_client.android.options.avd.SupportsAvdEnvOption; +import io.appium.java_client.android.options.avd.SupportsAvdLaunchTimeoutOption; +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.android.options.avd.SupportsNetworkSpeedOption; +import io.appium.java_client.android.options.context.SupportsAutoWebviewTimeoutOption; +import io.appium.java_client.android.options.context.SupportsChromeLoggingPrefsOption; +import io.appium.java_client.android.options.context.SupportsChromeOptionsOption; +import io.appium.java_client.android.options.context.SupportsChromedriverArgsOption; +import io.appium.java_client.android.options.context.SupportsChromedriverChromeMappingFileOption; +import io.appium.java_client.android.options.context.SupportsChromedriverDisableBuildCheckOption; +import io.appium.java_client.android.options.context.SupportsChromedriverExecutableDirOption; +import io.appium.java_client.android.options.context.SupportsChromedriverExecutableOption; +import io.appium.java_client.android.options.context.SupportsChromedriverPortOption; +import io.appium.java_client.android.options.context.SupportsChromedriverPortsOption; +import io.appium.java_client.android.options.context.SupportsChromedriverUseSystemExecutableOption; +import io.appium.java_client.android.options.context.SupportsEnsureWebviewsHavePagesOption; +import io.appium.java_client.android.options.context.SupportsExtractChromeAndroidPackageFromContextNameOption; +import io.appium.java_client.android.options.context.SupportsNativeWebScreenshotOption; +import io.appium.java_client.android.options.context.SupportsRecreateChromeDriverSessionsOption; +import io.appium.java_client.android.options.context.SupportsShowChromedriverLogOption; +import io.appium.java_client.android.options.context.SupportsWebviewDevtoolsPortOption; +import io.appium.java_client.android.options.localization.SupportsAppLocaleOption; +import io.appium.java_client.android.options.localization.SupportsLocaleScriptOption; +import io.appium.java_client.android.options.locking.SupportsSkipUnlockOption; +import io.appium.java_client.android.options.locking.SupportsUnlockKeyOption; +import io.appium.java_client.android.options.locking.SupportsUnlockStrategyOption; +import io.appium.java_client.android.options.locking.SupportsUnlockSuccessTimeoutOption; +import io.appium.java_client.android.options.locking.SupportsUnlockTypeOption; +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.android.options.server.SupportsEspressoBuildConfigOption; +import io.appium.java_client.android.options.server.SupportsEspressoServerLaunchTimeoutOption; +import io.appium.java_client.android.options.server.SupportsForceEspressoRebuildOption; +import io.appium.java_client.android.options.server.SupportsShowGradleLogOption; +import io.appium.java_client.android.options.server.SupportsSkipServerInstallationOption; +import io.appium.java_client.android.options.server.SupportsSystemPortOption; +import io.appium.java_client.android.options.signing.SupportsKeystoreOptions; +import io.appium.java_client.android.options.signing.SupportsNoSignOption; +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.remote.options.BaseOptions; +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; +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; + +/** + * 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 + SupportsDeviceNameOption, + SupportsUdidOption, + // Driver/Server options: https://github.com/appium/appium-uiautomator2-driver#driverserver + SupportsSystemPortOption, + SupportsSkipServerInstallationOption, + SupportsEspressoServerLaunchTimeoutOption, + SupportsForceEspressoRebuildOption, + SupportsShowGradleLogOption, + SupportsOrientationOption, + SupportsEspressoBuildConfigOption, + // App options: https://github.com/appium/appium-uiautomator2-driver#app + SupportsAppOption, + SupportsAppPackageOption, + SupportsAppActivityOption, + SupportsAppWaitActivityOption, + SupportsAppWaitPackageOption, + SupportsAppWaitDurationOption, + SupportsAndroidInstallTimeoutOption, + SupportsIntentOptionsOption, + SupportsActivityOptionsOption, + SupportsAutoGrantPermissionsOption, + SupportsOtherAppsOption, + SupportsUninstallOtherPackagesOption, + SupportsAllowTestPackagesOption, + SupportsRemoteAppsCacheLimitOption, + SupportsEnforceAppInstallOption, + // App localization options: https://github.com/appium/appium-uiautomator2-driver#app-localization + SupportsLocaleScriptOption, + SupportsLanguageOption, + SupportsLocaleOption, + SupportsAppLocaleOption, + // ADB options: https://github.com/appium/appium-uiautomator2-driver#adb + SupportsAdbPortOption, + SupportsRemoteAdbHostOption, + SupportsAdbExecTimeoutOption, + SupportsClearDeviceLogsOnStartOption, + SupportsBuildToolsVersionOption, + SupportsSkipLogcatCaptureOption, + SupportsSuppressKillServerOption, + SupportsIgnoreHiddenApiPolicyErrorOption, + SupportsMockLocationAppOption, + SupportsLogcatFormatOption, + SupportsLogcatFilterSpecsOption, + SupportsAllowDelayAdbOption, + // AVD options: https://github.com/appium/appium-uiautomator2-driver#emulator-android-virtual-device + SupportsAvdOption, + SupportsAvdLaunchTimeoutOption, + SupportsAvdReadyTimeoutOption, + SupportsAvdArgsOption, + SupportsAvdEnvOption, + SupportsNetworkSpeedOption, + SupportsGpsEnabledOption, + SupportsIsHeadlessOption, + // App signing options: https://github.com/appium/appium-uiautomator2-driver#app-signing + SupportsKeystoreOptions, + SupportsNoSignOption, + // Device locking options: https://github.com/appium/appium-uiautomator2-driver#device-locking + SupportsSkipUnlockOption, + SupportsUnlockTypeOption, + SupportsUnlockKeyOption, + SupportsUnlockStrategyOption, + SupportsUnlockSuccessTimeoutOption, + // MJPEG options: https://github.com/appium/appium-uiautomator2-driver#mjpeg + SupportsMjpegServerPortOption, + SupportsMjpegScreenshotUrlOption, + // Web Context options: https://github.com/appium/appium-uiautomator2-driver#web-context + SupportsAutoWebViewOption, + SupportsWebviewDevtoolsPortOption, + SupportsEnsureWebviewsHavePagesOption, + SupportsChromedriverPortOption, + SupportsChromedriverPortsOption, + SupportsChromedriverArgsOption, + SupportsChromedriverExecutableOption, + SupportsChromedriverExecutableDirOption, + SupportsChromedriverChromeMappingFileOption, + SupportsChromedriverUseSystemExecutableOption, + SupportsChromedriverDisableBuildCheckOption, + SupportsAutoWebviewTimeoutOption, + SupportsRecreateChromeDriverSessionsOption, + SupportsNativeWebScreenshotOption, + SupportsExtractChromeAndroidPackageFromContextNameOption, + SupportsShowChromedriverLogOption, + SupportsChromeOptionsOption, + SupportsChromeLoggingPrefsOption, + // Other options: https://github.com/appium/appium-uiautomator2-driver#other + SupportsDisableSuppressAccessibilityServiceOption, + SupportsSkipLogCaptureOption { + public EspressoOptions() { + setCommonOptions(); + } + + public EspressoOptions(Capabilities source) { + super(source); + setCommonOptions(); + } + + public EspressoOptions(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setPlatformName(MobilePlatform.ANDROID); + setAutomationName(AutomationName.ESPRESSO); + } +} 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 new file mode 100644 index 000000000..77115496f --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/UiAutomator2Options.java @@ -0,0 +1,228 @@ +/* + * 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.options; + +import io.appium.java_client.android.options.adb.SupportsAdbExecTimeoutOption; +import io.appium.java_client.android.options.adb.SupportsAdbPortOption; +import io.appium.java_client.android.options.adb.SupportsAllowDelayAdbOption; +import io.appium.java_client.android.options.adb.SupportsBuildToolsVersionOption; +import io.appium.java_client.android.options.adb.SupportsClearDeviceLogsOnStartOption; +import io.appium.java_client.android.options.adb.SupportsIgnoreHiddenApiPolicyErrorOption; +import io.appium.java_client.android.options.adb.SupportsLogcatFilterSpecsOption; +import io.appium.java_client.android.options.adb.SupportsLogcatFormatOption; +import io.appium.java_client.android.options.adb.SupportsMockLocationAppOption; +import io.appium.java_client.android.options.adb.SupportsRemoteAdbHostOption; +import io.appium.java_client.android.options.adb.SupportsSkipLogcatCaptureOption; +import io.appium.java_client.android.options.adb.SupportsSuppressKillServerOption; +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.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.SupportsIntentActionOption; +import io.appium.java_client.android.options.app.SupportsIntentCategoryOption; +import io.appium.java_client.android.options.app.SupportsIntentFlagsOption; +import io.appium.java_client.android.options.app.SupportsOptionalIntentArgumentsOption; +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; +import io.appium.java_client.android.options.avd.SupportsAvdEnvOption; +import io.appium.java_client.android.options.avd.SupportsAvdLaunchTimeoutOption; +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.android.options.avd.SupportsNetworkSpeedOption; +import io.appium.java_client.android.options.context.SupportsAutoWebviewTimeoutOption; +import io.appium.java_client.android.options.context.SupportsChromeLoggingPrefsOption; +import io.appium.java_client.android.options.context.SupportsChromeOptionsOption; +import io.appium.java_client.android.options.context.SupportsChromedriverArgsOption; +import io.appium.java_client.android.options.context.SupportsChromedriverChromeMappingFileOption; +import io.appium.java_client.android.options.context.SupportsChromedriverDisableBuildCheckOption; +import io.appium.java_client.android.options.context.SupportsChromedriverExecutableDirOption; +import io.appium.java_client.android.options.context.SupportsChromedriverExecutableOption; +import io.appium.java_client.android.options.context.SupportsChromedriverPortOption; +import io.appium.java_client.android.options.context.SupportsChromedriverPortsOption; +import io.appium.java_client.android.options.context.SupportsChromedriverUseSystemExecutableOption; +import io.appium.java_client.android.options.context.SupportsEnsureWebviewsHavePagesOption; +import io.appium.java_client.android.options.context.SupportsExtractChromeAndroidPackageFromContextNameOption; +import io.appium.java_client.android.options.context.SupportsNativeWebScreenshotOption; +import io.appium.java_client.android.options.context.SupportsRecreateChromeDriverSessionsOption; +import io.appium.java_client.android.options.context.SupportsShowChromedriverLogOption; +import io.appium.java_client.android.options.context.SupportsWebviewDevtoolsPortOption; +import io.appium.java_client.android.options.localization.SupportsLocaleScriptOption; +import io.appium.java_client.android.options.locking.SupportsSkipUnlockOption; +import io.appium.java_client.android.options.locking.SupportsUnlockKeyOption; +import io.appium.java_client.android.options.locking.SupportsUnlockStrategyOption; +import io.appium.java_client.android.options.locking.SupportsUnlockSuccessTimeoutOption; +import io.appium.java_client.android.options.locking.SupportsUnlockTypeOption; +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.android.options.other.SupportsUserProfileOption; +import io.appium.java_client.android.options.server.SupportsDisableWindowAnimationOption; +import io.appium.java_client.android.options.server.SupportsSkipDeviceInitializationOption; +import io.appium.java_client.android.options.server.SupportsSkipServerInstallationOption; +import io.appium.java_client.android.options.server.SupportsSystemPortOption; +import io.appium.java_client.android.options.server.SupportsUiautomator2ServerInstallTimeoutOption; +import io.appium.java_client.android.options.server.SupportsUiautomator2ServerLaunchTimeoutOption; +import io.appium.java_client.android.options.server.SupportsUiautomator2ServerReadTimeoutOption; +import io.appium.java_client.android.options.signing.SupportsKeystoreOptions; +import io.appium.java_client.android.options.signing.SupportsNoSignOption; +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsAppOption; +import io.appium.java_client.remote.options.SupportsAutoWebViewOption; +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; + +/** + * 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 + SupportsDeviceNameOption, + SupportsUdidOption, + // Driver/Server options: https://github.com/appium/appium-uiautomator2-driver#driverserver + SupportsSystemPortOption, + SupportsSkipServerInstallationOption, + SupportsUiautomator2ServerLaunchTimeoutOption, + SupportsUiautomator2ServerInstallTimeoutOption, + SupportsUiautomator2ServerReadTimeoutOption, + SupportsDisableWindowAnimationOption, + SupportsSkipDeviceInitializationOption, + SupportsOrientationOption, + SupportsClearSystemFilesOption, + SupportsEnablePerformanceLoggingOption, + // App options: https://github.com/appium/appium-uiautomator2-driver#app + SupportsAppOption, + SupportsAppPackageOption, + SupportsAppActivityOption, + SupportsAppWaitActivityOption, + SupportsAppWaitPackageOption, + SupportsAppWaitDurationOption, + SupportsAndroidInstallTimeoutOption, + SupportsAppWaitForLaunchOption, + SupportsIntentCategoryOption, + SupportsIntentActionOption, + SupportsIntentFlagsOption, + SupportsOptionalIntentArgumentsOption, + SupportsAutoGrantPermissionsOption, + SupportsOtherAppsOption, + SupportsUninstallOtherPackagesOption, + SupportsAllowTestPackagesOption, + SupportsRemoteAppsCacheLimitOption, + SupportsEnforceAppInstallOption, + // App localization options: https://github.com/appium/appium-uiautomator2-driver#app-localization + SupportsLocaleScriptOption, + SupportsLanguageOption, + SupportsLocaleOption, + // ADB options: https://github.com/appium/appium-uiautomator2-driver#adb + SupportsAdbPortOption, + SupportsRemoteAdbHostOption, + SupportsAdbExecTimeoutOption, + SupportsClearDeviceLogsOnStartOption, + SupportsBuildToolsVersionOption, + SupportsSkipLogcatCaptureOption, + SupportsSuppressKillServerOption, + SupportsIgnoreHiddenApiPolicyErrorOption, + SupportsMockLocationAppOption, + SupportsLogcatFormatOption, + SupportsLogcatFilterSpecsOption, + SupportsAllowDelayAdbOption, + // AVD options: https://github.com/appium/appium-uiautomator2-driver#emulator-android-virtual-device + SupportsAvdOption, + SupportsAvdLaunchTimeoutOption, + SupportsAvdReadyTimeoutOption, + SupportsAvdArgsOption, + SupportsAvdEnvOption, + SupportsNetworkSpeedOption, + SupportsGpsEnabledOption, + SupportsIsHeadlessOption, + // App signing options: https://github.com/appium/appium-uiautomator2-driver#app-signing + SupportsKeystoreOptions, + SupportsNoSignOption, + // Device locking options: https://github.com/appium/appium-uiautomator2-driver#device-locking + SupportsSkipUnlockOption, + SupportsUnlockTypeOption, + SupportsUnlockKeyOption, + SupportsUnlockStrategyOption, + SupportsUnlockSuccessTimeoutOption, + // MJPEG options: https://github.com/appium/appium-uiautomator2-driver#mjpeg + SupportsMjpegServerPortOption, + SupportsMjpegScreenshotUrlOption, + // Web Context options: https://github.com/appium/appium-uiautomator2-driver#web-context + SupportsAutoWebViewOption, + SupportsWebviewDevtoolsPortOption, + SupportsEnsureWebviewsHavePagesOption, + SupportsChromedriverPortOption, + SupportsChromedriverPortsOption, + SupportsChromedriverArgsOption, + SupportsChromedriverExecutableOption, + SupportsChromedriverExecutableDirOption, + SupportsChromedriverChromeMappingFileOption, + SupportsChromedriverUseSystemExecutableOption, + SupportsChromedriverDisableBuildCheckOption, + SupportsAutoWebviewTimeoutOption, + SupportsRecreateChromeDriverSessionsOption, + SupportsNativeWebScreenshotOption, + SupportsExtractChromeAndroidPackageFromContextNameOption, + SupportsShowChromedriverLogOption, + SupportsChromeOptionsOption, + SupportsChromeLoggingPrefsOption, + // Other options: https://github.com/appium/appium-uiautomator2-driver#other + SupportsDisableSuppressAccessibilityServiceOption, + SupportsUserProfileOption, + SupportsSkipLogCaptureOption { + public UiAutomator2Options() { + setCommonOptions(); + } + + public UiAutomator2Options(Capabilities source) { + super(source); + setCommonOptions(); + } + + public UiAutomator2Options(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setPlatformName(MobilePlatform.ANDROID); + setAutomationName(AutomationName.ANDROID_UIAUTOMATOR2); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsAdbExecTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsAdbExecTimeoutOption.java new file mode 100644 index 000000000..228ab056e --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsAdbExecTimeoutOption.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.android.options.adb; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsAdbExecTimeoutOption> extends + Capabilities, CanSetCapability { + String ADB_EXEC_TIMEOUT_OPTION = "adbExecTimeout"; + + /** + * Maximum time to wait until single ADB command is executed. + * 20000 ms by default. + * + * @param timeout ADB commands timeout. + * @return self instance for chaining. + */ + default T setAdbExecTimeout(Duration timeout) { + return amend(ADB_EXEC_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get maximum time to wait until single ADB command is executed. + * + * @return Timeout value. + */ + default Optional getAdbExecTimeout() { + return Optional.ofNullable(toDuration(getCapability(ADB_EXEC_TIMEOUT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsAdbPortOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsAdbPortOption.java new file mode 100644 index 000000000..461e609ab --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsAdbPortOption.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.android.options.adb; + +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 SupportsAdbPortOption> extends + Capabilities, CanSetCapability { + String ADB_PORT_OPTION = "adbPort"; + + /** + * Set number of the port where ADB is running. 5037 by default + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setAdbPort(int port) { + return amend(ADB_PORT_OPTION, port); + } + + /** + * Get number of the port where ADB is running. + * + * @return Adb port value + */ + default Optional getAdbPort() { + return Optional.ofNullable(toInteger(getCapability(ADB_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsAllowDelayAdbOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsAllowDelayAdbOption.java new file mode 100644 index 000000000..fc8ea3b80 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsAllowDelayAdbOption.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.android.options.adb; + +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 SupportsAllowDelayAdbOption> extends + Capabilities, CanSetCapability { + String ALLOW_DELAY_ADB_OPTION = "allowDelayAdb"; + + /** + * Being set to false prevents emulator to use -delay-adb feature to detect its startup. + * See https://github.com/appium/appium/issues/14773 for more details. + * + * @param value Set it to false in order to prevent the emulator to use -delay-adb feature. + * @return self instance for chaining. + */ + default T setAllowDelayAdb(boolean value) { + return amend(ALLOW_DELAY_ADB_OPTION, value); + } + + /** + * Get whether to prevent the emulator to use -delay-adb feature. + * + * @return True or false. + */ + default Optional doesAllowDelayAdb() { + return Optional.ofNullable(toSafeBoolean(getCapability(ALLOW_DELAY_ADB_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsBuildToolsVersionOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsBuildToolsVersionOption.java new file mode 100644 index 000000000..df5b27f5a --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsBuildToolsVersionOption.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.android.options.adb; + +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 SupportsBuildToolsVersionOption> extends + Capabilities, CanSetCapability { + String BUILD_TOOLS_VERSION_OPTION = "buildToolsVersion"; + + /** + * The version of Android build tools to use. By default, UiAutomator2 + * driver uses the most recent version of build tools installed on + * the machine, but sometimes it might be necessary to give it a hint + * (let say if there is a known bug in the most recent tools version). + * Example: 28.0.3 + * + * @param version The build tools version to use. + * @return self instance for chaining. + */ + default T setBuildToolsVersion(String version) { + return amend(BUILD_TOOLS_VERSION_OPTION, version); + } + + /** + * Get the version of Android build tools to use. + * + * @return Build tools version. + */ + default Optional getBuildToolsVersion() { + return Optional.ofNullable((String) getCapability(BUILD_TOOLS_VERSION_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsClearDeviceLogsOnStartOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsClearDeviceLogsOnStartOption.java new file mode 100644 index 000000000..f48891388 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsClearDeviceLogsOnStartOption.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.android.options.adb; + +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 SupportsClearDeviceLogsOnStartOption> extends + Capabilities, CanSetCapability { + String CLEAR_DEVICE_LOGS_ON_START_OPTION = "clearDeviceLogsOnStart"; + + /** + * Makes UiAutomator2 to delete all the existing logs in the + * device buffer before starting a new test. + * + * @return self instance for chaining. + */ + default T clearDeviceLogsOnStart() { + return amend(CLEAR_DEVICE_LOGS_ON_START_OPTION, true); + } + + + /** + * If set to true then UiAutomator2 deletes all the existing logs in the + * device buffer before starting a new test. + * + * @param value Set to false if you don't want to wait for the app to finish its launch. + * @return self instance for chaining. + */ + default T setClearDeviceLogsOnStart(boolean value) { + return amend(CLEAR_DEVICE_LOGS_ON_START_OPTION, value); + } + + /** + * Get whether to delete all the existing logs in the + * device buffer before starting a new test. + * + * @return True or false. + */ + default Optional doesClearDeviceLogsOnStart() { + return Optional.ofNullable(toSafeBoolean(getCapability(CLEAR_DEVICE_LOGS_ON_START_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsIgnoreHiddenApiPolicyErrorOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsIgnoreHiddenApiPolicyErrorOption.java new file mode 100644 index 000000000..29999e21d --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsIgnoreHiddenApiPolicyErrorOption.java @@ -0,0 +1,60 @@ +/* + * 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.options.adb; + +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 SupportsIgnoreHiddenApiPolicyErrorOption> extends + Capabilities, CanSetCapability { + String IGNORE_HIDDEN_API_POLICY_ERROR_OPTION = "ignoreHiddenApiPolicyError"; + + /** + * Prevents the driver from ever killing the ADB server explicitl. + * + * @return self instance for chaining. + */ + default T ignoreHiddenApiPolicyError() { + return amend(IGNORE_HIDDEN_API_POLICY_ERROR_OPTION, true); + } + + /** + * Being set to true ignores a failure while changing hidden API access policies. + * Could be useful on some devices, where access to these policies has been locked by its vendor. + * false by default. + * + * @param value Whether to ignore a failure while changing hidden API access policies. + * @return self instance for chaining. + */ + default T setIgnoreHiddenApiPolicyError(boolean value) { + return amend(IGNORE_HIDDEN_API_POLICY_ERROR_OPTION, value); + } + + /** + * Get whether to ignore a failure while changing hidden API access policies. + * + * @return True or false. + */ + default Optional doesIgnoreHiddenApiPolicyError() { + return Optional.ofNullable(toSafeBoolean(getCapability(IGNORE_HIDDEN_API_POLICY_ERROR_OPTION))); + } +} 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 new file mode 100644 index 000000000..f58076fe6 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsLogcatFilterSpecsOption.java @@ -0,0 +1,55 @@ +/* + * 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.options.adb; + +import io.appium.java_client.remote.options.BaseOptions; +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 + Capabilities, CanSetCapability { + String LOGCAT_FILTER_SPECS_OPTION = "logcatFilterSpecs"; + + /** + * Allows to customize logcat output filtering. + * + * @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(List format) { + return amend(LOGCAT_FILTER_SPECS_OPTION, format); + } + + /** + * Get the logcat filter format. + * + * @return Format specifier. See the documentation on {@link #setLogcatFilterSpecs(List)} for more details. + */ + 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/adb/SupportsLogcatFormatOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsLogcatFormatOption.java new file mode 100644 index 000000000..98302419e --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsLogcatFormatOption.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.android.options.adb; + +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 SupportsLogcatFormatOption> extends + Capabilities, CanSetCapability { + String LOGCAT_FORMAT_OPTION = "logcatFormat"; + + /** + * The log print format, where format is one of: brief process tag thread raw time + * threadtime long. threadtime is the default value. + * + * @param format The format specifier. + * @return self instance for chaining. + */ + default T setLogcatFormat(String format) { + return amend(LOGCAT_FORMAT_OPTION, format); + } + + /** + * Get the log print format. + * + * @return Format specifier. + */ + default Optional getLogcatFormat() { + return Optional.ofNullable((String) getCapability(LOGCAT_FORMAT_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsMockLocationAppOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsMockLocationAppOption.java new file mode 100644 index 000000000..e0b690f38 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsMockLocationAppOption.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.android.options.adb; + +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 SupportsMockLocationAppOption> extends + Capabilities, CanSetCapability { + String MOCK_LOCATION_APP_OPTION = "mockLocationApp"; + + /** + * Sets the package identifier of the app, which is used as a system mock location + * provider since Appium 1.18.0+. This capability has no effect on emulators. + * If the value is set to null or an empty string, then Appium will skip the mocked + * location provider setup procedure. Defaults to Appium Setting package + * identifier (io.appium.settings). + * + * @param appIdentifier The identifier of the mock location provider app. + * @return self instance for chaining. + */ + default T setMockLocationApp(String appIdentifier) { + return amend(MOCK_LOCATION_APP_OPTION, appIdentifier); + } + + /** + * Get the identifier of the app, which is used as a system mock location provider. + * + * @return App identifier. + */ + default Optional getMockLocationApp() { + return Optional.ofNullable((String) getCapability(MOCK_LOCATION_APP_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsRemoteAdbHostOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsRemoteAdbHostOption.java new file mode 100644 index 000000000..6e8c94a1d --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsRemoteAdbHostOption.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.android.options.adb; + +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 SupportsRemoteAdbHostOption> extends + Capabilities, CanSetCapability { + String REMOTE_ADB_HOST_OPTION = "remoteAdbHost"; + + /** + * Address of the host where ADB is running (the value of -H ADB command line option). + * Localhost by default. + * + * @param host The name host where ADB server is running. + * @return self instance for chaining. + */ + default T setRemoteAdbHost(String host) { + return amend(REMOTE_ADB_HOST_OPTION, host); + } + + /** + * Get the address of the host where ADB is running. + * + * @return Host name. + */ + default Optional getRemoteAdbHost() { + return Optional.ofNullable((String) getCapability(REMOTE_ADB_HOST_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsSkipLogcatCaptureOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsSkipLogcatCaptureOption.java new file mode 100644 index 000000000..1bd0f6b42 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsSkipLogcatCaptureOption.java @@ -0,0 +1,60 @@ +/* + * 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.options.adb; + +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 SupportsSkipLogcatCaptureOption> extends + Capabilities, CanSetCapability { + String SKIP_LOGCAT_CAPTURE_OPTION = "skipLogcatCapture"; + + /** + * Disables automatic logcat output collection during the test run. + * + * @return self instance for chaining. + */ + default T skipLogcatCapture() { + return amend(SKIP_LOGCAT_CAPTURE_OPTION, true); + } + + /** + * Being set to true disables automatic logcat output collection during the test run. + * false by default + * + * @param value Whether to delete all the existing device logs before starting a new test. + * @return self instance for chaining. + */ + default T setSkipLogcatCapture(boolean value) { + return amend(SKIP_LOGCAT_CAPTURE_OPTION, value); + } + + /** + * Get whether to delete all the existing logs in the + * device buffer before starting a new test. + * + * @return True or false. + */ + default Optional doesSkipLogcatCapture() { + return Optional.ofNullable(toSafeBoolean(getCapability(SKIP_LOGCAT_CAPTURE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/adb/SupportsSuppressKillServerOption.java b/src/main/java/io/appium/java_client/android/options/adb/SupportsSuppressKillServerOption.java new file mode 100644 index 000000000..daaa9666b --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/adb/SupportsSuppressKillServerOption.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.android.options.adb; + +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 SupportsSuppressKillServerOption> extends + Capabilities, CanSetCapability { + String SUPPRESS_KILL_SERVER_OPTION = "suppressKillServer"; + + /** + * Prevents the driver from ever killing the ADB server explicitl. + * + * @return self instance for chaining. + */ + default T suppressKillServer() { + return amend(SUPPRESS_KILL_SERVER_OPTION, true); + } + + /** + * Being set to true prevents the driver from ever killing the ADB server explicitly. + * Could be useful if ADB is connected wirelessly. false by default. + * + * @param value Whether to prevent the driver from ever killing the ADB server explicitly. + * @return self instance for chaining. + */ + default T setSuppressKillServer(boolean value) { + return amend(SUPPRESS_KILL_SERVER_OPTION, value); + } + + /** + * Get whether to prevent the driver from ever killing the ADB server explicitly. + * + * @return True or false. + */ + default Optional doesSuppressKillServer() { + return Optional.ofNullable(toSafeBoolean(getCapability(SUPPRESS_KILL_SERVER_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/ActivityOptions.java b/src/main/java/io/appium/java_client/android/options/app/ActivityOptions.java new file mode 100644 index 000000000..6a3db25d7 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/ActivityOptions.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.android.options.app; + +import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.remote.options.BaseMapOptionData; + +import java.util.Map; +import java.util.Optional; + +public class ActivityOptions extends BaseMapOptionData { + public ActivityOptions() { + super(); + } + + public ActivityOptions(Map options) { + super(options); + } + + /** + * Display id which you want to assign to launch the main app activity on. + * This might be useful if the device under test supports multiple displays. + * + * @param id Display identifier. + * @return self instance for chaining. + */ + public ActivityOptions withLaunchDisplayId(int id) { + return assignOptionValue("launchDisplayId", id); + } + + /** + * Get display id which you want to assign to launch the main app activity on. + * + * @return Display identifier. + */ + public Optional getLaunchDisplayId() { + Optional result = getOptionValue("launchDisplayId"); + return result.map(CapabilityHelpers::toInteger); + } +} 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 new file mode 100644 index 000000000..67f09f2b5 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/IntentOptions.java @@ -0,0 +1,421 @@ +/* + * 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.options.app; + +import io.appium.java_client.remote.options.BaseMapOptionData; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class IntentOptions extends BaseMapOptionData { + public IntentOptions() { + super(); + } + + public IntentOptions(Map options) { + super(options); + } + + /** + * An intent action name. Application-specific actions should be prefixed with + * the vendor's package name. + * + * @param action E.g. ACTION_MAIN. + * @return self instance for chaining. + */ + public IntentOptions withAction(String action) { + return assignOptionValue("action", action); + } + + /** + * Get the action name. + * + * @return Action name. + */ + public Optional getAction() { + return getOptionValue("action"); + } + + /** + * Set an intent data URI. + * + * @param data E.g. content://contacts/people/1. + * @return self instance for chaining. + */ + public IntentOptions withData(String data) { + return assignOptionValue("data", data); + } + + /** + * Get intent data URI. + * + * @return Intent data URI. + */ + public Optional getData() { + return getOptionValue("data"); + } + + /** + * Intent MIME type. + * + * @param type E.g. image/png. + * @return self instance for chaining. + */ + public IntentOptions withType(String type) { + return assignOptionValue("type", type); + } + + /** + * Get an intent type. + * + * @return Intent type. + */ + public Optional getType() { + return getOptionValue("type"); + } + + /** + * Set intent categories. + * + * @param categories One or more comma-separated Intent categories. + * @return self instance for chaining. + */ + public IntentOptions withCategories(String categories) { + return assignOptionValue("categories", categories); + } + + /** + * Get intent categories. + * + * @return Intent categories. + */ + public Optional getCategories() { + return getOptionValue("categories"); + } + + /** + * Set intent component name with package name prefix + * to create an explicit intent. + * + * @param component E.g. com.example.app/.ExampleActivity. + * @return self instance for chaining. + */ + public IntentOptions withComponent(String component) { + return assignOptionValue("component", component); + } + + /** + * Get intent component name. + * + * @return Intent component name. + */ + public Optional getComponent() { + return getOptionValue("component"); + } + + /** + * Single-string value, which represents intent flags set encoded into + * an integer. Could also be provided in hexadecimal format. Check + * https://developer.android.com/reference/android/content/Intent.html#setFlags(int) + * for more details. + * + * @param intFlags E.g. 0x0F. + * @return self instance for chaining. + */ + public IntentOptions withIntFlags(String intFlags) { + return assignOptionValue("intFlags", intFlags); + } + + /** + * Get intent flags. + * + * @return Intent flags encoded into a hexadecimal value. + */ + public Optional getIntFlags() { + return getOptionValue("intFlags"); + } + + /** + * Comma-separated string of intent flag names. + * + * @param flags E.g. 'ACTIVITY_CLEAR_TASK' (the 'FLAG_' prefix is optional). + * @return self instance for chaining. + */ + public IntentOptions withFlags(String flags) { + return assignOptionValue("flags", flags); + } + + /** + * Get intent flag names. + * + * @return Comma-separated string of intent flag names. + */ + public Optional getFlags() { + return getOptionValue("flags"); + } + + /** + * The name of a class inside of the application package that + * will be used as the component for this Intent. + * + * @param className E.g. com.example.app.MainActivity. + * @return self instance for chaining. + */ + public IntentOptions withClassName(String className) { + return assignOptionValue("className", className); + } + + /** + * Get class name. + * + * @return Class name. + */ + public Optional getClassName() { + return getOptionValue("className"); + } + + /** + * Intent string parameters. + * + * @param es Map, where the key is arg parameter name and value is its string value. + * @return self instance for chaining. + */ + public IntentOptions withEs(Map es) { + return assignOptionValue("es", es); + } + + /** + * Get intent string parameters. + * + * @return Intent string parameters mapping. + */ + public Optional> getEs() { + return getOptionValue("es"); + } + + /** + * Intent null parameters. + * + * @param esn List, where keys are parameter names. + * @return self instance for chaining. + */ + public IntentOptions withEsn(List esn) { + return assignOptionValue("esn", esn); + } + + /** + * Get intent null parameters. + * + * @return Intent null parameters. + */ + public Optional> getEsn() { + return getOptionValue("esn"); + } + + /** + * Intent boolean parameters. + * + * @param ez Map, where keys are parameter names and values are booleans. + * @return self instance for chaining. + */ + public IntentOptions withEz(Map ez) { + return assignOptionValue("ez", ez); + } + + /** + * Get intent boolean parameters. + * + * @return Intent boolean parameters. + */ + public Optional> getEz() { + return getOptionValue("ez"); + } + + /** + * Intent integer parameters. + * + * @param ei Map, where keys are parameter names and values are integers. + * @return self instance for chaining. + */ + public IntentOptions withEi(Map ei) { + return assignOptionValue("ei", 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()))) + ); + } + + /** + * Get intent integer parameters. + * + * @return Intent integer parameters. + */ + public Optional> getEi() { + Optional> value = getOptionValue("ei"); + return value.map(v -> convertMapValues(v, Integer::parseInt)); + } + + /** + * Intent long parameters. + * + * @param el Map, where keys are parameter names and values are long numbers. + * @return self instance for chaining. + */ + public IntentOptions withEl(Map el) { + return assignOptionValue("el", el); + } + + /** + * Get intent long parameters. + * + * @return Intent long parameters. + */ + public Optional> getEl() { + Optional> value = getOptionValue("el"); + return value.map(v -> convertMapValues(v, Long::parseLong)); + } + + /** + * Intent float parameters. + * + * @param ef Map, where keys are parameter names and values are float numbers. + * @return self instance for chaining. + */ + public IntentOptions withEf(Map ef) { + return assignOptionValue("ef", ef); + } + + /** + * Get intent float parameters. + * + * @return Intent float parameters. + */ + public Optional> getEf() { + Optional> value = getOptionValue("ef"); + return value.map(v -> convertMapValues(v, Float::parseFloat)); + } + + /** + * Intent URI-data parameters. + * + * @param eu Map, where keys are parameter names and values are valid URIs. + * @return self instance for chaining. + */ + public IntentOptions withEu(Map eu) { + return assignOptionValue("eu", eu); + } + + /** + * Get intent URI parameters. + * + * @return Intent URI parameters. + */ + public Optional> getEu() { + return getOptionValue("eu"); + } + + /** + * Intent component name parameters. + * + * @param ecn Map, where keys are parameter names and values are valid component names. + * @return self instance for chaining. + */ + public IntentOptions withEcn(Map ecn) { + return assignOptionValue("ecn", ecn); + } + + /** + * Get intent component name parameters. + * + * @return Intent component name parameters. + */ + public Optional> getEcn() { + return getOptionValue("ecn"); + } + + private static Map mergeValues(Map map) { + return map.entrySet().stream() + .collect( + Collectors.toMap(Map.Entry::getKey, entry -> ((List) entry.getValue()).stream() + .map(String::valueOf) + .collect(Collectors.joining(","))) + ); + } + + /** + * Intent integer array parameters. + * + * @param eia Map, where keys are parameter names and values are lists of integers. + * @return self instance for chaining. + */ + public IntentOptions withEia(Map> eia) { + return assignOptionValue("eia", mergeValues(eia)); + } + + /** + * Get intent integer array parameters. + * + * @return Intent integer array parameters. + */ + public Optional> getEia() { + return getOptionValue("eia"); + } + + /** + * Intent long array parameters. + * + * @param ela Map, where keys are parameter names and values are lists of long numbers. + * @return self instance for chaining. + */ + public IntentOptions withEla(Map> ela) { + return assignOptionValue("ela", mergeValues(ela)); + } + + /** + * Get intent long array parameters. + * + * @return Intent long array parameters. + */ + public Optional> getEla() { + return getOptionValue("ela"); + } + + /** + * Intent float array parameters. + * + * @param efa Map, where keys are parameter names and values are lists of float numbers. + * @return self instance for chaining. + */ + public IntentOptions withEfa(Map> efa) { + return assignOptionValue("efa", mergeValues(efa)); + } + + /** + * Get intent float array parameters. + * + * @return Intent float array parameters. + */ + public Optional> getEfa() { + return getOptionValue("efa"); + } +} 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 new file mode 100644 index 000000000..59d5fe520 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsActivityOptionsOption.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.android.options.app; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +public interface SupportsActivityOptionsOption> extends + Capabilities, CanSetCapability { + String ACTIVITY_OPTIONS_OPTION = "activityOptions"; + + /** + * The mapping of custom options for the main app activity that is going to + * be started. Check + * https://github.com/appium/appium-espresso-driver#activity-options + * for more details. + * + * @param options Activity options. + * @return self instance for chaining. + */ + default T setActivityOptions(ActivityOptions options) { + return amend(ACTIVITY_OPTIONS_OPTION, options.toMap()); + } + + /** + * Get activity options. + * + * @return Activity options. + */ + default Optional getActivityOptions() { + //noinspection unchecked + return Optional.ofNullable(getCapability(ACTIVITY_OPTIONS_OPTION)) + .map(v -> new ActivityOptions((Map) v)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAllowTestPackagesOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAllowTestPackagesOption.java new file mode 100644 index 000000000..2926aa932 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAllowTestPackagesOption.java @@ -0,0 +1,61 @@ +/* + * 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.options.app; + +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 SupportsAllowTestPackagesOption> extends + Capabilities, CanSetCapability { + String ALLOW_TEST_PACKAGES_OPTION = "allowTestPackages"; + + /** + * Enables usage of packages built with the test flag for + * the automated testing (literally adds -t flag to the adb install command). + * + * @return self instance for chaining. + */ + default T allowTestPackages() { + return amend(ALLOW_TEST_PACKAGES_OPTION, true); + } + + /** + * If set to true then it would be possible to use packages built with the test flag for + * the automated testing (literally adds -t flag to the adb install command). false by default. + * + * @param value True to allow test packages installation. + * @return self instance for chaining. + */ + default T setAllowTestPackages(boolean value) { + return amend(ALLOW_TEST_PACKAGES_OPTION, value); + } + + /** + * Get whether it is possible to use packages built with the test flag for + * the automated testing (literally adds -t flag to the adb install command). + * + * @return True or false. + */ + default Optional doesAllowTestPackages() { + return Optional.ofNullable(toSafeBoolean(getCapability(ALLOW_TEST_PACKAGES_OPTION))); + } +} 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 new file mode 100644 index 000000000..ead32780b --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAndroidInstallTimeoutOption.java @@ -0,0 +1,52 @@ +/* + * 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.options.app; + +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 SupportsAndroidInstallTimeoutOption> extends + Capabilities, CanSetCapability { + String ANDROID_INSTALL_TIMEOUT_OPTION = "androidInstallTimeout"; + + /** + * Maximum amount of time to wait until the application under test is installed. + * 90000 ms by default + * + * @param installTimeout App install timeout. + * @return self instance for chaining. + */ + default T setAndroidInstallTimeout(Duration installTimeout) { + return amend(ANDROID_INSTALL_TIMEOUT_OPTION, installTimeout.toMillis()); + } + + /** + * Get maximum amount of time to wait until the application under test is installed. + * + * @return Timeout value. + */ + default Optional getAndroidInstallTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(ANDROID_INSTALL_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAppActivityOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAppActivityOption.java new file mode 100644 index 000000000..6e1e544bd --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAppActivityOption.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.android.options.app; + +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 SupportsAppActivityOption> extends + Capabilities, CanSetCapability { + String APP_ACTIVITY_OPTION = "appActivity"; + + /** + * Main application activity identifier. If not provided then UiAutomator2 + * will try to detect it automatically from the package provided by the app capability. + * + * @param appActivity Fully qualified app activity class name. + * @return self instance for chaining. + */ + default T setAppActivity(String appActivity) { + return amend(APP_ACTIVITY_OPTION, appActivity); + } + + /** + * Get the name of the main app activity. + * + * @return Activity class name. + */ + default Optional getAppActivity() { + return Optional.ofNullable((String) getCapability(APP_ACTIVITY_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAppPackageOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAppPackageOption.java new file mode 100644 index 000000000..a82cc5dfe --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAppPackageOption.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.android.options.app; + +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 SupportsAppPackageOption> extends + Capabilities, CanSetCapability { + String APP_PACKAGE_OPTION = "appPackage"; + + /** + * Application package identifier to be started. If not provided then UiAutomator2 will + * try to detect it automatically from the package provided by the app capability. + * + * @param appPackage App package identifier. + * @return self instance for chaining. + */ + default T setAppPackage(String appPackage) { + return amend(APP_PACKAGE_OPTION, appPackage); + } + + /** + * Get the app package identifier. + * + * @return Identifier value. + */ + default Optional getAppPackage() { + return Optional.ofNullable((String) getCapability(APP_PACKAGE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitActivityOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitActivityOption.java new file mode 100644 index 000000000..5567fbb1e --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitActivityOption.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.android.options.app; + +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 SupportsAppWaitActivityOption> extends + Capabilities, CanSetCapability { + String APP_WAIT_ACTIVITY_OPTION = "appWaitActivity"; + + /** + * Identifier of the activity that the driver should wait for + * (not necessarily the main one). + * If not provided then defaults to appium:appActivity. + * + * @param appWaitActivity Fully qualified app activity class name. + * @return self instance for chaining. + */ + default T setAppWaitActivity(String appWaitActivity) { + return amend(APP_WAIT_ACTIVITY_OPTION, appWaitActivity); + } + + /** + * Get the name of the app activity to wait for. + * + * @return Activity class name. + */ + default Optional getAppWaitActivity() { + return Optional.ofNullable((String) getCapability(APP_WAIT_ACTIVITY_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitDurationOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitDurationOption.java new file mode 100644 index 000000000..77039f70d --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitDurationOption.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.android.options.app; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsAppWaitDurationOption> extends + Capabilities, CanSetCapability { + String APP_WAIT_DURATION_OPTION = "appWaitDuration"; + + /** + * Maximum amount of time to wait until the application under test is started + * (e.g. an activity returns the control to the caller). 20000 ms by default. + * + * @param appWaitDuration Package identifier to wait for. + * @return self instance for chaining. + */ + default T setAppWaitDuration(Duration appWaitDuration) { + return amend(APP_WAIT_DURATION_OPTION, appWaitDuration.toMillis()); + } + + /** + * Get the identifier of the app package to wait for. + * + * @return Package identifier. + */ + default Optional getAppWaitDuration() { + return Optional.ofNullable(toDuration(getCapability(APP_WAIT_DURATION_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitForLaunchOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitForLaunchOption.java new file mode 100644 index 000000000..70e440b54 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitForLaunchOption.java @@ -0,0 +1,52 @@ +/* + * 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.options.app; + +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 SupportsAppWaitForLaunchOption> extends + Capabilities, CanSetCapability { + String APP_WAIT_FOR_LAUNCH_OPTION = "appWaitForLaunch"; + + /** + * Whether to block until the app under test returns the control to the + * caller after its activity has been started by Activity Manager + * (true, the default value) or to continue the test without waiting for that (false). + * + * @param value Set to false if you don't want to wait for the app to finish its launch. + * @return self instance for chaining. + */ + default T setAppWaitForLaunch(boolean value) { + return amend(APP_WAIT_FOR_LAUNCH_OPTION, value); + } + + /** + * Get whether to block until the app under test returns the control to the + * caller after its activity has been started by Activity Manager. + * + * @return True if the driver should block or false otherwise. + */ + default Optional doesAppWaitForLaunch() { + return Optional.ofNullable(toSafeBoolean(getCapability(APP_WAIT_FOR_LAUNCH_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitPackageOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitPackageOption.java new file mode 100644 index 000000000..6089d9bcf --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAppWaitPackageOption.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.android.options.app; + +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 SupportsAppWaitPackageOption> extends + Capabilities, CanSetCapability { + String APP_WAIT_PACKAGE_OPTION = "appWaitPackage"; + + /** + * Identifier of the package that the driver should wait for + * (not necessarily the main one). + * If not provided then defaults to appium:appPackage. + * + * @param appWaitPackage Package identifier to wait for. + * @return self instance for chaining. + */ + default T setAppWaitPackage(String appWaitPackage) { + return amend(APP_WAIT_PACKAGE_OPTION, appWaitPackage); + } + + /** + * Get the identifier of the app package to wait for. + * + * @return Package identifier. + */ + default Optional getAppWaitPackage() { + return Optional.ofNullable((String) getCapability(APP_WAIT_PACKAGE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsAutoGrantPermissionsOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsAutoGrantPermissionsOption.java new file mode 100644 index 000000000..7a7d6cde1 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsAutoGrantPermissionsOption.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.android.options.app; + +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 SupportsAutoGrantPermissionsOption> extends + Capabilities, CanSetCapability { + String AUTO_GRANT_PERMISSIONS_OPTION = "autoGrantPermissions"; + + /** + * Enables granting of all the requested application permissions + * automatically when a test starts. + * + * @return self instance for chaining. + */ + default T autoGrantPermissions() { + return amend(AUTO_GRANT_PERMISSIONS_OPTION, true); + } + + + /** + * Whether to grant all the requested application permissions + * automatically when a test starts(true). false by default. + * + * @param value Whether to enable or disable automatic permissions granting. + * @return self instance for chaining. + */ + default T setAutoGrantPermissions(boolean value) { + return amend(AUTO_GRANT_PERMISSIONS_OPTION, value); + } + + /** + * Get whether to grant all the requested application permissions + * automatically when a test starts. + * + * @return True if the permissions should be granted. + */ + default Optional doesAutoGrantPermissions() { + return Optional.ofNullable(toSafeBoolean(getCapability(AUTO_GRANT_PERMISSIONS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsIntentActionOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentActionOption.java new file mode 100644 index 000000000..7fc2afe89 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentActionOption.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.android.options.app; + +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 SupportsIntentActionOption> extends + Capabilities, CanSetCapability { + String INTENT_ACTION_OPTION = "intentAction"; + + /** + * Set an optional intent action to be applied when + * starting the given appActivity by Activity Manager. + * + * @param intentAction Intent action class name. + * @return self instance for chaining. + */ + default T setIntentAction(String intentAction) { + return amend(INTENT_ACTION_OPTION, intentAction); + } + + /** + * Get intent action to be applied when + * starting the given appActivity by Activity Manager. + * + * @return Intent action class name. + */ + default Optional getIntentAction() { + return Optional.ofNullable((String) getCapability(INTENT_ACTION_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsIntentCategoryOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentCategoryOption.java new file mode 100644 index 000000000..287ac09d4 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentCategoryOption.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.android.options.app; + +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 SupportsIntentCategoryOption> extends + Capabilities, CanSetCapability { + String INTENT_CATEGORY_OPTION = "intentCategory"; + + /** + * Set an optional intent category to be applied when + * starting the given appActivity by Activity Manager. + * + * @param intentCategory Intent category class name. + * @return self instance for chaining. + */ + default T setIntentCategory(String intentCategory) { + return amend(INTENT_CATEGORY_OPTION, intentCategory); + } + + /** + * Get intent category to be applied when + * starting the given appActivity by Activity Manager. + * + * @return Intent category class name. + */ + default Optional getIntentCategory() { + return Optional.ofNullable((String) getCapability(INTENT_CATEGORY_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsIntentFlagsOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentFlagsOption.java new file mode 100644 index 000000000..6c9ed08a8 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentFlagsOption.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.android.options.app; + +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 SupportsIntentFlagsOption> extends + Capabilities, CanSetCapability { + String INTENT_FLAGS_OPTION = "intentFlags"; + + /** + * Set an optional intent flags to be applied when + * starting the given appActivity by Activity Manager. + * + * @param intentFlags Intent flags hexadecimal string. + * @return self instance for chaining. + */ + default T setIntentFlags(String intentFlags) { + return amend(INTENT_FLAGS_OPTION, intentFlags); + } + + /** + * Get intent flags to be applied when + * starting the given appActivity by Activity Manager. + * + * @return Intent flags string. + */ + default Optional getIntentFlags() { + return Optional.ofNullable((String) getCapability(INTENT_FLAGS_OPTION)); + } +} 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 new file mode 100644 index 000000000..3c0fab894 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsIntentOptionsOption.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.android.options.app; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +public interface SupportsIntentOptionsOption> extends + Capabilities, CanSetCapability { + String INTENT_OPTIONS_OPTION = "intentOptions"; + + /** + * The mapping of custom options for the intent that is going to be passed + * to the main app activity. Check + * https://github.com/appium/appium-espresso-driver#intent-options + * for more details. + * + * @param options Intent options. + * @return self instance for chaining. + */ + default T setIntentOptions(IntentOptions options) { + return amend(INTENT_OPTIONS_OPTION, options.toMap()); + } + + /** + * Get intent options. + * + * @return Intent options. + */ + default Optional getIntentOptions() { + //noinspection unchecked + return Optional.ofNullable(getCapability(INTENT_OPTIONS_OPTION)) + .map(v -> new IntentOptions((Map) v)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsOptionalIntentArgumentsOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsOptionalIntentArgumentsOption.java new file mode 100644 index 000000000..da5d5a3c7 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsOptionalIntentArgumentsOption.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.android.options.app; + +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 SupportsOptionalIntentArgumentsOption> extends + Capabilities, CanSetCapability { + String OPTIONAL_INTENT_ARGUMENTS_OPTION = "optionalIntentArguments"; + + /** + * Set an optional intent arguments to be applied when + * starting the given appActivity by Activity Manager. + * + * @param arguments Intent arguments string. + * @return self instance for chaining. + */ + default T setOptionalIntentArguments(String arguments) { + return amend(OPTIONAL_INTENT_ARGUMENTS_OPTION, arguments); + } + + /** + * Get intent arguments to be applied when + * starting the given appActivity by Activity Manager. + * + * @return Intent arguments string. + */ + default Optional getOptionalIntentArguments() { + return Optional.ofNullable((String) getCapability(OPTIONAL_INTENT_ARGUMENTS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsRemoteAppsCacheLimitOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsRemoteAppsCacheLimitOption.java new file mode 100644 index 000000000..e44e8fcf4 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsRemoteAppsCacheLimitOption.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.android.options.app; + +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 SupportsRemoteAppsCacheLimitOption> extends + Capabilities, CanSetCapability { + String REMOTE_APPS_CACHE_LIMIT_OPTION = "remoteAppsCacheLimit"; + + /** + * Sets the maximum amount of application packages to be cached on the device under test. + * This is needed for devices that don't support streamed installs (Android 7 and below), + * because ADB must push app packages to the device first in order to install them, + * which takes some time. Setting this capability to zero disables apps caching. + * 10 by default. + * + * @param limit The maximum amount of cached apps. + * @return self instance for chaining. + */ + default T setRemoteAppsCacheLimit(int limit) { + return amend(REMOTE_APPS_CACHE_LIMIT_OPTION, limit); + } + + /** + * Get the maximum amount of apps that could be cached on the remote device. + * + * @return The maximum amount of cached apps. + */ + default Optional getRemoteAppsCacheLimit() { + return Optional.ofNullable(toInteger(getCapability(REMOTE_APPS_CACHE_LIMIT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/app/SupportsUninstallOtherPackagesOption.java b/src/main/java/io/appium/java_client/android/options/app/SupportsUninstallOtherPackagesOption.java new file mode 100644 index 000000000..fb1402d79 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/app/SupportsUninstallOtherPackagesOption.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.android.options.app; + +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 SupportsUninstallOtherPackagesOption> extends + Capabilities, CanSetCapability { + String UNINSTALL_OTHER_PACKAGES_OPTION = "uninstallOtherPackages"; + + /** + * Allows to set one or more comma-separated package + * identifiers to be uninstalled from the device before a test starts. + * + * @param packages one or more comma-separated package identifiers to uninstall. + * @return self instance for chaining. + */ + default T setUninstallOtherPackages(String packages) { + return amend(UNINSTALL_OTHER_PACKAGES_OPTION, packages); + } + + /** + * Get identifiers of packages to be uninstalled from the device before a test starts. + * + * @return Comma-separated package identifiers. + */ + default Optional getUninstallOtherPackages() { + return Optional.ofNullable((String) getCapability(UNINSTALL_OTHER_PACKAGES_OPTION)); + } +} 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 new file mode 100644 index 000000000..bca9866c0 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdArgsOption.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.android.options.avd; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.internal.Either; + +import java.util.List; +import java.util.Optional; + +public interface SupportsAvdArgsOption> extends + Capabilities, CanSetCapability { + String AVD_ARGS_OPTION = "avdArgs"; + + /** + * Set emulator command line arguments. + * + * @param args Emulator command line arguments. + * @return self instance for chaining. + */ + default T setAvdArgs(List args) { + return amend(AVD_ARGS_OPTION, args); + } + + /** + * Set emulator command line arguments. + * + * @param args Emulator command line arguments. + * @return self instance for chaining. + */ + default T setAvdArgs(String args) { + return amend(AVD_ARGS_OPTION, args); + } + + /** + * Get emulator command line arguments. + * + * @return Either arguments list of command line string. + */ + default Optional, String>> getAvdArgs() { + //noinspection unchecked + return Optional.ofNullable(getCapability(AVD_ARGS_OPTION)) + .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 new file mode 100644 index 000000000..9fadb6532 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdEnvOption.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.android.options.avd; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +public interface SupportsAvdEnvOption> extends + Capabilities, CanSetCapability { + String AVD_ENV_OPTION = "avdEnv"; + + /** + * Set the mapping of emulator environment variables. + * + * @param env Emulator environment variables mapping. + * @return self instance for chaining. + */ + default T setAvdEnv(Map env) { + return amend(AVD_ENV_OPTION, env); + } + + /** + * Get the mapping of emulator environment variables. + * + * @return Emulator environment variables mapping. + */ + default Optional> getAvdEnv() { + //noinspection unchecked + return Optional.ofNullable(getCapability(AVD_ENV_OPTION)) + .map(v -> (Map) v); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdLaunchTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdLaunchTimeoutOption.java new file mode 100644 index 000000000..974b02203 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdLaunchTimeoutOption.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.android.options.avd; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsAvdLaunchTimeoutOption> extends + Capabilities, CanSetCapability { + String AVD_LAUNCH_TIMEOUT_OPTION = "avdLaunchTimeout"; + + /** + * Maximum timeout to wait until Android Emulator is started. + * 60000 ms by default. + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setAvdLaunchTimeout(Duration timeout) { + return amend(AVD_LAUNCH_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout to wait until Android Emulator is started. + * + * @return The timeout value. + */ + default Optional getAvdLaunchTimeout() { + return Optional.ofNullable( + toDuration(getCapability(AVD_LAUNCH_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdOption.java b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdOption.java new file mode 100644 index 000000000..8eac74bd4 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdOption.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.android.options.avd; + +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 SupportsAvdOption> extends + Capabilities, CanSetCapability { + String AVD_OPTION = "avd"; + + /** + * The name of Android emulator to run the test on. + * The names of currently installed emulators could be listed using + * avdmanager list avd command. If the emulator with the given name + * is not running then it is going to be started before a test. + * + * @param avd The emulator name to use. + * @return self instance for chaining. + */ + default T setAvd(String avd) { + return amend(AVD_OPTION, avd); + } + + /** + * Get the name of Android emulator to run the test on. + * + * @return Emulator name. + */ + default Optional getAvd() { + return Optional.ofNullable((String) getCapability(AVD_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdReadyTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdReadyTimeoutOption.java new file mode 100644 index 000000000..68f1e27f2 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsAvdReadyTimeoutOption.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.android.options.avd; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsAvdReadyTimeoutOption> extends + Capabilities, CanSetCapability { + String AVD_READY_TIMEOUT_OPTION = "avdReadyTimeout"; + + /** + * Maximum timeout to wait until Android Emulator is fully booted and is ready for usage. + * 60000 ms by default + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setAvdReadyTimeout(Duration timeout) { + return amend(AVD_READY_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout to wait until Android Emulator is fully booted and is ready for usage. + * + * @return The timeout value. + */ + default Optional getAvdReadyTimeout() { + return Optional.ofNullable( + toDuration(getCapability(AVD_READY_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/avd/SupportsGpsEnabledOption.java b/src/main/java/io/appium/java_client/android/options/avd/SupportsGpsEnabledOption.java new file mode 100644 index 000000000..91dce65da --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsGpsEnabledOption.java @@ -0,0 +1,60 @@ +/* + * 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.options.avd; + +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 SupportsGpsEnabledOption> extends + Capabilities, CanSetCapability { + String GPS_ENABLED_OPTION = "gpsEnabled"; + + /** + * Enables GPS service in the Emulator. + * Unset by default, which means to not change the current value. + * + * @return self instance for chaining. + */ + default T gpsEnabled() { + return amend(GPS_ENABLED_OPTION, true); + } + + /** + * Sets whether to enable (true) or disable (false) GPS service in the Emulator. + * Unset by default, which means to not change the current value. + * + * @param value Whether to enable or disable the GPS service. + * @return self instance for chaining. + */ + default T setGpsEnabled(boolean value) { + return amend(GPS_ENABLED_OPTION, value); + } + + /** + * Get the state of GPS service on emulator. + * + * @return True or false. + */ + default Optional getGpsEnabled() { + return Optional.ofNullable(toSafeBoolean(getCapability(GPS_ENABLED_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/avd/SupportsNetworkSpeedOption.java b/src/main/java/io/appium/java_client/android/options/avd/SupportsNetworkSpeedOption.java new file mode 100644 index 000000000..5a3d05b47 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/avd/SupportsNetworkSpeedOption.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.android.options.avd; + +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 SupportsNetworkSpeedOption> extends + Capabilities, CanSetCapability { + String NETWORK_SPEED_OPTION = "networkSpeed"; + + /** + * Sets the desired network speed limit for the emulator. + * It is only applied if the emulator is not running before + * the test starts. See emulator command line arguments description + * for more details. + * + * @param speed Speed value. + * @return self instance for chaining. + */ + default T setNetworkSpeed(String speed) { + return amend(NETWORK_SPEED_OPTION, speed); + } + + /** + * Get the desired network speed limit for the emulator. + * + * @return Speed value. + */ + default Optional getNetworkSpeed() { + return Optional.ofNullable((String) getCapability(NETWORK_SPEED_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsAutoWebviewTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsAutoWebviewTimeoutOption.java new file mode 100644 index 000000000..0f0a07967 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsAutoWebviewTimeoutOption.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.android.options.context; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsAutoWebviewTimeoutOption> extends + Capabilities, CanSetCapability { + String AUTO_WEBVIEW_TIMEOUT_OPTION = "autoWebviewTimeout"; + + /** + * Set the maximum timeout to wait until a web view is + * available if autoWebview capability is set to true. 2000 ms by default. + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setAutoWebviewTimeout(Duration timeout) { + return amend(AUTO_WEBVIEW_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout to wait until a web view is available. + * + * @return The timeout value. + */ + default Optional getAutoWebviewTimeout() { + return Optional.ofNullable( + toDuration(getCapability(AUTO_WEBVIEW_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromeLoggingPrefsOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromeLoggingPrefsOption.java new file mode 100644 index 000000000..f47f4d865 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromeLoggingPrefsOption.java @@ -0,0 +1,52 @@ +/* + * 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.options.context; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +public interface SupportsChromeLoggingPrefsOption> extends + Capabilities, CanSetCapability { + String CHROME_LOGGING_PREFS_OPTION = "chromeLoggingPrefs"; + + /** + * Chrome logging preferences mapping. Basically the same as + * [goog:loggingPrefs](https://newbedev.com/ + * getting-console-log-output-from-chrome-with-selenium-python-api-bindings). + * It is set to {"browser": "ALL"} by default. + * + * @param opts Chrome logging preferences. + * @return self instance for chaining. + */ + default T setChromeLoggingPrefs(Map opts) { + return amend(CHROME_LOGGING_PREFS_OPTION, opts); + } + + /** + * Get chrome logging preferences. + * + * @return Chrome logging preferences. + */ + default Optional> getChromeLoggingPrefs() { + //noinspection unchecked + return Optional.ofNullable((Map) getCapability(CHROME_LOGGING_PREFS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromeOptionsOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromeOptionsOption.java new file mode 100644 index 000000000..955a6c2a7 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromeOptionsOption.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.android.options.context; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +public interface SupportsChromeOptionsOption> extends + Capabilities, CanSetCapability { + String CHROME_OPTIONS_OPTION = "chromeOptions"; + + /** + * A mapping, that allows to customize chromedriver options. + * See https://chromedriver.chromium.org/capabilities for the list + * of available entries. + * + * @param opts Chrome options. + * @return self instance for chaining. + */ + default T setChromeOptions(Map opts) { + return amend(CHROME_OPTIONS_OPTION, opts); + } + + /** + * Get chrome options. + * + * @return Chrome options. + */ + default Optional> getChromeOptions() { + //noinspection unchecked + return Optional.ofNullable((Map) getCapability(CHROME_OPTIONS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverArgsOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverArgsOption.java new file mode 100644 index 000000000..227088b35 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverArgsOption.java @@ -0,0 +1,52 @@ +/* + * 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.options.context; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.List; +import java.util.Optional; + +public interface SupportsChromedriverArgsOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_ARGS_OPTION = "chromedriverArgs"; + + /** + * Array of chromedriver [command line + * arguments](http://www.assertselenium.com/java/list-of-chrome-driver-command-line-arguments/). + * Note, that not all command line arguments that are available for the desktop + * browser are also available for the mobile one. + * + * @param args Chromedriver command line arguments. + * @return self instance for chaining. + */ + default T setChromedriverArgs(List args) { + return amend(CHROMEDRIVER_ARGS_OPTION, args); + } + + /** + * Get the array of chromedriver CLI arguments. + * + * @return Arguments list. + */ + default Optional> getChromedriverArgs() { + //noinspection unchecked + return Optional.ofNullable((List) getCapability(CHROMEDRIVER_ARGS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverChromeMappingFileOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverChromeMappingFileOption.java new file mode 100644 index 000000000..83faaef42 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverChromeMappingFileOption.java @@ -0,0 +1,52 @@ +/* + * 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.options.context; + +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 SupportsChromedriverChromeMappingFileOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_CHROME_MAPPING_FILE_OPTION = "chromedriverChromeMappingFile"; + + /** + * Full path to the chromedrivers mapping file. This file is used to statically + * map webview/browser versions to the chromedriver versions that are capable + * of automating them. Read [Automatic Chromedriver Discovery](https://github.com/ + * appium/appium/blob/master/docs/en/writing-running-appium/web/ + * chromedriver.md#automatic-discovery-of-compatible-chromedriver) + * article for more details. + * + * @param path Path to chromedrivers mapping file. + * @return self instance for chaining. + */ + default T setChromedriverChromeMappingFile(String path) { + return amend(CHROMEDRIVER_CHROME_MAPPING_FILE_OPTION, path); + } + + /** + * Get full path to the chromedrivers mapping file is located. + * + * @return Path to chromedrivers mapping file. + */ + default Optional getChromedriverChromeMappingFile() { + return Optional.ofNullable((String) getCapability(CHROMEDRIVER_CHROME_MAPPING_FILE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverDisableBuildCheckOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverDisableBuildCheckOption.java new file mode 100644 index 000000000..7757b0ed3 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverDisableBuildCheckOption.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.android.options.context; + +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 SupportsChromedriverDisableBuildCheckOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_DISABLE_BUILD_CHECK_OPTION = "chromedriverDisableBuildCheck"; + + /** + * Disables the compatibility validation between the current chromedriver + * and the destination browser/web view. + * + * @return self instance for chaining. + */ + default T chromedriverDisableBuildCheck() { + return amend(CHROMEDRIVER_DISABLE_BUILD_CHECK_OPTION, true); + } + + /** + * Being set to true disables the compatibility validation between the current + * chromedriver and the destination browser/web view. Use it with care. + * false by default. + * + * @param value Whether to enable the validation. + * @return self instance for chaining. + */ + default T setChromedriverDisableBuildCheck(boolean value) { + return amend(CHROMEDRIVER_DISABLE_BUILD_CHECK_OPTION, value); + } + + /** + * Get whether to disable the compatibility validation between the current + * chromedriver and the destination browser/web view. + * + * @return True or false. + */ + default Optional doesChromedriverDisableBuildCheck() { + return Optional.ofNullable(toSafeBoolean(getCapability(CHROMEDRIVER_DISABLE_BUILD_CHECK_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverExecutableDirOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverExecutableDirOption.java new file mode 100644 index 000000000..994021a5f --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverExecutableDirOption.java @@ -0,0 +1,52 @@ +/* + * 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.options.context; + +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 SupportsChromedriverExecutableDirOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_EXECUTABLE_DIR_OPTION = "chromedriverExecutableDir"; + + /** + * Full path to the folder where chromedriver executables are located. + * This folder is used then to store the downloaded chromedriver executables + * if automatic download is enabled. Read [Automatic Chromedriver + * Discovery](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/ + * web/chromedriver.md#automatic-discovery-of-compatible-chromedriver) + * article for more details. + * + * @param path Path to chromedriver executable. + * @return self instance for chaining. + */ + default T setChromedriverExecutableDir(String path) { + return amend(CHROMEDRIVER_EXECUTABLE_DIR_OPTION, path); + } + + /** + * Get full path to the folder where chromedriver executables are located. + * + * @return Path to chromedriver executable dir. + */ + default Optional getChromedriverExecutableDir() { + return Optional.ofNullable((String) getCapability(CHROMEDRIVER_EXECUTABLE_DIR_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverExecutableOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverExecutableOption.java new file mode 100644 index 000000000..4f73b42a2 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverExecutableOption.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.android.options.context; + +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 SupportsChromedriverExecutableOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_EXECUTABLE_OPTION = "chromedriverExecutable"; + + /** + * Full path to the chromedriver executable on the server file system. + * + * @param path Path to chromedriver executable. + * @return self instance for chaining. + */ + default T setChromedriverExecutable(String path) { + return amend(CHROMEDRIVER_EXECUTABLE_OPTION, path); + } + + /** + * Get the path to the chromedriver executable on the server file system.. + * + * @return Path to chromedriver executable. + */ + default Optional getChromedriverExecutable() { + return Optional.ofNullable((String) getCapability(CHROMEDRIVER_EXECUTABLE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverPortOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverPortOption.java new file mode 100644 index 000000000..fa6cc1f5a --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverPortOption.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.android.options.context; + +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 SupportsChromedriverPortOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_PORT_OPTION = "chromedriverPort"; + + /** + * The port number to use for Chromedriver communication. + * Any free port number is selected by default if unset. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setChromedriverPort(int port) { + return amend(CHROMEDRIVER_PORT_OPTION, port); + } + + /** + * Get the local port number to use for Chromedriver communication. + * + * @return Port number. + */ + default Optional getChromedriverPort() { + return Optional.ofNullable(toInteger(getCapability(CHROMEDRIVER_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverPortsOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverPortsOption.java new file mode 100644 index 000000000..eb0a4630c --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverPortsOption.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.android.options.context; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.List; +import java.util.Optional; + +public interface SupportsChromedriverPortsOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_PORTS_OPTION = "chromedriverPorts"; + + /** + * Array of possible port numbers to assign for Chromedriver communication. + * If none of the port in this array is free then a server error is thrown. + * + * @param ports one or more port numbers in range 0..65535 + * @return self instance for chaining. + */ + default T setChromedriverPorts(List ports) { + return amend(CHROMEDRIVER_PORTS_OPTION, ports); + } + + /** + * Get the local port number to use for Chromedriver communication. + * + * @return Port number. + */ + default Optional> getChromedriverPorts() { + //noinspection unchecked + return Optional.ofNullable((List) getCapability(CHROMEDRIVER_PORTS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverUseSystemExecutableOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverUseSystemExecutableOption.java new file mode 100644 index 000000000..62e653cb0 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsChromedriverUseSystemExecutableOption.java @@ -0,0 +1,61 @@ +/* + * 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.options.context; + +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 SupportsChromedriverUseSystemExecutableOption> extends + Capabilities, CanSetCapability { + String CHROMEDRIVER_USE_SYSTEM_EXECUTABLE_OPTION = "chromedriverUseSystemExecutable"; + + /** + * Enforce the usage of chromedriver, + * which gets downloaded by Appium automatically upon installation. + * + * @return self instance for chaining. + */ + default T chromedriverUseSystemExecutable() { + return amend(CHROMEDRIVER_USE_SYSTEM_EXECUTABLE_OPTION, true); + } + + /** + * Set it to true in order to enforce the usage of chromedriver, which gets + * downloaded by Appium automatically upon installation. This driver might not + * be compatible with the destination browser or a web view. false by default. + * + * @param value Whether to use the system chromedriver. + * @return self instance for chaining. + */ + default T setChromedriverUseSystemExecutable(boolean value) { + return amend(CHROMEDRIVER_USE_SYSTEM_EXECUTABLE_OPTION, value); + } + + /** + * Get whether to use the system chromedriver. + * + * @return True or false. + */ + default Optional doesChromedriverUseSystemExecutable() { + return Optional.ofNullable(toSafeBoolean(getCapability(CHROMEDRIVER_USE_SYSTEM_EXECUTABLE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsEnsureWebviewsHavePagesOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsEnsureWebviewsHavePagesOption.java new file mode 100644 index 000000000..d72cbe066 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsEnsureWebviewsHavePagesOption.java @@ -0,0 +1,60 @@ +/* + * 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.options.context; + +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 SupportsEnsureWebviewsHavePagesOption> extends + Capabilities, CanSetCapability { + String ENSURE_WEBVIEWS_HAVE_PAGES_OPTION = "ensureWebviewsHavePages"; + + /** + * Set to skip web views that have no pages from being shown in getContexts output. + * + * @return self instance for chaining. + */ + default T ensureWebviewsHavePages() { + return amend(ENSURE_WEBVIEWS_HAVE_PAGES_OPTION, true); + } + + /** + * Whether to skip web views that have no pages from being shown in getContexts + * output. The driver uses devtools connection to retrieve the information about + * existing pages. true by default since Appium 1.19.0, false if lower than 1.19.0. + * + * @param value Whether to ensure if web views have pages. + * @return self instance for chaining. + */ + default T setEnsureWebviewsHavePages(boolean value) { + return amend(ENSURE_WEBVIEWS_HAVE_PAGES_OPTION, value); + } + + /** + * Get whether to ensure if web views have pages. + * + * @return True or false. + */ + default Optional doesEnsureWebviewsHavePages() { + return Optional.ofNullable(toSafeBoolean(getCapability(ENSURE_WEBVIEWS_HAVE_PAGES_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsExtractChromeAndroidPackageFromContextNameOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsExtractChromeAndroidPackageFromContextNameOption.java new file mode 100644 index 000000000..447a1f2f4 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsExtractChromeAndroidPackageFromContextNameOption.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.android.options.context; + +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 SupportsExtractChromeAndroidPackageFromContextNameOption + > extends Capabilities, CanSetCapability { + String EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME_OPTION = + "extractChromeAndroidPackageFromContextName"; + + /** + * Tell chromedriver to attach to the android package we have associated + * with the context name, rather than the package of the application under test. + * + * @return self instance for chaining. + */ + default T extractChromeAndroidPackageFromContextName() { + return amend(EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME_OPTION, true); + } + + /** + * If set to true, tell chromedriver to attach to the android package we have associated + * with the context name, rather than the package of the application under test. + * false by default. + * + * @param value Whether to use the android package identifier associated with the context name. + * @return self instance for chaining. + */ + default T setExtractChromeAndroidPackageFromContextName(boolean value) { + return amend(EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME_OPTION, value); + } + + /** + * Get whether to use the android package identifier associated with the context name. + * + * @return True or false. + */ + default Optional doesExtractChromeAndroidPackageFromContextName() { + return Optional.ofNullable( + toSafeBoolean(getCapability(EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsNativeWebScreenshotOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsNativeWebScreenshotOption.java new file mode 100644 index 000000000..71e5934ff --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsNativeWebScreenshotOption.java @@ -0,0 +1,61 @@ +/* + * 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.options.context; + +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 SupportsNativeWebScreenshotOption> extends + Capabilities, CanSetCapability { + String NATIVE_WEB_SCREENSHOT_OPTION = "nativeWebScreenshot"; + + /** + * Enforce to use screenshoting endpoint provided by UiAutomator framework + * rather than the one provided by chromedriver. + * + * @return self instance for chaining. + */ + default T nativeWebScreenshot() { + return amend(NATIVE_WEB_SCREENSHOT_OPTION, true); + } + + /** + * Whether to use screenshoting endpoint provided by UiAutomator framework (true) + * rather than the one provided by chromedriver (false, the default value). + * Use it when you experience issues with the latter. + * + * @param value Whether to use native screenshots in web view context. + * @return self instance for chaining. + */ + default T setNativeWebScreenshot(boolean value) { + return amend(NATIVE_WEB_SCREENSHOT_OPTION, value); + } + + /** + * Get whether to use native screenshots in web view context. + * + * @return True or false. + */ + default Optional doesNativeWebScreenshot() { + return Optional.ofNullable(toSafeBoolean(getCapability(NATIVE_WEB_SCREENSHOT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsRecreateChromeDriverSessionsOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsRecreateChromeDriverSessionsOption.java new file mode 100644 index 000000000..a47ade424 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsRecreateChromeDriverSessionsOption.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.android.options.context; + +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 SupportsRecreateChromeDriverSessionsOption> extends + Capabilities, CanSetCapability { + String RECREATE_CHROME_DRIVER_SESSIONS = "recreateChromeDriverSessions"; + + /** + * Enforce chromedriver sessions to be killed and then recreated instead + * of just suspending it on context switch. + * + * @return self instance for chaining. + */ + default T recreateChromeDriverSessions() { + return amend(RECREATE_CHROME_DRIVER_SESSIONS, true); + } + + /** + * If this capability is set to true then chromedriver session is always going + * to be killed and then recreated instead of just suspending it on context + * switching. false by default. + * + * @param value Whether to recreate chromedriver sessions. + * @return self instance for chaining. + */ + default T setRecreateChromeDriverSessions(boolean value) { + return amend(RECREATE_CHROME_DRIVER_SESSIONS, value); + } + + /** + * Get whether chromedriver sessions should be killed and then recreated instead + * of just suspending it on context switch. + * + * @return True or false. + */ + default Optional doesRecreateChromeDriverSessions() { + return Optional.ofNullable(toSafeBoolean(getCapability(RECREATE_CHROME_DRIVER_SESSIONS))); + } +} 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 new file mode 100644 index 000000000..ef2b6f301 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsShowChromedriverLogOption.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.android.options.context; + +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 SupportsShowChromedriverLogOption> + extends Capabilities, CanSetCapability { + String SHOW_CHROMEDRIVER_LOG_OPTION = "showChromedriverLog"; + + /** + * Enforces all the output from chromedriver binary to be + * forwarded to the Appium server log. + * + * @return self instance for chaining. + */ + default T showChromedriverLog() { + return amend(SHOW_CHROMEDRIVER_LOG_OPTION, true); + } + + /** + * If set to true then all the output from chromedriver binary will be + * forwarded to the Appium server log. false by default. + * + * @param value Whether to forward chromedriver output to the Appium server log. + * @return self instance for chaining. + */ + default T setShowChromedriverLog(boolean value) { + return amend(SHOW_CHROMEDRIVER_LOG_OPTION, value); + } + + /** + * Get whether to forward chromedriver output to the Appium server log. + * + * @return True or false. + */ + default Optional doesShowChromedriverLog() { + return Optional.ofNullable( + toSafeBoolean(getCapability(SHOW_CHROMEDRIVER_LOG_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/context/SupportsWebviewDevtoolsPortOption.java b/src/main/java/io/appium/java_client/android/options/context/SupportsWebviewDevtoolsPortOption.java new file mode 100644 index 000000000..e48e73fad --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/context/SupportsWebviewDevtoolsPortOption.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.android.options.context; + +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 SupportsWebviewDevtoolsPortOption> extends + Capabilities, CanSetCapability { + String WEBVIEW_DEVTOOLS_PORT_OPTION = "webviewDevtoolsPort"; + + /** + * The local port number to use for devtools communication. By default, the first + * free port from 10900..11000 range is selected. Consider setting the custom + * value if you are running parallel tests. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setWebviewDevtoolsPort(int port) { + return amend(WEBVIEW_DEVTOOLS_PORT_OPTION, port); + } + + /** + * Get the local port number to use for devtools communication. + * + * @return Port number. + */ + default Optional getWebviewDevtoolsPort() { + return Optional.ofNullable(toInteger(getCapability(WEBVIEW_DEVTOOLS_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/localization/AppLocale.java b/src/main/java/io/appium/java_client/android/options/localization/AppLocale.java new file mode 100644 index 000000000..f8f0147f7 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/localization/AppLocale.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.android.options.localization; + +import io.appium.java_client.remote.options.BaseMapOptionData; + +import java.util.Map; +import java.util.Optional; + +public class AppLocale extends BaseMapOptionData { + public AppLocale() { + super(); + } + + public AppLocale(Map options) { + super(options); + } + + /** + * Language identifier. See + * https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers#language-identifiers + * for the list of available values. + * + * @param lang Language identifier, for example "zh". + * @return self instance for chaining. + */ + public AppLocale withLanguage(String lang) { + return assignOptionValue("language", lang); + } + + /** + * Get the language identifier. + * + * @return Language identifier. + */ + public Optional getLanguage() { + return getOptionValue("language"); + } + + /** + * Allows to set a country identifier. + * + * @param country Country identifier, for example "CN". + * @return self instance for chaining. + */ + public AppLocale withCountry(String country) { + return assignOptionValue("country", country); + } + + /** + * Get the country identifier. + * + * @return Country identifier. + */ + public Optional getCountry() { + return getOptionValue("country"); + } + + /** + * Allows to set an optional language variant value. + * + * @param variant Language variant, for example "Hans". + * @return self instance for chaining. + */ + public AppLocale withVariant(String variant) { + return assignOptionValue("variant", variant); + } + + /** + * Get the language variant. + * + * @return Language variant. + */ + public Optional getVariant() { + return getOptionValue("variant"); + } +} 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 new file mode 100644 index 000000000..d8fafba02 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/localization/SupportsAppLocaleOption.java @@ -0,0 +1,56 @@ +/* + * 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.options.localization; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +public interface SupportsAppLocaleOption> extends + Capabilities, CanSetCapability { + String APP_LOCALE_OPTION = "appLocale"; + + /** + * Sets the locale for the app under test. The main difference between this option + * and the above ones is that this option only changes the locale for the application + * under test and does not affect other parts of the system. Also, it only uses + * public APIs for its purpose. See + * https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers to get the + * list of available language abbreviations. + * Example: {"language": "zh", "country": "CN", "variant": "Hans"}. + * + * @param locale App locale data. + * @return this MobileOptions, for chaining. + */ + default T setAppLocale(AppLocale locale) { + return amend(APP_LOCALE_OPTION, locale.toMap()); + } + + /** + * Get the locale for the app under test. + * + * @return App locale data. + */ + default Optional getAppLocale() { + //noinspection unchecked + return Optional.ofNullable(getCapability(APP_LOCALE_OPTION)) + .map(v -> new AppLocale((Map) v)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/localization/SupportsLocaleScriptOption.java b/src/main/java/io/appium/java_client/android/options/localization/SupportsLocaleScriptOption.java new file mode 100644 index 000000000..835c7d41d --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/localization/SupportsLocaleScriptOption.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.android.options.localization; + +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 SupportsLocaleScriptOption> extends + Capabilities, CanSetCapability { + String LOCALE_SCRIPT_OPTION = "localeScript"; + + /** + * Set canonical name of the locale to be set for the app under test, + * for example zh-Hans-CN. + * See https://developer.android.com/reference/java/util/Locale.html for more details. + * + * @param localeScript is the language abbreviation. + * @return this MobileOptions, for chaining. + */ + default T setLocaleScript(String localeScript) { + return amend(LOCALE_SCRIPT_OPTION, localeScript); + } + + /** + * Get canonical name of the locale to be set for the app under test. + * + * @return Locale script value. + */ + default Optional getLocaleScript() { + return Optional.ofNullable((String) getCapability(LOCALE_SCRIPT_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/locking/SupportsSkipUnlockOption.java b/src/main/java/io/appium/java_client/android/options/locking/SupportsSkipUnlockOption.java new file mode 100644 index 000000000..0846dddfe --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/locking/SupportsSkipUnlockOption.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.android.options.locking; + +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 SupportsSkipUnlockOption> extends + Capabilities, CanSetCapability { + String SKIP_UNLOCK_OPTION = "skipUnlock"; + + /** + * Skip the check for lock screen presence. + * + * @return self instance for chaining. + */ + default T skipUnlock() { + return amend(SKIP_UNLOCK_OPTION, true); + } + + /** + * Whether to skip the check for lock screen presence (true). By default, + * UiAutomator2 driver tries to detect if the device's screen is locked + * before starting the test and to unlock that (which sometimes might be unstable). + * Note, that this operation takes some time, so it is highly recommended to set + * this capability to true and disable screen locking on devices under test. + * + * @param value Set it to true in order to skip screen unlock checks. + * @return self instance for chaining. + */ + default T setSkipUnlock(boolean value) { + return amend(SKIP_UNLOCK_OPTION, value); + } + + /** + * Get whether to skip the check for lock screen presence. + * + * @return True or false. + */ + default Optional doesSkipUnlock() { + return Optional.ofNullable(toSafeBoolean(getCapability(SKIP_UNLOCK_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockKeyOption.java b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockKeyOption.java new file mode 100644 index 000000000..011ca2c87 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockKeyOption.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.android.options.locking; + +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 SupportsUnlockKeyOption> extends + Capabilities, CanSetCapability { + String UNLOCK_KEY_OPTION = "unlockKey"; + + /** + * Allows to set an unlock key. + * Read [Unlock tutorial](https://github.com/appium/appium-android-driver/blob/master/docs/UNLOCK.md) + * for more details. + * + * @param unlockKey The unlock key. + * @return self instance for chaining. + */ + default T setUnlockKey(String unlockKey) { + return amend(UNLOCK_KEY_OPTION, unlockKey); + } + + /** + * Get the unlock key. + * + * @return Unlock key. + */ + default Optional getUnlockKey() { + return Optional.ofNullable((String) getCapability(UNLOCK_KEY_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockStrategyOption.java b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockStrategyOption.java new file mode 100644 index 000000000..6329d6861 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockStrategyOption.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.android.options.locking; + +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 SupportsUnlockStrategyOption> extends + Capabilities, CanSetCapability { + String UNLOCK_STRATEGY_OPTION = "unlockStrategy"; + + /** + * Either 'locksettings' (default) or 'uiautomator'. + * Setting it to 'uiautomator' will enforce the driver to avoid using special + * ADB shortcuts in order to speed up the unlock procedure. + * + * @param strategy The unlock strategy. + * @return self instance for chaining. + */ + default T setUnlockStrategy(String strategy) { + return amend(UNLOCK_STRATEGY_OPTION, strategy); + } + + /** + * Get the strategy key. + * + * @return Unlock strategy. + */ + default Optional getUnlockStrategy() { + return Optional.ofNullable((String) getCapability(UNLOCK_STRATEGY_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockSuccessTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockSuccessTimeoutOption.java new file mode 100644 index 000000000..487fbbdca --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockSuccessTimeoutOption.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.android.options.locking; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsUnlockSuccessTimeoutOption> extends + Capabilities, CanSetCapability { + String UNLOCK_SUCCESS_TIMEOUT_OPTION = "unlockSuccessTimeout"; + + /** + * Maximum timeout to wait until the device is unlocked. + * 2000 ms by default. + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setUnlockSuccessTimeout(Duration timeout) { + return amend(UNLOCK_SUCCESS_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout to wait until the device is unlocked. + * + * @return The timeout value. + */ + default Optional getUnlockSuccessTimeout() { + return Optional.ofNullable( + toDuration(getCapability(UNLOCK_SUCCESS_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockTypeOption.java b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockTypeOption.java new file mode 100644 index 000000000..76b749fef --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/locking/SupportsUnlockTypeOption.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.android.options.locking; + +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 SupportsUnlockTypeOption> extends + Capabilities, CanSetCapability { + String UNLOCK_TYPE_OPTION = "unlockType"; + + /** + * Set one of the possible types of Android lock screens to unlock. + * Read the [Unlock tutorial](https://github.com/appium/appium-android-driver/blob/master/docs/UNLOCK.md) + * for more details. + * + * @param unlockType One of possible unlock types. + * @return self instance for chaining. + */ + default T setUnlockType(String unlockType) { + return amend(UNLOCK_TYPE_OPTION, unlockType); + } + + /** + * Get the unlock type. + * + * @return Unlock type. + */ + default Optional getUnlockType() { + return Optional.ofNullable((String) getCapability(UNLOCK_TYPE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/mjpeg/SupportsMjpegScreenshotUrlOption.java b/src/main/java/io/appium/java_client/android/options/mjpeg/SupportsMjpegScreenshotUrlOption.java new file mode 100644 index 000000000..bd8111c94 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/mjpeg/SupportsMjpegScreenshotUrlOption.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.android.options.mjpeg; + +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.net.URL; +import java.util.Optional; + +public interface SupportsMjpegScreenshotUrlOption> extends + Capabilities, CanSetCapability { + String MJPEG_SCREENSHOT_URL_OPTION = "mjpegScreenshotUrl"; + + /** + * The URL of a service that provides realtime device screenshots in MJPEG format. + * If provided then the actual command to retrieve a screenshot will be + * requesting pictures from this service rather than directly from the server. + * + * @param url URL value. + * @return self instance for chaining. + */ + default T setMjpegScreenshotUrl(URL url) { + return amend(MJPEG_SCREENSHOT_URL_OPTION, url.toString()); + } + + /** + * The URL of a service that provides realtime device screenshots in MJPEG format. + * If provided then the actual command to retrieve a screenshot will be + * requesting pictures from this service rather than directly from the server. + * + * @param url URL value. + * @return self instance for chaining. + */ + default T setMjpegScreenshotUrl(String url) { + return amend(MJPEG_SCREENSHOT_URL_OPTION, url); + } + + /** + * Get URL of a service that provides realtime device screenshots in MJPEG format. + * + * @return URL value. + */ + default Optional getMjpegScreenshotUrl() { + return Optional.ofNullable(getCapability(MJPEG_SCREENSHOT_URL_OPTION)) + .map(CapabilityHelpers::toUrl); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/mjpeg/SupportsMjpegServerPortOption.java b/src/main/java/io/appium/java_client/android/options/mjpeg/SupportsMjpegServerPortOption.java new file mode 100644 index 000000000..2649a5eb0 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/mjpeg/SupportsMjpegServerPortOption.java @@ -0,0 +1,52 @@ +/* + * 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.options.mjpeg; + +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 SupportsMjpegServerPortOption> extends + Capabilities, CanSetCapability { + String MJPEG_SERVER_PORT_OPTION = "mjpegServerPort"; + + /** + * The number of the port UiAutomator2 server starts the MJPEG server on. + * If not provided then the screenshots broadcasting service on the remote + * device does not get exposed to a local port (e.g. no adb port forwarding + * is happening). + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setMjpegServerPort(int port) { + return amend(MJPEG_SERVER_PORT_OPTION, port); + } + + /** + * Get the number of the port UiAutomator2 server starts the MJPEG server on. + * + * @return Port number. + */ + default Optional getMjpegServerPort() { + return Optional.ofNullable(toInteger(getCapability(MJPEG_SERVER_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/other/SupportsDisableSuppressAccessibilityServiceOption.java b/src/main/java/io/appium/java_client/android/options/other/SupportsDisableSuppressAccessibilityServiceOption.java new file mode 100644 index 000000000..eab017873 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/other/SupportsDisableSuppressAccessibilityServiceOption.java @@ -0,0 +1,63 @@ +/* + * 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.options.other; + +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 SupportsDisableSuppressAccessibilityServiceOption> extends + Capabilities, CanSetCapability { + String DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE_OPTION = "disableSuppressAccessibilityService"; + + /** + * Tells the instrumentation process to not suppress accessibility services + * during the automated test. + * + * @return self instance for chaining. + */ + default T disableSuppressAccessibilityService() { + return amend(DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE_OPTION, true); + } + + /** + * Being set to true tells the instrumentation process to not suppress + * accessibility services during the automated test. This might be useful + * if your automated test needs these services. false by default. + * + * @param value Set it to true in order to suppress accessibility services. + * @return self instance for chaining. + */ + default T setDisableSuppressAccessibilityService(boolean value) { + return amend(DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE_OPTION, value); + } + + /** + * Get whether to suppress accessibility services. + * + * @return True or false. + */ + default Optional doesDisableSuppressAccessibilityService() { + return Optional.ofNullable( + toSafeBoolean(getCapability(DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/other/SupportsUserProfileOption.java b/src/main/java/io/appium/java_client/android/options/other/SupportsUserProfileOption.java new file mode 100644 index 000000000..0408abb1c --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/other/SupportsUserProfileOption.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.android.options.other; + +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 SupportsUserProfileOption> extends + Capabilities, CanSetCapability { + String USER_PROFILE_OPTION = "userProfile"; + + /** + * Integer identifier of a user profile. By default, the app under test is + * installed for the currently active user, but in case it is necessary to + * test how the app performs while being installed for a user profile, + * which is different from the current one, then this capability might + * come in handy. + * + * @param profileId User profile identifier. + * @return self instance for chaining. + */ + default T setUserProfile(int profileId) { + return amend(USER_PROFILE_OPTION, profileId); + } + + /** + * Get the integer identifier of a user profile. + * + * @return User profile id. + */ + default Optional getUserProfile() { + return Optional.ofNullable(toInteger(getCapability(USER_PROFILE_OPTION))); + } +} 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 new file mode 100644 index 000000000..4cbc571da --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/EspressoBuildConfig.java @@ -0,0 +1,317 @@ +/* + * 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.options.server; + +import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.remote.options.BaseMapOptionData; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class EspressoBuildConfig extends BaseMapOptionData { + public static final String TOOLS_VERSION = "toolsVersions"; + public static final String ADDITIONAL_APP_DEPENDENCIES = "additionalAppDependencies"; + public static final String ADDITIONAL_ANDROID_TEST_DEPENDENCIES + = "additionalAndroidTestDependencies"; + + public EspressoBuildConfig() { + super(); + } + + public EspressoBuildConfig(String json) { + super(json); + } + + private EspressoBuildConfig assignToolsVersionsField(String name, Object value) { + Optional> toolsVersionsOptional = getOptionValue(TOOLS_VERSION); + Map toolsVersions = toolsVersionsOptional.orElseGet(HashMap::new); + toolsVersions.put(name, value); + if (toolsVersionsOptional.isEmpty()) { + assignOptionValue(TOOLS_VERSION, toolsVersions); + } + return this; + } + + private Optional getToolsVersionsFieldValue(String name) { + Optional> toolsVersionsOptional = getOptionValue(TOOLS_VERSION); + //noinspection unchecked + return toolsVersionsOptional.map(v -> (R) v.getOrDefault(name, null)); + } + + /** + * Set Gradle version. + * By default, the version from the build.gradle is used. + * + * @param version E.g. "6.3". + * @return self instance for chaining. + */ + public EspressoBuildConfig withGradleVersion(String version) { + return assignToolsVersionsField("gradle", version); + } + + /** + * Get Gradle version. + * + * @return Gradle version. + */ + public Optional getGradleVersion() { + return getToolsVersionsFieldValue("gradle"); + } + + /** + * Set Gradle plugin version. It must correspond to the Gradle version + * (if provided). By default, the version from the build.gradle is used. + * + * @param version E.g. "4.1.1" + * @return self instance for chaining. + */ + public EspressoBuildConfig withAndroidGradlePluginVersion(String version) { + return assignToolsVersionsField("androidGradlePlugin", version); + } + + /** + * Get Gradle plugin version. + * + * @return Gradle plugin version. + */ + public Optional getAndroidGradlePluginVersion() { + return getToolsVersionsFieldValue("androidGradlePlugin"); + } + + /** + * Set Android build tools version to compile the server with. + * By default, the version from the build.gradle is used. + * + * @param version E.g. "28.0.3". + * @return self instance for chaining. + */ + public EspressoBuildConfig withBuildToolsVersion(String version) { + return assignToolsVersionsField("buildTools", version); + } + + /** + * Get Android build tools version. + * + * @return Android build tools version. + */ + public Optional getBuildToolsVersion() { + return getToolsVersionsFieldValue("buildTools"); + } + + /** + * Set Android SDK version to compile the server for. + * By default, the version from the app build.gradle is used. + * + * @param version E.g. "28" + * @return self instance for chaining. + */ + public EspressoBuildConfig withCompileSdkVersion(String version) { + return assignToolsVersionsField("compileSdk", version); + } + + /** + * Get the target Android SDK version. + * + * @return Android SDK version. + */ + public Optional getCompileSdkVersion() { + return getToolsVersionsFieldValue("compileSdk"); + } + + /** + * Set the version of the included androidx.compose.ui components to compile the server for. + * By default, the version from app's build.gradle is used. + * + * @param composeVersion E.g. "1.0.5" + * @return self instance for chaining. + */ + public EspressoBuildConfig withComposeVersion(String composeVersion) { + return assignToolsVersionsField("composeVersion", composeVersion); + } + + /** + * Get the version of included androidx.compose.ui components. + * + * @return Version of androidx.compose.ui components. + */ + public Optional getComposeVersion() { + return getToolsVersionsFieldValue("composeVersion"); + } + + /** + * Set the minimum version of JVM the project sources are compatible with. + * + * @param sourceCompatibility E.g. "VERSION_12" + * @return self instance for chaining. + */ + public EspressoBuildConfig withSourceCompatibility(String sourceCompatibility) { + return assignToolsVersionsField("sourceCompatibility", sourceCompatibility); + } + + /** + * Get the minimum version of JVM the project sources are compatible with. + * + * @return Minimum version of JVM the project sources are compatible with. + */ + public Optional getSourceCompatibility() { + return getToolsVersionsFieldValue("sourceCompatibility"); + } + + /** + * Set the target version of the generated JVM bytecode as a string. + * + * @param jvmTarget E.g. "1_10" or `1_8` + * @return self instance for chaining. + */ + public EspressoBuildConfig withJvmTarget(String jvmTarget) { + return assignToolsVersionsField("jvmTarget", jvmTarget); + } + + /** + * Get the target version of the generated JVM bytecode as a string. + * + * @return Target version of the generated JVM bytecode as a string. + */ + public Optional getJvmTarget() { + return getToolsVersionsFieldValue("jvmTarget"); + } + + /** + * Set the target version of JVM the project sources are compatible with. + * + * @param targetCompatibility E.g. "VERSION_12" + * @return self instance for chaining. + */ + public EspressoBuildConfig withTargetCompatibility(String targetCompatibility) { + return assignToolsVersionsField("targetCompatibility", targetCompatibility); + } + + /** + * Get the target version of JVM the project sources are compatible with. + * + * @return Target version of JVM the project sources are compatible with. + */ + public Optional getTargetCompatibility() { + return getToolsVersionsFieldValue("targetCompatibility"); + } + + /** + * Set the minimum Android SDK version to compile the server for. + * By default, the version from the app build.gradle is used. + * + * @param apiLevel E.g. 18. + * @return self instance for chaining. + */ + public EspressoBuildConfig withMinSdk(int apiLevel) { + return assignToolsVersionsField("minSdk", apiLevel); + } + + /** + * Get the minimum Android SDK version. + * + * @return Minimum Android SDK version. + */ + public Optional getMinSdkVersion() { + Optional result = getToolsVersionsFieldValue("minSdk"); + return result.map(CapabilityHelpers::toInteger); + } + + /** + * Set the target Android SDK version to compile the server for. + * By default, the version from the app build.gradle is used. + * + * @param apiLevel E.g. 28. + * @return self instance for chaining. + */ + public EspressoBuildConfig withTargetSdk(int apiLevel) { + return assignToolsVersionsField("targetSdk", apiLevel); + } + + /** + * Get the target Android SDK version. + * + * @return Target Android SDK version. + */ + public Optional getTargetSdkVersion() { + Optional result = getToolsVersionsFieldValue("targetSdk"); + return result.map(CapabilityHelpers::toInteger); + } + + /** + * Kotlin version to compile the server for. + * By default, the version from the build.gradle is used. + * + * @param version E.g. "1.5.10". + * @return self instance for chaining. + */ + public EspressoBuildConfig withKotlinVersion(String version) { + return assignToolsVersionsField("kotlin", version); + } + + /** + * Get the target Kotlin version. + * + * @return Kotlin version. + */ + public Optional getKotlinVersion() { + return getToolsVersionsFieldValue("kotlin"); + } + + /** + * Set a non-empty array of dependent module names with their versions. + * The scripts add all these items as "implementation" lines of dependencies + * category in the app build.gradle script. + * + * @param dependencies E.g. ["xerces.xercesImpl:2.8.0", "xerces.xmlParserAPIs:2.6.2"]. + * @return self instance for chaining. + */ + public EspressoBuildConfig withAdditionalAppDependencies(List dependencies) { + return assignOptionValue(ADDITIONAL_APP_DEPENDENCIES, dependencies); + } + + /** + * Get the array of dependent application module names with their versions. + * + * @return Dependent module names with their versions. + */ + public Optional> getAdditionalAppDependencies() { + return getOptionValue(ADDITIONAL_APP_DEPENDENCIES); + } + + /** + * Set a non-empty array of dependent module names with their versions. + * The scripts add all these items as "androidTestImplementation" lines of + * dependencies category in the app build.gradle script. + * + * @param dependencies E.g. ["xerces.xercesImpl:2.8.0", "xerces.xmlParserAPIs:2.6.2"]. + * @return self instance for chaining. + */ + public EspressoBuildConfig withAdditionalAndroidTestDependencies(List dependencies) { + return assignOptionValue(ADDITIONAL_ANDROID_TEST_DEPENDENCIES, dependencies); + } + + /** + * Get the array of dependent Android test module names with their versions. + * + * @return Dependent module names with their versions. + */ + public Optional> getAdditionalAndroidTestDependencies() { + return getOptionValue(ADDITIONAL_ANDROID_TEST_DEPENDENCIES); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsDisableWindowAnimationOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsDisableWindowAnimationOption.java new file mode 100644 index 000000000..c8acbaac9 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsDisableWindowAnimationOption.java @@ -0,0 +1,60 @@ +/* + * 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.options.server; + +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 SupportsDisableWindowAnimationOption> extends + Capabilities, CanSetCapability { + String DISABLE_WINDOWS_ANIMATION_OPTION = "disableWindowAnimation"; + + /** + * Disables window animations when starting the instrumentation process. + * + * @return self instance for chaining. + */ + default T disableWindowAnimation() { + return amend(DISABLE_WINDOWS_ANIMATION_OPTION, true); + } + + /** + * Set whether to disable window animations when starting the instrumentation process. + * false by default + * + * @param value True to disable window animations. + * @return self instance for chaining. + */ + default T setDisableWindowAnimation(boolean value) { + return amend(DISABLE_WINDOWS_ANIMATION_OPTION, value); + } + + /** + * Get whether window animations when starting the instrumentation process + * are disabled. + * + * @return True or false. + */ + default Optional doesDisableWindowAnimation() { + return Optional.ofNullable(toSafeBoolean(getCapability(DISABLE_WINDOWS_ANIMATION_OPTION))); + } +} 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 new file mode 100644 index 000000000..a916ec35b --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoBuildConfigOption.java @@ -0,0 +1,69 @@ +/* + * 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.options.server; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.internal.Either; + +import java.util.Optional; + +public interface SupportsEspressoBuildConfigOption> extends + Capabilities, CanSetCapability { + String ESPRESSO_BUILD_CONFIG_OPTION = "espressoBuildConfig"; + + /** + * This config allows to customize several important properties of + * Espresso server. Refer to + * https://github.com/appium/appium-espresso-driver#espresso-build-config + * for more information on how to properly construct such config. + * + * @param configPath The path to the config file on the server file system. + * @return self instance for chaining. + */ + default T setEspressoBuildConfig(String configPath) { + return amend(ESPRESSO_BUILD_CONFIG_OPTION, configPath); + } + + /** + * This config allows to customize several important properties of + * Espresso server. Refer to + * https://github.com/appium/appium-espresso-driver#espresso-build-config + * for more information on how to properly construct such config. + * + * @param config Config instance. + * @return self instance for chaining. + */ + default T setEspressoBuildConfig(EspressoBuildConfig config) { + return amend(ESPRESSO_BUILD_CONFIG_OPTION, config.toString()); + } + + /** + * Get the Espresso build config. + * + * @return Either the config itself or a path to a JSON file on the server FS. + */ + default Optional> getEspressoBuildConfig() { + return Optional.ofNullable(getCapability(ESPRESSO_BUILD_CONFIG_OPTION)) + .map(String::valueOf) + .map(v -> v.trim().startsWith("{") + ? Either.left(new EspressoBuildConfig(v)) + : Either.right(v) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoServerLaunchTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoServerLaunchTimeoutOption.java new file mode 100644 index 000000000..41bc66ee5 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsEspressoServerLaunchTimeoutOption.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.android.options.server; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsEspressoServerLaunchTimeoutOption> extends + Capabilities, CanSetCapability { + String ESPRESSO_SERVER_LAUNCH_TIMEOUT_OPTION = "espressoServerLaunchTimeout"; + + /** + * Set the maximum timeout to wait util Espresso is listening on the device. + * 45000 ms by default + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setUiautomator2ServerInstallTimeout(Duration timeout) { + return amend(ESPRESSO_SERVER_LAUNCH_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the maximum timeout to wait until Espresso server is listening on the device. + * + * @return The timeout value. + */ + default Optional getUiautomator2ServerInstallTimeout() { + return Optional.ofNullable( + toDuration(getCapability(ESPRESSO_SERVER_LAUNCH_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsForceEspressoRebuildOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsForceEspressoRebuildOption.java new file mode 100644 index 000000000..843d25ddc --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsForceEspressoRebuildOption.java @@ -0,0 +1,63 @@ +/* + * 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.options.server; + +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 SupportsForceEspressoRebuildOption> extends + Capabilities, CanSetCapability { + String FORCE_ESPRESSO_REBUILD_OPTION = "forceEspressoRebuild"; + + /** + * Enforces Espresso server rebuild on a new session startup. + * + * @return self instance for chaining. + */ + default T forceEspressoRebuild() { + return amend(FORCE_ESPRESSO_REBUILD_OPTION, true); + } + + /** + * Whether to always enforce Espresso server rebuild (true). + * By default, Espresso caches the already built server apk and only rebuilds + * it when it is necessary, because rebuilding process needs extra time. + * false by default. + * + * @param value True to force Espresso server rebuild on a new session startup. + * @return self instance for chaining. + */ + default T setForceEspressoRebuild(boolean value) { + return amend(FORCE_ESPRESSO_REBUILD_OPTION, value); + } + + /** + * Get to force Espresso server rebuild on a new session startup. + * + * @return True or false. + */ + default Optional doesForceEspressoRebuild() { + return Optional.ofNullable( + toSafeBoolean(getCapability(FORCE_ESPRESSO_REBUILD_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsShowGradleLogOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsShowGradleLogOption.java new file mode 100644 index 000000000..dabd0f0ec --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsShowGradleLogOption.java @@ -0,0 +1,61 @@ +/* + * 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.options.server; + +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 SupportsShowGradleLogOption> extends + Capabilities, CanSetCapability { + String SHOW_GRADLE_LOG_OPTION = "showGradleLog"; + + /** + * Enforces inclusion of the Gradle log to the regular server logs. + * + * @return self instance for chaining. + */ + default T showGradleLog() { + return amend(SHOW_GRADLE_LOG_OPTION, true); + } + + /** + * Whether to include Gradle log to the regular server logs while + * building Espresso server. false by default. + * + * @param value Whether to include Gradle log to the regular server logs. + * @return self instance for chaining. + */ + default T setShowGradleLog(boolean value) { + return amend(SHOW_GRADLE_LOG_OPTION, value); + } + + /** + * Get whether to include Gradle log to the regular server log. + * + * @return True or false. + */ + default Optional doesShowGradleLog() { + return Optional.ofNullable( + toSafeBoolean(getCapability(SHOW_GRADLE_LOG_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsSkipDeviceInitializationOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsSkipDeviceInitializationOption.java new file mode 100644 index 000000000..2b42327bb --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsSkipDeviceInitializationOption.java @@ -0,0 +1,60 @@ +/* + * 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.options.server; + +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 SupportsSkipDeviceInitializationOption> extends + Capabilities, CanSetCapability { + String SKIP_DEVICE_INITIALIZATION_OPTION = "skipDeviceInitialization"; + + /** + * Disables initial device startup checks by the server. + * + * @return self instance for chaining. + */ + default T skipDeviceInitialization() { + return amend(SKIP_DEVICE_INITIALIZATION_OPTION, true); + } + + /** + * If set to true then device startup checks (whether it is ready and whether + * Settings app is installed) will be canceled on session creation. + * Could speed up the session creation if you know what you are doing. false by default + * + * @param value True to skip device initialization. + * @return self instance for chaining. + */ + default T setSkipDeviceInitialization(boolean value) { + return amend(SKIP_DEVICE_INITIALIZATION_OPTION, value); + } + + /** + * Get whether initial device startup checks by the server are disabled. + * + * @return True or false. + */ + default Optional doesSkipDeviceInitialization() { + return Optional.ofNullable(toSafeBoolean(getCapability(SKIP_DEVICE_INITIALIZATION_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsSkipServerInstallationOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsSkipServerInstallationOption.java new file mode 100644 index 000000000..ad572b3f6 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsSkipServerInstallationOption.java @@ -0,0 +1,71 @@ +/* + * 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.options.server; + +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 SupportsSkipServerInstallationOption> extends + Capabilities, CanSetCapability { + String SKIP_SERVER_INSTALLATION_OPTION = "skipServerInstallation"; + + /** + * Enables skipping of the server components installation + * on the device under test and all the related checks. + * This could help to speed up the session startup if you know for sure the + * correct server version is installed on the device. + * In case the server is not installed or an incorrect version of it is installed + * then you may get an unexpected error later. + * + * @return self instance for chaining. + */ + default T skipServerInstallation() { + return amend(SKIP_SERVER_INSTALLATION_OPTION, true); + } + + /** + * Set whether to skip the server components installation + * on the device under test and all the related checks. + * This could help to speed up the session startup if you know for sure the + * correct server version is installed on the device. + * In case the server is not installed or an incorrect version of it is installed + * then you may get an unexpected error later. + * + * @param value True to skip the server installation. + * @return self instance for chaining. + */ + default T setSkipServerInstallation(boolean value) { + return amend(SKIP_SERVER_INSTALLATION_OPTION, value); + } + + /** + * Get whether to skip the server components installation + * on the device under test and all the related checks. + * + * @return True or false. + */ + default Optional doesSkipServerInstallation() { + return Optional.ofNullable( + toSafeBoolean(getCapability(SKIP_SERVER_INSTALLATION_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsSystemPortOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsSystemPortOption.java new file mode 100644 index 000000000..925b8ea3d --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsSystemPortOption.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.android.options.server; + +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 SupportsSystemPortOption> extends + Capabilities, CanSetCapability { + String SYSTEM_PORT_OPTION = "systemPort"; + + /** + * The number of the port the UiAutomator2 or Espresso server is listening on. + * By default, the first free port from 8200..8299 range is selected for UIA2 + * and 8300..8399 range is selected for Espresso. + * It is recommended to set this value if you are running parallel + * tests on the same machine. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setSystemPort(int port) { + return amend(SYSTEM_PORT_OPTION, port); + } + + /** + * Get the system port value. + * + * @return System port value + */ + default Optional getSystemPort() { + return Optional.ofNullable(toInteger(getCapability(SYSTEM_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerInstallTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerInstallTimeoutOption.java new file mode 100644 index 000000000..dac3b3b38 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerInstallTimeoutOption.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.android.options.server; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsUiautomator2ServerInstallTimeoutOption> extends + Capabilities, CanSetCapability { + String UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT_OPTION = "uiautomator2ServerInstallTimeout"; + + /** + * Set the maximum timeout to wait util UiAutomator2Server is installed on the device. + * 20000 ms by default + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setUiautomator2ServerInstallTimeout(Duration timeout) { + return amend(UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the maximum timeout to wait until UiAutomator2Server is installed on the device. + * + * @return The timeout value. + */ + default Optional getUiautomator2ServerInstallTimeout() { + return Optional.ofNullable( + toDuration(getCapability(UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerLaunchTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerLaunchTimeoutOption.java new file mode 100644 index 000000000..ea12f0d09 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerLaunchTimeoutOption.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.android.options.server; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsUiautomator2ServerLaunchTimeoutOption> extends + Capabilities, CanSetCapability { + String UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT_OPTION = "uiautomator2ServerLaunchTimeout"; + + /** + * Set the maximum timeout to wait util UiAutomator2Server is listening on + * the device. 30000 ms by default + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setUiautomator2ServerLaunchTimeout(Duration timeout) { + return amend(UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the maximum timeout to wait until UiAutomator2Server is listening on the device. + * + * @return The timeout value. + */ + default Optional getUiautomator2ServerLaunchTimeout() { + return Optional.ofNullable( + toDuration(getCapability(UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerReadTimeoutOption.java b/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerReadTimeoutOption.java new file mode 100644 index 000000000..699b73c48 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/server/SupportsUiautomator2ServerReadTimeoutOption.java @@ -0,0 +1,55 @@ +/* + * 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.options.server; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsUiautomator2ServerReadTimeoutOption> extends + Capabilities, CanSetCapability { + String UIAUTOMATOR2_SERVER_READ_TIMEOUT_OPTION = "uiautomator2ServerReadTimeout"; + + /** + * Set the maximum timeout to wait for a HTTP response from UiAutomator2Server. + * Only values greater than zero are accepted. If the given value is too low + * then expect driver commands to fail with timeout of Xms exceeded error. + * 240000 ms by default + * + * @param timeout Timeout value. + * @return self instance for chaining. + */ + default T setUiautomator2ServerReadTimeout(Duration timeout) { + return amend(UIAUTOMATOR2_SERVER_READ_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the maximum timeout to wait for a HTTP response from UiAutomator2Server. + * + * @return The timeout value. + */ + default Optional getUiautomator2ServerReadTimeout() { + return Optional.ofNullable( + toDuration(getCapability(UIAUTOMATOR2_SERVER_READ_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/IllegalCoordinatesException.java b/src/main/java/io/appium/java_client/android/options/signing/KeystoreConfig.java similarity index 70% rename from src/main/java/io/appium/java_client/IllegalCoordinatesException.java rename to src/main/java/io/appium/java_client/android/options/signing/KeystoreConfig.java index 8167ad65d..5ab0d78b6 100644 --- a/src/main/java/io/appium/java_client/IllegalCoordinatesException.java +++ b/src/main/java/io/appium/java_client/android/options/signing/KeystoreConfig.java @@ -14,15 +14,16 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.android.options.signing; -import org.openqa.selenium.WebDriverException; - -public class IllegalCoordinatesException extends WebDriverException { - private static final long serialVersionUID = 1L; - - public IllegalCoordinatesException(String message) { - super(message); - } +import lombok.Data; +import lombok.ToString; +@ToString() +@Data() +public class KeystoreConfig { + private final String path; + private final String password; + private final String keyAlias; + private final String keyPassword; } diff --git a/src/main/java/io/appium/java_client/android/options/signing/SupportsKeystoreOptions.java b/src/main/java/io/appium/java_client/android/options/signing/SupportsKeystoreOptions.java new file mode 100644 index 000000000..5fbfcc2ce --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/signing/SupportsKeystoreOptions.java @@ -0,0 +1,74 @@ +/* + * 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.options.signing; + +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 SupportsKeystoreOptions> extends + Capabilities, CanSetCapability { + String USE_KEYSTORE_OPTION = "useKeystore"; + String KEYSTORE_PATH_OPTION = "keystorePath"; + String KEYSTORE_PASSWORD_OPTION = "keystorePassword"; + String KEY_ALIAS_OPTION = "keyAlias"; + String KEY_PASSWORD_OPTION = "keyPassword"; + + /** + * Use a custom keystore to sign the app under test. + * + * @param keystoreConfig The keystore config to use. + * @return self instance for chaining. + */ + default T setKeystoreConfig(KeystoreConfig keystoreConfig) { + return amend(USE_KEYSTORE_OPTION, true) + .amend(KEYSTORE_PATH_OPTION, keystoreConfig.getPath()) + .amend(KEYSTORE_PASSWORD_OPTION, keystoreConfig.getPassword()) + .amend(KEY_ALIAS_OPTION, keystoreConfig.getKeyAlias()) + .amend(KEY_PASSWORD_OPTION, keystoreConfig.getKeyPassword()); + } + + /** + * Get whether to use a custom keystore. + * + * @return True or false. + */ + default Optional doesUseKeystore() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_KEYSTORE_OPTION))); + } + + /** + * Get the custom keystore config. + * + * @return The keystore config. + */ + default Optional getKeystoreConfig() { + if (!doesUseKeystore().orElse(false)) { + return Optional.empty(); + } + return Optional.of(new KeystoreConfig( + (String) getCapability(KEYSTORE_PATH_OPTION), + (String) getCapability(KEYSTORE_PASSWORD_OPTION), + (String) getCapability(KEY_ALIAS_OPTION), + (String) getCapability(KEY_PASSWORD_OPTION) + )); + } +} diff --git a/src/main/java/io/appium/java_client/android/options/signing/SupportsNoSignOption.java b/src/main/java/io/appium/java_client/android/options/signing/SupportsNoSignOption.java new file mode 100644 index 000000000..9518d7b8f --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/signing/SupportsNoSignOption.java @@ -0,0 +1,63 @@ +/* + * 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.options.signing; + +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 SupportsNoSignOption> extends + Capabilities, CanSetCapability { + String NO_SIGN_OPTION = "noSign"; + + /** + * Skips application signing. + * + * @return self instance for chaining. + */ + default T noSign() { + return amend(NO_SIGN_OPTION, true); + } + + /** + * Set it to true in order to skip application signing. By default, + * all apps are always signed with the default Appium debug signature + * if they don't have any. This capability cancels all the signing checks + * and makes the driver to use the application package as is. This + * capability does not affect .apks packages as these are expected to be + * already signed. + * + * @param value Whether to skip package signing. + * @return self instance for chaining. + */ + default T setNoSign(boolean value) { + return amend(NO_SIGN_OPTION, value); + } + + /** + * Get whether to skip package signing. + * + * @return True or false. + */ + default Optional doesNoSign() { + return Optional.ofNullable(toSafeBoolean(getCapability(NO_SIGN_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java b/src/main/java/io/appium/java_client/chromium/ChromiumDriver.java 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/events/DefaultAspect.java b/src/main/java/io/appium/java_client/events/DefaultAspect.java deleted file mode 100644 index 345ec77ee..000000000 --- a/src/main/java/io/appium/java_client/events/DefaultAspect.java +++ /dev/null @@ -1,572 +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.events; - -import static io.appium.java_client.events.DefaultBeanConfiguration.COMPONENT_BEAN; - -import com.google.common.collect.ImmutableList; - -import io.appium.java_client.events.api.Listener; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.After; -import org.aspectj.lang.annotation.AfterReturning; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.openqa.selenium.Alert; -import org.openqa.selenium.ContextAware; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.springframework.context.support.AbstractApplicationContext; - -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -@SuppressWarnings("unused") -@Aspect -class DefaultAspect { - - private static final List> listenable = ImmutableList.of(WebDriver.class, - WebElement.class, WebDriver.Navigation.class, WebDriver.TargetLocator.class, - ContextAware.class, Alert.class, WebDriver.Options.class, WebDriver.Window.class); - - private static final String EXECUTION_NAVIGATION_TO = "execution(* org.openqa.selenium.WebDriver." - + "Navigation.get(..)) || " - + "execution(* org.openqa.selenium.WebDriver.Navigation.to(..)) || " - + "execution(* org.openqa.selenium.WebDriver.get(..))"; - private static final String EXECUTION_NAVIGATION_BACK = "execution(* org.openqa.selenium.WebDriver." - + "Navigation.back(..))"; - private static final String EXECUTION_NAVIGATION_FORWARD = "execution(* org.openqa.selenium.WebDriver." - + "Navigation.forward(..))"; - private static final String EXECUTION_NAVIGATION_REFRESH = "execution(* org.openqa.selenium.WebDriver." - + "Navigation.refresh(..))"; - private static final String EXECUTION_SEARCH = "execution(* org.openqa.selenium.SearchContext." - + "findElement(..)) || " - + "execution(* org.openqa.selenium.SearchContext.findElements(..))"; - private static final String EXECUTION_CLICK = "execution(* org.openqa.selenium.WebElement.click(..))"; - private static final String EXECUTION_CHANGE_VALUE = "execution(* org.openqa.selenium.WebElement." - + "sendKeys(..)) || " - + "execution(* org.openqa.selenium.WebElement.clear(..)) || " - + "execution(* io.appium.java_client.android.AndroidElement.replaceValue(..)) || " - + "execution(* io.appium.java_client.MobileElement.setValue(..))"; - private static final String EXECUTION_SCRIPT = "execution(* org.openqa.selenium.JavascriptExecutor." - + "executeScript(..)) || " - + "execution(* org.openqa.selenium.JavascriptExecutor.executeAsyncScript(..))"; - private static final String EXECUTION_ALERT_ACCEPT = "execution(* org.openqa.selenium.Alert." - + "accept(..))"; - private static final String EXECUTION_ALERT_DISMISS = "execution(* org.openqa.selenium.Alert." - + "dismiss(..))"; - private static final String EXECUTION_ALERT_SEND_KEYS = "execution(* org.openqa.selenium.Alert." - + "sendKeys(..))"; - private static final String EXECUTION_WINDOW_SET_SIZE = "execution(* org.openqa.selenium." - + "WebDriver.Window.setSize(..))"; - private static final String EXECUTION_WINDOW_SET_POSITION = "execution(* org.openqa.selenium.WebDriver." - + "Window.setPosition(..))"; - private static final String EXECUTION_WINDOW_MAXIMIZE = "execution(* org.openqa.selenium.WebDriver." - + "Window.maximize(..))"; - private static final String EXECUTION_ROTATE = "execution(* org.openqa.selenium.Rotatable" - + ".rotate(..))"; - private static final String EXECUTION_CONTEXT = "execution(* org.openqa.selenium.ContextAware." - + "context(..))"; - private static final String EXECUTION_SWITCH_TO_WINDOW = "execution(* org.openqa.selenium.WebDriver.TargetLocator" - + ".window(..))"; - private static final String EXECUTION_TAKE_SCREENSHOT_AS = "execution(* org.openqa.selenium.TakesScreenshot" - + ".getScreenshotAs(..))"; - private static final String AROUND = "execution(* org.openqa.selenium.WebDriver.*(..)) || " - + "execution(* org.openqa.selenium.WebElement.*(..)) || " - + "execution(* org.openqa.selenium.WebDriver.Navigation.*(..)) || " - + "execution(* org.openqa.selenium.WebDriver.Options.*(..)) || " - + "execution(* org.openqa.selenium.WebDriver.TargetLocator.*(..)) || " - + "execution(* org.openqa.selenium.WebDriver.TargetLocator.*(..)) || " - + "execution(* org.openqa.selenium.JavascriptExecutor.*(..)) || " - + "execution(* org.openqa.selenium.ContextAware.*(..)) || " - + "execution(* io.appium.java_client.FindsByAccessibilityId.*(..)) || " - + "execution(* io.appium.java_client.FindsByAndroidUIAutomator.*(..)) || " - + "execution(* io.appium.java_client.FindsByAndroidDataMatcher.*(..)) || " - + "execution(* io.appium.java_client.FindsByAndroidViewMatcher.*(..)) || " - + "execution(* io.appium.java_client.FindsByWindowsAutomation.*(..)) || " - + "execution(* io.appium.java_client.FindsByIosNSPredicate.*(..)) || " - + "execution(* org.openqa.selenium.internal.FindsByClassName.*(..)) || " - + "execution(* org.openqa.selenium.internal.FindsByCssSelector.*(..)) || " - + "execution(* org.openqa.selenium.internal.FindsById.*(..)) || " - + "execution(* org.openqa.selenium.internal.FindsByLinkText.*(..)) || " - + "execution(* org.openqa.selenium.internal.FindsByName.*(..)) || " - + "execution(* org.openqa.selenium.internal.FindsByTagName.*(..)) || " - + "execution(* org.openqa.selenium.internal.FindsByXPath.*(..)) || " - + "execution(* org.openqa.selenium.WebDriver.Window.*(..)) || " - + "execution(* io.appium.java_client.android.AndroidElement.*(..)) || " - + "execution(* io.appium.java_client.ios.IOSElement.*(..)) || " - + "execution(* io.appium.java_client.android.AndroidDriver.*(..)) || " - + "execution(* io.appium.java_client.ios.IOSDriver.*(..)) || " - + "execution(* io.appium.java_client.AppiumDriver.*(..)) || " - + "execution(* io.appium.java_client.MobileElement.*(..)) || " - + "execution(* org.openqa.selenium.remote.RemoteWebDriver.*(..)) || " - + "execution(* org.openqa.selenium.remote.RemoteWebElement.*(..)) || " - + "execution(* org.openqa.selenium.Alert.*(..)) || " - + "execution(* org.openqa.selenium.TakesScreenshot.*(..))"; - - private final AbstractApplicationContext context; - private final WebDriver driver; - private final DefaultListener listener = new DefaultListener(); - - private static Throwable getRootCause(Throwable thrown) { - Class throwableClass = thrown.getClass(); - - if (!InvocationTargetException.class.equals(throwableClass) && !RuntimeException.class.equals(throwableClass)) { - return thrown; - } - if (thrown.getCause() != null) { - return getRootCause(thrown.getCause()); - } - return thrown; - } - - private static Class getClassForProxy(Class classOfObject) { - Class returnStatement = null; - for (Class c : listenable) { - if (!c.isAssignableFrom(classOfObject)) { - continue; - } - returnStatement = c; - } - return returnStatement; - } - - DefaultAspect(AbstractApplicationContext context, WebDriver driver) { - this.context = context; - this.driver = driver; - } - - private Object transformToListenable(Object toBeTransformed) { - if (toBeTransformed == null) { - return null; - } - - Object result = toBeTransformed; - if (getClassForProxy(toBeTransformed.getClass()) != null) { - result = context.getBean(COMPONENT_BEAN, toBeTransformed); - } - return result; - } - - private List returnProxyList(List originalList) { - try { - List proxyList = new ArrayList<>(); - for (Object o : originalList) { - if (getClassForProxy(o.getClass()) == null) { - proxyList.add(o); - } else { - proxyList.add(context.getBean(COMPONENT_BEAN, o)); - } - } - return proxyList; - } catch (Exception e) { - throw e; - } - - } - - public void add(Collection listeners) { - listener.add(listeners); - } - - @Before(EXECUTION_NAVIGATION_TO) - public void beforeNavigateTo(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeNavigateTo(String.valueOf(joinPoint.getArgs()[0]), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_NAVIGATION_TO) - public void afterNavigateTo(JoinPoint joinPoint) throws Throwable { - try { - listener.afterNavigateTo(String.valueOf(joinPoint.getArgs()[0]), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_NAVIGATION_BACK) - public void beforeNavigateBack(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeNavigateBack(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_NAVIGATION_BACK) - public void afterNavigateBack(JoinPoint joinPoint) throws Throwable { - try { - listener.afterNavigateBack(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_NAVIGATION_FORWARD) - public void beforeNavigateForward(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeNavigateForward(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_NAVIGATION_FORWARD) - public void afterNavigateForward(JoinPoint joinPoint) throws Throwable { - try { - listener.afterNavigateForward(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_NAVIGATION_REFRESH) - public void beforeNavigateRefresh(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeNavigateRefresh(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_NAVIGATION_REFRESH) - public void afterNavigateRefresh(JoinPoint joinPoint) throws Throwable { - try { - listener.afterNavigateRefresh(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @SuppressWarnings("unchecked") - private T castArgument(JoinPoint joinPoint, int argIndex) { - return (T) joinPoint.getArgs()[argIndex]; - } - - @SuppressWarnings("unchecked") - private T castTarget(JoinPoint joinPoint) { - return (T) joinPoint.getTarget(); - } - - @Before(EXECUTION_SEARCH) - public void beforeFindBy(JoinPoint joinPoint) throws Throwable { - try { - Object target = joinPoint.getTarget(); - if (!WebElement.class.isAssignableFrom(target.getClass())) { - listener.beforeFindBy(castArgument(joinPoint, 0), null, driver); - } else { - listener.beforeFindBy(castArgument(joinPoint, 0), - castTarget(joinPoint), driver); - } - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_SEARCH) - public void afterFindBy(JoinPoint joinPoint) throws Throwable { - try { - Object target = joinPoint.getTarget(); - if (!WebElement.class.isAssignableFrom(target.getClass())) { - listener.afterFindBy(castArgument(joinPoint, 0), null, driver); - } else { - listener.afterFindBy(castArgument(joinPoint, 0), - castTarget(joinPoint), driver); - } - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_CLICK) - public void beforeClickOn(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeClickOn(castTarget(joinPoint), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_CLICK) - public void afterClickOn(JoinPoint joinPoint) throws Throwable { - try { - listener.afterClickOn(castTarget(joinPoint), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_CHANGE_VALUE) - public void beforeChangeValueOf(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeChangeValueOf(castTarget(joinPoint), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_CHANGE_VALUE) - public void afterChangeValueOf(JoinPoint joinPoint) throws Throwable { - try { - listener.afterChangeValueOf(castTarget(joinPoint), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_SCRIPT) - public void beforeScript(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeScript(String.valueOf(joinPoint.getArgs()[0]), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_SCRIPT) - public void afterScript(JoinPoint joinPoint) throws Throwable { - try { - listener.afterScript(String.valueOf(joinPoint.getArgs()[0]), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_ALERT_ACCEPT) - public void beforeAlertAccept(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeAlertAccept(driver, castTarget(joinPoint)); - listener.beforeAlertAccept(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_ALERT_ACCEPT) - public void afterAlertAccept(JoinPoint joinPoint) throws Throwable { - try { - listener.afterAlertAccept(driver, castTarget(joinPoint)); - listener.afterAlertAccept(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_ALERT_DISMISS) - public void beforeAlertDismiss(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeAlertDismiss(driver, castTarget(joinPoint)); - listener.beforeAlertDismiss(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_ALERT_DISMISS) - public void afterAlertDismiss(JoinPoint joinPoint) throws Throwable { - try { - listener.afterAlertDismiss(driver, castTarget(joinPoint)); - listener.afterAlertDismiss(driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_ALERT_SEND_KEYS) - public void beforeAlertSendKeys(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeAlertSendKeys(driver, castTarget(joinPoint), - String.valueOf(joinPoint.getArgs()[0])); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_ALERT_SEND_KEYS) - public void afterAlertSendKeys(JoinPoint joinPoint) throws Throwable { - try { - listener.afterAlertSendKeys(driver, castTarget(joinPoint), - String.valueOf(joinPoint.getArgs()[0])); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_WINDOW_SET_SIZE) - public void beforeWindowIsResized(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeWindowChangeSize(driver, - castTarget(joinPoint), castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_WINDOW_SET_SIZE) - public void afterWindowIsResized(JoinPoint joinPoint) throws Throwable { - try { - listener.afterWindowChangeSize(driver, castTarget(joinPoint), - castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_WINDOW_SET_POSITION) - public void beforeWindowIsMoved(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeWindowIsMoved(driver, castTarget(joinPoint), - castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_WINDOW_SET_POSITION) - public void afterWindowIsMoved(JoinPoint joinPoint) throws Throwable { - try { - listener.afterWindowIsMoved(driver, castTarget(joinPoint), - castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_WINDOW_MAXIMIZE) - public void beforeMaximization(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeWindowIsMaximized(driver, castTarget(joinPoint)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_WINDOW_MAXIMIZE) - public void afterMaximization(JoinPoint joinPoint) throws Throwable { - try { - listener.afterWindowIsMaximized(driver, castTarget(joinPoint)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_SWITCH_TO_WINDOW) - public void beforeSwitchToWindow(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeSwitchToWindow(castArgument(joinPoint, 0), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_SWITCH_TO_WINDOW) - public void afterSwitchToWindow(JoinPoint joinPoint) throws Throwable { - try { - listener.afterSwitchToWindow(castArgument(joinPoint, 0), driver); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_TAKE_SCREENSHOT_AS) - public void beforeTakeScreenShot(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeGetScreenshotAs(castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @AfterReturning(value = EXECUTION_TAKE_SCREENSHOT_AS, returning = "result") - public void afterTakeScreenShot(JoinPoint joinPoint, Object result) throws Throwable { - try { - listener.afterGetScreenshotAs(castArgument(joinPoint, 0), result); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_ROTATE) - public void beforeRotation(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeRotation(driver, castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - - } - - @After(EXECUTION_ROTATE) - public void afterRotation(JoinPoint joinPoint) throws Throwable { - try { - listener.afterRotation(driver, castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Before(EXECUTION_CONTEXT) - public void beforeSwitchingToContext(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeSwitchingToContext(driver, String.valueOf(joinPoint.getArgs()[0])); - } catch (Throwable t) { - throw getRootCause(t); - } - - } - - @After(EXECUTION_CONTEXT) - public void afterSwitchingToContextn(JoinPoint joinPoint) throws Throwable { - try { - listener.afterSwitchingToContext(driver, String.valueOf(joinPoint.getArgs()[0])); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @Around(AROUND) - public Object doAround(ProceedingJoinPoint point) throws Throwable { - Throwable t = null; - Object result = null; - try { - result = point.proceed(); - } catch (Throwable e) { - t = e; - } - if (t != null) { - Throwable rootCause = getRootCause(t); - listener.onException(rootCause, driver); - throw rootCause; - } - - if (result == null) { // maybe it was "void" - return null; - } - if (List.class.isAssignableFrom(result.getClass())) { - return returnProxyList((List) (result)); - } - - return transformToListenable(result); - } -} diff --git a/src/main/java/io/appium/java_client/events/DefaultBeanConfiguration.java b/src/main/java/io/appium/java_client/events/DefaultBeanConfiguration.java deleted file mode 100644 index b50a36ee3..000000000 --- a/src/main/java/io/appium/java_client/events/DefaultBeanConfiguration.java +++ /dev/null @@ -1,64 +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.events; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.context.annotation.Scope; -import org.springframework.context.support.AbstractApplicationContext; - -import java.util.ArrayList; -import java.util.List; - -@Configuration -@EnableAspectJAutoProxy(proxyTargetClass = true) -class DefaultBeanConfiguration { - public static final String LISTENABLE_OBJECT = "object"; - public static final String COMPONENT_BEAN = "component"; - - protected final List listeners = new ArrayList<>(); - protected WebDriver driver; - protected AbstractApplicationContext context; - - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - @Bean(name = LISTENABLE_OBJECT) - public T init(T t, WebDriver driver, List listeners, - AbstractApplicationContext context) { - this.driver = driver; - this.listeners.addAll(listeners); - this.context = context; - return t; - } - - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - @Bean(name = COMPONENT_BEAN) - public T getComponent(T t) { - return t; - } - - @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) - @Bean(name = "defaultAspect") - public DefaultAspect getAspect() { - DefaultAspect aspect = new DefaultAspect(context, driver); - aspect.add(listeners); - return aspect; - } -} diff --git a/src/main/java/io/appium/java_client/events/DefaultListener.java b/src/main/java/io/appium/java_client/events/DefaultListener.java deleted file mode 100644 index b260b1875..000000000 --- a/src/main/java/io/appium/java_client/events/DefaultListener.java +++ /dev/null @@ -1,258 +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.events; - -import io.appium.java_client.events.api.Listener; -import io.appium.java_client.events.api.general.AlertEventListener; -import io.appium.java_client.events.api.general.AppiumWebDriverEventListener; -import io.appium.java_client.events.api.general.ElementEventListener; -import io.appium.java_client.events.api.general.JavaScriptEventListener; -import io.appium.java_client.events.api.general.ListensToException; -import io.appium.java_client.events.api.general.NavigationEventListener; -import io.appium.java_client.events.api.general.SearchingEventListener; -import io.appium.java_client.events.api.general.WindowEventListener; -import io.appium.java_client.events.api.mobile.ContextEventListener; -import io.appium.java_client.events.api.mobile.RotationEventListener; -import org.openqa.selenium.Alert; -import org.openqa.selenium.By; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.Point; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.events.WebDriverEventListener; - -import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -class DefaultListener - implements Listener, AppiumWebDriverEventListener, ListensToException, SearchingEventListener, - NavigationEventListener, JavaScriptEventListener, ElementEventListener, AlertEventListener, - WindowEventListener, ContextEventListener, RotationEventListener { - - private final List listeners = new ArrayList<>(); - - private Object dispatcher = Proxy.newProxyInstance(Listener.class.getClassLoader(), - new Class[] {AlertEventListener.class, ContextEventListener.class, - ElementEventListener.class, JavaScriptEventListener.class, ListensToException.class, - NavigationEventListener.class, RotationEventListener.class, - SearchingEventListener.class, WindowEventListener.class, WebDriverEventListener.class}, - new ListenerInvocationHandler(listeners)); - - @Override public void beforeNavigateTo(String url, WebDriver driver) { - ((NavigationEventListener) dispatcher).beforeNavigateTo(url, driver); - } - - @Override public void afterNavigateTo(String url, WebDriver driver) { - ((NavigationEventListener) dispatcher).afterNavigateTo(url, driver); - } - - @Override public void beforeNavigateBack(WebDriver driver) { - ((NavigationEventListener) dispatcher).beforeNavigateBack(driver); - } - - @Override public void afterNavigateBack(WebDriver driver) { - ((NavigationEventListener) dispatcher).afterNavigateBack(driver); - } - - @Override public void beforeNavigateForward(WebDriver driver) { - ((NavigationEventListener) dispatcher).beforeNavigateForward(driver); - } - - @Override public void afterNavigateForward(WebDriver driver) { - ((NavigationEventListener) dispatcher).afterNavigateForward(driver); - } - - @Override public void beforeNavigateRefresh(WebDriver driver) { - ((NavigationEventListener) dispatcher).beforeNavigateRefresh(driver); - } - - @Override public void afterNavigateRefresh(WebDriver driver) { - ((NavigationEventListener) dispatcher).afterNavigateRefresh(driver); - } - - @Override public void beforeFindBy(By by, WebElement element, WebDriver driver) { - ((SearchingEventListener) dispatcher).beforeFindBy(by, element, driver); - } - - @Override public void afterFindBy(By by, WebElement element, WebDriver driver) { - ((SearchingEventListener) dispatcher).afterFindBy(by, element, driver); - } - - @Override public void beforeClickOn(WebElement element, WebDriver driver) { - ((ElementEventListener) dispatcher).beforeClickOn(element, driver); - } - - @Override public void afterClickOn(WebElement element, WebDriver driver) { - ((ElementEventListener) dispatcher).afterClickOn(element, driver); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver) { - ((ElementEventListener) dispatcher).beforeChangeValueOf(element, driver); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - ((ElementEventListener) dispatcher).beforeChangeValueOf(element, driver, keysToSend); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver) { - ((ElementEventListener) dispatcher).afterChangeValueOf(element, driver); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - ((ElementEventListener) dispatcher).afterChangeValueOf(element, driver, keysToSend); - } - - @Override public void beforeScript(String script, WebDriver driver) { - ((JavaScriptEventListener) dispatcher).beforeScript(script, driver); - } - - @Override public void afterScript(String script, WebDriver driver) { - ((JavaScriptEventListener) dispatcher).afterScript(script, driver); - } - - @Override public void onException(Throwable throwable, WebDriver driver) { - ((ListensToException) dispatcher).onException(throwable, driver); - } - - @Override - public void beforeGetScreenshotAs(OutputType target) { - ((WebDriverEventListener) dispatcher).beforeGetScreenshotAs(target); - } - - @Override - public void afterGetScreenshotAs(OutputType target, X screenshot) { - ((WebDriverEventListener) dispatcher).afterGetScreenshotAs(target, screenshot); - } - - @Override - public void beforeGetText(WebElement element, WebDriver driver) { - ((ElementEventListener) dispatcher).beforeGetText(element, driver); - } - - @Override - public void afterGetText(WebElement element, WebDriver driver, String text) { - ((ElementEventListener) dispatcher).afterGetText(element, driver, text); - } - - public void add(Collection listeners) { - this.listeners.addAll(listeners); - } - - @Override public void beforeAlertAccept(WebDriver driver, Alert alert) { - ((AlertEventListener) dispatcher).beforeAlertAccept(driver, alert); - } - - @Override - public void beforeAlertAccept(WebDriver driver) { - ((WebDriverEventListener) dispatcher).beforeAlertAccept(driver); - } - - @Override public void afterAlertAccept(WebDriver driver, Alert alert) { - ((AlertEventListener) dispatcher).afterAlertAccept(driver, alert); - } - - @Override - public void afterAlertAccept(WebDriver driver) { - ((WebDriverEventListener) dispatcher).afterAlertAccept(driver); - } - - @Override public void afterAlertDismiss(WebDriver driver, Alert alert) { - ((AlertEventListener) dispatcher).afterAlertDismiss(driver, alert); - } - - @Override - public void afterAlertDismiss(WebDriver driver) { - ((WebDriverEventListener) dispatcher).afterAlertDismiss(driver); - } - - @Override public void beforeAlertDismiss(WebDriver driver, Alert alert) { - ((AlertEventListener) dispatcher).beforeAlertDismiss(driver, alert); - } - - @Override - public void beforeAlertDismiss(WebDriver driver) { - ((WebDriverEventListener) dispatcher).beforeAlertDismiss(driver); - } - - @Override public void beforeAlertSendKeys(WebDriver driver, Alert alert, String keys) { - ((AlertEventListener) dispatcher).beforeAlertSendKeys(driver, alert, keys); - } - - @Override public void afterAlertSendKeys(WebDriver driver, Alert alert, String keys) { - ((AlertEventListener) dispatcher).afterAlertSendKeys(driver, alert, keys); - } - - @Override public void beforeWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize) { - ((WindowEventListener) dispatcher).beforeWindowChangeSize(driver, window, targetSize); - } - - @Override public void afterWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize) { - ((WindowEventListener) dispatcher).afterWindowChangeSize(driver, window, targetSize); - } - - @Override - public void beforeWindowIsMoved(WebDriver driver, WebDriver.Window window, Point targetPoint) { - ((WindowEventListener) dispatcher).beforeWindowIsMoved(driver, window, targetPoint); - } - - @Override - public void afterWindowIsMoved(WebDriver driver, WebDriver.Window window, Point targetPoint) { - ((WindowEventListener) dispatcher).afterWindowIsMoved(driver, window, targetPoint); - } - - @Override public void beforeWindowIsMaximized(WebDriver driver, WebDriver.Window window) { - ((WindowEventListener) dispatcher).beforeWindowIsMaximized(driver, window); - } - - @Override public void afterWindowIsMaximized(WebDriver driver, WebDriver.Window window) { - ((WindowEventListener) dispatcher).afterWindowIsMaximized(driver, window); - } - - @Override - public void beforeSwitchToWindow(String windowName, WebDriver driver) { - ((WebDriverEventListener) dispatcher).beforeSwitchToWindow(windowName, driver); - } - - @Override - public void afterSwitchToWindow(String windowName, WebDriver driver) { - ((WebDriverEventListener) dispatcher).afterSwitchToWindow(windowName, driver); - } - - @Override public void beforeSwitchingToContext(WebDriver driver, String context) { - ((ContextEventListener) dispatcher).beforeSwitchingToContext(driver, context); - } - - @Override public void afterSwitchingToContext(WebDriver driver, String context) { - ((ContextEventListener) dispatcher).afterSwitchingToContext(driver, context); - } - - @Override public void beforeRotation(WebDriver driver, ScreenOrientation orientation) { - ((RotationEventListener) dispatcher).beforeRotation(driver, orientation); - } - - @Override public void afterRotation(WebDriver driver, ScreenOrientation orientation) { - ((RotationEventListener) dispatcher).afterRotation(driver, orientation); - } -} diff --git a/src/main/java/io/appium/java_client/events/EventFiringObjectFactory.java b/src/main/java/io/appium/java_client/events/EventFiringObjectFactory.java deleted file mode 100644 index 7b5d723c0..000000000 --- a/src/main/java/io/appium/java_client/events/EventFiringObjectFactory.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.appium.java_client.events; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.AbstractApplicationContext; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.ServiceLoader; - -public class EventFiringObjectFactory { - - /** - * This method makes an event firing object. - * - * @param t an original {@link Object} that is - * supposed to be listenable - * @param driver an instance of {@link org.openqa.selenium.WebDriver} - * @param listeners is a collection of {@link io.appium.java_client.events.api.Listener} that - * is supposed to be used for the event firing - * @param T - * @return an {@link Object} that fires events - */ - @SuppressWarnings("unchecked") - public static T getEventFiringObject(T t, WebDriver driver, Collection listeners) { - final List listenerList = new ArrayList<>(); - - for (Listener listener : ServiceLoader.load( - Listener.class)) { - listenerList.add(listener); - } - - listeners.stream().filter(listener -> !listenerList.contains(listener)).forEach(listenerList::add); - - AbstractApplicationContext context = new AnnotationConfigApplicationContext( - DefaultBeanConfiguration.class); - return (T) context.getBean( - DefaultBeanConfiguration.LISTENABLE_OBJECT, t, driver, listenerList, context); - } - - /** - * This method makes an event firing object. - * - * @param t an original {@link Object} that is - * supposed to be listenable - * @param driver an instance of {@link org.openqa.selenium.WebDriver} - * @param T - * @return an {@link Object} that fires events - */ - public static T getEventFiringObject(T t, WebDriver driver) { - return getEventFiringObject(t, driver, Collections.emptyList()); - } - - /** - * This method makes an event firing object. - * - * @param t an original {@link Object} that is - * supposed to be listenable - * @param driver an instance of {@link org.openqa.selenium.WebDriver} - * @param listeners is an array of {@link io.appium.java_client.events.api.Listener} that - * is supposed to be used for the event firing - * - * @param T - * @return an instance of {@link org.openqa.selenium.WebDriver} that fires events - */ - public static T getEventFiringObject(T t, WebDriver driver, Listener ... listeners) { - return getEventFiringObject(t, driver, Arrays.asList(listeners)); - } -} diff --git a/src/main/java/io/appium/java_client/events/EventFiringWebDriverFactory.java b/src/main/java/io/appium/java_client/events/EventFiringWebDriverFactory.java deleted file mode 100644 index 7b53dc346..000000000 --- a/src/main/java/io/appium/java_client/events/EventFiringWebDriverFactory.java +++ /dev/null @@ -1,67 +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.events; - -import static io.appium.java_client.events.EventFiringObjectFactory.getEventFiringObject; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; - -import java.util.Collection; - -public class EventFiringWebDriverFactory { - - /** - * This method makes an event firing instance of {@link org.openqa.selenium.WebDriver}. - * - * @param driver an original instance of {@link org.openqa.selenium.WebDriver} that is - * supposed to be listenable - * @param T - * @return an instance of {@link org.openqa.selenium.WebDriver} that fires events - */ - public static T getEventFiringWebDriver(T driver) { - return getEventFiringObject(driver, driver); - } - - /** - * This method makes an event firing instance of {@link org.openqa.selenium.WebDriver}. - * - * @param driver an original instance of {@link org.openqa.selenium.WebDriver} that is - * supposed to be listenable - * @param listeners is a set of {@link io.appium.java_client.events.api.Listener} that - * is supposed to be used for the event firing - * @param T - * @return an instance of {@link org.openqa.selenium.WebDriver} that fires events - */ - public static T getEventFiringWebDriver(T driver, Listener ... listeners) { - return getEventFiringObject(driver, driver, listeners); - } - - /** - * This method makes an event firing instance of {@link org.openqa.selenium.WebDriver}. - * - * @param driver an original instance of {@link org.openqa.selenium.WebDriver} that is - * supposed to be listenable - * @param listeners is a collection of {@link io.appium.java_client.events.api.Listener} that - * is supposed to be used for the event firing - * @param T - * @return an instance of {@link org.openqa.selenium.WebDriver} that fires events - */ - public static T getEventFiringWebDriver(T driver, Collection listeners) { - return getEventFiringObject(driver, driver, listeners); - } -} diff --git a/src/main/java/io/appium/java_client/events/ListenerInvocationHandler.java b/src/main/java/io/appium/java_client/events/ListenerInvocationHandler.java deleted file mode 100644 index d6e51c0ce..000000000 --- a/src/main/java/io/appium/java_client/events/ListenerInvocationHandler.java +++ /dev/null @@ -1,62 +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.events; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.support.events.WebDriverEventListener; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.util.List; - -class ListenerInvocationHandler implements InvocationHandler { - - private final List listeners; - - ListenerInvocationHandler(List listeners) { - this.listeners = listeners; - } - - private Method findElementInWebDriverEventListener(Method m) { - try { - return WebDriverEventListener.class.getMethod(m.getName(), m.getParameterTypes()); - } catch (NoSuchMethodException e) { - return null; - } - } - - @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - for (Listener l: listeners) { - boolean isInvoked = false; - if (method.getDeclaringClass().isAssignableFrom(l.getClass())) { - method.invoke(l, args); - isInvoked = true; - } - - if (isInvoked) { - continue; - } - - Method webDriverEventListenerMethod = findElementInWebDriverEventListener(method); - if (webDriverEventListenerMethod != null - && WebDriverEventListener.class.isAssignableFrom(l.getClass())) { - webDriverEventListenerMethod.invoke(l, args); - } - } - return null; - } -} diff --git a/src/main/java/io/appium/java_client/events/api/general/AlertEventListener.java b/src/main/java/io/appium/java_client/events/api/general/AlertEventListener.java deleted file mode 100644 index 2a96d1792..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/AlertEventListener.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.events.api.general; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.Alert; -import org.openqa.selenium.WebDriver; - -public interface AlertEventListener extends Listener { - - /** - * This action will be performed each time before {@link Alert#accept()}. - * - * @param driver WebDriver - * @param alert {@link Alert} which is being accepted - */ - void beforeAlertAccept(WebDriver driver, Alert alert); - - /** - * This action will be performed each time after {@link Alert#accept()}. - * - * @param driver WebDriver - * @param alert {@link Alert} which has been accepted - */ - void afterAlertAccept(WebDriver driver, Alert alert); - - /** - * This action will be performed each time before {@link Alert#dismiss()}. - * - * @param driver WebDriver - * @param alert {@link Alert} which which is being dismissed - */ - void afterAlertDismiss(WebDriver driver, Alert alert); - - /** - * This action will be performed each time after {@link Alert#dismiss()}. - * - * @param driver WebDriver - * @param alert {@link Alert} which has been dismissed - */ - void beforeAlertDismiss(WebDriver driver, Alert alert); - - /** - * This action will be performed each time before {@link Alert#sendKeys(String)}. - * - * @param driver WebDriver - * @param alert {@link Alert} which is receiving keys - * @param keys Keys which are being sent - */ - void beforeAlertSendKeys(WebDriver driver, Alert alert, String keys); - - /** - * This action will be performed each time after {@link Alert#sendKeys(String)}. - * - * @param driver WebDriver - * @param alert {@link Alert} which has received keys - * @param keys Keys which have been sent - */ - void afterAlertSendKeys(WebDriver driver, Alert alert, String keys); -} diff --git a/src/main/java/io/appium/java_client/events/api/general/AppiumWebDriverEventListener.java b/src/main/java/io/appium/java_client/events/api/general/AppiumWebDriverEventListener.java deleted file mode 100644 index 5b249b3f2..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/AppiumWebDriverEventListener.java +++ /dev/null @@ -1,25 +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.events.api.general; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.support.events.WebDriverEventListener; - -public interface AppiumWebDriverEventListener extends Listener, WebDriverEventListener, ListensToException, - SearchingEventListener, NavigationEventListener, - JavaScriptEventListener, ElementEventListener { -} diff --git a/src/main/java/io/appium/java_client/events/api/general/ElementEventListener.java b/src/main/java/io/appium/java_client/events/api/general/ElementEventListener.java deleted file mode 100644 index 1f0f9be3e..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/ElementEventListener.java +++ /dev/null @@ -1,97 +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.events.api.general; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public interface ElementEventListener extends Listener { - /** - * Called before {@link WebElement#click WebElement.click()}. - * - * @param driver WebDriver - * @param element the WebElement being used for the action - */ - void beforeClickOn(WebElement element, WebDriver driver); - - /** - * Called after {@link WebElement#click WebElement.click()}. - * Not called, if an exception is thrown. - * - * @param driver WebDriver - * @param element the WebElement being used for the action - */ - void afterClickOn(WebElement element, WebDriver driver); - - /** - * Called before {@link WebElement#clear WebElement.clear()}, - * {@link WebElement#sendKeys WebElement.sendKeys(...)}. - * - * @param driver WebDriver - * @param element the WebElement being used for the action - */ - void beforeChangeValueOf(WebElement element, WebDriver driver); - - /** - * Called before {@link WebElement#clear WebElement.clear()}, - * {@link WebElement#sendKeys WebElement.sendKeys(...)}. - * - * @param driver WebDriver - * @param element the WebElement being used for the action - * @param keysToSend character sequence to send to the element - */ - void beforeChangeValueOf(WebElement element, WebDriver driver, CharSequence[] keysToSend); - - /** - * Called after {@link WebElement#clear WebElement.clear()}, - * {@link WebElement#sendKeys WebElement.sendKeys(...)}. - * Not called, if an exception is thrown. - * - * @param driver WebDriver - * @param element the WebElement being used for the action - */ - void afterChangeValueOf(WebElement element, WebDriver driver); - - /** - * Called after {@link WebElement#clear WebElement.clear()}, - * {@link WebElement#sendKeys WebElement.sendKeys(...)} . - * Not called, if an exception is thrown. - * - * @param driver WebDriver - * @param element the WebElement being used for the action - * @param keysToSend character sequence to send to the element - */ - void afterChangeValueOf(WebElement element, WebDriver driver, CharSequence[] keysToSend); - - /** - * Called before {@link WebElement#getText()} method is being called. - * - * @param element - {@link WebElement} against which call is being made - * @param driver - instance of {@link WebDriver} - */ - void beforeGetText(WebElement element, WebDriver driver); - - /** - * Called right after {@link WebElement#getText()} method is being called. - * - * @param element - {@link WebElement} against which call is being made - * @param driver - instance of {@link WebDriver} - * @param text - {@link String} object extracted from respective {@link WebElement} - */ - void afterGetText(WebElement element, WebDriver driver, String text); -} diff --git a/src/main/java/io/appium/java_client/events/api/general/JavaScriptEventListener.java b/src/main/java/io/appium/java_client/events/api/general/JavaScriptEventListener.java deleted file mode 100644 index f7addb68e..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/JavaScriptEventListener.java +++ /dev/null @@ -1,41 +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.events.api.general; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; - -public interface JavaScriptEventListener extends Listener { - /** - * Called before - * {@link org.openqa.selenium.JavascriptExecutor#executeScript(String, Object[]) }. - * - * @param driver WebDriver - * @param script the script to be executed - */ - void beforeScript(String script, WebDriver driver); - - /** - * Called after - * {@link org.openqa.selenium.remote.RemoteWebDriver#executeScript(String, Object[]) }. - * Not called if an exception is thrown - * - * @param driver WebDriver - * @param script the script that was executed - */ - void afterScript(String script, WebDriver driver); -} diff --git a/src/main/java/io/appium/java_client/events/api/general/NavigationEventListener.java b/src/main/java/io/appium/java_client/events/api/general/NavigationEventListener.java deleted file mode 100644 index d8662960f..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/NavigationEventListener.java +++ /dev/null @@ -1,89 +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.events.api.general; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; - -public interface NavigationEventListener extends Listener { - - /** - * Called before {@link org.openqa.selenium.WebDriver#get get(String url)} - * respectively {@link org.openqa.selenium.WebDriver.Navigation#to - * navigate().to(String url)}. - * - * @param url URL - * @param driver WebDriver - */ - void beforeNavigateTo(String url, WebDriver driver); - - /** - * Called after {@link org.openqa.selenium.WebDriver#get get(String url)} - * respectively {@link org.openqa.selenium.WebDriver.Navigation#to - * navigate().to(String url)}. Not called, if an exception is thrown. - * - * @param url URL - * @param driver WebDriver - */ - void afterNavigateTo(String url, WebDriver driver); - - /** - * Called before {@link org.openqa.selenium.WebDriver.Navigation#back - * navigate().back()}. - * - * @param driver WebDriver - */ - void beforeNavigateBack(WebDriver driver); - - /** - * Called after {@link org.openqa.selenium.WebDriver.Navigation - * navigate().back()}. Not called, if an exception is thrown. - * - * @param driver WebDriver - */ - void afterNavigateBack(WebDriver driver); - - /** - * Called before {@link org.openqa.selenium.WebDriver.Navigation#forward - * navigate().forward()}. - * - * @param driver WebDriver - */ - void beforeNavigateForward(WebDriver driver); - - /** - * Called after {@link org.openqa.selenium.WebDriver.Navigation#forward - * navigate().forward()}. Not called, if an exception is thrown. - * - * @param driver WebDriver - */ - void afterNavigateForward(WebDriver driver); - - /** - * Called before {@link org.openqa.selenium.WebDriver.Navigation#refresh navigate().refresh()}. - * - * @param driver WebDriver - */ - void beforeNavigateRefresh(WebDriver driver); - - /** - * Called after {@link org.openqa.selenium.WebDriver.Navigation#refresh navigate().refresh()}. - * - * @param driver WebDriver - */ - void afterNavigateRefresh(WebDriver driver); -} diff --git a/src/main/java/io/appium/java_client/events/api/general/SearchingEventListener.java b/src/main/java/io/appium/java_client/events/api/general/SearchingEventListener.java deleted file mode 100644 index bd5d47ae3..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/SearchingEventListener.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.events.api.general; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public interface SearchingEventListener extends Listener { - - /** - * Called before {@link org.openqa.selenium.WebDriver#findElement WebDriver.findElement(...)}, - * or - * {@link org.openqa.selenium.WebDriver#findElements WebDriver.findElements(...)}, or - * {@link org.openqa.selenium.WebElement#findElement WebElement.findElement(...)}, or - * {@link org.openqa.selenium.WebElement#findElement WebElement.findElements(...)}. - * - * @param element will be null, if a find method of WebDriver - * is called. - * @param by locator being used - * @param driver WebDriver - */ - void beforeFindBy(By by, WebElement element, WebDriver driver); - - /** - * Called after {@link org.openqa.selenium.WebDriver#findElement WebDriver.findElement(...)}, - * or - * {@link org.openqa.selenium.WebDriver#findElements WebDriver.findElements(...)}, or - * {@link org.openqa.selenium.WebElement#findElement WebElement.findElement(...)}, or - * {@link org.openqa.selenium.WebElement#findElement WebElement.findElements(...)}. - * - * @param element will be null, if a find method of WebDriver - * is called. - * @param by locator being used - * @param driver WebDriver - */ - void afterFindBy(By by, WebElement element, WebDriver driver); -} diff --git a/src/main/java/io/appium/java_client/events/api/general/WindowEventListener.java b/src/main/java/io/appium/java_client/events/api/general/WindowEventListener.java deleted file mode 100644 index 056043a59..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/WindowEventListener.java +++ /dev/null @@ -1,99 +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.events.api.general; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.Point; -import org.openqa.selenium.WebDriver; - -public interface WindowEventListener extends Listener { - /** - * This action will be performed each time before {@link WebDriver.Window#setSize(Dimension)}. - * - * @param driver WebDriver - * @param window is the window whose size is going to be changed - * @param targetSize is the new size - */ - void beforeWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize); - - /** - * This action will be performed each time after {@link WebDriver.Window#setSize(Dimension)}. - * - * @param driver WebDriver - * @param window is the window whose size has been changed - * @param targetSize is the new size - */ - void afterWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize); - - /** - * This action will be performed each time before {@link WebDriver.Window#setPosition(Point)}. - * - * @param driver WebDriver - * @param window is the window whose position is going to be changed - * @param targetPoint is the new window coordinates - */ - void beforeWindowIsMoved(WebDriver driver, WebDriver.Window window, - Point targetPoint); - - /** - * This action will be performed each time after {@link WebDriver.Window#setPosition(Point)}. - * - * @param driver WebDriver - * @param window is the window whose position has been changed - * @param targetPoint is the new window coordinates - */ - void afterWindowIsMoved(WebDriver driver, WebDriver.Window window, - Point targetPoint); - - - /** - * This action will be performed each time before {@link WebDriver.Window#maximize()}. - * - * @param driver WebDriver - * @param window is the window which is going to be maximized - */ - void beforeWindowIsMaximized(WebDriver driver, WebDriver.Window window); - - /** - * This action will be performed each time after {@link WebDriver.Window#maximize()}. - * - * @param driver WebDriver - * @param window is the window which has been maximized - */ - void afterWindowIsMaximized(WebDriver driver, WebDriver.Window window); - - /** - * This action will be performed each time before - * {@link org.openqa.selenium.WebDriver.TargetLocator#window(java.lang.String)}. - * - * @param windowName the name of the window to switch - * @param driver WebDriver - */ - void beforeSwitchToWindow(String windowName, WebDriver driver); - - /** - * This action will be performed each time after - * {@link org.openqa.selenium.WebDriver.TargetLocator#window(java.lang.String)}. - * - * @param windowName the name of the window to switch - * @param driver WebDriver - */ - void afterSwitchToWindow(String windowName, WebDriver driver); -} diff --git a/src/main/java/io/appium/java_client/events/api/mobile/RotationEventListener.java b/src/main/java/io/appium/java_client/events/api/mobile/RotationEventListener.java deleted file mode 100644 index a500c3d76..000000000 --- a/src/main/java/io/appium/java_client/events/api/mobile/RotationEventListener.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.events.api.mobile; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; - -public interface RotationEventListener extends Listener { - - /** - * Called before {@link org.openqa.selenium.Rotatable#rotate(ScreenOrientation)}. - * - * @param driver WebDriver - * @param orientation the desired screen orientation - */ - void beforeRotation(WebDriver driver, ScreenOrientation orientation); - - /** - * Called after {@link org.openqa.selenium.Rotatable#rotate(ScreenOrientation)}. - * - * @param driver WebDriver - * @param orientation the desired screen orientation - */ - void afterRotation(WebDriver driver, ScreenOrientation orientation); -} 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 new file mode 100644 index 000000000..07cb9e3e7 --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/GeckoDriver.java @@ -0,0 +1,142 @@ +/* + * 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.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; +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; + +/** + * GeckoDriver is an officially supported Appium driver + * created to automate Mobile browsers and web views based on + * the Gecko engine. The driver uses W3C + * WebDriver protocol and is built on top of Mozilla's geckodriver + * server. Read https://github.com/appium/appium-geckodriver + * for more details on how to configure and use it. + * + * @since Appium 1.20.0 + */ +public class GeckoDriver extends AppiumDriver { + private static final String AUTOMATION_NAME = AutomationName.GECKO; + + public GeckoDriver(HttpCommandExecutor executor, Capabilities capabilities) { + super(executor, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public GeckoDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public GeckoDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public GeckoDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public GeckoDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(service, httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public GeckoDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public GeckoDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(builder, httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME)); + } + + public GeckoDriver(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 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}. + * For example: + * + *
+     *
+     * ClientConfig clientConfig = ClientConfig.defaultConfig()
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * GeckoOptions options = new GeckoOptions();
+     * GeckoDriver driver = new GeckoDriver(clientConfig, options);
+     *
+     * 
+ * + * @param clientConfig take a look at {@link ClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public GeckoDriver(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));
+     * 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) { + super(ensureAutomationName(capabilities, AUTOMATION_NAME)); + } +} 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 new file mode 100644 index 000000000..2e1b4f1fd --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/options/GeckoOptions.java @@ -0,0 +1,69 @@ +/* + * 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.gecko.options; + +import io.appium.java_client.mac.options.SupportsSystemPortOption; +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsAcceptInsecureCertsOption; +import io.appium.java_client.remote.options.SupportsBrowserNameOption; +import io.appium.java_client.remote.options.SupportsBrowserVersionOption; +import io.appium.java_client.remote.options.SupportsPageLoadStrategyOption; +import io.appium.java_client.remote.options.SupportsProxyOption; +import io.appium.java_client.remote.options.SupportsSetWindowRectOption; +import io.appium.java_client.remote.options.SupportsUnhandledPromptBehaviorOption; +import org.openqa.selenium.Capabilities; + +import java.util.Map; + +/** + * Provides options specific to the Geckodriver. + * + *

For more details, refer to the + * capabilities documentation

+ */ +public class GeckoOptions extends BaseOptions implements + SupportsBrowserNameOption, + SupportsBrowserVersionOption, + SupportsMarionettePortOption, + SupportsSystemPortOption, + SupportsVerbosityOption, + SupportsAndroidStorageOption, + SupportsMozFirefoxOptionsOption, + SupportsAcceptInsecureCertsOption, + SupportsPageLoadStrategyOption, + SupportsSetWindowRectOption, + SupportsProxyOption, + SupportsUnhandledPromptBehaviorOption { + public GeckoOptions() { + setCommonOptions(); + } + + public GeckoOptions(Capabilities source) { + super(source); + setCommonOptions(); + } + + public GeckoOptions(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setAutomationName(AutomationName.GECKO); + } +} diff --git a/src/main/java/io/appium/java_client/gecko/options/SupportsAndroidStorageOption.java b/src/main/java/io/appium/java_client/gecko/options/SupportsAndroidStorageOption.java new file mode 100644 index 000000000..b85438271 --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/options/SupportsAndroidStorageOption.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.gecko.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 SupportsAndroidStorageOption> extends + Capabilities, CanSetCapability { + String ANDROID_STORAGE_OPTION = "androidStorage"; + + /** + * See + * https://firefox-source-docs.mozilla.org/testing/geckodriver + * /Flags.html#code-android-storage-var-android-storage-var-code + * + * @param storage One of supported Android storage types. + * @return self instance for chaining. + */ + default T setAndroidStorage(String storage) { + return amend(ANDROID_STORAGE_OPTION, storage); + } + + /** + * Get the currently set storage type. + * + * @return String representing the name of the device. + */ + default Optional getAndroidStorage() { + return Optional.ofNullable((String) getCapability(ANDROID_STORAGE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/gecko/options/SupportsMarionettePortOption.java b/src/main/java/io/appium/java_client/gecko/options/SupportsMarionettePortOption.java new file mode 100644 index 000000000..75bbcbf60 --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/options/SupportsMarionettePortOption.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.gecko.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 SupportsMarionettePortOption> extends + Capabilities, CanSetCapability { + String MARIONETTE_PORT_OPTION = "marionettePort"; + + /** + * Selects the port for Geckodriver’s connection to the Marionette + * remote protocol. The existing Firefox instance must have Marionette + * enabled. To enable the remote protocol in Firefox, you can pass the + * -marionette flag. Unless the marionette.port preference has been + * user-set, Marionette will listen on port 2828, which is the default + * value for this capability. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setMarionettePort(int port) { + return amend(MARIONETTE_PORT_OPTION, port); + } + + /** + * Get the number of the port for the Marionette server to listen on. + * + * @return Marionette port value. + */ + default Optional getMarionettePort() { + return Optional.ofNullable(toInteger(getCapability(MARIONETTE_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/gecko/options/SupportsMozFirefoxOptionsOption.java b/src/main/java/io/appium/java_client/gecko/options/SupportsMozFirefoxOptionsOption.java new file mode 100644 index 000000000..16b2d9579 --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/options/SupportsMozFirefoxOptionsOption.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.gecko.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.Map; +import java.util.Optional; + +public interface SupportsMozFirefoxOptionsOption> extends + Capabilities, CanSetCapability { + String MOZ_FIREFOX_OPTIONS_OPTION = "moz:firefoxOptions"; + + /** + * See https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions. + * + * @param options Firefox options mapping. + * @return self instance for chaining. + */ + default T setMozFirefoxOptions(Map options) { + return amend(MOZ_FIREFOX_OPTIONS_OPTION, options); + } + + /** + * Get Firefox options mapping. + * + * @return Firefox options mapping. + */ + default Optional> getMozFirefoxOptions() { + //noinspection unchecked + return Optional.ofNullable((Map) getCapability(MOZ_FIREFOX_OPTIONS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/gecko/options/SupportsSystemPortOption.java b/src/main/java/io/appium/java_client/gecko/options/SupportsSystemPortOption.java new file mode 100644 index 000000000..db89a31f4 --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/options/SupportsSystemPortOption.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.gecko.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 SupportsSystemPortOption> extends + Capabilities, CanSetCapability { + String SYSTEM_PORT_OPTION = "systemPort"; + + /** + * The number of the port for the driver to listen on. Must be unique + * for each session. If not provided then Appium will try to detect + * it automatically. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setSystemPort(int port) { + return amend(SYSTEM_PORT_OPTION, port); + } + + /** + * Get the number of the port for the internal server to listen on. + * + * @return System port value. + */ + default Optional getSystemPort() { + return Optional.ofNullable(toInteger(getCapability(SYSTEM_PORT_OPTION))); + } +} 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 new file mode 100644 index 000000000..32d37f61e --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/options/SupportsVerbosityOption.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.gecko.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 java.util.Locale.ROOT; + +public interface SupportsVerbosityOption> extends + Capabilities, CanSetCapability { + String VERBOSITY_OPTION = "verbosity"; + + /** + * The verbosity level of driver logging. + * By default, minimum verbosity is applied. + * + * @param verbosity Verbosity value. + * @return self instance for chaining. + */ + default T setVerbosity(Verbosity verbosity) { + return amend(VERBOSITY_OPTION, verbosity.name().toLowerCase(ROOT)); + } + + /** + * Get the verbosity level of driver logging. + * + * @return Verbosity value. + */ + default Optional getVerbosity() { + return Optional.ofNullable(getCapability(VERBOSITY_OPTION)) + .map(String::valueOf) + .map(verbosity -> verbosity.toUpperCase(ROOT)) + .map(Verbosity::valueOf); + } +} diff --git a/src/main/java/io/appium/java_client/events/api/Listener.java b/src/main/java/io/appium/java_client/gecko/options/Verbosity.java similarity index 84% rename from src/main/java/io/appium/java_client/events/api/Listener.java rename to src/main/java/io/appium/java_client/gecko/options/Verbosity.java index e1ea35c1d..f9a5c599e 100644 --- a/src/main/java/io/appium/java_client/events/api/Listener.java +++ b/src/main/java/io/appium/java_client/gecko/options/Verbosity.java @@ -14,10 +14,8 @@ * limitations under the License. */ -package io.appium.java_client.events.api; +package io.appium.java_client.gecko.options; -/** - * This interface just marks event listeners. - */ -public interface Listener { +public enum Verbosity { + DEBUG, TRACE } 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 baad10d2a..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,14 +16,16 @@ 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; + private Integer matchNeighbourThreshold; /** * At what normalized threshold to reject an occurrence. @@ -36,11 +38,38 @@ public OccurrenceMatchingOptions withThreshold(double threshold) { return this; } + /** + * Whether to enable the support of multiple image occurrences. + * + * @since Appium 1.21.0 + * @return self instance for chaining. + */ + public OccurrenceMatchingOptions enableMultiple() { + this.multiple = true; + return this; + } + + /** + * The pixel distance between matches we consider + * to be part of the same template match. This option is only + * considered if multiple matches mode is enabled. + * 10 pixels by default. + * + * @since Appium 1.21.0 + * @param threshold The threshold value in pixels. + * @return self instance for chaining. + */ + public OccurrenceMatchingOptions withMatchNeighbourThreshold(int threshold) { + this.matchNeighbourThreshold = threshold; + return this; + } + @Override public Map build() { - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.putAll(super.build()); - ofNullable(threshold).map(x -> builder.put("threshold", 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 273747247..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,23 +18,170 @@ 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 SCORE = "score"; - public OccurrenceMatchingResult(Map input) { + private final boolean hasMultiple; + + public OccurrenceMatchingResult(Object input) { super(input); + hasMultiple = input instanceof List; } /** - * Returns rectangle of partial image occurrence. + * Check whether the current instance contains multiple matches. + * + * @return True or false. + */ + public boolean hasMultiple() { + return hasMultiple; + } + + /** + * 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); + } + + /** + * Returns the list of multiple matches (if any). + * This property only works if the `multiple` option is enabled. + * + * @since Appium 1.21.0 + * @return The list containing properties of each single match or an empty list. + * @throws IllegalStateException If the current instance does not represent multiple matches. + */ + public List getMultiple() { + 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 + 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 b97da189c..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,15 +16,22 @@ 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; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; 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. @@ -46,7 +53,7 @@ public static T getCapability(Capabilities caps, String name, Class expec if (caps.getCapability(capName) == null) { continue; } - + if (expectedType == String.class) { return expectedType.cast(String.valueOf(caps.getCapability(capName))); } @@ -56,4 +63,119 @@ public static T getCapability(Capabilities caps, String name, Class expec } return null; } + + /** + * Converts generic capability value to boolean without + * throwing exceptions. + * + * @param value The capability value. + * @return null is the passed value is null otherwise the converted value. + */ + @Nullable + public static Boolean toSafeBoolean(Object value) { + return value == null ? null : Boolean.parseBoolean(String.valueOf(value)); + } + + /** + * Converts generic capability value to integer. + * + * @param value The capability value. + * @return null is the passed value is null otherwise the converted value. + * @throws NumberFormatException If the given value cannot be parsed to a valid integer. + */ + @Nullable + public static Integer toInteger(Object value) { + if (value == null) { + return null; + } else if (value instanceof Number) { + return ((Number) value).intValue(); + } else { + return Integer.parseInt(String.valueOf(value)); + } + } + + /** + * Converts generic capability value to long. + * + * @param value The capability value. + * @return null is the passed value is null otherwise the converted value. + * @throws NumberFormatException If the given value cannot be parsed to a valid long. + */ + @Nullable + public static Long toLong(Object value) { + if (value == null) { + return null; + } else if (value instanceof Number) { + return ((Number) value).longValue(); + } else { + return Long.parseLong(String.valueOf(value)); + } + } + + /** + * Converts generic capability value to double. + * + * @param value The capability value. + * @return null is the passed value is null otherwise the converted value. + * @throws NumberFormatException If the given value cannot be parsed to a valid long. + */ + @Nullable + public static Double toDouble(Object value) { + if (value == null) { + return null; + } else if (value instanceof Number) { + return ((Number) value).doubleValue(); + } else { + return Double.parseDouble(String.valueOf(value)); + } + } + + /** + * Converts generic capability value to duration. The value is assumed to be + * measured in milliseconds. + * + * @param value The capability value. + * @return null is the passed value is null otherwise the converted value. + * @throws NumberFormatException If the given value cannot be parsed to a valid number. + */ + @Nullable + public static Duration toDuration(Object value) { + return toDuration(value, Duration::ofMillis); + } + + /** + * Converts generic capability value to duration. + * + * @param value The capability value. + * @param converter Converts the numeric value to a Duration instance. + * @return null is the passed value is null otherwise the converted value. + * @throws NumberFormatException If the given value cannot be parsed to a valid number. + */ + @Nullable + public static Duration toDuration(Object value, + Function converter) { + Long v = toLong(value); + return v == null ? null : converter.apply(v); + } + + /** + * Converts generic capability value to a url. + * + * @param value The capability value. + * @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) { + if (value == null) { + return null; + } + try { + return (value instanceof URL) + ? (URL) value : + new URL(String.valueOf(value)); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } } 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/ElementMap.java b/src/main/java/io/appium/java_client/internal/ElementMap.java deleted file mode 100644 index 5522f7cb4..000000000 --- a/src/main/java/io/appium/java_client/internal/ElementMap.java +++ /dev/null @@ -1,89 +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.internal; - -import static org.apache.commons.lang3.StringUtils.isBlank; - -import com.google.common.collect.ImmutableMap; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.android.AndroidElement; -import io.appium.java_client.ios.IOSElement; -import io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobilePlatform; -import io.appium.java_client.windows.WindowsElement; -import org.openqa.selenium.remote.RemoteWebElement; - -import java.util.Map; -import java.util.Optional; - -public enum ElementMap { - ANDROID_UIAUTOMATOR2(AutomationName.ANDROID_UIAUTOMATOR2.toLowerCase(), AndroidElement.class), - SELENDROID(AutomationName.SELENDROID.toLowerCase(), AndroidElement.class), - IOS_XCUI_TEST(AutomationName.IOS_XCUI_TEST.toLowerCase(), IOSElement.class), - ANDROID_UI_AUTOMATOR(MobilePlatform.ANDROID.toLowerCase(), AndroidElement.class), - IOS_UI_AUTOMATION(MobilePlatform.IOS.toLowerCase(), IOSElement.class), - WINDOWS(MobilePlatform.WINDOWS, WindowsElement.class); - - - private static final Map mobileElementMap; - - static { - ImmutableMap.Builder builder = ImmutableMap.builder(); - for (ElementMap e:values()) { - builder.put(e.getPlatformOrAutomation(), e); - } - mobileElementMap = builder.build(); - } - - private final String platformOrAutomation; - private final Class elementClass; - - ElementMap(String platformOrAutomation, Class elementClass) { - this.platformOrAutomation = platformOrAutomation; - this.elementClass = elementClass; - } - - public String getPlatformOrAutomation() { - return platformOrAutomation; - } - - public Class getElementClass() { - return elementClass; - } - - /** - * Gets element class by {@code platform} and mobile {@code automation} type. - * - * @param platform is the mobile platform. See {@link MobilePlatform}. - * @param automation is the mobile automation type. See {@link AutomationName} - * @return subclass of {@link RemoteWebElement} that convenient to current session details. - */ - public static Class getElementClass(String platform, String automation) { - if (isBlank(platform) && isBlank(automation)) { - return RemoteWebElement.class; - } - ElementMap element = Optional.ofNullable(mobileElementMap.get( - String.valueOf(platform).toLowerCase().trim())) - .orElseGet(() -> mobileElementMap - .get(String.valueOf(automation).toLowerCase().trim())); - if (element == null) { - return RemoteWebElement.class; - } - return element.getElementClass(); - } -} diff --git a/src/main/java/io/appium/java_client/internal/JsonToMobileElementConverter.java b/src/main/java/io/appium/java_client/internal/JsonToMobileElementConverter.java deleted file mode 100644 index 9f8674a90..000000000 --- a/src/main/java/io/appium/java_client/internal/JsonToMobileElementConverter.java +++ /dev/null @@ -1,84 +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.internal; - -import static io.appium.java_client.internal.ElementMap.getElementClass; - -import io.appium.java_client.remote.MobileCapabilityType; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.remote.CapabilityType; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.remote.internal.JsonToWebElementConverter; - -import java.lang.reflect.Constructor; - -/** - * Reconstitutes {@link org.openqa.selenium.WebElement}s from their JSON representation. Will recursively convert Lists - * and Maps to catch nested references. All other values pass through the converter unchanged. - */ -public class JsonToMobileElementConverter extends JsonToWebElementConverter { - - protected final RemoteWebDriver driver; - - private final String platform; - private final String automation; - - /** - * Creates a new instance based on {@code driver} and object with session details. - * - * @param driver an instance of {@link RemoteWebDriver} subclass - */ - public JsonToMobileElementConverter(RemoteWebDriver driver) { - super(driver); - this.driver = driver; - Capabilities caps = driver.getCapabilities(); - this.platform = CapabilityHelpers.getCapability(caps, CapabilityType.PLATFORM_NAME, String.class); - this.automation = CapabilityHelpers.getCapability(caps, MobileCapabilityType.AUTOMATION_NAME, String.class); - } - - @Override - public Object apply(Object result) { - Object toBeReturned = result; - if (toBeReturned instanceof RemoteWebElement) { - toBeReturned = newRemoteWebElement(); - ((RemoteWebElement) toBeReturned).setId(((RemoteWebElement) result).getId()); - } - - return super.apply(toBeReturned); - } - - @Override - protected RemoteWebElement newRemoteWebElement() { - Class target; - target = getElementClass(platform, automation); - - try { - Constructor constructor = target.getDeclaredConstructor(); - constructor.setAccessible(true); - RemoteWebElement result = constructor.newInstance(); - - result.setParent(driver); - result.setFileDetector(driver.getFileDetector()); - - return result; - } catch (Exception e) { - throw new WebDriverException(e); - } - } -} 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/HasIOSSettings.java b/src/main/java/io/appium/java_client/ios/HasIOSSettings.java index 83a994a43..0f27380b3 100644 --- a/src/main/java/io/appium/java_client/ios/HasIOSSettings.java +++ b/src/main/java/io/appium/java_client/ios/HasIOSSettings.java @@ -19,7 +19,7 @@ import io.appium.java_client.HasSettings; import io.appium.java_client.Setting; -interface HasIOSSettings extends HasSettings { +public interface HasIOSSettings extends HasSettings { /** * Set the `nativeWebTap` setting. *iOS-only method*. * Sets whether Safari/webviews should convert element taps into x/y taps. 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 229aac70b..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,55 +16,65 @@ 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.FindsByIosClassChain; -import io.appium.java_client.FindsByIosNSPredicate; +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.LocksDevice; +import io.appium.java_client.PerformsTouchActions; +import io.appium.java_client.PullsFiles; +import io.appium.java_client.PushesFiles; import io.appium.java_client.battery.HasBattery; -import io.appium.java_client.remote.MobilePlatform; +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; import io.appium.java_client.screenrecording.CanRecordScreen; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; import io.appium.java_client.ws.StringWebSocketClient; import org.openqa.selenium.Alert; import org.openqa.selenium.Capabilities; -import org.openqa.selenium.WebElement; +import org.openqa.selenium.Platform; import org.openqa.selenium.remote.DriverCommand; import org.openqa.selenium.remote.HttpCommandExecutor; import org.openqa.selenium.remote.Response; +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; /** * iOS driver implementation. - * - * @param the required type of class which implement - * {@link org.openqa.selenium.WebElement}. - * Instances of the defined type will be returned via findElement* and findElements*. - * Warning (!!!). Allowed types: - * {@link org.openqa.selenium.WebElement} - * {@link org.openqa.selenium.remote.RemoteWebElement} - * {@link io.appium.java_client.MobileElement} - * {@link io.appium.java_client.ios.IOSElement} */ -public class IOSDriver - extends AppiumDriver - implements HidesKeyboardWithKeyName, ShakesDevice, HasIOSSettings, HasOnScreenKeyboard, - LocksDevice, PerformsTouchID, FindsByIosNSPredicate, FindsByIosClassChain, - PushesFiles, CanRecordScreen, HasIOSClipboard, ListensToSyslogMessages, +public class IOSDriver extends AppiumDriver implements + SupportsContextSwitching, + SupportsRotation, + SupportsLocation, + HidesKeyboard, + HasDeviceTime, + PullsFiles, + InteractsWithApps, + HasAppStrings, + PerformsTouchActions, + HidesKeyboardWithKeyName, + ShakesDevice, + HasIOSSettings, + HasOnScreenKeyboard, + LocksDevice, + PerformsTouchID, + PushesFiles, + CanRecordScreen, + HasIOSClipboard, + ListensToSyslogMessages, HasBattery { - - private static final String IOS_DEFAULT_PLATFORM = MobilePlatform.IOS; + private static final String PLATFORM_NAME = Platform.IOS.name(); private StringWebSocketClient syslogClient; @@ -77,17 +87,17 @@ public class IOSDriver * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(HttpCommandExecutor executor, Capabilities capabilities) { - super(executor, updateDefaultPlatformName(capabilities, IOS_DEFAULT_PLATFORM)); + super(executor, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** * Creates a new instance based on Appium server URL and {@code capabilities}. * * @param remoteAddress is the address of remotely/locally started Appium server - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(URL remoteAddress, Capabilities desiredCapabilities) { - super(remoteAddress, updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + public IOSDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** @@ -95,22 +105,21 @@ public IOSDriver(URL remoteAddress, Capabilities desiredCapabilities) { * * @param remoteAddress is the address of remotely/locally started Appium server * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(remoteAddress, httpClientFactory, - updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** * Creates a new instance based on Appium driver local service and {@code capabilities}. * * @param service take a look at {@link AppiumDriverLocalService} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(AppiumDriverLocalService service, Capabilities desiredCapabilities) { - super(service, updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + public IOSDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** @@ -118,21 +127,21 @@ public IOSDriver(AppiumDriverLocalService service, Capabilities desiredCapabilit * * @param service take a look at {@link AppiumDriverLocalService} * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(service, httpClientFactory, updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** * Creates a new instance based on Appium service builder and {@code capabilities}. * * @param builder take a look at {@link AppiumServiceBuilder} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) { - super(builder, updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + public IOSDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** @@ -140,42 +149,100 @@ public IOSDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) * * @param builder take a look at {@link AppiumServiceBuilder} * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(builder, httpClientFactory, - updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + Capabilities capabilities) { + super(builder, httpClientFactory, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** * Creates a new instance based on HTTP client factory and {@code capabilities}. * * @param httpClientFactory take a look at {@link HttpClient.Factory} - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(HttpClient.Factory httpClientFactory, Capabilities desiredCapabilities) { - super(httpClientFactory, updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + public IOSDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, 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: + * + *
+     *
+     * ClientConfig clientConfig = ClientConfig.defaultConfig()
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * XCUITestOptions options = new XCUITestOptions();
+     * IOSDriver driver = new IOSDriver(clientConfig, options);
+     *
+     * 
+ * + * @param clientConfig take a look at {@link ClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public IOSDriver(ClientConfig clientConfig, Capabilities capabilities) { + 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); } /** * Creates a new instance based on {@code capabilities}. * - * @param desiredCapabilities take a look at {@link Capabilities} + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(Capabilities desiredCapabilities) { - super(updateDefaultPlatformName(desiredCapabilities, IOS_DEFAULT_PLATFORM)); + public IOSDriver(Capabilities capabilities) { + super(ensurePlatformName(capabilities, PLATFORM_NAME)); } @Override public TargetLocator switchTo() { 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 { @@ -206,7 +273,7 @@ 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)); } } @@ -214,7 +281,7 @@ class IOSAlert implements Alert { @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 8a2c19199..aca87955d 100644 --- a/src/main/java/io/appium/java_client/ios/IOSTouchAction.java +++ b/src/main/java/io/appium/java_client/ios/IOSTouchAction.java @@ -22,6 +22,19 @@ import io.appium.java_client.touch.offset.ElementOption; import io.appium.java_client.touch.offset.PointOption; +/** + * iOS-specific touch action. + * + * @deprecated Touch actions are deprecated. + * Please use W3C Actions instead or the corresponding + * extension methods for the driver (if available). + * Check + * - https://www.youtube.com/watch?v=oAJ7jwMNFVU + * - https://appiumpro.com/editions/30-ios-specific-touch-action-methods + * - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api + * for more details. + */ +@Deprecated public class IOSTouchAction extends TouchAction { public IOSTouchAction(PerformsTouchActions performsTouchActions) { @@ -35,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; } @@ -48,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/PushesFiles.java b/src/main/java/io/appium/java_client/ios/PushesFiles.java deleted file mode 100644 index 097deacf3..000000000 --- a/src/main/java/io/appium/java_client/ios/PushesFiles.java +++ /dev/null @@ -1,76 +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 com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.MobileCommand.pushFileCommand; - -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.io.IOException; - -public interface PushesFiles extends ExecutesMethod { - - /** - * Saves base64 encoded data as a media file on the remote mobile device. - * The server should have ifuse - * libraries installed and configured properly for this feature to work - * on real devices. - * - * @see iFuse GitHub page - * @see osxFuse FAQ - * - * @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 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)); - } - - /** - * Saves base64 encoded data as a media file on the remote mobile device. - * The server should have ifuse - * libraries installed and configured properly for this feature to work - * on real devices. - * - * @see iFuse GitHub page - * @see osxFuse FAQ - * - * @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 - */ - 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))); - } - -} 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 new file mode 100644 index 000000000..41d5047a2 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java @@ -0,0 +1,253 @@ +/* + * 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; + +import io.appium.java_client.ios.options.app.SupportsAppInstallStrategyOption; +import io.appium.java_client.ios.options.app.SupportsAppPushTimeoutOption; +import io.appium.java_client.ios.options.app.SupportsBundleIdOption; +import io.appium.java_client.ios.options.app.SupportsLocalizableStringsDirOption; +import io.appium.java_client.ios.options.general.SupportsIncludeDeviceCapsToSessionInfoOption; +import io.appium.java_client.ios.options.general.SupportsResetLocationServiceOption; +import io.appium.java_client.ios.options.other.SupportsCommandTimeoutsOption; +import io.appium.java_client.ios.options.other.SupportsLaunchWithIdbOption; +import io.appium.java_client.ios.options.other.SupportsResetOnSessionStartOnlyOption; +import io.appium.java_client.ios.options.other.SupportsShowIosLogOption; +import io.appium.java_client.ios.options.other.SupportsUseJsonSourceOption; +import io.appium.java_client.ios.options.simulator.SupportsCalendarAccessAuthorizedOption; +import io.appium.java_client.ios.options.simulator.SupportsCalendarFormatOption; +import io.appium.java_client.ios.options.simulator.SupportsConnectHardwareKeyboardOption; +import io.appium.java_client.ios.options.simulator.SupportsCustomSslCertOption; +import io.appium.java_client.ios.options.simulator.SupportsEnforceFreshSimulatorCreationOption; +import io.appium.java_client.ios.options.simulator.SupportsForceSimulatorSoftwareKeyboardPresenceOption; +import io.appium.java_client.ios.options.simulator.SupportsIosSimulatorLogsPredicateOption; +import io.appium.java_client.ios.options.simulator.SupportsKeepKeyChainsOption; +import io.appium.java_client.ios.options.simulator.SupportsKeychainsExcludePatternsOption; +import io.appium.java_client.ios.options.simulator.SupportsPermissionsOption; +import io.appium.java_client.ios.options.simulator.SupportsReduceMotionOption; +import io.appium.java_client.ios.options.simulator.SupportsScaleFactorOption; +import io.appium.java_client.ios.options.simulator.SupportsShutdownOtherSimulatorsOption; +import io.appium.java_client.ios.options.simulator.SupportsSimulatorDevicesSetPathOption; +import io.appium.java_client.ios.options.simulator.SupportsSimulatorPasteboardAutomaticSyncOption; +import io.appium.java_client.ios.options.simulator.SupportsSimulatorStartupTimeoutOption; +import io.appium.java_client.ios.options.simulator.SupportsSimulatorTracePointerOption; +import io.appium.java_client.ios.options.simulator.SupportsSimulatorWindowCenterOption; +import io.appium.java_client.ios.options.simulator.SupportsWebkitResponseTimeoutOption; +import io.appium.java_client.ios.options.wda.SupportsAllowProvisioningDeviceRegistrationOption; +import io.appium.java_client.ios.options.wda.SupportsAutoAcceptAlertsOption; +import io.appium.java_client.ios.options.wda.SupportsAutoDismissAlertsOption; +import io.appium.java_client.ios.options.wda.SupportsDerivedDataPathOption; +import io.appium.java_client.ios.options.wda.SupportsDisableAutomaticScreenshotsOption; +import io.appium.java_client.ios.options.wda.SupportsForceAppLaunchOption; +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; +import io.appium.java_client.ios.options.wda.SupportsShouldTerminateAppOption; +import io.appium.java_client.ios.options.wda.SupportsShouldUseSingletonTestManagerOption; +import io.appium.java_client.ios.options.wda.SupportsShowXcodeLogOption; +import io.appium.java_client.ios.options.wda.SupportsSimpleIsVisibleCheckOption; +import io.appium.java_client.ios.options.wda.SupportsUpdatedWdaBundleIdOption; +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; +import io.appium.java_client.ios.options.wda.SupportsWaitForQuiescenceOption; +import io.appium.java_client.ios.options.wda.SupportsWdaBaseUrlOption; +import io.appium.java_client.ios.options.wda.SupportsWdaConnectionTimeoutOption; +import io.appium.java_client.ios.options.wda.SupportsWdaEventloopIdleDelayOption; +import io.appium.java_client.ios.options.wda.SupportsWdaLaunchTimeoutOption; +import io.appium.java_client.ios.options.wda.SupportsWdaLocalPortOption; +import io.appium.java_client.ios.options.wda.SupportsWdaStartupRetriesOption; +import io.appium.java_client.ios.options.wda.SupportsWdaStartupRetryIntervalOption; +import io.appium.java_client.ios.options.wda.SupportsWebDriverAgentUrlOption; +import io.appium.java_client.ios.options.wda.SupportsXcodeCertificateOptions; +import io.appium.java_client.ios.options.webview.SupportsAbsoluteWebLocationsOption; +import io.appium.java_client.ios.options.webview.SupportsAdditionalWebviewBundleIdsOption; +import io.appium.java_client.ios.options.webview.SupportsEnableAsyncExecuteFromHttpsOption; +import io.appium.java_client.ios.options.webview.SupportsFullContextListOption; +import io.appium.java_client.ios.options.webview.SupportsIncludeSafariInWebviewsOption; +import io.appium.java_client.ios.options.webview.SupportsNativeWebTapOption; +import io.appium.java_client.ios.options.webview.SupportsNativeWebTapStrictOption; +import io.appium.java_client.ios.options.webview.SupportsSafariAllowPopupsOption; +import io.appium.java_client.ios.options.webview.SupportsSafariGarbageCollectOption; +import io.appium.java_client.ios.options.webview.SupportsSafariIgnoreFraudWarningOption; +import io.appium.java_client.ios.options.webview.SupportsSafariIgnoreWebHostnamesOption; +import io.appium.java_client.ios.options.webview.SupportsSafariInitialUrlOption; +import io.appium.java_client.ios.options.webview.SupportsSafariLogAllCommunicationHexDumpOption; +import io.appium.java_client.ios.options.webview.SupportsSafariLogAllCommunicationOption; +import io.appium.java_client.ios.options.webview.SupportsSafariOpenLinksInBackgroundOption; +import io.appium.java_client.ios.options.webview.SupportsSafariSocketChunkSizeOption; +import io.appium.java_client.ios.options.webview.SupportsSafariWebInspectorMaxFrameLengthOption; +import io.appium.java_client.ios.options.webview.SupportsWebviewConnectRetriesOption; +import io.appium.java_client.ios.options.webview.SupportsWebviewConnectTimeoutOption; +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsAppOption; +import io.appium.java_client.remote.options.SupportsAutoWebViewOption; +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; + +/** + * 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 + SupportsDeviceNameOption, + SupportsUdidOption, + SupportsIncludeDeviceCapsToSessionInfoOption, + SupportsResetLocationServiceOption, + // Localization Options + SupportsLocalizableStringsDirOption, + SupportsLanguageOption, + SupportsLocaleOption, + // App Options: https://github.com/appium/appium-xcuitest-driver#app + SupportsAppOption, + SupportsBundleIdOption, + SupportsOtherAppsOption, + SupportsAppPushTimeoutOption, + SupportsAppInstallStrategyOption, + SupportsEnforceAppInstallOption, + // WebDriverAgent options: https://github.com/appium/appium-xcuitest-driver#webdriveragent + SupportsXcodeCertificateOptions, + SupportsKeychainOptions, + SupportsUpdatedWdaBundleIdOption, + SupportsDerivedDataPathOption, + SupportsWebDriverAgentUrlOption, + SupportsUseNewWdaOption, + SupportsWdaLaunchTimeoutOption, + SupportsWdaConnectionTimeoutOption, + SupportsWdaStartupRetriesOption, + SupportsWdaStartupRetryIntervalOption, + SupportsWdaLocalPortOption, + SupportsWdaBaseUrlOption, + SupportsShowXcodeLogOption, + SupportsUsePrebuiltWdaOption, + SupportsUsePreinstalledWdaOption, + SupportsPrebuiltWdaPathOption, + SupportsShouldUseSingletonTestManagerOption, + SupportsWaitForIdleTimeoutOption, + SupportsUseXctestrunFileOption, + SupportsUseSimpleBuildTestOption, + SupportsWdaEventloopIdleDelayOption, + SupportsProcessArgumentsOption, + SupportsAllowProvisioningDeviceRegistrationOption, + SupportsResultBundlePathOption, + SupportsMaxTypingFrequencyOption, + SupportsSimpleIsVisibleCheckOption, + SupportsWaitForQuiescenceOption, + SupportsMjpegServerPortOption, + SupportsScreenshotQualityOption, + SupportsAutoAcceptAlertsOption, + SupportsAutoDismissAlertsOption, + SupportsDisableAutomaticScreenshotsOption, + SupportsShouldTerminateAppOption, + SupportsForceAppLaunchOption, + SupportsUseNativeCachingStrategyOption, + // Simulator options: https://github.com/appium/appium-xcuitest-driver#simulator + SupportsOrientationOption, + SupportsScaleFactorOption, + SupportsConnectHardwareKeyboardOption, + SupportsForceSimulatorSoftwareKeyboardPresenceOption, + SupportsCalendarAccessAuthorizedOption, + SupportsCalendarFormatOption, + SupportsIsHeadlessOption, + SupportsSimulatorWindowCenterOption, + SupportsSimulatorStartupTimeoutOption, + SupportsSimulatorTracePointerOption, + SupportsShutdownOtherSimulatorsOption, + SupportsEnforceFreshSimulatorCreationOption, + SupportsKeepKeyChainsOption, + SupportsKeychainsExcludePatternsOption, + SupportsReduceMotionOption, + SupportsPermissionsOption, + SupportsIosSimulatorLogsPredicateOption, + SupportsSimulatorPasteboardAutomaticSyncOption, + SupportsSimulatorDevicesSetPathOption, + SupportsCustomSslCertOption, + // Web context options: https://github.com/appium/appium-xcuitest-driver#web-context + SupportsAutoWebViewOption, + SupportsAbsoluteWebLocationsOption, + SupportsSafariGarbageCollectOption, + SupportsIncludeSafariInWebviewsOption, + SupportsSafariLogAllCommunicationOption, + SupportsSafariLogAllCommunicationHexDumpOption, + SupportsSafariSocketChunkSizeOption, + SupportsSafariWebInspectorMaxFrameLengthOption, + SupportsAdditionalWebviewBundleIdsOption, + SupportsWebviewConnectTimeoutOption, + SupportsSafariIgnoreWebHostnamesOption, + SupportsNativeWebTapOption, + SupportsNativeWebTapStrictOption, + SupportsSafariInitialUrlOption, + SupportsSafariAllowPopupsOption, + SupportsSafariIgnoreFraudWarningOption, + SupportsSafariOpenLinksInBackgroundOption, + SupportsWebviewConnectRetriesOption, + SupportsWebkitResponseTimeoutOption, + SupportsEnableAsyncExecuteFromHttpsOption, + SupportsFullContextListOption, + SupportsEnablePerformanceLoggingOption, + // Other options: https://github.com/appium/appium-xcuitest-driver#other + SupportsResetOnSessionStartOnlyOption, + SupportsCommandTimeoutsOption, + SupportsUseJsonSourceOption, + SupportsSkipLogCaptureOption, + SupportsLaunchWithIdbOption, + SupportsShowIosLogOption, + SupportsClearSystemFilesOption { + + public XCUITestOptions() { + setCommonOptions(); + } + + public XCUITestOptions(Capabilities source) { + super(source); + setCommonOptions(); + } + + public XCUITestOptions(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setPlatformName(MobilePlatform.IOS); + setAutomationName(AutomationName.IOS_XCUI_TEST); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/app/SupportsAppInstallStrategyOption.java b/src/main/java/io/appium/java_client/ios/options/app/SupportsAppInstallStrategyOption.java new file mode 100644 index 000000000..f74d1db15 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/app/SupportsAppInstallStrategyOption.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.ios.options.app; + +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 SupportsAppInstallStrategyOption> extends + Capabilities, CanSetCapability { + String APP_INSTALL_STRATEGY_OPTION = "appInstallStrategy"; + + /** + * Select application installation strategy for real devices. The following + * strategies are supported: + * * serial (default) - pushes app files to the device in a sequential order; + * this is the least performant strategy, although the most reliable; + * * parallel - pushes app files simultaneously; this is usually the + * most performant strategy, but sometimes could not be very stable; + * * ios-deploy - tells the driver to use a third-party tool ios-deploy to + * install the app; obviously the tool must be installed separately + * first and must be present in PATH before it could be used. + * @param strategy App installation strategy. + * @return self instance for chaining. + */ + default T setAppInstallStrategy(String strategy) { + return amend(APP_INSTALL_STRATEGY_OPTION, strategy); + } + + /** + * Get the app install strategy. + * + * @return App installation strategy. + */ + default Optional getAppInstallStrategy() { + return Optional.ofNullable((String) getCapability(APP_INSTALL_STRATEGY_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/app/SupportsAppPushTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/app/SupportsAppPushTimeoutOption.java new file mode 100644 index 000000000..1e3d54aeb --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/app/SupportsAppPushTimeoutOption.java @@ -0,0 +1,52 @@ +/* + * 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.app; + +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 SupportsAppPushTimeoutOption> extends + Capabilities, CanSetCapability { + String APP_PUSH_TIMEOUT_OPTION = "appPushTimeout"; + + /** + * The timeout for application upload. + * Works for real devices only. The default value is 30000ms. + * + * @param timeout App push timeout. + * @return self instance for chaining. + */ + default T setAppPushTimeout(Duration timeout) { + return amend(APP_PUSH_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get maximum timeout for application upload. + * + * @return Timeout value. + */ + default Optional getAppPushTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(APP_PUSH_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/app/SupportsBundleIdOption.java b/src/main/java/io/appium/java_client/ios/options/app/SupportsBundleIdOption.java new file mode 100644 index 000000000..97e53d2f4 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/app/SupportsBundleIdOption.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.app; + +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 SupportsBundleIdOption> extends + Capabilities, CanSetCapability { + String BUNDLE_ID_OPTION = "bundleId"; + + /** + * Bundle identifier of the app under test, for example com.mycompany.myapp. + * The capability value is calculated automatically if app is provided. + * If neither app nor bundleId capability is provided then XCUITest driver + * starts from the Home screen. + * + * @param identifier App identifier. + * @return self instance for chaining. + */ + default T setBundleId(String identifier) { + return amend(BUNDLE_ID_OPTION, identifier); + } + + /** + * Get the app bundle identifier. + * + * @return Identifier value. + */ + default Optional getBundleId() { + return Optional.ofNullable((String) getCapability(BUNDLE_ID_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/app/SupportsLocalizableStringsDirOption.java b/src/main/java/io/appium/java_client/ios/options/app/SupportsLocalizableStringsDirOption.java new file mode 100644 index 000000000..f02635bf6 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/app/SupportsLocalizableStringsDirOption.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.ios.options.app; + +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 SupportsLocalizableStringsDirOption> extends + Capabilities, CanSetCapability { + String LOCALIZABLE_STRINGS_DIR_OPTION = "localizableStringsDir"; + + /** + * Where to look for localizable strings in the application bundle. + * Defaults to en.lproj. + * + * @param folder The resource folder name where the main locale strings are stored. + * @return self instance for chaining. + */ + default T setLocalizableStringsDir(String folder) { + return amend(LOCALIZABLE_STRINGS_DIR_OPTION, folder); + } + + /** + * Get the resource folder name where the main locale strings are stored. + * + * @return Folder name. + */ + default Optional getLocalizableStringsDir() { + return Optional.ofNullable((String) getCapability(LOCALIZABLE_STRINGS_DIR_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/general/SupportsIncludeDeviceCapsToSessionInfoOption.java b/src/main/java/io/appium/java_client/ios/options/general/SupportsIncludeDeviceCapsToSessionInfoOption.java new file mode 100644 index 000000000..9760cbdf5 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/general/SupportsIncludeDeviceCapsToSessionInfoOption.java @@ -0,0 +1,52 @@ +/* + * 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.general; + +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 SupportsIncludeDeviceCapsToSessionInfoOption> extends + Capabilities, CanSetCapability { + String INCLUDE_DEVICE_CAPS_TO_SESSION_INFO_OPTION = "includeDeviceCapsToSessionInfo"; + + /** + * Whether to include screen information as the result of Get Session Capabilities. + * It includes pixelRatio, statBarHeight and viewportRect, but + * it causes an extra API call to WDA which may increase the response time. + * Defaults to true. + * + * @param value Whether to include screen information as the result of Get Session Capabilities. + * @return self instance for chaining. + */ + default T setIncludeDeviceCapsToSessionInfo(boolean value) { + return amend(INCLUDE_DEVICE_CAPS_TO_SESSION_INFO_OPTION, value); + } + + /** + * Get whether to include screen information as the result of Get Session Capabilities. + * + * @return True or false. + */ + default Optional doesIncludeDeviceCapsToSessionInfo() { + return Optional.ofNullable(toSafeBoolean(getCapability(INCLUDE_DEVICE_CAPS_TO_SESSION_INFO_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/general/SupportsResetLocationServiceOption.java b/src/main/java/io/appium/java_client/ios/options/general/SupportsResetLocationServiceOption.java new file mode 100644 index 000000000..5bd8da1d7 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/general/SupportsResetLocationServiceOption.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.general; + +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 SupportsResetLocationServiceOption> extends + Capabilities, CanSetCapability { + String RESET_LOCATION_SERVICE_OPTION = "resetLocationService"; + + /** + * Set to reset the location service in the session deletion on real device. + * + * @return self instance for chaining. + */ + default T resetLocationService() { + return amend(RESET_LOCATION_SERVICE_OPTION, true); + } + + /** + * Whether reset the location service in the session deletion on real device. + * Defaults to false. + * + * @param value Whether to reset the location service in the session deletion on real device. + * @return self instance for chaining. + */ + default T setResetLocationService(boolean value) { + return amend(RESET_LOCATION_SERVICE_OPTION, value); + } + + /** + * Get whether to reset the location service in the session deletion on real device. + * + * @return True or false. + */ + default Optional doesResetLocationService() { + return Optional.ofNullable(toSafeBoolean(getCapability(RESET_LOCATION_SERVICE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/other/CommandTimeouts.java b/src/main/java/io/appium/java_client/ios/options/other/CommandTimeouts.java new file mode 100644 index 000000000..36b435a90 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/other/CommandTimeouts.java @@ -0,0 +1,77 @@ +/* + * 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.other; + +import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.remote.options.BaseMapOptionData; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +public class CommandTimeouts extends BaseMapOptionData { + public static final String DEFAULT_COMMAND = "default"; + + public CommandTimeouts() { + } + + public CommandTimeouts(Map timeouts) { + super(timeouts); + } + + public CommandTimeouts(String json) { + super(json); + } + + /** + * Sets the timeout for the particular Appium command that + * is proxied to WDA. + * Command names you can find in logs, look for + * "Executing command 'command_name'" records. + * Timeout value is expected to contain max milliseconds to wait for + * the given WDA command to be executed before terminating the session forcefully. + * + * @param commandName The command name. + * @param timeout Command timeout. + * @return self instance for chaining. + */ + public CommandTimeouts withCommandTimeout(String commandName, Duration timeout) { + return assignOptionValue(commandName, timeout.toMillis()); + } + + /** + * Sets the default timeout for all Appium commands that + * are proxied to WDA. + * + * @param timeout Commands timeout. + * @return self instance for chaining. + */ + public CommandTimeouts withDefaultCommandTimeout(Duration timeout) { + return withCommandTimeout(DEFAULT_COMMAND, timeout); + } + + /** + * Get the command timeout. + * + * @param commandName The command name + * @return Timeout value. + */ + public Optional getCommandTimeout(String commandName) { + Optional result = getOptionValue(commandName); + return result.map(CapabilityHelpers::toDuration); + } +} 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 new file mode 100644 index 000000000..d19e6272f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/other/SupportsCommandTimeoutsOption.java @@ -0,0 +1,70 @@ +/* + * 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.other; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.internal.Either; + +import java.time.Duration; +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsCommandTimeoutsOption> extends + Capabilities, CanSetCapability { + String COMMAND_TIMEOUTS_OPTION = "commandTimeouts"; + + /** + * 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. + * + * @param timeouts Command timeouts. + * @return self instance for chaining. + */ + default T setCommandTimeouts(CommandTimeouts timeouts) { + return amend(COMMAND_TIMEOUTS_OPTION, timeouts.toString()); + } + + /** + * Custom timeout for all 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. + * + * @param timeout The timeout value for all commands. + * @return self instance for chaining. + */ + default T setCommandTimeouts(Duration timeout) { + return amend(COMMAND_TIMEOUTS_OPTION, String.valueOf(timeout.toMillis())); + } + + /** + * Get custom timeout(s) in milliseconds for WDA backend commands execution. + * + * @return Either a global timeout duration or detailed command timeouts. + */ + default Optional> getCommandTimeouts() { + return Optional.ofNullable(getCapability(COMMAND_TIMEOUTS_OPTION)) + .map(String::valueOf) + .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/other/SupportsLaunchWithIdbOption.java b/src/main/java/io/appium/java_client/ios/options/other/SupportsLaunchWithIdbOption.java new file mode 100644 index 000000000..8c36b58bf --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/other/SupportsLaunchWithIdbOption.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.ios.options.other; + +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 SupportsLaunchWithIdbOption> extends + Capabilities, CanSetCapability { + String LAUNCH_WITH_IDB_OPTION = "launchWithIDB"; + + /** + * Enforces launching of WebDriverAgentRunner with idb instead of xcodebuild. + * + * @return self instance for chaining. + */ + default T launchWithIdb() { + return amend(LAUNCH_WITH_IDB_OPTION, true); + } + + /** + * Launch WebDriverAgentRunner with idb instead of xcodebuild. This could save + * a significant amount of time by skipping the xcodebuild process, although the + * idb might not be very reliable, especially with fresh Xcode SDKs. Check + * the idb repository for more details on possible compatibility issues. + * Defaults to false. + * + * @param value Whether to launch WebDriverAgentRunner with idb instead of xcodebuild. + * @return self instance for chaining. + */ + default T setLaunchWithIdb(boolean value) { + return amend(LAUNCH_WITH_IDB_OPTION, value); + } + + /** + * Get whether to launch WebDriverAgentRunner with idb instead of xcodebuild. + * + * @return True or false. + */ + default Optional doesLaunchWithIdb() { + return Optional.ofNullable(toSafeBoolean(getCapability(LAUNCH_WITH_IDB_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/other/SupportsResetOnSessionStartOnlyOption.java b/src/main/java/io/appium/java_client/ios/options/other/SupportsResetOnSessionStartOnlyOption.java new file mode 100644 index 000000000..65f29c837 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/other/SupportsResetOnSessionStartOnlyOption.java @@ -0,0 +1,52 @@ +/* + * 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.other; + +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 SupportsResetOnSessionStartOnlyOption> extends + Capabilities, CanSetCapability { + String RESET_ON_SESSION_START_ONLY_OPTION = "resetOnSessionStartOnly"; + + /** + * 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. + * + * @param value Whether to perform reset on test session finish (false) or not (true).. + * @return self instance for chaining. + */ + default T setResetOnSessionStartOnly(boolean value) { + return amend(RESET_ON_SESSION_START_ONLY_OPTION, value); + } + + /** + * Get whether to perform reset on test session finish (false) or not (true). + * + * @return True or false. + */ + default Optional doesResetOnSessionStartOnly() { + return Optional.ofNullable(toSafeBoolean(getCapability(RESET_ON_SESSION_START_ONLY_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/other/SupportsShowIosLogOption.java b/src/main/java/io/appium/java_client/ios/options/other/SupportsShowIosLogOption.java new file mode 100644 index 000000000..ee0676d44 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/other/SupportsShowIosLogOption.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.other; + +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 SupportsShowIosLogOption> extends + Capabilities, CanSetCapability { + String SHOW_IOS_LOG_OPTION = "showIOSLog"; + + /** + * Enforces showing any logs captured from a device in the appium logs. + * + * @return self instance for chaining. + */ + default T showIosLog() { + return amend(SHOW_IOS_LOG_OPTION, true); + } + + /** + * Whether to show any logs captured from a device in the appium logs. + * Default false. + * + * @param value Whether to show any logs captured from a device in the appium logs. + * @return self instance for chaining. + */ + default T setShowIosLog(boolean value) { + return amend(SHOW_IOS_LOG_OPTION, value); + } + + /** + * Get whether to show any logs captured from a device in the appium logs. + * + * @return True or false. + */ + default Optional doesShowIosLog() { + return Optional.ofNullable(toSafeBoolean(getCapability(SHOW_IOS_LOG_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/other/SupportsUseJsonSourceOption.java b/src/main/java/io/appium/java_client/ios/options/other/SupportsUseJsonSourceOption.java new file mode 100644 index 000000000..dd88a9a34 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/other/SupportsUseJsonSourceOption.java @@ -0,0 +1,60 @@ +/* + * 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.other; + +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 SupportsUseJsonSourceOption> extends + Capabilities, CanSetCapability { + String USE_JSON_SOURCE_OPTION = "useJSONSource"; + + /** + * Enforces getting JSON source from WDA and transform it to XML on the Appium + * server side. + * + * @return self instance for chaining. + */ + default T useJSONSource() { + return amend(USE_JSON_SOURCE_OPTION, true); + } + + /** + * Get JSON source from WDA and transform it to XML on the Appium server side. + * Defaults to false. + * + * @param value Whether to get JSON source from WDA and transform it to XML. + * @return self instance for chaining. + */ + default T setUseJSONSource(boolean value) { + return amend(USE_JSON_SOURCE_OPTION, value); + } + + /** + * Get whether to get JSON source from WDA and transform it to XML. + * + * @return True or false. + */ + default Optional doesUseJSONSource() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_JSON_SOURCE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/PasteboardSyncState.java b/src/main/java/io/appium/java_client/ios/options/simulator/PasteboardSyncState.java new file mode 100644 index 000000000..885229dd8 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/PasteboardSyncState.java @@ -0,0 +1,21 @@ +/* + * 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.simulator; + +public enum PasteboardSyncState { + ON, OFF, SYSTEM +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/Permissions.java b/src/main/java/io/appium/java_client/ios/options/simulator/Permissions.java new file mode 100644 index 000000000..53094e5ab --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/Permissions.java @@ -0,0 +1,63 @@ +/* + * 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.simulator; + +import io.appium.java_client.remote.options.BaseMapOptionData; + +import java.util.Map; +import java.util.Optional; + +public class Permissions extends BaseMapOptionData { + public Permissions() { + } + + public Permissions(Map permissions) { + super(permissions); + } + + public Permissions(String json) { + super(json); + } + + /** + * Since Xcode SDK 11.4 Apple provides native APIs to interact with + * application settings. Check the output of `xcrun simctl privacy booted` + * command to get the list of available permission names. Use yes, no + * and unset as values in order to grant, revoke or reset the corresponding + * permission. Below Xcode SDK 11.4 it is required that applesimutils package + * is installed and available in PATH. The list of available service names + * and statuses can be found at https://github.com/wix/AppleSimulatorUtils. + * For example: {"com.apple.mobilecal": {"calendar": "YES"}} + * + * @param bundleId The app identifier to change permissions for. + * @param mapping Permissions mapping, where keys are perm names and vales are YES/NO. + * @return self instance for chaining. + */ + public Permissions withAppPermissions(String bundleId, Map mapping) { + return assignOptionValue(bundleId, mapping); + } + + /** + * Get permissions mapping for the given app bundle identifier. + * + * @param bundleId App bundle identifier. + * @return Permissions mapping. + */ + public Optional> getAppPermissions(String bundleId) { + return getOptionValue(bundleId); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCalendarAccessAuthorizedOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCalendarAccessAuthorizedOption.java new file mode 100644 index 000000000..800bac79e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCalendarAccessAuthorizedOption.java @@ -0,0 +1,61 @@ +/* + * 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.simulator; + +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 SupportsCalendarAccessAuthorizedOption> extends + Capabilities, CanSetCapability { + String CALENDAR_ACCESS_AUTHORIZED_OPTION = "calendarAccessAuthorized"; + + /** + * Enable calendar access on IOS Simulator. + * + * @return self instance for chaining. + */ + default T calendarAccessAuthorized() { + return amend(CALENDAR_ACCESS_AUTHORIZED_OPTION, true); + } + + /** + * Set this to true if you want to enable calendar access on IOS Simulator + * with given bundleId. Set to false, if you want to disable calendar access + * on IOS Simulator with given bundleId. If not set, the calendar + * authorization status will not be set. + * + * @param value Whether to enable calendar access on IOS Simulator. + * @return self instance for chaining. + */ + default T setCalendarAccessAuthorized(boolean value) { + return amend(CALENDAR_ACCESS_AUTHORIZED_OPTION, value); + } + + /** + * Get whether to enable calendar access on IOS Simulator. + * + * @return True or false. + */ + default Optional doesCalendarAccessAuthorized() { + return Optional.ofNullable(toSafeBoolean(getCapability(CALENDAR_ACCESS_AUTHORIZED_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCalendarFormatOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCalendarFormatOption.java new file mode 100644 index 000000000..f38a31bd2 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCalendarFormatOption.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.ios.options.simulator; + +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 SupportsCalendarFormatOption> extends + Capabilities, CanSetCapability { + String CALENDAR_FORMAT_OPTION = "calendarFormat"; + + /** + * Set calendar format for the iOS Simulator. + * + * @param format Calendar format to set for the iOS Simulator. + * @return self instance for chaining. + */ + default T setCalendarFormat(String format) { + return amend(CALENDAR_FORMAT_OPTION, format); + } + + /** + * Get calendar format to set for the iOS Simulator. + * + * @return Calendar format. + */ + default Optional getCalendarFormat() { + return Optional.ofNullable((String) getCapability(CALENDAR_FORMAT_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsConnectHardwareKeyboardOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsConnectHardwareKeyboardOption.java new file mode 100644 index 000000000..3eed21650 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsConnectHardwareKeyboardOption.java @@ -0,0 +1,63 @@ +/* + * 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.simulator; + +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 SupportsConnectHardwareKeyboardOption> extends + Capabilities, CanSetCapability { + String CONNECT_HARDWARE_KEYBOARD_OPTION = "connectHardwareKeyboard"; + + /** + * Enforce connecting of hardware keyboard to Simulator. + * + * @return self instance for chaining. + */ + default T connectHardwareKeyboard() { + return amend(CONNECT_HARDWARE_KEYBOARD_OPTION, true); + } + + /** + * Set this option to true in order to enable hardware keyboard in Simulator. + * The preference works only when Appium launches a simulator instance with + * this value. It is set to false by default, because this helps to workaround + * some XCTest bugs. connectHardwareKeyboard: true makes + * forceSimulatorSoftwareKeyboardPresence: false if no explicit value is set + * for forceSimulatorSoftwareKeyboardPresence capability since Appium 1.22.0. + * + * @param value Whether to connect hardware keyboard to Simulator. + * @return self instance for chaining. + */ + default T setConnectHardwareKeyboard(boolean value) { + return amend(CONNECT_HARDWARE_KEYBOARD_OPTION, value); + } + + /** + * Get whether to connect hardware keyboard to Simulator. + * + * @return True or false. + */ + default Optional doesConnectHardwareKeyboard() { + return Optional.ofNullable(toSafeBoolean(getCapability(CONNECT_HARDWARE_KEYBOARD_OPTION))); + } +} 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 new file mode 100644 index 000000000..af501a76e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsCustomSslCertOption.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.ios.options.simulator; + +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 SupportsCustomSslCertOption> extends + Capabilities, CanSetCapability { + String CUSTOM_SSLCERT_OPTION = "customSSLCert"; + + /** + * Adds a root SSL certificate to IOS Simulator. + * The certificate content must be provided in PEM format. + * + * @param cert Certificate content in PEM format. + * @return self instance for chaining. + */ + default T setCustomSSLCert(String cert) { + return amend(CUSTOM_SSLCERT_OPTION, cert); + } + + /** + * Get the SSL certificate content. + * + * @return Certificate content. + */ + default Optional getCustomSSLCert() { + return Optional.ofNullable((String) getCapability(CUSTOM_SSLCERT_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsEnforceFreshSimulatorCreationOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsEnforceFreshSimulatorCreationOption.java new file mode 100644 index 000000000..010049af9 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsEnforceFreshSimulatorCreationOption.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.simulator; + +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 SupportsEnforceFreshSimulatorCreationOption> extends + Capabilities, CanSetCapability { + String ENFORCE_FRESH_SIMULATOR_CREATION_OPTION = "enforceFreshSimulatorCreation"; + + /** + * Enforce creation of a new simulator for each new test session. + * + * @return self instance for chaining. + */ + default T enforceFreshSimulatorCreation() { + return amend(ENFORCE_FRESH_SIMULATOR_CREATION_OPTION, true); + } + + /** + * Creates a new simulator in session creation and deletes it in session deletion. + * Defaults to false. + * + * @param value Whether to create a new simulator for each new test session. + * @return self instance for chaining. + */ + default T setEnforceFreshSimulatorCreation(boolean value) { + return amend(ENFORCE_FRESH_SIMULATOR_CREATION_OPTION, value); + } + + /** + * Get whether to create a new simulator for each new test session. + * + * @return True or false. + */ + default Optional doesEnforceFreshSimulatorCreation() { + return Optional.ofNullable(toSafeBoolean(getCapability(ENFORCE_FRESH_SIMULATOR_CREATION_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsForceSimulatorSoftwareKeyboardPresenceOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsForceSimulatorSoftwareKeyboardPresenceOption.java new file mode 100644 index 000000000..ba30bdcaa --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsForceSimulatorSoftwareKeyboardPresenceOption.java @@ -0,0 +1,65 @@ +/* + * 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.simulator; + +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 SupportsForceSimulatorSoftwareKeyboardPresenceOption> extends + Capabilities, CanSetCapability { + String FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE_OPTION = "forceSimulatorSoftwareKeyboardPresence"; + + /** + * Enforce software keyboard presence. + * + * @return self instance for chaining. + */ + default T forceSimulatorSoftwareKeyboardPresence() { + return amend(FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE_OPTION, true); + } + + /** + * Set this option to true in order to turn software keyboard on and turn + * hardware keyboard off in Simulator since Appium 1.22.0. This option helps + * to avoid Keyboard is not present error. It is set to true by default. + * Appium respects preset simulator software/hardware keyboard preference + * when this value is false, so connectHardwareKeyboard: false and + * forceSimulatorSoftwareKeyboardPresence: false means for Appium to keep + * the current Simulator keyboard preferences. This option has priority + * over connectHardwareKeyboard. + * + * @param value Whether to enforce software keyboard presence. + * @return self instance for chaining. + */ + default T setForceSimulatorSoftwareKeyboardPresence(boolean value) { + return amend(FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE_OPTION, value); + } + + /** + * Get to enforce software keyboard presence. + * + * @return True or false. + */ + default Optional doesForceSimulatorSoftwareKeyboardPresence() { + return Optional.ofNullable(toSafeBoolean(getCapability(FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsIosSimulatorLogsPredicateOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsIosSimulatorLogsPredicateOption.java new file mode 100644 index 000000000..da69dc30b --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsIosSimulatorLogsPredicateOption.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.ios.options.simulator; + +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 SupportsIosSimulatorLogsPredicateOption> extends + Capabilities, CanSetCapability { + String IOS_SIMULATOR_LOGS_PREDICATE_OPTION = "iosSimulatorLogsPredicate"; + + /** + * Set the --predicate flag in the ios simulator logs. + * + * @param predicate Predicate value, e.g. 'process != "locationd" AND process != "DTServiceHub"'. + * @return self instance for chaining. + */ + default T setIosSimulatorLogsPredicate(String predicate) { + return amend(IOS_SIMULATOR_LOGS_PREDICATE_OPTION, predicate); + } + + /** + * Get Simulator log filtering predicate. + * + * @return Predicate value. + */ + default Optional getIosSimulatorLogsPredicate() { + return Optional.ofNullable((String) getCapability(IOS_SIMULATOR_LOGS_PREDICATE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsKeepKeyChainsOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsKeepKeyChainsOption.java new file mode 100644 index 000000000..cc444192c --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsKeepKeyChainsOption.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.simulator; + +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 SupportsKeepKeyChainsOption> extends + Capabilities, CanSetCapability { + String KEEP_KEY_CHAINS_OPTION = "keepKeyChains"; + + /** + * Enforce preservation of Simulator keychains folder after full reset. + * + * @return self instance for chaining. + */ + default T keepKeyChains() { + return amend(KEEP_KEY_CHAINS_OPTION, true); + } + + /** + * Set the capability to true in order to preserve Simulator keychains folder after + * full reset. This feature has no effect on real devices. Defaults to false. + * + * @param value Whether to preserve Simulator keychains after full reset. + * @return self instance for chaining. + */ + default T setKeepKeyChains(boolean value) { + return amend(KEEP_KEY_CHAINS_OPTION, value); + } + + /** + * Get whether to preserve Simulator keychains after full reset. + * + * @return True or false. + */ + default Optional doesKeepKeyChains() { + return Optional.ofNullable(toSafeBoolean(getCapability(KEEP_KEY_CHAINS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsKeychainsExcludePatternsOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsKeychainsExcludePatternsOption.java new file mode 100644 index 000000000..960a5ed2e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsKeychainsExcludePatternsOption.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.ios.options.simulator; + +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 SupportsKeychainsExcludePatternsOption> extends + Capabilities, CanSetCapability { + String KEYCHAINS_EXCLUDE_PATTERNS_OPTION = "keychainsExcludePatterns"; + + /** + * This capability accepts comma-separated path patterns, + * which are going to be excluded from keychains restore while + * full reset is being performed on Simulator. It might be + * useful if you want to exclude only particular keychain types + * from being restored, like the applications keychain. This + * feature has no effect on real devices. E.g. "*keychain*.db*" + * to exclude applications keychain from being restored + * + * @param patterns Comma-separated list of file exclude patterns. + * @return self instance for chaining. + */ + default T setKeychainsExcludePatterns(String patterns) { + return amend(KEYCHAINS_EXCLUDE_PATTERNS_OPTION, patterns); + } + + /** + * Get keychains exclude patterns. + * + * @return Exclude patterns. + */ + default Optional getKeychainsExcludePatterns() { + return Optional.ofNullable((String) getCapability(KEYCHAINS_EXCLUDE_PATTERNS_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 new file mode 100644 index 000000000..3f55a335c --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsPermissionsOption.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.ios.options.simulator; + +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 SupportsPermissionsOption> extends + Capabilities, CanSetCapability { + String PERMISSIONS_OPTION = "permissions"; + + /** + * Allows setting of permissions for the specified application bundle on + * Simulator only. + * + * @param permissions Permissions mapping. + * @return self instance for chaining. + */ + default T setPermissions(Permissions permissions) { + return amend(PERMISSIONS_OPTION, permissions.toString()); + } + + /** + * Get Simulator permissions. + * + * @return Permissions object. + */ + default Optional getPermissions() { + return Optional.ofNullable(getCapability(PERMISSIONS_OPTION)) + .map(v -> new Permissions(String.valueOf(v))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsReduceMotionOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsReduceMotionOption.java new file mode 100644 index 000000000..5cee8feb0 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsReduceMotionOption.java @@ -0,0 +1,60 @@ +/* + * 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.simulator; + +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 SupportsReduceMotionOption> extends + Capabilities, CanSetCapability { + String REDUCE_MOTION_OPTION = "reduceMotion"; + + /** + * Enforce reduce motion accessibility preference. + * + * @return self instance for chaining. + */ + default T reduceMotion() { + return amend(REDUCE_MOTION_OPTION, true); + } + + /** + * It allows to turn on/off reduce motion accessibility preference. + * Setting reduceMotion on helps to reduce flakiness during tests. + * Only on simulators. + * + * @param value Whether to turn on/off reduce motion accessibility preference. + * @return self instance for chaining. + */ + default T setReduceMotion(boolean value) { + return amend(REDUCE_MOTION_OPTION, value); + } + + /** + * Get whether to reduce motion accessibility preference. + * + * @return True or false. + */ + default Optional doesReduceMotion() { + return Optional.ofNullable(toSafeBoolean(getCapability(REDUCE_MOTION_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsScaleFactorOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsScaleFactorOption.java new file mode 100644 index 000000000..1644da81d --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsScaleFactorOption.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.ios.options.simulator; + +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 SupportsScaleFactorOption> extends + Capabilities, CanSetCapability { + String SCALE_FACTOR_OPTION = "scaleFactor"; + + /** + * 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. + * Acceptable values for simulators running Xcode SDK 8 and older are: '1.0', + * '0.75', '0.5', '0.33' and '0.25', where '1.0' means 100% scale. + * For simulators running Xcode SDK 9 and above the value could be any valid + * positive float number. + * + * @param scaleFactor Scale factor value. + * @return self instance for chaining. + */ + default T setScaleFactor(String scaleFactor) { + return amend(SCALE_FACTOR_OPTION, scaleFactor); + } + + /** + * Get Simulator scale factor. + * + * @return Scale factor value. + */ + default Optional getScaleFactor() { + return Optional.ofNullable((String) getCapability(SCALE_FACTOR_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsShutdownOtherSimulatorsOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsShutdownOtherSimulatorsOption.java new file mode 100644 index 000000000..1fd54493f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsShutdownOtherSimulatorsOption.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.options.simulator; + +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 SupportsShutdownOtherSimulatorsOption> extends + Capabilities, CanSetCapability { + String SHUTDOWN_OTHER_SIMULATORS_OPTION = "shutdownOtherSimulators"; + + /** + * Enforce shutdown of other booted simulators except of the current one. + * + * @return self instance for chaining. + */ + default T shutdownOtherSimulators() { + return amend(SHUTDOWN_OTHER_SIMULATORS_OPTION, true); + } + + /** + * If this capability set to true and the current device under test is an iOS + * Simulator then Appium will try to shutdown all the other running Simulators + * before to start a new session. This might be useful while executing webview + * tests on different devices, since only one device can be debugged remotely + * at once due to an Apple bug. The capability only has an effect if + * --relaxed-security command line argument is provided to the server. + * Defaults to false. + * + * @param value Whether shutdown of other booted simulators except of the current one. + * @return self instance for chaining. + */ + default T setShutdownOtherSimulators(boolean value) { + return amend(SHUTDOWN_OTHER_SIMULATORS_OPTION, value); + } + + /** + * Get whether to shutdown of other booted simulators except of the current one. + * + * @return True or false. + */ + default Optional doesShutdownOtherSimulators() { + return Optional.ofNullable(toSafeBoolean(getCapability(SHUTDOWN_OTHER_SIMULATORS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorDevicesSetPathOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorDevicesSetPathOption.java new file mode 100644 index 000000000..cf6f0df41 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorDevicesSetPathOption.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.simulator; + +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 SupportsSimulatorDevicesSetPathOption> extends + Capabilities, CanSetCapability { + String SIMULATOR_DEVICES_SET_PATH_OPTION = "simulatorDevicesSetPath"; + + /** + * This capability allows to set an alternative path to the simulator devices + * set in case you have multiple sets deployed on your local system. Such + * feature could be useful if you, for example, would like to save disk space + * on the main system volume. + * + * @param path Alternative path to the simulator devices set. + * @return self instance for chaining. + */ + default T setSimulatorDevicesSetPath(String path) { + return amend(SIMULATOR_DEVICES_SET_PATH_OPTION, path); + } + + /** + * Get the alternative path to the simulator devices set. + * + * @return Path string. + */ + default Optional getSimulatorDevicesSetPath() { + return Optional.ofNullable((String) getCapability(SIMULATOR_DEVICES_SET_PATH_OPTION)); + } +} 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 new file mode 100644 index 000000000..5f3f5615b --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorPasteboardAutomaticSyncOption.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.ios.options.simulator; + +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 java.util.Locale.ROOT; + +public interface SupportsSimulatorPasteboardAutomaticSyncOption> extends + Capabilities, CanSetCapability { + String SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC = "simulatorPasteboardAutomaticSync"; + + /** + * Handle the -PasteboardAutomaticSync flag when simulator process launches. + * It could improve launching simulator performance not to sync pasteboard with + * the system when this value is off. on forces the flag enabled. system does + * not provide the flag to the launching command. on, off, or system is available. + * They are case-insensitive. Defaults to off. + * + * @param state Either on, off or system. + * @return self instance for chaining. + */ + default T setSimulatorPasteboardAutomaticSync(PasteboardSyncState state) { + return amend(SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC, state.toString().toLowerCase(ROOT)); + } + + /** + * Get the pasteboard automation sync state. + * + * @return Pasteboard sync state. + */ + default Optional getSimulatorPasteboardAutomaticSync() { + return Optional.ofNullable(getCapability(SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC)) + .map(v -> PasteboardSyncState.valueOf(String.valueOf(v).toUpperCase(ROOT))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorStartupTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorStartupTimeoutOption.java new file mode 100644 index 000000000..c3e593d7c --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorStartupTimeoutOption.java @@ -0,0 +1,55 @@ +/* + * 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.simulator; + +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 SupportsSimulatorStartupTimeoutOption> extends + Capabilities, CanSetCapability { + String SIMULATOR_STARTUP_TIMEOUT_OPTION = "simulatorStartupTimeout"; + + /** + * Allows to change the default timeout for Simulator startup. + * By default, this value is set to 120000ms (2 minutes), + * although the startup could take longer on a weak hardware + * or if other concurrent processes use much system resources + * during the boot up procedure. + * + * @param timeout Simulator startup timeout. + * @return self instance for chaining. + */ + default T setSimulatorStartupTimeout(Duration timeout) { + return amend(SIMULATOR_STARTUP_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the Simulator startup timeout. + * + * @return Timeout value. + */ + default Optional getSimulatorStartupTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(SIMULATOR_STARTUP_TIMEOUT_OPTION)) + ); + } +} 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 new file mode 100644 index 000000000..d7a1d115e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorTracePointerOption.java @@ -0,0 +1,61 @@ +/* + * 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.simulator; + +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 SupportsSimulatorTracePointerOption> extends + Capabilities, CanSetCapability { + String SIMULATOR_TRACE_POINTER_OPTION = "simulatorTracePointer"; + + /** + * Enforce highlight of pointer moves in the Simulator window. + * + * @return self instance for chaining. + */ + default T simulatorTracePointer() { + return amend(SIMULATOR_TRACE_POINTER_OPTION, true); + } + + /** + * Whether to highlight pointer moves in the Simulator window. + * The Simulator UI client must be shut down before the session + * startup in order for this capability to be applied properly. + * false by default. + * + * @param value Whether to highlight pointer moves in the Simulator window. + * @return self instance for chaining. + */ + default T setSimulatorTracePointer(boolean value) { + return amend(SIMULATOR_TRACE_POINTER_OPTION, value); + } + + /** + * Get whether to highlight pointer moves in the Simulator window. + * + * @return True or false. + */ + default Optional doesSimulatorTracePointer() { + return Optional.ofNullable(toSafeBoolean(getCapability(SIMULATOR_TRACE_POINTER_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorWindowCenterOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorWindowCenterOption.java new file mode 100644 index 000000000..480e0286e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsSimulatorWindowCenterOption.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.simulator; + +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 SupportsSimulatorWindowCenterOption> extends + Capabilities, CanSetCapability { + String SIMULATOR_WINDOW_CENTER_OPTION = "simulatorWindowCenter"; + + /** + * Allows to explicitly set the coordinates of Simulator window center + * for Xcode9+ SDK. This capability only has an effect if Simulator + * window has not been opened yet for the current session before it started. + * e.g. "{-100.0,100.0}" or "{500,500}", spaces are not allowed + * + * @param coordinates Window center coordinates. + * @return self instance for chaining. + */ + default T setSimulatorWindowCenter(String coordinates) { + return amend(SIMULATOR_WINDOW_CENTER_OPTION, coordinates); + } + + /** + * Get Simulator window center coordinates. + * + * @return Coordinates string. + */ + default Optional getSimulatorWindowCenter() { + return Optional.ofNullable((String) getCapability(SIMULATOR_WINDOW_CENTER_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/simulator/SupportsWebkitResponseTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsWebkitResponseTimeoutOption.java new file mode 100644 index 000000000..1ddd413ec --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/simulator/SupportsWebkitResponseTimeoutOption.java @@ -0,0 +1,52 @@ +/* + * 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.simulator; + +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 SupportsWebkitResponseTimeoutOption> extends + Capabilities, CanSetCapability { + String WEBKIT_RESPONSE_TIMEOUT_OPTION = "webkitResponseTimeout"; + + /** + * (Real device only) Set the time to wait for a response from + * WebKit in a Safari session. Defaults to 5000ms. + * + * @param timeout Response timeout. + * @return self instance for chaining. + */ + default T setWebkitResponseTimeout(Duration timeout) { + return amend(WEBKIT_RESPONSE_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the time to wait for a response from WebKit in a Safari session. + * + * @return Timeout value. + */ + default Optional getWebkitResponseTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(WEBKIT_RESPONSE_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/windows/WindowsElement.java b/src/main/java/io/appium/java_client/ios/options/wda/Keychain.java similarity index 73% rename from src/main/java/io/appium/java_client/windows/WindowsElement.java rename to src/main/java/io/appium/java_client/ios/options/wda/Keychain.java index 4f7ec7ba2..8a2e69891 100644 --- a/src/main/java/io/appium/java_client/windows/WindowsElement.java +++ b/src/main/java/io/appium/java_client/ios/options/wda/Keychain.java @@ -14,10 +14,14 @@ * limitations under the License. */ -package io.appium.java_client.windows; +package io.appium.java_client.ios.options.wda; -import io.appium.java_client.FindsByWindowsAutomation; -import io.appium.java_client.MobileElement; +import lombok.Data; +import lombok.ToString; -public class WindowsElement extends MobileElement implements FindsByWindowsAutomation { +@ToString() +@Data() +public class Keychain { + private final String path; + private final String password; } 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 new file mode 100644 index 000000000..08b06f9f8 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/ProcessArguments.java @@ -0,0 +1,56 @@ +/* + * 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 lombok.ToString; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@ToString() +public class ProcessArguments { + private final List args; + private final Map env; + + public ProcessArguments(List args, Map env) { + this.args = args; + this.env = env; + } + + public ProcessArguments(List args) { + this(args, null); + } + + public ProcessArguments(Map env) { + this(null, env); + } + + /** + * Returns the data object content as a map. + * + * @return Properties as a map. + */ + 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)); + return Collections.unmodifiableMap(result); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsAllowProvisioningDeviceRegistrationOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsAllowProvisioningDeviceRegistrationOption.java new file mode 100644 index 000000000..0ddf36ddb --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsAllowProvisioningDeviceRegistrationOption.java @@ -0,0 +1,60 @@ +/* + * 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 SupportsAllowProvisioningDeviceRegistrationOption> extends + Capabilities, CanSetCapability { + String ALLOW_PROVISIONING_DEVICE_REGISTRATION_OPTION = "allowProvisioningDeviceRegistration"; + + /** + * Allows xcodebuild to register your destination device on the developer portal. + * + * @return self instance for chaining. + */ + default T allowProvisioningDeviceRegistration() { + return amend(ALLOW_PROVISIONING_DEVICE_REGISTRATION_OPTION, true); + } + + /** + * Allow xcodebuild to register your destination device on the developer portal + * if necessary. Requires a developer account to have been added in Xcode's Accounts + * preference pane. Defaults to false. + * + * @param value Whether to allow xcodebuild to register your destination device on the developer portal. + * @return self instance for chaining. + */ + default T setAllowProvisioningDeviceRegistration(boolean value) { + return amend(ALLOW_PROVISIONING_DEVICE_REGISTRATION_OPTION, value); + } + + /** + * Get whether to allow xcodebuild to register your destination device on the developer portal. + * + * @return True or false. + */ + default Optional doesAllowProvisioningDeviceRegistration() { + return Optional.ofNullable(toSafeBoolean(getCapability(ALLOW_PROVISIONING_DEVICE_REGISTRATION_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsAutoAcceptAlertsOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsAutoAcceptAlertsOption.java new file mode 100644 index 000000000..1a3a86347 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsAutoAcceptAlertsOption.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 SupportsAutoAcceptAlertsOption> extends + Capabilities, CanSetCapability { + String AUTO_ACCEPT_ALERTS_OPTION = "autoAcceptAlerts"; + + /** + * Enforce to accept all alerts automatically. + * + * @return self instance for chaining. + */ + default T autoAcceptAlerts() { + return amend(AUTO_ACCEPT_ALERTS_OPTION, true); + } + + /** + * Accept all iOS alerts automatically if they pop up. This includes privacy + * access permission alerts (e.g., location, contacts, photos). Default is false. + * + * @param value Whether to accepts alerts automatically. + * @return self instance for chaining. + */ + default T setAutoAcceptAlerts(boolean value) { + return amend(AUTO_ACCEPT_ALERTS_OPTION, value); + } + + /** + * Get whether to accept all alerts automatically. + * + * @return True or false. + */ + default Optional doesAutoAcceptAlerts() { + return Optional.ofNullable(toSafeBoolean(getCapability(AUTO_ACCEPT_ALERTS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsAutoDismissAlertsOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsAutoDismissAlertsOption.java new file mode 100644 index 000000000..82a85754e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsAutoDismissAlertsOption.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 SupportsAutoDismissAlertsOption> extends + Capabilities, CanSetCapability { + String AUTO_DISMISS_ALERTS_OPTION = "autoDismissAlerts"; + + /** + * Enforce to dismiss all alerts automatically. + * + * @return self instance for chaining. + */ + default T autoDismissAlerts() { + return amend(AUTO_DISMISS_ALERTS_OPTION, true); + } + + /** + * Dismiss all iOS alerts automatically if they pop up. This includes privacy + * access permission alerts (e.g., location, contacts, photos). Default is false. + * + * @param value Whether to dismiss alerts automatically. + * @return self instance for chaining. + */ + default T setAutoDismissAlerts(boolean value) { + return amend(AUTO_DISMISS_ALERTS_OPTION, value); + } + + /** + * Get whether to dismiss all alerts automatically. + * + * @return True or false. + */ + default Optional doesAutoDismissAlerts() { + return Optional.ofNullable(toSafeBoolean(getCapability(AUTO_DISMISS_ALERTS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsDerivedDataPathOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsDerivedDataPathOption.java new file mode 100644 index 000000000..e3bfc1f2f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsDerivedDataPathOption.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.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 SupportsDerivedDataPathOption> extends + Capabilities, CanSetCapability { + String DERIVED_DATA_PATH_OPTION = "derivedDataPath"; + + /** + * Use along with usePrebuiltWDA capability and choose where to search for the existing WDA app. If the capability + * is not set then Xcode will store the derived data in the default root taken from preferences. + * It also makes sense to choose different folders for parallel WDA sessions. + * + * @param path Derived data folder path. + * @return self instance for chaining. + */ + default T setDerivedDataPath(String path) { + return amend(DERIVED_DATA_PATH_OPTION, path); + } + + /** + * Get the path to the derived data WDA folder. + * + * @return Derived data folder path. + */ + default Optional getDerivedDataPath() { + return Optional.ofNullable((String) getCapability(DERIVED_DATA_PATH_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsDisableAutomaticScreenshotsOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsDisableAutomaticScreenshotsOption.java new file mode 100644 index 000000000..90c0b2683 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsDisableAutomaticScreenshotsOption.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.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 SupportsDisableAutomaticScreenshotsOption> extends + Capabilities, CanSetCapability { + String DISABLE_AUTOMATIC_SCREENSHOTS_OPTION = "disableAutomaticScreenshots"; + + /** + * Disable automatic screenshots taken by XCTest at every interaction. + * Default is up to WebDriverAgent's config to decide, which currently + * defaults to true. + * + * @param value Whether to disable automatic XCTest screenshots. + * @return self instance for chaining. + */ + default T setDisableAutomaticScreenshots(boolean value) { + return amend(DISABLE_AUTOMATIC_SCREENSHOTS_OPTION, value); + } + + /** + * Get whether to disable automatic XCTest screenshots. + * + * @return True or false. + */ + default Optional doesDisableAutomaticScreenshots() { + return Optional.ofNullable(toSafeBoolean(getCapability(DISABLE_AUTOMATIC_SCREENSHOTS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsForceAppLaunchOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsForceAppLaunchOption.java new file mode 100644 index 000000000..4c2ab4780 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsForceAppLaunchOption.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.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 SupportsForceAppLaunchOption> extends + Capabilities, CanSetCapability { + String FORCE_APP_LAUNCH_OPTION = "forceAppLaunch"; + + /** + * Specify if the app should be forcefully restarted if it is already + * running on session startup. This capability only has an effect if an + * application identifier has been passed to the test session (either + * explicitly, by setting bundleId, or implicitly, by providing app). + * Default is true unless noReset capability is set to true. + * + * @param value Whether to enforce app restart on session startup. + * @return self instance for chaining. + */ + default T setForceAppLaunch(boolean value) { + return amend(FORCE_APP_LAUNCH_OPTION, value); + } + + /** + * Get whether to enforce app restart on session startup. + * + * @return True or false. + */ + default Optional doesForceAppLaunch() { + return Optional.ofNullable(toSafeBoolean(getCapability(FORCE_APP_LAUNCH_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsKeychainOptions.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsKeychainOptions.java new file mode 100644 index 000000000..92e7a33bf --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsKeychainOptions.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.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 SupportsKeychainOptions> extends + Capabilities, CanSetCapability { + String KEYCHAIN_PATH_OPTION = "keychainPath"; + String KEYCHAIN_PASSWORD_OPTION = "keychainPassword"; + + /** + * Provides details to access custom keychain, which + * contains the private development key exported from the system keychain. + * + * @param keychain Keychain access properties. + * @return self instance for chaining. + */ + default T setKeychain(Keychain keychain) { + return amend(KEYCHAIN_PATH_OPTION, keychain.getPath()) + .amend(KEYCHAIN_PASSWORD_OPTION, keychain.getPassword()); + } + + /** + * Get details to access custom keychain. + * + * @return Keychain access properties + */ + default Optional getKeychain() { + String path = (String) getCapability(KEYCHAIN_PATH_OPTION); + String password = (String) getCapability(KEYCHAIN_PASSWORD_OPTION); + return path == null || password == null + ? Optional.empty() + : Optional.of(new Keychain(path, password)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsMaxTypingFrequencyOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsMaxTypingFrequencyOption.java new file mode 100644 index 000000000..bbc434838 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsMaxTypingFrequencyOption.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.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.toInteger; + +public interface SupportsMaxTypingFrequencyOption> extends + Capabilities, CanSetCapability { + String MAX_TYPING_FREQUENCY_OPTION = "maxTypingFrequency"; + + /** + * 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. + * + * @param frequency The number of keystrokes per minute. + * @return self instance for chaining. + */ + default T setMaxTypingFrequency(int frequency) { + return amend(MAX_TYPING_FREQUENCY_OPTION, frequency); + } + + /** + * Get the number of keystrokes per minute. + * + * @return The number of keystrokes per minute. + */ + default Optional getMaxTypingFrequency() { + return Optional.ofNullable(toInteger(getCapability(MAX_TYPING_FREQUENCY_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsMjpegServerPortOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsMjpegServerPortOption.java new file mode 100644 index 000000000..6b14fab5e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsMjpegServerPortOption.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.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.toInteger; + +public interface SupportsMjpegServerPortOption> extends + Capabilities, CanSetCapability { + String MJPEG_SERVER_PORT_OPTION = "mjpegServerPort"; + + /** + * The port number on which WDA broadcasts screenshots stream encoded into MJPEG + * format from the device under test. It might be necessary to change this value + * if the default port is busy because of other tests running in parallel. + * Default value: 9100. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setMjpegServerPort(int port) { + return amend(MJPEG_SERVER_PORT_OPTION, port); + } + + /** + * Get the port number on which WDA broadcasts screenshots stream encoded into MJPEG + * format from the device under test. + * + * @return The port number. + */ + default Optional getMjpegServerPort() { + return Optional.ofNullable(toInteger(getCapability(MJPEG_SERVER_PORT_OPTION))); + } +} 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/SupportsProcessArgumentsOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsProcessArgumentsOption.java new file mode 100644 index 000000000..2f30a6e1d --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsProcessArgumentsOption.java @@ -0,0 +1,56 @@ +/* + * 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.List; +import java.util.Map; +import java.util.Optional; + +public interface SupportsProcessArgumentsOption> extends + Capabilities, CanSetCapability { + String PROCESS_ARGUMENTS_OPTION = "processArguments"; + + /** + * Provides process arguments and environment which will be sent + * to the WebDriverAgent server. + * + * @param pa Process arguments. + * @return self instance for chaining. + */ + default T setProcessArguments(ProcessArguments pa) { + return amend(PROCESS_ARGUMENTS_OPTION, pa.toMap()); + } + + /** + * Get process arguments of the app under test. + * + * @return Process arguments. + */ + @SuppressWarnings("unchecked") + default Optional getProcessArguments() { + Map pa = (Map) getCapability(PROCESS_ARGUMENTS_OPTION); + return pa == null || !(pa.containsKey("args") || pa.containsKey("env")) + ? Optional.empty() + : Optional.of(new ProcessArguments( + (List) pa.getOrDefault("args", null), + (Map) pa.getOrDefault("env", null))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsResultBundlePathOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsResultBundlePathOption.java new file mode 100644 index 000000000..01156e770 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsResultBundlePathOption.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.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 SupportsResultBundlePathOption> extends + Capabilities, CanSetCapability { + String RESULT_BUNDLE_PATH_OPTION = "resultBundlePath"; + + /** + * Specify the path to the result bundle path as xcodebuild argument for + * WebDriverAgent build under a security flag. WebDriverAgent process must + * start/stop every time to pick up changed value of this property. + * Specifying useNewWDA to true may help there. Please read man xcodebuild + * for more details. + * + * @param path The path where the resulting XCTest bundle should be stored. + * @return self instance for chaining. + */ + default T setResultBundlePath(String path) { + return amend(RESULT_BUNDLE_PATH_OPTION, path); + } + + /** + * Get the path where the resulting XCTest bundle should be stored. + * + * @return XCTest result bundle path. + */ + default Optional getResultBundlePath() { + return Optional.ofNullable((String) getCapability(RESULT_BUNDLE_PATH_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsScreenshotQualityOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsScreenshotQualityOption.java new file mode 100644 index 000000000..0eb3481f7 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsScreenshotQualityOption.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.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.toInteger; + +public interface SupportsScreenshotQualityOption> extends + Capabilities, CanSetCapability { + String SCREENSHOT_QUALITY_OPTION = "screenshotQuality"; + + /** + * Changes the quality of phone display screenshots following + * xctest/xctimagequality Default value is 1. 0 is the highest and + * 2 is the lowest quality. You can also change it via settings + * command. 0 might cause OutOfMemory crash on high-resolution + * devices like iPad Pro. + * + * @param quality Quality value in range 0..2. + * @return self instance for chaining. + */ + default T setScreenshotQuality(int quality) { + return amend(SCREENSHOT_QUALITY_OPTION, quality); + } + + /** + * Get the screenshot quality value. + * + * @return The screenshot quality value. + */ + default Optional getScreenshotQuality() { + return Optional.ofNullable(toInteger(getCapability(SCREENSHOT_QUALITY_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsShouldTerminateAppOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsShouldTerminateAppOption.java new file mode 100644 index 000000000..4c92b812f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsShouldTerminateAppOption.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.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 SupportsShouldTerminateAppOption> extends + Capabilities, CanSetCapability { + String SHOULD_TERMINATE_APP_OPTION = "shouldTerminateApp"; + + /** + * Specify if the app should be terminated on session end. + * This capability only has an effect if an application identifier + * has been passed to the test session (either explicitly, + * by setting bundleId, or implicitly, by providing app). + * Default is true unless noReset capability is set to true. + * + * @param value Whether to enforce app termination on session quit. + * @return self instance for chaining. + */ + default T setShouldTerminateApp(boolean value) { + return amend(SHOULD_TERMINATE_APP_OPTION, value); + } + + /** + * Get whether to enforce app termination on session quit. + * + * @return True or false. + */ + default Optional doesTerminateApp() { + return Optional.ofNullable(toSafeBoolean(getCapability(SHOULD_TERMINATE_APP_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsShouldUseSingletonTestManagerOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsShouldUseSingletonTestManagerOption.java new file mode 100644 index 000000000..16ee1d2cd --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsShouldUseSingletonTestManagerOption.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 SupportsShouldUseSingletonTestManagerOption> extends + Capabilities, CanSetCapability { + String SHOULD_USE_SINGLETON_TEST_MANAGER_OPTION = "shouldUseSingletonTestManager"; + + /** + * Enforce usage of the default proxy for test management within WebDriverAgent. + * + * @return self instance for chaining. + */ + default T shouldUseSingletonTestManager() { + return amend(SHOULD_USE_SINGLETON_TEST_MANAGER_OPTION, true); + } + + /** + * Use default proxy for test management within WebDriverAgent. Setting this to false + * sometimes helps with socket hangup problems. Defaults to true. + * + * @param value Whether to use the default proxy for test management within WebDriverAgent. + * @return self instance for chaining. + */ + default T setShouldUseSingletonTestManager(boolean value) { + return amend(SHOULD_USE_SINGLETON_TEST_MANAGER_OPTION, value); + } + + /** + * Get whether to use the default proxy for test management within WebDriverAgent. + * + * @return True or false. + */ + default Optional doesUseSingletonTestManager() { + return Optional.ofNullable(toSafeBoolean(getCapability(SHOULD_USE_SINGLETON_TEST_MANAGER_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsShowXcodeLogOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsShowXcodeLogOption.java new file mode 100644 index 000000000..902b67ced --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsShowXcodeLogOption.java @@ -0,0 +1,61 @@ +/* + * 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 SupportsShowXcodeLogOption> extends + Capabilities, CanSetCapability { + String SHOW_XCODE_LOG_OPTION = "showXcodeLog"; + + /** + * Enforce to display the output of the Xcode command used to run the tests + * in Appium logs. + * + * @return self instance for chaining. + */ + default T showXcodeLog() { + return amend(SHOW_XCODE_LOG_OPTION, true); + } + + /** + * Whether to display the output of the Xcode command used to run the tests in + * Appium logs. If this is true, there will be lots of extra logging at startup. + * Defaults to false. + * + * @param value Whether to display the output of the Xcode command used to run the tests. + * @return self instance for chaining. + */ + default T setShowXcodeLog(boolean value) { + return amend(SHOW_XCODE_LOG_OPTION, value); + } + + /** + * Get whether to display the output of the Xcode command used to run the tests. + * + * @return True or false. + */ + default Optional doesShowXcodeLog() { + return Optional.ofNullable(toSafeBoolean(getCapability(SHOW_XCODE_LOG_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsSimpleIsVisibleCheckOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsSimpleIsVisibleCheckOption.java new file mode 100644 index 000000000..72bebc33a --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsSimpleIsVisibleCheckOption.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.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 SupportsSimpleIsVisibleCheckOption> extends + Capabilities, CanSetCapability { + String SIMPLE_IS_VISIBLE_CHECK_OPTION = "simpleIsVisibleCheck"; + + /** + * Enforce usage of native methods for determining visibility of elements. + * + * @return self instance for chaining. + */ + default T simpleIsVisibleCheck() { + return amend(SIMPLE_IS_VISIBLE_CHECK_OPTION, true); + } + + /** + * 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. + * + * @param value Whether to use native methods for determining visibility of elements + * @return self instance for chaining. + */ + default T setSimpleIsVisibleCheck(boolean value) { + return amend(SIMPLE_IS_VISIBLE_CHECK_OPTION, value); + } + + /** + * Get whether to use native methods for determining visibility of elements. + * + * @return True or false. + */ + default Optional doesSimpleIsVisibleCheck() { + return Optional.ofNullable(toSafeBoolean(getCapability(SIMPLE_IS_VISIBLE_CHECK_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsUpdatedWdaBundleIdOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUpdatedWdaBundleIdOption.java new file mode 100644 index 000000000..2e4da983f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUpdatedWdaBundleIdOption.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.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 SupportsUpdatedWdaBundleIdOption> extends + Capabilities, CanSetCapability { + String UPDATED_WDA_BUNDLE_ID_OPTION = "updatedWDABundleId"; + + /** + * Bundle id to update WDA to before building and launching on real devices. + * This bundle id must be associated with a valid provisioning profile. + * + * @param identifier Bundle identifier. + * @return self instance for chaining. + */ + default T setUpdatedWdaBundleId(String identifier) { + return amend(UPDATED_WDA_BUNDLE_ID_OPTION, identifier); + } + + /** + * Get the WDA bundle identifier. + * + * @return Identifier value. + */ + default Optional getUpdatedWdaBundleId() { + return Optional.ofNullable((String) getCapability(UPDATED_WDA_BUNDLE_ID_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseNativeCachingStrategyOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseNativeCachingStrategyOption.java new file mode 100644 index 000000000..10f394aee --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseNativeCachingStrategyOption.java @@ -0,0 +1,52 @@ +/* + * 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 SupportsUseNativeCachingStrategyOption> extends + Capabilities, CanSetCapability { + String USE_NATIVE_CACHING_STRATEGY_OPTION = "useNativeCachingStrategy"; + + /** + * Set this capability to false in order to use the custom elements caching + * strategy. This might help to avoid stale element exception on property + * change. By default, the native XCTest cache resolution is used (true) + * for all native locators (e.g. all, but xpath). + * + * @param value Whether to use the native caching strategy. + * @return self instance for chaining. + */ + default T setUseNativeCachingStrategy(boolean value) { + return amend(USE_NATIVE_CACHING_STRATEGY_OPTION, value); + } + + /** + * Get whether to use the native caching strategy. + * + * @return True or false. + */ + default Optional doesUseNativeCachingStrategy() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_NATIVE_CACHING_STRATEGY_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseNewWdaOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseNewWdaOption.java new file mode 100644 index 000000000..2422c078c --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseNewWdaOption.java @@ -0,0 +1,70 @@ +/* + * 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 SupportsUseNewWdaOption> extends + Capabilities, CanSetCapability { + String USE_NEW_WDA_OPTION = "useNewWDA"; + + /** + * Enforce uninstall of any existing WebDriverAgent app on the device under test. + * + * @return self instance for chaining. + */ + default T useNewWDA() { + return amend(USE_NEW_WDA_OPTION, true); + } + + /** + * If true, forces uninstall of any existing WebDriverAgent app on device. + * Set it to 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 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 + * webDriverAgentUrl capability is not set. The negative/unset value of useNewWDA + * capability has no effect prior to xcuitest driver version 2.35.0. + * + * @param value Whether to force uninstall of any existing WebDriverAgent app on device. + * @return self instance for chaining. + */ + default T setUseNewWDA(boolean value) { + return amend(USE_NEW_WDA_OPTION, value); + } + + /** + * Get whether to uninstall of any existing WebDriverAgent app on the device under test. + * + * @return True or false. + */ + default Optional doesUseNewWDA() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_NEW_WDA_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsUsePrebuiltWdaOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUsePrebuiltWdaOption.java new file mode 100644 index 000000000..bf4ce84ff --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUsePrebuiltWdaOption.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 SupportsUsePrebuiltWdaOption> extends + Capabilities, CanSetCapability { + String USE_PREBUILT_WDA_OPTION = "usePrebuiltWDA"; + + /** + * Enforce to skip the build phase of running the WDA app. + * + * @return self instance for chaining. + */ + default T usePrebuiltWda() { + return amend(USE_PREBUILT_WDA_OPTION, true); + } + + /** + * 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. + * + * @param value Whether to skip the build phase of running the WDA app. + * @return self instance for chaining. + */ + default T setUsePrebuiltWda(boolean value) { + return amend(USE_PREBUILT_WDA_OPTION, value); + } + + /** + * Get whether to skip the build phase of running the WDA app. + * + * @return True or false. + */ + default Optional doesUsePrebuiltWda() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_PREBUILT_WDA_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/SupportsUseSimpleBuildTestOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseSimpleBuildTestOption.java new file mode 100644 index 000000000..8b455e629 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseSimpleBuildTestOption.java @@ -0,0 +1,60 @@ +/* + * 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 SupportsUseSimpleBuildTestOption> extends + Capabilities, CanSetCapability { + String USE_SIMPLE_BUILD_TEST_OPTION = "useSimpleBuildTest"; + + /** + * Enforce usage of simple build test. + * + * @return self instance for chaining. + */ + default T useSimpleBuildTest() { + return amend(USE_SIMPLE_BUILD_TEST_OPTION, true); + } + + /** + * Build with build and run test with test in xcodebuild for all Xcode versions if + * this is true, or build with build-for-testing and run tests with + * test-without-building for over Xcode 8 if this is false. Defaults to false. + * + * @param value Whether to use simple build test. + * @return self instance for chaining. + */ + default T setUseSimpleBuildTest(boolean value) { + return amend(USE_SIMPLE_BUILD_TEST_OPTION, value); + } + + /** + * Get whether to use simple build test. + * + * @return True or false. + */ + default Optional doesUseSimpleBuildTest() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_SIMPLE_BUILD_TEST_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseXctestrunFileOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseXctestrunFileOption.java new file mode 100644 index 000000000..f29c55a77 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsUseXctestrunFileOption.java @@ -0,0 +1,69 @@ +/* + * 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 SupportsUseXctestrunFileOption> extends + Capabilities, CanSetCapability { + String USE_XCTESTRUN_FILE_OPTION = "useXctestrunFile"; + + /** + * Enforce usage of .xctestrun file to launch WDA. + * + * @return self instance for chaining. + */ + default T useXctestrunFile() { + return amend(USE_XCTESTRUN_FILE_OPTION, true); + } + + /** + * Use Xctestrun file to launch WDA. It will search for such file in bootstrapPath. + * Expected name of file is WebDriverAgentRunner_iphoneos<sdkVersion>-arm64.xctestrun for + * real device and WebDriverAgentRunner_iphonesimulator<sdkVersion>-x86_64.xctestrun for + * simulator. One can do build-for-testing for WebDriverAgent project for simulator and + * real device and then you will see Product Folder like this and you need to copy content + * of this folder at bootstrapPath location. Since this capability expects that you have + * already built WDA project, it neither checks whether you have necessary dependencies to + * build WDA nor will it try to build project. Defaults to false. Tips: Xcodebuild builds for the + * target platform version. We'd recommend you to build with minimal OS version which you'd + * like to run as the original WDA module. e.g. If you build WDA for 12.2, the module cannot + * run on iOS 11.4 because of loading some module error on simulator. A module built with 11.4 + * can work on iOS 12.2. (This is xcodebuild's expected behaviour.) + * + * @param value Whether to use .xctestrun file to launch WDA. + * @return self instance for chaining. + */ + default T setUseXctestrunFile(boolean value) { + return amend(USE_XCTESTRUN_FILE_OPTION, value); + } + + /** + * Get whether to use of .xctestrun file to launch WDA + * + * @return True or false. + */ + default Optional doesUseXctestrunFile() { + return Optional.ofNullable(toSafeBoolean(getCapability(USE_XCTESTRUN_FILE_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 new file mode 100644 index 000000000..f9dd2401a --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWaitForIdleTimeoutOption.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.ios.options.wda; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsWaitForIdleTimeoutOption> extends + Capabilities, CanSetCapability { + String WAIT_FOR_IDLE_TIMEOUT_OPTION = "waitForIdleTimeout"; + + /** + * The time to wait until the application under test is idling. + * XCTest requires the app's main thread to be idling in order to execute any action on it, + * so WDA might not even start/freeze if the app under test is constantly hogging the main + * thread. The default value is 10 (seconds). Setting it to zero disables idling checks completely + * (not recommended) and has the same effect as setting waitForQuiescence to false. + * Available since Appium 1.20.0. + * + * @param timeout Idle timeout. + * @return self instance for chaining. + */ + default T setWaitForIdleTimeout(Duration timeout) { + return amend(WAIT_FOR_IDLE_TIMEOUT_OPTION, timeout.toMillis() / 1000.0); + } + + /** + * Get the maximum timeout to wait until WDA responds to HTTP requests. + * + * @return Timeout value. + */ + default Optional getWaitForIdleTimeout() { + return Optional.ofNullable(getCapability(WAIT_FOR_IDLE_TIMEOUT_OPTION)) + .map(CapabilityHelpers::toDouble) + .map(d -> toDuration((long) (d * 1000.0))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWaitForQuiescenceOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWaitForQuiescenceOption.java new file mode 100644 index 000000000..9c49e469a --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWaitForQuiescenceOption.java @@ -0,0 +1,52 @@ +/* + * 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 SupportsWaitForQuiescenceOption> extends + Capabilities, CanSetCapability { + String WAIT_FOR_QUIESCENCE_OPTION = "waitForQuiescence"; + + /** + * It allows to turn on/off waiting for application quiescence in WebDriverAgent, + * while performing queries. The default value is true. You can avoid this kind + * of issues if you turn it off. Consider using waitForIdleTimeout capability + * instead for this purpose since Appium 1.20.0. + * + * @param value Whether to wait for application quiescence. + * @return self instance for chaining. + */ + default T setWaitForQuiescence(boolean value) { + return amend(WAIT_FOR_QUIESCENCE_OPTION, value); + } + + /** + * Get whether to wait for application quiescence. + * + * @return True or false. + */ + default Optional doesWaitForQuiescence() { + return Optional.ofNullable(toSafeBoolean(getCapability(WAIT_FOR_QUIESCENCE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaBaseUrlOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaBaseUrlOption.java new file mode 100644 index 000000000..2404aa5ef --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaBaseUrlOption.java @@ -0,0 +1,66 @@ +/* + * 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.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.net.URL; +import java.util.Optional; + +public interface SupportsWdaBaseUrlOption> extends + Capabilities, CanSetCapability { + String WDA_BASE_URL_OPTION = "wdaBaseUrl"; + + /** + * This value, if specified, will be used as a prefix to build a custom + * WebDriverAgent url. It is different from webDriverAgentUrl, because + * if the latter is set then it expects WebDriverAgent to be already + * listening and skips the building phase. Defaults to http://localhost. + * + * @param url The URL prefix. + * @return self instance for chaining. + */ + default T setWdaBaseUrl(URL url) { + return amend(WDA_BASE_URL_OPTION, url.toString()); + } + + /** + * This value, if specified, will be used as a prefix to build a custom + * WebDriverAgent url. It is different from webDriverAgentUrl, because + * if the latter is set then it expects WebDriverAgent to be already + * listening and skips the building phase. Defaults to http://localhost. + * + * @param url The URL prefix. + * @return self instance for chaining. + */ + default T setWdaBaseUrl(String url) { + return amend(WDA_BASE_URL_OPTION, url); + } + + /** + * Get a prefix to build a custom WebDriverAgent URL. + * + * @return The URL prefix. + */ + default Optional getWdaBaseUrl() { + return Optional.ofNullable(getCapability(WDA_BASE_URL_OPTION)) + .map(CapabilityHelpers::toUrl); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaConnectionTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaConnectionTimeoutOption.java new file mode 100644 index 000000000..8885b8c3e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaConnectionTimeoutOption.java @@ -0,0 +1,52 @@ +/* + * 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.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 SupportsWdaConnectionTimeoutOption> extends + Capabilities, CanSetCapability { + String WDA_CONNECTION_TIMEOUT_OPTION = "wdaConnectionTimeout"; + + /** + * Connection timeout to wait for a response from WebDriverAgent. + * Defaults to 240000ms. + * + * @param timeout WDA connection timeout. + * @return self instance for chaining. + */ + default T setWdaConnectionTimeout(Duration timeout) { + return amend(WDA_CONNECTION_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the maximum timeout to wait until WDA responds to HTTP requests. + * + * @return Timeout value. + */ + default Optional getWdaConnectionTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(WDA_CONNECTION_TIMEOUT_OPTION)) + ); + } +} 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 new file mode 100644 index 000000000..3d48703b5 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaEventloopIdleDelayOption.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.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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsWdaEventloopIdleDelayOption> extends + Capabilities, CanSetCapability { + String WDA_EVENTLOOP_IDLE_DELAY_OPTION = "wdaEventloopIdleDelay"; + + /** + * Delays the invocation of -[XCUIApplicationProcess setEventLoopHasIdled:] by the + * duration specified with this capability. This can help quiescence apps + * that fail to do so for no obvious reason (and creating a session fails for + * that reason). This increases the time for session creation + * because -[XCUIApplicationProcess setEventLoopHasIdled:] is called multiple times. + * If you enable this capability start with at least 3 seconds and try increasing it, + * if creating the session still fails. Defaults to 0. + * + * @param duration Idle duration. + * @return self instance for chaining. + */ + default T setWdaEventloopIdleDelay(Duration duration) { + return amend(WDA_EVENTLOOP_IDLE_DELAY_OPTION, duration.toMillis() / 1000.0); + } + + /** + * Get the event loop idle delay. + * + * @return Idle duration. + */ + default Optional getWdaEventloopIdleDelay() { + return Optional.ofNullable(getCapability(WDA_EVENTLOOP_IDLE_DELAY_OPTION)) + .map(CapabilityHelpers::toDouble) + .map(d -> toDuration((long) (d * 1000.0))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaLaunchTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaLaunchTimeoutOption.java new file mode 100644 index 000000000..96ec72521 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaLaunchTimeoutOption.java @@ -0,0 +1,52 @@ +/* + * 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.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 SupportsWdaLaunchTimeoutOption> extends + Capabilities, CanSetCapability { + String WDA_LAUNCH_TIMEOUT_OPTION = "wdaLaunchTimeout"; + + /** + * Timeout to wait for WebDriverAgent to be pingable, + * e.g. finishes building. Defaults to 60000ms. + * + * @param timeout Timeout to wait until WDA is listening. + * @return self instance for chaining. + */ + default T setWdaLaunchTimeout(Duration timeout) { + return amend(WDA_LAUNCH_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the maximum timeout to wait until WDA is listening. + * + * @return Timeout value. + */ + default Optional getWdaLaunchTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(WDA_LAUNCH_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaLocalPortOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaLocalPortOption.java new file mode 100644 index 000000000..a011edeac --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaLocalPortOption.java @@ -0,0 +1,52 @@ +/* + * 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.toInteger; + +public interface SupportsWdaLocalPortOption> extends + Capabilities, CanSetCapability { + String WDA_LOCAL_PORT_OPTION = "wdaLocalPort"; + + /** + * This value, if specified, will be used to forward traffic from + * Mac host to real ios devices over USB. + * Default value is the same as the port number used by WDA on + * the device under test (8100). + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setWdaLocalPort(int port) { + return amend(WDA_LOCAL_PORT_OPTION, port); + } + + /** + * Get the local port number where the WDA traffic is being forwarded. + * + * @return The port number. + */ + default Optional getWdaLocalPort() { + return Optional.ofNullable(toInteger(getCapability(WDA_LOCAL_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaStartupRetriesOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaStartupRetriesOption.java new file mode 100644 index 000000000..e69a52d42 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaStartupRetriesOption.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; + +import static io.appium.java_client.internal.CapabilityHelpers.toInteger; + +public interface SupportsWdaStartupRetriesOption> extends + Capabilities, CanSetCapability { + String WDA_STARTUP_RETRIES_OPTION = "wdaStartupRetries"; + + /** + * Number of times to try to build and launch WebDriverAgent onto the device. + * Defaults to 2. + * + * @param count Retries count. + * @return self instance for chaining. + */ + default T setWdaStartupRetries(int count) { + return amend(WDA_STARTUP_RETRIES_OPTION, count); + } + + /** + * Get number of retries before to fail WDA deployment. + * + * @return Retries count. + */ + default Optional getWdaStartupRetries() { + return Optional.ofNullable(toInteger(getCapability(WDA_STARTUP_RETRIES_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaStartupRetryIntervalOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaStartupRetryIntervalOption.java new file mode 100644 index 000000000..c1fe04a35 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWdaStartupRetryIntervalOption.java @@ -0,0 +1,52 @@ +/* + * 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.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 SupportsWdaStartupRetryIntervalOption> extends + Capabilities, CanSetCapability { + String WDA_STARTUP_RETRY_INTERVAL_OPTION = "wdaStartupRetryInterval"; + + /** + * Time interval to wait between tries to build and launch WebDriverAgent. + * Defaults to 10000ms. + * + * @param interval Interval value. + * @return self instance for chaining. + */ + default T setWdaStartupRetryInterval(Duration interval) { + return amend(WDA_STARTUP_RETRY_INTERVAL_OPTION, interval.toMillis()); + } + + /** + * Get the interval to wait between tries to build and launch WebDriverAgent. + * + * @return Interval value. + */ + default Optional getWdaStartupRetryInterval() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(WDA_STARTUP_RETRY_INTERVAL_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/wda/SupportsWebDriverAgentUrlOption.java b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWebDriverAgentUrlOption.java new file mode 100644 index 000000000..a985e280b --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsWebDriverAgentUrlOption.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.ios.options.wda; + +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.net.URL; +import java.util.Optional; + +public interface SupportsWebDriverAgentUrlOption> extends + Capabilities, CanSetCapability { + String WEB_DRIVER_AGENT_URL_OPTION = "webDriverAgentUrl"; + + /** + * If provided, Appium will connect to an existing WebDriverAgent + * instance at this URL instead of starting a new one. + * + * @param url The URL where WDA is listening. + * @return self instance for chaining. + */ + default T setWebDriverAgentUrl(URL url) { + return amend(WEB_DRIVER_AGENT_URL_OPTION, url.toString()); + } + + /** + * If provided, Appium will connect to an existing WebDriverAgent + * instance at this URL instead of starting a new one. + * + * @param url The URL where WDA is listening. + * @return self instance for chaining. + */ + default T setWebDriverAgentUrl(String url) { + return amend(WEB_DRIVER_AGENT_URL_OPTION, url); + } + + /** + * Get the WDA URL. + * + * @return The URL where WDA is listening. + */ + default Optional getWebDriverAgentUrl() { + return Optional.ofNullable(getCapability(WEB_DRIVER_AGENT_URL_OPTION)) + .map(CapabilityHelpers::toUrl); + } +} 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 new file mode 100644 index 000000000..45d437195 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/SupportsXcodeCertificateOptions.java @@ -0,0 +1,56 @@ +/* + * 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 SupportsXcodeCertificateOptions> extends + Capabilities, CanSetCapability { + String XCODE_ORG_ID_OPTION = "xcodeOrgId"; + String XCODE_SIGNING_ID_OPTION = "xcodeSigningId"; + String DEFAULT_XCODE_SIGNING_ID = "iPhone Developer"; + + /** + * Provides a signing certificate for WebDriverAgent compilation. + * If signing id is not provided/null then it defaults to "iPhone Developer" + * + * @param cert Certificate credentials. + * @return self instance for chaining. + */ + default T setXcodeCertificate(XcodeCertificate cert) { + String signingId = Optional.ofNullable(cert.getXcodeSigningId()) + .orElse(DEFAULT_XCODE_SIGNING_ID); + return amend(XCODE_ORG_ID_OPTION, cert.getXcodeOrgId()) + .amend(XCODE_SIGNING_ID_OPTION, signingId); + } + + /** + * Get a signing certificate for WebDriverAgent compilation. + * + * @return Certificate value. + */ + 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)); + } +} diff --git a/src/main/java/io/appium/java_client/android/AndroidOptions.java b/src/main/java/io/appium/java_client/ios/options/wda/XcodeCertificate.java similarity index 60% rename from src/main/java/io/appium/java_client/android/AndroidOptions.java rename to src/main/java/io/appium/java_client/ios/options/wda/XcodeCertificate.java index 768bf75eb..49d2d4639 100644 --- a/src/main/java/io/appium/java_client/android/AndroidOptions.java +++ b/src/main/java/io/appium/java_client/ios/options/wda/XcodeCertificate.java @@ -14,19 +14,23 @@ * limitations under the License. */ -package io.appium.java_client.android; +package io.appium.java_client.ios.options.wda; -import io.appium.java_client.remote.MobileOptions; -import io.appium.java_client.remote.MobilePlatform; -import org.openqa.selenium.Capabilities; +import lombok.Data; +import lombok.ToString; -public class AndroidOptions extends MobileOptions { - public AndroidOptions() { - setPlatformName(MobilePlatform.ANDROID); +@ToString() +@Data() +public class XcodeCertificate { + private final String xcodeOrgId; + private final String xcodeSigningId; + + public XcodeCertificate(String xcodeOrgId, String xcodeSigningId) { + this.xcodeOrgId = xcodeOrgId; + this.xcodeSigningId = xcodeSigningId; } - public AndroidOptions(Capabilities source) { - this(); - merge(source); + public XcodeCertificate(String xcodeOrgId) { + this(xcodeOrgId, null); } } diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsAbsoluteWebLocationsOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsAbsoluteWebLocationsOption.java new file mode 100644 index 000000000..ae3375b70 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsAbsoluteWebLocationsOption.java @@ -0,0 +1,63 @@ +/* + * 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.webview; + +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 SupportsAbsoluteWebLocationsOption> extends + Capabilities, CanSetCapability { + String ABSOLUTE_WEB_LOCATIONS_OPTION = "absoluteWebLocations"; + + /** + * Enforces Get Element Location to return coordinates + * relative to the page origin for web view elements. + * + * @return self instance for chaining. + */ + default T absoluteWebLocations() { + return amend(ABSOLUTE_WEB_LOCATIONS_OPTION, true); + } + + /** + * This capability will direct the Get Element Location command, when used + * within webviews, to return coordinates which are relative to the origin of + * the page, rather than relative to the current scroll offset. This capability + * has no effect outside of webviews. Default false. + * + * @param value Whether to return coordinates relative to the page origin for web view elements. + * @return self instance for chaining. + */ + default T setAbsoluteWebLocations(boolean value) { + return amend(ABSOLUTE_WEB_LOCATIONS_OPTION, value); + } + + /** + * Get whether Get Element Location returns coordinates + * relative to the page origin for web view elements. + * + * @return True or false. + */ + default Optional doesAbsoluteWebLocations() { + return Optional.ofNullable(toSafeBoolean(getCapability(ABSOLUTE_WEB_LOCATIONS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsAdditionalWebviewBundleIdsOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsAdditionalWebviewBundleIdsOption.java new file mode 100644 index 000000000..a21044b8f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsAdditionalWebviewBundleIdsOption.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.ios.options.webview; + +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.CanSetCapability; +import org.openqa.selenium.Capabilities; + +import java.util.List; +import java.util.Optional; + +public interface SupportsAdditionalWebviewBundleIdsOption> extends + Capabilities, CanSetCapability { + String ADDITIONAL_WEBVIEW_BUNDLE_IDS_OPTION = "additionalWebviewBundleIds"; + + /** + * Array of possible bundle identifiers for webviews. This is sometimes + * necessary if the Web Inspector is found to be returning a modified + * bundle identifier for the app. Defaults to []. + * + * @param identifiers Identifiers list. + * @return self instance for chaining. + */ + default T setAdditionalWebviewBundleIds(List identifiers) { + return amend(ADDITIONAL_WEBVIEW_BUNDLE_IDS_OPTION, identifiers); + } + + /** + * Get the array of possible bundle identifiers for webviews. + * + * @return Identifier list. + */ + default Optional> getAdditionalWebviewBundleIds() { + //noinspection unchecked + return Optional.ofNullable( + (List) getCapability(ADDITIONAL_WEBVIEW_BUNDLE_IDS_OPTION) + ); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsEnableAsyncExecuteFromHttpsOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsEnableAsyncExecuteFromHttpsOption.java new file mode 100644 index 000000000..36e135436 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsEnableAsyncExecuteFromHttpsOption.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.webview; + +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 SupportsEnableAsyncExecuteFromHttpsOption> extends + Capabilities, CanSetCapability { + String ENABLE_ASYNC_EXECUTE_FROM_HTTPS_OPTION = "enableAsyncExecuteFromHttps"; + + /** + * Enforces to allow simulators to execute async JavaScript on pages using HTTPS. + * + * @return self instance for chaining. + */ + default T enableAsyncExecuteFromHttps() { + return amend(ENABLE_ASYNC_EXECUTE_FROM_HTTPS_OPTION, true); + } + + /** + * Capability to allow simulators to execute asynchronous JavaScript + * on pages using HTTPS. Defaults to false. + * + * @param value Whether to allow simulators to execute async JavaScript on pages using HTTPS. + * @return self instance for chaining. + */ + default T setEnableAsyncExecuteFromHttps(boolean value) { + return amend(ENABLE_ASYNC_EXECUTE_FROM_HTTPS_OPTION, value); + } + + /** + * Get whether to allow simulators to execute async JavaScript on pages using HTTPS. + * + * @return True or false. + */ + default Optional doesEnableAsyncExecuteFromHttps() { + return Optional.ofNullable(toSafeBoolean(getCapability(ENABLE_ASYNC_EXECUTE_FROM_HTTPS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsFullContextListOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsFullContextListOption.java new file mode 100644 index 000000000..da432480c --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsFullContextListOption.java @@ -0,0 +1,63 @@ +/* + * 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.webview; + +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 SupportsFullContextListOption> extends + Capabilities, CanSetCapability { + String FULL_CONTEXT_LIST_OPTION = "fullContextList"; + + /** + * Enforces to return the detailed information on contexts for the get available + * context command. + * + * @return self instance for chaining. + */ + default T fullContextList() { + return amend(FULL_CONTEXT_LIST_OPTION, true); + } + + /** + * Sets to return the detailed information on contexts for the get available + * context command. If this capability is enabled, then each item in the returned + * contexts list would additionally include WebView title, full URL and the bundle + * identifier. Defaults to false. + * + * @param value Whether to return the detailed info on available context command. + * @return self instance for chaining. + */ + default T setFullContextList(boolean value) { + return amend(FULL_CONTEXT_LIST_OPTION, value); + } + + /** + * Get whether to return the detailed information on contexts for the get available + * context command. + * + * @return True or false. + */ + default Optional doesFullContextList() { + return Optional.ofNullable(toSafeBoolean(getCapability(FULL_CONTEXT_LIST_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsIncludeSafariInWebviewsOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsIncludeSafariInWebviewsOption.java new file mode 100644 index 000000000..c7709050b --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsIncludeSafariInWebviewsOption.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.ios.options.webview; + +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 SupportsIncludeSafariInWebviewsOption> extends + Capabilities, CanSetCapability { + String INCLUDE_SAFARI_IN_WEBVIEWS_OPTION = "includeSafariInWebviews"; + + /** + * Enforces Safari web views to be added to the list of contexts available + * during a native/webview app test. + * + * @return self instance for chaining. + */ + default T includeSafariInWebviews() { + return amend(INCLUDE_SAFARI_IN_WEBVIEWS_OPTION, true); + } + + /** + * Add Safari web contexts to the list of contexts available during a + * native/webview app test. This is useful if the test opens Safari and + * needs to be able to interact with it. Defaults to false. + * + * @param value Whether to add Safari to the list of contexts available during a native/webview app test. + * @return self instance for chaining. + */ + default T setIncludeSafariInWebviews(boolean value) { + return amend(INCLUDE_SAFARI_IN_WEBVIEWS_OPTION, value); + } + + /** + * Get whether to add Safari web views to the list of contexts available + * during a native/webview app test. + * + * @return True or false. + */ + default Optional doesIncludeSafariInWebviews() { + return Optional.ofNullable(toSafeBoolean(getCapability(INCLUDE_SAFARI_IN_WEBVIEWS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsNativeWebTapOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsNativeWebTapOption.java new file mode 100644 index 000000000..5158a06f7 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsNativeWebTapOption.java @@ -0,0 +1,60 @@ +/* + * 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.webview; + +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 SupportsNativeWebTapOption> extends + Capabilities, CanSetCapability { + String NATIVE_WEB_TAP_OPTION = "nativeWebTap"; + + /** + * Enforces native non-javascript-based taps in web context mode. + * + * @return self instance for chaining. + */ + default T nativeWebTap() { + return amend(NATIVE_WEB_TAP_OPTION, true); + } + + /** + * Enable native, non-javascript-based taps being in web context mode. Defaults + * to false. Warning: sometimes the preciseness of native taps could be broken, + * because there is no reliable way to map web element coordinates to native ones. + * + * @param value Whether to enable native taps in web view mode. + * @return self instance for chaining. + */ + default T setNativeWebTap(boolean value) { + return amend(NATIVE_WEB_TAP_OPTION, value); + } + + /** + * Get whether to enable native taps in web view mode. + * + * @return True or false. + */ + default Optional doesNativeWebTap() { + return Optional.ofNullable(toSafeBoolean(getCapability(NATIVE_WEB_TAP_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsNativeWebTapStrictOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsNativeWebTapStrictOption.java new file mode 100644 index 000000000..0711d9b1f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsNativeWebTapStrictOption.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.webview; + +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 SupportsNativeWebTapStrictOption> extends + Capabilities, CanSetCapability { + String NATIVE_WEB_TAP_STRICT_OPTION = "nativeWebTapStrict"; + + /** + * Enforce native taps to be done by XCUITest driver rather than WebDriverAgent. + * + * @return self instance for chaining. + */ + default T nativeWebTapStrict() { + return amend(NATIVE_WEB_TAP_STRICT_OPTION, true); + } + + /** + * Configure native taps to be done by XCUITest driver rather than WebDriverAgent. + * Only applicable if nativeWebTap is enabled. false by default. + * + * @param value Whether native taps are done by XCUITest driver rather than WebDriverAgent. + * @return self instance for chaining. + */ + default T setNativeWebTapStrict(boolean value) { + return amend(NATIVE_WEB_TAP_STRICT_OPTION, value); + } + + /** + * Get whether native taps are done by XCUITest driver rather than WebDriverAgent. + * + * @return True or false. + */ + default Optional doesNativeWebTapStrict() { + return Optional.ofNullable(toSafeBoolean(getCapability(NATIVE_WEB_TAP_STRICT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariAllowPopupsOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariAllowPopupsOption.java new file mode 100644 index 000000000..d7ad7d1f0 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariAllowPopupsOption.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.ios.options.webview; + +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 SupportsSafariAllowPopupsOption> extends + Capabilities, CanSetCapability { + String SAFARI_ALLOW_POPUPS_OPTION = "safariAllowPopups"; + + /** + * Enforces to allow javascript to open popups in Safari. + * + * @return self instance for chaining. + */ + default T safariAllowPopups() { + return amend(SAFARI_ALLOW_POPUPS_OPTION, true); + } + + /** + * Allow javascript to open new windows in Safari. Default keeps current sim setting. + * + * @param value Whether to allow javascript to open popups in Safari. + * @return self instance for chaining. + */ + default T setSafariAllowPopups(boolean value) { + return amend(SAFARI_ALLOW_POPUPS_OPTION, value); + } + + /** + * Get whether to allow javascript to open new windows in Safari. + * + * @return True or false. + */ + default Optional doesSafariAllowPopups() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_ALLOW_POPUPS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariGarbageCollectOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariGarbageCollectOption.java new file mode 100644 index 000000000..2990e2a75 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariGarbageCollectOption.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.webview; + +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 SupportsSafariGarbageCollectOption> extends + Capabilities, CanSetCapability { + String SAFARI_GARBAGE_COLLECT_OPTION = "safariGarbageCollect"; + + /** + * Enforces to turn on garbage collection when executing scripts on Safari. + * + * @return self instance for chaining. + */ + default T safariGarbageCollect() { + return amend(SAFARI_GARBAGE_COLLECT_OPTION, true); + } + + /** + * Turns on/off Web Inspector garbage collection when executing scripts on Safari. + * Turning on may improve performance. Defaults to false. + * + * @param value Whether to turn on garbage collection when executing scripts on Safari. + * @return self instance for chaining. + */ + default T setSafariGarbageCollect(boolean value) { + return amend(SAFARI_GARBAGE_COLLECT_OPTION, value); + } + + /** + * Get whether to turn on garbage collection when executing scripts on Safari. + * + * @return True or false. + */ + default Optional doesSafariGarbageCollect() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_GARBAGE_COLLECT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariIgnoreFraudWarningOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariIgnoreFraudWarningOption.java new file mode 100644 index 000000000..ad51e338a --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariIgnoreFraudWarningOption.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.webview; + +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 SupportsSafariIgnoreFraudWarningOption> extends + Capabilities, CanSetCapability { + String SAFARI_IGNORE_FRAUD_WARNING_OPTION = "safariIgnoreFraudWarning"; + + /** + * Enforces to prevent Safari from showing a fraudulent website warning. + * + * @return self instance for chaining. + */ + default T safariIgnoreFraudWarning() { + return amend(SAFARI_IGNORE_FRAUD_WARNING_OPTION, true); + } + + /** + * Prevent Safari from showing a fraudulent website warning. + * Default keeps current sim setting.. + * + * @param value Whether to prevent Safari from showing a fraudulent website warning. + * @return self instance for chaining. + */ + default T setSafariIgnoreFraudWarning(boolean value) { + return amend(SAFARI_IGNORE_FRAUD_WARNING_OPTION, value); + } + + /** + * Get whether to prevent Safari from showing a fraudulent website warning. + * + * @return True or false. + */ + default Optional doesSafariIgnoreFraudWarning() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_IGNORE_FRAUD_WARNING_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariIgnoreWebHostnamesOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariIgnoreWebHostnamesOption.java new file mode 100644 index 000000000..e8e154e72 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariIgnoreWebHostnamesOption.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.ios.options.webview; + +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 SupportsSafariIgnoreWebHostnamesOption> extends + Capabilities, CanSetCapability { + String SAFARI_IGNORE_WEB_HOSTNAMES_OPTION = "safariIgnoreWebHostnames"; + + /** + * Provide a list of hostnames (comma-separated) that the Safari automation + * tools should ignore. This is to provide a workaround to prevent a webkit + * bug where the web context is unintentionally changed to a 3rd party website + * and the test gets stuck. The common culprits are search engines (yahoo, bing, + * google) and about:blank. + * + * @param hostnames E.g. 'www.yahoo.com, www.bing.com, www.google.com, about:blank'. + * @return self instance for chaining. + */ + default T setSafariIgnoreWebHostnames(String hostnames) { + return amend(SAFARI_IGNORE_WEB_HOSTNAMES_OPTION, hostnames); + } + + /** + * Get the comma-separated list of host names to be ignored. + * + * @return XCTest result bundle path. + */ + default Optional getSafariIgnoreWebHostnames() { + return Optional.ofNullable((String) getCapability(SAFARI_IGNORE_WEB_HOSTNAMES_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariInitialUrlOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariInitialUrlOption.java new file mode 100644 index 000000000..bd2193a3e --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariInitialUrlOption.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.ios.options.webview; + +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 SupportsSafariInitialUrlOption> extends + Capabilities, CanSetCapability { + String SAFARI_INITIAL_URL_OPTION = "safariInitialUrl"; + + /** + * Set initial safari url, default is a local welcome page. + * + * @param url Initial safari url. + * @return self instance for chaining. + */ + default T setSafariInitialUrl(String url) { + return amend(SAFARI_INITIAL_URL_OPTION, url); + } + + /** + * Get the initial safari url. + * + * @return Initial safari url. + */ + default Optional getSafariInitialUrl() { + return Optional.ofNullable((String) getCapability(SAFARI_INITIAL_URL_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariLogAllCommunicationHexDumpOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariLogAllCommunicationHexDumpOption.java new file mode 100644 index 000000000..4dbece202 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariLogAllCommunicationHexDumpOption.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.options.webview; + +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 SupportsSafariLogAllCommunicationHexDumpOption> extends + Capabilities, CanSetCapability { + String SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP_OPTION = "safariLogAllCommunicationHexDump"; + + /** + * Enforces logging of plists sent to and received from the Web Inspector + * in hex dump format. + * + * @return self instance for chaining. + */ + default T safariLogAllCommunicationHexDump() { + return amend(SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP_OPTION, true); + } + + /** + * Log all communication sent to and received from the Web Inspector, as raw + * hex dump and printable characters. This logging is done before any data + * manipulation, and so can elucidate some communication issues. Like + * appium:safariLogAllCommunication, this can produce a lot of data in some cases, + * so it is recommended to be used only when necessary. Defaults to false. + * + * @param value Whether to log all internal web debugger communication in hex dump format. + * @return self instance for chaining. + */ + default T setSafariLogAllCommunicationHexDump(boolean value) { + return amend(SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP_OPTION, value); + } + + /** + * Get whether to log of plists sent to and received from the Web Inspector + * in hex dump format. + * + * @return True or false. + */ + default Optional doesSafariLogAllCommunicationHexDump() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariLogAllCommunicationOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariLogAllCommunicationOption.java new file mode 100644 index 000000000..e351cbb62 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariLogAllCommunicationOption.java @@ -0,0 +1,60 @@ +/* + * 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.webview; + +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 SupportsSafariLogAllCommunicationOption> extends + Capabilities, CanSetCapability { + String SAFARI_LOG_ALL_COMMUNICATION_OPTION = "safariLogAllCommunication"; + + /** + * Enforces logging of plists sent to and received from the Web Inspector. + * + * @return self instance for chaining. + */ + default T safariLogAllCommunication() { + return amend(SAFARI_LOG_ALL_COMMUNICATION_OPTION, true); + } + + /** + * Log all plists sent to and received from the Web Inspector, as plain text. + * For some operations this can be a lot of data, so it is recommended to + * be used only when necessary. Defaults to false. + * + * @param value Whether to log all internal web debugger communication. + * @return self instance for chaining. + */ + default T setSafariLogAllCommunication(boolean value) { + return amend(SAFARI_LOG_ALL_COMMUNICATION_OPTION, value); + } + + /** + * Get whether to log of plists sent to and received from the Web Inspector. + * + * @return True or false. + */ + default Optional doesSafariLogAllCommunication() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_LOG_ALL_COMMUNICATION_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariOpenLinksInBackgroundOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariOpenLinksInBackgroundOption.java new file mode 100644 index 000000000..28b79a962 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariOpenLinksInBackgroundOption.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.webview; + +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 SupportsSafariOpenLinksInBackgroundOption> extends + Capabilities, CanSetCapability { + String SAFARI_OPEN_LINKS_IN_BACKGROUND_OPTION = "safariOpenLinksInBackground"; + + /** + * Enforces Safari to open links in new windows. + * + * @return self instance for chaining. + */ + default T safariOpenLinksInBackground() { + return amend(SAFARI_OPEN_LINKS_IN_BACKGROUND_OPTION, true); + } + + /** + * Whether Safari should allow links to open in new windows. + * Default keeps current sim setting. + * + * @param value Whether Safari should allow links to open in new windows. + * @return self instance for chaining. + */ + default T setSafariOpenLinksInBackground(boolean value) { + return amend(SAFARI_OPEN_LINKS_IN_BACKGROUND_OPTION, value); + } + + /** + * Get whether Safari should allow links to open in new windows. + * + * @return True or false. + */ + default Optional doesSafariOpenLinksInBackground() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_OPEN_LINKS_IN_BACKGROUND_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariSocketChunkSizeOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariSocketChunkSizeOption.java new file mode 100644 index 000000000..24190efaa --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariSocketChunkSizeOption.java @@ -0,0 +1,52 @@ +/* + * 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.webview; + +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 SupportsSafariSocketChunkSizeOption> extends + Capabilities, CanSetCapability { + String SAFARI_SOCKET_CHUNK_SIZE_OPTION = "safariSocketChunkSize"; + + /** + * The size, in bytes, of the data to be sent to the Web Inspector on + * iOS 11+ real devices. Some devices hang when sending large amounts of + * data to the Web Inspector, and breaking them into smaller parts can be + * helpful in those cases. Defaults to 16384 (also the maximum possible). + * + * @param size Socket chunk size in range 1..16384. + * @return self instance for chaining. + */ + default T setSafariSocketChunkSize(int size) { + return amend(SAFARI_SOCKET_CHUNK_SIZE_OPTION, size); + } + + /** + * Get the size of a single remote debugger socket chunk. + * + * @return Chunk size in bytes. + */ + default Optional getSafariSocketChunkSize() { + return Optional.ofNullable(toInteger(getCapability(SAFARI_SOCKET_CHUNK_SIZE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariWebInspectorMaxFrameLengthOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariWebInspectorMaxFrameLengthOption.java new file mode 100644 index 000000000..b3b4c6f26 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsSafariWebInspectorMaxFrameLengthOption.java @@ -0,0 +1,52 @@ +/* + * 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.webview; + +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 SupportsSafariWebInspectorMaxFrameLengthOption> extends + Capabilities, CanSetCapability { + String SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH_OPTION = "safariWebInspectorMaxFrameLength"; + + /** + * The maximum size in bytes of a single data frame for the Web Inspector. + * Too high values could introduce slowness and/or memory leaks. + * Too low values could introduce possible buffer overflow exceptions. + * Defaults to 20MB (20*1024*1024). + * + * @param length Max size of a single data frame. + * @return self instance for chaining. + */ + default T setSafariWebInspectorMaxFrameLength(int length) { + return amend(SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH_OPTION, length); + } + + /** + * Get the maximum size in bytes of a single data frame for the Web Inspector. + * + * @return Size in bytes. + */ + default Optional getSafariWebInspectorMaxFrameLength() { + return Optional.ofNullable(toInteger(getCapability(SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebkitResponseTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebkitResponseTimeoutOption.java new file mode 100644 index 000000000..dc9378399 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebkitResponseTimeoutOption.java @@ -0,0 +1,52 @@ +/* + * 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.webview; + +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 SupportsWebkitResponseTimeoutOption> extends + Capabilities, CanSetCapability { + String WEBKIT_RESPONSE_TIMEOUT_OPTION = "webkitResponseTimeout"; + + /** + * (Real device only) Set the time to wait for a response from WebKit in + * a Safari session. Defaults to 5000ms. + * + * @param timeout Timeout to wait for a response from WebKit in a Safari session. + * @return self instance for chaining. + */ + default T setWebviewConnectTimeout(Duration timeout) { + return amend(WEBKIT_RESPONSE_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout to wait for a response from WebKit in a Safari session. + * + * @return Timeout value. + */ + default Optional getWebviewConnectTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(WEBKIT_RESPONSE_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebviewConnectRetriesOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebviewConnectRetriesOption.java new file mode 100644 index 000000000..a1b658945 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebviewConnectRetriesOption.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.ios.options.webview; + +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 SupportsWebviewConnectRetriesOption> extends + Capabilities, CanSetCapability { + String WEBVIEW_CONNECT_RETRIES_OPTION = "webviewConnectRetries"; + + /** + * Number of times to send connection message to remote debugger, + * to get a webview. Default: 8. + * + * @param retries Max retries count. + * @return self instance for chaining. + */ + default T setWebviewConnectRetries(int retries) { + return amend(WEBVIEW_CONNECT_RETRIES_OPTION, retries); + } + + /** + * Get the number of retries to send connection message to remote debugger, + * to get a webview. + * + * @return Max retries count. + */ + default Optional getWebviewConnectRetries() { + return Optional.ofNullable(toInteger(getCapability(WEBVIEW_CONNECT_RETRIES_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebviewConnectTimeoutOption.java b/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebviewConnectTimeoutOption.java new file mode 100644 index 000000000..cd00db23f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/webview/SupportsWebviewConnectTimeoutOption.java @@ -0,0 +1,52 @@ +/* + * 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.webview; + +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 SupportsWebviewConnectTimeoutOption> extends + Capabilities, CanSetCapability { + String WEBVIEW_CONNECT_TIMEOUT_OPTION = "webviewConnectTimeout"; + + /** + * The time to wait for the initial presence of webviews in + * MobileSafari or hybrid apps. Defaults to 0ms. + * + * @param timeout Timeout to wait for the initial presence of webviews. + * @return self instance for chaining. + */ + default T setWebviewConnectTimeout(Duration timeout) { + return amend(WEBVIEW_CONNECT_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout to wait for the initial presence of webviews. + * + * @return Timeout value. + */ + default Optional getWebviewConnectTimeout() { + return Optional.ofNullable( + CapabilityHelpers.toDuration(getCapability(WEBVIEW_CONNECT_TIMEOUT_OPTION)) + ); + } +} 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 new file mode 100644 index 000000000..46911c314 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/Mac2Driver.java @@ -0,0 +1,154 @@ +/* + * 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.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; +import io.appium.java_client.screenrecording.CanRecordScreen; +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.Platform; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.HttpClient; + +import java.net.URL; + +/** + * Mac2Driver is an officially supported Appium driver + * created to automate Mac OS apps. The driver uses W3C + * WebDriver protocol and is built on top of Apple's XCTest + * automation framework. Read https://github.com/appium/appium-mac2-driver + * for more details on how to configure and use it. + * + * @since Appium 1.20.0 + */ +public class Mac2Driver extends AppiumDriver implements + PerformsTouchActions, + CanRecordScreen { + private static final String PLATFORM_NAME = Platform.MAC.name(); + private static final String AUTOMATION_NAME = AutomationName.MAC2; + + public Mac2Driver(HttpCommandExecutor executor, Capabilities capabilities) { + super(executor, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public Mac2Driver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public Mac2Driver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public Mac2Driver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public Mac2Driver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public Mac2Driver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public Mac2Driver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(builder, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public Mac2Driver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, ensurePlatformAndAutomationNames( + 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}. + * For example: + * + *
+     *
+     * ClientConfig clientConfig = ClientConfig.defaultConfig()
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * Mac2Options options = new Mac2Options();
+     * Mac2Driver driver = new Mac2Driver(clientConfig, options);
+     *
+     * 
+ * + * @param clientConfig take a look at {@link ClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public Mac2Driver(ClientConfig clientConfig, Capabilities capabilities) { + 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)); + } + + public Mac2Driver(Capabilities capabilities) { + super(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 new file mode 100644 index 000000000..342598b62 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/Mac2StartScreenRecordingOptions.java @@ -0,0 +1,152 @@ +/* + * 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.mac; + +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; + +public class Mac2StartScreenRecordingOptions + extends BaseStartScreenRecordingOptions { + private Integer fps; + private String videoFilter; + private String preset; + private Boolean captureCursor; + private Boolean captureClicks; + private Integer deviceId; + + public static Mac2StartScreenRecordingOptions startScreenRecordingOptions() { + return new Mac2StartScreenRecordingOptions(); + } + + /** + * The count of frames per second in the resulting video. + * Increasing fps value also increases the size of the resulting + * video file and the CPU usage. + * + * @param fps The actual frames per second value. + * The default value is 15. + * @return self instance for chaining. + */ + public Mac2StartScreenRecordingOptions withFps(int fps) { + this.fps = fps; + return this; + } + + /** + * Whether to capture the mouse cursor while recording + * the screen. Disabled by default. + * + * @return self instance for chaining. + */ + public Mac2StartScreenRecordingOptions enableCursorCapture() { + this.captureCursor = true; + return this; + } + + /** + * Whether to capture the click gestures while recording + * the screen. Disabled by default. + * + * @return self instance for chaining. + */ + public Mac2StartScreenRecordingOptions enableClicksCapture() { + this.captureClicks = true; + return this; + } + + /** + * Screen device index to use for the recording. + * The list of available devices could be retrieved using + * `ffmpeg -f avfoundation -list_devices true -i` command. + * This option is mandatory and must be always provided. + * + * @param deviceId The valid screen device identifier. + * @return self instance for chaining. + */ + public Mac2StartScreenRecordingOptions withDeviceId(Integer deviceId) { + this.deviceId = deviceId; + return this; + } + + /** + * The video filter spec to apply for ffmpeg. + * See https://trac.ffmpeg.org/wiki/FilteringGuide for more details on the possible values. + * Example: Set it to `scale=ifnot(gte(iw\,1024)\,iw\,1024):-2` in order to limit the video width + * to 1024px. The height will be adjusted automatically to match the actual screen aspect ratio. + * + * @param videoFilter Valid ffmpeg video filter spec string. + * @return self instance for chaining. + */ + public Mac2StartScreenRecordingOptions withVideoFilter(String videoFilter) { + this.videoFilter = videoFilter; + return this; + } + + /** + * A preset is a collection of options that will provide a certain encoding speed to compression ratio. + * A slower preset will provide better compression (compression is quality per filesize). + * This means that, for example, if you target a certain file size or constant bit rate, you will + * achieve better quality with a slower preset. Read https://trac.ffmpeg.org/wiki/Encode/H.264 + * for more details. + * + * @param preset One of the supported encoding presets. Possible values are: + * - ultrafast + * - superfast + * - veryfast (default) + * - faster + * - fast + * - medium + * - slow + * - slower + * - veryslow + * @return self instance for chaining. + */ + public Mac2StartScreenRecordingOptions withPreset(String preset) { + this.preset = preset; + return this; + } + + /** + * The maximum recording time. The default value is 600 seconds (10 minutes). + * The minimum time resolution unit is one second. + * + * @param timeLimit The actual time limit of the recorded video. + * @return self instance for chaining. + */ + @Override + public Mac2StartScreenRecordingOptions withTimeLimit(Duration timeLimit) { + return super.withTimeLimit(timeLimit); + } + + @Override + public Map 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/Mac2StopScreenRecordingOptions.java b/src/main/java/io/appium/java_client/mac/Mac2StopScreenRecordingOptions.java new file mode 100644 index 000000000..8984460be --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/Mac2StopScreenRecordingOptions.java @@ -0,0 +1,28 @@ +/* + * 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.mac; + +import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; + +public class Mac2StopScreenRecordingOptions extends + BaseStopScreenRecordingOptions { + + public static Mac2StopScreenRecordingOptions stopScreenRecordingOptions() { + return new Mac2StopScreenRecordingOptions(); + } + +} diff --git a/src/main/java/io/appium/java_client/mac/options/AppleScriptData.java b/src/main/java/io/appium/java_client/mac/options/AppleScriptData.java new file mode 100644 index 000000000..91b74aa98 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/AppleScriptData.java @@ -0,0 +1,73 @@ +/* + * 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.mac.options; + +import io.appium.java_client.remote.options.SystemScript; + +import java.util.Map; +import java.util.Optional; + +public class AppleScriptData extends SystemScript { + public AppleScriptData() { + } + + public AppleScriptData(Map options) { + super(options); + } + + /** + * Allows to provide a multiline AppleScript. + * + * @param script A valid AppleScript. + * @return self instance for chaining. + */ + @Override + public AppleScriptData withScript(String script) { + return super.withScript(script); + } + + /** + * Get a multiline AppleScript. + * + * @return AppleScript snippet. + */ + @Override + public Optional getScript() { + return super.getScript(); + } + + /** + * Allows to provide a single-line AppleScript. + * + * @param command A valid AppleScript. + * @return self instance for chaining. + */ + @Override + public AppleScriptData withCommand(String command) { + return super.withCommand(command); + } + + /** + * Get a single-line AppleScript. + * + * @return AppleScript snippet. + */ + @Override + public Optional getCommand() { + return super.getCommand(); + } +} 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 new file mode 100644 index 000000000..230c04c90 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/Mac2Options.java @@ -0,0 +1,116 @@ +/* + * 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.mac.options; + +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsPostrunOption; +import io.appium.java_client.remote.options.SupportsPrerunOption; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +/** + * Provides options specific to the Appium Mac2 Driver. + * + *

For more details, refer to the + * capabilities documentation

+ */ +public class Mac2Options extends BaseOptions implements + SupportsSystemPortOption, + SupportsSystemHostOption, + SupportsWebDriverAgentMacUrlOption, + SupportsBootstrapRootOption, + SupportsBundleIdOption, + SupportsArgumentsOption, + SupportsEnvironmentOption, + SupportsServerStartupTimeoutOption, + SupportsSkipAppKillOption, + SupportsShowServerLogsOption, + SupportsPrerunOption, + SupportsPostrunOption { + public Mac2Options() { + setCommonOptions(); + } + + public Mac2Options(Capabilities source) { + super(source); + setCommonOptions(); + } + + public Mac2Options(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setPlatformName(MobilePlatform.MAC); + setAutomationName(AutomationName.MAC2); + } + + /** + * An object containing either script or command key. The value of + * each key must be a valid AppleScript script or command to be + * executed after before Mac2Driver session is started. See + * https://github.com/appium/appium-mac2-driver#applescript-commands-execution + * for more details. + * + * @param script A valid AppleScript snippet. + * @return self instance for chaining. + */ + public Mac2Options setPrerun(AppleScriptData script) { + return amend(PRERUN_OPTION, script.toMap()); + } + + /** + * Get the prerun script. + * + * @return Prerun script. + */ + public Optional getPrerun() { + //noinspection unchecked + return Optional.ofNullable(getCapability(PRERUN_OPTION)) + .map(v -> new AppleScriptData((Map) v)); + } + + /** + * An object containing either script or command key. The value of + * each key must be a valid AppleScript script or command to be + * executed after Mac2Driver session is stopped. See + * https://github.com/appium/appium-mac2-driver#applescript-commands-execution + * for more details. + * + * @param script A valid AppleScript snippet. + * @return self instance for chaining. + */ + public Mac2Options setPostrun(AppleScriptData script) { + return amend(POSTRUN_OPTION, script.toMap()); + } + + /** + * Get the postrun script. + * + * @return Postrun script. + */ + public Optional getPostrun() { + //noinspection unchecked + return Optional.ofNullable(getCapability(POSTRUN_OPTION)) + .map(v -> new AppleScriptData((Map) v)); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsArgumentsOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsArgumentsOption.java new file mode 100644 index 000000000..8d8c427f5 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsArgumentsOption.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.mac.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.List; +import java.util.Optional; + +public interface SupportsArgumentsOption> extends + Capabilities, CanSetCapability { + String ARGUMENTS_OPTION = "arguments"; + + /** + * Array of application command line arguments. This capability is + * only going to be applied if the application is not running on session startup. + * + * @param arguments E.g. ["--help"]. + * @return self instance for chaining. + */ + default T setArguments(List arguments) { + return amend(ARGUMENTS_OPTION, arguments); + } + + /** + * Get the array of application command line arguments. + * + * @return Application arguments. + */ + default Optional> getArguments() { + //noinspection unchecked + return Optional.ofNullable((List) getCapability(ARGUMENTS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsBootstrapRootOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsBootstrapRootOption.java new file mode 100644 index 000000000..cb3245148 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsBootstrapRootOption.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.mac.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 SupportsBootstrapRootOption> extends + Capabilities, CanSetCapability { + String BOOTSTRAP_ROOT_OPTION = "bootstrapRoot"; + + /** + * The full path to WebDriverAgentMac root folder where Xcode project + * of the server sources lives. By default, this project is located in + * the same folder where the corresponding driver Node.js module lives. + * + * @param path The full path to WebDriverAgentMac root folder. + * @return self instance for chaining. + */ + default T setBootstrapRoot(String path) { + return amend(BOOTSTRAP_ROOT_OPTION, path); + } + + /** + * Get the full path to WebDriverAgentMac root folder where Xcode project + * of the server sources lives. + * + * @return The full path to WebDriverAgentMac root folder. + */ + default Optional getBootstrapRoot() { + return Optional.ofNullable((String) getCapability(BOOTSTRAP_ROOT_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsBundleIdOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsBundleIdOption.java new file mode 100644 index 000000000..d19420c8f --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsBundleIdOption.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.mac.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 SupportsBundleIdOption> extends + Capabilities, CanSetCapability { + String BUNDLE_ID_OPTION = "bundleId"; + + /** + * The bundle identifier of the application to automate, for example + * com.apple.TextEdit. This is an optional capability. If it is not provided + * then the session will be started without an application under test + * (actually, it will be Finder). If the application with the given + * identifier is not installed then an error will be thrown on session + * startup. If the application is already running then it will be moved to + * the foreground. + * + * @param identifier A valid application bundle identifier. + * @return self instance for chaining. + */ + default T setBundleId(String identifier) { + return amend(BUNDLE_ID_OPTION, identifier); + } + + /** + * Get the bundle identifier of the application to automate. + * + * @return Application bundle identifier. + */ + default Optional getBundleId() { + return Optional.ofNullable((String) getCapability(BUNDLE_ID_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsEnvironmentOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsEnvironmentOption.java new file mode 100644 index 000000000..803561030 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsEnvironmentOption.java @@ -0,0 +1,52 @@ +/* + * 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.mac.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.Map; +import java.util.Optional; + +public interface SupportsEnvironmentOption> extends + Capabilities, CanSetCapability { + String ENVIRONMENT_OPTION = "environment"; + + /** + * A dictionary of environment variables (name->value) that are going to be passed + * to the application under test on top of environment variables inherited from + * the parent process. This capability is only going to be applied if the application + * is not running on session startup. + * + * @param env E.g. ["--help"]. + * @return self instance for chaining. + */ + default T setEnvironment(Map env) { + return amend(ENVIRONMENT_OPTION, env); + } + + /** + * Get the application environment variables mapping. + * + * @return Application environment mapping. + */ + default Optional> getEnvironment() { + //noinspection unchecked + return Optional.ofNullable((Map) getCapability(ENVIRONMENT_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsServerStartupTimeoutOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsServerStartupTimeoutOption.java new file mode 100644 index 000000000..97d052928 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsServerStartupTimeoutOption.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.mac.options; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsServerStartupTimeoutOption> extends + Capabilities, CanSetCapability { + String SERVER_STARTUP_TIMEOUT_OPTION = "serverStartupTimeout"; + + /** + * The timeout to wait util the WebDriverAgentMac + * project is built and started. 120000ms by default + * + * @param timeout The timeout value. + * @return self instance for chaining. + */ + default T setServerStartupTimeout(Duration timeout) { + return amend(SERVER_STARTUP_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout to wait util the WebDriverAgentMac + * project is built and started. + * + * @return The timeout value. + */ + default Optional getServerStartupTimeout() { + return Optional.ofNullable( + toDuration(getCapability(SERVER_STARTUP_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsShowServerLogsOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsShowServerLogsOption.java new file mode 100644 index 000000000..f91a32d97 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsShowServerLogsOption.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.mac.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 SupportsShowServerLogsOption> extends + Capabilities, CanSetCapability { + String SHOW_SERVER_LOGS_OPTION = "showServerLogs"; + + /** + * Enforce showing of WDA server logs in the Appium log.. + * + * @return self instance for chaining. + */ + default T showServerLogs() { + return amend(SHOW_SERVER_LOGS_OPTION, true); + } + + /** + * Set it to true in order to include xcodebuild output to the Appium + * server log. false by default. + * + * @param value Whether to show WDA server logs in the Appium log. + * @return self instance for chaining. + */ + default T setShowServerLogs(boolean value) { + return amend(SHOW_SERVER_LOGS_OPTION, value); + } + + /** + * Get whether to show WDA server logs in the Appium log. + * + * @return True or false. + */ + default Optional doesShowServerLogs() { + return Optional.ofNullable(toSafeBoolean(getCapability(SHOW_SERVER_LOGS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsSkipAppKillOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsSkipAppKillOption.java new file mode 100644 index 000000000..5c166b80a --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsSkipAppKillOption.java @@ -0,0 +1,61 @@ +/* + * 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.mac.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 SupportsSkipAppKillOption> extends + Capabilities, CanSetCapability { + String SKIP_APP_KILL_OPTION = "skipAppKill"; + + /** + * Enforces skipping the termination of the application under test + * when the testing session quits. + * + * @return self instance for chaining. + */ + default T skipAppKill() { + return amend(SKIP_APP_KILL_OPTION, true); + } + + /** + * Set whether to skip the termination of the application under test + * when the testing session quits. false by default. This capability + * is only going to be applied if bundleId is set. + * + * @param value True to skip the termination of the application under test. + * @return self instance for chaining. + */ + default T setSkipAppKill(boolean value) { + return amend(SKIP_APP_KILL_OPTION, value); + } + + /** + * Get whether to skip the termination of the application under test. + * + * @return True or false. + */ + default Optional doesSkipAppKill() { + return Optional.ofNullable(toSafeBoolean(getCapability(SKIP_APP_KILL_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsSystemHostOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsSystemHostOption.java new file mode 100644 index 000000000..8f6a92cc1 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsSystemHostOption.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.mac.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 SupportsSystemHostOption> extends + Capabilities, CanSetCapability { + String SYSTEM_HOST_OPTION = "systemHost"; + + /** + * The name of the host for the internal server to listen on. + * If not provided then Mac2Driver will use the default host + * address 127.0.0.1. You could set it to 0.0.0.0 to make the + * server listening on all available network interfaces. + * It is also possible to set the particular interface name, for example en1. + * + * @param host Host name. + * @return self instance for chaining. + */ + default T setSystemHost(String host) { + return amend(SYSTEM_HOST_OPTION, host); + } + + /** + * Get the name of the host for the internal server to listen on. + * + * @return System host value. + */ + default Optional getSystemHost() { + return Optional.ofNullable((String) getCapability(SYSTEM_HOST_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsSystemPortOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsSystemPortOption.java new file mode 100644 index 000000000..bf648a7bb --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsSystemPortOption.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.mac.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 SupportsSystemPortOption> extends + Capabilities, CanSetCapability { + String SYSTEM_PORT_OPTION = "systemPort"; + + /** + * The number of the port for the internal server to listen on. + * If not provided then Mac2Driver will use the default port 10100. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setSystemPort(int port) { + return amend(SYSTEM_PORT_OPTION, port); + } + + /** + * Get the number of the port for the internal server to listen on. + * + * @return System port value. + */ + default Optional getSystemPort() { + return Optional.ofNullable(toInteger(getCapability(SYSTEM_PORT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/mac/options/SupportsWebDriverAgentMacUrlOption.java b/src/main/java/io/appium/java_client/mac/options/SupportsWebDriverAgentMacUrlOption.java new file mode 100644 index 000000000..9f968c9c0 --- /dev/null +++ b/src/main/java/io/appium/java_client/mac/options/SupportsWebDriverAgentMacUrlOption.java @@ -0,0 +1,63 @@ +/* + * 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.mac.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.net.URL; +import java.util.Optional; + +public interface SupportsWebDriverAgentMacUrlOption> extends + Capabilities, CanSetCapability { + String WEB_DRIVER_AGENT_MAC_URL_OPTION = "webDriverAgentMacUrl"; + + /** + * Set the URL Appium will connect to an existing WebDriverAgentMac + * instance at this URL instead of starting a new one. + * + * @param url E.g. "http://192.168.10.1:10101" + * @return self instance for chaining. + */ + default T setWebDriverAgentMacUrl(URL url) { + return amend(WEB_DRIVER_AGENT_MAC_URL_OPTION, url.toString()); + } + + /** + * Set the URL Appium will connect to an existing WebDriverAgentMac + * instance at this URL instead of starting a new one. + * + * @param url E.g. "http://192.168.10.1:10101" + * @return self instance for chaining. + */ + default T setWebDriverAgentMacUrl(String url) { + return amend(WEB_DRIVER_AGENT_MAC_URL_OPTION, url); + } + + /** + * Get the URL Appium will connect to an existing WebDriverAgentMac + * instance. + * + * @return Server URL. + */ + default Optional getWebDriverAgentMacUrl() { + return Optional.ofNullable(getCapability(WEB_DRIVER_AGENT_MAC_URL_OPTION)) + .map(CapabilityHelpers::toUrl); + } +} 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 1d2b9caa0..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,22 +16,11 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.internal.ElementMap.getElementClass; -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.MobileElement; -import io.appium.java_client.android.AndroidElement; import io.appium.java_client.internal.CapabilityHelpers; -import io.appium.java_client.ios.IOSElement; import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.locator.CacheableLocator; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.windows.WindowsElement; -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; @@ -40,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; @@ -54,24 +45,30 @@ 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 * {@literal @AndroidFindBy}, {@literal @AndroidFindBys}, or * {@literal @iOSFindBy/@iOSFindBys} annotation with a proxy that locates the * elements using the passed in ElementLocatorFactory. - * Please pay attention: fields of {@link WebElement}, {@link RemoteWebElement}, - * {@link MobileElement}, {@link AndroidElement} and {@link IOSElement} are allowed + * Please pay attention: fields of {@link WebElement} or {@link RemoteWebElement} * to use with this decorator */ public class AppiumFieldDecorator implements FieldDecorator { - private static final List> availableElementClasses = ImmutableList.of(WebElement.class, - RemoteWebElement.class, MobileElement.class, AndroidElement.class, - IOSElement.class, WindowsElement.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; @@ -86,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); } @@ -129,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); } /** @@ -151,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)) { @@ -185,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); - return getEnhancedProxy(getElementClass(platform, automation), elementInterceptor); + 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 40a990898..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,14 +16,9 @@ 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; -import io.appium.java_client.pagefactory.bys.builder.ByAll; import io.appium.java_client.pagefactory.bys.builder.ByChained; import io.appium.java_client.pagefactory.bys.builder.HowToUseSelectors; import org.openqa.selenium.By; @@ -32,6 +27,7 @@ import org.openqa.selenium.support.FindAll; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindBys; +import org.openqa.selenium.support.pagefactory.ByAll; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; @@ -46,11 +42,15 @@ 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"; private static final String VALUE = "value"; - private static final Class[] ANNOTATION_ARGUMENTS = new Class[]{}; + private static final Class[] ANNOTATION_ARGUMENTS = new Class[]{}; private static final Object[] ANNOTATION_PARAMETERS = new Object[]{}; public DefaultElementByBuilder(String platform, String automation) { @@ -155,7 +155,7 @@ private By[] getBys(Class singleLocator, Class 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 08c58627c..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 io.appium.java_client.MobileElement}. + * 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/ContentType.java b/src/main/java/io/appium/java_client/pagefactory/bys/ContentType.java index c22160b4a..f5a17a219 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/ContentType.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/ContentType.java @@ -17,6 +17,5 @@ package io.appium.java_client.pagefactory.bys; public enum ContentType { - HTML_OR_DEFAULT, - NATIVE_MOBILE_SPECIFIC; + HTML_OR_DEFAULT, NATIVE_MOBILE_SPECIFIC } 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 827a8ecbe..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,43 +16,48 @@ 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.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 java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.List; +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[] {}; - - private static final List METHODS_TO_BE_EXCLUDED_WHEN_ANNOTATION_IS_READ = - new ArrayList() { - private static final long serialVersionUID = 1L; { - List objectClassMethodNames = - getMethodNames(Object.class.getDeclaredMethods()); - addAll(objectClassMethodNames); - List annotationClassMethodNames = - getMethodNames(Annotation.class.getDeclaredMethods()); - annotationClassMethodNames.removeAll(objectClassMethodNames); - addAll(annotationClassMethodNames); - } - }; + protected static final Class[] DEFAULT_ANNOTATION_METHOD_ARGUMENTS = new Class[]{}; + + private static final List METHODS_TO_BE_EXCLUDED_WHEN_ANNOTATION_IS_READ = new ArrayList() { + private static final long serialVersionUID = 1L; + + { + Stream.of(Object.class, Annotation.class, Proxy.class) + .map(Class::getDeclaredMethods) + .map(AppiumByBuilder::getMethodNames) + .flatMap(List::stream) + .forEach(this::add); + } + }; protected final AnnotatedElementContainer annotatedElementContainer; protected final String platform; protected final String automation; @@ -64,79 +69,68 @@ protected AppiumByBuilder(String platform, String automation) { } private static List getMethodNames(Method[] methods) { - List names = new ArrayList<>(); - for (Method m : methods) { - names.add(m.getName()); - } - return names; + return Stream.of(methods).map(Method::getName).collect(Collectors.toList()); } private static Method[] prepareAnnotationMethods(Class annotation) { - List targeAnnotationMethodNamesList = - getMethodNames(annotation.getDeclaredMethods()); - targeAnnotationMethodNamesList.removeAll(METHODS_TO_BE_EXCLUDED_WHEN_ANNOTATION_IS_READ); - Method[] result = new Method[targeAnnotationMethodNamesList.size()]; - for (String methodName : targeAnnotationMethodNamesList) { - try { - result[targeAnnotationMethodNamesList.indexOf(methodName)] = - annotation.getMethod(methodName, DEFAULT_ANNOTATION_METHOD_ARGUMENTS); - } catch (NoSuchMethodException | SecurityException e) { - throw new RuntimeException(e); - } - } - return result; + List targetAnnotationMethodNamesList = getMethodNames(annotation.getDeclaredMethods()); + targetAnnotationMethodNamesList.removeAll(METHODS_TO_BE_EXCLUDED_WHEN_ANNOTATION_IS_READ); + return targetAnnotationMethodNamesList.stream() + .map(methodName -> { + try { + return annotation.getMethod(methodName, DEFAULT_ANNOTATION_METHOD_ARGUMENTS); + } catch (NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + }).toArray(Method[]::new); } private static String getFilledValue(Annotation mobileBy) { - Method[] values = prepareAnnotationMethods(mobileBy.getClass()); - for (Method value : values) { - if (!String.class.equals(value.getReturnType())) { - continue; - } - - try { - String strategyParameter = value.invoke(mobileBy).toString(); - if (!strategyParameter.isEmpty()) { - return value.getName(); - } - } catch (IllegalAccessException - | IllegalArgumentException - | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - throw new IllegalArgumentException( - "@" + mobileBy.getClass().getSimpleName() + ": one of " + Strategies.strategiesNames() - .toString() + " should be filled"); + return Stream.of(prepareAnnotationMethods(mobileBy.getClass())) + .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) { + throw new RuntimeException(e); + } + }) + .findFirst() + .map(Method::getName) + .orElseThrow(() -> new IllegalArgumentException( + String.format("@%s: one of %s should be filled", + mobileBy.getClass().getSimpleName(), Strategies.strategiesNames()) + )); } private static By getMobileBy(Annotation annotation, String valueName) { - Strategies[] strategies = Strategies.values(); - for (Strategies strategy : strategies) { - if (strategy.returnValueName().equals(valueName)) { - return strategy.getBy(annotation); - } - } - throw new IllegalArgumentException( - "@" + annotation.getClass().getSimpleName() + ": There is an unknown strategy " - + valueName); + return Stream.of(Strategies.values()) + .filter(strategy -> strategy.returnValueName().equals(valueName)) + .findFirst() + .map(strategy -> strategy.getBy(annotation)) + .orElseThrow(() -> new IllegalArgumentException( + String.format("@%s: There is an unknown strategy %s", + annotation.getClass().getSimpleName(), valueName) + )); } - private static T getComplexMobileBy(Annotation[] annotations, - Class requiredByClass) { - By[] byArray = new By[annotations.length]; - for (int i = 0; i < annotations.length; i++) { - byArray[i] = getMobileBy(annotations[i], getFilledValue(annotations[i])); - } + private static T getComplexMobileBy(Annotation[] annotations, Class requiredByClass) { + By[] byArray = Stream.of(annotations) + .map(annotation -> getMobileBy(annotation, getFilledValue(annotation))) + .toArray(By[]::new); try { Constructor c = requiredByClass.getConstructor(By[].class); - Object[] values = new Object[] {byArray}; + Object[] values = new Object[]{byArray}; return c.newInstance(values); - } catch (Exception e) { + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException + | IllegalAccessException e) { throw new RuntimeException(e); } } + @Nullable protected static By createBy(Annotation[] annotations, HowToUseSelectors howToUseLocators) { if (annotations == null || annotations.length == 0) { return null; @@ -178,6 +172,10 @@ protected boolean isIOS() { return IOS.equalsIgnoreCase(platform); } + protected boolean isTvOS() { + return TVOS.equalsIgnoreCase(platform); + } + protected boolean isIOSXcuit() { return isIOS() && IOS_XCUI_TEST.equalsIgnoreCase(automation); } 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 7332fe389..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByAll.java +++ /dev/null @@ -1,54 +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; - - -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 0ea7dde77..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,14 +24,20 @@ 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) { + return null; + } return input.findElement(by); } catch (NoSuchElementException e) { return null; @@ -48,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"); } @@ -58,20 +60,18 @@ 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 " + toString()); + throw new NoSuchElementException("Cannot locate an element using " + this); } } } diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/HowToUseSelectors.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/HowToUseSelectors.java index 8fce3c467..a4d4f4fdb 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/HowToUseSelectors.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/HowToUseSelectors.java @@ -17,7 +17,5 @@ package io.appium.java_client.pagefactory.bys.builder; public enum HowToUseSelectors { - USE_ONE, - BUILD_CHAINED, - USE_ANY; + USE_ONE, BUILD_CHAINED, USE_ANY } 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 718cd403c..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,17 +16,17 @@ package io.appium.java_client.pagefactory.bys.builder; -import io.appium.java_client.MobileBy; +import io.appium.java_client.AppiumBy; import io.appium.java_client.pagefactory.AndroidBy; import io.appium.java_client.pagefactory.AndroidFindBy; -import io.appium.java_client.pagefactory.iOSBy; import org.openqa.selenium.By; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; enum Strategies { BYUIAUTOMATOR("uiAutomator") { @@ -34,36 +34,34 @@ enum Strategies { String value = getValue(annotation, this); if (annotation.annotationType().equals(AndroidFindBy.class) || annotation.annotationType().equals(AndroidBy.class)) { - return MobileBy.AndroidUIAutomator(value); + return AppiumBy.androidUIAutomator(value); } return super.getBy(annotation); } }, - BYACCESSABILITY("accessibility") { + BYACCESSIBILITY("accessibility") { @Override By getBy(Annotation annotation) { - return MobileBy.AccessibilityId(getValue(annotation, this)); + return AppiumBy.accessibilityId(getValue(annotation, this)); } }, BYCLASSNAME("className") { @Override By getBy(Annotation annotation) { - return By.className(getValue(annotation, this)); + return AppiumBy.className(getValue(annotation, this)); } }, BYID("id") { @Override By getBy(Annotation annotation) { - return By.id(getValue(annotation, this)); + return AppiumBy.id(getValue(annotation, this)); } }, BYTAG("tagName") { @Override By getBy(Annotation annotation) { - return By - .tagName(getValue(annotation, this)); + return By.tagName(getValue(annotation, this)); } }, BYNAME("name") { @Override By getBy(Annotation annotation) { - return By - .name(getValue(annotation, this)); + return AppiumBy.name(getValue(annotation, this)); } }, BYXPATH("xpath") { @@ -84,33 +82,27 @@ enum Strategies { .partialLinkText(getValue(annotation, this)); } }, - BYWINDOWSAUTOMATION("windowsAutomation") { - @Override By getBy(Annotation annotation) { - return MobileBy - .windowsAutomation(getValue(annotation, this)); - } - }, BY_CLASS_CHAIN("iOSClassChain") { @Override By getBy(Annotation annotation) { - return MobileBy + return AppiumBy .iOSClassChain(getValue(annotation, this)); } }, BY_DATA_MATCHER("androidDataMatcher") { @Override By getBy(Annotation annotation) { - return MobileBy + return AppiumBy .androidDataMatcher(getValue(annotation, this)); } }, BY_VIEW_MATCHER("androidViewMatcher") { @Override By getBy(Annotation annotation) { - return MobileBy + return AppiumBy .androidViewMatcher(getValue(annotation, this)); } }, BY_NS_PREDICATE("iOSNsPredicate") { @Override By getBy(Annotation annotation) { - return MobileBy + return AppiumBy .iOSNsPredicateString(getValue(annotation, this)); } }; @@ -122,12 +114,7 @@ enum Strategies { } static List strategiesNames() { - Strategies[] strategies = values(); - List result = new ArrayList<>(); - for (Strategies strategy : strategies) { - result.add(strategy.valueName); - } - return result; + 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 b15ee6775..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.HasSessionDetails; +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 and MobileElement implement WrapsDriver + // 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,32 +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 HasSessionDetails} + * 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 (HasSessionDetails.class.isAssignableFrom(driver.getClass())) { - HasSessionDetails hasSessionDetails = (HasSessionDetails) driver; - - if (!hasSessionDetails.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/events/api/mobile/ContextEventListener.java b/src/main/java/io/appium/java_client/proxy/HasMethodCallListeners.java similarity index 51% rename from src/main/java/io/appium/java_client/events/api/mobile/ContextEventListener.java rename to src/main/java/io/appium/java_client/proxy/HasMethodCallListeners.java index f3282df76..b5807f71b 100644 --- a/src/main/java/io/appium/java_client/events/api/mobile/ContextEventListener.java +++ b/src/main/java/io/appium/java_client/proxy/HasMethodCallListeners.java @@ -14,26 +14,22 @@ * limitations under the License. */ -package io.appium.java_client.events.api.mobile; - -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; - -public interface ContextEventListener extends Listener { +package io.appium.java_client.proxy; +public interface HasMethodCallListeners { /** - * Called before {@link org.openqa.selenium.ContextAware#context(String)}. + * The setter is dynamically created by ByteBuddy to store + * method call listeners on the instrumented proxy instance. * - * @param driver Webdriver - * @param context The context that is needed to switch to. + * @param methodCallListeners Array of method call listeners assigned to the proxy instance. */ - void beforeSwitchingToContext(WebDriver driver, String context); + void setMethodCallListeners(MethodCallListener[] methodCallListeners); /** - * Called after {@link org.openqa.selenium.ContextAware#context(String)}. + * The getter is dynamically created by ByteBuddy to access + * method call listeners on the instrumented proxy instance. * - * @param driver Webdriver - * @param context The context that is needed to switch to. + * @return Array of method call listeners assigned the proxy instance. */ - void afterSwitchingToContext(WebDriver driver, String context); + 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 ea0ade5f0..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,27 +16,17 @@ 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.lang.String.format; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Optional.ofNullable; -import static java.util.logging.Logger.getLogger; -import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION; - -import com.google.common.base.Supplier; import com.google.common.base.Throwables; - -import com.google.common.io.CountingOutputStream; -import com.google.common.io.FileBackedOutputStream; - -import io.appium.java_client.internal.Config; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.ImmutableCapabilities; +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.internal.ReflectionHelpers; +import lombok.Getter; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.Command; import org.openqa.selenium.remote.CommandCodec; +import org.openqa.selenium.remote.CommandExecutor; import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.Dialect; import org.openqa.selenium.remote.DriverCommand; @@ -44,190 +34,189 @@ import org.openqa.selenium.remote.ProtocolHandshake; 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.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.http.W3CHttpCommandCodec; import org.openqa.selenium.remote.service.DriverService; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.net.ConnectException; +import java.net.MalformedURLException; import java.net.URL; 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 final Optional serviceOptional; - - private AppiumCommandExecutor(Map additionalCommands, DriverService service, - URL addressOfRemoteServer, - HttpClient.Factory httpClientFactory) { + @Getter + private final AppiumClientConfig appiumClientConfig; + + /** + * 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(service) - .map(DriverService::getUrl) - .orElse(addressOfRemoteServer), httpClientFactory); + 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); + @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); + public AppiumCommandExecutor(Map additionalCommands, URL addressOfRemoteServer, + @Nullable Factory httpClientFactory) { + this(additionalCommands, null, httpClientFactory, + AppiumClientConfig.defaultConfig().baseUrl(requireNonNull(addressOfRemoteServer))); } + 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, URL addressOfRemoteServer, + AppiumClientConfig appiumClientConfig) { + this(additionalCommands, null, HttpClient.Factory.createDefault(), + appiumClientConfig.baseUrl(requireNonNull(addressOfRemoteServer))); + } + + public AppiumCommandExecutor(Map additionalCommands, DriverService service) { + this(additionalCommands, service, HttpClient.Factory.createDefault(), + AppiumClientConfig.defaultConfig().baseUrl(service.getUrl())); } public AppiumCommandExecutor(Map additionalCommands, - DriverService service) { - this(additionalCommands, service, HttpClient.Factory.createDefault()); + DriverService service, AppiumClientConfig appiumClientConfig) { + this(additionalCommands, service, HttpClient.Factory.createDefault(), appiumClientConfig); } - protected B getPrivateFieldValue(String fieldName, Class fieldType) { - Class superclass = getClass().getSuperclass(); - Throwable recentException = null; - while (superclass != Object.class) { - try { - final Field f = superclass.getDeclaredField(fieldName); - f.setAccessible(true); - return fieldType.cast(f.get(this)); - } catch (NoSuchFieldException | IllegalAccessException e) { - recentException = e; - } - superclass = superclass.getSuperclass(); - } - throw new WebDriverException(recentException); + @Deprecated + @SuppressWarnings("SameParameterValue") + protected void setPrivateFieldValue( + Class cls, String fieldName, Object newValue) { + ReflectionHelpers.setPrivateFieldValue(cls, this, fieldName, newValue); } - protected void setPrivateFieldValue(String fieldName, Object newValue) { - Class superclass = getClass().getSuperclass(); - Throwable recentException = null; - while (superclass != Object.class) { - try { - final Field f = superclass.getDeclaredField(fieldName); - f.setAccessible(true); - f.set(this, newValue); - return; - } catch (NoSuchFieldException | IllegalAccessException e) { - recentException = e; - } - superclass = superclass.getSuperclass(); - } - throw new WebDriverException(recentException); + public Map getAdditionalCommands() { + return additionalCommands; } - protected Map getAdditionalCommands() { - //noinspection unchecked - return getPrivateFieldValue("additionalCommands", Map.class); + public Factory getHttpClientFactory() { + return httpClientFactory; } + @Nullable protected CommandCodec getCommandCodec() { - //noinspection unchecked - return getPrivateFieldValue("commandCodec", CommandCodec.class); + return this.commandCodec; } - protected void setCommandCodec(CommandCodec newCodec) { - setPrivateFieldValue("commandCodec", newCodec); + public void setCommandCodec(CommandCodec newCodec) { + this.commandCodec = newCodec; } - protected void setResponseCodec(ResponseCodec codec) { - setPrivateFieldValue("responseCodec", codec); + public void setResponseCodec(ResponseCodec codec) { + this.responseCodec = codec; } protected HttpClient getClient() { - return getPrivateFieldValue("client", HttpClient.class); + return this.client; } - protected HttpClient withRequestsPatchedByIdempotencyKey(HttpClient httpClient) { - return (request) -> { - request.setHeader(IDEMPOTENCY_KEY_HEADER, UUID.randomUUID().toString().toLowerCase()); - return httpClient.execute(request); - }; + /** + * 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 { if (getCommandCodec() != null) { throw new SessionNotCreatedException("Session already exists"); } - ProtocolHandshake handshake = new ProtocolHandshake() { - @SuppressWarnings("unchecked") - public Result createSession(HttpClient client, Command command) throws IOException { - Capabilities desiredCapabilities = (Capabilities) command.getParameters().get("desiredCapabilities"); - Capabilities desired = desiredCapabilities == null ? new ImmutableCapabilities() : desiredCapabilities; - - //the number of bytes before the stream should switch to buffering to a file - 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); - NewAppiumSessionPayload payload = NewAppiumSessionPayload.create(desired); - payload.writeTo(writer); - - try (InputStream rawIn = os.asByteSource().openBufferedStream(); - BufferedInputStream contentStream = new BufferedInputStream(rawIn)) { - - Method createSessionMethod = this.getClass().getSuperclass() - .getDeclaredMethod("createSession", HttpClient.class, InputStream.class, long.class); - createSessionMethod.setAccessible(true); - - Optional result = (Optional) createSessionMethod.invoke(this, - withRequestsPatchedByIdempotencyKey(client), contentStream, counter.getCount()); - - return result.map(result1 -> { - Result toReturn = result.get(); - getLogger(ProtocolHandshake.class.getName()) - .info(format("Detected dialect: %s", toReturn.getDialect())); - return toReturn; - }).orElseThrow(() -> new SessionNotCreatedException( - format("Unable to create a new remote session. Desired capabilities = %s", desired))); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new SessionNotCreatedException(format("Unable to create a new remote session. " - + "Make sure your project dependencies config does not override " - + "Selenium API version %s used by java-client library.", - Config.main().getValue("selenium.version", String.class)), e); - } catch (InvocationTargetException e) { - String message = "Unable to create a new remote session."; - if (e.getCause() != null) { - if (e.getCause() instanceof WebDriverException) { - message += " Please check the server log for more details."; - } - message += format(" Original error: %s", e.getCause().getMessage()); - } - throw new SessionNotCreatedException(message, e); - } - } finally { - os.reset(); - } - } - }; - - ProtocolHandshake.Result result = handshake - .createSession(getClient(), command); + var result = new ProtocolHandshake().createSession(getClient(), command); Dialect dialect = result.getDialect(); - setCommandCodec(dialect.getCommandCodec()); - getAdditionalCommands().forEach(this::defineCommand); + if (!(dialect.getCommandCodec() instanceof W3CHttpCommandCodec)) { + throw new SessionNotCreatedException("Only W3C sessions are supported. " + + "Please make sure your server is up to date."); + } + 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(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 @@ -242,9 +231,8 @@ public Response execute(Command command) throws WebDriverException { }); } - Response response; try { - response = NEW_SESSION.equals(command.getName()) ? createSession(command) : super.execute(command); + return NEW_SESSION.equals(command.getName()) ? createSession(command) : super.execute(command); } catch (Throwable t) { Throwable rootCause = Throwables.getRootCause(t); if (rootCause instanceof ConnectException @@ -255,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); @@ -265,13 +252,5 @@ public Response execute(Command command) throws WebDriverException { serviceOptional.ifPresent(DriverService::stop); } } - - if (DriverCommand.NEW_SESSION.equals(command.getName()) - && getCommandCodec() instanceof W3CHttpCommandCodec) { - setCommandCodec(new AppiumW3CHttpCommandCodec()); - getAdditionalCommands().forEach(this::defineCommand); - } - - return response; } } diff --git a/src/main/java/io/appium/java_client/remote/AppiumNewSessionCommandPayload.java b/src/main/java/io/appium/java_client/remote/AppiumNewSessionCommandPayload.java new file mode 100644 index 000000000..31635dabb --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/AppiumNewSessionCommandPayload.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.remote; + +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. + * + * @param possiblyInvalidCapabilities user-provided capabilities mapping. + * @return Fixed capabilities mapping. + */ + private static Map makeW3CSafe(Capabilities possiblyInvalidCapabilities) { + return Require.nonNull("Capabilities", possiblyInvalidCapabilities) + .asMap().entrySet().stream() + .collect(Collectors.toUnmodifiableMap( + entry -> BaseOptions.toW3cName(entry.getKey()), + Map.Entry::getValue + )); + } + + /** + * Overrides the default new session behavior to + * only handle W3C capabilities. + * + * @param capabilities User-provided capabilities. + */ + public AppiumNewSessionCommandPayload(Capabilities capabilities) { + super(NEW_SESSION, Map.of( + "capabilities", Set.of(makeW3CSafe(capabilities)), + "desiredCapabilities", capabilities + )); + } +} diff --git a/src/main/java/io/appium/java_client/DriverMobileCommand.java b/src/main/java/io/appium/java_client/remote/AppiumProtocolHandshake.java similarity index 71% rename from src/main/java/io/appium/java_client/DriverMobileCommand.java rename to src/main/java/io/appium/java_client/remote/AppiumProtocolHandshake.java index 9d991d3cb..ef2f659da 100644 --- a/src/main/java/io/appium/java_client/DriverMobileCommand.java +++ b/src/main/java/io/appium/java_client/remote/AppiumProtocolHandshake.java @@ -14,14 +14,15 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.remote; + +import org.openqa.selenium.remote.ProtocolHandshake; /** - * An empty interface defining constants for the standard commands defined in the Mobile JSON - * wire protocol. + * This class is deprecated and should be removed. * - * @author jonahss@gmail.com (Jonah Stiennon) + * @deprecated Use ProtocolHandshake instead. */ -public interface DriverMobileCommand { - //TODO Jonah: we'll probably need this +@Deprecated +public class AppiumProtocolHandshake extends ProtocolHandshake { } 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 aec7ebd75..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,29 +16,20 @@ 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; import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_SIZE; import static org.openqa.selenium.remote.DriverCommand.GET_PAGE_SOURCE; import static org.openqa.selenium.remote.DriverCommand.IS_ELEMENT_DISPLAYED; -import static org.openqa.selenium.remote.DriverCommand.SEND_KEYS_TO_ACTIVE_ELEMENT; import static org.openqa.selenium.remote.DriverCommand.SEND_KEYS_TO_ELEMENT; import static org.openqa.selenium.remote.DriverCommand.SET_TIMEOUT; import static org.openqa.selenium.remote.DriverCommand.SUBMIT_ELEMENT; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.interactions.KeyInput; -import org.openqa.selenium.interactions.Sequence; -import org.openqa.selenium.remote.http.W3CHttpCommandCodec; - -import java.util.Collection; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; - public class AppiumW3CHttpCommandCodec extends W3CHttpCommandCodec { /** * This class overrides the built-in Selenium W3C commands codec, @@ -51,7 +42,6 @@ public AppiumW3CHttpCommandCodec() { defineCommand(GET_ELEMENT_ATTRIBUTE, get("/session/:sessionId/element/:id/attribute/:name")); defineCommand(IS_ELEMENT_DISPLAYED, get("/session/:sessionId/element/:id/displayed")); defineCommand(GET_PAGE_SOURCE, get("/session/:sessionId/source")); - defineCommand(SEND_KEYS_TO_ACTIVE_ELEMENT, post("/session/:sessionId/actions")); } @Override @@ -76,25 +66,6 @@ public void alias(String commandName, String isAnAliasFor) { protected Map amendParameters(String name, Map parameters) { // This blocks parent constructor from undesirable parameters amending switch (name) { - case SEND_KEYS_TO_ACTIVE_ELEMENT: - Object rawValue = parameters.get("value"); - //noinspection unchecked - Stream source = (rawValue instanceof Collection) - ? ((Collection) rawValue).stream() - : Stream.of((CharSequence[]) rawValue); - String text = source - .flatMap(Stream::of) - .collect(Collectors.joining()); - - final KeyInput keyboard = new KeyInput("keyboard"); - Sequence sequence = new Sequence(keyboard, 0); - for (int i = 0; i < text.length(); ++i) { - sequence.addAction(keyboard.createKeyDown(text.charAt(i))) - .addAction(keyboard.createKeyUp(text.charAt(i))); - } - return ImmutableMap.builder() - .put("actions", ImmutableList.of(sequence.toJson())) - .build(); case SEND_KEYS_TO_ELEMENT: case SET_TIMEOUT: return super.amendParameters(name, parameters); 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 ce85512c1..e941d516b 100644 --- a/src/main/java/io/appium/java_client/remote/AutomationName.java +++ b/src/main/java/io/appium/java_client/remote/AutomationName.java @@ -16,13 +16,28 @@ package io.appium.java_client.remote; - public interface AutomationName { - String APPIUM = "Appium"; - @Deprecated - String SELENDROID = "Selendroid"; + // Officially supported drivers + // https://github.com/appium/appium-xcuitest-driver String IOS_XCUI_TEST = "XCuiTest"; + // https://github.com/appium/appium-uiautomator2-driver String ANDROID_UIAUTOMATOR2 = "UIAutomator2"; - String YOUI_ENGINE = "youiengine"; + // https://github.com/appium/appium-espresso-driver String ESPRESSO = "Espresso"; + // https://github.com/appium/appium-mac2-driver + String MAC2 = "Mac2"; + // https://github.com/appium/appium-windows-driver + String WINDOWS = "Windows"; + // https://github.com/appium/appium-safari-driver + 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 4fcf8a444..000000000 --- a/src/main/java/io/appium/java_client/remote/IOSMobileCapabilityType.java +++ /dev/null @@ -1,391 +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"; - - /** - * Amount of time in ms to wait for instruments before assuming it hung and - * failing the session. - */ - String LAUNCH_TIMEOUT = "launchTimeout"; - - /** - * (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/MobileBrowserType.java b/src/main/java/io/appium/java_client/remote/MobileBrowserType.java index 9ca3c79ed..bcd0382a2 100644 --- a/src/main/java/io/appium/java_client/remote/MobileBrowserType.java +++ b/src/main/java/io/appium/java_client/remote/MobileBrowserType.java @@ -17,7 +17,7 @@ package io.appium.java_client.remote; public interface MobileBrowserType { - + String ANDROID = "Android"; String SAFARI = "Safari"; String BROWSER = "Browser"; String CHROMIUM = "Chromium"; 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 1f4eb00f1..000000000 --- a/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java +++ /dev/null @@ -1,145 +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"; - - /** - * This is the flag which forces server to switch to the mobile JSONWP. - * If {@code false} then it is switched to W3C mode. - */ - String FORCE_MJSONWP = "forceMjsonwp"; - - /** - * (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 6a7810f1a..000000000 --- a/src/main/java/io/appium/java_client/remote/MobileOptions.java +++ /dev/null @@ -1,515 +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; - -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) { - merge(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); - } - - /** - * Get the kind of mobile device or emulator to use. - * - * @return String representing the kind of mobile device or emulator to use. - * @see org.openqa.selenium.remote.CapabilityType#PLATFORM_NAME - */ - public String getPlatformName() { - return (String) getCapability(CapabilityType.PLATFORM_NAME); - } - - /** - * 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/MobilePlatform.java b/src/main/java/io/appium/java_client/remote/MobilePlatform.java index 07ecc8223..97e8deaf3 100644 --- a/src/main/java/io/appium/java_client/remote/MobilePlatform.java +++ b/src/main/java/io/appium/java_client/remote/MobilePlatform.java @@ -23,4 +23,5 @@ public interface MobilePlatform { String FIREFOX_OS = "FirefoxOS"; String WINDOWS = "Windows"; String TVOS = "tvOS"; + String MAC = "Mac"; } diff --git a/src/main/java/io/appium/java_client/remote/NewAppiumSessionPayload.java b/src/main/java/io/appium/java_client/remote/NewAppiumSessionPayload.java deleted file mode 100644 index 8d51f914d..000000000 --- a/src/main/java/io/appium/java_client/remote/NewAppiumSessionPayload.java +++ /dev/null @@ -1,539 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License.writeTo - -package io.appium.java_client.remote; - -import static com.google.common.collect.ImmutableMap.of; -import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; -import static io.appium.java_client.remote.MobileCapabilityType.FORCE_MJSONWP; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toList; -import static org.openqa.selenium.json.Json.LIST_OF_MAPS_TYPE; -import static org.openqa.selenium.json.Json.MAP_TYPE; -import static org.openqa.selenium.remote.CapabilityType.PLATFORM; -import static org.openqa.selenium.remote.CapabilityType.PLATFORM_NAME; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedSet; -import com.google.common.collect.Ordering; -import com.google.common.collect.Sets; -import com.google.common.io.CharSource; -import com.google.common.io.CharStreams; -import com.google.common.io.FileBackedOutputStream; - -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.ImmutableCapabilities; -import org.openqa.selenium.json.Json; -import org.openqa.selenium.json.JsonInput; -import org.openqa.selenium.json.JsonOutput; -import org.openqa.selenium.remote.AcceptedW3CCapabilityKeys; -import org.openqa.selenium.remote.Dialect; -import org.openqa.selenium.remote.session.CapabilitiesFilter; -import org.openqa.selenium.remote.session.CapabilityTransform; -import org.openqa.selenium.remote.session.ChromeFilter; -import org.openqa.selenium.remote.session.EdgeFilter; -import org.openqa.selenium.remote.session.FirefoxFilter; -import org.openqa.selenium.remote.session.InternetExplorerFilter; -import org.openqa.selenium.remote.session.OperaFilter; -import org.openqa.selenium.remote.session.ProxyTransform; -import org.openqa.selenium.remote.session.SafariFilter; -import org.openqa.selenium.remote.session.StripAnyPlatform; -import org.openqa.selenium.remote.session.W3CPlatformNameNormaliser; - -import java.io.Closeable; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.StringReader; -import java.io.Writer; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Queue; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.TreeMap; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.annotation.Nullable; - -public class NewAppiumSessionPayload implements Closeable { - - private static final List APPIUM_CAPABILITIES = ImmutableList.builder() - .addAll(getAppiumCapabilities(MobileCapabilityType.class)) - .addAll(getAppiumCapabilities(AndroidMobileCapabilityType.class)) - .addAll(getAppiumCapabilities(IOSMobileCapabilityType.class)) - .addAll(getAppiumCapabilities(YouiEngineCapabilityType.class)).build(); - private static final String DESIRED_CAPABILITIES = "desiredCapabilities"; - private static final String CAPABILITIES = "capabilities"; - private static final String REQUIRED_CAPABILITIES = "requiredCapabilities"; - private static final String FIRST_MATCH = "firstMatch"; - private static final String ALWAYS_MATCH = "alwaysMatch"; - - private static final Predicate ACCEPTED_W3C_PATTERNS = new AcceptedW3CCapabilityKeys(); - - private final Set adapters; - private final Set transforms; - private final boolean forceMobileJSONWP; - - private final Json json = new Json(); - private final FileBackedOutputStream backingStore; - - private static List getAppiumCapabilities(Class capabilityList) { - return Arrays.stream(capabilityList.getDeclaredFields()).map(field -> { - field.setAccessible(true); - try { - return field.get(capabilityList).toString(); - } catch (IllegalAccessException e) { - throw new IllegalArgumentException(e); - } - }).filter(s -> !FORCE_MJSONWP.equals(s)).collect(toList()); - } - - /** - * Creates instance of {@link NewAppiumSessionPayload}. - * - * @param caps capabilities to create a new session - * @return instance of {@link NewAppiumSessionPayload} - * @throws IOException On file system I/O error. - */ - public static NewAppiumSessionPayload create(Capabilities caps) throws IOException { - boolean forceMobileJSONWP = - ofNullable(caps.getCapability(FORCE_MJSONWP)) - .map(o -> Boolean.class.isAssignableFrom(o.getClass()) && Boolean.class.cast(o)) - .orElse(false); - - HashMap capabilityMap = new HashMap<>(caps.asMap()); - capabilityMap.remove(FORCE_MJSONWP); - Map source = of(DESIRED_CAPABILITIES, capabilityMap); - String json = new Json().toJson(source); - return new NewAppiumSessionPayload(new StringReader(json), forceMobileJSONWP); - } - - private NewAppiumSessionPayload(Reader source, boolean forceMobileJSONWP) throws IOException { - this.forceMobileJSONWP = forceMobileJSONWP; - // Dedicate up to 10% of all RAM or 20% of available RAM (whichever is smaller) to storing this - // payload. - int threshold = (int) Math.min( - Integer.MAX_VALUE, - Math.min( - Runtime.getRuntime().freeMemory() / 5, - Runtime.getRuntime().maxMemory() / 10)); - - backingStore = new FileBackedOutputStream(threshold); - try (Writer writer = new OutputStreamWriter(backingStore, UTF_8)) { - CharStreams.copy(source, writer); - } - - ImmutableSet.Builder adapters = ImmutableSet.builder(); - ServiceLoader.load(CapabilitiesFilter.class).forEach(adapters::add); - adapters - .add(new ChromeFilter()) - .add(new EdgeFilter()) - .add(new FirefoxFilter()) - .add(new InternetExplorerFilter()) - .add(new OperaFilter()) - .add(new SafariFilter()); - this.adapters = adapters.build(); - - ImmutableSet.Builder transforms = ImmutableSet.builder(); - ServiceLoader.load(CapabilityTransform.class).forEach(transforms::add); - transforms - .add(new ProxyTransform()) - .add(new StripAnyPlatform()) - .add(new W3CPlatformNameNormaliser()); - this.transforms = transforms.build(); - - ImmutableSet.Builder dialects = ImmutableSet.builder(); - if (getOss() != null) { - dialects.add(Dialect.OSS); - } - if (getAlwaysMatch() != null || getFirstMatch() != null) { - dialects.add(Dialect.W3C); - } - - validate(); - } - - private void validate() throws IOException { - Map alwaysMatch = getAlwaysMatch(); - if (alwaysMatch == null) { - alwaysMatch = of(); - } - Map always = alwaysMatch; - Collection> firsts = getFirstMatch(); - if (firsts == null) { - firsts = ImmutableList.of(of()); - } - - if (firsts.isEmpty()) { - throw new IllegalArgumentException("First match w3c capabilities is zero length"); - } - - //noinspection ResultOfMethodCallIgnored - firsts.stream() - .peek(map -> { - Set overlap = Sets.intersection(always.keySet(), map.keySet()); - if (!overlap.isEmpty()) { - throw new IllegalArgumentException( - "Overlapping keys between w3c always and first match capabilities: " + overlap); - } - }) - .map(first -> { - Map toReturn = new HashMap<>(); - toReturn.putAll(always); - toReturn.putAll(first); - return toReturn; - }) - .peek(map -> { - ImmutableSortedSet nullKeys = map.entrySet().stream() - .filter(entry -> entry.getValue() == null) - .map(Map.Entry::getKey) - .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural())); - if (!nullKeys.isEmpty()) { - throw new IllegalArgumentException( - "Null values found in w3c capabilities. Keys are: " + nullKeys); - } - }) - .peek(map -> { - ImmutableSortedSet illegalKeys = map.entrySet().stream() - .filter(entry -> !ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .map(Map.Entry::getKey) - .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural())); - if (!illegalKeys.isEmpty()) { - throw new IllegalArgumentException( - "Illegal key values seen in w3c capabilities: " + illegalKeys); - } - }); - } - - /** - * Writes json capabilities to some appendable object. - * - * @param appendable to write a json - * @throws IOException On file system I/O error. - */ - public void writeTo(Appendable appendable) throws IOException { - try (JsonOutput json = new Json().newOutput(appendable)) { - json.beginObject(); - - Map first = getOss(); - if (first == null) { - //noinspection unchecked - first = (Map) stream().findFirst() - .orElse(new ImmutableCapabilities()) - .asMap(); - } - - // Write the first capability we get as the desired capability. - json.name(DESIRED_CAPABILITIES); - json.write(first); - - if (!forceMobileJSONWP) { - // And write the first capability for gecko13 - json.name(CAPABILITIES); - json.beginObject(); - - // Then write everything into the w3c payload. Because of the way we do this, it's easiest - // to just populate the "firstMatch" section. The spec says it's fine to omit the - // "alwaysMatch" field, so we do this. - json.name(FIRST_MATCH); - json.beginArray(); - getW3C().forEach(json::write); - json.endArray(); - - json.endObject(); // Close "capabilities" object - } - - writeMetaData(json); - - json.endObject(); - } - } - - private void writeMetaData(JsonOutput out) throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - switch (name) { - case CAPABILITIES: - case DESIRED_CAPABILITIES: - case REQUIRED_CAPABILITIES: - input.skipValue(); - break; - - default: - out.name(name); - out.write(input.read(Object.class)); - break; - } - } - } - } - - /** - * Stream the {@link Capabilities} encoded in the payload used to create this instance. The - * {@link Stream} will start with a {@link Capabilities} object matching the OSS capabilities, and - * will then expand each of the "{@code firstMatch}" and "{@code alwaysMatch}" contents as defined - * in the W3C WebDriver spec. The OSS {@link Capabilities} are listed first because converting the - * OSS capabilities to the equivalent W3C capabilities isn't particularly easy, so it's hoped that - * this approach gives us the most compatible implementation. - * - * @return The capabilities as a stream. - * @throws IOException On file system I/O error. - */ - public Stream stream() throws IOException { - // OSS first - Stream> oss = Stream.of(getOss()); - - // And now W3C - Stream> w3c = getW3C(); - - return Stream.concat(oss, w3c) - .filter(Objects::nonNull) - .map(this::applyTransforms) - .filter(Objects::nonNull) - .distinct() - .map(ImmutableCapabilities::new); - } - - @Override - public void close() throws IOException { - backingStore.reset(); - } - - private @Nullable Map getOss() throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - if (DESIRED_CAPABILITIES.equals(name)) { - return input.read(MAP_TYPE); - } - input.skipValue(); - } - } - return null; - } - - private Stream> getW3C() throws IOException { - // If there's an OSS value, generate a stream of capabilities from that using the transforms, - // then add magic to generate each of the w3c capabilities. For the sake of simplicity, we're - // going to make the (probably wrong) assumption we can hold all of the firstMatch values and - // alwaysMatch value in memory at the same time. - Map oss = convertOssToW3C(getOss()); - Stream> fromOss; - - if (oss != null) { - Set usedKeys = new HashSet<>(); - - // Are there any values we care want to pull out into a mapping of their own? - List> firsts = adapters.stream() - .map(adapter -> adapter.apply(oss)) - .filter(Objects::nonNull) - .filter(map -> !map.isEmpty()) - .map(map -> - map.entrySet().stream() - .filter(entry -> entry.getKey() != null) - .filter(entry -> ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .filter(entry -> entry.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) - .peek(map -> usedKeys.addAll(map.keySet())) - .collect(ImmutableList.toImmutableList()); - if (firsts.isEmpty()) { - firsts = ImmutableList.of(of()); - } - - // Are there any remaining unused keys? - Map always = oss.entrySet().stream() - .filter(entry -> !usedKeys.contains(entry.getKey())) - .filter(entry -> entry.getValue() != null) - .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue)); - - // Firsts contains at least one entry, always contains everything else. Let's combine them - // into the stream to form a unified set of capabilities. Woohoo! - fromOss = firsts.stream() - .map(first -> ImmutableMap.builder().putAll(always).putAll(first).build()) - .map(this::applyTransforms) - .map(map -> map.entrySet().stream() - .filter(entry -> !forceMobileJSONWP || ACCEPTED_W3C_PATTERNS.test(entry.getKey())) - .map((Function, Map.Entry>) stringObjectEntry -> - new Map.Entry() { - @Override - public String getKey() { - String key = stringObjectEntry.getKey(); - if (APPIUM_CAPABILITIES.contains(key) && !forceMobileJSONWP) { - return APPIUM_PREFIX + key; - } - return key; - } - - @Override - public Object getValue() { - return stringObjectEntry.getValue(); - } - - @Override - public Object setValue(Object value) { - return stringObjectEntry.setValue(value); - } - }) - .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue))) - .map(map -> map); - } else { - fromOss = Stream.of(); - } - - Stream> fromW3c; - Map alwaysMatch = getAlwaysMatch(); - Collection> firsts = getFirstMatch(); - - if (alwaysMatch == null && firsts == null) { - fromW3c = Stream.of(); // No W3C capabilities. - } else { - if (alwaysMatch == null) { - alwaysMatch = of(); - } - Map always = alwaysMatch; // Keep the comoiler happy. - if (firsts == null) { - firsts = ImmutableList.of(of()); - } - - fromW3c = firsts.stream() - .map(first -> ImmutableMap.builder().putAll(always).putAll(first).build()); - } - - return Stream.concat(fromOss, fromW3c).distinct(); - } - - private @Nullable Map convertOssToW3C(Map capabilities) { - if (capabilities == null) { - return null; - } - - Map toReturn = new TreeMap<>(capabilities); - - // Platform name - if (capabilities.containsKey(PLATFORM) && !capabilities.containsKey(PLATFORM_NAME)) { - toReturn.put(PLATFORM_NAME, String.valueOf(capabilities.get(PLATFORM))); - } - - return toReturn; - } - - private @Nullable Map getAlwaysMatch() throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - if (CAPABILITIES.equals(name)) { - input.beginObject(); - while (input.hasNext()) { - name = input.nextName(); - if (ALWAYS_MATCH.equals(name)) { - return input.read(MAP_TYPE); - } - input.skipValue(); - } - input.endObject(); - } else { - input.skipValue(); - } - } - } - return null; - } - - private @Nullable Collection> getFirstMatch() throws IOException { - CharSource charSource = backingStore.asByteSource().asCharSource(UTF_8); - try (Reader reader = charSource.openBufferedStream(); - JsonInput input = json.newInput(reader)) { - input.beginObject(); - while (input.hasNext()) { - String name = input.nextName(); - if (CAPABILITIES.equals(name)) { - input.beginObject(); - while (input.hasNext()) { - name = input.nextName(); - if (FIRST_MATCH.equals(name)) { - return input.read(LIST_OF_MAPS_TYPE); - } - input.skipValue(); - } - input.endObject(); - } else { - input.skipValue(); - } - } - } - return null; - } - - private Map applyTransforms(Map caps) { - Queue> toExamine = new LinkedList<>(caps.entrySet()); - Set seenKeys = new HashSet<>(); - Map toReturn = new TreeMap<>(); - - // Take each entry and apply the transforms - while (!toExamine.isEmpty()) { - Map.Entry entry = toExamine.remove(); - seenKeys.add(entry.getKey()); - - if (entry.getValue() == null) { - continue; - } - - for (CapabilityTransform transform : transforms) { - Collection> result = transform.apply(entry); - if (result == null) { - toReturn.remove(entry.getKey()); - break; - } - - for (Map.Entry newEntry : result) { - if (!seenKeys.contains(newEntry.getKey())) { - toExamine.add(newEntry); - continue; - } - if (newEntry.getKey().equals(entry.getKey())) { - entry = newEntry; - } - toReturn.put(newEntry.getKey(), newEntry.getValue()); - } - } - } - return toReturn; - } -} diff --git a/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java b/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java new file mode 100644 index 000000000..c576583dc --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/SupportsContextSwitching.java @@ -0,0 +1,80 @@ +/* + * 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 io.appium.java_client.ExecutesMethod; +import io.appium.java_client.MobileCommand; +import io.appium.java_client.NoSuchContextException; +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.remote.Response; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +public interface SupportsContextSwitching extends WebDriver, ExecutesMethod { + /** + * Switches to the given context. + * + * @param name The name of the context to switch to. + * @return self instance for chaining. + */ + default WebDriver context(String name) { + requireNonNull(name, "Must supply a context name"); + try { + execute(MobileCommand.SWITCH_TO_CONTEXT, Map.of("name", name)); + return this; + } catch (WebDriverException e) { + throw new NoSuchContextException(e.getMessage(), e); + } + } + + /** + * Get the names of available contexts. + * + * @return List list of context names. + */ + default Set getContextHandles() { + Response response = execute(MobileCommand.GET_CONTEXT_HANDLES, Map.of()); + Object value = response.getValue(); + try { + //noinspection unchecked + List returnedValues = (List) value; + return new LinkedHashSet<>(returnedValues); + } catch (ClassCastException ex) { + throw new WebDriverException( + "Returned value cannot be converted to List: " + value, ex); + } + } + + /** + * Get the name of the current context. + * + * @return Context name or null if it cannot be determined. + */ + @Nullable + default String getContext() { + String contextName = + 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 new file mode 100644 index 000000000..c19dcc96c --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/SupportsLocation.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.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.WebDriverException; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +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) { + 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 new file mode 100644 index 000000000..eb8a52b44 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/SupportsRotation.java @@ -0,0 +1,61 @@ +/* + * 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 io.appium.java_client.ExecutesMethod; +import io.appium.java_client.MobileCommand; +import org.openqa.selenium.DeviceRotation; +import org.openqa.selenium.ScreenOrientation; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.remote.Response; + +import java.util.Map; + +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(MobileCommand.GET_SCREEN_ROTATION); + //noinspection unchecked + return new DeviceRotation((Map) response.getValue()); + } + + default void rotate(DeviceRotation rotation) { + execute(MobileCommand.SET_SCREEN_ROTATION, rotation.parameters()); + } + + default void rotate(ScreenOrientation orientation) { + execute(MobileCommand.SET_SCREEN_ORIENTATION, + Map.of("orientation", orientation.value().toUpperCase(ROOT))); + } + + /** + * Get device orientation. + * + * @return The orientation value. + */ + default ScreenOrientation getOrientation() { + Response response = execute(MobileCommand.GET_SCREEN_ORIENTATION); + String orientation = String.valueOf(response.getValue()); + 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 new file mode 100644 index 000000000..dc5ada3a5 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/BaseMapOptionData.java @@ -0,0 +1,88 @@ +/* + * 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 com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public abstract class BaseMapOptionData> { + private Map options; + private static final Gson GSON = new Gson(); + + public BaseMapOptionData() { + } + + public BaseMapOptionData(Map options) { + this.options = options; + } + + public BaseMapOptionData(String json) { + //noinspection unchecked + this((Map) GSON.fromJson(json, Map.class)); + } + + /** + * Sets the given value on the data object. + * + * @param key Key name. + * @param value The actual value to set. + * @return self instance for chaining. + */ + public T assignOptionValue(String key, Object value) { + if (options == null) { + options = new HashMap<>(); + } + options.put(key, value); + //noinspection unchecked + return (T) this; + } + + /** + * Retrieves a value with the given name from a data object. + * This method does not perform any type transformation, but rather + * just tries to cast the received value to the given type, so + * be careful while providing a very specific result type value to + * not get a type cast error. + * + * @param name Key name. + * @param The expected value type. + * @return The actual value. + */ + public Optional getOptionValue(String name) { + //noinspection unchecked + return Optional.ofNullable(options) + .map(opts -> (R) opts.getOrDefault(name, null)); + } + + public Map toMap() { + return Optional.ofNullable(options).orElseGet(Collections::emptyMap); + } + + public JsonObject toJson() { + return GSON.toJsonTree(toMap()).getAsJsonObject(); + } + + @Override + public String toString() { + 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 new file mode 100644 index 000000000..cc544022c --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/BaseOptions.java @@ -0,0 +1,167 @@ +/* + * 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.jspecify.annotations.Nullable; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.MutableCapabilities; +import org.openqa.selenium.Platform; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.CapabilityType; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; +import static java.util.Collections.unmodifiableMap; + +/** + * This class represents capabilities that are available in the base driver, + * e.g. are acceptable by any Appium driver + * + * @param The child class for a proper chaining. + */ +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public class BaseOptions> extends MutableCapabilities implements + CanSetCapability, + SupportsAutomationNameOption, + SupportsEventTimingsOption, + SupportsPrintPageSourceOnFindFailureOption, + SupportsNoResetOption, + SupportsFullResetOption, + SupportsNewCommandTimeoutOption, + SupportsBrowserNameOption, + SupportsPlatformVersionOption, + SupportsWebSocketUrlOption { + + /** + * Creates new instance with no preset capabilities. + */ + public BaseOptions() { + } + + /** + * Creates new instance with provided capabilities. + * + * @param source Capabilities map to merge into new instance + */ + public BaseOptions(Map source) { + super(source); + } + + /** + * Creates new instance with provided capabilities. + * + * @param source is Capabilities instance to merge into new instance + */ + public BaseOptions(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 self instance for chaining. + * @see CapabilityType#PLATFORM_NAME + */ + public T setPlatformName(String platform) { + return amend(CapabilityType.PLATFORM_NAME, platform); + } + + @Override + @Nullable + public Platform getPlatformName() { + return Optional.ofNullable(getCapability(CapabilityType.PLATFORM_NAME)) + .map(cap -> { + if (cap instanceof Platform) { + return (Platform) cap; + } + + try { + return Platform.fromString(String.valueOf(cap)); + } catch (WebDriverException e) { + return null; + } + }) + .orElse(null); + } + + @Override + public Map asMap() { + return unmodifiableMap(super.asMap().entrySet().stream() + .collect(Collectors.toMap(entry -> toW3cName(entry.getKey()), Map.Entry::getValue) + )); + } + + @Override + public T merge(Capabilities extraCapabilities) { + T result = this.clone(); + extraCapabilities.asMap().forEach((key, value) -> { + if (value != null) { + result.setCapability(key, value); + } + }); + return result; + } + + /** + * Makes a deep clone of the current Options instance. + * + * @return A deep instance clone. + */ + @SuppressWarnings("MethodDoesntCallSuperMethod") + public T clone() { + try { + Constructor constructor = getClass().getConstructor(Capabilities.class); + //noinspection unchecked + return (T) constructor.newInstance(this); + } catch (InvocationTargetException | NoSuchMethodException + | InstantiationException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void setCapability(String key, @Nullable Object value) { + Require.nonNull("Capability name", key); + super.setCapability(toW3cName(key), value); + } + + @Override + @Nullable + public Object getCapability(String capabilityName) { + Object value = super.getCapability(capabilityName); + return value == null + ? super.getCapability(APPIUM_PREFIX + capabilityName) + : value; + } + + /** + * Adds the 'appium:' prefix to the given capability name if necessary. + * + * @param capName the original capability name. + * @return The preformatted W3C-compatible capability name. + */ + public static String toW3cName(String capName) { + return W3CCapabilityKeys.INSTANCE.test(capName) ? capName : APPIUM_PREFIX + capName; + } +} diff --git a/src/main/java/io/appium/java_client/events/api/general/ListensToException.java b/src/main/java/io/appium/java_client/remote/options/CanSetCapability.java similarity index 59% rename from src/main/java/io/appium/java_client/events/api/general/ListensToException.java rename to src/main/java/io/appium/java_client/remote/options/CanSetCapability.java index 89733e693..c992569c6 100644 --- a/src/main/java/io/appium/java_client/events/api/general/ListensToException.java +++ b/src/main/java/io/appium/java_client/remote/options/CanSetCapability.java @@ -14,16 +14,21 @@ * limitations under the License. */ -package io.appium.java_client.events.api.general; +package io.appium.java_client.remote.options; -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.WebDriver; +public interface CanSetCapability> { + void setCapability(String key, Object value); -public interface ListensToException extends Listener { /** - * Called whenever an exception would be thrown. - * @param throwable the exception that will be thrown - * @param driver WebDriver + * Set a custom option. + * + * @param optionName Option name. + * @param value Option value. + * @return self instance for chaining. */ - void onException(Throwable throwable, WebDriver driver); + default T amend(String optionName, Object value) { + setCapability(optionName, value); + //noinspection unchecked + return (T) this; + } } diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsAcceptInsecureCertsOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsAcceptInsecureCertsOption.java new file mode 100644 index 000000000..5146d7991 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsAcceptInsecureCertsOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsAcceptInsecureCertsOption> extends + Capabilities, CanSetCapability { + String ACCEPT_INSECURE_CERTS_OPTION = "acceptInsecureCerts"; + + /** + * Enforces untrusted and self-signed TLS certificates are + * implicitly trusted on navigation for the duration of the session. + * + * @return self instance for chaining. + */ + default T acceptInsecureCerts() { + return setAcceptInsecureCerts(true); + } + + /** + * Set whether untrusted and self-signed TLS certificates are + * implicitly trusted on navigation for the duration of the session. + * + * @param bool True or false. + * @return self instance for chaining. + */ + default T setAcceptInsecureCerts(boolean bool) { + return amend(ACCEPT_INSECURE_CERTS_OPTION, bool); + } + + /** + * Get whether untrusted and self-signed TLS certificates are + * implicitly trusted on navigation for the duration of the session. + * + * @return true or false. + */ + default Optional doesAcceptInsecureCerts() { + return Optional.ofNullable(toSafeBoolean(getCapability(ACCEPT_INSECURE_CERTS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsAppOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsAppOption.java new file mode 100644 index 000000000..043904fde --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsAppOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.net.URL; +import java.util.Optional; + +public interface SupportsAppOption> extends + Capabilities, CanSetCapability { + String APP_OPTION = "app"; + + /** + * Set the absolute local path for the location of the App. + * The app must be located on the same machine where Appium + * server is running. + * + * @param path is a String representing the location of the App + * @return self instance for chaining. + */ + default T setApp(String path) { + return amend(APP_OPTION, path); + } + + /** + * Set the remote http URL for the location of the App. + * + * @param url is the URL representing the location of the App + * @return self instance for chaining. + */ + default T setApp(URL url) { + return setApp(url.toString()); + } + + /** + * Get the app location. + * + * @return String representing app location + */ + default Optional getApp() { + return Optional.ofNullable((String) getCapability(APP_OPTION)); + } +} 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 new file mode 100644 index 000000000..3cd2d7e93 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsAutoWebViewOption.java @@ -0,0 +1,56 @@ +/* + * 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; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsAutoWebViewOption> extends + Capabilities, CanSetCapability { + String AUTO_WEB_VIEW_OPTION = "autoWebview"; + + /** + * Set the app to move directly into Webview context. + * + * @return self instance for chaining. + */ + default T autoWebview() { + return setAutoWebview(true); + } + + /** + * Set whether the app moves directly into Webview context. + * + * @param bool is whether the app moves directly into Webview context. + * @return self instance for chaining. + */ + default T setAutoWebview(boolean bool) { + return amend(AUTO_WEB_VIEW_OPTION, bool); + } + + /** + * Get whether the app moves directly into Webview context. + * + * @return true if app moves directly into Webview context. + */ + default Optional doesAutoWebview() { + return Optional.ofNullable(toSafeBoolean(getCapability(AUTO_WEB_VIEW_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsAutomationNameOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsAutomationNameOption.java new file mode 100644 index 000000000..2fdb5870d --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsAutomationNameOption.java @@ -0,0 +1,45 @@ +/* + * 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 SupportsAutomationNameOption> extends + Capabilities, CanSetCapability { + String AUTOMATION_NAME_OPTION = "automationName"; + + /** + * Set the automation driver to use. + * + * @param automationName One of supported automation names. + * @return self instance for chaining. + */ + default T setAutomationName(String automationName) { + return amend(AUTOMATION_NAME_OPTION, automationName); + } + + /** + * Get the automation driver to use. + * + * @return String representing the name of the automation engine + */ + default Optional getAutomationName() { + return Optional.ofNullable((String) getCapability(AUTOMATION_NAME_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/FindsByIosNSPredicate.java b/src/main/java/io/appium/java_client/remote/options/SupportsBrowserNameOption.java similarity index 56% rename from src/main/java/io/appium/java_client/FindsByIosNSPredicate.java rename to src/main/java/io/appium/java_client/remote/options/SupportsBrowserNameOption.java index 84bc3ff67..b0506feda 100644 --- a/src/main/java/io/appium/java_client/FindsByIosNSPredicate.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsBrowserNameOption.java @@ -14,19 +14,21 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.remote.options; -import org.openqa.selenium.WebElement; +import org.openqa.selenium.Capabilities; -import java.util.List; +public interface SupportsBrowserNameOption> extends + Capabilities, CanSetCapability { + String BROWSER_NAME_OPTION = "browserName"; -public interface FindsByIosNSPredicate extends FindsByFluentSelector { - - default T findElementByIosNsPredicate(String using) { - return findElement(MobileSelector.IOS_PREDICATE_STRING.toString(), using); - } - - default List findElementsByIosNsPredicate(String using) { - return findElements(MobileSelector.IOS_PREDICATE_STRING.toString(), using); + /** + * Set the browser name to use. + * + * @param browserName One of supported browser names. + * @return self instance for chaining. + */ + default T withBrowserName(String browserName) { + return amend(BROWSER_NAME_OPTION, browserName); } } 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 new file mode 100644 index 000000000..0e940b5d6 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsBrowserVersionOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +public interface SupportsBrowserVersionOption> extends + Capabilities, CanSetCapability { + String BROWSER_VERSION_OPTION = "browserVersion"; + + /** + * Provide the version number of the browser to automate if there are multiple + * versions installed on the same machine where the driver is running. + * + * @param version Browser version to use. + * @return self instance for chaining. + */ + default T setBrowserVersion(String version) { + return amend(BROWSER_VERSION_OPTION, version); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsClearSystemFilesOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsClearSystemFilesOption.java new file mode 100644 index 000000000..d30001f3e --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsClearSystemFilesOption.java @@ -0,0 +1,56 @@ +/* + * 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; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsClearSystemFilesOption> extends + Capabilities, CanSetCapability { + String CLEAR_SYSTEM_FILES_OPTION = "clearSystemFiles"; + + /** + * Set the app to delete any generated files at the end of a session. + * + * @return self instance for chaining. + */ + default T clearSystemFiles() { + 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 self instance for chaining. + */ + default T setClearSystemFiles(boolean bool) { + return amend(CLEAR_SYSTEM_FILES_OPTION, 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. + */ + default Optional doesClearSystemFiles() { + return Optional.ofNullable(toSafeBoolean(getCapability(CLEAR_SYSTEM_FILES_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsDeviceNameOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsDeviceNameOption.java new file mode 100644 index 000000000..f1d268d1a --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsDeviceNameOption.java @@ -0,0 +1,45 @@ +/* + * 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 SupportsDeviceNameOption> extends + Capabilities, CanSetCapability { + String DEVICE_NAME_OPTION = "deviceName"; + + /** + * Set the name of the device. + * + * @param deviceName is the name of the device. + * @return self instance for chaining. + */ + default T setDeviceName(String deviceName) { + return amend(DEVICE_NAME_OPTION, deviceName); + } + + /** + * Get the name of the device. + * + * @return String representing the name of the device. + */ + default Optional getDeviceName() { + return Optional.ofNullable((String) getCapability(DEVICE_NAME_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsEnablePerformanceLoggingOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsEnablePerformanceLoggingOption.java new file mode 100644 index 000000000..cf3601925 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsEnablePerformanceLoggingOption.java @@ -0,0 +1,56 @@ +/* + * 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; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsEnablePerformanceLoggingOption> extends + Capabilities, CanSetCapability { + String ENABLE_PERFORMANCE_LOGGING_OPTION = "enablePerformanceLogging"; + + /** + * Set the app to enable performance logging. + * + * @return self instance for chaining. + */ + default T enablePerformanceLogging() { + return setEnablePerformanceLogging(true); + } + + /** + * Set whether the app logs performance. + * + * @param bool is whether the app logs performance. + * @return self instance for chaining. + */ + default T setEnablePerformanceLogging(boolean bool) { + return amend(ENABLE_PERFORMANCE_LOGGING_OPTION, bool); + } + + /** + * Get the app logs performance. + * + * @return true if the app logs performance. + */ + default Optional isEnablePerformanceLogging() { + return Optional.ofNullable(toSafeBoolean(getCapability(ENABLE_PERFORMANCE_LOGGING_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsEnforceAppInstallOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsEnforceAppInstallOption.java new file mode 100644 index 000000000..5e343938c --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsEnforceAppInstallOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsEnforceAppInstallOption> extends + Capabilities, CanSetCapability { + String ENFORCE_APP_INSTALL_OPTION = "enforceAppInstall"; + + /** + * Sets the application under test is always reinstalled even if a newer version + * of it already exists on the device under test. + * + * @return self instance for chaining. + */ + default T enforceAppInstall() { + return amend(ENFORCE_APP_INSTALL_OPTION, true); + } + + /** + * 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 (Android), true (iOS) by default. + * + * @param value True to allow test packages installation. + * @return self instance for chaining. + */ + default T setEnforceAppInstall(boolean value) { + return amend(ENFORCE_APP_INSTALL_OPTION, value); + } + + /** + * Get whether the application under test is always reinstalled even + * if a newer version of it already exists on the device under test. + * + * @return True or false. + */ + default Optional doesEnforceAppInstall() { + return Optional.ofNullable(toSafeBoolean(getCapability(ENFORCE_APP_INSTALL_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsEventTimingsOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsEventTimingsOption.java new file mode 100644 index 000000000..c961841f8 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsEventTimingsOption.java @@ -0,0 +1,56 @@ +/* + * 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; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsEventTimingsOption> extends + Capabilities, CanSetCapability { + String EVENT_TIMINGS_OPTION = "eventTimings"; + + /** + * Set the app to report the timings for various Appium-internal events. + * + * @return self instance for chaining. + */ + default T eventTimings() { + 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 self instance for chaining. + */ + default T setEventTimings(boolean bool) { + return amend(EVENT_TIMINGS_OPTION, bool); + } + + /** + * Get whether the app reports the timings for various Appium-internal events. + * + * @return true if the app reports event timings. + */ + default Optional doesEventTimings() { + return Optional.ofNullable(toSafeBoolean(getCapability(EVENT_TIMINGS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsFullResetOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsFullResetOption.java new file mode 100644 index 000000000..667bb1f3d --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsFullResetOption.java @@ -0,0 +1,56 @@ +/* + * 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; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsFullResetOption> extends + Capabilities, CanSetCapability { + String FULL_RESET_OPTION = "fullReset"; + + /** + * Set the app to do a full reset. + * + * @return self instance for chaining. + */ + default T fullReset() { + return setFullReset(true); + } + + /** + * Set whether the app does a full reset. + * + * @param bool is whether the app does a full reset. + * @return self instance for chaining. + */ + default T setFullReset(boolean bool) { + return amend(FULL_RESET_OPTION, bool); + } + + /** + * Get whether the app does a full reset. + * + * @return true if the app does a full reset. + */ + default Optional doesFullReset() { + return Optional.ofNullable(toSafeBoolean(getCapability(FULL_RESET_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsIsHeadlessOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsIsHeadlessOption.java new file mode 100644 index 000000000..759d7b8b6 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsIsHeadlessOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsIsHeadlessOption> extends + Capabilities, CanSetCapability { + String IS_HEADLESS_OPTION = "isHeadless"; + + /** + * Set emulator/simulator to start in headless mode (e.g. no UI is shown). + * It is only applied if the emulator is not running before the test starts. + * + * @return self instance for chaining. + */ + default T headless() { + return amend(IS_HEADLESS_OPTION, true); + } + + /** + * If set to true then emulator/simulator starts in headless mode (e.g. no UI is shown). + * It is only applied if the emulator is not running before the test starts. + * false by default. + * + * @param value Whether to enable or disable headless mode. + * @return self instance for chaining. + */ + default T setIsHeadless(boolean value) { + return amend(IS_HEADLESS_OPTION, value); + } + + /** + * Get whether the emulator/simulator starts in headless mode. + * + * @return True or false. + */ + default Optional isHeadless() { + return Optional.ofNullable(toSafeBoolean(getCapability(IS_HEADLESS_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsLanguageOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsLanguageOption.java new file mode 100644 index 000000000..92b41e9f6 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsLanguageOption.java @@ -0,0 +1,45 @@ +/* + * 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 SupportsLanguageOption> extends + Capabilities, CanSetCapability { + String LANGUAGE_OPTION = "language"; + + /** + * Set language abbreviation for use in session. + * + * @param language is the language abbreviation. + * @return self instance for chaining. + */ + default T setLanguage(String language) { + return amend(LANGUAGE_OPTION, language); + } + + /** + * Get language abbreviation for use in session. + * + * @return String representing the language abbreviation. + */ + default Optional getLanguage() { + return Optional.ofNullable((String) getCapability(LANGUAGE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsLocaleOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsLocaleOption.java new file mode 100644 index 000000000..37689be59 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsLocaleOption.java @@ -0,0 +1,45 @@ +/* + * 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 SupportsLocaleOption> extends + Capabilities, CanSetCapability { + String LOCALE_OPTION = "locale"; + + /** + * Set locale abbreviation for use in session. + * + * @param locale is the locale abbreviation. + * @return self instance for chaining. + */ + default T setLocale(String locale) { + return amend(LOCALE_OPTION, locale); + } + + /** + * Get locale abbreviation for use in session. + * + * @return String representing the locale abbreviation. + */ + default Optional getLocale() { + return Optional.ofNullable((String) getCapability(LOCALE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsNewCommandTimeoutOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsNewCommandTimeoutOption.java new file mode 100644 index 000000000..f9358fa95 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsNewCommandTimeoutOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.time.Duration; +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsNewCommandTimeoutOption> extends + Capabilities, CanSetCapability { + String NEW_COMMAND_TIMEOUT_OPTION = "newCommandTimeout"; + + /** + * Set the timeout for new commands. + * + * @param duration is the allowed time before seeing a new command. + * @return self instance for chaining. + */ + default T setNewCommandTimeout(Duration duration) { + return amend(NEW_COMMAND_TIMEOUT_OPTION, duration.getSeconds()); + } + + /** + * Get the timeout for new commands. + * + * @return allowed time before seeing a new command. + */ + default Optional getNewCommandTimeout() { + return Optional.ofNullable( + toDuration(getCapability(NEW_COMMAND_TIMEOUT_OPTION), Duration::ofSeconds) + ); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsNoResetOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsNoResetOption.java new file mode 100644 index 000000000..9116cac48 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsNoResetOption.java @@ -0,0 +1,56 @@ +/* + * 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; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsNoResetOption> extends + Capabilities, CanSetCapability { + String NO_RESET_OPTION = "noReset"; + + /** + * Set the app not to do a reset. + * + * @return self instance for chaining. + */ + default T noReset() { + return setNoReset(true); + } + + /** + * Set whether the app does not do a reset. + * + * @param bool is whether the app does not do a reset. + * @return self instance for chaining. + */ + default T setNoReset(boolean bool) { + return amend(NO_RESET_OPTION, bool); + } + + /** + * Get whether the app does not do a reset. + * + * @return true if the app does not do a reset. + */ + default Optional doesNoReset() { + return Optional.ofNullable(toSafeBoolean(getCapability(NO_RESET_OPTION))); + } +} 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 new file mode 100644 index 000000000..2f5ef1645 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsOrientationOption.java @@ -0,0 +1,52 @@ +/* + * 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 org.openqa.selenium.ScreenOrientation; + +import java.util.Optional; + +import static java.util.Locale.ROOT; + +public interface SupportsOrientationOption> extends + Capabilities, CanSetCapability { + String ORIENTATION_OPTION = "orientation"; + + /** + * Set the orientation of the screen. + * + * @param orientation is the screen orientation. + * @return self instance for chaining. + */ + default T setOrientation(ScreenOrientation orientation) { + return amend(ORIENTATION_OPTION, orientation.name()); + } + + /** + * Get the orientation of the screen. + * + * @return ScreenOrientation of the app. + */ + default Optional getOrientation() { + return Optional.ofNullable(getCapability(ORIENTATION_OPTION)) + .map(v -> v instanceof ScreenOrientation + ? (ScreenOrientation) v + : ScreenOrientation.valueOf((String.valueOf(v)).toUpperCase(ROOT)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsOtherAppsOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsOtherAppsOption.java new file mode 100644 index 000000000..fa08176bc --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsOtherAppsOption.java @@ -0,0 +1,45 @@ +/* + * 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 SupportsOtherAppsOption> extends + Capabilities, CanSetCapability { + String OTHER_APPS_OPTION = "otherApps"; + + /** + * Set the location of the app(s) to install before running a test. + * + * @param apps is the apps to install. + * @return self instance for chaining. + */ + default T setOtherApps(String apps) { + return amend(OTHER_APPS_OPTION, apps); + } + + /** + * Get the list of apps to install before running a test. + * + * @return String of apps to install. + */ + default Optional getOtherApps() { + return Optional.ofNullable((String) getCapability(OTHER_APPS_OPTION)); + } +} 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 new file mode 100644 index 000000000..63511c9b0 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsPageLoadStrategyOption.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.remote.options; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.PageLoadStrategy; + +import java.util.Optional; + +import static java.util.Locale.ROOT; + +public interface SupportsPageLoadStrategyOption> extends + Capabilities, CanSetCapability { + String PAGE_LOAD_STRATEGY_OPTION = "pageLoadStrategy"; + + /** + * Defines the current session’s page load strategy. + * + * @param strategy Page load strategy. + * @return self instance for chaining. + */ + default T setPageLoadStrategy(PageLoadStrategy strategy) { + return amend(PAGE_LOAD_STRATEGY_OPTION, strategy.toString()); + } + + /** + * Get the current session’s page load strategy. + * + * @return Page load strategy. + */ + default Optional getPageLoadStrategy() { + return Optional.ofNullable(getCapability(PAGE_LOAD_STRATEGY_OPTION)) + .map(String::valueOf) + .map(strategy -> strategy.toUpperCase(ROOT)) + .map(PageLoadStrategy::valueOf); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsPlatformVersionOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsPlatformVersionOption.java new file mode 100644 index 000000000..fbb319f32 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsPlatformVersionOption.java @@ -0,0 +1,45 @@ +/* + * 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 SupportsPlatformVersionOption> extends + Capabilities, CanSetCapability { + String PLATFORM_VERSION_OPTION = "platformVersion"; + + /** + * Set the version of the platform. + * + * @param version is the platform version. + * @return self instance for chaining. + */ + default T setPlatformVersion(String version) { + return amend(PLATFORM_VERSION_OPTION, version); + } + + /** + * Get the version of the platform. + * + * @return String representing the platform version. + */ + default Optional getPlatformVersion() { + return Optional.ofNullable((String) getCapability(PLATFORM_VERSION_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/ios/IOSOptions.java b/src/main/java/io/appium/java_client/remote/options/SupportsPostrunOption.java similarity index 66% rename from src/main/java/io/appium/java_client/ios/IOSOptions.java rename to src/main/java/io/appium/java_client/remote/options/SupportsPostrunOption.java index 14af6aad6..e055cb69f 100644 --- a/src/main/java/io/appium/java_client/ios/IOSOptions.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsPostrunOption.java @@ -14,19 +14,17 @@ * limitations under the License. */ -package io.appium.java_client.ios; +package io.appium.java_client.remote.options; -import io.appium.java_client.remote.MobileOptions; -import io.appium.java_client.remote.MobilePlatform; import org.openqa.selenium.Capabilities; -public class IOSOptions extends MobileOptions { - public IOSOptions() { - setPlatformName(MobilePlatform.IOS); - } +import java.util.Optional; - public IOSOptions(Capabilities source) { - this(); - merge(source); - } +public interface SupportsPostrunOption, S extends SystemScript> + extends Capabilities, CanSetCapability { + String POSTRUN_OPTION = "postrun"; + + T setPostrun(S script); + + Optional getPostrun(); } diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsPrerunOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsPrerunOption.java new file mode 100644 index 000000000..a44d2c53f --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsPrerunOption.java @@ -0,0 +1,30 @@ +/* + * 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 SupportsPrerunOption, S extends SystemScript> + extends Capabilities, CanSetCapability { + String PRERUN_OPTION = "prerun"; + + T setPrerun(S script); + + Optional getPrerun(); +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsPrintPageSourceOnFindFailureOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsPrintPageSourceOnFindFailureOption.java new file mode 100644 index 000000000..3d82738c5 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsPrintPageSourceOnFindFailureOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsPrintPageSourceOnFindFailureOption> extends + Capabilities, CanSetCapability { + String PRINT_PAGE_SOURCE_ON_FIND_FAILURE_OPTION = "printPageSourceOnFindFailure"; + + /** + * Set the app to print page source when a find operation fails. + * + * @return self instance for chaining. + */ + default T printPageSourceOnFindFailure() { + 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 self instance for chaining. + */ + default T setPrintPageSourceOnFindFailure(boolean bool) { + return amend(PRINT_PAGE_SOURCE_ON_FIND_FAILURE_OPTION, bool); + } + + /** + * Get whether the app to print page source when a find operation fails. + * + * @return true if app prints page source. + */ + default Optional doesPrintPageSourceOnFindFailure() { + return Optional.ofNullable( + toSafeBoolean(getCapability(PRINT_PAGE_SOURCE_ON_FIND_FAILURE_OPTION)) + ); + } +} 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 new file mode 100644 index 000000000..d69be4d2b --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsProxyOption.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.remote.options; + +import com.google.gson.Gson; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.Proxy; + +import java.util.Map; +import java.util.Optional; + +public interface SupportsProxyOption> extends + Capabilities, CanSetCapability { + String PROXY_OPTION = "proxy"; + + /** + * Defines the current session’s proxy configuration. + * + * @param proxy Session proxy config. + * @return self instance for chaining. + */ + default T setProxy(Proxy proxy) { + return amend(PROXY_OPTION, proxy.toJson()); + } + + /** + * Get the current session’s proxy configuration. + * + * @return Proxy config. + */ + default Optional getProxy() { + return Optional.ofNullable(getCapability(PROXY_OPTION)) + .map(String::valueOf) + .map(v -> new Gson().fromJson(v, Map.class)) + .map(Proxy::new); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsSetWindowRectOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsSetWindowRectOption.java new file mode 100644 index 000000000..046fb6cfa --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsSetWindowRectOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsSetWindowRectOption> extends + Capabilities, CanSetCapability { + String SET_WINDOW_RECT_OPTION = "setWindowRect"; + + /** + * Indicates whether the remote end supports all + * of the resizing and repositioning commands. + * + * @param bool True or false. + * @return self instance for chaining. + */ + default T setWindowRect(boolean bool) { + return amend(SET_WINDOW_RECT_OPTION, bool); + } + + /** + * Get whether the remote end supports all + * of the resizing and repositioning commands. + * + * @return true or false. + */ + default Optional doesSetWindowRect() { + return Optional.ofNullable(toSafeBoolean(getCapability(SET_WINDOW_RECT_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsSkipLogCaptureOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsSkipLogCaptureOption.java new file mode 100644 index 000000000..8da26009c --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsSkipLogCaptureOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +import static io.appium.java_client.internal.CapabilityHelpers.toSafeBoolean; + +public interface SupportsSkipLogCaptureOption> extends + Capabilities, CanSetCapability { + String SKIP_LOG_CAPTURE_OPTION = "skipLogCapture"; + + /** + * Skips capturing system logs. + * + * @return self instance for chaining. + */ + default T skipLogCapture() { + return amend(SKIP_LOG_CAPTURE_OPTION, true); + } + + /** + * Skips to start capturing system logs. It might improve network performance. + * Log-related commands won't work if the capability is enabled. Defaults to false. + * + * @param value Set it to true in order to skip logcat capture. + * @return self instance for chaining. + */ + default T setSkipLogCapture(boolean value) { + return amend(SKIP_LOG_CAPTURE_OPTION, value); + } + + /** + * Get whether to skip capturing system logs. + * + * @return True or false. + */ + default Optional doesSkipLogCapture() { + return Optional.ofNullable(toSafeBoolean(getCapability(SKIP_LOG_CAPTURE_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsUdidOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsUdidOption.java new file mode 100644 index 000000000..fc1364b3d --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsUdidOption.java @@ -0,0 +1,45 @@ +/* + * 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 SupportsUdidOption> extends + Capabilities, CanSetCapability { + String UDID_OPTION = "udid"; + + /** + * Set the id of the device. + * + * @param id is the unique device identifier. + * @return self instance, for chaining. + */ + default T setUdid(String id) { + return amend(UDID_OPTION, id); + } + + /** + * Get the id of the device. + * + * @return String representing the unique device identifier. + */ + default Optional getUdid() { + return Optional.ofNullable((String) getCapability(UDID_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/remote/options/SupportsUnhandledPromptBehaviorOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsUnhandledPromptBehaviorOption.java new file mode 100644 index 000000000..a6fded38a --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsUnhandledPromptBehaviorOption.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.remote.options; + +import org.openqa.selenium.Capabilities; + +import java.util.Optional; + +public interface SupportsUnhandledPromptBehaviorOption> extends + Capabilities, CanSetCapability { + String UNHANDLED_PROMPT_BEHAVIOR_OPTION = "unhandledPromptBehavior"; + + /** + * Defines the current session’s page load strategy. + * + * @param strategy Page load strategy. + * @return self instance for chaining. + */ + default T setUnhandledPromptBehavior(UnhandledPromptBehavior strategy) { + return amend(UNHANDLED_PROMPT_BEHAVIOR_OPTION, strategy.toString()); + } + + /** + * Get the current session’s page load strategy. + * + * @return Page load strategy. + */ + default Optional getUnhandledPromptBehavior() { + return Optional.ofNullable(getCapability(UNHANDLED_PROMPT_BEHAVIOR_OPTION)) + .map(String::valueOf) + .map(UnhandledPromptBehavior::fromString); + } +} 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/FindsByIosClassChain.java b/src/main/java/io/appium/java_client/remote/options/SystemScript.java similarity index 50% rename from src/main/java/io/appium/java_client/FindsByIosClassChain.java rename to src/main/java/io/appium/java_client/remote/options/SystemScript.java index 92482663a..901d8e220 100644 --- a/src/main/java/io/appium/java_client/FindsByIosClassChain.java +++ b/src/main/java/io/appium/java_client/remote/options/SystemScript.java @@ -14,19 +14,32 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.remote.options; -import org.openqa.selenium.WebElement; +import java.util.Map; +import java.util.Optional; -import java.util.List; +public abstract class SystemScript> extends BaseMapOptionData { + public SystemScript() { + } + + public SystemScript(Map options) { + super(options); + } -public interface FindsByIosClassChain extends FindsByFluentSelector { + public T withScript(String script) { + return assignOptionValue("script", script); + } + + public Optional getScript() { + return getOptionValue("script"); + } - default T findElementByIosClassChain(String using) { - return findElement(MobileSelector.IOS_CLASS_CHAIN.toString(), using); + public T withCommand(String command) { + return assignOptionValue("command", command); } - default List findElementsByIosClassChain(String using) { - return findElements(MobileSelector.IOS_CLASS_CHAIN.toString(), using); + public Optional getCommand() { + return getOptionValue("command"); } } 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 new file mode 100644 index 000000000..52c2ea9d5 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/UnhandledPromptBehavior.java @@ -0,0 +1,52 @@ +/* + * 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 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, + IGNORE; + + @Override + public String toString() { + return name().toLowerCase(ROOT).replace("_", " "); + } + + /** + * Converts the given value to an enum member. + * + * @param value The value to convert. + * @return Enum member. + * @throws IllegalArgumentException If the provided value cannot be matched. + */ + public static UnhandledPromptBehavior fromString(String value) { + return Arrays.stream(values()) + .filter(v -> v.toString().equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + String.format("Unhandled prompt behavior '%s' is not supported. " + + "The only supported values are: %s", value, + Arrays.stream(values()).map(UnhandledPromptBehavior::toString) + .collect(Collectors.joining(","))) + )); + } +} 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 new file mode 100644 index 000000000..09ff1680f --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/W3CCapabilityKeys.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.remote.options; + +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class W3CCapabilityKeys implements Predicate { + public static final W3CCapabilityKeys INSTANCE = new W3CCapabilityKeys(); + private static final Predicate ACCEPTED_W3C_PATTERNS = Stream.of( + "^[\\w-\\.]+:.*$", + "^acceptInsecureCerts$", + "^browserName$", + "^browserVersion$", + "^platformName$", + "^pageLoadStrategy$", + "^proxy$", + "^setWindowRect$", + "^strictFileInteractability$", + "^timeouts$", + "^unhandledPromptBehavior$", + "^webSocketUrl$") // from webdriver-bidi + .map(Pattern::compile) + .map(Pattern::asPredicate) + .reduce(identity -> false, Predicate::or); + + protected W3CCapabilityKeys() { + } + + @Override + public boolean test(String capabilityName) { + return ACCEPTED_W3C_PATTERNS.test(capabilityName); + } +} diff --git a/src/main/java/io/appium/java_client/safari/SafariDriver.java b/src/main/java/io/appium/java_client/safari/SafariDriver.java new file mode 100644 index 000000000..f8f10bb01 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/SafariDriver.java @@ -0,0 +1,151 @@ +/* + * 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.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; +import io.appium.java_client.service.local.AppiumServiceBuilder; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.Platform; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.HttpClient; + +import java.net.URL; + +/** + * GeckoDriver is an officially supported Appium driver + * created to automate mobile Safari browser. The driver uses W3C + * WebDriver protocol and is built on top of Apple's safaridriver + * server. Read https://github.com/appium/appium-safari-driver + * for more details on how to configure and use it. + * + * @since Appium 1.20.0 + */ +public class SafariDriver extends AppiumDriver { + private static final String PLATFORM_NAME = Platform.IOS.toString(); + private static final String AUTOMATION_NAME = AutomationName.SAFARI; + + public SafariDriver(HttpCommandExecutor executor, Capabilities capabilities) { + super(executor, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public SafariDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public SafariDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public SafariDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public SafariDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public SafariDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public SafariDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, + Capabilities capabilities) { + super(builder, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public SafariDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, ensurePlatformAndAutomationNames( + 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}. + * For example: + * + *
+     *
+     * ClientConfig clientConfig = ClientConfig.defaultConfig()
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * SafariOptions options = new SafariOptions();
+     * SafariDriver driver = new SafariDriver(clientConfig, options);
+     *
+     * 
+ * + * @param clientConfig take a look at {@link ClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public SafariDriver(ClientConfig clientConfig, Capabilities capabilities) { + 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)); + } + + public SafariDriver(Capabilities capabilities) { + super(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 new file mode 100644 index 000000000..9639509a1 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SafariOptions.java @@ -0,0 +1,74 @@ +/* + * 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.safari.options; + +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsAcceptInsecureCertsOption; +import io.appium.java_client.remote.options.SupportsBrowserNameOption; +import io.appium.java_client.remote.options.SupportsBrowserVersionOption; +import io.appium.java_client.remote.options.SupportsPageLoadStrategyOption; +import io.appium.java_client.remote.options.SupportsProxyOption; +import io.appium.java_client.remote.options.SupportsSetWindowRectOption; +import io.appium.java_client.remote.options.SupportsUnhandledPromptBehaviorOption; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.Platform; + +import java.util.Map; + +/** + * Provides options specific to the Safari Driver. + * + *

For more details, refer to the + * capabilities documentation

+ */ +public class SafariOptions extends BaseOptions implements + SupportsBrowserNameOption, + SupportsBrowserVersionOption, + SupportsSafariPlatformVersionOption, + SupportsSafariPlatformBuildVersionOption, + SupportsSafariUseSimulatorOption, + SupportsSafariDeviceTypeOption, + SupportsSafariDeviceNameOption, + SupportsSafariDeviceUdidOption, + SupportsSafariAutomaticInspectionOption, + SupportsSafariAutomaticProfilingOption, + SupportsWebkitWebrtcOption, + SupportsAcceptInsecureCertsOption, + SupportsPageLoadStrategyOption, + SupportsSetWindowRectOption, + SupportsProxyOption, + SupportsUnhandledPromptBehaviorOption { + public SafariOptions() { + setCommonOptions(); + } + + public SafariOptions(Capabilities source) { + super(source); + setCommonOptions(); + } + + public SafariOptions(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setPlatformName(Platform.IOS.toString()); + setAutomationName(AutomationName.SAFARI); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariAutomaticInspectionOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariAutomaticInspectionOption.java new file mode 100644 index 000000000..9b6e3ad29 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariAutomaticInspectionOption.java @@ -0,0 +1,61 @@ +/* + * 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.safari.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 SupportsSafariAutomaticInspectionOption> extends + Capabilities, CanSetCapability { + String SAFARI_AUTOMATIC_INSPECTION_OPTION = "safari:automaticInspection"; + + /** + * Enforces safaridriver to use Automatic Inspection. + * + * @return self instance for chaining. + */ + default T safariAutomaticInspection() { + return amend(SAFARI_AUTOMATIC_INSPECTION_OPTION, true); + } + + /** + * This capability instructs Safari to preload the Web Inspector and JavaScript + * debugger in the background prior to returning a newly-created window. + * To pause the test's execution in JavaScript and bring up Web Inspector's + * Debugger tab, you can simply evaluate a debugger; statement in the test page. + * + * @param bool Whether to use automatic inspection. + * @return self instance for chaining. + */ + default T setSafariAutomaticInspection(boolean bool) { + return amend(SAFARI_AUTOMATIC_INSPECTION_OPTION, bool); + } + + /** + * Get whether to use automatic inspection. + * + * @return true or false. + */ + default Optional doesSafariAutomaticInspection() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_AUTOMATIC_INSPECTION_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariAutomaticProfilingOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariAutomaticProfilingOption.java new file mode 100644 index 000000000..3fcc75215 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariAutomaticProfilingOption.java @@ -0,0 +1,61 @@ +/* + * 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.safari.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 SupportsSafariAutomaticProfilingOption> extends + Capabilities, CanSetCapability { + String SAFARI_AUTOMATIC_PROFILING_OPTION = "safari:automaticProfiling"; + + /** + * Enforces safaridriver to use Automatic Inspection. + * + * @return self instance for chaining. + */ + default T safariAutomaticProfiling() { + return amend(SAFARI_AUTOMATIC_PROFILING_OPTION, true); + } + + /** + * This capability instructs Safari to preload the Web Inspector and start + * a Timeline recording in the background prior to returning a newly-created + * window. To view the recording, open the Web Inspector through Safari's + * Develop menu. + * + * @param bool Whether to use automatic profiling. + * @return self instance for chaining. + */ + default T setSafariAutomaticProfiling(boolean bool) { + return amend(SAFARI_AUTOMATIC_PROFILING_OPTION, bool); + } + + /** + * Get whether to use automatic profiling. + * + * @return true or false. + */ + default Optional doesSafariAutomaticProfiling() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_AUTOMATIC_PROFILING_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceNameOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceNameOption.java new file mode 100644 index 000000000..4e814d01f --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceNameOption.java @@ -0,0 +1,52 @@ +/* + * 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.safari.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 SupportsSafariDeviceNameOption> extends + Capabilities, CanSetCapability { + String SAFARI_DEVICE_NAME_OPTION = "safari:deviceName"; + + /** + * safaridriver will only create a session using hosts whose device name + * matches the value of safari:deviceName. Device names are compared + * case-insensitively. NOTE: Device names for connected devices are shown in + * iTunes. If Xcode is installed, device names for connected devices are available + * via the output of instruments(1) and in the Devices and Simulators window + * (accessed in Xcode via "Window -> Devices and Simulators"). + * + * @param deviceName Device name. + * @return self instance for chaining. + */ + default T setSafariDeviceName(String deviceName) { + return amend(SAFARI_DEVICE_NAME_OPTION, deviceName); + } + + /** + * Get the name of the device. + * + * @return String representing the name of the device. + */ + default Optional getSafariDeviceName() { + return Optional.ofNullable((String) getCapability(SAFARI_DEVICE_NAME_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceTypeOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceTypeOption.java new file mode 100644 index 000000000..12179de61 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceTypeOption.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.safari.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 SupportsSafariDeviceTypeOption> extends + Capabilities, CanSetCapability { + String SAFARI_DEVICE_TYPE_OPTION = "safari:deviceType"; + + /** + * If the value of safari:deviceType is 'iPhone', safaridriver will only create a session + * using an iPhone device or iPhone simulator. If the value of safari:deviceType is 'iPad', + * safaridriver will only create a session using an iPad device or iPad simulator. + * Values of safari:deviceType are compared case-insensitively. + * + * @param deviceType Device type name. + * @return self instance for chaining. + */ + default T setSafariDeviceType(String deviceType) { + return amend(SAFARI_DEVICE_TYPE_OPTION, deviceType); + } + + /** + * Get the type of the device. + * + * @return String representing the type of the device. + */ + default Optional getSafariDeviceType() { + return Optional.ofNullable((String) getCapability(SAFARI_DEVICE_TYPE_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceUdidOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceUdidOption.java new file mode 100644 index 000000000..80ae57856 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariDeviceUdidOption.java @@ -0,0 +1,52 @@ +/* + * 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.safari.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 SupportsSafariDeviceUdidOption> extends + Capabilities, CanSetCapability { + String SAFARI_DEVICE_UDID_OPTION = "safari:deviceUdid"; + + /** + * safaridriver will only create a session using hosts whose device UDID + * matches the value of safari:deviceUDID. Device UDIDs are compared + * case-insensitively. NOTE: If Xcode is installed, UDIDs for connected + * devices are available via the output of instruments(1) and in the + * Devices and Simulators window (accessed in Xcode via + * "Window -> Devices and Simulators"). + * + * @param deviceUdid Device UDID. + * @return self instance for chaining. + */ + default T setSafariDeviceUdid(String deviceUdid) { + return amend(SAFARI_DEVICE_UDID_OPTION, deviceUdid); + } + + /** + * Get the UDID of the device. + * + * @return String representing the UDID of the device. + */ + default Optional getSafariDeviceUdid() { + return Optional.ofNullable((String) getCapability(SAFARI_DEVICE_UDID_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariPlatformBuildVersionOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariPlatformBuildVersionOption.java new file mode 100644 index 000000000..d73993fe5 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariPlatformBuildVersionOption.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.safari.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 SupportsSafariPlatformBuildVersionOption> extends + Capabilities, CanSetCapability { + String SAFARI_PLATFORM_BUILD_VERSION_OPTION = "safari:platformBuildVersion"; + + /** + * safaridriver will only create a session using hosts whose OS build + * version matches the value of safari:platformBuildVersion. Example + * of a macOS build version is '18E193'. On macOS, the OS build version + * can be determined by running the sw_vers(1) utility. + * + * @param version is the platform build version. + * @return self instance for chaining. + */ + default T setSafariPlatformBuildVersion(String version) { + return amend(SAFARI_PLATFORM_BUILD_VERSION_OPTION, version); + } + + /** + * Get the build version of the platform. + * + * @return String representing the platform build version. + */ + default Optional getSafariPlatformBuildVersion() { + return Optional.ofNullable((String) getCapability(SAFARI_PLATFORM_BUILD_VERSION_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariPlatformVersionOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariPlatformVersionOption.java new file mode 100644 index 000000000..f6800b310 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariPlatformVersionOption.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.safari.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 SupportsSafariPlatformVersionOption> extends + Capabilities, CanSetCapability { + String SAFARI_PLATFORM_VERSION_OPTION = "safari:platformVersion"; + + /** + * safaridriver will only create a session using hosts whose OS + * version matches the value of safari:platformVersion. OS version + * numbers are prefix-matched. For example, if the value of safari:platformVersion + * is '12', this will allow hosts with an OS version of '12.0' or '12.1' but not '10.12'. + * + * @param version is the platform version. + * @return self instance for chaining. + */ + default T setSafariPlatformVersion(String version) { + return amend(SAFARI_PLATFORM_VERSION_OPTION, version); + } + + /** + * Get the version of the platform. + * + * @return String representing the platform version. + */ + default Optional getSafariPlatformVersion() { + return Optional.ofNullable((String) getCapability(SAFARI_PLATFORM_VERSION_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsSafariUseSimulatorOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsSafariUseSimulatorOption.java new file mode 100644 index 000000000..948ef0a45 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsSafariUseSimulatorOption.java @@ -0,0 +1,61 @@ +/* + * 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.safari.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 SupportsSafariUseSimulatorOption> extends + Capabilities, CanSetCapability { + String SAFARI_USE_SIMULATOR_OPTION = "safari:useSimulator"; + + /** + * Enforces safaridriver to use iOS Simulator. + * + * @return self instance for chaining. + */ + default T safariUseSimulator() { + return amend(SAFARI_USE_SIMULATOR_OPTION, true); + } + + /** + * If the value of safari:useSimulator is true, safaridriver will only use + * iOS Simulator hosts. If the value of safari:useSimulator is false, safaridriver + * will not use iOS Simulator hosts. NOTE: An Xcode installation is required + * in order to run WebDriver tests on iOS Simulator hosts. + * + * @param bool is whether to use iOS Simulator. + * @return self instance for chaining. + */ + default T setSafariUseSimulator(boolean bool) { + return amend(SAFARI_USE_SIMULATOR_OPTION, bool); + } + + /** + * Get whether to use iOS Simulator. + * + * @return true or false. + */ + default Optional doesSafariUseSimulator() { + return Optional.ofNullable(toSafeBoolean(getCapability(SAFARI_USE_SIMULATOR_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/SupportsWebkitWebrtcOption.java b/src/main/java/io/appium/java_client/safari/options/SupportsWebkitWebrtcOption.java new file mode 100644 index 000000000..e22309f65 --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/SupportsWebkitWebrtcOption.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.safari.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.Map; +import java.util.Optional; + +public interface SupportsWebkitWebrtcOption> extends + Capabilities, CanSetCapability { + String WEBKIT_WEB_RTC_OPTION = "webkit:WebRTC"; + + /** + * This capability allows a test to temporarily change Safari's policies + * for WebRTC and Media Capture. + * + * @param webrtcData WebRTC policies. + * @return self instance for chaining. + */ + default T setWebkitWebrtc(WebrtcData webrtcData) { + return amend(WEBKIT_WEB_RTC_OPTION, webrtcData.toMap()); + } + + /** + * Get WebRTC policies. + * + * @return WebRTC policies. + */ + default Optional getWebkitWebrtc() { + //noinspection unchecked + return Optional.ofNullable((Map) getCapability(WEBKIT_WEB_RTC_OPTION)) + .map(WebrtcData::new); + } +} diff --git a/src/main/java/io/appium/java_client/safari/options/WebrtcData.java b/src/main/java/io/appium/java_client/safari/options/WebrtcData.java new file mode 100644 index 000000000..b1343432e --- /dev/null +++ b/src/main/java/io/appium/java_client/safari/options/WebrtcData.java @@ -0,0 +1,77 @@ +/* + * 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.safari.options; + +import io.appium.java_client.remote.options.BaseMapOptionData; + +import java.util.Map; +import java.util.Optional; + +public class WebrtcData extends BaseMapOptionData { + public WebrtcData() { + } + + public WebrtcData(Map options) { + super(options); + } + + /** + * Normally, Safari refuses to allow media capture over insecure connections. + * This restriction is relaxed by default for WebDriver sessions for testing + * purposes (for example, a test web server not configured for HTTPS). When + * this capability is specified, Safari will revert to the normal behavior of + * preventing media capture over insecure connections. + * + * @param disabled True to disable insecure media capture. + * @return self instance for chaining. + */ + public WebrtcData withDisableInsecureMediaCapture(boolean disabled) { + return assignOptionValue("DisableInsecureMediaCapture", disabled); + } + + /** + * Get whether to disable insecure media capture. + * + * @return True or false. + */ + public Optional doesDisableInsecureMediaCapture() { + return getOptionValue("DisableInsecureMediaCapture"); + } + + /** + * To protect a user's privacy, Safari normally filters out WebRTC + * ICE candidates that correspond to internal network addresses when + * capture devices are not in use. This capability suppresses ICE candidate + * filtering so that both internal and external network addresses are + * always sent as ICE candidates. + * + * @param disabled True to disable ICE candidates filtering. + * @return self instance for chaining. + */ + public WebrtcData withDisableIceCandidateFiltering(boolean disabled) { + return assignOptionValue("DisableICECandidateFiltering", disabled); + } + + /** + * Get whether to disable ICE candidates filtering. + * + * @return True or false. + */ + public Optional doesDisableIceCandidateFiltering() { + return getOptionValue("DisableICECandidateFiltering"); + } +} 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 3e82e0de4..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,70 +16,71 @@ 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 com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -import org.apache.commons.lang3.StringUtils; - -import org.openqa.selenium.net.UrlChecker; -import org.openqa.selenium.os.CommandLine; +import lombok.Getter; +import lombok.SneakyThrows; +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.os.ExternalProcess; import org.openqa.selenium.remote.service.DriverService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.lang.reflect.Field; -import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; +import java.util.Map; +import java.util.Optional; 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/wd/hub"; + 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 ImmutableList nodeJSArgs; - private final ImmutableMap nodeJSEnvironment; - private final long startupTimeout; - private final TimeUnit timeUnit; + private final List nodeJSArgs; + private final Map nodeJSEnvironment; + 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; - - private CommandLine process = null; + @Getter + private String basePath; - AppiumDriverLocalService(String ipAddress, File nodeJSExec, int nodeJSPort, - ImmutableList nodeJSArgs, ImmutableMap nodeJSEnvironment, - long startupTimeout, TimeUnit timeUnit) throws IOException { - super(nodeJSExec, nodeJSPort, nodeJSArgs, nodeJSEnvironment); + private ExternalProcess process = null; + + AppiumDriverLocalService(String ipAddress, File nodeJSExec, + int nodeJSPort, Duration startupTimeout, + List nodeJSArgs, Map nodeJSEnvironment + ) throws IOException { + super(nodeJSExec, nodeJSPort, startupTimeout, nodeJSArgs, nodeJSEnvironment); this.nodeJSExec = nodeJSExec; this.nodeJSArgs = nodeJSArgs; this.nodeJSEnvironment = nodeJSEnvironment; this.startupTimeout = startupTimeout; - this.timeUnit = timeUnit; this.url = new URL(String.format(URL_MASK, ipAddress, nodeJSPort)); } @@ -91,6 +92,22 @@ public static AppiumDriverLocalService buildService(AppiumServiceBuilder builder return builder.build(); } + public AppiumDriverLocalService withBasePath(String basePath) { + this.basePath = basePath; + return this; + } + + @SneakyThrows + private static URL addSuffix(URL url, String suffix) { + return url.toURI().resolve("." + (suffix.startsWith("/") ? suffix : "/" + suffix)).toURL(); + } + + @SneakyThrows + @SuppressWarnings("SameParameterValue") + private static URL replaceHost(URL source, String oldHost, String newHost) { + return new URL(source.toString().replaceFirst(oldHost, newHost)); + } + /** * Base URL. * @@ -98,47 +115,55 @@ public static AppiumDriverLocalService buildService(AppiumServiceBuilder builder */ @Override public URL getUrl() { - return url; + return basePath == null ? url : addSuffix(url, basePath); } @Override public boolean isRunning() { lock.lock(); try { - if (process == null) { - return false; - } - - if (!process.isRunning()) { + if (process == null || !process.isAlive()) { return false; } try { - ping(1500, TimeUnit.MILLISECONDS); - 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(long time, TimeUnit timeUnit) throws UrlChecker.TimeoutException, MalformedURLException { - // The operating system might block direct access to the universal broadcast IP address - URL status = new URL(url.toString().replace(BROADCAST_IP_ADDRESS, "127.0.0.1") + "/status"); - new UrlChecker().waitUntilAvailable(time, timeUnit, 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 { @@ -147,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(); - ping(startupTimeout, timeUnit); - } 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"; - } - } + 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); + } - throw new AppiumServerHasNotBeenStartedLocallyException(msgTxt, e); + var didPingSucceed = false; + try { + ping(startupTimeout); + 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(); + } } } 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. @@ -191,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); } /** @@ -241,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); } /** @@ -255,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); } @@ -266,12 +300,22 @@ 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); } } + /** + * Remove the outputStream which is receiving server output data. + * + * @return the outputStream has been removed if it is present + */ + public Optional removeOutPutStream(OutputStream outputStream) { + requireNonNull(outputStream, "outputStream parameter is NULL!"); + return stream.remove(outputStream); + } + /** * Remove all existing server output streams. * @@ -353,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; } @@ -387,18 +430,17 @@ 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() { - StringBuilder lineBuilder = new StringBuilder(); + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @Override public void write(int chr) { try { - lineBuilder.append((char) chr); - Matcher matcher = LOG_MESSAGE_PATTERN.matcher(lineBuilder.toString()); - if (matcher.matches()) { - consumer.accept(matcher.group(1)); - lineBuilder = new StringBuilder(); + outputStream.write(chr); + if (chr == '\n') { + consumer.accept(outputStream.toString()); + outputStream.reset(); } } catch (Exception e) { // log error and continue 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 caef4d36a..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,43 +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.common.collect.ImmutableMap; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import io.appium.java_client.android.options.context.SupportsChromedriverExecutableOption; +import io.appium.java_client.android.options.signing.SupportsKeystoreOptions; +import io.appium.java_client.remote.MobileBrowserType; +import io.appium.java_client.remote.options.SupportsAppOption; +import io.appium.java_client.service.local.flags.GeneralServerFlag; import io.appium.java_client.service.local.flags.ServerArgument; - -import 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 lombok.SneakyThrows; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; +import org.openqa.selenium.Platform; import org.openqa.selenium.os.ExecutableFinder; -import org.openqa.selenium.remote.BrowserType; -import org.openqa.selenium.remote.DesiredCapabilities; +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.concurrent.TimeUnit; 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 { @@ -70,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 DesiredCapabilities capabilities; - private static final Function APPIUM_JS_NOT_EXIST_ERROR = (fullPath) -> String.format( + 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( "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()); - // The first starting is slow sometimes on some environment - private long startupTimeout = 120; - private TimeUnit timeUnit = TimeUnit.SECONDS; + 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); @@ -110,8 +114,8 @@ public int score(Capabilities capabilities) { } String browserName = capabilities.getBrowserName(); - if (browserName.equals(BrowserType.CHROME) || browserName.equals(BrowserType.ANDROID) - || browserName.equals(BrowserType.SAFARI)) { + if (Browser.CHROME.is(browserName) || browserName.equalsIgnoreCase(MobileBrowserType.ANDROID) + || Browser.SAFARI.is(browserName)) { score++; } @@ -140,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); @@ -159,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)); @@ -201,7 +204,7 @@ public AppiumServiceBuilder withArgument(ServerArgument argument) { * @return the self-reference. */ public AppiumServiceBuilder withArgument(ServerArgument argument, String value) { - String argName = argument.getArgument().trim().toLowerCase(); + String argName = argument.getArgument(); switch (argName) { case "--port": case "-p": @@ -215,6 +218,9 @@ public AppiumServiceBuilder withArgument(ServerArgument argument, String value) case "-g": withLogFile(new File(value)); break; + case "--base-path": + serverArguments.put(argName, sanitizeBasePath(value)); + break; default: serverArguments.put(argName, value); break; @@ -222,23 +228,42 @@ public AppiumServiceBuilder withArgument(ServerArgument argument, String value) return this; } + private static String sanitizeBasePath(String basePath) { + 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 + "/"; + } + /** - * Adds a desired capabilities. + * Adds capabilities. * - * @param capabilities is an instance of {@link DesiredCapabilities}. + * @param capabilities is an instance of {@link Capabilities}. * @return the self-reference. */ - public AppiumServiceBuilder withCapabilities(DesiredCapabilities capabilities) { - if (this.capabilities == null) { - this.capabilities = capabilities; - } else { - DesiredCapabilities desiredCapabilities = new DesiredCapabilities(); - desiredCapabilities.merge(this.capabilities).merge(capabilities); - this.capabilities = desiredCapabilities; - } + public AppiumServiceBuilder withCapabilities(Capabilities capabilities) { + this.capabilities = (this.capabilities == null ? capabilities : this.capabilities) + .merge(capabilities); return this; } + /** + * Adds capabilities. + * + * @param capabilities is an instance of {@link Capabilities}. + * @param autoQuoteCapabilitiesOnWindows automatically escape quote all + * capabilities when calling appium. + * This is required on windows systems only. + * @return the self-reference. + */ + public AppiumServiceBuilder withCapabilities(Capabilities capabilities, + boolean autoQuoteCapabilitiesOnWindows) { + this.autoQuoteCapabilitiesOnWindows = autoQuoteCapabilitiesOnWindows; + return withCapabilities(capabilities); + } + /** * Sets an executable appium.js. * @@ -256,28 +281,13 @@ public AppiumServiceBuilder withIPAddress(String ipAddress) { return this; } - /** - * Sets start up timeout. - * - * @param time a time value for the service starting up. - * @param timeUnit a time unit for the service starting up. - * @return self-reference. - */ - public AppiumServiceBuilder withStartUpTimeOut(long time, TimeUnit timeUnit) { - checkNotNull(timeUnit); - checkArgument(time > 0, "Time value should be greater than zero", time); - this.startupTimeout = time; - this.timeUnit = timeUnit; - return this; - } - @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() { @@ -296,7 +306,46 @@ private void loadPathToMainScript() { this.appiumJS = findMainScript(); } + private String capabilitiesToQuotedCmdlineArg() { + if (capabilities == null) { + return "{}"; + } + StringBuilder result = new StringBuilder(); + Map capabilitiesMap = capabilities.asMap(); + Set> entries = capabilitiesMap.entrySet(); + + for (Map.Entry entry : entries) { + Object value = entry.getValue(); + + if (value == null) { + continue; + } + + if (value instanceof String) { + String valueString = (String) value; + if (PATH_CAPABILITIES.contains(entry.getKey())) { + value = "\\\"" + valueString.replace("\\", "/") + "\\\""; + } else { + value = "\\\"" + valueString + "\\\""; + } + } else { + value = String.valueOf(value); + } + + String key = "\\\"" + entry.getKey() + "\\\""; + if (result.length() > 0) { + result.append(", "); + } + result.append(key).append(": ").append(value); + } + + return "{" + result + "}"; + } + private String capabilitiesToCmdlineArg() { + if (autoQuoteCapabilitiesOnWindows && Platform.getCurrent().is(Platform.WINDOWS)) { + return capabilitiesToQuotedCmdlineArg(); + } Gson gson = new GsonBuilder() .disableHtmlEscaping() .serializeNulls() @@ -308,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); @@ -338,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); } } @@ -353,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()); + } } /** @@ -412,15 +461,15 @@ public AppiumServiceBuilder withLogFile(File logFile) { return super.withLogFile(logFile); } + @SneakyThrows @Override protected AppiumDriverLocalService createDriverService(File nodeJSExecutable, int nodeJSPort, - ImmutableList nodeArguments, - ImmutableMap nodeEnvironment) { - try { - return new AppiumDriverLocalService(ipAddress, nodeJSExecutable, nodeJSPort, - nodeArguments, nodeEnvironment, startupTimeout, timeUnit); - } catch (IOException e) { - throw new RuntimeException(e); - } + Duration startupTimeout, + List nodeArguments, + Map nodeEnvironment) { + String basePath = serverArguments.getOrDefault( + GeneralServerFlag.BASEPATH.getArgument(), serverArguments.get("-pa")); + return new AppiumDriverLocalService(ipAddress, nodeJSExecutable, nodeJSPort, startupTimeout, nodeArguments, + nodeEnvironment).withBasePath(basePath); } } 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 6586d5cc1..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 @@ -20,6 +20,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Optional; class ListOutputStream extends OutputStream { @@ -30,6 +31,10 @@ ListOutputStream add(OutputStream stream) { return this; } + Optional remove(OutputStream stream) { + return streams.remove(stream) ? Optional.of(stream) : Optional.empty(); + } + @Override public void write(int i) throws IOException { for (OutputStream stream : streams) { stream.write(i); @@ -48,12 +53,14 @@ ListOutputStream add(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 d59ff750d..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 @@ -16,7 +16,6 @@ package io.appium.java_client.service.local.flags; - /** * Here is the list of common Appium server arguments. */ @@ -39,12 +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 - */ - PRE_LAUNCH("--pre-launch"), /** * The message log level to be shown. * Sample: --log-level debug @@ -75,14 +68,6 @@ public enum GeneralServerFlag implements ServerArgument { * --nodeconfig /abs/path/to/nodeconfig.json */ CONFIGURATION_FILE("--nodeconfig"), - /** - * IP Address of robot. Sample: --robot-address 0.0.0.0 - */ - ROBOT_ADDRESS("--robot-address"), - /** - * Port for robot. Sample: --robot-port 4242 - */ - ROBOT_PORT("--robot-port"), /** * Show info about the Appium server configuration and exit. Default: false */ @@ -133,7 +118,28 @@ public enum GeneralServerFlag implements ServerArgument { * Default: [] * Sample: --deny-insecure=foo,bar */ - DENY_INSECURE("--deny-insecure"); + DENY_INSECURE("--deny-insecure"), + /** + * Plugins are little programs which can be added to an Appium installation and activated, for the purpose of + * extending or modifying the behavior of pretty much any aspect of Appium. + * Plugins are available with Appium as of Appium 2.0. + * To activate all plugins, you can use the single string "all" as the value (e.g --plugins=all) + * Default: [] + * Sample: --use-plugins=device-farm,images + */ + USE_PLUGINS("--use-plugins"), + /** + * A comma-separated list of installed driver names that should be active for this server. + * All drivers will be active by default. + * Default: [] + * Sample: --use-drivers=uiautomator2,xcuitest + */ + USE_DRIVERS("--use-drivers"), + /** + * Base path to use as the prefix for all webdriver routes running on this server. + * Sample: --base-path=/wd/hub + */ + BASEPATH("--base-path"); private final String arg; 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 ede90103c..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,16 +1,17 @@ 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.internal.HasIdentity; +import org.openqa.selenium.remote.RemoteWebElement; 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,11 +83,11 @@ 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 " - + "implements org.openqa.selenium.internal.HasIdentity", - element instanceof HasIdentity); - elementId = ((HasIdentity) element).getId(); + + "extends org.openqa.selenium.remote.RemoteWebElement", + element instanceof RemoteWebElement); + elementId = ((RemoteWebElement) element).getId(); return this; } 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/PressesKeyCode.java b/src/main/java/io/appium/java_client/windows/PressesKeyCode.java deleted file mode 100644 index e6286d894..000000000 --- a/src/main/java/io/appium/java_client/windows/PressesKeyCode.java +++ /dev/null @@ -1,66 +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.windows; - -import static io.appium.java_client.MobileCommand.longPressKeyCodeCommand; -import static io.appium.java_client.MobileCommand.pressKeyCodeCommand; - -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; - -public interface PressesKeyCode extends ExecutesMethod { - - /** - * Send a key event to the device. - * - * @param key code for the key pressed on the device. - */ - default void pressKeyCode(int key) { - CommandExecutionHelper.execute(this, pressKeyCodeCommand(key)); - } - - /** - * Send a key event along with an Android metastate to an Android device. - * Metastates are things like *shift* to get uppercase characters. - * - * @param key code for the key pressed on the Android device. - * @param metastate metastate for the keypress. - */ - default void pressKeyCode(int key, Integer metastate) { - CommandExecutionHelper.execute(this, pressKeyCodeCommand(key, metastate)); - } - - /** - * Send a long key event to the device. - * - * @param key code for the key pressed on the device. - */ - default void longPressKeyCode(int key) { - CommandExecutionHelper.execute(this, longPressKeyCodeCommand(key)); - } - - /** - * Send a long key event along with an Android metastate to an Android device. - * Metastates are things like *shift* to get uppercase characters. - * - * @param key code for the key pressed on the Android device. - * @param metastate metastate for the keypress. - */ - default void longPressKeyCode(int key, Integer metastate) { - CommandExecutionHelper.execute(this, longPressKeyCodeCommand(key, metastate)); - } -} 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 3c8126ac1..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,60 +16,139 @@ package io.appium.java_client.windows; -import static io.appium.java_client.remote.MobilePlatform.WINDOWS; - +import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; -import io.appium.java_client.FindsByWindowsAutomation; -import io.appium.java_client.HidesKeyboardWithKeyName; +import io.appium.java_client.MobileCommand; +import io.appium.java_client.PerformsTouchActions; +import io.appium.java_client.PullsFiles; +import io.appium.java_client.PushesFiles; +import io.appium.java_client.remote.AutomationName; import io.appium.java_client.screenrecording.CanRecordScreen; 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.WebElement; +import org.openqa.selenium.Platform; import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; import org.openqa.selenium.remote.http.HttpClient; import java.net.URL; -public class WindowsDriver - extends AppiumDriver implements PressesKeyCode, HidesKeyboardWithKeyName, - FindsByWindowsAutomation, CanRecordScreen { +public class WindowsDriver extends AppiumDriver implements + PerformsTouchActions, + PullsFiles, + PushesFiles, + CanRecordScreen { + private static final String PLATFORM_NAME = Platform.WINDOWS.name(); + private static final String AUTOMATION_NAME = AutomationName.WINDOWS; public WindowsDriver(HttpCommandExecutor executor, Capabilities capabilities) { - super(executor, updateDefaultPlatformName(capabilities, WINDOWS)); + super(executor, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } - public WindowsDriver(URL remoteAddress, Capabilities desiredCapabilities) { - super(remoteAddress, updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + public WindowsDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } - public WindowsDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities desiredCapabilities) { - super(remoteAddress, httpClientFactory, updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + public WindowsDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } - public WindowsDriver(AppiumDriverLocalService service, Capabilities desiredCapabilities) { - super(service, updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + public WindowsDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } public WindowsDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(service, httpClientFactory, updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } - public WindowsDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) { - super(builder, updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + public WindowsDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } public WindowsDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(builder, httpClientFactory, updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + Capabilities capabilities) { + super(builder, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); + } + + public WindowsDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, ensurePlatformAndAutomationNames( + 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}. + * For example: + * + *
+     *
+     * ClientConfig clientConfig = ClientConfig.defaultConfig()
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * WindowsOptions options = new WindowsOptions();
+     * WindowsDriver driver = new WindowsDriver(clientConfig, options);
+     *
+     * 
+ * + * @param clientConfig take a look at {@link ClientConfig} + * @param capabilities take a look at {@link Capabilities} + * + */ + public WindowsDriver(ClientConfig clientConfig, Capabilities capabilities) { + 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)); + } + + public WindowsDriver(Capabilities capabilities) { + super(ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } - public WindowsDriver(HttpClient.Factory httpClientFactory, Capabilities desiredCapabilities) { - super(httpClientFactory, updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + /** + * Launch the application app under test after it was closed. + */ + public void launchApp() { + execute(MobileCommand.LAUNCH_APP); } - public WindowsDriver(Capabilities desiredCapabilities) { - super(updateDefaultPlatformName(desiredCapabilities, WINDOWS)); + /** + * Close the app under test. + */ + public void closeApp() { + execute(MobileCommand.CLOSE_APP); } } diff --git a/src/main/java/io/appium/java_client/windows/WindowsKeyCode.java b/src/main/java/io/appium/java_client/windows/WindowsKeyCode.java deleted file mode 100644 index c2968291f..000000000 --- a/src/main/java/io/appium/java_client/windows/WindowsKeyCode.java +++ /dev/null @@ -1,41 +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.windows; - -/** - * Created by STikhomirov on 15.12.2016. - */ -public interface WindowsKeyCode { - int POWER = 0; - int WINDOWS = 1; - int VOLUME_UP = 2; - int VOLUME_DOWN = 3; - int ROTATION_LOCK = 4; - int COUNT_MIN = 5; - int BACK = 5; - int SEARCH = 6; - int CAMERA_FOCUS = 7; - int CAMERA_SHUTTER = 8; - int RINGER_TOGGLE = 9; - int HEAD_SET = 10; - int HWKB_DPLOY = 11; - int CAMERA_LENS = 12; - int OEM_CUSTOM = 13; - int OEM_CUSTOM2 = 14; - int OEM_CUSTOM3 = 15; - int COUNT = 16; -} 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/PowerShellData.java b/src/main/java/io/appium/java_client/windows/options/PowerShellData.java new file mode 100644 index 000000000..6dc97f495 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/PowerShellData.java @@ -0,0 +1,73 @@ +/* + * 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.windows.options; + +import io.appium.java_client.remote.options.SystemScript; + +import java.util.Map; +import java.util.Optional; + +public class PowerShellData extends SystemScript { + public PowerShellData() { + } + + public PowerShellData(Map options) { + super(options); + } + + /** + * Allows to provide a multiline PowerShell script. + * + * @param script A valid PowerShell script. + * @return self instance for chaining. + */ + @Override + public PowerShellData withScript(String script) { + return super.withScript(script); + } + + /** + * Get a multiline PowerShell script. + * + * @return PowerShell script. + */ + @Override + public Optional getScript() { + return super.getScript(); + } + + /** + * Allows to provide a single-line PowerShell script. + * + * @param command A valid PowerShell script. + * @return self instance for chaining. + */ + @Override + public PowerShellData withCommand(String command) { + return super.withCommand(command); + } + + /** + * Get a single-line PowerShell script. + * + * @return PowerShell script. + */ + @Override + public Optional getCommand() { + return super.getCommand(); + } +} 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 new file mode 100644 index 000000000..1b4465a88 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/SupportsAppArgumentsOption.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.windows.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 SupportsAppArgumentsOption> extends + Capabilities, CanSetCapability { + String APP_ARGUMENTS_OPTION = "appArguments"; + + /** + * Application arguments string. + * + * @param args E.g. "/?" + * @return self instance for chaining. + */ + default T setAppArguments(String args) { + return amend(APP_ARGUMENTS_OPTION, args); + } + + /** + * Get application arguments. + * + * @return Application arguments. + */ + default Optional setAppArguments() { + return Optional.ofNullable((String) getCapability(APP_ARGUMENTS_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/windows/options/SupportsAppTopLevelWindowOption.java b/src/main/java/io/appium/java_client/windows/options/SupportsAppTopLevelWindowOption.java new file mode 100644 index 000000000..8490398f1 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/SupportsAppTopLevelWindowOption.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.windows.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 SupportsAppTopLevelWindowOption> extends + Capabilities, CanSetCapability { + String APP_TOP_LEVEL_WINDOW_OPTION = "appTopLevelWindow"; + + /** + * Set the hexadecimal handle of an existing application top level + * window to attach to, for example 0x12345 (should be of string type). + * Either this capability or app one must be provided on session startup. + * + * @param identifier E.g. "0x12345". + * @return self instance for chaining. + */ + default T setAppTopLevelWindow(String identifier) { + return amend(APP_TOP_LEVEL_WINDOW_OPTION, identifier); + } + + /** + * Get the hexadecimal handle of an existing application top level + * window to attach to. + * + * @return Top level window handle. + */ + default Optional getAppTopLevelWindow() { + return Optional.ofNullable((String) getCapability(APP_TOP_LEVEL_WINDOW_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/windows/options/SupportsAppWorkingDirOption.java b/src/main/java/io/appium/java_client/windows/options/SupportsAppWorkingDirOption.java new file mode 100644 index 000000000..bc3e1e074 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/SupportsAppWorkingDirOption.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.windows.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 SupportsAppWorkingDirOption> extends + Capabilities, CanSetCapability { + String APP_WORKING_DIR_OPTION = "appWorkingDir"; + + /** + * Full path to the folder, which is going to be set as the working + * dir for the application under test. This is only applicable for classic apps. + * + * @param path Existing folder path on the server file system. + * @return self instance for chaining. + */ + default T setAppWorkingDir(String path) { + return amend(APP_WORKING_DIR_OPTION, path); + } + + /** + * Get the full path to the folder, which is going to be set as the working + * dir for the application under test. + * + * @return Folder path on the server file system. + */ + default Optional getAppWorkingDir() { + return Optional.ofNullable((String) getCapability(APP_WORKING_DIR_OPTION)); + } +} diff --git a/src/main/java/io/appium/java_client/windows/options/SupportsCreateSessionTimeoutOption.java b/src/main/java/io/appium/java_client/windows/options/SupportsCreateSessionTimeoutOption.java new file mode 100644 index 000000000..334209fe8 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/SupportsCreateSessionTimeoutOption.java @@ -0,0 +1,55 @@ +/* + * 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.windows.options; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsCreateSessionTimeoutOption> extends + Capabilities, CanSetCapability { + String CREATE_SESSION_TIMEOUT_OPTION = "createSessionTimeout"; + + /** + * Set the timeout used to retry Appium Windows Driver session startup. + * This capability could be used as a workaround for the long startup times + * of UWP applications (aka Failed to locate opened application window + * with appId: TestCompany.my_app4!App, and processId: 8480). Default value is 20000ms. + * + * @param timeout The timeout value. + * @return self instance for chaining. + */ + default T setCreateSessionTimeout(Duration timeout) { + return amend(CREATE_SESSION_TIMEOUT_OPTION, timeout.toMillis()); + } + + /** + * Get the timeout used to retry Appium Windows Driver session startup. + * + * @return The timeout value. + */ + default Optional getCreateSessionTimeout() { + return Optional.ofNullable( + toDuration(getCapability(CREATE_SESSION_TIMEOUT_OPTION)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/windows/options/SupportsMsExperimentalWebDriverOption.java b/src/main/java/io/appium/java_client/windows/options/SupportsMsExperimentalWebDriverOption.java new file mode 100644 index 000000000..a4415707a --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/SupportsMsExperimentalWebDriverOption.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.windows.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 SupportsMsExperimentalWebDriverOption> extends + Capabilities, CanSetCapability { + String MS_EXPERIMENTAL_WEBDRIVER_OPTION = "ms:experimental-webdriver"; + + /** + * Enforce to enable experimental driver features and optimizations. + * + * @return self instance for chaining. + */ + default T experimentalWebDriver() { + return amend(MS_EXPERIMENTAL_WEBDRIVER_OPTION, true); + } + + /** + * Enables experimental features and optimizations. See Appium Windows + * Driver release notes for more details on this capability. false by default. + * + * @param value Whether to enable experimental features and optimizations. + * @return self instance for chaining. + */ + default T setExperimentalWebDriver(boolean value) { + return amend(MS_EXPERIMENTAL_WEBDRIVER_OPTION, value); + } + + /** + * Get whether to enable experimental features and optimizations. + * + * @return True or false. + */ + default Optional isExperimentalWebDriver() { + return Optional.ofNullable(toSafeBoolean(getCapability(MS_EXPERIMENTAL_WEBDRIVER_OPTION))); + } +} diff --git a/src/main/java/io/appium/java_client/windows/options/SupportsMsWaitForAppLaunchOption.java b/src/main/java/io/appium/java_client/windows/options/SupportsMsWaitForAppLaunchOption.java new file mode 100644 index 000000000..2066616ce --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/SupportsMsWaitForAppLaunchOption.java @@ -0,0 +1,55 @@ +/* + * 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.windows.options; + +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; + +import static io.appium.java_client.internal.CapabilityHelpers.toDuration; + +public interface SupportsMsWaitForAppLaunchOption> extends + Capabilities, CanSetCapability { + String MS_WAIT_FOR_APP_LAUNCH_OPTION = "ms:waitForAppLaunch"; + + /** + * Similar to createSessionTimeout, but is + * applied on the server side. Enables Appium Windows Driver to wait for + * a defined amount of time after an app launch is initiated prior to + * attaching to the application session. The limit for this is 50 seconds. + * + * @param timeout The timeout value. + * @return self instance for chaining. + */ + default T setWaitForAppLaunch(Duration timeout) { + return amend(MS_WAIT_FOR_APP_LAUNCH_OPTION, timeout.getSeconds()); + } + + /** + * Get the timeout used to retry Appium Windows Driver session startup. + * + * @return The timeout value. + */ + default Optional doesWaitForAppLaunch() { + return Optional.ofNullable( + toDuration(getCapability(MS_WAIT_FOR_APP_LAUNCH_OPTION), Duration::ofSeconds) + ); + } +} diff --git a/src/main/java/io/appium/java_client/windows/options/SupportsSystemPortOption.java b/src/main/java/io/appium/java_client/windows/options/SupportsSystemPortOption.java new file mode 100644 index 000000000..65ab9fb31 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/SupportsSystemPortOption.java @@ -0,0 +1,52 @@ +/* + * 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.windows.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 SupportsSystemPortOption> extends + Capabilities, CanSetCapability { + String SYSTEM_PORT_OPTION = "systemPort"; + + /** + * The port number to execute Appium Windows Driver server listener on, + * for example 5556. The port must not be occupied. The default starting port + * number for a new Appium Windows Driver session is 4724. If this port is + * already busy then the next free port will be automatically selected. + * + * @param port port number in range 0..65535 + * @return self instance for chaining. + */ + default T setSystemPort(int port) { + return amend(SYSTEM_PORT_OPTION, port); + } + + /** + * Get the port number to execute Appium Windows Driver server listener on. + * + * @return System port value. + */ + default Optional getSystemPort() { + return Optional.ofNullable(toInteger(getCapability(SYSTEM_PORT_OPTION))); + } +} 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 new file mode 100644 index 000000000..257c2807a --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/options/WindowsOptions.java @@ -0,0 +1,114 @@ +/* + * 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.windows.options; + +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.remote.MobilePlatform; +import io.appium.java_client.remote.options.BaseOptions; +import io.appium.java_client.remote.options.SupportsAppOption; +import io.appium.java_client.remote.options.SupportsPostrunOption; +import io.appium.java_client.remote.options.SupportsPrerunOption; +import org.openqa.selenium.Capabilities; + +import java.util.Map; +import java.util.Optional; + +/** + * https://github.com/appium/appium-windows-driver#usage. + */ +public class WindowsOptions extends BaseOptions implements + SupportsAppOption, + SupportsAppArgumentsOption, + SupportsAppTopLevelWindowOption, + SupportsAppWorkingDirOption, + SupportsCreateSessionTimeoutOption, + SupportsMsWaitForAppLaunchOption, + SupportsMsExperimentalWebDriverOption, + SupportsSystemPortOption, + SupportsPrerunOption, + SupportsPostrunOption { + public WindowsOptions() { + setCommonOptions(); + } + + public WindowsOptions(Capabilities source) { + super(source); + setCommonOptions(); + } + + public WindowsOptions(Map source) { + super(source); + setCommonOptions(); + } + + private void setCommonOptions() { + setPlatformName(MobilePlatform.WINDOWS); + setAutomationName(AutomationName.WINDOWS); + } + + /** + * An object containing either script or command key. The value of + * 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 + * for more details. + * + * @param script E.g. {script: 'Get-Process outlook -ErrorAction SilentlyContinue'}. + * @return self instance for chaining. + */ + public WindowsOptions setPrerun(PowerShellData script) { + return amend(PRERUN_OPTION, script.toMap()); + } + + /** + * Get the prerun script. + * + * @return Prerun script. + */ + public Optional getPrerun() { + //noinspection unchecked + return Optional.ofNullable(getCapability(PRERUN_OPTION)) + .map(v -> new PowerShellData((Map) v)); + } + + /** + * An object containing either script or command key. The value of + * 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 + * for more details. + * + * @param script E.g. {script: 'Get-Process outlook -ErrorAction SilentlyContinue'}. + * @return self instance for chaining. + */ + public WindowsOptions setPostrun(PowerShellData script) { + return amend(POSTRUN_OPTION, script.toMap()); + } + + /** + * Get the postrun script. + * + * @return Postrun script. + */ + public Optional getPostrun() { + //noinspection unchecked + return Optional.ofNullable(getCapability(POSTRUN_OPTION)) + .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 d7ebe559e..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,21 +16,20 @@ package io.appium.java_client.ws; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; +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.concurrent.TimeUnit; import java.util.function.Consumer; -import javax.annotation.Nullable; - -public class StringWebSocketClient extends WebSocketListener implements +public class StringWebSocketClient implements WebSocket.Listener, CanHandleMessages, CanHandleErrors, CanHandleConnects, CanHandleDisconnects { private final List> messageHandlers = new CopyOnWriteArrayList<>(); private final List> errorHandlers = new CopyOnWriteArrayList<>(); @@ -39,6 +38,12 @@ public class StringWebSocketClient extends WebSocketListener implements 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,37 +70,41 @@ public void connect(URI endpoint) { return; } - OkHttpClient client = new OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .build(); - Request request = new Request.Builder() - .url(endpoint.toString()) - .build(); - client.newWebSocket(request, this); - client.dispatcher().executorService().shutdown(); + HttpRequest request = new HttpRequest(HttpMethod.GET, endpoint.toString()); + Objects.requireNonNull(httpClient.get()).openSocket(request, this); + onOpen(); setEndpoint(endpoint); } - @Override - public void onOpen(WebSocket webSocket, Response response) { - getConnectionHandlers().forEach(Runnable::run); - isListening = true; + /** + * The callback method invoked on websocket opening. + */ + public void onOpen() { + try { + getConnectionHandlers().forEach(Runnable::run); + } finally { + isListening = true; + } } @Override - public void onClosing(WebSocket webSocket, int code, String reason) { - getDisconnectionHandlers().forEach(Runnable::run); - isListening = false; + public void onClose(int code, String reason) { + try { + getDisconnectionHandlers().forEach(Runnable::run); + } finally { + isListening = false; + } } @Override - public void onFailure(WebSocket webSocket, Throwable t, Response response) { + public void onError(Throwable t) { getErrorHandlers().forEach(x -> x.accept(t)); } @Override - public void onMessage(WebSocket webSocket, String text) { + public void onText(CharSequence data) { + String text = data.toString(); getMessageHandlers().forEach(x -> x.accept(text)); } diff --git a/src/main/java/org/openqa/selenium/SearchContext.java b/src/main/java/org/openqa/selenium/SearchContext.java deleted file mode 100644 index d6498972e..000000000 --- a/src/main/java/org/openqa/selenium/SearchContext.java +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium; - -import java.util.List; - -public interface SearchContext { - /** - * Find all elements within the current context using the given mechanism. - * - * @param The type of the resulting elements. - * @param by The locating mechanism to use - * @return A list of all {@link WebElement}s, or an empty list if nothing matches - * @see org.openqa.selenium.By - */ - List findElements(By by); - - - /** - * Find the first {@link WebElement} using the given method. - * - * @param The type of the resulting element. - * @param by The locating mechanism - * @return The first matching element on the current context - * @throws NoSuchElementException If no matching elements are found - */ - T findElement(By by); -} diff --git a/src/main/java/org/openqa/selenium/WebDriver.java b/src/main/java/org/openqa/selenium/WebDriver.java deleted file mode 100644 index 08de94859..000000000 --- a/src/main/java/org/openqa/selenium/WebDriver.java +++ /dev/null @@ -1,516 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium; - -import org.openqa.selenium.logging.LoggingPreferences; -import org.openqa.selenium.logging.Logs; - -import java.net.URL; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - - -/** - * The main interface to use for testing, which represents an idealised web browser. The methods in - * this class fall into three categories: - *
    - *
  • Control of the browser itself
  • - *
  • Selection of {@link WebElement}s
  • - *
  • Debugging aids
  • - *
- *

- * Key methods are {@link WebDriver#get(String)}, which is used to load a new web page, and the - * various methods similar to {@link WebDriver#findElement(By)}, which is used to find - * {@link WebElement}s. - *

- * Currently, you will need to instantiate implementations of this class directly. It is hoped that - * you write your tests against this interface so that you may "swap in" a more fully featured - * browser when there is a requirement for one. - *

- * Note that all methods that use XPath to locate elements will throw a {@link RuntimeException} - * should there be an error thrown by the underlying XPath engine. - */ -public interface WebDriver extends SearchContext { - // Navigation - - /** - * Load a new web page in the current browser window. This is done using an HTTP GET operation, - * and the method will block until the load is complete. This will follow redirects issued either - * by the server or as a meta-redirect from within the returned HTML. Should a meta-redirect - * "rest" for any duration of time, it is best to wait until this timeout is over, since should - * the underlying page change whilst your test is executing the results of future calls against - * this interface will be against the freshly loaded page. Synonym for - * {@link org.openqa.selenium.WebDriver.Navigation#to(String)}. - * - * @param url The URL to load. It is best to use a fully qualified URL - */ - void get(String url); - - /** - * Get a string representing the current URL that the browser is looking at. - * - * @return The URL of the page currently loaded in the browser - */ - String getCurrentUrl(); - - // General properties - - /** - * The title of the current page. - * - * @return The title of the current page, with leading and trailing whitespace stripped, or null - * if one is not already set - */ - String getTitle(); - - /** - * Find all elements within the current page using the given mechanism. - * This method is affected by the 'implicit wait' times in force at the time of execution. When - * implicitly waiting, this method will return as soon as there are more than 0 items in the - * found collection, or will return an empty list if the timeout is reached. - * - * @param by The locating mechanism to use - * @return A list of all {@link WebElement}s, or an empty list if nothing matches - * @see org.openqa.selenium.By - * @see org.openqa.selenium.WebDriver.Timeouts - */ - List findElements(By by); - - - /** - * Find the first {@link WebElement} using the given method. - * This method is affected by the 'implicit wait' times in force at the time of execution. - * The findElement(..) invocation will return a matching row, or try again repeatedly until - * the configured timeout is reached. - * - * findElement should not be used to look for non-present elements, use {@link #findElements(By)} - * and assert zero length response instead. - * - * @param by The locating mechanism - * @return The first matching element on the current page - * @throws NoSuchElementException If no matching elements are found - * @see org.openqa.selenium.By - * @see org.openqa.selenium.WebDriver.Timeouts - */ - T findElement(By by); - - // Misc - - /** - * Get the source of the last loaded page. If the page has been modified after loading (for - * example, by Javascript) there is no guarantee that the returned text is that of the modified - * page. Please consult the documentation of the particular driver being used to determine whether - * the returned text reflects the current state of the page or the text last sent by the web - * server. The page source returned is a representation of the underlying DOM: do not expect it to - * be formatted or escaped in the same way as the response sent from the web server. Think of it as - * an artist's impression. - * - * @return The source of the current page - */ - String getPageSource(); - - /** - * Close the current window, quitting the browser if it's the last window currently open. - */ - void close(); - - /** - * Quits this driver, closing every associated window. - */ - void quit(); - - /** - * Return a set of window handles which can be used to iterate over all open windows of this - * WebDriver instance by passing them to {@link #switchTo()}.{@link Options#window()} - * - * @return A set of window handles which can be used to iterate over all open windows. - */ - Set getWindowHandles(); - - /** - * Return an opaque handle to this window that uniquely identifies it within this driver instance. - * This can be used to switch to this window at a later date - * - * @return the current window handle - */ - String getWindowHandle(); - - /** - * Send future commands to a different frame or window. - * - * @return A TargetLocator which can be used to select a frame or window - * @see org.openqa.selenium.WebDriver.TargetLocator - */ - TargetLocator switchTo(); - - /** - * An abstraction allowing the driver to access the browser's history and to navigate to a given - * URL. - * - * @return A {@link org.openqa.selenium.WebDriver.Navigation} that allows the selection of what to - * do next - */ - Navigation navigate(); - - /** - * Gets the Option interface - * - * @return An option interface - * @see org.openqa.selenium.WebDriver.Options - */ - Options manage(); - - /** - * An interface for managing stuff you would do in a browser menu - */ - interface Options { - - /** - * Add a specific cookie. If the cookie's domain name is left blank, it is assumed that the - * cookie is meant for the domain of the current document. - * - * @param cookie The cookie to add. - */ - void addCookie(Cookie cookie); - - /** - * Delete the named cookie from the current domain. This is equivalent to setting the named - * cookie's expiry date to some time in the past. - * - * @param name The name of the cookie to delete - */ - void deleteCookieNamed(String name); - - /** - * Delete a cookie from the browser's "cookie jar". The domain of the cookie will be ignored. - * - * @param cookie nom nom nom - */ - void deleteCookie(Cookie cookie); - - /** - * Delete all the cookies for the current domain. - */ - void deleteAllCookies(); - - /** - * Get all the cookies for the current domain. This is the equivalent of calling - * "document.cookie" and parsing the result - * - * @return A Set of cookies for the current domain. - */ - Set getCookies(); - - /** - * Get a cookie with a given name. - * - * @param name the name of the cookie - * @return the cookie, or null if no cookie with the given name is present - */ - Cookie getCookieNamed(String name); - - /** - * @return the interface for managing driver timeouts. - */ - Timeouts timeouts(); - - /** - * @return the interface for controlling IME engines to generate complex-script input. - */ - ImeHandler ime(); - - /** - * @return the interface for managing the current window. - */ - Window window(); - - /** - * Gets the {@link Logs} interface used to fetch different types of logs. - * - *

To set the logging preferences {@link LoggingPreferences}. - * - * @return A Logs interface. - */ - @Beta - Logs logs(); - } - - /** - * An interface for managing timeout behavior for WebDriver instances. - */ - interface Timeouts { - - /** - * Specifies the amount of time the driver should wait when searching for an element if it is - * not immediately present. - *

- * When searching for a single element, the driver should poll the page until the element has - * been found, or this timeout expires before throwing a {@link NoSuchElementException}. When - * searching for multiple elements, the driver should poll the page until at least one element - * has been found or this timeout has expired. - *

- * Increasing the implicit wait timeout should be used judiciously as it will have an adverse - * effect on test run time, especially when used with slower location strategies like XPath. - * - * @param time The amount of time to wait. - * @param unit The unit of measure for {@code time}. - * @return A self reference. - */ - Timeouts implicitlyWait(long time, TimeUnit unit); - - /** - * Sets the amount of time to wait for an asynchronous script to finish execution before - * throwing an error. If the timeout is negative, then the script will be allowed to run - * indefinitely. - * - * @param time The timeout value. - * @param unit The unit of time. - * @return A self reference. - * @see JavascriptExecutor#executeAsyncScript(String, Object...) - */ - Timeouts setScriptTimeout(long time, TimeUnit unit); - - /** - * Sets the amount of time to wait for a page load to complete before throwing an error. - * If the timeout is negative, page loads can be indefinite. - * - * @param time The timeout value. - * @param unit The unit of time. - * @return A Timeouts interface. - */ - Timeouts pageLoadTimeout(long time, TimeUnit unit); - } - - /** - * Used to locate a given frame or window. - */ - interface TargetLocator { - /** - * Select a frame by its (zero-based) index. Selecting a frame by index is equivalent to the - * JS expression window.frames[index] where "window" is the DOM window represented by the - * current context. Once the frame has been selected, all subsequent calls on the WebDriver - * interface are made to that frame. - * - * @param index (zero-based) index - * @return This driver focused on the given frame - * @throws NoSuchFrameException If the frame cannot be found - */ - WebDriver frame(int index); - - /** - * Select a frame by its name or ID. Frames located by matching name attributes are always given - * precedence over those matched by ID. - * - * @param nameOrId the name of the frame window, the id of the <frame> or <iframe> - * element, or the (zero-based) index - * @return This driver focused on the given frame - * @throws NoSuchFrameException If the frame cannot be found - */ - WebDriver frame(String nameOrId); - - /** - * Select a frame using its previously located {@link WebElement}. - * - * @param frameElement The frame element to switch to. - * @return This driver focused on the given frame. - * @throws NoSuchFrameException If the given element is neither an IFRAME nor a FRAME element. - * @throws StaleElementReferenceException If the WebElement has gone stale. - * @see WebDriver#findElement(By) - */ - WebDriver frame(WebElement frameElement); - - /** - * Change focus to the parent context. If the current context is the top level browsing context, - * the context remains unchanged. - * - * @return This driver focused on the parent frame - */ - WebDriver parentFrame(); - - /** - * Switch the focus of future commands for this driver to the window with the given name/handle. - * - * @param nameOrHandle The name of the window or the handle as returned by - * {@link WebDriver#getWindowHandle()} - * @return This driver focused on the given window - * @throws NoSuchWindowException If the window cannot be found - */ - WebDriver window(String nameOrHandle); - - /** - * Selects either the first frame on the page, or the main document when a page contains - * iframes. - * - * @return This driver focused on the top window/first frame. - */ - WebDriver defaultContent(); - - /** - * Switches to the element that currently has focus within the document currently "switched to", - * or the body element if this cannot be detected. This matches the semantics of calling - * "document.activeElement" in Javascript. - * - * @return The WebElement with focus, or the body element if no element with focus can be - * detected. - */ - WebElement activeElement(); - - /** - * Switches to the currently active modal dialog for this particular driver instance. - * - * @return A handle to the dialog. - * @throws NoAlertPresentException If the dialog cannot be found - */ - Alert alert(); - } - - interface Navigation { - /** - * Move back a single "item" in the browser's history. - */ - void back(); - - /** - * Move a single "item" forward in the browser's history. Does nothing if we are on the latest - * page viewed. - */ - void forward(); - - - /** - * Load a new web page in the current browser window. This is done using an HTTP GET operation, - * and the method will block until the load is complete. This will follow redirects issued - * either by the server or as a meta-redirect from within the returned HTML. Should a - * meta-redirect "rest" for any duration of time, it is best to wait until this timeout is over, - * since should the underlying page change whilst your test is executing the results of future - * calls against this interface will be against the freshly loaded page. - * - * @param url The URL to load. It is best to use a fully qualified URL - */ - void to(String url); - - /** - * Overloaded version of {@link #to(String)} that makes it easy to pass in a URL. - * - * @param url URL - */ - void to(URL url); - - /** - * Refresh the current page - */ - void refresh(); - } - - /** - * An interface for managing input methods. - */ - interface ImeHandler { - /** - * All available engines on the machine. To use an engine, it has to be activated. - * - * @return list of available IME engines. - * @throws ImeNotAvailableException if the host does not support IME. - */ - List getAvailableEngines(); - - /** - * Get the name of the active IME engine. The name string is platform-specific. - * - * @return name of the active IME engine. - * @throws ImeNotAvailableException if the host does not support IME. - */ - String getActiveEngine(); - - /** - * Indicates whether IME input active at the moment (not if it's available). - * - * @return true if IME input is available and currently active, false otherwise. - * @throws ImeNotAvailableException if the host does not support IME. - */ - boolean isActivated(); - - /** - * De-activate IME input (turns off the currently activated engine). Note that getActiveEngine - * may still return the name of the engine but isActivated will return false. - * - * @throws ImeNotAvailableException if the host does not support IME. - */ - void deactivate(); - - /** - * Make an engines that is available (appears on the list returned by getAvailableEngines) - * active. After this call, the only loaded engine on the IME daemon will be this one and the - * input sent using sendKeys will be converted by the engine. Noteh that this is a - * platform-independent method of activating IME (the platform-specific way being using keyboard - * shortcuts). - * - * - * @param engine name of engine to activate. - * @throws ImeNotAvailableException if the host does not support IME. - * @throws ImeActivationFailedException if the engine is not available or if activation failed - * for other reasons. - */ - void activateEngine(String engine); - } - - @Beta - interface Window { - /** - * Set the size of the current window. This will change the outer window dimension, - * not just the view port, synonymous to window.resizeTo() in JS. - * - * @param targetSize The target size. - */ - void setSize(Dimension targetSize); - - /** - * Set the position of the current window. This is relative to the upper left corner of the - * screen, synonymous to window.moveTo() in JS. - * - * @param targetPosition The target position of the window. - */ - void setPosition(Point targetPosition); - - /** - * Get the size of the current window. This will return the outer window dimension, not just - * the view port. - * - * @return The current window size. - */ - Dimension getSize(); - - /** - * Get the position of the current window, relative to the upper left corner of the screen. - * - * @return The current window position. - */ - Point getPosition(); - - /** - * Maximizes the current window if it is not already maximized - */ - void maximize(); - - /** - * Fullscreen the current window if it is not already fullscreen - */ - void fullscreen(); - } -} \ No newline at end of file diff --git a/src/main/java/org/openqa/selenium/WebElement.java b/src/main/java/org/openqa/selenium/WebElement.java deleted file mode 100644 index 1aa2da524..000000000 --- a/src/main/java/org/openqa/selenium/WebElement.java +++ /dev/null @@ -1,227 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium; - -import java.util.List; - -/** - * Represents an HTML element. Generally, all interesting operations to do with interacting with a - * page will be performed through this interface. - *

- * All method calls will do a freshness check to ensure that the element reference is still valid. - * This essentially determines whether or not the element is still attached to the DOM. If this test - * fails, then an {@link org.openqa.selenium.StaleElementReferenceException} is thrown, and all - * future calls to this instance will fail. - */ -public interface WebElement extends SearchContext, TakesScreenshot { - /** - * Click this element. If this causes a new page to load, you - * should discard all references to this element and any further - * operations performed on this element will throw a - * StaleElementReferenceException. - * - * Note that if click() is done by sending a native event (which is - * the default on most browsers/platforms) then the method will - * _not_ wait for the next page to load and the caller should verify - * that themselves. - * - * There are some preconditions for an element to be clicked. The - * element must be visible and it must have a height and width - * greater then 0. - * - * @throws StaleElementReferenceException If the element no - * longer exists as initially defined - */ - void click(); - - /** - * If this current element is a form, or an element within a form, then this will be submitted to - * the remote server. If this causes the current page to change, then this method will block until - * the new page is loaded. - * - * @throws NoSuchElementException If the given element is not within a form - */ - void submit(); - - /** - * Use this method to simulate typing into an element, which may set its value. - * - * @param keysToSend character sequence to send to the element - */ - void sendKeys(CharSequence... keysToSend); - - /** - * If this element is a text entry element, this will clear the value. Has no effect on other - * elements. Text entry elements are INPUT and TEXTAREA elements. - * - * Note that the events fired by this event may not be as you'd expect. In particular, we don't - * fire any keyboard or mouse events. If you want to ensure keyboard events are fired, consider - * using something like {@link #sendKeys(CharSequence...)} with the backspace key. To ensure - * you get a change event, consider following with a call to {@link #sendKeys(CharSequence...)} - * with the tab key. - */ - void clear(); - - /** - * Get the tag name of this element. Not the value of the name attribute: will return - * "input" for the element <input name="foo" />. - * - * @return The tag name of this element. - */ - String getTagName(); - - /** - * Get the value of the given attribute of the element. Will return the current value, even if - * this has been modified after the page has been loaded. - * - *

More exactly, this method will return the value of the property with the given name, if it - * exists. If it does not, then the value of the attribute with the given name is returned. If - * neither exists, null is returned. - * - *

The "style" attribute is converted as best can be to a text representation with a trailing - * semi-colon. - * - *

The following are deemed to be "boolean" attributes, and will return either "true" or null: - * - *

async, autofocus, autoplay, checked, compact, complete, controls, declare, defaultchecked, - * defaultselected, defer, disabled, draggable, ended, formnovalidate, hidden, indeterminate, - * iscontenteditable, ismap, itemscope, loop, multiple, muted, nohref, noresize, noshade, - * novalidate, nowrap, open, paused, pubdate, readonly, required, reversed, scoped, seamless, - * seeking, selected, truespeed, willvalidate - * - *

Finally, the following commonly mis-capitalized attribute/property names are evaluated as - * expected: - * - *

    - *
  • If the given name is "class", the "className" property is returned. - *
  • If the given name is "readonly", the "readOnly" property is returned. - *
- * - * Note: The reason for this behavior is that users frequently confuse attributes and - * properties. If you need to do something more precise, e.g., refer to an attribute even when a - * property of the same name exists, then you should evaluate Javascript to obtain the result - * you desire. - * - * @param name The name of the attribute. - * @return The attribute/property's current value or null if the value is not set. - */ - String getAttribute(String name); - - /** - * Determine whether or not this element is selected or not. This operation only applies to input - * elements such as checkboxes, options in a select and radio buttons. - * - * @return True if the element is currently selected or checked, false otherwise. - */ - boolean isSelected(); - - /** - * Is the element currently enabled or not? This will generally return true for everything but - * disabled input elements. - * - * @return True if the element is enabled, false otherwise. - */ - boolean isEnabled(); - - /** - * Get the visible (i.e. not hidden by CSS) innerText of this element, including sub-elements, - * without any leading or trailing whitespace. - * - * @return The innerText of this element. - */ - String getText(); - - /** - * Find all elements within the current context using the given mechanism. When using xpath be - * aware that webdriver follows standard conventions: a search prefixed with "//" will search the - * entire document, not just the children of this current node. Use ".//" to limit your search to - * the children of this WebElement. - * This method is affected by the 'implicit wait' times in force at the time of execution. When - * implicitly waiting, this method will return as soon as there are more than 0 items in the - * found collection, or will return an empty list if the timeout is reached. - * - * @param by The locating mechanism to use - * @return A list of all {@link WebElement}s, or an empty list if nothing matches. - * @see org.openqa.selenium.By - * @see org.openqa.selenium.WebDriver.Timeouts - */ - List findElements(By by); - - /** - * Find the first {@link WebElement} using the given method. See the note in - * {@link #findElements(By)} about finding via XPath. - * This method is affected by the 'implicit wait' times in force at the time of execution. - * The findElement(..) invocation will return a matching row, or try again repeatedly until - * the configured timeout is reached. - * - * findElement should not be used to look for non-present elements, use {@link #findElements(By)} - * and assert zero length response instead. - * - * @param by The locating mechanism - * @return The first matching element on the current context. - * @throws NoSuchElementException If no matching elements are found - * @see org.openqa.selenium.By - * @see org.openqa.selenium.WebDriver.Timeouts - */ - T findElement(By by); - - /** - * Is this element displayed or not? This method avoids the problem of having to parse an - * element's "style" attribute. - * - * @return Whether or not the element is displayed - */ - boolean isDisplayed(); - - /** - * Where on the page is the top left-hand corner of the rendered element? - * - * @return A point, containing the location of the top left-hand corner of the element - */ - Point getLocation(); - - /** - * What is the width and height of the rendered element? - * - * @return The size of the element on the page. - */ - Dimension getSize(); - - /** - * @return The location and size of the rendered element - */ - Rectangle getRect(); - - /** - * Get the value of a given CSS property. - * Color values should be returned as rgba strings, so, - * for example if the "background-color" property is set as "green" in the - * HTML source, the returned value will be "rgba(0, 255, 0, 1)". - * - * Note that shorthand CSS properties (e.g. background, font, border, border-top, margin, - * margin-top, padding, padding-top, list-style, outline, pause, cue) are not returned, - * in accordance with the - * DOM CSS2 specification - * - you should directly access the longhand properties (e.g. background-color) to access the - * desired values. - * - * @param propertyName the css property name of the element - * @return The current, computed value of the property. - */ - String getCssValue(String propertyName); -} \ No newline at end of file diff --git a/src/main/java/org/openqa/selenium/internal/FindsByClassName.java b/src/main/java/org/openqa/selenium/internal/FindsByClassName.java deleted file mode 100644 index cc28072ec..000000000 --- a/src/main/java/org/openqa/selenium/internal/FindsByClassName.java +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium.internal; - -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByClassName { - T findElementByClassName(String using); - - List findElementsByClassName(String using); -} diff --git a/src/main/java/org/openqa/selenium/internal/FindsByCssSelector.java b/src/main/java/org/openqa/selenium/internal/FindsByCssSelector.java deleted file mode 100644 index 74074e534..000000000 --- a/src/main/java/org/openqa/selenium/internal/FindsByCssSelector.java +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium.internal; - -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByCssSelector { - T findElementByCssSelector(String using); - - List findElementsByCssSelector(String using); -} diff --git a/src/main/java/org/openqa/selenium/internal/FindsById.java b/src/main/java/org/openqa/selenium/internal/FindsById.java deleted file mode 100644 index ee4fdd9d3..000000000 --- a/src/main/java/org/openqa/selenium/internal/FindsById.java +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium.internal; - -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsById { - T findElementById(String using); - - List findElementsById(String using); -} diff --git a/src/main/java/org/openqa/selenium/internal/FindsByLinkText.java b/src/main/java/org/openqa/selenium/internal/FindsByLinkText.java deleted file mode 100644 index 52c09f6a1..000000000 --- a/src/main/java/org/openqa/selenium/internal/FindsByLinkText.java +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium.internal; - -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByLinkText { - T findElementByLinkText(String using); - - List findElementsByLinkText(String using); - - T findElementByPartialLinkText(String using); - - List findElementsByPartialLinkText(String using); -} diff --git a/src/main/java/org/openqa/selenium/internal/FindsByName.java b/src/main/java/org/openqa/selenium/internal/FindsByName.java deleted file mode 100644 index 7d39ac1d8..000000000 --- a/src/main/java/org/openqa/selenium/internal/FindsByName.java +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium.internal; - -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByName { - T findElementByName(String using); - - List findElementsByName(String using); -} diff --git a/src/main/java/org/openqa/selenium/internal/FindsByTagName.java b/src/main/java/org/openqa/selenium/internal/FindsByTagName.java deleted file mode 100644 index f5df666af..000000000 --- a/src/main/java/org/openqa/selenium/internal/FindsByTagName.java +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium.internal; - -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByTagName { - T findElementByTagName(String using); - - List findElementsByTagName(String using); -} diff --git a/src/main/java/org/openqa/selenium/internal/FindsByXPath.java b/src/main/java/org/openqa/selenium/internal/FindsByXPath.java deleted file mode 100644 index bea6007cb..000000000 --- a/src/main/java/org/openqa/selenium/internal/FindsByXPath.java +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package org.openqa.selenium.internal; - -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByXPath { - T findElementByXPath(String using); - - List findElementsByXPath(String using); -} 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/TestUtils.java b/src/test/java/io/appium/java_client/TestUtils.java deleted file mode 100644 index 4195d4ef9..000000000 --- a/src/test/java/io/appium/java_client/TestUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.appium.java_client; - -import java.io.IOException; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.net.SocketException; -import java.net.URL; -import java.net.UnknownHostException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -public class 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()) { - socket.connect(InetAddress.getByName("8.8.8.8"), 10002); - return socket.getLocalAddress().getHostAddress(); - } - } - - public static Path resourcePathToLocalPath(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); - } - } -} 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 e45b30253..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidAbilityToUseSupplierTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.appium.java_client.android; - -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.Assert.assertNotEquals; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.functions.ActionSupplier; -import io.appium.java_client.touch.offset.ElementOption; -import org.junit.Test; -import org.openqa.selenium.Point; - -import java.util.List; - -public class AndroidAbilityToUseSupplierTest extends BaseAndroidTest { - - 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(); - - 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.findElementByAccessibilityId("Gallery"))) - - .waitAction(waitOptions(ofSeconds(2))) - - .moveTo(element(driver.findElementByAccessibilityId("Auto Complete"))) - .release(); - - @Test public void horizontalSwipingWithSupplier() { - Activity activity = new Activity("io.appium.android.apis", ".view.Gallery1"); - driver.startActivity(activity); - AndroidElement gallery = driver.findElementById("io.appium.android.apis:id/gallery"); - List images = gallery - .findElementsByClassName("android.widget.ImageView"); - int originalImageCount = images.size(); - - horizontalSwipe.get().perform(); - - assertNotEquals(originalImageCount, gallery - .findElementsByClassName("android.widget.ImageView").size()); - } - - @Test public void verticalSwipingWithSupplier() throws Exception { - driver.resetApp(); - driver.findElementByAccessibilityId("Views").click(); - - Point originalLocation = driver.findElementByAccessibilityId("Gallery").getLocation(); - verticalSwiping.get().perform(); - Thread.sleep(5000); - assertNotEquals(originalLocation, driver.findElementByAccessibilityId("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 0acfd0b49..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.Assert.assertEquals; - -import io.appium.java_client.android.nativekey.AndroidKey; -import io.appium.java_client.android.nativekey.KeyEvent; -import org.junit.Before; -import org.junit.Test; - -public class AndroidActivityTest extends BaseAndroidTest { - - @Before 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/AndroidElementTest.java b/src/test/java/io/appium/java_client/android/AndroidElementTest.java deleted file mode 100644 index 50bed4895..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidElementTest.java +++ /dev/null @@ -1,90 +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.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; - -import io.appium.java_client.MobileBy; -import io.appium.java_client.MobileElement; -import org.junit.Before; -import org.junit.Test; -import org.openqa.selenium.By; - -public class AndroidElementTest extends BaseAndroidTest { - - @Before public void setup() { - Activity activity = new Activity("io.appium.android.apis", ".ApiDemos"); - driver.startActivity(activity); - } - - - @Test public void findByAccessibilityIdTest() { - assertNotEquals(driver.findElementById("android:id/content") - .findElement(MobileBy.AccessibilityId("Graphics")).getText(), null); - assertEquals(driver.findElementById("android:id/content") - .findElements(MobileBy.AccessibilityId("Graphics")).size(), 1); - } - - @Test public void findByAndroidUIAutomatorTest() { - assertNotEquals(driver.findElementById("android:id/content") - .findElement(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).getText(), null); - assertNotEquals(driver.findElementById("android:id/content") - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 0); - assertNotEquals(driver.findElementById("android:id/content") - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 1); - } - - @Test public void replaceValueTest() { - String originalValue = "original value"; - - Activity activity = new Activity("io.appium.android.apis", ".view.Controls1"); - driver.startActivity(activity); - AndroidElement editElement = driver - .findElementByAndroidUIAutomator("resourceId(\"io.appium.android.apis:id/edit\")"); - editElement.sendKeys(originalValue); - assertEquals(originalValue, editElement.getText()); - String replacedValue = "replaced value"; - editElement.replaceValue(replacedValue); - assertEquals(replacedValue, editElement.getText()); - } - - @Test public void scrollingToSubElement() { - driver.findElementByAccessibilityId("Views").click(); - AndroidElement list = driver.findElement(By.id("android:id/list")); - MobileElement radioGroup = list - .findElement(MobileBy - .AndroidUIAutomator("new UiScrollable(new UiSelector()).scrollIntoView(" - + "new UiSelector().text(\"Radio Group\"));")); - assertNotNull(radioGroup.getLocation()); - } - - @Test public void setValueTest() { - String value = "new value"; - - Activity activity = new Activity("io.appium.android.apis", ".view.Controls1"); - driver.startActivity(activity); - AndroidElement editElement = driver - .findElementByAndroidUIAutomator("resourceId(\"io.appium.android.apis:id/edit\")"); - editElement.setValue(value); - assertEquals(value, editElement.getText()); - } -} diff --git a/src/test/java/io/appium/java_client/android/AndroidOptionsTest.java b/src/test/java/io/appium/java_client/android/AndroidOptionsTest.java deleted file mode 100644 index 6b2d5d211..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidOptionsTest.java +++ /dev/null @@ -1,77 +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 io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobilePlatform; -import org.junit.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.Assert.assertEquals; - -public class AndroidOptionsTest { - private AndroidOptions androidOptions = new AndroidOptions(); - - @Test - public void setsPlatformNameByDefault() { - assertEquals(MobilePlatform.ANDROID, androidOptions.getPlatformName()); - } - - @Test - public void acceptsExistingCapabilities() { - MutableCapabilities capabilities = new MutableCapabilities(); - capabilities.setCapability("deviceName", "Pixel"); - capabilities.setCapability("platformVersion", "10"); - capabilities.setCapability("newCommandTimeout", 60); - - androidOptions = new AndroidOptions(capabilities); - - assertEquals("Pixel", androidOptions.getDeviceName()); - assertEquals("10", androidOptions.getPlatformVersion()); - assertEquals(Duration.ofSeconds(60), androidOptions.getNewCommandTimeout()); - } - - @Test - public void acceptsMobileCapabilities() throws MalformedURLException { - androidOptions.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", androidOptions.getApp()); - assertEquals(AutomationName.ANDROID_UIAUTOMATOR2, androidOptions.getAutomationName()); - assertEquals("10", androidOptions.getPlatformVersion()); - assertEquals("Pixel", androidOptions.getDeviceName()); - assertEquals("/path/to/app.apk", androidOptions.getOtherApps()); - assertEquals("fr_CA", androidOptions.getLocale()); - assertEquals("1ae203187fc012g", androidOptions.getUdid()); - assertEquals(ScreenOrientation.LANDSCAPE, androidOptions.getOrientation()); - assertEquals(Duration.ofSeconds(60), androidOptions.getNewCommandTimeout()); - assertEquals("fr", androidOptions.getLanguage()); - } -} diff --git a/src/test/java/io/appium/java_client/android/AndroidSearchingTest.java b/src/test/java/io/appium/java_client/android/AndroidSearchingTest.java deleted file mode 100644 index 84f4b88b2..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidSearchingTest.java +++ /dev/null @@ -1,68 +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.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; - -import io.appium.java_client.MobileBy; -import io.appium.java_client.MobileElement; -import org.junit.Before; -import org.junit.Test; - - -public class AndroidSearchingTest extends BaseAndroidTest { - - @Before - public void setup() { - Activity activity = new Activity("io.appium.android.apis", ".ApiDemos"); - driver.startActivity(activity); - } - - @Test public void findByAccessibilityIdTest() { - assertNotEquals(driver.findElement(MobileBy.AccessibilityId("Graphics")).getText(), null); - assertEquals(driver.findElements(MobileBy.AccessibilityId("Graphics")).size(), 1); - } - - @Test public void findByAndroidUIAutomatorTest() { - assertNotEquals(driver - .findElement(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).getText(), null); - assertNotEquals(driver - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 0); - assertNotEquals(driver - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 1); - } - - @Test public void findByXPathTest() { - String byXPath = "//android.widget.TextView[contains(@text, 'Animat')]"; - assertNotNull(driver.findElementByXPath(byXPath).getText()); - assertEquals(driver.findElementsByXPath(byXPath).size(), 1); - } - - @Test public void findScrollable() { - driver.findElementByAccessibilityId("Views").click(); - MobileElement radioGroup = driver - .findElementByAndroidUIAutomator("new UiScrollable(new UiSelector()" - + ".resourceId(\"android:id/list\")).scrollIntoView(" - + "new UiSelector().text(\"Radio Group\"));"); - assertNotNull(radioGroup.getLocation()); - } -} 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 1c375bae9..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidTouchTest.java +++ /dev/null @@ -1,207 +0,0 @@ -package io.appium.java_client.android; - -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.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.MultiTouchAction; -import io.appium.java_client.TouchAction; -import org.junit.Before; -import org.junit.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 { - - @Before - 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); - AndroidElement dragDot1 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_1")); - AndroidElement 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 = dragDot1.getCenter(); - Point center2 = dragDot3.getCenter(); - - 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); - AndroidElement dragDot1 = driver.findElement(By.id("io.appium.android.apis:id/drag_dot_1")); - AndroidElement 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 = dragDot1.getCenter(); - Point center2 = dragDot3.getCenter(); - - 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.findElementById("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 - .findElementById("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.findElementById("io.appium.android.apis:id/button_toggle"))) - .waitAction(waitOptions(ofSeconds(1))) - .release() - .perform(); - assertEquals("ON" ,driver - .findElementById("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); - AndroidElement chronometer = - driver.findElementById("io.appium.android.apis:id/chronometer"); - - TouchAction startStop = new TouchAction(driver) - .tap(tapOptions().withElement(element(driver.findElementById("io.appium.android.apis:id/start")))) - .waitAction(waitOptions(ofSeconds(2))) - .tap(tapOptions().withElement(element(driver.findElementById("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); - AndroidElement chronometer = - driver.findElementById("io.appium.android.apis:id/chronometer"); - - Point center1 = driver.findElementById("io.appium.android.apis:id/start").getCenter(); - - TouchAction startStop = new TouchAction(driver) - .tap(point(center1.x, center1.y)) - .tap(element(driver.findElementById("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); - - AndroidElement gallery = driver.findElementById("io.appium.android.apis:id/gallery"); - List images = gallery - .findElementsByClassName("android.widget.ImageView"); - int originalImageCount = images.size(); - Point location = gallery.getLocation(); - Point center = gallery.getCenter(); - - 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 - .findElementsByClassName("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.findElementById("io.appium.android.apis:id/button_toggle"))) - .waitAction(waitOptions(ofSeconds(1))) - .release(); - new MultiTouchAction(driver) - .add(press) - .perform(); - assertEquals("ON" ,driver - .findElementById("io.appium.android.apis:id/button_toggle").getText()); - } - -} diff --git a/src/test/java/io/appium/java_client/android/BaseAndroidTest.java b/src/test/java/io/appium/java_client/android/BaseAndroidTest.java deleted file mode 100644 index 1e12834d1..000000000 --- a/src/test/java/io/appium/java_client/android/BaseAndroidTest.java +++ /dev/null @@ -1,67 +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 io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; - -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import org.openqa.selenium.remote.DesiredCapabilities; - -import static io.appium.java_client.TestResources.apiDemosApk; - -public class BaseAndroidTest { - public static final String APP_ID = "io.appium.android.apis"; - private static AppiumDriverLocalService service; - protected static AndroidDriver driver; - - /** - * initialization. - */ - @BeforeClass public static void beforeClass() { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException( - "An appium server node is not started!"); - } - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, apiDemosApk().toAbsolutePath().toString()); - capabilities.setCapability("eventTimings", true); - driver = new AndroidDriver<>(service.getUrl(), capabilities); - } - - /** - * finishing. - */ - @AfterClass public static void afterClass() { - if (driver != null) { - driver.quit(); - } - if (service != null) { - service.stop(); - } - } -} 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 d2fde8e25..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.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.util.function.Predicate; - -public class IntentTest { - private static AppiumDriverLocalService service; - protected static AndroidDriver driver; - - /** - * initialization. - */ - @BeforeClass public static void beforeClass() { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new RuntimeException("An appium server node is not started!"); - } - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, intentExampleApk().toAbsolutePath().toString()); - driver = new AndroidDriver<>(service.getUrl(), capabilities); - } - - /** - * finishing. - */ - @AfterClass 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.findElementById("com.prgguru.android:id/textView1").getText(), - "Welcome AppiumIntentTest"); - } -} diff --git a/src/test/java/io/appium/java_client/android/OpenNotificationsTest.java b/src/test/java/io/appium/java_client/android/OpenNotificationsTest.java deleted file mode 100644 index 7806ecc14..000000000 --- a/src/test/java/io/appium/java_client/android/OpenNotificationsTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.appium.java_client.android; - -import static org.junit.Assert.assertNotEquals; -import static org.openqa.selenium.By.id; - -import org.junit.Test; - -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.util.List; - -public class OpenNotificationsTest extends BaseAndroidTest { - @Test - public void openNotification() { - driver.closeApp(); - driver.openNotifications(); - WebDriverWait wait = new WebDriverWait(driver, 20); - assertNotEquals(0, wait.until(input -> { - List result = input - .findElements(id("com.android.systemui:id/settings_button")); - - return result.isEmpty() ? null : result; - }).size()); - } -} 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/appium/AndroidTest.java b/src/test/java/io/appium/java_client/appium/AndroidTest.java deleted file mode 100644 index aa3d2929e..000000000 --- a/src/test/java/io/appium/java_client/appium/AndroidTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package io.appium.java_client.appium; - -import static io.appium.java_client.TestResources.apiDemosApk; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.MobileBy; -import io.appium.java_client.MobileElement; -import io.appium.java_client.android.Activity; -import io.appium.java_client.android.AndroidElement; -import io.appium.java_client.android.StartsActivity; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.remote.MobilePlatform; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.openqa.selenium.remote.Response; - -import java.util.Map; - -public class AndroidTest { - - private static AppiumDriverLocalService service; - private static AppiumDriver driver; - private StartsActivity startsActivity; - - /** - * initialization. - */ - @BeforeClass - public static void beforeClass() { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException( - "An appium server node is not started!"); - } - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, apiDemosApk().toAbsolutePath().toString()); - driver = new AppiumDriver<>(service.getUrl(), capabilities); - } - - /** - * finishing. - */ - @AfterClass - public static void afterClass() { - if (driver != null) { - driver.quit(); - } - if (service != null) { - service.stop(); - } - } - - @Before - public void setUp() { - 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("io.appium.android.apis", ".ApiDemos"); - startsActivity.startActivity(activity); - } - - @Test - public void findByAccessibilityIdFromDriverTest() { - assertNotEquals(driver.findElementByAccessibilityId("Graphics").getText(), null); - assertEquals(driver.findElementsByAccessibilityId("Graphics").size(), 1); - } - - @Test public void findByAndroidUIAutomatorFromDriverTest() { - assertNotEquals(driver - .findElement(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).getText(), null); - assertNotEquals(driver - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 0); - assertNotEquals(driver - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 1); - } - - @Test public void findByAccessibilityIdFromElementTest() { - assertNotEquals(driver.findElementById("android:id/content") - .findElement(MobileBy.AccessibilityId("Graphics")).getText(), null); - assertEquals(driver.findElementById("android:id/content") - .findElements(MobileBy.AccessibilityId("Graphics")).size(), 1); - } - - @Test public void findByAndroidUIAutomatorFromElementTest() { - assertNotEquals(driver.findElementById("android:id/content") - .findElement(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).getText(), null); - assertNotEquals(driver.findElementById("android:id/content") - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 0); - assertNotEquals(driver.findElementById("android:id/content") - .findElements(MobileBy - .AndroidUIAutomator("new UiSelector().clickable(true)")).size(), 1); - } - - @Test public void replaceValueTest() { - String originalValue = "original value"; - - Activity activity = new Activity("io.appium.android.apis", ".view.Controls1"); - startsActivity.startActivity(activity); - AndroidElement editElement = driver - .findElement(MobileBy - .AndroidUIAutomator("resourceId(\"io.appium.android.apis:id/edit\")")); - editElement.sendKeys(originalValue); - assertEquals(originalValue, editElement.getText()); - String replacedValue = "replaced value"; - editElement.replaceValue(replacedValue); - assertEquals(replacedValue, editElement.getText()); - } - - @Test public void scrollingToSubElement() { - driver.findElementByAccessibilityId("Views").click(); - AndroidElement list = driver.findElement(By.id("android:id/list")); - MobileElement radioGroup = list - .findElement(MobileBy - .AndroidUIAutomator("new UiScrollable(new UiSelector()).scrollIntoView(" - + "new UiSelector().text(\"Radio Group\"));")); - assertNotNull(radioGroup.getLocation()); - } - -} diff --git a/src/test/java/io/appium/java_client/appium/AppiumFluentWaitTest.java b/src/test/java/io/appium/java_client/appium/AppiumFluentWaitTest.java deleted file mode 100644 index ead5bff7d..000000000 --- a/src/test/java/io/appium/java_client/appium/AppiumFluentWaitTest.java +++ /dev/null @@ -1,97 +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.appium; - -import static java.time.Duration.ofSeconds; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsEqual.equalTo; - -import io.appium.java_client.AppiumFluentWait; - -import org.junit.Assert; -import org.junit.Test; -import org.openqa.selenium.TimeoutException; -import org.openqa.selenium.support.ui.Wait; - -import java.time.Clock; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; - -public class AppiumFluentWaitTest { - - private static class FakeElement { - public boolean isDisplayed() { - return false; - } - } - - @Test(expected = TimeoutException.class) - public void testDefaultStrategy() { - final FakeElement el = new FakeElement(); - final Wait wait = new AppiumFluentWait<>(el, Clock.systemDefaultZone(), duration -> { - assertThat(duration.getSeconds(), is(equalTo(1L))); - Thread.sleep(duration.toMillis()); - }).withPollingStrategy(AppiumFluentWait.IterationInfo::getInterval) - .withTimeout(ofSeconds(3)) - .pollingEvery(ofSeconds(1)); - wait.until(FakeElement::isDisplayed); - Assert.fail("TimeoutException is expected"); - } - - @Test - public void testCustomStrategyOverridesDefaultInterval() { - final FakeElement el = new FakeElement(); - final AtomicInteger callsCounter = new AtomicInteger(0); - final Wait wait = new AppiumFluentWait<>(el, Clock.systemDefaultZone(), duration -> { - callsCounter.incrementAndGet(); - assertThat(duration.getSeconds(), is(equalTo(2L))); - Thread.sleep(duration.toMillis()); - }).withPollingStrategy(info -> ofSeconds(2)) - .withTimeout(ofSeconds(3)) - .pollingEvery(ofSeconds(1)); - try { - wait.until(FakeElement::isDisplayed); - Assert.fail("TimeoutException is expected"); - } catch (TimeoutException e) { - // this is expected - assertThat(callsCounter.get(), is(equalTo(2))); - } - } - - @Test - public void testIntervalCalculationForCustomStrategy() { - final FakeElement el = new FakeElement(); - final AtomicInteger callsCounter = new AtomicInteger(0); - // Linear dependency - final Function pollingStrategy = x -> x * 2; - final Wait wait = new AppiumFluentWait<>(el, Clock.systemDefaultZone(), duration -> { - int callNumber = callsCounter.incrementAndGet(); - assertThat(duration.getSeconds(), is(equalTo(pollingStrategy.apply((long) callNumber)))); - Thread.sleep(duration.toMillis()); - }).withPollingStrategy(info -> ofSeconds(pollingStrategy.apply(info.getNumber()))) - .withTimeout(ofSeconds(4)) - .pollingEvery(ofSeconds(1)); - try { - wait.until(FakeElement::isDisplayed); - Assert.fail("TimeoutException is expected"); - } catch (TimeoutException e) { - // this is expected - assertThat(callsCounter.get(), is(equalTo(2))); - } - } -} diff --git a/src/test/java/io/appium/java_client/appium/element/generation/BaseElementGenerationTest.java b/src/test/java/io/appium/java_client/appium/element/generation/BaseElementGenerationTest.java deleted file mode 100644 index 83271f229..000000000 --- a/src/test/java/io/appium/java_client/appium/element/generation/BaseElementGenerationTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.appium.java_client.appium.element.generation; - -import static org.junit.Assert.assertEquals; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import org.junit.After; -import org.openqa.selenium.By; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.SessionNotCreatedException; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.util.function.BiPredicate; -import java.util.function.Supplier; - -public class BaseElementGenerationTest { - protected AppiumDriver driver; - protected AppiumDriverLocalService service; - - protected final BiPredicate> commonPredicate = (by, aClass) -> { - WebElement element = driver.findElement(by); - assertEquals(element.getClass(), aClass); - return true; - }; - - @After - public void tearDown() { - try { - if (driver != null) { - driver.quit(); - } - if (service != null) { - service.stop(); - } - } finally { - driver = null; - service = null; - } - } - -} diff --git a/src/test/java/io/appium/java_client/appium/element/generation/android/AndroidElementGeneratingTest.java b/src/test/java/io/appium/java_client/appium/element/generation/android/AndroidElementGeneratingTest.java deleted file mode 100644 index 1582c7b18..000000000 --- a/src/test/java/io/appium/java_client/appium/element/generation/android/AndroidElementGeneratingTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.appium.java_client.appium.element.generation.android; - -import static io.appium.java_client.MobileBy.AndroidUIAutomator; -import static io.appium.java_client.TestResources.apiDemosApk; -import static org.junit.Assert.assertTrue; -import static org.openqa.selenium.By.name; -import static org.openqa.selenium.By.tagName; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.android.AndroidElement; -import io.appium.java_client.appium.element.generation.BaseElementGenerationTest; -import io.appium.java_client.remote.AndroidMobileCapabilityType; -import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.remote.MobilePlatform; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.util.function.BiPredicate; -import java.util.function.Supplier; - -public class AndroidElementGeneratingTest extends BaseElementGenerationTest { - - private final Supplier commonCapabilitiesSupplier = () -> { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - serverCapabilities.setCapability(MobileCapabilityType.APP, apiDemosApk().toAbsolutePath().toString()); - return serverCapabilities; - }; - - @Test - public void whenAndroidNativeAppIsLaunched() { - assertTrue(check(() -> { - DesiredCapabilities clientCapabilities = commonCapabilitiesSupplier.get(); - clientCapabilities.setCapability(MobileCapabilityType.FULL_RESET, true); - clientCapabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 60); - return clientCapabilities; - }, commonPredicate, AndroidUIAutomator("new UiSelector().clickable(true)") - )); - } - - @Test - public void whenAndroidHybridAppIsLaunched() { - assertTrue(check(() -> { - DesiredCapabilities clientCapabilities = commonCapabilitiesSupplier.get(); - clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, "io.appium.android.apis"); - clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, ".view.WebView1"); - return clientCapabilities; - }, (by, aClass) -> { - driver.context("WEBVIEW_io.appium.android.apis"); - return commonPredicate.test(by, aClass); - }, tagName("a"))); - } - - @Test - public void whenAndroidBrowserIsLaunched() { - assertTrue(check(() -> { - DesiredCapabilities clientCapabilities = commonCapabilitiesSupplier.get(); - clientCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); - clientCapabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.BROWSER); - clientCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - return clientCapabilities; - }, (by, aClass) -> { - driver.get("https://www.google.com"); - return commonPredicate.test(by, aClass); - }, name("q"))); - } - - private boolean check(Supplier capabilitiesSupplier, - BiPredicate> filter, - By by) { - service = AppiumDriverLocalService.buildDefaultService(); - driver = new AppiumDriver<>(service, capabilitiesSupplier.get()); - return filter.test(by, AndroidElement.class); - } -} diff --git a/src/test/java/io/appium/java_client/appium/element/generation/ios/IOSElementGenerationTest.java b/src/test/java/io/appium/java_client/appium/element/generation/ios/IOSElementGenerationTest.java deleted file mode 100644 index 3115f8997..000000000 --- a/src/test/java/io/appium/java_client/appium/element/generation/ios/IOSElementGenerationTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package io.appium.java_client.appium.element.generation.ios; - -import static io.appium.java_client.MobileBy.AccessibilityId; -import static io.appium.java_client.TestResources.testAppZip; -import static io.appium.java_client.TestResources.vodQaAppZip; -import static org.junit.Assert.assertTrue; -import static org.openqa.selenium.By.id; -import static org.openqa.selenium.By.name; -import static org.openqa.selenium.By.partialLinkText; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.appium.element.generation.BaseElementGenerationTest; -import io.appium.java_client.ios.BaseIOSTest; -import io.appium.java_client.ios.IOSElement; -import io.appium.java_client.remote.IOSMobileCapabilityType; -import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.remote.MobilePlatform; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import org.junit.Ignore; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.SessionNotCreatedException; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.io.File; -import java.util.function.BiPredicate; -import java.util.function.Function; -import java.util.function.Supplier; - -public class IOSElementGenerationTest extends BaseElementGenerationTest { - - private static final File testApp = testAppZip().toFile(); - - private static final File webViewApp = vodQaAppZip().toFile(); - - private Supplier commonAppCapabilitiesSupplier = () -> { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, BaseIOSTest.DEVICE_NAME); - serverCapabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, - 500000); //some environment is too slow - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, BaseIOSTest.PLATFORM_VERSION); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); - return serverCapabilities; - }; - - private Function> appFileSupplierFunction = file -> { - final DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - return () -> { - clientCapabilities.setCapability(MobileCapabilityType.APP, file.getAbsolutePath()); - return clientCapabilities; - }; - }; - - private final Supplier serverBrowserCapabilitiesSupplier = () -> { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.SAFARI); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, BaseIOSTest.PLATFORM_VERSION); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, BaseIOSTest.DEVICE_NAME); - //sometimes environment has performance problems - serverCapabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - return serverCapabilities; - }; - - private final Supplier clientBrowserCapabilitiesSupplier = () -> { - DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - clientCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); - return clientCapabilities; - }; - - @Test - public void whenIOSNativeAppIsLaunched() { - assertTrue(check(() -> { - Capabilities caps = commonAppCapabilitiesSupplier.get(); - return caps.merge(appFileSupplierFunction.apply(testApp).get()); - }, commonPredicate, - AccessibilityId("Answer") - )); - } - - @Ignore - @Test - public void whenIOSHybridAppIsLaunched() { - assertTrue(check(() -> { - Capabilities caps = commonAppCapabilitiesSupplier.get(); - return caps.merge(appFileSupplierFunction.apply(webViewApp).get()); - }, (by, aClass) -> { - new WebDriverWait(driver, 30) - .until(ExpectedConditions.presenceOfElementLocated(id("login"))) - .click(); - driver.findElementByAccessibilityId("webView").click(); - new WebDriverWait(driver, 30) - .until(ExpectedConditions - .presenceOfElementLocated(AccessibilityId("Webview"))); - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - driver.getContextHandles().forEach((handle) -> { - if (handle.contains("WEBVIEW")) { - driver.context(handle); - } - }); - return commonPredicate.test(by, aClass); - }, partialLinkText("login"))); - } - - @Test - public void whenIOSBrowserIsLaunched() { - assertTrue(check(() -> { - Capabilities caps = serverBrowserCapabilitiesSupplier.get(); - return caps.merge(clientBrowserCapabilitiesSupplier.get()); - }, (by, aClass) -> { - driver.get("https://www.google.com"); - return commonPredicate.test(by, aClass); - }, name("q"))); - } - - @Test - public void whenIOSNativeAppIsLaunched2() { - assertTrue(check(() -> { - DesiredCapabilities serverCapabilities = commonAppCapabilitiesSupplier.get(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, BaseIOSTest.PLATFORM_VERSION); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); - return serverCapabilities.merge(appFileSupplierFunction.apply(testApp).get()); - }, commonPredicate, id("IntegerA"))); - } - - @Test - public void whenIOSBrowserIsLaunched2() { - assertTrue(check(() -> { - DesiredCapabilities serverCapabilities = serverBrowserCapabilitiesSupplier.get(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, BaseIOSTest.PLATFORM_VERSION); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); - return serverCapabilities.merge(clientBrowserCapabilitiesSupplier.get()); - }, (by, aClass) -> { - driver.get("https://www.google.com"); - return commonPredicate.test(by, aClass); - }, name("q"))); - } - - private boolean check(Supplier capabilitiesSupplier, - BiPredicate> filter, - By by) { - service = AppiumDriverLocalService.buildDefaultService(); - Capabilities caps = capabilitiesSupplier.get(); - DesiredCapabilities fixedCaps = new DesiredCapabilities(caps); - fixedCaps.setCapability("commandTimeouts", "120000"); - try { - driver = new AppiumDriver<>(service, fixedCaps); - } catch (SessionNotCreatedException e) { - fixedCaps.setCapability("useNewWDA", true); - driver = new AppiumDriver<>(service, fixedCaps); - } - return filter.test(by, IOSElement.class); - } -} 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 new file mode 100644 index 000000000..4ab700ca3 --- /dev/null +++ b/src/test/java/io/appium/java_client/drivers/options/OptionsBuildingTest.java @@ -0,0 +1,213 @@ +/* + * 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.drivers.options; + +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; +import io.appium.java_client.ios.options.other.CommandTimeouts; +import io.appium.java_client.ios.options.simulator.Permissions; +import io.appium.java_client.mac.options.AppleScriptData; +import io.appium.java_client.mac.options.Mac2Options; +import io.appium.java_client.remote.AutomationName; +import io.appium.java_client.safari.options.SafariOptions; +import io.appium.java_client.safari.options.WebrtcData; +import io.appium.java_client.windows.options.PowerShellData; +import io.appium.java_client.windows.options.WindowsOptions; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.Platform; + +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; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings("ConstantConditions") +public class OptionsBuildingTest { + @Test + public void canBuildXcuiTestOptions() throws MalformedURLException { + XCUITestOptions options = new XCUITestOptions(); + assertEquals(Platform.IOS, options.getPlatformName()); + assertEquals(AutomationName.IOS_XCUI_TEST, options.getAutomationName().orElse(null)); + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .noReset() + .setWdaBaseUrl("http://localhost:8000") + .setPermissions(new Permissions() + .withAppPermissions("com.apple.MobileSafari", + 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")) + .orElse(null)); + assertEquals(10L, (long) options.getSafariSocketChunkSize().orElse(0)); + assertEquals(1L, options.getCommandTimeouts().orElse(null).left() + .getCommandTimeout("yolo").orElse(null).getSeconds()); + } + + @Test + public void canBuildUiAutomator2Options() throws MalformedURLException { + UiAutomator2Options options = new UiAutomator2Options(); + assertEquals(Platform.ANDROID, options.getPlatformName()); + assertEquals(AutomationName.ANDROID_UIAUTOMATOR2, options.getAutomationName().orElse(null)); + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .noReset() + .setAdbExecTimeout(Duration.ofSeconds(3)) + .suppressKillServer() + .setMjpegScreenshotUrl(new URL("http://yolo.com")) + .setKeystoreConfig(new KeystoreConfig("path", "password", "keyAlias", "keyPassword")); + assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); + assertEquals(Duration.ofSeconds(3), options.getAdbExecTimeout().orElse(null)); + assertEquals(new URL("http://yolo.com"), options.getMjpegScreenshotUrl().orElse(null)); + assertEquals("path", options.getKeystoreConfig().orElse(null).getPath()); + assertEquals("keyAlias", options.getKeystoreConfig().orElse(null).getKeyAlias()); + assertTrue(options.doesSuppressKillServer().orElse(false)); + } + + @Test + public void canBuildEspressoOptions() { + EspressoOptions options = new EspressoOptions(); + assertEquals(Platform.ANDROID, options.getPlatformName()); + assertEquals(AutomationName.ESPRESSO, options.getAutomationName().orElse(null)); + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .forceEspressoRebuild() + .setAppLocale(new AppLocale() + .withCountry("CN") + .withLanguage("zh") + .withVariant("hans")) + .setEspressoBuildConfig(new EspressoBuildConfig() + .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()); + assertTrue(options.doesForceEspressoRebuild().orElse(false)); + } + + @Test + public void canBuildWindowsOptions() { + WindowsOptions options = new WindowsOptions(); + assertEquals(Platform.WINDOWS, options.getPlatformName()); + assertEquals(AutomationName.WINDOWS, options.getAutomationName().orElse(null)); + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .setPrerun(new PowerShellData().withScript("yolo prescript")) + .setPostrun(new PowerShellData().withCommand("yolo command")); + assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); + assertEquals("yolo prescript", options.getPrerun().orElse(null).getScript().orElse(null)); + assertEquals("yolo command", options.getPostrun().orElse(null).getCommand().orElse(null)); + } + + @Test + public void canBuildMac2Options() { + Mac2Options options = new Mac2Options(); + assertEquals(Platform.MAC, options.getPlatformName()); + assertEquals(AutomationName.MAC2, options.getAutomationName().orElse(null)); + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .skipAppKill() + .setPrerun(new AppleScriptData().withScript("yolo prescript")) + .setPostrun(new AppleScriptData().withCommand("yolo command")); + assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); + assertEquals("yolo prescript", options.getPrerun().orElse(null).getScript().orElse(null)); + assertEquals("yolo command", options.getPostrun().orElse(null).getCommand().orElse(null)); + assertTrue(options.doesSkipAppKill().orElse(false)); + assertFalse(options.doesEventTimings().isPresent()); + } + + @Test + public void canBuildGeckoOptions() { + GeckoOptions options = new GeckoOptions(); + options.setPlatformName(Platform.MAC.toString()); + assertEquals(Platform.MAC, options.getPlatformName()); + assertEquals(AutomationName.GECKO, options.getAutomationName().orElse(null)); + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .setVerbosity(Verbosity.TRACE) + .setMozFirefoxOptions(Map.of( + "profile", "yolo" + )); + assertEquals(Duration.ofSeconds(10), options.getNewCommandTimeout().orElse(null)); + assertEquals(Verbosity.TRACE, options.getVerbosity().orElse(null)); + assertEquals("yolo", options.getMozFirefoxOptions().orElse(null) + .get("profile")); + } + + @Test + public void canBuildSafariOptions() { + SafariOptions options = new SafariOptions(); + assertEquals(Platform.IOS, options.getPlatformName()); + assertEquals(AutomationName.SAFARI, options.getAutomationName().orElse(null)); + options.setNewCommandTimeout(Duration.ofSeconds(10)) + .safariUseSimulator() + .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) + .doesDisableIceCandidateFiltering().orElse(false)); + 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/AbilityToDefineListenersExternally.java b/src/test/java/io/appium/java_client/events/AbilityToDefineListenersExternally.java deleted file mode 100644 index 7357cc39b..000000000 --- a/src/test/java/io/appium/java_client/events/AbilityToDefineListenersExternally.java +++ /dev/null @@ -1,103 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; - -import io.appium.java_client.events.listeners.AlertListener2; -import io.appium.java_client.events.listeners.ContextListener2; -import io.appium.java_client.events.listeners.ElementListener2; -import io.appium.java_client.events.listeners.ExceptionListener2; -import io.appium.java_client.events.listeners.JavaScriptListener2; -import io.appium.java_client.events.listeners.NavigationListener2; -import io.appium.java_client.events.listeners.RotationListener2; -import io.appium.java_client.events.listeners.SearchingListener2; -import io.appium.java_client.events.listeners.WindowListener2; -import org.junit.BeforeClass; -import org.junit.Test; - -public class AbilityToDefineListenersExternally extends BaseListenerTest { - - private static final String PREFIX = "Externally defined listener: "; - private static EmptyWebDriver emptyWebDriver; - private static SearchingListener2 searchingListener = new SearchingListener2(); - private static NavigationListener2 navigationListener = new NavigationListener2(); - private static ElementListener2 elementListener = new ElementListener2(); - private static JavaScriptListener2 javaScriptListener = new JavaScriptListener2(); - private static ExceptionListener2 exceptionListener = new ExceptionListener2(); - private static AlertListener2 alertListener = new AlertListener2(); - private static ContextListener2 contextListener = new ContextListener2(); - private static RotationListener2 rotationListener = new RotationListener2(); - private static WindowListener2 windowListener = new WindowListener2(); - - @BeforeClass public static void beforeClass() { - emptyWebDriver = new EmptyWebDriver(); - emptyWebDriver = EventFiringWebDriverFactory.getEventFiringWebDriver(emptyWebDriver, - searchingListener, navigationListener, elementListener, javaScriptListener, - exceptionListener, alertListener, contextListener, rotationListener, windowListener); - } - - @Test - public void searchContextEventTest() { - assertThat(super.assertThatSearchListenerWorks(emptyWebDriver, searchingListener, PREFIX), - is(true)); - } - - @Test - public void searchContextEventTest2() { - assertThat( - super - .assertThatSearchListenerWorksAgainstElements(emptyWebDriver, searchingListener, PREFIX), - is(true)); - } - - @Test - public void navigationEventTest() throws Exception { - assertThat( - super - .assertThatNavigationListenerWorks(emptyWebDriver, navigationListener, PREFIX), - is(true)); - } - - @Test - public void elementEventTest() { - assertThat(super.assertThatElementListenerWorks(emptyWebDriver, elementListener, PREFIX), - is(true)); - } - - @Test - public void javaScriptEventTest() { - assertThat(super - .assertThatJavaScriptListenerWorks(emptyWebDriver, javaScriptListener, PREFIX), - is(true)); - } - - @Test - public void exceptionEventTest() { - assertThat(super.assertThatExceptionListenerWorks(emptyWebDriver, exceptionListener, PREFIX), - is(true)); - } - - @Test - public void alertEventTest() { - assertThat(super.assertThatAlertListenerWorks(emptyWebDriver, alertListener, PREFIX), - is(true)); - } - - @Test - public void contextEventListener() { - assertThat(super.assertThatConrextListenerWorks(emptyWebDriver, contextListener, PREFIX), - is(true)); - } - - @Test - public void rotationEventListener() { - assertThat(super.assertThatRotationListenerWorks(emptyWebDriver, rotationListener, PREFIX), - is(true)); - } - - @Test - public void windowEventListener() { - assertThat(super.assertThatWindowListenerWorks(emptyWebDriver, windowListener, PREFIX), - is(true)); - } -} diff --git a/src/test/java/io/appium/java_client/events/BaseListenerTest.java b/src/test/java/io/appium/java_client/events/BaseListenerTest.java deleted file mode 100644 index 6d8ef0170..000000000 --- a/src/test/java/io/appium/java_client/events/BaseListenerTest.java +++ /dev/null @@ -1,286 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.Matchers.contains; -import static org.junit.Assert.assertThat; - -import io.appium.java_client.MobileBy; -import io.appium.java_client.events.listeners.TestListener; -import org.openqa.selenium.Alert; -import org.openqa.selenium.By; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.Point; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; - -import java.net.URL; -import java.util.List; - -public class BaseListenerTest { - - protected boolean assertThatSearchListenerWorks(EmptyWebDriver driver, TestListener listener, - String prefix) { - try { - driver.findElement(By.id("someId")); - assertThat(listener.messages, - contains(prefix + "Attempt to find something using By.id: someId. The root element is null", - prefix + "The searching for something using By.id: someId has beed finished. " - + "The root element was null")); - - driver.findElements(By.id("someId2")); - - assertThat(listener.messages, - contains(prefix + "Attempt to find something using By.id: someId. The root element is null", - prefix + "The searching for something using By.id: someId has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.id: someId2. The root element is null", - prefix + "The searching for something using By.id: someId2 has beed finished. " - + "The root element was null")); - - driver.findElement(By.id("someId")).findElement(By.className("someClazz")); - - assertThat(listener.messages, - contains(prefix + "Attempt to find something using By.id: someId. The root element is null", - prefix + "The searching for something using By.id: someId has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.id: someId2. The root element is null", - prefix + "The searching for something using By.id: someId2 has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.id: someId. " - + "The root element is null", - prefix + "The searching for something using By.id: someId has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.className: someClazz. " - + "The root element is io.appium.java_client.events.StubWebElement", - prefix + "The searching for something using By.className: someClazz has beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement")); - - driver.findElements(By.id("someId2")).get(0).findElements(By.className("someClazz2")); - assertThat(listener.messages, - contains(prefix + "Attempt to find something using By.id: someId. The root element is null", - prefix + "The searching for something using By.id: someId has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.id: someId2. The root element is null", - prefix + "The searching for something using By.id: someId2 has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.id: someId. The root element is null", - prefix + "The searching for something using By.id: someId has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.className: someClazz. " - + "The root element is io.appium.java_client.events.StubWebElement", - prefix + "The searching for something using By.className: someClazz has beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement", - prefix + "Attempt to find something using By.id: someId2. The root element is null", - prefix + "The searching for something using By.id: someId2 has beed finished. " - + "The root element was null", - prefix + "Attempt to find something using By.className: someClazz2. " - + "The root element is io.appium.java_client.events.StubWebElement", - prefix + "The searching for something using By.className: someClazz2 has beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement")); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatSearchListenerWorksAgainstElements(EmptyWebDriver driver, TestListener listener, - String prefix) { - try { - List els = driver.findElementsByAccessibilityId("SomeAccessibility"); - StubWebElement e = driver.findElementByXPath("Some Path"); - - e.findElementByAccessibilityId("SomeAccessibility") - .findElement(MobileBy.AndroidUIAutomator("Android UI Automator")); - - assertThat(listener.messages, - contains(prefix + "Attempt to find something using By.AndroidUIAutomator: Android UI Automator. " - + "The root element is io.appium.java_client.events.StubWebElement", - prefix + "The searching for something using By.AndroidUIAutomator: " - + "Android UI Automator has " - + "beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement")); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatNavigationListenerWorks(EmptyWebDriver driver, - TestListener listener, String prefix) throws Exception { - try { - driver.get("www.google.com"); - driver.navigate().to("www.google2.com"); - driver.navigate().to(new URL("https://www.google3.com")); - driver.navigate().forward(); - driver.navigate().back(); - driver.navigate().refresh(); - - assertThat(listener.messages, - contains(prefix + "Attempt to navigate to www.google.com", - prefix + "Navigation to www.google.com was successful", - prefix + "Attempt to navigate to www.google2.com", - prefix + "Navigation to www.google2.com was successful", - prefix + "Attempt to navigate to https://www.google3.com", - prefix + "Navigation to https://www.google3.com was successful", - prefix + "Attempt to navigate forward", - prefix + "Navigation forward was successful", - prefix + "Attempt to navigate back", - prefix + "Navigation back was successful", - prefix + "Attempt to refresh", - prefix + "The refreshing was successful")); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatElementListenerWorks(EmptyWebDriver driver, TestListener listener, String prefix) { - try { - StubWebElement e = driver.findElementByXPath("Some Path"); - e.click(); - e.sendKeys("Test keys"); - - assertThat(listener.messages, - contains(prefix + "Attempt to click on the element", - prefix + "Thee element was clicked", - prefix + "Attempt to change value of the element", - prefix + "The value of the element was changed")); - return true; - } finally { - listener.messages.clear(); - } - } - - - protected boolean assertThatJavaScriptListenerWorks(EmptyWebDriver driver, TestListener listener, String prefix) { - try { - driver.executeScript("Some test script"); - driver.executeAsyncScript("Some test async script"); - - assertThat(listener.messages, - contains(prefix + "Attempt to perform java script: Some test script", - prefix + "Java script Some test script was performed", - prefix + "Attempt to perform java script: Some test async script", - prefix + "Java script Some test async script was performed")); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatExceptionListenerWorks(EmptyWebDriver driver, TestListener listener, String prefix) { - try { - try { - driver.getPageSource(); - } catch (Exception ignored) { - ignored.printStackTrace(); - } - - try { - driver.findElementByXPath("Some Path").getRect(); - } catch (Exception ignored) { - ignored.printStackTrace(); - } - - assertThat(listener.messages, - contains(prefix + "The exception was thrown: " - + WebDriverException.class, - prefix + "The exception was thrown: " - + WebDriverException.class)); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatAlertListenerWorks(EmptyWebDriver driver, TestListener listener, String prefix) { - try { - Alert alert = driver.switchTo().alert(); - alert.accept(); - alert.dismiss(); - alert.sendKeys("Keys"); - - assertThat(listener.messages, - contains(prefix + "Attempt to accept alert", - prefix + "The alert was accepted", - prefix + "Attempt to dismiss alert", - prefix + "The alert was dismissed", - prefix + "Attempt to send keys to alert", - prefix + "Keys were sent to alert")); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatConrextListenerWorks(EmptyWebDriver driver, TestListener listener, String prefix) { - try { - driver.context("NATIVE_APP"); - driver.context("WEB_VIEW"); - - assertThat(listener.messages, - contains(prefix + "Attempt to change current context to NATIVE_APP", - prefix + "The previous context has been changed to NATIVE_APP", - prefix + "Attempt to change current context to WEB_VIEW", - prefix + "The previous context has been changed to WEB_VIEW")); - return true; - } finally { - listener.messages.clear(); - } - } - - - protected boolean assertThatRotationListenerWorks(EmptyWebDriver driver, TestListener listener, - String prefix) { - try { - driver.rotate(ScreenOrientation.LANDSCAPE); - driver.rotate(ScreenOrientation.PORTRAIT); - - assertThat(listener.messages, - contains(prefix + "Attempt to change screen orientation. The new screen orientation is " - + ScreenOrientation.LANDSCAPE.toString(), - prefix + "The screen orientation has been changed to " - + ScreenOrientation.LANDSCAPE.toString(), - prefix + "Attempt to change screen orientation. The new screen orientation is " - + ScreenOrientation.PORTRAIT.toString(), - prefix + "The screen orientation has been changed to " - + ScreenOrientation.PORTRAIT.toString())); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatWindowListenerWorks(EmptyWebDriver driver, TestListener listener, String prefix) { - try { - WebDriver.Window window = driver.manage().window(); - Dimension d = new Dimension(500, 500); - window.setSize(d); - - Point p = new Point(50, 50); - window.setPosition(p); - - window.maximize(); - - driver.switchTo().window("Test window"); - - assertThat(listener.messages, - contains(prefix + "Attempt to change size of the window. The height is " + d.getHeight() - + " the width is " + d.getWidth(), - prefix + "Size of the window has been changed. The height is " + d.getHeight() - + " the width is " + d.getWidth(), - prefix + "Attempt to change position of the window. The X is " + p.getX() - + " the Y is " + p.getY(), - prefix + "The position the window has been changed. The X is " + p.getX() - + " the Y is " + p.getY(), - prefix + "Attempt to maximize the window.", - prefix + "The window has been maximized", - prefix + "Attempt to switch to window Test window", - prefix + "driver is switched to window Test window")); - return true; - } finally { - listener.messages.clear(); - } - - } -} diff --git a/src/test/java/io/appium/java_client/events/CustomListener.java b/src/test/java/io/appium/java_client/events/CustomListener.java new file mode 100644 index 000000000..e9d446462 --- /dev/null +++ b/src/test/java/io/appium/java_client/events/CustomListener.java @@ -0,0 +1,73 @@ +/* + * 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.events; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.events.WebDriverListener; + +import java.lang.reflect.Method; + +public class CustomListener implements WebDriverListener { + private boolean didCallBeforeGet = false; + private boolean didCallAfterGet = false; + private String url; + + private boolean didCallBeforeAnyWebDriverCall = false; + private boolean didCallAfterWebDriverAnyCall = false; + + @Override + public void beforeGet(WebDriver driver, String url) { + didCallBeforeGet = true; + this.url = url; + } + + @Override + public void afterGet(WebDriver driver, String url) { + didCallAfterGet = true; + this.url = url; + } + + @Override + public void beforeAnyWebDriverCall(WebDriver driver, Method method, Object[] args) { + didCallBeforeAnyWebDriverCall = true; + } + + @Override + public void afterAnyWebDriverCall(WebDriver driver, Method method, Object[] args, Object result) { + didCallAfterWebDriverAnyCall = true; + } + + public boolean isDidCallBeforeGet() { + return didCallBeforeGet; + } + + public boolean isDidCallAfterGet() { + return didCallAfterGet; + } + + public String getUrl() { + return url; + } + + public boolean isDidCallBeforeAnyWebDriverCall() { + return didCallBeforeAnyWebDriverCall; + } + + public boolean isDidCallAfterWebDriverAnyCall() { + return didCallAfterWebDriverAnyCall; + } +} \ No newline at end of file diff --git a/src/test/java/io/appium/java_client/events/DefaultEventListenerTest.java b/src/test/java/io/appium/java_client/events/DefaultEventListenerTest.java deleted file mode 100644 index a2454edca..000000000 --- a/src/test/java/io/appium/java_client/events/DefaultEventListenerTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; - -import io.appium.java_client.events.listeners.AlertListener; -import io.appium.java_client.events.listeners.ContextListener; -import io.appium.java_client.events.listeners.ElementListener; -import io.appium.java_client.events.listeners.ExceptionListener; -import io.appium.java_client.events.listeners.JavaScriptListener; -import io.appium.java_client.events.listeners.NavigationListener; -import io.appium.java_client.events.listeners.RotationListener; -import io.appium.java_client.events.listeners.SearchingListener; -import io.appium.java_client.events.listeners.SingleListeners; -import io.appium.java_client.events.listeners.WindowListener; -import org.apache.commons.lang3.StringUtils; -import org.junit.BeforeClass; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.openqa.selenium.Capabilities; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class DefaultEventListenerTest extends BaseListenerTest { - - private static EmptyWebDriver driver; - - @BeforeClass public static void beforeClass() { - EmptyWebDriver emptyWebDriver = new EmptyWebDriver(); - driver = EventFiringWebDriverFactory.getEventFiringWebDriver(emptyWebDriver); - } - - @Test - public void searchContextEventTest() { - assertThat(super.assertThatSearchListenerWorks(driver, SingleListeners - .listeners.get(SearchingListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void searchContextEventTest2() { - assertThat(super.assertThatSearchListenerWorksAgainstElements(driver, SingleListeners - .listeners.get(SearchingListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void navigationEventTest() throws Exception { - assertThat(super.assertThatNavigationListenerWorks(driver, SingleListeners - .listeners.get(NavigationListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void elementEventTest() { - assertThat(super.assertThatElementListenerWorks(driver, SingleListeners - .listeners.get(ElementListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void javaScriptEventTest() { - assertThat(super.assertThatJavaScriptListenerWorks(driver, SingleListeners - .listeners.get(JavaScriptListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void exceptionEventTest() { - assertThat(super.assertThatExceptionListenerWorks(driver, SingleListeners - .listeners.get(ExceptionListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void alertEventTest() { - assertThat(super.assertThatAlertListenerWorks(driver, SingleListeners - .listeners.get(AlertListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void contextEventListener() { - assertThat(super.assertThatConrextListenerWorks(driver, SingleListeners - .listeners.get(ContextListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void rotationEventListener() { - assertThat(super.assertThatRotationListenerWorks(driver, SingleListeners - .listeners.get(RotationListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void windowEventListener() { - assertThat(super.assertThatWindowListenerWorks(driver, SingleListeners - .listeners.get(WindowListener.class), StringUtils.EMPTY), is(true)); - } - - @Test - public void whenNonListenableObjectIsReturned() { - Capabilities capabilities = driver.getCapabilities(); - assertNotNull(capabilities); - assertEquals(capabilities.asMap().size(), 2); - } -} diff --git a/src/test/java/io/appium/java_client/events/EmptyWebDriver.java b/src/test/java/io/appium/java_client/events/EmptyWebDriver.java deleted file mode 100644 index 460c0b773..000000000 --- a/src/test/java/io/appium/java_client/events/EmptyWebDriver.java +++ /dev/null @@ -1,338 +0,0 @@ -package io.appium.java_client.events; - -import com.google.common.collect.ImmutableList; - -import io.appium.java_client.FindsByAccessibilityId; -import io.appium.java_client.FindsByAndroidUIAutomator; -import io.appium.java_client.FindsByFluentSelector; - -import org.apache.commons.lang3.StringUtils; -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; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.internal.FindsByClassName; -import org.openqa.selenium.internal.FindsByCssSelector; -import org.openqa.selenium.internal.FindsById; -import org.openqa.selenium.internal.FindsByLinkText; -import org.openqa.selenium.internal.FindsByTagName; -import org.openqa.selenium.internal.FindsByXPath; -import org.openqa.selenium.logging.Logs; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.net.URL; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class EmptyWebDriver implements WebDriver, ContextAware, Rotatable, FindsByClassName, - FindsByCssSelector, FindsById, FindsByLinkText, FindsByTagName, FindsByXPath, - FindsByAccessibilityId, FindsByAndroidUIAutomator, - JavascriptExecutor, HasCapabilities, FindsByFluentSelector, TakesScreenshot { - - private static List createStubList() { - return ImmutableList.of(new StubWebElement(), new StubWebElement()); - } - - @Override public WebDriver context(String name) { - return null; - } - - @Override public Set getContextHandles() { - return null; - } - - @Override public String getContext() { - return StringUtils.EMPTY; - } - - @Override public void rotate(ScreenOrientation orientation) { - //The rotation does nothing there - } - - @Override public void rotate(DeviceRotation rotation) { - //The rotation does nothing there - } - - @Override public ScreenOrientation getOrientation() { - return null; - } - - @Override public DeviceRotation rotation() { - return null; - } - - @Override public void get(String url) { - //There is no navigation. It is added only for event firing - } - - @Override public String getCurrentUrl() { - return null; - } - - @Override public String getTitle() { - return null; - } - - @Override public StubWebElement findElement(By by) { - return new StubWebElement(); - } - - @Override - public StubWebElement findElement(String by, String using) throws WebDriverException, NoSuchElementException { - return new StubWebElement(); - } - - @Override public List findElements(By by) { - return createStubList(); - } - - @Override - public List findElements(String by, String using) throws WebDriverException { - return createStubList(); - } - - @Override public String getPageSource() { - throw new WebDriverException(); - } - - @Override public void close() { - //There is no closing - } - - @Override public void quit() { - //It is only the stub - } - - @Override public Set getWindowHandles() { - return null; - } - - @Override public String getWindowHandle() { - throw new WebDriverException(); - } - - @Override public TargetLocator switchTo() { - return new StubTargetLocator(this); - } - - @Override public Navigation navigate() { - return new StubNavigation(); - } - - @Override public Options manage() { - return new StubOptions(); - } - - @Override public StubWebElement findElementByClassName(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByClassName(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementByCssSelector(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByCssSelector(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementById(String using) { - return new StubWebElement(); - } - - @Override public List findElementsById(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementByLinkText(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByLinkText(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementByPartialLinkText(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByPartialLinkText(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementByTagName(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByTagName(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementByXPath(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByXPath(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementByAccessibilityId(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByAccessibilityId(String using) { - return createStubList(); - } - - @Override public StubWebElement findElementByAndroidUIAutomator(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByAndroidUIAutomator(String using) { - return createStubList(); - } - - @Override public Object executeScript(String script, Object... args) { - return null; - } - - @Override public Object executeAsyncScript(String script, Object... args) { - return null; - } - - @Override public Capabilities getCapabilities() { - Map map = new HashMap<>(); - map.put("0",StringUtils.EMPTY); - map.put("1",StringUtils.EMPTY); - return new DesiredCapabilities(map); - } - - @Override - public X getScreenshotAs(OutputType target) throws WebDriverException { - return target.convertFromPngBytes(new byte[]{1,2}); - } - - private class StubTargetLocator implements TargetLocator { - - private final WebDriver driver; - - StubTargetLocator(WebDriver driver) { - this.driver = driver; - } - - @Override public WebDriver frame(int index) { - return driver; - } - - @Override public WebDriver frame(String nameOrId) { - return driver; - } - - @Override public WebDriver frame(WebElement frameElement) { - return driver; - } - - @Override public WebDriver parentFrame() { - return driver; - } - - @Override public WebDriver window(String nameOrHandle) { - return driver; - } - - @Override public WebDriver defaultContent() { - return driver; - } - - @Override public WebElement activeElement() { - return new StubWebElement(); - } - - @Override public Alert alert() { - return new StubAlert(); - } - } - - private class StubOptions implements Options { - - @Override public void addCookie(Cookie cookie) { - //STUB: No adding cookie - } - - @Override public void deleteCookieNamed(String name) { - //STUB No removal cookie - } - - @Override public void deleteCookie(Cookie cookie) { - //STUB No deleting cookie - } - - @Override public void deleteAllCookies() { - //STUB it does nothing - } - - @Override public Set getCookies() { - return null; - } - - @Override public Cookie getCookieNamed(String name) { - return null; - } - - @Override public Timeouts timeouts() { - return null; - } - - @Override public ImeHandler ime() { - return null; - } - - @Override public Window window() { - return new StubWindow(); - } - - @Override public Logs logs() { - return null; - } - } - - private class StubNavigation implements Navigation { - - @Override public void back() { - //STUB: It doesn't navigate back - } - - @Override public void forward() { - //STUB: No the navigation forward - } - - @Override public void to(String url) { - //STUB: Added only for event firing - } - - @Override public void to(URL url) { - //STUB: Event firing of the navigation to some URL - } - - @Override public void refresh() { - //STUB: The firing of the refreshing - } - } -} diff --git a/src/test/java/io/appium/java_client/events/EventsFiringTest.java b/src/test/java/io/appium/java_client/events/EventsFiringTest.java new file mode 100644 index 000000000..cd00dce37 --- /dev/null +++ b/src/test/java/io/appium/java_client/events/EventsFiringTest.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.events; + +import io.appium.java_client.events.stubs.EmptyWebDriver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.events.EventFiringDecorator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EventsFiringTest { + private final WebDriver emptyWebDriver = new EmptyWebDriver(); + private CustomListener listener; + private WebDriver decorated; + + @BeforeEach + public void beforeTest() { + listener = new CustomListener(); + decorated = new EventFiringDecorator(listener).decorate(emptyWebDriver); + } + + @Test + public void checkBasicEventsFiring() { + decorated.get("http://example.com/"); + assertTrue(listener.isDidCallBeforeGet()); + assertTrue(listener.isDidCallAfterGet()); + assertEquals(listener.getUrl(), "http://example.com/"); + } + + @Test + public void checkAnyWebDriverEventsFiring() { + decorated.get("http://example.com/"); + assertTrue(listener.isDidCallBeforeAnyWebDriverCall()); + assertTrue(listener.isDidCallAfterWebDriverAnyCall()); + } +} diff --git a/src/test/java/io/appium/java_client/events/ExtendedEventListenerTest.java b/src/test/java/io/appium/java_client/events/ExtendedEventListenerTest.java deleted file mode 100644 index 1ea04de2d..000000000 --- a/src/test/java/io/appium/java_client/events/ExtendedEventListenerTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.core.IsIterableContaining.hasItems; -import static org.junit.Assert.assertThat; - -import io.appium.java_client.MobileBy; -import io.appium.java_client.events.listeners.SearchingListener; -import io.appium.java_client.events.listeners.SingleListeners; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.By; - -public class ExtendedEventListenerTest { - - private static EmptyWebDriver stubWebDriver; - - @BeforeClass public static void beforeClass() { - stubWebDriver = new EmptyWebDriver(); - stubWebDriver = EventFiringWebDriverFactory.getEventFiringWebDriver(stubWebDriver); - } - - @Test - public void searchingTest() { - StubWebElement androidElement = stubWebDriver.findElement(By.id("someId")); - androidElement.findElement("-some-criteria", "some value") - .findElements(MobileBy.AndroidUIAutomator("Android UI Automator")); - androidElement.findElements("-some-criteria2", "some value2").get(0) - .findElements(MobileBy.AndroidUIAutomator("Android UI Automator2")); - - SearchingListener listener = (SearchingListener) SingleListeners - .listeners.get(SearchingListener.class); - assertThat(listener.messages, - hasItems("Attempt to find something using By.AndroidUIAutomator: Android UI Automator. " - + "The root element is io.appium.java_client.events.StubWebElement", - "The searching for something using By.AndroidUIAutomator: Android UI Automator has " - + "beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement", - "Attempt to find something using By.AndroidUIAutomator: Android UI Automator2. " - + "The root element is io.appium.java_client.events.StubWebElement", - "The searching for something using By.AndroidUIAutomator: Android UI Automator2 " - + "has beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement")); - } -} diff --git a/src/test/java/io/appium/java_client/events/FewInstancesTest.java b/src/test/java/io/appium/java_client/events/FewInstancesTest.java deleted file mode 100644 index 4b5f7dcf9..000000000 --- a/src/test/java/io/appium/java_client/events/FewInstancesTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThat; - -import io.appium.java_client.events.listeners.AlertListener; -import io.appium.java_client.events.listeners.ContextListener; -import io.appium.java_client.events.listeners.ElementListener; -import io.appium.java_client.events.listeners.ExceptionListener; -import io.appium.java_client.events.listeners.JavaScriptListener; -import io.appium.java_client.events.listeners.NavigationListener; -import io.appium.java_client.events.listeners.RotationListener; -import io.appium.java_client.events.listeners.SearchingListener; -import io.appium.java_client.events.listeners.SingleListeners; -import io.appium.java_client.events.listeners.WindowListener; -import org.apache.commons.lang3.StringUtils; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.WebDriver; - -public class FewInstancesTest extends BaseListenerTest { - - private static EmptyWebDriver emptyWebDriver1; - private static SearchingListener searchingListener1; - private static NavigationListener navigationListener1; - private static ElementListener elementListener1; - private static JavaScriptListener javaScriptListener1; - private static ExceptionListener exceptionListener1; - private static AlertListener alertListener1; - private static ContextListener contextListener1; - private static RotationListener rotationListener1; - private static WindowListener windowListener1; - - private static SearchingListener searchingListener2; - private static NavigationListener navigationListener2; - private static ElementListener elementListener2; - private static JavaScriptListener javaScriptListener2; - private static ExceptionListener exceptionListener2; - private static AlertListener alertListener2; - private static ContextListener contextListener2; - private static RotationListener rotationListener2; - private static WindowListener windowListener2; - - @BeforeClass public static void beforeClass() { - emptyWebDriver1 = new EmptyWebDriver(); - emptyWebDriver1 = EventFiringWebDriverFactory.getEventFiringWebDriver(emptyWebDriver1); - - searchingListener1 = (SearchingListener) SingleListeners - .listeners.get(SearchingListener.class); - navigationListener1 = (NavigationListener) SingleListeners - .listeners.get(NavigationListener.class); - elementListener1 = (ElementListener) SingleListeners - .listeners.get(ElementListener.class); - javaScriptListener1 = (JavaScriptListener) SingleListeners - .listeners.get(JavaScriptListener.class); - exceptionListener1 = (ExceptionListener) SingleListeners - .listeners.get(ExceptionListener.class); - alertListener1 = (AlertListener) SingleListeners - .listeners.get(AlertListener.class); - contextListener1 = (ContextListener) SingleListeners - .listeners.get(ContextListener.class); - rotationListener1 = (RotationListener) SingleListeners - .listeners.get(RotationListener.class); - windowListener1 = - (WindowListener) SingleListeners.listeners.get(WindowListener.class); - - WebDriver stubWebDriver2 = new EmptyWebDriver(); - EventFiringWebDriverFactory.getEventFiringWebDriver(stubWebDriver2); - - searchingListener2 = (SearchingListener) SingleListeners - .listeners.get(SearchingListener.class); - navigationListener2 = (NavigationListener) SingleListeners - .listeners.get(NavigationListener.class); - elementListener2 = (ElementListener) SingleListeners - .listeners.get(ElementListener.class); - javaScriptListener2 = (JavaScriptListener) SingleListeners - .listeners.get(JavaScriptListener.class); - exceptionListener2 = (ExceptionListener) SingleListeners - .listeners.get(ExceptionListener.class); - alertListener2 = (AlertListener) SingleListeners - .listeners.get(AlertListener.class); - contextListener2 = (ContextListener) SingleListeners - .listeners.get(ContextListener.class); - rotationListener2 = (RotationListener) SingleListeners - .listeners.get(RotationListener.class); - windowListener2 = - (WindowListener) SingleListeners.listeners.get(WindowListener.class); - } - - @Test - public void listenersAreDifferent() { - assertNotEquals(searchingListener1, searchingListener2); - assertNotEquals(elementListener1, elementListener2); - assertNotEquals(navigationListener1, navigationListener2); - assertNotEquals(javaScriptListener1, javaScriptListener2); - assertNotEquals(exceptionListener1, exceptionListener2); - assertNotEquals(alertListener1, alertListener2); - assertNotEquals(contextListener1, contextListener2); - assertNotEquals(rotationListener1, rotationListener2); - assertNotEquals(windowListener1, windowListener2); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother() { - assertThat(super.assertThatSearchListenerWorks(emptyWebDriver1, - searchingListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second searching listener should have no messages", - searchingListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother2() { - assertThat(super.assertThatSearchListenerWorksAgainstElements(emptyWebDriver1, - searchingListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second searching listener should have no messages", - searchingListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother3() throws Exception { - assertThat(super.assertThatNavigationListenerWorks(emptyWebDriver1, - navigationListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second navigation listener should have no messages", - navigationListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother4() { - assertThat(super.assertThatJavaScriptListenerWorks(emptyWebDriver1, - javaScriptListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second java script listener should have no messages", - javaScriptListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother5() { - assertThat(super.assertThatExceptionListenerWorks(emptyWebDriver1, - exceptionListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second exception listener should have no messages", - exceptionListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother6() { - assertThat(super.assertThatAlertListenerWorks(emptyWebDriver1, - alertListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second alert listener should have no messages", - alertListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother7() { - assertThat(super.assertThatConrextListenerWorks(emptyWebDriver1, - contextListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second context listener should have no messages", - contextListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother8() { - assertThat(super.assertThatRotationListenerWorks(emptyWebDriver1, - rotationListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second rotation listener should have no messages", - rotationListener2.messages.size(), is(0)); - } - - @Test - public void assertThatOneDriverDoNotListensToAnother9() { - assertThat(super.assertThatWindowListenerWorks(emptyWebDriver1, - windowListener1, StringUtils.EMPTY), - is(true)); - assertThat("The second window listener should have no messages", - windowListener2.messages.size(), is(0)); - } -} diff --git a/src/test/java/io/appium/java_client/events/ListenableObjectTest.java b/src/test/java/io/appium/java_client/events/ListenableObjectTest.java deleted file mode 100644 index 4d7002390..000000000 --- a/src/test/java/io/appium/java_client/events/ListenableObjectTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package io.appium.java_client.events; - -import static com.google.common.collect.ImmutableSortedSet.of; -import static io.appium.java_client.events.EventFiringObjectFactory.getEventFiringObject; -import static io.appium.java_client.events.EventFiringWebDriverFactory.getEventFiringWebDriver; -import static io.appium.java_client.events.listeners.SingleListeners.listeners; -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.hamcrest.Matchers.contains; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.events.listeners.AlertListener; -import io.appium.java_client.events.listeners.AlertListener2; -import io.appium.java_client.events.listeners.ContextListener; -import io.appium.java_client.events.listeners.ContextListener2; -import io.appium.java_client.events.listeners.SearchingListener; -import io.appium.java_client.events.listeners.SearchingListener2; -import org.junit.Test; -import org.openqa.selenium.Alert; -import org.openqa.selenium.By; -import org.openqa.selenium.ContextAware; -import org.openqa.selenium.WebDriver; - -import java.util.Set; -import java.util.function.Predicate; - -public class ListenableObjectTest { - private static final String PREFIX = "Externally defined listener: "; - - private final EmptyWebDriver emptyWebDriver = new EmptyWebDriver(); - private final ContextListener2 contextListener = new ContextListener2(); - private final AlertListener2 alertListener = new AlertListener2(); - private final SearchingListener2 searchingListener = new SearchingListener2(); - - private final ContextAware contextAware = new ContextAware() { - @Override - public WebDriver context(String name) { - return emptyWebDriver; - } - - @Override - public Set getContextHandles() { - return of(EMPTY); - } - - @Override - public String getContext() { - return EMPTY; - } - }; - - private final Predicate contextAwarePredicate = (contextAware) -> { - contextAware.context("WEB_VIEW"); - - assertThat(contextListener.messages, - contains(PREFIX + "Attempt to change current context to NATIVE_APP", - PREFIX + "The previous context has been changed to NATIVE_APP", - PREFIX + "Attempt to change current context to WEB_VIEW", - PREFIX + "The previous context has been changed to WEB_VIEW")); - - ContextListener singleContextListener = (ContextListener) - listeners.get(ContextListener.class); - assertThat(singleContextListener.messages, - contains("Attempt to change current context to NATIVE_APP", - "The previous context has been changed to NATIVE_APP", - "Attempt to change current context to WEB_VIEW", - "The previous context has been changed to WEB_VIEW")); - return true; - }; - - private final Predicate alertPredicate = alert -> { - alert.accept(); - alert.dismiss(); - alert.sendKeys("Keys"); - - assertThat(alertListener.messages, - contains(PREFIX + "Attempt to accept alert", - PREFIX + "The alert was accepted", - PREFIX + "Attempt to dismiss alert", - PREFIX + "The alert was dismissed", - PREFIX + "Attempt to send keys to alert", - PREFIX + "Keys were sent to alert")); - - AlertListener singleAlertListener = (AlertListener) - listeners.get(AlertListener.class); - - assertThat(singleAlertListener.messages, - contains("Attempt to accept alert", - "The alert was accepted", - "Attempt to dismiss alert", - "The alert was dismissed", - "Attempt to send keys to alert", - "Keys were sent to alert")); - return true; - }; - - private final Predicate webDriverPredicate = driver -> { - driver.findElement(By.id("someId")); - assertThat(searchingListener.messages, - contains(PREFIX + "Attempt to find something using By.id: someId. The root element is null", - PREFIX + "The searching for something using By.id: someId has beed finished. " - + "The root element was null")); - - SearchingListener singleSearchingListener = (SearchingListener) - listeners.get(SearchingListener.class); - - assertThat(singleSearchingListener.messages, - contains("Attempt to find something using By.id: someId. The root element is null", - "The searching for something using By.id: someId has beed finished. " - + "The root element was null")); - return true; - }; - - - @Test public void listenableObjectSample() { - try { - ContextAware listenableContextAware = - getEventFiringObject(contextAware, emptyWebDriver, contextListener, alertListener); - WebDriver webDriver = listenableContextAware.context("NATIVE_APP"); - assertTrue(contextAwarePredicate.test(listenableContextAware)); - - Alert alert = webDriver.switchTo().alert(); - assertTrue(alertPredicate.test(alert)); - - assertTrue(webDriverPredicate.test(getEventFiringWebDriver(webDriver, searchingListener))); - } finally { - listeners.get(ContextListener.class).messages.clear(); - listeners.get(AlertListener.class).messages.clear(); - listeners.get(SearchingListener.class).messages.clear(); - } - } -} diff --git a/src/test/java/io/appium/java_client/events/StubAlert.java b/src/test/java/io/appium/java_client/events/StubAlert.java deleted file mode 100644 index aad32c596..000000000 --- a/src/test/java/io/appium/java_client/events/StubAlert.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.appium.java_client.events; - -import org.apache.commons.lang3.StringUtils; -import org.openqa.selenium.Alert; - -public class StubAlert implements Alert { - @Override public void dismiss() { - //STUB it does nothing - } - - @Override public void accept() { - //STUB it does nothing - } - - @Override public String getText() { - return StringUtils.EMPTY; - } - - @Override public void sendKeys(String keysToSend) { - //STUB it does nothing - } -} diff --git a/src/test/java/io/appium/java_client/events/StubWebElement.java b/src/test/java/io/appium/java_client/events/StubWebElement.java deleted file mode 100644 index 36179206d..000000000 --- a/src/test/java/io/appium/java_client/events/StubWebElement.java +++ /dev/null @@ -1,188 +0,0 @@ -package io.appium.java_client.events; - -import com.google.common.collect.ImmutableList; - -import io.appium.java_client.FindsByAccessibilityId; -import io.appium.java_client.FindsByAndroidUIAutomator; -import io.appium.java_client.FindsByFluentSelector; -import org.openqa.selenium.By; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.NoSuchElementException; -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 org.openqa.selenium.internal.FindsByClassName; -import org.openqa.selenium.internal.FindsByCssSelector; -import org.openqa.selenium.internal.FindsById; -import org.openqa.selenium.internal.FindsByLinkText; -import org.openqa.selenium.internal.FindsByTagName; -import org.openqa.selenium.internal.FindsByXPath; - -import java.util.ArrayList; -import java.util.List; - -public class StubWebElement implements WebElement, FindsByClassName, FindsByCssSelector, FindsById, - FindsByLinkText, FindsByTagName, FindsByXPath, FindsByAccessibilityId, - FindsByAndroidUIAutomator, FindsByFluentSelector { - - private static List createStubSubElementList() { - return new ArrayList<>(ImmutableList.of(new StubWebElement(), new StubWebElement())); - } - - - @Override public WebElement findElementByAccessibilityId(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByAccessibilityId(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementByAndroidUIAutomator(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByAndroidUIAutomator(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementByClassName(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByClassName(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementByCssSelector(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByCssSelector(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementById(String using) { - return new StubWebElement(); - } - - @Override public List findElementsById(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementByLinkText(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByLinkText(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementByPartialLinkText(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByPartialLinkText(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementByTagName(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByTagName(String using) { - return createStubSubElementList(); - } - - @Override public WebElement findElementByXPath(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByXPath(String using) { - return createStubSubElementList(); - } - - @Override public void click() { - //There is no clicking. It is STUB. - } - - @Override public void submit() { - //No submitting - } - - @Override public void sendKeys(CharSequence... keysToSend) { - //There is no the sending keys. - } - - @Override public void clear() { - //It doesn't clearing anything. - } - - @Override public String getTagName() { - return null; - } - - @Override public String getAttribute(String name) { - return null; - } - - @Override public boolean isSelected() { - return false; - } - - @Override public boolean isEnabled() { - return false; - } - - @Override public String getText() { - return null; - } - - @Override public List findElements(By by) { - return createStubSubElementList(); - } - - @Override - public List findElements(String by, String using) throws WebDriverException { - return createStubSubElementList(); - } - - @Override public WebElement findElement(By by) { - return new StubWebElement(); - } - - @Override - public WebElement findElement(String by, String using) throws WebDriverException, NoSuchElementException { - return new StubWebElement(); - } - - @Override public boolean isDisplayed() { - return false; - } - - @Override public Point getLocation() { - return null; - } - - @Override public Dimension getSize() { - return null; - } - - @Override public Rectangle getRect() { - throw new WebDriverException(); - } - - @Override public String getCssValue(String propertyName) { - return null; - } - - @Override public X getScreenshotAs(OutputType target) throws WebDriverException { - return target.convertFromPngBytes(new byte[]{1,2}); - } - - @Override public String toString() { - return this.getClass().getCanonicalName(); - } -} diff --git a/src/test/java/io/appium/java_client/events/StubWindow.java b/src/test/java/io/appium/java_client/events/StubWindow.java deleted file mode 100644 index 066f2d08f..000000000 --- a/src/test/java/io/appium/java_client/events/StubWindow.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.appium.java_client.events; - -import org.openqa.selenium.Dimension; -import org.openqa.selenium.Point; -import org.openqa.selenium.WebDriver; - -public class StubWindow implements WebDriver.Window { - @Override public void setSize(Dimension targetSize) { - //STUB it does nothing - } - - @Override public void setPosition(Point targetPosition) { - //STUB it does nothing - } - - @Override public Dimension getSize() { - return null; - } - - @Override public Point getPosition() { - return null; - } - - @Override public void maximize() { - //STUB it does nothing - } - - @Override public void fullscreen() { - //STUB it does nothing - } -} diff --git a/src/test/java/io/appium/java_client/events/WebDriverEventListenerCompatibilityTest.java b/src/test/java/io/appium/java_client/events/WebDriverEventListenerCompatibilityTest.java deleted file mode 100644 index 0380b7a59..000000000 --- a/src/test/java/io/appium/java_client/events/WebDriverEventListenerCompatibilityTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsIterableContaining.hasItems; -import static org.junit.Assert.assertThat; -import static org.openqa.selenium.By.id; -import static org.openqa.selenium.By.xpath; -import static org.openqa.selenium.OutputType.BASE64; - -import io.appium.java_client.events.listeners.AppiumListener; -import io.appium.java_client.events.listeners.SingleListeners; -import org.junit.BeforeClass; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.openqa.selenium.Alert; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class WebDriverEventListenerCompatibilityTest extends BaseListenerTest { - - private static EmptyWebDriver driver; - private static AppiumListener listener; - private static final String WEBDRIVER_EVENT_LISTENER = "WebDriverEventListener: "; - - @BeforeClass public static void beforeClass() { - EmptyWebDriver emptyWebDriver = new EmptyWebDriver(); - driver = EventFiringWebDriverFactory.getEventFiringWebDriver(emptyWebDriver); - listener = (AppiumListener) SingleListeners.listeners.get(AppiumListener.class); - } - - @Test - public void searchContextEventTest() { - assertThat(super.assertThatSearchListenerWorks(driver, listener, WEBDRIVER_EVENT_LISTENER), - is(true)); - } - - @Test - public void searchContextEventTest2() { - assertThat(super.assertThatSearchListenerWorksAgainstElements(driver, - listener, WEBDRIVER_EVENT_LISTENER), - is(true)); - } - - @Test - public void navigationEventTest() throws Exception { - assertThat(super.assertThatNavigationListenerWorks(driver, - listener, WEBDRIVER_EVENT_LISTENER), - is(true)); - } - - @Test - public void elementEventTest() { - assertThat(super.assertThatElementListenerWorks(driver, listener, WEBDRIVER_EVENT_LISTENER), - is(true)); - } - - @Test - public void javaScriptEventTest() { - assertThat(super.assertThatJavaScriptListenerWorks(driver, - listener, WEBDRIVER_EVENT_LISTENER), - is(true)); - } - - @Test - public void alertEventTest() { - try { - Alert alert = driver.switchTo().alert(); - alert.accept(); - alert.dismiss(); - alert.sendKeys("Keys"); - - assertThat(listener.messages, - hasItems(WEBDRIVER_EVENT_LISTENER + "Attempt to accept alert", - WEBDRIVER_EVENT_LISTENER + "The alert was accepted", - WEBDRIVER_EVENT_LISTENER + "Attempt to dismiss alert", - WEBDRIVER_EVENT_LISTENER + "The alert was dismissed")); - } finally { - listener.messages.clear(); - } - } - - @Test - public void takeScreenShotEventTest() { - try { - driver.getScreenshotAs(BASE64); - driver.findElement(xpath(".//some//path")).findElement(id("someId")).getScreenshotAs(BASE64); - - assertThat(listener.messages, - contains("WebDriverEventListener: Attempt to take screen shot. Target type is OutputType.BASE64", - "WebDriverEventListener: Screen shot was taken successfully. Target type is " - + "OutputType.BASE64, result is AQI=", - "WebDriverEventListener: Attempt to find something using By.xpath: .//some//path. " - + "The root element is null", - "WebDriverEventListener: The searching for something using " - + "By.xpath: .//some//path has beed finished. " - + "The root element was null", - "WebDriverEventListener: Attempt to find something using By.id: someId. " - + "The root element is io.appium.java_client.events.StubWebElement", - "WebDriverEventListener: The searching for something using " - + "By.id: someId has beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement", - "WebDriverEventListener: Attempt to take screen shot. Target type is OutputType.BASE64", - "WebDriverEventListener: Screen shot was taken successfully. " - + "Target type is OutputType.BASE64, result is AQI=")); - } finally { - listener.messages.clear(); - } - } - - @Test - public void exceptionEventTest() { - assertThat(super.assertThatExceptionListenerWorks(driver, listener, WEBDRIVER_EVENT_LISTENER), - is(true)); - } - - @Test - public void windowListenerTest() { - try { - driver.switchTo().window("Test window"); - assertThat(listener.messages, - hasItems(WEBDRIVER_EVENT_LISTENER + "Attempt to switch to window Test window", - WEBDRIVER_EVENT_LISTENER + "driver is switched to window Test window")); - } finally { - listener.messages.clear(); - } - - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/AlertListener.java b/src/test/java/io/appium/java_client/events/listeners/AlertListener.java deleted file mode 100644 index 949431247..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/AlertListener.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.AlertEventListener; -import org.openqa.selenium.Alert; -import org.openqa.selenium.WebDriver; - -public class AlertListener extends TestListener implements AlertEventListener { - @Override public void beforeAlertAccept(WebDriver driver, Alert alert) { - messages.add("Attempt to accept alert"); - } - - @Override public void afterAlertAccept(WebDriver driver, Alert alert) { - messages.add("The alert was accepted"); - } - - @Override public void afterAlertDismiss(WebDriver driver, Alert alert) { - messages.add("The alert was dismissed"); - } - - @Override public void beforeAlertDismiss(WebDriver driver, Alert alert) { - messages.add("Attempt to dismiss alert"); - } - - @Override public void beforeAlertSendKeys(WebDriver driver, Alert alert, String keys) { - messages.add("Attempt to send keys to alert"); - } - - @Override public void afterAlertSendKeys(WebDriver driver, Alert alert, String keys) { - messages.add("Keys were sent to alert"); - } - - @Override protected void add() { - SingleListeners.listeners.put(AlertListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/AlertListener2.java b/src/test/java/io/appium/java_client/events/listeners/AlertListener2.java deleted file mode 100644 index 5d75ed58b..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/AlertListener2.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.AlertEventListener; -import org.openqa.selenium.Alert; -import org.openqa.selenium.WebDriver; - -public class AlertListener2 extends TestListener implements AlertEventListener { - @Override public void beforeAlertAccept(WebDriver driver, Alert alert) { - messages.add("Externally defined listener: Attempt to accept alert"); - } - - @Override public void afterAlertAccept(WebDriver driver, Alert alert) { - messages.add("Externally defined listener: The alert was accepted"); - } - - @Override public void afterAlertDismiss(WebDriver driver, Alert alert) { - messages.add("Externally defined listener: The alert was dismissed"); - } - - @Override public void beforeAlertDismiss(WebDriver driver, Alert alert) { - messages.add("Externally defined listener: Attempt to dismiss alert"); - } - - @Override public void beforeAlertSendKeys(WebDriver driver, Alert alert, String keys) { - messages.add("Externally defined listener: Attempt to send keys to alert"); - } - - @Override public void afterAlertSendKeys(WebDriver driver, Alert alert, String keys) { - messages.add("Externally defined listener: Keys were sent to alert"); - } - - @Override protected void add() { - SingleListeners.listeners.put(AlertListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/AppiumListener.java b/src/test/java/io/appium/java_client/events/listeners/AppiumListener.java deleted file mode 100644 index b74cad265..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/AppiumListener.java +++ /dev/null @@ -1,147 +0,0 @@ -package io.appium.java_client.events.listeners; - -import static java.lang.String.format; - -import io.appium.java_client.events.api.general.AppiumWebDriverEventListener; -import org.openqa.selenium.By; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class AppiumListener extends TestListener implements AppiumWebDriverEventListener { - @Override protected void add() { - SingleListeners.listeners.put(AppiumListener.class, this); - } - - @Override - public void beforeAlertAccept(WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to accept alert"); - } - - @Override - public void afterAlertAccept(WebDriver driver) { - messages.add("WebDriverEventListener: The alert was accepted"); - } - - @Override - public void afterAlertDismiss(WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to dismiss alert"); - } - - @Override - public void beforeAlertDismiss(WebDriver driver) { - messages.add("WebDriverEventListener: The alert was dismissed"); - } - - @Override public void beforeNavigateTo(String url, WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to navigate to " + url); - } - - @Override public void afterNavigateTo(String url, WebDriver driver) { - messages.add("WebDriverEventListener: Navigation to " + url + " was successful"); - } - - @Override public void beforeNavigateBack(WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to navigate back"); - } - - @Override public void afterNavigateBack(WebDriver driver) { - messages.add("WebDriverEventListener: Navigation back was successful"); - } - - @Override public void beforeNavigateForward(WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to navigate forward"); - } - - @Override public void afterNavigateForward(WebDriver driver) { - messages.add("WebDriverEventListener: Navigation forward was successful"); - } - - @Override public void beforeNavigateRefresh(WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to refresh"); - } - - @Override public void afterNavigateRefresh(WebDriver driver) { - messages.add("WebDriverEventListener: The refreshing was successful"); - } - - @Override public void beforeFindBy(By by, WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to find something using " + by.toString() - + ". The root element is " - + String.valueOf(element)); - } - - @Override public void afterFindBy(By by, WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: The searching for something using " - + by.toString() + " has beed finished. " - + "The root element was " - + String.valueOf(element)); - } - - @Override public void beforeClickOn(WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to click on the element"); - } - - @Override public void afterClickOn(WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: Thee element was clicked"); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - messages.add("WebDriverEventListener: Attempt to click on the element"); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to change value of the element"); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: The value of the element was changed"); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - messages.add("WebDriverEventListener: Thee element was clicked"); - } - - @Override public void beforeScript(String script, WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to perform java script: " + script); - } - - @Override public void afterScript(String script, WebDriver driver) { - messages.add("WebDriverEventListener: Java script " + script + " was performed"); - } - - @Override public void onException(Throwable throwable, WebDriver driver) { - messages.add("WebDriverEventListener: The exception was thrown: " + throwable.getClass()); - } - - @Override - public void beforeGetScreenshotAs(OutputType target) { - messages.add(format("WebDriverEventListener: Attempt to take screen shot. Target type is %s", target)); - } - - @Override - public void afterGetScreenshotAs(OutputType target, X screenshot) { - messages.add(format("WebDriverEventListener: Screen shot was taken successfully. " - + "Target type is %s, result is %s", target, screenshot)); - } - - @Override public void beforeGetText(WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to get text of the element"); - } - - @Override public void afterGetText(WebElement element, WebDriver driver, String text) { - messages.add("WebDriverEventListener: Got the text of an element"); - } - - @Override - public void beforeSwitchToWindow(String windowName, WebDriver driver) { - messages.add(format("WebDriverEventListener: Attempt to switch to window %s", windowName)); - } - - @Override - public void afterSwitchToWindow(String windowName, WebDriver driver) { - messages.add(format("WebDriverEventListener: driver is switched to window %s", windowName)); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/ContextListener.java b/src/test/java/io/appium/java_client/events/listeners/ContextListener.java deleted file mode 100644 index 5b97d1dd4..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ContextListener.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.mobile.ContextEventListener; -import org.openqa.selenium.WebDriver; - -public class ContextListener extends TestListener implements ContextEventListener { - @Override public void beforeSwitchingToContext(WebDriver driver, String context) { - messages.add("Attempt to change current context to " + context); - } - - @Override public void afterSwitchingToContext(WebDriver driver, String context) { - messages.add("The previous context has been changed to " + context); - } - - @Override protected void add() { - SingleListeners.listeners.put(ContextListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/ContextListener2.java b/src/test/java/io/appium/java_client/events/listeners/ContextListener2.java deleted file mode 100644 index 979c19a1a..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ContextListener2.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.mobile.ContextEventListener; -import org.openqa.selenium.WebDriver; - -public class ContextListener2 extends TestListener implements ContextEventListener { - @Override public void beforeSwitchingToContext(WebDriver driver, String context) { - messages.add("Externally defined listener: Attempt to change current context to " + context); - } - - @Override public void afterSwitchingToContext(WebDriver driver, String context) { - messages.add("Externally defined listener: The previous context has been changed to " + context); - } - - @Override protected void add() { - SingleListeners.listeners.put(ContextListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/ElementListener.java b/src/test/java/io/appium/java_client/events/listeners/ElementListener.java deleted file mode 100644 index f1733aa53..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ElementListener.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.ElementEventListener; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class ElementListener extends TestListener implements ElementEventListener { - - @Override public void beforeClickOn(WebElement element, WebDriver driver) { - messages.add("Attempt to click on the element"); - } - - @Override public void afterClickOn(WebElement element, WebDriver driver) { - messages.add("Thee element was clicked"); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver) { - messages.add("Attempt to change value of the element"); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - messages.add("Attempt to change value of the element"); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver) { - messages.add("The value of the element was changed"); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - messages.add("The value of the element was changed"); - } - - @Override public void beforeGetText(WebElement element, WebDriver driver) { - messages.add("Attempt to get text of the element"); - } - - @Override public void afterGetText(WebElement element, WebDriver driver, String text) { - messages.add("Got the text of an element"); - } - - @Override protected void add() { - SingleListeners.listeners.put(ElementListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/ElementListener2.java b/src/test/java/io/appium/java_client/events/listeners/ElementListener2.java deleted file mode 100644 index 134db81e0..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ElementListener2.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.ElementEventListener; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class ElementListener2 extends TestListener implements ElementEventListener { - - @Override public void beforeClickOn(WebElement element, WebDriver driver) { - messages.add("Externally defined listener: Attempt to click on the element"); - } - - @Override public void afterClickOn(WebElement element, WebDriver driver) { - messages.add("Externally defined listener: Thee element was clicked"); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver) { - messages.add("Externally defined listener: Attempt to change value of the element"); - } - - @Override public void beforeChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - messages.add("Externally defined listener: Attempt to change value of the element"); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver) { - messages.add("Externally defined listener: The value of the element was changed"); - } - - @Override public void afterChangeValueOf(WebElement element, WebDriver driver, - CharSequence[] keysToSend) { - messages.add("Externally defined listener: The value of the element was changed"); - } - - @Override public void beforeGetText(WebElement element, WebDriver driver) { - messages.add("WebDriverEventListener: Attempt to get text of the element"); - } - - @Override public void afterGetText(WebElement element, WebDriver driver, String text) { - messages.add("WebDriverEventListener: Got the text of an element"); - } - - @Override protected void add() { - SingleListeners.listeners.put(ElementListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/ExceptionListener.java b/src/test/java/io/appium/java_client/events/listeners/ExceptionListener.java deleted file mode 100644 index 837bcce1f..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ExceptionListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.ListensToException; -import org.openqa.selenium.WebDriver; - -public class ExceptionListener extends TestListener implements ListensToException { - @Override public void onException(Throwable throwable, WebDriver driver) { - messages.add("The exception was thrown: " + throwable.getClass()); - } - - @Override protected void add() { - SingleListeners.listeners.put(ExceptionListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/ExceptionListener2.java b/src/test/java/io/appium/java_client/events/listeners/ExceptionListener2.java deleted file mode 100644 index 194cad08c..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ExceptionListener2.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.ListensToException; -import org.openqa.selenium.WebDriver; - -public class ExceptionListener2 extends TestListener implements ListensToException { - @Override public void onException(Throwable throwable, WebDriver driver) { - messages.add("Externally defined listener: The exception was thrown: " + throwable.getClass()); - } - - @Override protected void add() { - SingleListeners.listeners.put(ExceptionListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/JavaScriptListener.java b/src/test/java/io/appium/java_client/events/listeners/JavaScriptListener.java deleted file mode 100644 index 09b7bad6e..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/JavaScriptListener.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.JavaScriptEventListener; -import org.openqa.selenium.WebDriver; - -public class JavaScriptListener extends TestListener implements JavaScriptEventListener { - @Override public void beforeScript(String script, WebDriver driver) { - messages.add("Attempt to perform java script: " + script); - } - - @Override public void afterScript(String script, WebDriver driver) { - messages.add("Java script " + script + " was performed"); - } - - @Override protected void add() { - SingleListeners.listeners.put(JavaScriptListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/JavaScriptListener2.java b/src/test/java/io/appium/java_client/events/listeners/JavaScriptListener2.java deleted file mode 100644 index 2ec869751..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/JavaScriptListener2.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.JavaScriptEventListener; -import org.openqa.selenium.WebDriver; - -public class JavaScriptListener2 extends TestListener implements JavaScriptEventListener { - @Override public void beforeScript(String script, WebDriver driver) { - messages.add("Externally defined listener: Attempt to perform java script: " + script); - } - - @Override public void afterScript(String script, WebDriver driver) { - messages.add("Externally defined listener: Java script " + script + " was performed"); - } - - @Override protected void add() { - SingleListeners.listeners.put(JavaScriptListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/NavigationListener.java b/src/test/java/io/appium/java_client/events/listeners/NavigationListener.java deleted file mode 100644 index ac9cd94f0..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/NavigationListener.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.NavigationEventListener; -import org.openqa.selenium.WebDriver; - -public class NavigationListener extends TestListener implements NavigationEventListener { - - @Override public void beforeNavigateTo(String url, WebDriver driver) { - messages.add("Attempt to navigate to " + url); - } - - @Override public void afterNavigateTo(String url, WebDriver driver) { - messages.add("Navigation to " + url + " was successful"); - } - - @Override public void beforeNavigateBack(WebDriver driver) { - messages.add("Attempt to navigate back"); - } - - @Override public void afterNavigateBack(WebDriver driver) { - messages.add("Navigation back was successful"); - } - - @Override public void beforeNavigateForward(WebDriver driver) { - messages.add("Attempt to navigate forward"); - } - - @Override public void afterNavigateForward(WebDriver driver) { - messages.add("Navigation forward was successful"); - } - - @Override public void beforeNavigateRefresh(WebDriver driver) { - messages.add("Attempt to refresh"); - } - - @Override public void afterNavigateRefresh(WebDriver driver) { - messages.add("The refreshing was successful"); - } - - @Override protected void add() { - SingleListeners.listeners.put(NavigationListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/NavigationListener2.java b/src/test/java/io/appium/java_client/events/listeners/NavigationListener2.java deleted file mode 100644 index 327f05bca..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/NavigationListener2.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.NavigationEventListener; -import org.openqa.selenium.WebDriver; - -public class NavigationListener2 extends TestListener implements NavigationEventListener { - - @Override public void beforeNavigateTo(String url, WebDriver driver) { - messages.add("Externally defined listener: Attempt to navigate to " + url); - } - - @Override public void afterNavigateTo(String url, WebDriver driver) { - messages.add("Externally defined listener: Navigation to " + url + " was successful"); - } - - @Override public void beforeNavigateBack(WebDriver driver) { - messages.add("Externally defined listener: Attempt to navigate back"); - } - - @Override public void afterNavigateBack(WebDriver driver) { - messages.add("Externally defined listener: Navigation back was successful"); - } - - @Override public void beforeNavigateForward(WebDriver driver) { - messages.add("Externally defined listener: Attempt to navigate forward"); - } - - @Override public void afterNavigateForward(WebDriver driver) { - messages.add("Externally defined listener: Navigation forward was successful"); - } - - @Override public void beforeNavigateRefresh(WebDriver driver) { - messages.add("Externally defined listener: Attempt to refresh"); - } - - @Override public void afterNavigateRefresh(WebDriver driver) { - messages.add("Externally defined listener: The refreshing was successful"); - } - - @Override protected void add() { - SingleListeners.listeners.put(NavigationListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/RotationListener.java b/src/test/java/io/appium/java_client/events/listeners/RotationListener.java deleted file mode 100644 index e98d274cc..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/RotationListener.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.mobile.RotationEventListener; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; - -public class RotationListener extends TestListener implements RotationEventListener { - - @Override public void beforeRotation(WebDriver driver, ScreenOrientation orientation) { - messages.add("Attempt to change screen orientation. The new screen orientation is " - + orientation.toString()); - } - - @Override public void afterRotation(WebDriver driver, ScreenOrientation orientation) { - messages.add("The screen orientation has been changed to " - + orientation.toString()); - } - - @Override protected void add() { - SingleListeners.listeners.put(RotationListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/RotationListener2.java b/src/test/java/io/appium/java_client/events/listeners/RotationListener2.java deleted file mode 100644 index 9e7cd27ec..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/RotationListener2.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.mobile.RotationEventListener; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; - -public class RotationListener2 extends TestListener implements RotationEventListener { - - @Override public void beforeRotation(WebDriver driver, ScreenOrientation orientation) { - messages.add("Externally defined listener: Attempt to change screen orientation. " - + "The new screen orientation is " - + orientation.toString()); - } - - @Override public void afterRotation(WebDriver driver, ScreenOrientation orientation) { - messages.add("Externally defined listener: The screen orientation has been changed to " - + orientation.toString()); - } - - @Override protected void add() { - SingleListeners.listeners.put(RotationListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/SearchingListener.java b/src/test/java/io/appium/java_client/events/listeners/SearchingListener.java deleted file mode 100644 index 22e46257a..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/SearchingListener.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.SearchingEventListener; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class SearchingListener extends TestListener implements SearchingEventListener { - - @Override public void beforeFindBy(By by, WebElement element, WebDriver driver) { - messages.add("Attempt to find something using " + by.toString() + ". The root element is " - + String.valueOf(element)); - } - - @Override public void afterFindBy(By by, WebElement element, WebDriver driver) { - messages.add("The searching for something using " + by.toString() + " has beed finished. " - + "The root element was " - + String.valueOf(element)); - } - - @Override protected void add() { - SingleListeners.listeners.put(SearchingListener.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/SearchingListener2.java b/src/test/java/io/appium/java_client/events/listeners/SearchingListener2.java deleted file mode 100644 index bc3459a4d..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/SearchingListener2.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.SearchingEventListener; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; - -public class SearchingListener2 extends TestListener implements SearchingEventListener { - - @Override public void beforeFindBy(By by, WebElement element, WebDriver driver) { - messages.add("Externally defined listener: Attempt to find something using " - + by.toString() + ". The root element is " - + String.valueOf(element)); - } - - @Override public void afterFindBy(By by, WebElement element, WebDriver driver) { - messages.add("Externally defined listener: The searching for something using " - + by.toString() + " has beed finished. " - + "The root element was " - + String.valueOf(element)); - } - - @Override protected void add() { - SingleListeners.listeners.put(SearchingListener2.class, this); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/SingleListeners.java b/src/test/java/io/appium/java_client/events/listeners/SingleListeners.java deleted file mode 100644 index a61f12e56..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/SingleListeners.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.Listener; - -import java.util.HashMap; -import java.util.Map; - -public class SingleListeners { - - public static final Map, TestListener> listeners = new HashMap<>(); - -} diff --git a/src/test/java/io/appium/java_client/events/listeners/TestListener.java b/src/test/java/io/appium/java_client/events/listeners/TestListener.java deleted file mode 100644 index 21c8e9419..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/TestListener.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.Listener; - -import java.util.ArrayList; -import java.util.List; - -public abstract class TestListener implements Listener { - - public final List messages = new ArrayList<>(); - - public TestListener() { - add(); - } - - protected abstract void add(); -} diff --git a/src/test/java/io/appium/java_client/events/listeners/WindowListener.java b/src/test/java/io/appium/java_client/events/listeners/WindowListener.java deleted file mode 100644 index cf8c786c0..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/WindowListener.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.appium.java_client.events.listeners; - -import static java.lang.String.format; - -import io.appium.java_client.events.api.general.WindowEventListener; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.Point; -import org.openqa.selenium.WebDriver; - -public class WindowListener extends TestListener implements WindowEventListener { - - @Override protected void add() { - SingleListeners.listeners.put(WindowListener.class, this); - } - - @Override public void beforeWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize) { - messages.add("Attempt to change size of the window. The height is " + targetSize.getHeight() - + " the width is " + targetSize.getWidth()); - } - - @Override public void afterWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize) { - messages.add("Size of the window has been changed. The height is " + targetSize.getHeight() - + " the width is " + targetSize.getWidth()); - } - - @Override - public void beforeWindowIsMoved(WebDriver driver, WebDriver.Window window, Point targetPoint) { - messages.add("Attempt to change position of the window. The X is " + targetPoint.getX() - + " the Y is " + targetPoint.getY()); - } - - @Override - public void afterWindowIsMoved(WebDriver driver, WebDriver.Window window, Point targetPoint) { - messages.add("The position the window has been changed. The X is " + targetPoint.getX() - + " the Y is " + targetPoint.getY()); - } - - @Override public void beforeWindowIsMaximized(WebDriver driver, WebDriver.Window window) { - messages.add("Attempt to maximize the window."); - } - - @Override public void afterWindowIsMaximized(WebDriver driver, WebDriver.Window window) { - messages.add("The window has been maximized"); - } - - @Override - public void beforeSwitchToWindow(String windowName, WebDriver driver) { - messages.add(format("Attempt to switch to window %s", windowName)); - } - - @Override - public void afterSwitchToWindow(String windowName, WebDriver driver) { - messages.add(format("driver is switched to window %s", windowName)); - } -} diff --git a/src/test/java/io/appium/java_client/events/listeners/WindowListener2.java b/src/test/java/io/appium/java_client/events/listeners/WindowListener2.java deleted file mode 100644 index 60a912364..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/WindowListener2.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.appium.java_client.events.listeners; - -import static java.lang.String.format; - -import io.appium.java_client.events.api.general.WindowEventListener; -import org.openqa.selenium.Dimension; -import org.openqa.selenium.Point; -import org.openqa.selenium.WebDriver; - -public class WindowListener2 extends TestListener implements WindowEventListener { - - @Override protected void add() { - SingleListeners.listeners.put(WindowListener2.class, this); - } - - @Override public void beforeWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize) { - messages.add("Externally defined listener: Attempt to change size of the window. " - + "The height is " + targetSize.getHeight() - + " the width is " + targetSize.getWidth()); - } - - @Override public void afterWindowChangeSize(WebDriver driver, WebDriver.Window window, - Dimension targetSize) { - messages.add("Externally defined listener: Size of the window has " - + "been changed. The height is " + targetSize.getHeight() - + " the width is " + targetSize.getWidth()); - } - - @Override - public void beforeWindowIsMoved(WebDriver driver, WebDriver.Window window, Point targetPoint) { - messages.add("Externally defined listener: Attempt to change position of the window. " - + "The X is " + targetPoint.getX() - + " the Y is " + targetPoint.getY()); - } - - @Override - public void afterWindowIsMoved(WebDriver driver, WebDriver.Window window, Point targetPoint) { - messages.add("Externally defined listener: The position the window has been changed. " - + "The X is " + targetPoint.getX() - + " the Y is " + targetPoint.getY()); - } - - @Override public void beforeWindowIsMaximized(WebDriver driver, WebDriver.Window window) { - messages.add("Externally defined listener: Attempt to maximize the window."); - } - - @Override public void afterWindowIsMaximized(WebDriver driver, WebDriver.Window window) { - messages.add("Externally defined listener: The window has been maximized"); - } - - @Override - public void beforeSwitchToWindow(String windowName, WebDriver driver) { - messages.add(format("Externally defined listener: Attempt to switch to window %s", windowName)); - } - - @Override - public void afterSwitchToWindow(String windowName, WebDriver driver) { - messages.add(format("Externally defined listener: driver is switched to window %s", windowName)); - } -} 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 new file mode 100644 index 000000000..f4d4aab96 --- /dev/null +++ b/src/test/java/io/appium/java_client/events/stubs/EmptyWebDriver.java @@ -0,0 +1,226 @@ +/* + * 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.events.stubs; + +import org.openqa.selenium.Alert; +import org.openqa.selenium.By; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.Cookie; +import org.openqa.selenium.HasCapabilities; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.WindowType; +import org.openqa.selenium.logging.Logs; +import org.openqa.selenium.remote.DesiredCapabilities; + +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class EmptyWebDriver implements WebDriver, JavascriptExecutor, HasCapabilities, TakesScreenshot { + public EmptyWebDriver() { + } + + private static List createStubList() { + return List.of(new StubWebElement(), new StubWebElement()); + } + + public void get(String url) { + } + + public String getCurrentUrl() { + return null; + } + + public String getTitle() { + return null; + } + + public StubWebElement findElement(By by) { + return new StubWebElement(); + } + + public StubWebElement findElement(String by, String using) throws WebDriverException, NoSuchElementException { + return new StubWebElement(); + } + + public List findElements(By by) { + return createStubList(); + } + + public List findElements(String by, String using) throws WebDriverException { + return createStubList(); + } + + public String getPageSource() { + throw new WebDriverException(); + } + + public void close() { + } + + public void quit() { + } + + public Set getWindowHandles() { + return null; + } + + public String getWindowHandle() { + throw new WebDriverException(); + } + + public TargetLocator switchTo() { + return new EmptyWebDriver.StubTargetLocator(this); + } + + public Navigation navigate() { + return new EmptyWebDriver.StubNavigation(); + } + + public Options manage() { + return new EmptyWebDriver.StubOptions(); + } + + public Object executeScript(String script, Object... args) { + return null; + } + + public Object executeAsyncScript(String script, Object... args) { + return null; + } + + public Capabilities getCapabilities() { + Map map = new HashMap<>(); + map.put("0", ""); + map.put("1", ""); + return new DesiredCapabilities(map); + } + + public X getScreenshotAs(OutputType target) throws WebDriverException { + return target.convertFromPngBytes(new byte[]{1, 2}); + } + + private class StubNavigation implements Navigation { + private StubNavigation() { + } + + public void back() { + } + + public void forward() { + } + + public void to(String url) { + } + + public void to(URL url) { + } + + public void refresh() { + } + } + + private class StubOptions implements Options { + private StubOptions() { + } + + public void addCookie(Cookie cookie) { + } + + public void deleteCookieNamed(String name) { + } + + public void deleteCookie(Cookie cookie) { + } + + public void deleteAllCookies() { + } + + public Set getCookies() { + return null; + } + + public Cookie getCookieNamed(String name) { + return null; + } + + public Timeouts timeouts() { + return null; + } + + public Window window() { + return new StubWindow(); + } + + public Logs logs() { + return null; + } + } + + private class StubTargetLocator implements TargetLocator { + private final WebDriver driver; + + StubTargetLocator(WebDriver driver) { + this.driver = driver; + } + + public WebDriver frame(int index) { + return this.driver; + } + + public WebDriver frame(String nameOrId) { + return this.driver; + } + + public WebDriver frame(WebElement frameElement) { + return this.driver; + } + + public WebDriver parentFrame() { + return this.driver; + } + + public WebDriver window(String nameOrHandle) { + return this.driver; + } + + @Override + public WebDriver newWindow(WindowType typeHint) { + return null; + } + + public WebDriver defaultContent() { + return this.driver; + } + + public WebElement activeElement() { + return new StubWebElement(); + } + + public Alert alert() { + return new StubAlert(); + } + } +} diff --git a/src/main/java/io/appium/java_client/ios/IOSElement.java b/src/test/java/io/appium/java_client/events/stubs/StubAlert.java similarity index 67% rename from src/main/java/io/appium/java_client/ios/IOSElement.java rename to src/test/java/io/appium/java_client/events/stubs/StubAlert.java index a406053a0..c89ec4bb7 100644 --- a/src/main/java/io/appium/java_client/ios/IOSElement.java +++ b/src/test/java/io/appium/java_client/events/stubs/StubAlert.java @@ -14,12 +14,24 @@ * limitations under the License. */ -package io.appium.java_client.ios; +package io.appium.java_client.events.stubs; -import io.appium.java_client.FindsByIosClassChain; -import io.appium.java_client.FindsByIosNSPredicate; -import io.appium.java_client.MobileElement; +import org.openqa.selenium.Alert; -public class IOSElement extends MobileElement - implements FindsByIosNSPredicate, FindsByIosClassChain { +public class StubAlert implements Alert { + public StubAlert() { + } + + public void dismiss() { + } + + public void accept() { + } + + public String getText() { + return ""; + } + + public void sendKeys(String keysToSend) { + } } 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 new file mode 100644 index 000000000..a84708083 --- /dev/null +++ b/src/test/java/io/appium/java_client/events/stubs/StubWebElement.java @@ -0,0 +1,106 @@ +/* + * 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.events.stubs; + +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.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<>(List.of(new StubWebElement(), new StubWebElement())); + } + + public void click() { + } + + public void submit() { + } + + public void sendKeys(CharSequence... keysToSend) { + } + + public void clear() { + } + + public String getTagName() { + return null; + } + + public String getAttribute(String name) { + return null; + } + + public boolean isSelected() { + return false; + } + + public boolean isEnabled() { + return false; + } + + public String getText() { + return null; + } + + public List findElements(By by) { + return createStubSubElementList(); + } + + public WebElement findElement(By by) { + return new StubWebElement(); + } + + public boolean isDisplayed() { + return false; + } + + public Point getLocation() { + return null; + } + + public Dimension getSize() { + return null; + } + + public Rectangle getRect() { + throw new WebDriverException(); + } + + public String getCssValue(String propertyName) { + return null; + } + + public X getScreenshotAs(OutputType target) throws WebDriverException { + return target.convertFromPngBytes(new byte[]{1, 2}); + } + + public String toString() { + return this.getClass().getCanonicalName(); + } +} + diff --git a/src/test/java/io/appium/java_client/ios/IOSAppStringsTest.java b/src/test/java/io/appium/java_client/events/stubs/StubWindow.java similarity index 54% rename from src/test/java/io/appium/java_client/ios/IOSAppStringsTest.java rename to src/test/java/io/appium/java_client/events/stubs/StubWindow.java index d1cb976d0..d0e0fb82d 100644 --- a/src/test/java/io/appium/java_client/ios/IOSAppStringsTest.java +++ b/src/test/java/io/appium/java_client/events/stubs/StubWindow.java @@ -14,23 +14,37 @@ * limitations under the License. */ -package io.appium.java_client.ios; +package io.appium.java_client.events.stubs; -import static org.junit.Assert.assertNotEquals; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Point; +import org.openqa.selenium.WebDriver.Window; -import org.junit.Test; +public class StubWindow implements Window { + public StubWindow() { + } + + public void setSize(Dimension targetSize) { + } + + public void setPosition(Point targetPosition) { + } + + public Dimension getSize() { + return null; + } -public class IOSAppStringsTest extends AppIOSTest { + public Point getPosition() { + return null; + } - @Test public void getAppStrings() { - assertNotEquals(0, driver.getAppStringMap().size()); + public void maximize() { } - @Test public void getGetAppStringsUsingLang() { - assertNotEquals(0, driver.getAppStringMap("en").size()); + @Override + public void minimize() { } - @Test public void getAppStringsUsingLangAndFileStrings() { - assertNotEquals(0, driver.getAppStringMap("en", "Localizable.strings").size()); + public void fullscreen() { } } 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 d9185dc5f..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,34 +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.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +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.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(expected = IllegalArgumentException.class) - public void verifyGettingNonExistingValue() { - assertThat(Config.main().getValue(MISSING_KEY, String.class).length(), greaterThan(0)); + @Test + void verifyGettingNonExistingValue() { + assertThrows(IllegalArgumentException.class, () -> Config.main().getValue(MISSING_KEY, String.class)); } - @Test(expected = ClassCastException.class) - public void verifyGettingExistingValueWithWrongClass() { - assertThat(Config.main().getValue(EXISTING_KEY, Integer.class), greaterThan(0)); + @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/main/java/io/appium/java_client/FindsByAndroidDataMatcher.java b/src/test/java/io/appium/java_client/internal/SessionConnectTest.java similarity index 50% rename from src/main/java/io/appium/java_client/FindsByAndroidDataMatcher.java rename to src/test/java/io/appium/java_client/internal/SessionConnectTest.java index a60477870..a97653882 100644 --- a/src/main/java/io/appium/java_client/FindsByAndroidDataMatcher.java +++ b/src/test/java/io/appium/java_client/internal/SessionConnectTest.java @@ -14,19 +14,25 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.internal; -import org.openqa.selenium.WebElement; +import io.appium.java_client.ios.IOSDriver; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.WebDriverException; -import java.util.List; +import java.net.MalformedURLException; +import java.net.URL; -public interface FindsByAndroidDataMatcher extends FindsByFluentSelector { +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; - default T findElementByAndroidDataMatcher(String using) { - return findElement(MobileSelector.ANDROID_DATA_MATCHER.toString(), using); - } +public class SessionConnectTest { - default List findElementsByAndroidDataMatcher(String using) { - return findElements(MobileSelector.ANDROID_DATA_MATCHER.toString(), using); + @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 12426ebd9..000000000 --- a/src/test/java/io/appium/java_client/ios/AppIOSTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.appium.java_client.ios; - -import io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.IOSMobileCapabilityType; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; -import org.openqa.selenium.SessionNotCreatedException; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.net.URL; - -import static io.appium.java_client.TestResources.testAppZip; - -public class AppIOSTest extends BaseIOSTest { - - public static final String BUNDLE_ID = "io.appium.TestApp"; - - @BeforeClass - public static void beforeClass() throws Exception { - final String ip = startAppiumServer(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!"); - } - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, PLATFORM_VERSION); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, DEVICE_NAME); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.IOS_XCUI_TEST); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability("commandTimeouts", "120000"); - capabilities.setCapability(MobileCapabilityType.APP, testAppZip().toAbsolutePath().toString()); - try { - driver = new IOSDriver<>(new URL("http://" + ip + ":" + PORT + "/wd/hub"), capabilities); - } catch (SessionNotCreatedException e) { - capabilities.setCapability("useNewWDA", true); - driver = new IOSDriver<>(new URL("http://" + ip + ":" + PORT + "/wd/hub"), capabilities); - } - } -} diff --git a/src/test/java/io/appium/java_client/ios/BaseSafariTest.java b/src/test/java/io/appium/java_client/ios/BaseSafariTest.java deleted file mode 100644 index e58af254b..000000000 --- a/src/test/java/io/appium/java_client/ios/BaseSafariTest.java +++ /dev/null @@ -1,48 +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 io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.IOSMobileCapabilityType; -import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.IOException; -import java.net.URL; - -public class BaseSafariTest extends BaseIOSTest { - - @BeforeClass public static void beforeClass() throws IOException { - final String ip = startAppiumServer(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!"); - } - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.SAFARI); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.IOS_XCUI_TEST); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, PLATFORM_VERSION); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, DEVICE_NAME); - driver = new IOSDriver<>(new URL("http://" + ip + ":" + PORT + "/wd/hub"), capabilities); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSDriverTest.java b/src/test/java/io/appium/java_client/ios/IOSDriverTest.java deleted file mode 100644 index 995ac4c58..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSDriverTest.java +++ /dev/null @@ -1,143 +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.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.lessThan; -import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.appmanagement.ApplicationState; -import io.appium.java_client.remote.HideKeyboardStrategy; -import io.appium.java_client.remote.MobileCapabilityType; -import org.junit.Ignore; -import org.junit.Test; - -import org.openqa.selenium.By; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.html5.Location; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.time.Duration; - -public class IOSDriverTest extends AppIOSTest { - - @Test - public void getDeviceTimeTest() { - String time = driver.getDeviceTime(); - assertFalse(time.isEmpty()); - } - - @Test public void resetTest() { - driver.resetApp(); - } - - @Test public void hideKeyboardWithParametersTest() { - new WebDriverWait(driver, 30) - .until(ExpectedConditions.presenceOfElementLocated(By.id("IntegerA"))) - .click(); - driver.hideKeyboard(HideKeyboardStrategy.PRESS_KEY, "Done"); - } - - @Ignore - @Test public void geolocationTest() { - Location location = new Location(45, 45, 100); - try { - driver.setLocation(location); - } catch (Exception e) { - fail("Not able to set location"); - } - } - - @Test public void orientationTest() { - assertEquals(ScreenOrientation.PORTRAIT, driver.getOrientation()); - driver.rotate(ScreenOrientation.LANDSCAPE); - assertEquals(ScreenOrientation.LANDSCAPE, driver.getOrientation()); - driver.rotate(ScreenOrientation.PORTRAIT); - } - - @Test public void lockTest() { - try { - driver.lockDevice(); - assertTrue(driver.isDeviceLocked()); - } finally { - driver.unlockDevice(); - assertFalse(driver.isDeviceLocked()); - } - } - - @Test public void pullFileTest() { - byte[] data = driver.pullFile("@io.appium.TestApp/TestApp"); - assert (data.length > 0); - } - - @Test public void keyboardTest() { - MobileElement element = driver.findElementById("IntegerA"); - element.click(); - assertTrue(driver.isKeyboardShown()); - } - - @Test public void putAppIntoBackgroundAndRestoreTest() { - final long msStarted = System.currentTimeMillis(); - driver.runAppInBackground(Duration.ofSeconds(4)); - assertThat(System.currentTimeMillis() - msStarted, greaterThan(3000L)); - } - - @Test public void applicationsManagementTest() throws InterruptedException { - // This only works since Xcode9 - try { - if (Double.parseDouble( - (String) driver.getCapabilities() - .getCapability(MobileCapabilityType.PLATFORM_VERSION)) < 11) { - return; - } - } catch (NumberFormatException | NullPointerException e) { - return; - } - assertThat(driver.queryAppState(BUNDLE_ID), equalTo(ApplicationState.RUNNING_IN_FOREGROUND)); - Thread.sleep(500); - driver.runAppInBackground(Duration.ofSeconds(-1)); - assertThat(driver.queryAppState(BUNDLE_ID), lessThan(ApplicationState.RUNNING_IN_FOREGROUND)); - Thread.sleep(500); - driver.activateApp(BUNDLE_ID); - assertThat(driver.queryAppState(BUNDLE_ID), equalTo(ApplicationState.RUNNING_IN_FOREGROUND)); - } - - @Test public void putAIntoBackgroundWithoutRestoreTest() { - assertThat(driver.findElementsById("IntegerA"), is(not(empty()))); - driver.runAppInBackground(Duration.ofSeconds(-1)); - assertThat(driver.findElementsById("IntegerA"), is(empty())); - driver.launchApp(); - } - - @Ignore - @Test public void touchIdTest() { - driver.toggleTouchIDEnrollment(true); - driver.performTouchID(true); - driver.performTouchID(false); - assertEquals(true, true); - } -} 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 4389d1888..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSElementTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.appium.java_client.ios; - -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNot.not; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - -import org.junit.FixMethodOrder; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.openqa.selenium.By; -import org.openqa.selenium.support.ui.WebDriverWait; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class IOSElementTest extends AppIOSTest { - - @Test - public void findByAccessibilityIdTest() { - assertThat(driver.findElementsByAccessibilityId("Compute Sum").size(), - not(is(0))); - } - - // FIXME: Stabilize the test on CI - @Ignore - @Test - public void setValueTest() { - WebDriverWait wait = new WebDriverWait(driver, 20); - - IOSElement slider = wait.until( - driver1 -> driver1.findElement(By.className("XCUIElementTypeSlider"))); - slider.setValue("0%"); - assertEquals("0%", slider.getAttribute("value")); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSOptionsTest.java b/src/test/java/io/appium/java_client/ios/IOSOptionsTest.java deleted file mode 100644 index 5fc1262e8..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSOptionsTest.java +++ /dev/null @@ -1,77 +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 io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobilePlatform; -import org.junit.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.Assert.assertEquals; - -public class IOSOptionsTest { - private IOSOptions iosOptions = new IOSOptions(); - - @Test - public void setsPlatformNameByDefault() { - assertEquals(MobilePlatform.IOS, iosOptions.getPlatformName()); - } - - @Test - public void acceptsExistingCapabilities() { - MutableCapabilities capabilities = new MutableCapabilities(); - capabilities.setCapability("deviceName", "Pixel"); - capabilities.setCapability("platformVersion", "10"); - capabilities.setCapability("newCommandTimeout", 60); - - iosOptions = new IOSOptions(capabilities); - - assertEquals("Pixel", iosOptions.getDeviceName()); - assertEquals("10", iosOptions.getPlatformVersion()); - assertEquals(Duration.ofSeconds(60), iosOptions.getNewCommandTimeout()); - } - - @Test - public void acceptsMobileCapabilities() throws MalformedURLException { - iosOptions.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", iosOptions.getApp()); - assertEquals(AutomationName.ANDROID_UIAUTOMATOR2, iosOptions.getAutomationName()); - assertEquals("10", iosOptions.getPlatformVersion()); - assertEquals("Pixel", iosOptions.getDeviceName()); - assertEquals("/path/to/app.apk", iosOptions.getOtherApps()); - assertEquals("fr_CA", iosOptions.getLocale()); - assertEquals("1ae203187fc012g", iosOptions.getUdid()); - assertEquals(ScreenOrientation.LANDSCAPE, iosOptions.getOrientation()); - assertEquals(Duration.ofSeconds(60), iosOptions.getNewCommandTimeout()); - assertEquals("fr", iosOptions.getLanguage()); - } -} 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 82b1a122f..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSSearchingTest.java +++ /dev/null @@ -1,51 +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.Assert.assertNotEquals; - -import org.junit.Test; - -public class IOSSearchingTest extends AppIOSTest { - - @Test public void findByAccessibilityIdTest() { - assertNotEquals(driver - .findElementByAccessibilityId("ComputeSumButton") - .getText(), null); - assertNotEquals(driver - .findElementsByAccessibilityId("ComputeSumButton") - .size(), 0); - } - - @Test public void findByByIosPredicatesTest() { - assertNotEquals(driver - .findElementByIosNsPredicate("name like 'Answer'") - .getText(), null); - assertNotEquals(driver - .findElementsByIosNsPredicate("name like 'Answer'") - .size(), 0); - } - - @Test public void findByByIosClassChainTest() { - assertNotEquals(driver - .findElementByIosClassChain("**/XCUIElementTypeButton") - .getText(), null); - assertNotEquals(driver - .findElementsByIosClassChain("**/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 560fb2245..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSTouchTest.java +++ /dev/null @@ -1,80 +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.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.MultiTouchAction; -import io.appium.java_client.TouchAction; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.openqa.selenium.support.ui.WebDriverWait; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class IOSTouchTest extends AppIOSTest { - - @Test - public void tapTest() { - IOSElement intA = driver.findElementById("IntegerA"); - IOSElement intB = driver.findElementById("IntegerB"); - intA.clear(); - intB.clear(); - intA.sendKeys("2"); - intB.sendKeys("4"); - - MobileElement e = driver.findElementByAccessibilityId("ComputeSumButton"); - new IOSTouchAction(driver).tap(tapOptions().withElement(element(e))).perform(); - assertEquals(driver.findElementByXPath("//*[@name = \"Answer\"]").getText(), "6"); - } - - @Test - public void touchWithPressureTest() { - IOSElement intA = driver.findElementById("IntegerA"); - IOSElement intB = driver.findElementById("IntegerB"); - intA.clear(); - intB.clear(); - intA.sendKeys("2"); - intB.sendKeys("4"); - - MobileElement e = driver.findElementByAccessibilityId("ComputeSumButton"); - new IOSTouchAction(driver) - .press(iosPressOptions() - .withElement(element(e)) - .withPressure(1)) - .waitAction(waitOptions(ofMillis(100))) - .release() - .perform(); - assertEquals(driver.findElementByXPath("//*[@name = \"Answer\"]").getText(), "6"); - } - - @Test public void multiTouchTest() { - MobileElement e = driver.findElementByAccessibilityId("ComputeSumButton"); - MobileElement e2 = driver.findElementByAccessibilityId("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, 10); - assertNotNull(waiting.until(alertIsPresent())); - driver.switchTo().alert().accept(); - } - - @Test public void doubleTapTest() { - IOSElement firstField = driver.findElementById("IntegerA"); - firstField.sendKeys("2"); - - IOSTouchAction iosTouchAction = new IOSTouchAction(driver); - iosTouchAction.doubleTap(element(firstField)); - IOSElement editingMenu = driver.findElementByClassName("XCUIElementTypeTextField"); - assertNotNull(editingMenu); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSWebViewTest.java b/src/test/java/io/appium/java_client/ios/IOSWebViewTest.java deleted file mode 100644 index 7c8f380c3..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSWebViewTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.appium.java_client.ios; - -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.MobileBy; -import org.junit.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; - -public class IOSWebViewTest extends BaseIOSWebViewTest { - - @Test public void webViewPageTestCase() throws InterruptedException { - new WebDriverWait(driver, 30) - .until(ExpectedConditions.presenceOfElementLocated(By.id("login"))) - .click(); - driver.findElementByAccessibilityId("webView").click(); - new WebDriverWait(driver, 30) - .until(ExpectedConditions.presenceOfElementLocated(MobileBy.AccessibilityId("Webview"))); - findAndSwitchToWebView(); - WebElement el = driver.findElementByPartialLinkText("login"); - assertTrue(el.isDisplayed()); - } -} diff --git a/src/test/java/io/appium/java_client/ios/UICatalogIOSTest.java b/src/test/java/io/appium/java_client/ios/UICatalogIOSTest.java deleted file mode 100644 index 95bbc5f6a..000000000 --- a/src/test/java/io/appium/java_client/ios/UICatalogIOSTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.appium.java_client.ios; - -import io.appium.java_client.remote.IOSMobileCapabilityType; -import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.IOException; -import java.net.URL; - -import static io.appium.java_client.TestResources.uiCatalogAppZip; - -public class UICatalogIOSTest extends BaseIOSTest { - - @BeforeClass - public static void beforeClass() throws IOException { - final String ip = startAppiumServer(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException( - "An appium server node is not started!"); - } - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "9.2"); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone 6"); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability(MobileCapabilityType.APP, uiCatalogAppZip().toAbsolutePath().toString()); - driver = new IOSDriver<>(new URL("http://" + ip + ":" + PORT + "/wd/hub"), capabilities); - } -} 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 63ff333b5..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.Assert.assertNotEquals; -import static org.junit.Assert.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 org.junit.BeforeClass; -import org.junit.Test; +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") @@ -48,24 +53,25 @@ public class DesktopBrowserCompatibilityTest { private List foundLinks; private List main; //this list is located by id="main" private WebDriver trap1; - private List> trap2; + private List trap2; /** * The starting. */ - @BeforeClass public static void beforeClass() { + @BeforeAll public static void beforeClass() { chromedriver().setup(); } @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/GenericTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/GenericTest.java deleted file mode 100644 index 16e83e2fe..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/GenericTest.java +++ /dev/null @@ -1,300 +0,0 @@ -package io.appium.java_client.pagefactory_tests; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.MobileCapabilityType; - -import org.apache.commons.lang3.NotImplementedException; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.HasCapabilities; -import org.openqa.selenium.Platform; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.CapabilityType; -import org.openqa.selenium.support.PageFactory; -import io.appium.java_client.MobileElement; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class GenericTest { - - /** - * The Generic types are null on an unbound page. - */ - private static void assertUnboundGenericsNull(T genericItem, - List genericItems) { - assertNull(genericItem); - assertNull(genericItems); - } - - /** - * The Generic types are not null on a page bound by a WebElement (or WebElement - * sub type). - */ - private static void assertBoundGenericsNotNull(T genericItem, - List genericItems) { - assertNotNull(genericItem); - assertNotNull(genericItems); - } - - /** - * The Element types are never null. - */ - private static void assertElementsNotNull(WebElement elementItem, - List elementItems) { - assertNotNull(elementItem); - assertNotNull(elementItems); - } - - /** - * The Object types are always null. - */ - private static void assertObjectsNull(Object objectItem, List objectItems) { - assertNull(objectItem); - assertNull(objectItems); - } - - /** - * A page with no generic types. The Object types are never initialized. - */ - static class TempNoGenericsPage { - public WebElement webElementItem; - public MobileElement mobileElementItem; - public Object objectItem; - - public List webElementItems; - public List mobileElementItems; - public List objectItems; - - public void assertInit() { - assertElementsNotNull(webElementItem, webElementItems); - assertElementsNotNull(mobileElementItem, mobileElementItems); - assertObjectsNull(objectItem, objectItems); - } - } - - /** - * A page with an unbound generic type. The generic and Object types are never - * initialized. - */ - static class TempUnboundPage { - public T genericItem; - public WebElement webElementItem; - public MobileElement mobileElementItem; - public Object objectItem; - - public List genericItems; - public List webElementItems; - public List mobileElementItems; - public List objectItems; - - public void assertInit() { - assertUnboundGenericsNull(genericItem, genericItems); - assertElementsNotNull(webElementItem, webElementItems); - assertElementsNotNull(mobileElementItem, mobileElementItems); - assertObjectsNull(objectItem, objectItems); - } - } - - /** - * A page with a WebElement bound generic type. The Object types are never - * initialized. - */ - static class TempWebBoundPage { - public T genericItem; - public WebElement webElementItem; - public MobileElement mobileElementItem; - public Object objectItem; - - public List genericItems; - public List webElementItems; - public List mobileElementItems; - public List objectItems; - - public void assertInit() { - assertBoundGenericsNotNull(genericItem, genericItems); - assertElementsNotNull(webElementItem, webElementItems); - assertElementsNotNull(mobileElementItem, mobileElementItems); - assertObjectsNull(objectItem, objectItems); - } - } - - /** - * A page with a MobileElement bound generic type. The Object types are never - * initialized. - */ - static class TempMobileBoundPage { - public T genericItem; - public WebElement webElementItem; - public MobileElement mobileElementItem; - public Object objectItem; - - public List genericItems; - public List webElementItems; - public List mobileElementItems; - public List objectItems; - - public void assertInit() { - assertBoundGenericsNotNull(genericItem, genericItems); - assertElementsNotNull(webElementItem, webElementItems); - assertElementsNotNull(mobileElementItem, mobileElementItems); - assertObjectsNull(objectItem, objectItems); - } - } - - static class MockWebDriver implements WebDriver, HasCapabilities { - - @Override - public void get(String url) { - System.out.print(url); - } - - @Override - public String getCurrentUrl() { - return null; - } - - @Override - public String getTitle() { - return null; - } - - @Override - public List findElements(By by) { - throw new NotImplementedException("MockWebDriver did not expect to findElements"); - } - - @Override - public T findElement(By by) { - throw new NotImplementedException("MockWebDriver did not expect to findElement"); - } - - @Override - public String getPageSource() { - return null; - } - - @Override - public void close() { - System.out.print("Closed"); - } - - @Override - public void quit() { - System.out.print("Died"); - } - - @Override - public Set getWindowHandles() { - return null; - } - - @Override - public String getWindowHandle() { - return null; - } - - @Override - public TargetLocator switchTo() { - return null; - } - - @Override - public Navigation navigate() { - return null; - } - - @Override - public Options manage() { - return null; - } - - @Override - public Capabilities getCapabilities() { - - final Map capabilities = new HashMap<>(); - - // These are needed to map the proxy element to a MobileElement. - capabilities.put(CapabilityType.PLATFORM_NAME, Platform.ANY.toString()); - capabilities.put(MobileCapabilityType.AUTOMATION_NAME, - AutomationName.IOS_XCUI_TEST.toString()); - - return new Capabilities() { - - @Override - public Object getCapability(String capabilityName) { - return capabilities.get(capabilityName); - } - - @Override - public Map asMap() { - return capabilities; - } - }; - } - } - - @Test - public void noGenericsTestCase() { - TempNoGenericsPage page = new TempNoGenericsPage(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } - - @Test - public void unBoundTestCase() { - TempUnboundPage page = new TempUnboundPage<>(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } - - @Test - public void unboundWebElementTestCase() { - TempUnboundPage page = new TempUnboundPage<>(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } - - @Test - public void webBoundUnknownElementTestCase() { - TempWebBoundPage page = new TempWebBoundPage<>(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } - - @Test - public void webBoundWebElementTestCase() { - TempWebBoundPage page = new TempWebBoundPage<>(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } - - @Test - public void webBoundMobileElementTestCase() { - TempWebBoundPage page = new TempWebBoundPage<>(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } - - @Test - public void mobileBoundUnknownElementTestCase() { - TempMobileBoundPage page = new TempMobileBoundPage<>(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } - - @Test - public void mobileBoundMobileElementTestCase() { - TempMobileBoundPage page = new TempMobileBoundPage<>(); - PageFactory.initElements(new AppiumFieldDecorator(new MockWebDriver()), page); - page.assertInit(); - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/IOSMobileBrowserCompatibilityTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/IOSMobileBrowserCompatibilityTest.java deleted file mode 100644 index 42f9239c1..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/IOSMobileBrowserCompatibilityTest.java +++ /dev/null @@ -1,96 +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 java.time.Duration.ofSeconds; - -import io.appium.java_client.ios.IOSDriver; -import io.appium.java_client.pagefactory.AndroidFindBy; -import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import io.appium.java_client.pagefactory.iOSXCUITFindBy; -import io.appium.java_client.remote.IOSMobileCapabilityType; -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.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.PageFactory; - -import java.util.List; - -public class IOSMobileBrowserCompatibilityTest { - - private WebDriver driver; - private AppiumDriverLocalService service; - - @FindBy(name = "q") - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/someId\")") - @iOSXCUITFindBy(className = "someClass") private WebElement searchTextField; - - @AndroidFindBy(className = "someClass") - @FindBys({@FindBy(className = "r"), @FindBy(tagName = "a")}) @iOSXCUITFindBy(className = "someClass") - private List foundLinks; - - /** - * The setting up. - */ - @Before public void setUp() { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.SAFARI); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "9.2"); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - driver = new IOSDriver<>(service.getUrl(), capabilities); - PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(5)), this); - } - - /** - * finishing. - */ - @After public void tearDown() { - if (driver != null) { - driver.quit(); - } - - if (service != null) { - service.stop(); - } - } - - @Test public void test() { - driver.get("https://www.google.com"); - - searchTextField.sendKeys("Hello"); - searchTextField.submit(); - Assert.assertNotEquals(0, foundLinks.size()); - searchTextField.clear(); - searchTextField.sendKeys("Hello, Appium!"); - searchTextField.submit(); - } - -} 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 ea2300e09..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,33 +16,34 @@ 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.Matchers.lessThanOrEqualTo; -import static org.junit.Assert.assertThat; -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.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +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.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) { @@ -77,7 +78,7 @@ private static String assertionMessage(Duration expectedDuration) { formatDuration(expectedDuration.toMillis(), "H:mm:ss:SSS", true)); } - @BeforeClass + @BeforeAll public static void beforeAll() { chromedriver().setup(); } @@ -85,8 +86,8 @@ public static void beforeAll() { /** * The setting up. */ - @Before public void setUp() { - driver = new ChromeDriver(); + @BeforeEach public void setUp() { + driver = new ChromeDriver(new ChromeOptions().addArguments("--headless=new")); timeOutDuration = DEFAULT_WAITING_TIMEOUT; initElements(new AppiumFieldDecorator(driver, timeOutDuration), this); } @@ -94,7 +95,7 @@ public static void beforeAll() { /** * finishing. */ - @After public void tearDown() { + @AfterEach public void tearDown() { driver.quit(); } 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 ee2324256..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java +++ /dev/null @@ -1,153 +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.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.MobileElement; -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.Before; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.util.List; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class XCUITModeTest extends AppIOSTest { - - private boolean populated = false; - private WebDriverWait waiting = new WebDriverWait(driver, 10); - - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "label contains 'Compute'") - @iOSXCUITFindBy(className = "XCUIElementTypeButton") - private MobileElement computeButton; - - @HowToUseLocators(iOSXCUITAutomation = CHAIN) - @iOSXCUITFindBy(iOSNsPredicate = "name like 'Answer'") - private WebElement answer; - - @iOSXCUITFindBy(iOSNsPredicate = "name = 'IntegerA'") - private MobileElement textField1; - - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "name = 'IntegerB'") - @iOSXCUITFindBy(accessibility = "IntegerB") - private MobileElement textField2; - - @iOSXCUITFindBy(iOSNsPredicate = "name ENDSWITH 'Gesture'") - private MobileElement gesture; - - @iOSXCUITFindBy(className = "XCUIElementTypeSlider") - private MobileElement slider; - - @iOSXCUITFindBy(id = "locationStatus") - private MobileElement locationStatus; - - @HowToUseLocators(iOSXCUITAutomation = CHAIN) - @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'contact'") - private MobileElement contactAlert; - - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'location'") - private MobileElement locationAlert; - - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeTextField[2]") - private MobileElement secondTextField; - - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton[-1]") - private MobileElement lastButton; - - @iOSXCUITFindBy(iOSClassChain = "XCUIElementTypeWindow/*/XCUIElementTypeButton") - private List allButtons; - - /** - * The setting up. - */ - @Before 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.setValue("2"); - textField2.setValue("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 7e3d5783a..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,31 +1,33 @@ 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.APPIUM; -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.HasSessionDetails; +import io.appium.java_client.HasBrowserCheck; import org.openqa.selenium.By; import org.openqa.selenium.Capabilities; import org.openqa.selenium.Cookie; import org.openqa.selenium.HasCapabilities; +import org.openqa.selenium.ImmutableCapabilities; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; import org.openqa.selenium.logging.Logs; -import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.Response; -import java.util.HashMap; +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; -public abstract class AbstractStubWebDriver implements WebDriver, HasSessionDetails, +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, HasCapabilities { @Override public Response execute(String driverCommand, Map parameters) { @@ -37,12 +39,6 @@ public Response execute(String driverCommand) { return null; } - @Override - public abstract String getPlatformName(); - - @Override - public abstract String getAutomationName(); - @Override public boolean isBrowser() { return false; @@ -64,8 +60,8 @@ public String getTitle() { } @Override - public List findElements(By by) { - return of(new StubWebElement(this, by), new StubWebElement(this, by)); + public List findElements(By by) { + return List.of(new StubWebElement(this, by), new StubWebElement(this, by)); } @Override @@ -108,12 +104,20 @@ public Navigation navigate() { return null; } + public String getPlatformName() { + return ""; + } + + public String getAutomationName() { + return ""; + } + @Override public Capabilities getCapabilities() { - Map caps = new HashMap<>(); - caps.put("platformName", getPlatformName()); - caps.put("automationName", getAutomationName()); - return new DesiredCapabilities(caps); + return new ImmutableCapabilities( + "appium:platformName", getPlatformName(), + "appium:automationName", getAutomationName() + ); } @Override @@ -152,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 @@ -195,20 +246,7 @@ public String getPlatformName() { @Override public String getAutomationName() { - return APPIUM; - } - } - - public static class StubIOSDriver extends AbstractStubWebDriver { - - @Override - public String getPlatformName() { - return IOS; - } - - @Override - public String getAutomationName() { - return APPIUM; + return ANDROID_UIAUTOMATOR2; } } @@ -234,7 +272,7 @@ public String getPlatformName() { @Override public String getAutomationName() { - return APPIUM; + return WINDOWS; } } @@ -260,7 +298,7 @@ public String getPlatformName() { @Override public String getAutomationName() { - return APPIUM; + return ANDROID; } @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/ExtendedWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ExtendedWidgetTest.java deleted file mode 100644 index d8d3a7b60..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ExtendedWidgetTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests; - -import static java.util.stream.Collectors.toList; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsString; -import static org.junit.Assert.assertThat; - -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; - -import java.util.List; - -public abstract class ExtendedWidgetTest extends WidgetTest { - - protected ExtendedWidgetTest(ExtendedApp app, WebDriver driver) { - super(app, driver); - } - - @Test - public abstract void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation(); - - @Test - public abstract void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass(); - - @Test - public abstract void checkCaseWhenBothWidgetFieldAndClassHaveDeclaredAnnotations(); - - 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()), - containsString(rootLocator.toString()))); - - assertThat(single.getSubWidget().toString(), containsString(subLocator.toString())); - assertThat(single.getSubWidgets().stream().map(Object::toString).collect(toList()), - contains(containsString(subLocator.toString()), - containsString(subLocator.toString()))); - - assertThat(multiple.stream().map(abstractWidget -> abstractWidget.getSubWidget().toString()).collect(toList()), - contains(containsString(subLocator.toString()), - containsString(subLocator.toString()))); - } -} 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 52aa50807..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 6ef019438..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,20 +1,22 @@ package io.appium.java_client.pagefactory_tests.widget.tests; -import static java.util.Arrays.copyOf; -import static org.openqa.selenium.support.PageFactory.initElements; - import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; +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; - protected static Object[] dataArray(Object...objects) { - return copyOf(objects, objects.length); - } - protected WidgetTest(AbstractApp app, WebDriver driver) { this.app = app; initElements(new AppiumFieldDecorator(driver), app); @@ -22,4 +24,31 @@ protected WidgetTest(AbstractApp app, WebDriver driver) { @Test public abstract void checkThatWidgetsAreCreatedCorrectly(); + + @Test + public abstract void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation(); + + @Test + public abstract void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass(); + + @Test + public abstract void checkCaseWhenBothWidgetFieldAndClassHaveDeclaredAnnotations(); + + 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()), + containsString(rootLocator.toString()))); + + assertThat(single.getSubWidget().toString(), containsString(subLocator.toString())); + assertThat(single.getSubWidgets().stream().map(Object::toString).collect(toList()), + contains(containsString(subLocator.toString()), + containsString(subLocator.toString()))); + + assertThat(multiple.stream().map(abstractWidget -> abstractWidget.getSubWidget().toString()).collect(toList()), + contains(containsString(subLocator.toString()), + containsString(subLocator.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 3b94b21cd..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,46 +1,51 @@ package io.appium.java_client.pagefactory_tests.widget.tests.android; -import static io.appium.java_client.MobileBy.AndroidUIAutomator; +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.ExtendedWidgetTest; - -public class AndroidWidgetTest extends ExtendedWidgetTest { +public class AndroidWidgetTest extends WidgetTest { public AndroidWidgetTest() { super(new AndroidApp(), new AbstractStubWebDriver.StubAndroidDriver()); } + @Test @Override public void checkThatWidgetsAreCreatedCorrectly() { checkThatLocatorsAreCreatedCorrectly(app.getWidget(), app.getWidgets(), - AndroidUIAutomator(ANDROID_DEFAULT_WIDGET_LOCATOR), AndroidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); + androidUIAutomator(ANDROID_DEFAULT_WIDGET_LOCATOR), androidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); } + @Test @Override public void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation() { checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getAnnotatedWidget(), ((ExtendedApp) app).getAnnotatedWidgets(), - AndroidUIAutomator(ANDROID_ROOT_WIDGET_LOCATOR), AndroidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); + androidUIAutomator(ANDROID_ROOT_WIDGET_LOCATOR), androidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); } + @Test @Override public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() { checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidget(), ((ExtendedApp) app).getExtendedWidgets(), - AndroidUIAutomator(ANDROID_ROOT_WIDGET_LOCATOR), AndroidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); + androidUIAutomator(ANDROID_ROOT_WIDGET_LOCATOR), androidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); } + @Test @Override public void checkCaseWhenBothWidgetFieldAndClassHaveDeclaredAnnotations() { checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidgetWithOverriddenLocators(), ((ExtendedApp) app).getExtendedWidgetsWithOverriddenLocators(), - AndroidUIAutomator(ANDROID_EXTERNALLY_DEFINED_WIDGET_LOCATOR), - AndroidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); + androidUIAutomator(ANDROID_EXTERNALLY_DEFINED_WIDGET_LOCATOR), + androidUIAutomator(ANDROID_SUB_WIDGET_LOCATOR)); } } 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 a0d87a4f8..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,70 +1,67 @@ package io.appium.java_client.pagefactory_tests.widget.tests.combined; -import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toList; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; - +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.WidgetTest; 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.runner.RunWith; -import org.junit.runners.Parameterized; +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.Collection; import java.util.List; +import java.util.stream.Stream; -@RunWith(Parameterized.class) -public class CombinedAppTest extends WidgetTest { +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; - private final Class widgetClass; +@SuppressWarnings({"unused", "unchecked"}) +public class CombinedAppTest { /** * Test data generation. * * @return test parameters */ - @Parameterized.Parameters - public static Collection data() { - return asList( - dataArray(new CombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), - dataArray(new CombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), + public static Stream data() { + return Stream.of( + arguments(new CombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), + arguments(new CombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), DefaultIosXCUITWidget.class), - dataArray(new CombinedApp(), new AbstractStubWebDriver.StubWindowsDriver(), DefaultWindowsWidget.class), - dataArray(new CombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), - dataArray(new CombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), + arguments(new CombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), + arguments(new CombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), DefaultFindByWidget.class), - dataArray(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), - dataArray(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubIOSDriver(), - DefaultStubWidget.class), - dataArray(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubIOSXCUITDriver(), DefaultStubWidget.class), - dataArray(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubWindowsDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubWindowsDriver(), DefaultStubWidget.class), - dataArray(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), - dataArray(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), + arguments(new PartiallyCombinedApp(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), DefaultFindByWidget.class) ); } - public CombinedAppTest(AbstractApp app, WebDriver driver, Class widgetClass) { - super(app, driver); - this.widgetClass = widgetClass; - } - - @Override - public void checkThatWidgetsAreCreatedCorrectly() { - assertThat("Excpected widget class was " + widgetClass.getName(), + @ParameterizedTest + @MethodSource("data") + 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()) @@ -77,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 253603e2d..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,74 +1,74 @@ package io.appium.java_client.pagefactory_tests.widget.tests.combined; -import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toList; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertThat; - +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.WidgetTest; 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.runner.RunWith; -import org.junit.runners.Parameterized; +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.Collection; 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; -@RunWith(Parameterized.class) -public class CombinedWidgetTest extends WidgetTest { - private final Class widgetClass; +@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. * * @return test parameters */ - @Parameterized.Parameters - public static Collection data() { - return asList( - dataArray(new AppWithCombinedWidgets(), + public static Stream data() { + return Stream.of( + Arguments.of(new AppWithCombinedWidgets(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), - dataArray(new AppWithCombinedWidgets(), + Arguments.of(new AppWithCombinedWidgets(), new AbstractStubWebDriver.StubIOSXCUITDriver(), DefaultIosXCUITWidget.class), - dataArray(new AppWithCombinedWidgets(), - new AbstractStubWebDriver.StubWindowsDriver(), DefaultWindowsWidget.class), - dataArray(new AppWithCombinedWidgets(), + Arguments.of(new AppWithCombinedWidgets(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), - dataArray(new AppWithCombinedWidgets(), + Arguments.of(new AppWithCombinedWidgets(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), DefaultFindByWidget.class), - dataArray(new AppWithPartiallyCombinedWidgets(), + Arguments.of(new AppWithPartiallyCombinedWidgets(), new AbstractStubWebDriver.StubAndroidDriver(), DefaultAndroidWidget.class), - dataArray(new AppWithPartiallyCombinedWidgets(), - new AbstractStubWebDriver.StubIOSDriver(), DefaultStubWidget.class), - dataArray(new AppWithPartiallyCombinedWidgets(), + Arguments.of(new AppWithPartiallyCombinedWidgets(), new AbstractStubWebDriver.StubIOSXCUITDriver(), DefaultStubWidget.class), - dataArray(new AppWithPartiallyCombinedWidgets(), + Arguments.of(new AppWithPartiallyCombinedWidgets(), new AbstractStubWebDriver.StubWindowsDriver(), DefaultStubWidget.class), - dataArray(new AppWithPartiallyCombinedWidgets(), + Arguments.of(new AppWithPartiallyCombinedWidgets(), new AbstractStubWebDriver.StubBrowserDriver(), DefaultFindByWidget.class), - dataArray(new AppWithPartiallyCombinedWidgets(), + Arguments.of(new AppWithPartiallyCombinedWidgets(), new AbstractStubWebDriver.StubAndroidBrowserOrWebViewDriver(), DefaultFindByWidget.class) ); } - public CombinedWidgetTest(AbstractApp app, WebDriver driver, Class widgetClass) { - super(app, driver); - this.widgetClass = widgetClass; - } - - @Override - public void checkThatWidgetsAreCreatedCorrectly() { - assertThat("Excpected widget class was " + widgetClass.getName(), + @ParameterizedTest + @MethodSource("data") + void checkThatWidgetsAreCreatedCorrectly(AbstractApp app, WebDriver driver, Class widgetClass) { + assertProxyClassCacheGrowth(); + initElements(new AppiumFieldDecorator(driver), app); + assertThat("Expected widget class was " + widgetClass.getName(), app.getWidget().getSubWidget().getSelfReference().getClass(), equalTo(widgetClass)); @@ -86,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; @@ -173,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 56abc937a..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,27 +1,30 @@ package io.appium.java_client.pagefactory_tests.widget.tests.ios; -import static io.appium.java_client.MobileBy.iOSNsPredicateString; +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.ExtendedWidgetTest; - -public class XCUITWidgetTest extends ExtendedWidgetTest { +public class XCUITWidgetTest extends WidgetTest { public XCUITWidgetTest() { super(new IosApp(), new AbstractStubWebDriver.StubIOSXCUITDriver()); } + @Test @Override public void checkThatWidgetsAreCreatedCorrectly() { checkThatLocatorsAreCreatedCorrectly(app.getWidget(), app.getWidgets(), iOSNsPredicateString(IOS_XCUIT_WIDGET_LOCATOR), iOSNsPredicateString(XCUIT_SUB_WIDGET_LOCATOR)); } + @Test @Override public void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation() { checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getAnnotatedWidget(), @@ -29,6 +32,7 @@ public void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation() { iOSNsPredicateString(XCUIT_ROOT_WIDGET_LOCATOR), iOSNsPredicateString(XCUIT_SUB_WIDGET_LOCATOR)); } + @Test @Override public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() { checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidget(), @@ -36,6 +40,7 @@ public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() iOSNsPredicateString(XCUIT_ROOT_WIDGET_LOCATOR), iOSNsPredicateString(XCUIT_SUB_WIDGET_LOCATOR)); } + @Test @Override public void checkCaseWhenBothWidgetFieldAndClassHaveDeclaredAnnotations() { checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidgetWithOverriddenLocators(), 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 4d264820c..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsApp.java +++ /dev/null @@ -1,124 +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.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 io.appium.java_client.MobileBy#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 io.appium.java_client.MobileBy#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 io.appium.java_client.MobileBy#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 io.appium.java_client.MobileBy#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 io.appium.java_client.MobileBy#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 io.appium.java_client.MobileBy#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 fbd9da9a6..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/windows/WindowsWidgetTest.java +++ /dev/null @@ -1,46 +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.ExtendedWidgetTest; - -public class WindowsWidgetTest extends ExtendedWidgetTest { - - public WindowsWidgetTest() { - super(new WindowsApp(), new AbstractStubWebDriver.StubWindowsDriver()); - } - - @Override - public void checkThatWidgetsAreCreatedCorrectly() { - checkThatLocatorsAreCreatedCorrectly(app.getWidget(), app.getWidgets(), - windowsAutomation(WINDOWS_DEFAULT_WIDGET_LOCATOR), windowsAutomation(WINDOWS_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getAnnotatedWidget(), - ((ExtendedApp) app).getAnnotatedWidgets(), - windowsAutomation(WINDOWS_ROOT_WIDGET_LOCATOR), windowsAutomation(WINDOWS_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidget(), - ((ExtendedApp) app).getExtendedWidgets(), - windowsAutomation(WINDOWS_ROOT_WIDGET_LOCATOR), windowsAutomation(WINDOWS_SUB_WIDGET_LOCATOR)); - } - - @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 27e1a6765..000000000 --- a/src/test/java/io/appium/java_client/remote/MobileOptionsTest.java +++ /dev/null @@ -1,109 +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.Test; -import org.openqa.selenium.MutableCapabilities; -import org.openqa.selenium.ScreenOrientation; - -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Duration; -import java.util.ArrayList; - -import static org.junit.Assert.*; - -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/service/local/AppiumDriverLocalServiceTest.java b/src/test/java/io/appium/java_client/service/local/AppiumDriverLocalServiceTest.java deleted file mode 100644 index e6d51247b..000000000 --- a/src/test/java/io/appium/java_client/service/local/AppiumDriverLocalServiceTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.appium.java_client.service.local; - -import static io.appium.java_client.service.local.AppiumDriverLocalService.parseSlf4jContextFromLogMessage; -import static org.junit.Assert.assertEquals; -import static org.slf4j.LoggerFactory.getLogger; -import static org.slf4j.event.Level.DEBUG; -import static org.slf4j.event.Level.INFO; - -import org.junit.Test; -import org.slf4j.event.Level; - -public class AppiumDriverLocalServiceTest { - - @Test - public void canParseSlf4jLoggerContext() { - assertLoggerContext(INFO, "appium.service.androidbootstrap", - "[AndroidBootstrap] [BOOTSTRAP LOG] [debug] json loading complete."); - assertLoggerContext(INFO, "appium.service.adb", - "[ADB] Cannot read version codes of "); - assertLoggerContext(INFO, "appium.service.xcuitest", - "[XCUITest] Determining device to run tests on: udid: '1234567890', real device: true"); - assertLoggerContext(INFO, "appium.service", - "no-prefix log message."); - assertLoggerContext(INFO, "appium.service", - "no-prefix log [not-a-logger-name] message."); - assertLoggerContext(DEBUG, "appium.service.mjsonwp", - "[debug] [MJSONWP] Calling AppiumDriver.getStatus() with args: []"); - assertLoggerContext(DEBUG, "appium.service.xcuitest", - "[debug] [XCUITest] Xcode version set to 'x.y.z' "); - assertLoggerContext(DEBUG, "appium.service.jsonwpproxy", - "[debug] [JSONWP Proxy] Proxying [GET /status] to [GET http://localhost:18218/status] with no body"); - } - - private void assertLoggerContext(Level expectedLevel, String expectedLoggerName, String logMessage) { - Slf4jLogMessageContext ctx = parseSlf4jContextFromLogMessage(logMessage); - assertEquals(expectedLoggerName, ctx.getName()); - assertEquals(expectedLevel, ctx.getLevel()); - assertEquals(getLogger(expectedLoggerName), ctx.getLogger()); - } -} diff --git a/src/test/java/io/appium/java_client/service/local/StartingAppLocallyTest.java b/src/test/java/io/appium/java_client/service/local/StartingAppLocallyTest.java deleted file mode 100644 index 8cc855440..000000000 --- a/src/test/java/io/appium/java_client/service/local/StartingAppLocallyTest.java +++ /dev/null @@ -1,191 +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.service.local; - -import static io.appium.java_client.TestResources.apiDemosApk; -import static io.appium.java_client.TestResources.uiCatalogAppZip; -import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.android.AndroidDriver; -import io.appium.java_client.ios.BaseIOSTest; -import io.appium.java_client.ios.IOSDriver; -import io.appium.java_client.remote.AndroidMobileCapabilityType; -import io.appium.java_client.remote.AutomationName; -import io.appium.java_client.remote.IOSMobileCapabilityType; -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.github.bonigarcia.wdm.WebDriverManager; -import org.junit.Test; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.remote.DesiredCapabilities; - -public class StartingAppLocallyTest { - - @Test public void startingAndroidAppWithCapabilitiesOnlyTest() { - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, apiDemosApk().toAbsolutePath().toString()); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); - - AndroidDriver driver = new AndroidDriver<>(capabilities); - try { - Capabilities caps = driver.getCapabilities(); - - assertEquals(AutomationName.APPIUM, caps.getCapability(MobileCapabilityType.AUTOMATION_NAME)); - assertEquals(MobilePlatform.ANDROID, caps.getCapability(MobileCapabilityType.PLATFORM_NAME)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - assertEquals(apiDemosApk().toAbsolutePath().toString(), caps.getCapability(MobileCapabilityType.APP)); - } finally { - driver.quit(); - } - } - - @Test public void startingAndroidAppWithCapabilitiesAndServiceTest() { - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, apiDemosApk().toAbsolutePath().toString()); - - AppiumServiceBuilder builder = new AppiumServiceBuilder() - .withArgument(GeneralServerFlag.SESSION_OVERRIDE) - .withArgument(GeneralServerFlag.STRICT_CAPS); - - AndroidDriver driver = new AndroidDriver<>(builder, capabilities); - try { - Capabilities caps = driver.getCapabilities(); - - assertEquals(MobilePlatform.ANDROID, caps.getCapability(MobileCapabilityType.PLATFORM_NAME)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - } finally { - driver.quit(); - } - } - - @Test public void startingAndroidAppWithCapabilitiesAndFlagsOnServerSideTest() { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android"); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - serverCapabilities.setCapability(MobileCapabilityType.FULL_RESET, true); - serverCapabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 60); - serverCapabilities.setCapability(MobileCapabilityType.APP, apiDemosApk().toAbsolutePath().toString()); - - WebDriverManager chromeManager = chromedriver(); - chromeManager.setup(); - serverCapabilities.setCapability(AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE, - chromeManager.getBinaryPath()); - - AppiumServiceBuilder builder = new AppiumServiceBuilder() - .withArgument(GeneralServerFlag.SESSION_OVERRIDE) - .withArgument(GeneralServerFlag.STRICT_CAPS).withCapabilities(serverCapabilities); - - DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - clientCapabilities - .setCapability(AndroidMobileCapabilityType.APP_PACKAGE, "io.appium.android.apis"); - clientCapabilities - .setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, ".view.WebView1"); - - AndroidDriver driver = new AndroidDriver<>(builder, clientCapabilities); - try { - Capabilities caps = driver.getCapabilities(); - - assertEquals(MobilePlatform.ANDROID, caps.getCapability(MobileCapabilityType.PLATFORM_NAME)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - } finally { - driver.quit(); - } - } - - @Test public void startingIOSAppWithCapabilitiesOnlyTest() { - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, BaseIOSTest.PLATFORM_VERSION); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, BaseIOSTest.DEVICE_NAME); - capabilities.setCapability(MobileCapabilityType.APP, uiCatalogAppZip().toAbsolutePath().toString()); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); - - IOSDriver driver = new IOSDriver<>(capabilities); - try { - Capabilities caps = driver.getCapabilities(); - - assertEquals(AutomationName.APPIUM, caps.getCapability(MobileCapabilityType.AUTOMATION_NAME)); - assertEquals(MobilePlatform.IOS, caps.getCapability(MobileCapabilityType.PLATFORM_NAME)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - assertEquals(BaseIOSTest.PLATFORM_VERSION, caps.getCapability(MobileCapabilityType.PLATFORM_VERSION)); - assertEquals(uiCatalogAppZip().toAbsolutePath().toString(), caps.getCapability(MobileCapabilityType.APP)); - } finally { - driver.quit(); - } - } - - - @Test public void startingIOSAppWithCapabilitiesAndServiceTest() { - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, BaseIOSTest.DEVICE_NAME); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); - capabilities.setCapability(MobileCapabilityType.APP, uiCatalogAppZip().toAbsolutePath().toString()); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, BaseIOSTest.PLATFORM_VERSION); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - - AppiumServiceBuilder builder = new AppiumServiceBuilder() - .withArgument(GeneralServerFlag.SESSION_OVERRIDE) - .withArgument(GeneralServerFlag.STRICT_CAPS); - - IOSDriver driver = new IOSDriver<>(builder, capabilities); - try { - Capabilities caps = driver.getCapabilities(); - assertTrue(caps.getCapability(MobileCapabilityType.PLATFORM_NAME) - .toString().equalsIgnoreCase(MobilePlatform.IOS)); - assertNotNull(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - } finally { - driver.quit(); - } - } - - @Test public void startingIOSAppWithCapabilitiesAndFlagsOnServerSideTest() { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, BaseIOSTest.DEVICE_NAME); - serverCapabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, - 500000); //some environment is too slow - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, BaseIOSTest.PLATFORM_VERSION); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); - - DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - clientCapabilities.setCapability(MobileCapabilityType.APP, uiCatalogAppZip().toAbsolutePath().toString()); - - AppiumServiceBuilder builder = new AppiumServiceBuilder() - .withArgument(GeneralServerFlag.SESSION_OVERRIDE) - .withArgument(GeneralServerFlag.STRICT_CAPS).withCapabilities(serverCapabilities); - - IOSDriver driver = new IOSDriver<>(builder, clientCapabilities); - try { - Capabilities caps = driver.getCapabilities(); - assertTrue(caps.getCapability(MobileCapabilityType.PLATFORM_NAME) - .toString().equalsIgnoreCase(MobilePlatform.IOS)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - assertFalse(driver.isBrowser()); - } finally { - driver.quit(); - } - } -} 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 93c1dd804..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 4476ed33e..000000000 --- a/src/test/java/io/appium/java_client/touch/TouchOptionsTests.java +++ /dev/null @@ -1,97 +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 junit.framework.TestCase.fail; -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 io.appium.java_client.touch.offset.ElementOption; -import io.appium.java_client.touch.offset.PointOption; -import org.junit.Test; -import org.openqa.selenium.Point; -import org.openqa.selenium.WebElement; -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(expected = IllegalArgumentException.class) - public void invalidEmptyPointOptionsShouldFailOnBuild() { - new PointOption().build(); - fail("The exception throwing was expected"); - } - - @Test(expected = IllegalArgumentException.class) - public void invalidEmptyElementOptionsShouldFailOnBuild() { - new ElementOption().build(); - fail("The exception throwing was expected"); - } - - @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/utils/TestUtils.java b/src/test/java/io/appium/java_client/utils/TestUtils.java new file mode 100644 index 000000000..8aa90892c --- /dev/null +++ b/src/test/java/io/appium/java_client/utils/TestUtils.java @@ -0,0 +1,106 @@ +/* + * 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 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.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()) { + socket.connect(InetAddress.getByName("8.8.8.8"), 10002); + return socket.getLocalAddress().getHostAddress(); + } + } + + 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)); + } + try { + return Paths.get(url.toURI()).toAbsolutePath(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public static void waitUntilTrue(Supplier func, Duration timeout, Duration interval) { + long started = System.currentTimeMillis(); + RuntimeException lastError = null; + while (System.currentTimeMillis() - started < timeout.toMillis()) { + lastError = null; + try { + Boolean result = func.get(); + if (result != null && result) { + return; + } + //noinspection BusyWait + Thread.sleep(interval.toMillis()); + } catch (RuntimeException | InterruptedException e) { + if (e instanceof InterruptedException) { + throw new RuntimeException(e); + } else { + lastError = (RuntimeException) e; + } + } + } + if (lastError != null) { + throw lastError; + } + throw new TimeoutException(String.format("Condition unmet after %sms timeout", timeout.toMillis())); + } + + public static Point getCenter(WebElement webElement) { + return getCenter(webElement, null); + } + + public static Point getCenter(WebElement webElement, @Nullable Point location) { + Dimension dim = webElement.getSize(); + if (location == null) { + location = webElement.getLocation(); + } + 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