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..ec814bf63 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,175 @@ +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 + # FIXME: Sonatype returns 401 for the snapshots repository + # latest_snapshot=$(curl -sf https://oss.sonatype.org/content/repositories/snapshots/org/seleniumhq/selenium/selenium-api/ | \ + # python -c "import sys,re; print(re.findall(r'\d+\.\d+\.\d+-SNAPSHOT', sys.stdin.read())[-1])") + # echo ">>> $latest_snapshot" + # echo "latest_snapshot=$latest_snapshot" >> "$GITHUB_ENV" + # ./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot + run: | + GROUP_ID="org.seleniumhq.selenium" + ARTIFACT_ID="selenium-api" + REPO_URL="https://repo1.maven.org/maven2" + GROUP_PATH="${GROUP_ID//.//}" + METADATA_URL="${REPO_URL}/${GROUP_PATH}/${ARTIFACT_ID}/maven-metadata.xml" + + metadata=$(curl -s "$METADATA_URL") + latest_snapshot=$(python3 -c " + import sys, xml.etree.ElementTree as ET + root = ET.fromstring(sys.stdin.read()) + print(root.findtext('./versioning/latest')) + " <<< "$metadata") + if [ -z "$latest_snapshot" ]; then + echo "❌ Failed to extract latest released version of ${ARTIFACT_ID} from $metadata" + exit 1 + fi + echo "✅ Latest released version of ${ARTIFACT_ID} is: $latest_snapshot" + echo "latest_snapshot=$latest_snapshot" >> "$GITHUB_ENV" + ./gradlew clean build -PisCI -Pselenium.version=$latest_snapshot + + - 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/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 a43128f4a..da44d7acb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,11 @@ settings.xml build/ .gradle .gradle/* - +classes/ # Eclipse /bin /target /.settings .classpath .project +.vscode diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d902f762e..000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: java - -jdk: - - oraclejdk8 - -sudo: required - -install: true - -script: ./gradlew clean build -x test -x signArchives 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/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index de0ad9449..6dc55c833 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,12 +1,12 @@ ## Description -Please describe the issue. It may be a bug description. So then please briefly descride steps which you were trying to perform and what happened instead. -If there is the feature you want to see added to Appium Java client so please describe necessity of this feature and the way that it should work. +Please describe the issue. If it is a bug, please briefly describe the steps to reproduce and the actual vs expected behavior. +If there is a feature that you would like to see added to the Appium Java client, please describe the necessity of this feature and the way that it should work. ## Environment -* 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: @@ -19,16 +19,16 @@ Please provide more details, if necessary. ## Code To Reproduce Issue [ Good To Have ] -Please remember that, with sample code; it's easier to reproduce bug and much faster to fix it. -You can git clone https://github.com/appium/sample-code or https://github.com/appium/sample-code/tree/master/sample-code/apps and reproduce an issue using Java and sample apps. +Please remember: it's easier to reproduce and fix the bug with sample code. +You can git clone https://github.com/appium/appium/tree/master/sample-code or https://github.com/appium/appium/tree/master/sample-code/apps and reproduce an issue using Java and sample apps. Also you can create a [gist](https://gist.github.com) with pasted java code sample or put it here 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 +## Exception Stacktraces -Please create a [gist](https://gist.github.com) with pasted stacktrace of exception thrown by java. +Please create a [gist](https://gist.github.com) with the pasted stacktrace of the exception thrown by java. -## Link to Appium logs +## Link To Appium Logs Please create a [gist](https://gist.github.com) which is a paste of your _full_ Appium logs, and link them here. Do _not_ paste your full Appium logs here, as it will make this issue very long and hard to read! If you are reporting a bug, _always_ include Appium logs as linked gists! It helps to define the problem correctly and clearly. diff --git a/NOTICE b/NOTICE index c74837a3d..fab54b9d8 100644 --- a/NOTICE +++ b/NOTICE @@ -1 +1 @@ -Copyright 2014-2017 Appium Contributors +Copyright 2014-2018 Appium Contributors diff --git a/README.md b/README.md index de0e3d128..7f3f54480 100644 --- a/README.md +++ b/README.md @@ -1,438 +1,273 @@ # 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) -[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/io.appium/java-client/badge.svg)](http://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) +[![Maven Central Version](https://img.shields.io/maven-central/v/io.appium/java-client)](https://central.sonatype.com/artifact/io.appium/java-client) +[![Javadocs](https://www.javadoc.io/badge/io.appium/java-client.svg)](https://www.javadoc.io/doc/io.appium/java-client) +[![Appium Java Client CI](https://github.com/appium/java-client/actions/workflows/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](http://appium.github.io/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. + +## v7 to v8 Migration + +Since version 8 Appium Java Client had several major changes, which might require to +update your client code. Make sure to follow the [v7 to v8 Migration Guide](./docs/v7-to-v8-migration-guide.md) +to streamline the migration process. + +## Add Appium java client to your test framework + +### 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 + + + jitpack.io + https://jitpack.io + + +``` + +Add the dependency: + +```xml + + com.github.appium + java-client + latest commit ID from master branch + +``` + +#### Gradle + +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: + +```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.36.0` +`10.0.0` | `4.35.0`, `4.36.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 -*5.0.0 (under construction yet)* -- **[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) - -*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/archive/docs/How-to-propose-a-PR.md b/archive/docs/How-to-propose-a-PR.md deleted file mode 100644 index 22a1a6995..000000000 --- a/archive/docs/How-to-propose-a-PR.md +++ /dev/null @@ -1,24 +0,0 @@ -# A good pull-request should contain - -### Change list - -There should be provided briefly described change list which are you going to propose. - -### Types of changes - -What types of changes are proposed/introduced to Java client? -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -### Details - -There should be provided more details about changes if it is necessary. If there are new features you -can provide code samples which show the way they work and possible use cases. Also you can create [gists](https://gist.github.com) -with pasted java code samples or put them at a PR 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/) - -# Pull-request template - -There is [PULL_REQUEST_TEMPLATE.md)](https://github.com/appium/java-client/blob/master/PULL_REQUEST_TEMPLATE.md) which should help you to make a good pull request. diff --git a/archive/docs/How-to-report-an-issue.md b/archive/docs/How-to-report-an-issue.md deleted file mode 100644 index 0adf13fbf..000000000 --- a/archive/docs/How-to-report-an-issue.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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. -Be sure that this bug is not reported [here](https://github.com/appium/appium/issues) and/or there is -no progress on this issue. - -# The good issue report should contain - -### Description - -The bug report should contain a brief description of a bug. -If it is the feature request then there should be the description of this feature and the way that it should work. - -### 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: -* 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: -* Real device or emulator/simulator: - -### Details - -If it is necessary there should provided more details - - -### Code To Reproduce Issue (good to Have if you report a bug) - -It's easier to reproduce bug and much faster to fix it. -You can git clone https://github.com/appium/sample-code or https://github.com/appium/sample-code/tree/master/sample-code/apps and reproduce an issue using Java and sample apps. -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) - -There should be created a [gist](https://gist.github.com) with pasted stacktrace of exception thrown by java. - -### Link to Appium logs - -There should be created a [gist](https://gist.github.com) which is a paste of your _full_ Appium logs, and link them to a new issue. Do _not_ paste your full Appium logs at the issue description, as it will make this issue very long and hard to read! -If you are reporting a bug, _always_ include Appium logs as linked gists! It helps to define the problem correctly and clearly. - - -# Issue template -There is [ISSUE_TEMPLATE.md](https://github.com/appium/java-client/blob/master/ISSUE_TEMPLATE.md) which should help you to make a good issue report. - -# ... And don't say that you weren't warned. - -If a report is not readable and/or there is no response from a reporter and some important details are needed then the issue will be closed after some time. diff --git a/archive/docs/Installing-the-project.md b/archive/docs/Installing-the-project.md deleted file mode 100644 index a11590564..000000000 --- a/archive/docs/Installing-the-project.md +++ /dev/null @@ -1,31 +0,0 @@ -[Download the jar from Maven](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.appium%22%20AND%20a%3A%22java-client%22) or add the following to pom.xml: - -``` - - io.appium - java-client - 4.1.2 - -``` - -It currently depends on selenium-java 2.53.1. If it is necessary to use another version of Selenium then you can configure pom.xml as follows: - -``` - - io.appium - java-client - 4.1.1 - - - org.seleniumhq.selenium - selenium-java - - - - - - org.seleniumhq.selenium - selenium-java - ${selenium.version.you.require} - -``` diff --git a/archive/docs/Note-for-developers.md b/archive/docs/Note-for-developers.md deleted file mode 100644 index 97f9dbe2f..000000000 --- a/archive/docs/Note-for-developers.md +++ /dev/null @@ -1,63 +0,0 @@ -# IDE - -The **Intellij Idea** is recommended. - -# Settings - -## Importing the project - -This is the Gradle project. - -![](https://cloud.githubusercontent.com/assets/4927589/18324097/6141ef7c-7543-11e6-8661-81d631615502.png) - -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) - -## Coding Standards - -Appium java-client strictly follows [Google Java Style](http://google-styleguide.googlecode.com/svn/trunk/javaguide.html) as a coding standards. Contributors are requested to follow this by configuring in their IDE's Editor Code style, - -* Clone [Google Style Guide](https://github.com/google/styleguide.git) from git - -**Intellij IDEA** users can configure this way, -`Files -> Other Settings -> Default Settings ->Editor -> Code Style -> Manage -> Manage -> Import -> eclipse-java-google-style.xml (Downloaded from Google Style Guide)-> Apply` - -Reformat your code before raising a Pull Request. - - -## Code Coverage - -`jacoco-maven-plugin` generates the coverage reports, once integration tests are successfully run by `maven-surefire-plugin` - -**Intellij IDEA** user's can configure and view this way, -`Analyse -> show coverage Data -> Add -> Select ${basedir}/target/coverage-reports/jacoco-unit.exec (jacoco-unit.exec generated after integration test run) -> Click Show Selected -> Coverage Results displayed in IDE` - -# Please do not forget to check the code before the pull-request proposal. - -It is needed to go to the directory where `java_client` is located. You can do it via command line. And then run the following command - -`gradle check`. If everything is ok then all checks should be passed. Otherwise you can open reports at `JAVA_CLIENT_DIRECTORY/build/reports` - -**The adding of new tests is required when new feature is added or some bug is fixed.** \ No newline at end of file diff --git a/archive/docs/Page-objects.md b/archive/docs/Page-objects.md deleted file mode 100644 index 9b8ae8aba..000000000 --- a/archive/docs/Page-objects.md +++ /dev/null @@ -1,586 +0,0 @@ -Appium Java client has facilities which components to [Page Object](https://github.com/SeleniumHQ/selenium/wiki/PageObjects) design pattern and [Selenium PageFactory](https://github.com/SeleniumHQ/selenium/wiki/PageFactory). - - -# WebElement/list of WebElement field can be populated by default: -```java -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; -... - -@FindBy(someStrategy) //for browser or web view html UI -//also for mobile native applications when other locator strategies are not defined -WebElement someElement; - -@FindBy(someStrategy) //for browser or web view html UI -//also for mobile native applications when other locator strategies are not defined -List someElements; -``` - -# If there is need to use convinient locators for mobile native applications then the following is available: - -```java -import io.appium.java_client.android.AndroidElement; -import org.openqa.selenium.remote.RemoteWebElement; -import io.appium.java_client.pagefactory.*; -import io.appium.java_client.ios.IOSElement; - -@AndroidFindBy(someStrategy) //for Android UI when Android UI automator is used -AndroidElement someElement; - -@AndroidFindBy(someStrategy) //for Android UI when Android UI automator is used -List someElements; - -@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is used -RemoteWebElement someElement; - -@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is used -List someElements; - -@iOSFindBy(someStrategy) //for iOS native UI -IOSElement someElement; - -@iOSFindBy(someStrategy) //for iOS native UI -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; - -@AndroidFindBy(someStrategy) //for the crossplatform mobile native -@iOSFindBy(someStrategy) //testing -List someElements; -``` - -# The fully cross platform examle - -```java -import org.openqa.selenium.remote.RemoteWebElement; -import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBy; - -//the fully cross platform examle -@FindBy(someStrategy) //for browser or web view html UI -@AndroidFindBy(someStrategy) //for Android native UI -@iOSFindBy(someStrategy) //for iOS native UI -RemoteWebElement someElement; - -//the fully cross platform examle -@FindBy(someStrategy) -@AndroidFindBy(someStrategy) //for Android native UI -@iOSFindBy(someStrategy) //for iOS native UI -List someElements; -``` - -# Also it is possible to define chained or any possible locators. - -## - Chained - -```java -import org.openqa.selenium.remote.RemoteWebElement; -import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.FindBy; - -@FindBys({@FindBy(someStrategy1), @FindBy(someStrategy2)}) -@AndroidFindBys({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) -@iOSFindBys({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) -RemoteWebElement someElement; - -@FindBys({@FindBy(someStrategy1), @FindBy(someStrategy2)}) -@AndroidFindBys({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) -@iOSFindBys({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) -List someElements; -``` - -## - Any possible - -```java -import org.openqa.selenium.remote.RemoteWebElement; -import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindByAll; - -@FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)}) -@AndroidFindAll({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) -@iOSFindAll({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) -RemoteWebElement someElement; - -@FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)}) -@AndroidFindAll({@AndroidFindBy(someStrategy1), @AndroidFindBy(someStrategy2)}) -@iOSFindAll({@iOSFindBy(someStrategy1), @iOSFindBy(someStrategy2)}) -List someElements; -``` - -# Appium Java client is integrated with Selenium PageFactory by AppiumFieldDecorator. - -Object fields are populated as below: - -- -```java -import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.PageFactory; - -PageFactory.initElements(new AppiumFieldDecorator(searchContext - /*searchContext is a WebDriver or WebElement - instance */), - pageObject //an instance of PageObject.class -); -``` - -- -```java -import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.PageFactory; -import java.util.concurrent.TimeUnit; - -PageFactory.initElements(new AppiumFieldDecorator(searchContext, - /*searchContext is a WebDriver or WebElement - instance */ - 15, //default implicit waiting timeout for all strategies - TimeUnit.SECONDS), - pageObject //an instance of PageObject.class -); -``` - -- -```java -import io.appium.java_client.pagefactory.*; -import org.openqa.selenium.support.PageFactory; -import java.util.concurrent.TimeUnit; - -PageFactory.initElements(new AppiumFieldDecorator(searchContext, - /*searchContext is a WebDriver or WebElement - instance */ - new TimeOutDuration(15, //default implicit waiting timeout for all strategies - TimeUnit.SECONDS)), - pageObject //an instance of PageObject.class -); -``` - -If time of the waiting for elements differs from usual (longer, or shorter when element is needed only for quick checkings/assertions) then - -```java -import io.appium.java_client.pagefactory.*; - -@WithTimeout(timeOut = yourTime, timeUnit = yourTimeUnit) -RemoteWebElement someElement; - -@WithTimeout(timeOut = yourTime, timeUnit = yourTimeUnit) -List someElements; -``` - -# The additional feature. - -## The simple example -Let's imagine that the task is to check an Android client of the [http://www.rottentomatoes.com](http://www.rottentomatoes.com/). Let it be like a picture below - -![](https://cloud.githubusercontent.com/assets/4927589/11120641/51c1fda8-8962-11e5-8b17-323b5f236fce.png) Lets imagine that it is only a part of the screen. - -A typical page object could look like: - -```java -public class RottenTomatoesScreen { - //convinient locator - private List titles; - - //convinient locator - private List scores; - - //convinient locator - private List castings; - //element declaration goes on - - public String getMovieCount(){ - //....... - } - - public String getTitle(params){ - //....... - } - - public String getScore(params){ - //....... - } - - public String getCasting(params){ - //....... - } - - public void openMovieInfo(params){ - //....... - } - - //method declaration goes on -} -``` - -The description above can be decomposed. Let's work it out! - -Firstly a Movie-widget could be described this way: - -```java -import io.appium.java_client.pagefactory.Widget; -import org.openqa.selenium.WebElement; - -public class Movie extends Widget{ - protected Movie(WebElement element) { - super(element); - } - - //convinient locator - private AndroidElement title; - - //convinient locator - private AndroidElement score; - - //convinient locator - private AndroidElement casting; - - public String getTitle(params){ - //....... - } - - public String getScore(params){ - //....... - } - - public String getCasting(params){ - //....... - } - - public void openMovieInfo(params){ - ((AndroidElement) getWrappedElement()).tap(1, 1500); - } - -} -``` - -So, now page object looks - -```java -public class RottenTomatoesScreen { - - @AndroidFindBy(a locator which convinient to find a single movie-root - element) - private List movies; - - //element declaration goes on - - public String getMovieCount(){ - return movies.size(); - } - - public Movie getMovie(int index){ - //any interaction with sub-elements of a movie-element - //will be performed outside of the page-object instance - return movie.get(index); - } - //method declaration goes on -} -``` - -### Ok. What if Movie-class is reused and a wrapped root element is usually found by the same locator? - -Then -```java -//the class is annotated !!! -@AndroidFindBy(a locator which convinient to find a single movie-root - element) -public class Movie extends Widget{ -... -} - -``` -and - -```java -public class RottenTomatoesScreen { - //!!! locator is not necessary at this case - private List movies; -... -} -``` - -### Ok. What if movie list is not a whole screen? E.g. we want to describe it as a widget with nested movies. - -Then: - -```java -//with the usual locator or without it -public class Movies extends Widget{ - - //with a custom locator or without it - private List movies; -... -} -``` - -and - -```java -public class RottenTomatoesScreen { - - //with a custom locator or without it - Movies movies; -... -} -``` - -### Good! How to poputate all these fields? - -As usual: - -```java -RottenTomatoesScreen screen = new RottenTomatoesScreen(); -PageFactory.initElements(new AppiumFieldDecorator(searchContext /*WebDriver or WebElement - instance */), screen); -``` - - -## Specification - -A class which describes a widget or group of elements should extend - -```java -io.appium.java_client.pagefactory.Widget; -``` - -Any widget/group of elements can be described it terms of sub-elements or nested sub-widgets. -Appium-specific annotations are used for this purpose. - -### Any class which describes the real widget or group of elements can be annotated - -That means that when the same "widget" is used frequently and any root element of this can be found by the same locator then user can - -```java -@FindBy(relevant locator) //how to find a root element -public class UsersWidget extends Widget{ - - @FindBy(relevant locator) //this element will be found - //using the root element - WebElement subElement1; - - @FindBy(relevant locator) //this element will be found - //using the root element - WebElement subElement2; - - @FindBy(relevant locator) //a root element - //of this widget is the sub-element which - //will be found from top-element - UsersWidget subWidget; - - //and so on.. -} -``` - -and then it is enough - -```java - //above is the other field declaration - - UsersWidget widget; - - //below is the other field/method declaration -``` - -If the widget really should be found using an another locator then - -```java - //above is the other field declaration - @FindBy(another relevant locator) //this locator overrides - //the declared in the using class - UsersWidget widget; - - //below is the other field/method declaration -``` - -### Ok. What should users do if they want to implement a subclass which describes a similar group of elements for the same platform? - -There is nothing special. - -```java -@FindBy(relevant locator) //how to find a root element -public class UsersWidget extends Widget{ -... -} - -``` -```java -//at this case the root element will be found by the locator -//which is declared in superclass -public class UsersOverriddenWidget extends UsersWidget { -... -} -``` - -and - -```java -@FindBy(relevant locator2) //this locator overrides -//all locators declared in superclasses -public class UsersOverriddenWidget2 extends UsersWidget { -... -} -``` - -### Is it possible to reuse "widgets" in crossplatform testing? - -If there is no special details of interaction with an application browser version and/or versions for different mobile OS's then - - -```java -@FindBy(relevant locator for browser/webview html or by default) -@AndroidFindBy(relevant locator for Android UI automator) -@iOSFindBy(relevant locator for iOS UI automation) -public class UsersWidget extends Widget { - - @FindBy(relevant locator for browser/webview html or by default) - @AndroidFindBy(relevant locator for Android UI automator) - @iOSFindBy(relevant locator for iOS UI automation) - RemoteWebElement subElement1; - - @FindBy(relevant locator for browser/webview html or by default) - @AndroidFindBy(relevant locator for Android UI automator) - @iOSFindBy(relevant locator for iOS UI automation) - RemoteWebElement subElement2; - - //overrides a html/default - //locator declared in the used class - @FindBy(relevant locator for browser/webview html or by default) - //overrides an Android UI automator - //locator declared in the used class - @AndroidFindBy(relevant locator for Android UI automator) - //overrides an iOS UI automation - //locator declared in the using class - @iOSFindBy(relevant locator for iOS UI automation) - UsersWidget subWidget; - - //and so on.. -} -``` - -### What if interaction with a "widget" has special details for each used platform, but the same at high-level - -Then it is possible - -```java -public /*abstract*/ class DefaultAbstractUsersWidget extends Widget{ - -} -``` - -and - -```java -@FindBy(locator) -public class UsersWidgetForHtml extends DefaultAbstractUsersWidget { - -} -``` - -and - -```java -@AndroidFindBy(locator) -public class UsersWidgetForAndroid extends DefaultAbstractUsersWidget { - -} -``` - -and even - -```java -@iOSFindBy(locator) -public class UsersWidgetForIOS extends DefaultAbstractUsersWidget { - -} -``` - -and then - - -```java - import io.appium.java_client.pagefactory.OverrideWidget; - ... - - //above is the other field declaration - @OverrideWidget(html = UsersWidgetForHtml.class, - androidUIAutomator = UsersWidgetForAndroid.class, - iOSUIAutomation = UsersWidgetForIOS .class) - DefaultAbstractUsersWidget widget; - - //below is the other field/method declaration -``` - -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 - -```java - //above is the other field declaration - - @OverrideWidget(iOSUIAutomation = UsersWidgetForIOS .class) - DefaultUsersWidget widget; //lets assume that there are differences of - //interaction with iOS and by default we use DefaultUsersWidget. - //Then DefaultUsersWidget should not be abstract too. - // - - //below is the other field/method declaration -``` - -- for now it is not possible to - -```java - import io.appium.java_client.pagefactory.OverrideWidget; - ... - - //above is the other field declaration - @OverrideWidget(html = UsersWidgetForHtml.class, - androidUIAutomator = UsersWidgetForAndroid.class, - iOSUIAutomation = UsersWidgetForIOS .class) - DefaultAbstractUsersWidget widget; - - //below is the other field/method declaration - - //user's code - ((UsersWidgetForAndroid) widget).doSpecialWorkForAndroing() -``` - -The workaround: - -```java - import io.appium.java_client.pagefactory.OverrideWidget; - ... - - //above is the other field declaration - @OverrideWidget(html = UsersWidgetForHtml.class, - androidUIAutomator = UsersWidgetForAndroid.class, - iOSUIAutomation = UsersWidgetForIOS .class) - DefaultAbstractUsersWidget widget; - - //below is the other field/method declaration - - //user's code - ((UsersWidgetForAndroid) widget.getSelfReference()).doSpecialWorkForAndroing() -``` - -### Good! What about widget lists? - -All that has been mentioned above is true for "widget" lists. - -### One more restriction - -It is strongly recommended to implement each subclass of __io.appium.java_client.pagefactory.Widget__ with this constructor - -```java - public /*or any other available modifier*/ WidgetSubclass(WebElement element) { - super(element); - } -``` \ No newline at end of file diff --git a/archive/docs/The-event_firing.md b/archive/docs/The-event_firing.md deleted file mode 100644 index 38071a9a7..000000000 --- a/archive/docs/The-event_firing.md +++ /dev/null @@ -1,125 +0,0 @@ -since 4.1.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 - -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): - -- `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. - -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](http://seleniumworks.blogspot.ru/2014/02/eventfiringwebdriver.html). - -Here we were trying to improve existing drawbacks and restrictions using: - -- API splitting, see above. - -- the binding of some [Spring framework engines](https://projects.spring.io/spring-framework/) with [AspectJ](https://en.wikipedia.org/wiki/AspectJ). - -# How to use - -It is easy. - -```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); -``` - -## 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) - -- define the list of default listeners at the `io.appium.java_client.events.api.Listener` - -![image](https://cloud.githubusercontent.com/assets/4927589/16731509/2734a4e0-4781-11e6-81cb-ab64a5924c35.png) - -And then it is enough - -```java - -//and so on -... -import io.appium.java_client.events.EventFiringWebDriverFactory; -... - -AndroidDriver driver = new AndroidDriver(parameters); -driver = EventFiringWebDriverFactory.getEventFiringWebDriver(driver); -``` - -If there are listeners defined externally when this collection is merged with default set of listeners. - -# How to reuse customized WebDriverEventListener - -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. - - -```java -import org.openqa.selenium.support.events.WebDriverEventListener; -import io.appium.java_client.events.api.general.AppiumWebDriverEventListener; - -public class UsersWebDriverEventListener implements WebDriverEventListener, AppiumWebDriverEventListener { -... -} -``` - -or just - -```java -import io.appium.java_client.events.api.general.AppiumWebDriverEventListener; - -public class UsersWebDriverEventListener implements AppiumWebDriverEventListener { -... -} -``` diff --git a/archive/docs/The-starting-of-an-Android-app.md b/archive/docs/The-starting-of-an-Android-app.md deleted file mode 100644 index 31ff4203e..000000000 --- a/archive/docs/The-starting-of-an-Android-app.md +++ /dev/null @@ -1,88 +0,0 @@ -# Steps: - -- you have to prepare environment for Android. Details are provided here: http://appium.io/slate/en/master/?java#setup-(android) - -- you have to download the desktop app [for Windows or Mac OS X](https://bitbucket.org/appium/appium.app/downloads/) or install it using _npm_ -_$ npm install -g appium_ or _$ npm install appium@required_version_ - -- it needs to launch the appium server. If you use the server installed via npm then - - _$ node **the_path_to_js_file** --arg1 value1 --arg2 value2_ -where **the_path_to_js_file** is the full path to **appium.js** file (if the node server version version <= 1.4.16) or **main.js** (if the node server version version >= 1.5.0). It is not necessary to use arguments. The list of arguments: http://appium.io/slate/en/master/?java#appium-server-arguments - - -# The starting of an app - -It looks like creation of a common [RemoteWebDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/remote/RemoteWebDriver.html) instance. - -[Common capabilities](http://appium.io/slate/en/master/?java#the---default-capabilities-flag) - -[Android-specific capabilities](http://appium.io/slate/en/master/?java#android-only) - -[Common capabilities provided by Java client](http://appium.github.io/java-client/io/appium/java_client/remote/MobileCapabilityType.html) - -[Android-specific capabilities provided by Java client](http://appium.github.io/java-client/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()); -//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: - -```java -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.android.AndroidDriver; -import org.openqa.selenium.remote.RemoteWebElement; -import java.net.URL; - - -... -DesiredCapabilities capabilities = new DesiredCapabilities(); -capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); -capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.CHROME); -//if it is necessary to use the default Android browser then MobileBrowserType.BROWSER -//is your choise -... -//you are free to set additional capabilities -AppiumDriver driver = new AndroidDriver<>( -new URL("http://target_ip:used_port/wd/hub"), capabilities); -``` - -or - -```java -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.remote.MobilePlatform; -import org.openqa.selenium.remote.RemoteWebDriver; -import java.net.URL; - - -... -DesiredCapabilities capabilities = new DesiredCapabilities(); -capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); -capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); -capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.CHROME); -//you are free to set additional capabilities -RemoteWebDriver driver = new RemoteWebDriver( -new URL("http://target_ip:used_port/wd/hub"), capabilities); -``` \ No newline at end of file diff --git a/archive/docs/The-starting-of-an-app-using-Appium-node-server-started-programmatically.md b/archive/docs/The-starting-of-an-app-using-Appium-node-server-started-programmatically.md deleted file mode 100644 index 6b897c61d..000000000 --- a/archive/docs/The-starting-of-an-app-using-Appium-node-server-started-programmatically.md +++ /dev/null @@ -1,289 +0,0 @@ -# Requirements -- Installed Node.js 0.12 or greater. - -- At least an appium server instance installed via __npm__. - -# The basic principle. - -It works the similar way as common [ChromeDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/chrome/ChromeDriver.html), [InternetExplorerDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/ie/InternetExplorerDriver.html) of Selenium project or [PhantomJSDriver](http://cdn.ivandemarino.me/phantomjsdriver-javadoc/org/openqa/selenium/phantomjs/PhantomJSDriver.html). They use subclasses of the [DriverService](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/remote/service/DriverService.html). - -# Which capabilities this feature provides - -This feature providese 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 - - -## If there is no specific parameters then - -```java - import io.appium.java_client.service.local.AppiumDriverLocalService; - ... - - AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - ... - service.stop(); -``` - -### FYI - -There are possible problems related to local environment which could break this: -```java -AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); -``` - -It is more usual for UNIX/LINUX-like OS's. Also there are situations when should be used an another Node.JS instance, e.g. the instance which is installed in the directory that differs from one defined at the PATH environmental variable. The same may be true for Appium node server (it is related to _appium.js_ file (v <= 1.4.16) and _main.js_ (v >= 1.5.0)). - -At this case user is able to set up values of the **NODE_BINARY_PATH** (The environmental variable used to define the path to executable NodeJS file (node.exe for WIN and node for Linux/MacOS X)) and the **APPIUM_BINARY_PATH** (The environmental variable used to define the path to executable appium.js (1.4.x and lower) or main.js (1.5.x and higher)) environmental variables/system properties. Also it is possible to define these values programmatically: - -```java -//appium.node.js.exec.path -System.setProperty(AppiumServiceBuilder.NODE_PATH , -"the path to the desired node.js executable"); - -System.setProperty(AppiumServiceBuilder.APPIUM_PATH , -"the path to the desired appium.js or main.js"); - -AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); -``` - -## If there should be non default parameters specified then - -```java -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; -... - -AppiumDriverLocalService service = AppiumDriverLocalService. -buildService(new AppiumServiceBuilder(). -withArgument(GeneralServerFlag.TEMP_DIRECTORY, - "The_path_to_the_temporary_directory")); -``` - -or - -```java -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; -... - -AppiumDriverLocalService service = new AppiumServiceBuilder(). -withArgument(GeneralServerFlag.TEMP_DIRECTORY, - "The_path_to_the_temporary_directory").build(); -``` - -Lists of available server side flags are here: - -- io.appium.java_client.service.local.flags.GeneralServerFlag; -- io.appium.java_client.service.local.flags.AndroidServerFlag; -- io.appium.java_client.service.local.flags.IOSServerFlag - - -## Which parameters also can be defined - -- If it is necessary to define some specific port or any free port - -```java -new AppiumServiceBuilder().usingPort(4000); -``` - -or - -```java -new AppiumServiceBuilder().usingAnyFreePort(); -``` - -- If it is necessary to use another IP address - -```java -new AppiumServiceBuilder().withIPAddress("127.0.0.1"); -``` - -- If it is necessary to define output log file - -```java -import java.io.File; - ... - -new AppiumServiceBuilder().withLogFile(logFile); -``` - -- If it is necessary to define another Node.js executable file - -```java -import java.io.File; - -... - -new AppiumServiceBuilder().usingDriverExecutable(nodeJSExecutable); -``` - -- If it is necessary to define another appium.js/main.js file - -```java -import java.io.File; - -... -//appiumJS is the full or relative path to -//the appium.js (v<=1.4.16) or maim.js (v>=1.5.0) -new AppiumServiceBuilder().withAppiumJS(new File(appiumJS)); -``` - -- It is possible to define server capabilities (node server v >= 1.5.0) - -```java -DesiredCapabilities serverCapabilities = new DesiredCapabilities(); -...//the capability filling - -AppiumServiceBuilder builder = new AppiumServiceBuilder(). -withCapabilities(serverCapabilities); -AppiumDriverLocalService service = builder.build(); -service.start(); -... -service.stop(); -``` - -Capabilities which are used by a builder can be completed/orerriden any similar way: - -```java -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, app.getAbsolutePath()); -serverCapabilities.setCapability(AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE, -chrome.getAbsolutePath()); //this capability set can be used for all cases - -AppiumServiceBuilder builder = new AppiumServiceBuilder(). -withCapabilities(serverCapabilities); -AppiumDriverLocalService service = builder.build(); - -DesiredCapabilities clientCapabilities = new DesiredCapabilities(); -clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, -"io.appium.android.apis"); -clientCapabilities.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, -".view.WebView1"); -``` - -then - -```java -AndroidDriver driver = -new AndroidDriver<>(service, clientCapabilities); -``` - -or - -```java -AndroidDriver driver = -new AndroidDriver<>(builder, clientCapabilities); -``` - -or - -```java -service.start(); -AndroidDriver driver = -new AndroidDriver<>(service.getUrl(), clientCapabilities); -``` - -# How to create an AppiumDriver instance - -Many constructors of [AndroidDriver](http://appium.github.io/java-client/io/appium/java_client/android/AndroidDriver.html)/[IOSDriver](http://appium.github.io/java-client/io/appium/java_client/ios/IOSDriver.html) use [AppiumDriverLocalService](http://appium.github.io/java-client/io/appium/java_client/service/local/AppiumDriverLocalService.html) or [AppiumServiceBuilder](http://appium.github.io/java-client/io/appium/java_client/service/local/AppiumServiceBuilder.html) as parameters. -The list of constructors is below. - -```java -public AndroidDriver(URL remoteAddress, - org.openqa.selenium.Capabilities desiredCapabilities) - -public AndroidDriver(URL remoteAddress, - org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public AndroidDriver(AppiumDriverLocalService service, - org.openqa.selenium.Capabilities desiredCapabilities) - -public AndroidDriver(AppiumDriverLocalService service, - org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public AndroidDriver(AppiumServiceBuilder builder, - org.openqa.selenium.Capabilities desiredCapabilities) - -public AndroidDriver(AppiumServiceBuilder builder, - org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public AndroidDriver(org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public AndroidDriver(org.openqa.selenium.Capabilities desiredCapabilities) -``` - -```java -public IOSDriver(URL remoteAddress, - org.openqa.selenium.Capabilities desiredCapabilities) - -public IOSDriver(URL remoteAddress, - org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public IOSDriver(AppiumDriverLocalService service, - org.openqa.selenium.Capabilities desiredCapabilities) - -public IOSDriver(AppiumDriverLocalService service, - org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public IOSDriver(AppiumServiceBuilder builder, - org.openqa.selenium.Capabilities desiredCapabilities) - -public IOSDriver(AppiumServiceBuilder builder, - org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public IOSDriver(org.openqa.selenium.remote.http.HttpClient.Factory httpClientFactory, - org.openqa.selenium.Capabilities desiredCapabilities) - -public IOSDriver(org.openqa.selenium.Capabilities desiredCapabilities) -``` - -An instance of __AppiumDriverLocalService__ which has passed through constructors will be stopped when - -```java - driver.quit(); -``` - -If it is necessary to keep the service alive during a long time then something like that is available - -```java - service.start(); - - .... - - new IOSDriver(service.getUrl(), capabilities) -``` diff --git a/archive/docs/The-starting-of-an-iOS-app.md b/archive/docs/The-starting-of-an-iOS-app.md deleted file mode 100644 index a470c4137..000000000 --- a/archive/docs/The-starting-of-an-iOS-app.md +++ /dev/null @@ -1,91 +0,0 @@ -# Steps: - -- you have to prepare environment for iOS. Details are provided here: http://appium.io/slate/en/master/?ruby#system-setup-(ios) - -- you have to download the desktop app [for Mac OS X](https://bitbucket.org/appium/appium.app/downloads/) or install it using _npm_ -_$ npm install -g appium_ or _$ npm install appium@required_version_ - -- it needs to launch the appium server. If you use the server installed via npm then - - _$ node **the_path_to_js_file** --arg1 value1 --arg2 value2_ -where **the_path_to_js_file** is the full path to **appium.js** file (if the node server version version <= 1.4.16) or **main.js** (if the node server version version >= 1.5.0). It is not necessary to use arguments. The list of arguments: http://appium.io/slate/en/master/?ruby#appium-server-arguments - -# The starting of an app - -It looks like creation of a common [RemoteWebDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/remote/RemoteWebDriver.html) instance. - -[Common capabilities](http://appium.io/slate/en/master/?ruby#the---default-capabilities-flag) - -[iOS-specific capabilities](http://appium.io/slate/en/master/?ruby#ios-only) - -[Common capabilities provided by Java client](http://appium.github.io/java-client/io/appium/java_client/remote/MobileCapabilityType.html) - -[iOS-specific capabilities provided by Java client](http://appium.github.io/java-client/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.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 -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.remote.MobileBrowserType; -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.ios.IOSDriver; -import org.openqa.selenium.remote.RemoteWebElement; -import java.net.URL; - - -... -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.BROWSER_NAME, MobileBrowserType.SAFARI); -... -//you are free to set additional capabilities -AppiumDriver driver = new IOSDriver<>( -new URL("http://target_ip:used_port/wd/hub"), capabilities); -``` - -or - -```java -import io.appium.java_client.remote.MobilePlatform; -import org.openqa.selenium.remote.DesiredCapabilities; -import io.appium.java_client.remote.MobileBrowserType; -import org.openqa.selenium.remote.RemoteWebDriver; -import java.net.URL; - - -... -DesiredCapabilities capabilities = new DesiredCapabilities(); -capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); -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.BROWSER_NAME, MobileBrowserType.SAFARI); -//you are free to set additional capabilities -RemoteWebDriver driver = new RemoteWebDriver( -new URL("http://target_ip:used_port/wd/hub"), capabilities); -``` diff --git a/archive/docs/Touch-actions.md b/archive/docs/Touch-actions.md deleted file mode 100644 index 20467c458..000000000 --- a/archive/docs/Touch-actions.md +++ /dev/null @@ -1 +0,0 @@ -... under constraction \ No newline at end of file diff --git a/archive/pom.xml b/archive/pom.xml deleted file mode 100644 index 30378f00c..000000000 --- a/archive/pom.xml +++ /dev/null @@ -1,362 +0,0 @@ - - - - - 4.0.0 - - io.appium - java-client - 5.0.0-SNAPSHOT - - - org.seleniumhq.selenium - selenium-java - 2.53.1 - - - cglib - cglib-nodep - - - com.google.code.gson - gson - - - - - com.google.code.gson - gson - 2.7 - - - junit - junit - 4.12 - test - - - org.apache.httpcomponents - httpclient - 4.5.2 - - - com.google.guava - guava - 19.0 - - - commons-validator - commons-validator - 1.5.1 - - - org.apache.commons - commons-lang3 - 3.4 - - - cglib - cglib-nodep - 3.2.4 - - - org.springframework - spring-context - 4.3.2.RELEASE - compile - - - org.aspectj - aspectjweaver - 1.8.9 - compile - - - jar - java-client - Java client for Appium Mobile Webdriver - http://appium.io - - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - https://github.com/appium/java-client - scm:git:ssh://git@github.com/appium/java-client.git - scm:git:ssh://git@github.com/appium/java-client.git - - HEAD - - - - - jonahss@gmail.com - Jonah Stiennon - https://github.com/jonahss - jonahss - - - tichomirovsergey@gmail.com - Sergey Tikhomirov - https://github.com/TikhomirovSergey - TikhomirovSergey - - - srinivasan.sekar1990@gmail.com - Srinivasan Sekar - https://github.com/SrinivasanTarget - SrinivasanTarget - - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - - src/main/resources - - - - - org.apache.maven.plugins - maven-eclipse-plugin - 2.10 - - true - true - ${basedir} - - https://google-styleguide.googlecode.com/svn/trunk/eclipse-java-google-style.xml - - - - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - - org.apache.maven.scm - maven-scm-provider-jgit - 1.9.5 - - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.4 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.5.1 - - 1.7 - 1.7 - eclipse - - - - org.codehaus.plexus - plexus-compiler-eclipse - 2.8 - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.19.1 - - - org.apache.maven.surefire - surefire-junit47 - 2.19.1 - - - - - - test - - integration-test - - - **/*Test.java - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 2.17 - - - com.puppycrawl.tools - checkstyle - LATEST - - - - ${basedir}/google-style.xml - UTF-8 - false - true - true - true - - - - validate - validate - - check - - - - - - org.owasp - dependency-check-maven - 1.4.0 - - 22 - - - - - check - - - - - - org.jacoco - jacoco-maven-plugin - 0.7.7.201606060606 - - ${basedir}/target/coverage-reports/jacoco-unit.exec - ${basedir}/target/coverage-reports/jacoco-unit.exec - - - - jacoco-initialize - - prepare-agent - - - - jacoco-site - package - - report - - - - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 2.17 - - ${basedir}/google-style.xml - UTF-8 - false - true - false - true - - - - - checkstyle - - - - - - org.apache.maven.plugins - maven-jxr-plugin - 2.3 - - - org.owasp - dependency-check-maven - 1.4.0 - - - - diff --git a/build.gradle b/build.gradle index 26098d0a1..2990ec51c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,211 +1,322 @@ -apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'eclipse' -apply plugin: 'jacoco' -apply plugin: 'checkstyle' -apply plugin: 'signing' -apply plugin: 'maven-publish' - -group 'io.appium' -version '5.0.0-BETA9' +import org.apache.tools.ant.filters.* -repositories { - jcenter() - maven { - url "http://repo.maven.apache.org/maven2" - } +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() - maven { - url "http://repo.maven.apache.org/maven2" - } - } - dependencies { - classpath "org.owasp:dependency-check-gradle:1.4.0" - classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.4' - } +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 -} +repositories { + mavenCentral() -dependencies { - ecj 'org.eclipse.jdt.core.compiler:ecj:4.5.1' + if (project.hasProperty("isCI")) { + maven { + url uri('https://central.sonatype.com/api/v1/publisher') + mavenContent { + snapshotsOnly() + } + } + } } -compileJava { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - - def ecjJar = configurations.ecj.singleFile - - options.fork = true - options.fork executable: 'java', jvmArgs: [ '-cp', ecjJar.path, 'org.eclipse.jdt.internal.compiler.batch.Main' ] - options.define compilerArgs: [ - '-encoding', 'utf-8' - ] +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + withJavadocJar() + withSourcesJar() } dependencies { - compile('org.seleniumhq.selenium:selenium-java:3.4.0'){ - exclude module: 'cglib' - exclude group: 'com.google.code.gson' - } - compile 'com.google.code.gson:gson:2.8.1' - compile 'org.apache.httpcomponents:httpclient:4.5.3' - compile 'cglib:cglib:3.2.5' - compile 'commons-validator:commons-validator:1.6' - compile 'org.apache.commons:commons-lang3:3.5' - compile 'commons-io:commons-io:2.5' - compile 'org.springframework:spring-context:4.3.8.RELEASE' - compile 'org.aspectj:aspectjweaver:1.8.10' - compile 'org.openpnp:opencv:3.2.0-1' - - testCompile 'junit:junit:4.12' - testCompile 'org.hamcrest:hamcrest-all:1.3' -} - -ext { - Sources = fileTree("$buildDir/src/main/java").include('**/*.java') - Tests = fileTree("$buildDir/src/test/java").include('**/*.java') - Docs = file("$buildDir/doc") -} + compileOnly 'org.projectlombok:lombok:1.18.42' + annotationProcessor 'org.projectlombok:lombok:1.18.42' -sourceSets { - main { - java { - srcDir('src/main/java') + 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}" + } } - resources { - srcDir('src/main/resources') + api('org.seleniumhq.selenium:selenium-remote-driver') { + version { + strictly "[${seleniumVersion}, 5.0)" + prefer "${seleniumVersion}" + } } - } - test { - java { - srcDir('src/test/java') + api('org.seleniumhq.selenium:selenium-support') { + version { + strictly "[${seleniumVersion}, 5.0)" + prefer "${seleniumVersion}" + } } } + 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.7.7.201606060606" + toolVersion = '0.8.13' } -tasks.withType(JacocoReport) { - description = "Generate Jacoco coverage reports after running tests" +tasks.withType(JacocoReport).configureEach { + description = 'Generate Jacoco coverage reports after running tests' sourceSets sourceSets.main reports { - html.enabled true - html.destination "${buildDir}/Reports/jacoco" + html.required = true + html.outputLocation = file("${buildDir}/Reports/jacoco") } } jacocoTestReport.dependsOn test +apply plugin: 'checkstyle' + checkstyle { - toolVersion = "7.0" + toolVersion = '10.23.1' + configFile = configDirectory.file('appium-style.xml').get().getAsFile() showViolations = true + ignoreFailures = false } -dependencies { - checkstyle( 'com.puppycrawl.tools:checkstyle:7.0' ) +javadoc { + options.addStringOption('encoding', 'UTF-8') } -tasks.withType(Checkstyle) { - ignoreFailures = false - configFile = file("$projectDir/google-style.xml") - exclude '**/org/openqa/selenium/**' +publishing { + publications { + mavenJava(MavenPublication) { + groupId = 'io.appium' + artifactId = 'java-client' + version = appiumClientVersion + from components.java + pom { + name = 'java-client' + description = 'Java client for Appium Mobile Webdriver' + url = 'http://appium.io' + developers { + developer { + name = 'Jonah Stiennon' + email = 'jonahss@gmail.com' + url = 'https://github.com/jonahss' + id = 'jonahss' + } + developer { + name = 'Sergey Tikhomirov' + email = 'tichomirovsergey@gmail.com' + url = 'https://github.com/TikhomirovSergey' + id = 'TikhomirovSergey' + } + developer { + name = 'Srinivasan Sekar' + email = 'srinivasan.sekar1990@gmail.com' + url = 'https://github.com/SrinivasanTarget' + id = 'SrinivasanTarget' + } + developer { + name = 'Mykola Mokhnach' + url = 'https://github.com/mykola-mokhnach' + id = 'mykola-mokhnach' + } + developer { + name = 'Valery Yatsynovich' + url = 'https://github.com/valfirst' + id = 'valfirst' + } + } + licenses { + license { + name = 'Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + scm { + url = 'https://github.com/appium/java-client' + connection = 'scm:git:ssh://git@github.com/appium/java-client.git' + developerConnection = 'scm:git:ssh://git@github.com/appium/java-client.git' + tag = 'HEAD' + } + } + } + } + repositories { + maven { + url = layout.buildDirectory.dir('staging-deploy') + } + } } -task javadocJar(type: Jar) { - classifier = 'javadoc' - from javadoc +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } } -task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allSource +wrapper { + gradleVersion = '9.1.0' + distributionType = Wrapper.DistributionType.ALL } -artifacts { - archives javadocJar, sourcesJar +processResources { + filter ReplaceTokens, tokens: [ + 'selenium.version' : seleniumVersion, + 'appiumClient.version': appiumClientVersion + ] } -signing { - sign configurations.archives -} +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' + } + } + } + } -uploadArchives { - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + 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 + } + } + } - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: ossrhUsername, password: ossrhPassword) + e2eIosTest(JvmTestSuite) { + sources { + java { + srcDirs = ['src/e2eIosTest/java'] + } + } + dependencies { + implementation project() + implementation(sourceSets.test.output) + implementation('org.apache.commons:commons-lang3:3.20.0') } - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: ossrhUsername, password: ossrhPassword) + targets.configureEach { + testTask.configure { + shouldRunAfter(test) + filter { + exclude '**/IOSScreenRecordTest.class' + exclude '**/ImagesComparisonTest.class' + exclude '**/IOSNativeWebTapSettingTest.class' + } + } } + } - pom.project { - packaging 'jar' - name 'java-client' - description 'Java client for Appium Mobile Webdriver' - url 'http://appium.io' - developers { - developer { - name 'Jonah Stiennon' - email 'jonahss@gmail.com' - url 'https://github.com/jonahss' - id 'jonahss' - }; - developer { - name 'Sergey Tikhomirov' - email 'tichomirovsergey@gmail.com' - url 'https://github.com/TikhomirovSergey' - id 'TikhomirovSergey' - }; - developer { - name 'Srinivasan Sekar' - email 'srinivasan.sekar1990@gmail.com' - url 'https://github.com/SrinivasanTarget' - id 'SrinivasanTarget' - }; + e2eAndroidTest(JvmTestSuite) { + sources { + java { + srcDirs = ['src/e2eAndroidTest/java'] } - licenses { - license { - name 'Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } + } + dependencies { + implementation project() + implementation(sourceSets.test.output) + implementation('io.github.bonigarcia:webdrivermanager:6.3.3') { + exclude group: 'org.seleniumhq.selenium' } - scm { - url 'https://github.com/appium/java-client' - connection 'scm:git:ssh://git@github.com/appium/java-client.git' - developerConnection 'scm:git:ssh://git@github.com/appium/java-client.git' - tag 'HEAD' + } + + 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' + } } } } - } -} -task wrapper(type: Wrapper) { - gradleVersion = '2.14.1' - description 'Generates the Gradle wrapper scripts.' -} + e2eFlutterTest(JvmTestSuite) { + sources { + java { + srcDirs = ['src/e2eFlutterTest/java'] + } + } + dependencies { + implementation project() + implementation(sourceSets.test.output) + } -test { - useJUnit() + 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 81% rename from google-style.xml rename to config/checkstyle/appium-style.xml index 06e2c452c..b7473e937 100755 --- a/google-style.xml +++ b/config/checkstyle/appium-style.xml @@ -1,21 +1,11 @@ - - + "-//Puppy Crawl//DTD Check Configuration 1.3//EN" + "http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd"> - @@ -41,13 +31,16 @@ - - - - - - - + + + + + + + + + + @@ -55,10 +48,7 @@ - - - - + @@ -73,6 +63,7 @@ + @@ -97,7 +88,7 @@ value="Package name ''{0}'' must match pattern ''{1}''."/> - + @@ -138,6 +129,7 @@ + @@ -162,12 +154,6 @@ - - - - - - @@ -181,29 +167,25 @@ - - - + - - - + + - - + - - - + + + @@ -215,8 +197,26 @@ - + + + + + + + - \ No newline at end of file + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..96609f2a7 --- /dev/null +++ b/docs/Advanced-By.md @@ -0,0 +1,160 @@ +# Standard Selectors + +## AndroidFindBy / iOSXCUITFindBy / WindowsFindBy + +# Advanced Selectors + +## iOS's String Predicates + +You can specify a [predicate](https://developer.apple.com/documentation/foundation/nspredicate) +in your Class Chain to limit the number of matched items. There's +[a detailed guide to that](https://appium.io/docs/en/writing-running-appium/ios/ios-predicate/index.html) +on the Appium Docs website with some Appium-specific considerations. + +## iOS's Class Chain Queries + +Our XCUiTest integration has full support for the 'Class Chain' concept. This +can do much of what XPath does...but faster. Note that many Class Chains leverage +String Predicates too. + +### String Predicates in Class Chains + +There's a special array-style syntax for defining predicates in a Class Chain: + +``` +// Start with [ +// Followed by ` +// Followed by some text +// Followed by ` +// End with ] +[`label != 'a'`] +[`isWDVisible == 1`] +[`value < 0`] +``` + +#### Searching Descendents + +If you replace the backticks (`` ` ``) around a predicate with dollar signs (`$`), +then the Class Chain will match against the children, grandchildren, and other +descendants of each element. + +``` +// Find a cell element with the label 'here' +XCUIElementTypeCell[`label == 'here'`] +// Find a cell element which contains SOMETHING ELSE that has the label 'here' +XCUIElementTypeCell[$label == 'here'$] +``` + +#### Handling Quote Marks + +Most of the time, you can treat pairs of single quotes or double quotes +interchangeably. If you're searching with a string that contains quote marks, +though, you [need to be careful](https://stackoverflow.com/q/14116217). + +```c +// Make sure to escape each quote mark that matches your delimiter +"text with \"some\" 'quote' marks" +// To NSPredicate, the line above and the line below are equivalent +'text with "some" \'quote\' marks' +``` +```java +// When defining a iOSXCUITFindBy annotation, you'll be constrained by the +// Java string-quoting rules too. +@iOSXCUITFindBy(iOSClassChain = "**/SomeElement[`'text with \"some\" \\\'quote\\\' marks'`]") +``` + +### External References + +Refer to [the official WebDriverAgent query docs](https://github.com/facebookarchive/WebDriverAgent/wiki/Class-Chain-Queries-Construction-Rules) +to learn more about the general concept. + +### Sample usage: + +```java +// Selector for image elements +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeImage") + +// Selector for the first image element on screen +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeImage[1]") +// Selector for the second image element on screen +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeImage[2]") +// Selector for the last image element on screen +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeImage[-1]") +// Selector for the penultimate image element on screen +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeImage[-2]") + +// Selector for every cell with the name 'Foo' (single quote style) +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[`name == 'Foo'`]") +// Selector for every cell with the name "Foo" (double quote style) +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[`name == \"Foo\"`]") + +// Selector for every cell with a name that starts with 'Foo' (single quote style) +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[`name BEGINSWITH 'Foo'`]") +// Selector for every cell with a name that starts with "Foo" (double quote style) +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[`name BEGINSWITH \"Foo\"`]") + +// Selector for every cell with a name that starts with "it's not" +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[`name BEGINSWITH \"it's not\"`]") + +// Selector that'll match every top-level element on screen +@iOSXCUITFindBy(iOSClassChain = "*") +// Selector that'll match every leaf element on screen (watch out: this can be SLOW) +@iOSXCUITFindBy(iOSClassChain = "**/*") + +// You can place an index after a predicate: the following finds the last image element with name 'Foo' +@iOSXCUITFindBy(iOSClassChain = "**/XCUIElementTypeCell[`name == 'Foo'`][-1]") +``` + +## Android's uiAutomator String + +Available when using [AndroidBy](AndroidBy) and [AndroidFindBy](AndroidFindBy) with +[appium-uiautomator2-server](https://github.com/appium/appium-uiautomator2-server). This +string will be used by the server to construct a UiSelector or UiScrollable object. + +### External References + +For an overview of what the backend is capable of, please check out the + +* [Main UI Automator Guide](https://developer.android.com/training/testing/ui-automator) +* [UiScrollable API docs](https://developer.android.com/reference/androidx/test/uiautomator/UiScrollable) +and +* [UiSelector API docs](https://developer.android.com/reference/androidx/test/uiautomator/UiSelector) + +### Sample Strings + +Here are some ways you could configure a UiSelector in your project: + +```java +// Create a selector that looks for the text "Hello World": +@AndroidFindBy(uiAutomator = "new UiSelector().text(\"Hello World\")") + +// Create a selector that tries to find an ImageView: +@AndroidFindBy(uiAutomator = "new UiSelector().className(\"android.widget.ImageView\")") + +// Create a selector that matches resource ids against a regular expression: +private static final String looksLikeAPage = "page_number_\d*"; +@AndroidFindBy(uiAutomator = "new UiSelector().resourceIdMatches(\"" + looksLikeAPage + "\")") + +// The agent also supports some abbreviated forms - all 3 of the below +// strings are equivalent. +@AndroidFindBy(uiAutomator = "new UiSelector().className(\"android.widget.EditText\")") +@AndroidFindBy(uiAutomator = "UiSelector().className(\"android.widget.EditText\")") +@AndroidFindBy(uiAutomator = ".className(\"android.widget.EditText\")") + +// You can connect up conditions to search for multiple things at once +@AndroidFindBy(uiAutomator = ".resourceId(\"android:id/list\").classNameMatches(\"\.*RecyclerView\").index(3)") +``` + +..and here are some that create UiScrollable objects: + +```java +private static final String ourImageSelector = ".className(\"android.widget.ImageView\")"; +private static final String ourListSelector = ".className(\"android.widget.ListView\")"; + +// Create a scrollable associated with a list (by itself, this doesn't do anything useful...) +@AndroidFindBy(uiAutomator = "new UiScrollable(" + ourListSelector + ")") + +// Create a scrollable that scrolls forward along a list until it finds an ImageView: +@AndroidFindBy(uiAutomator = "new UiScrollable(" + ourListSelector + ").scrollIntoView(" + ourImageSelector + ")") + +``` 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-propose-a-PR.md b/docs/How-to-propose-a-PR.md index 22a1a6995..83dfefa64 100644 --- a/docs/How-to-propose-a-PR.md +++ b/docs/How-to-propose-a-PR.md @@ -21,4 +21,4 @@ with pasted java code samples or put them at a PR description using markdown. Ab # Pull-request template -There is [PULL_REQUEST_TEMPLATE.md)](https://github.com/appium/java-client/blob/master/PULL_REQUEST_TEMPLATE.md) which should help you to make a good pull request. +There is [PULL_REQUEST_TEMPLATE.md](https://github.com/appium/java-client/blob/master/PULL_REQUEST_TEMPLATE.md) which should help you to make a good pull request. diff --git a/docs/How-to-report-an-issue.md b/docs/How-to-report-an-issue.md index 0adf13fbf..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: @@ -28,11 +28,11 @@ If it is necessary there should provided more details ### Code To Reproduce Issue (good to Have if you report a bug) It's easier to reproduce bug and much faster to fix it. -You can git clone https://github.com/appium/sample-code or https://github.com/appium/sample-code/tree/master/sample-code/apps and reproduce an issue using Java and sample apps. +You can git clone https://github.com/appium/appium/tree/master/sample-code or https://github.com/appium/appium/tree/master/sample-code/apps and reproduce an issue using Java and sample apps. 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 14f20686d..000000000 --- a/docs/Installing-the-project.md +++ /dev/null @@ -1,83 +0,0 @@ -# Requirements - -Firstly you should install appium server. [Appium getting started](http://appium.io/getting-started.html). The version 1.6.3 or greater 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 97f9dbe2f..d6182196d 100644 --- a/docs/Note-for-developers.md +++ b/docs/Note-for-developers.md @@ -12,32 +12,11 @@ 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 -Appium java-client strictly follows [Google Java Style](http://google-styleguide.googlecode.com/svn/trunk/javaguide.html) as a coding standards. Contributors are requested to follow this by configuring in their IDE's Editor Code style, +Appium java-client strictly follows [Google Java Style](https://google.github.io/styleguide/javaguide.html) as a coding standards. Contributors are requested to follow this by configuring in their IDE's Editor Code style, * Clone [Google Style Guide](https://github.com/google/styleguide.git) from git diff --git a/docs/Page-objects.md b/docs/Page-objects.md index 7c41cc612..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,32 +46,31 @@ 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 examle +# The fully cross platform example ```java import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; import org.openqa.selenium.support.FindBy; -//the fully cross platform examle +//the fully cross platform example @FindBy(someStrategy) //for browser or web view html UI @AndroidFindBy(someStrategy) //for Android native UI @iOSFindBy(someStrategy) //for iOS native UI RemoteWebElement someElement; -//the fully cross platform examle +//the fully cross platform example @FindBy(someStrategy) @AndroidFindBy(someStrategy) //for Android native UI @iOSFindBy(someStrategy) //for iOS native UI @@ -305,18 +304,19 @@ If time of the waiting for elements differs from usual (longer, or shorter when ```java import io.appium.java_client.pagefactory.*; +import java.time.temporal.ChronoUnit; -@WithTimeout(timeOut = yourTime, timeUnit = yourTimeUnit) +@WithTimeout(timeOut = yourTime, chronoUnit = yourTimeUnit) RemoteWebElement someElement; -@WithTimeout(timeOut = yourTime, timeUnit = yourTimeUnit) +@WithTimeout(timeOut = yourTime, chronoUnit = yourTimeUnit) List someElements; ``` # The additional feature. ## The simple example -Let's imagine that the task is to check an Android client of the [http://www.rottentomatoes.com](http://www.rottentomatoes.com/). Let it be like a picture below +Let's imagine that the task is to check an Android client of the [https://www.rottentomatoes.com](https://www.rottentomatoes.com/). Let it be like a picture below ![](https://cloud.githubusercontent.com/assets/4927589/11120641/51c1fda8-8962-11e5-8b17-323b5f236fce.png) Lets imagine that it is only a part of the screen. @@ -324,13 +324,13 @@ A typical page object could look like: ```java public class RottenTomatoesScreen { - //convinient locator + //convenient locator private List titles; - //convinient locator + //convenient locator private List scores; - //convinient locator + //convenient locator private List castings; //element declaration goes on @@ -371,13 +371,13 @@ public class Movie extends Widget{ super(element); } - //convinient locator + //convenient locator private AndroidElement title; - //convinient locator + //convenient locator private AndroidElement score; - //convinient locator + //convenient locator private AndroidElement casting; public String getTitle(params){ @@ -404,7 +404,7 @@ So, now page object looks ```java public class RottenTomatoesScreen { - @AndroidFindBy(a locator which convinient to find a single movie-root - element) + @AndroidFindBy(a locator which convenient to find a single movie-root - element) private List movies; //element declaration goes on @@ -427,7 +427,7 @@ public class RottenTomatoesScreen { Then ```java //the class is annotated !!! -@AndroidFindBy(a locator which convinient to find a single movie-root - element) +@AndroidFindBy(a locator which convenient to find a single movie-root - element) public class Movie extends Widget{ ... } @@ -658,7 +658,7 @@ This use case has some restrictions; - All classes which are declared by the OverrideWidget annotation should be subclasses of the class declared by field -- All classes which are declared by the OverrideWidget should not be abstract. If a declared class is overriden partially like +- All classes which are declared by the OverrideWidget should not be abstract. If a declared class is overridden partially like ```java //above is the other field declaration @@ -720,4 +720,4 @@ It is strongly recommended to implement each subclass of __io.appium.java_clien public /*or any other available modifier*/ WidgetSubclass(WebElement element) { super(element); } -``` \ No newline at end of file +``` diff --git a/docs/Tech-stack.md b/docs/Tech-stack.md deleted file mode 100644 index e3b66655e..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://projects.spring.io/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 e703e5cd7..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](http://seleniumworks.blogspot.ru/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://projects.spring.io/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 d65e4c20b..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](http://appium.io/slate/en/master/?java#setup-(android)) - -- 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](http://appium.io/slate/en/master/?java#appium-server-arguments) - - -# The starting of an app - -It looks like creation of a common [RemoteWebDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/remote/RemoteWebDriver.html) instance. - -[Common capabilities](http://appium.io/slate/en/master/?java#the---default-capabilities-flag) - -[Android-specific capabilities](http://appium.io/slate/en/master/?java#android-only) - -[Common capabilities provided by Java client](http://appium.github.io/java-client/io/appium/java_client/remote/MobileCapabilityType.html) - -[Android-specific capabilities provided by Java client](http://appium.github.io/java-client/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 6e75bc93b..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 @@ -1,35 +1,11 @@ # Requirements -- Installed Node.js 4 or greater. +- Installed Node.js 7 or greater. - At least an appium server instance installed via __npm__. # The basic principle. -It works the similar way as common [ChromeDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/chrome/ChromeDriver.html), [InternetExplorerDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/ie/InternetExplorerDriver.html) of Selenium project or [PhantomJSDriver](http://cdn.ivandemarino.me/phantomjsdriver-javadoc/org/openqa/selenium/phantomjs/PhantomJSDriver.html). They use subclasses of the [DriverService](https://selenium.googlecode.com/git/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); -``` +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). # 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(); ``` @@ -148,7 +125,7 @@ import java.io.File; ... //appiumJS is the full or relative path to -//the appium.js (v<=1.4.16) or maim.js (v>=1.5.0) +//the appium.js (v<=1.4.16) or main.js (v>=1.5.0) new AppiumServiceBuilder().withAppiumJS(new File(appiumJS)); ``` 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 d126f8333..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](http://appium.io/slate/en/master/?ruby#system-setup-(ios)) - -- 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](http://appium.io/slate/en/master/?java#appium-server-arguments) - -# The starting of an app - -It looks like creation of a common [RemoteWebDriver](https://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/remote/RemoteWebDriver.html) instance. - -[Common capabilities](http://appium.io/slate/en/master/?ruby#the---default-capabilities-flag) - -[iOS-specific capabilities](http://appium.io/slate/en/master/?ruby#ios-only) - -[Common capabilities provided by Java client](http://appium.github.io/java-client/io/appium/java_client/remote/MobileCapabilityType.html) - -[iOS-specific capabilities provided by Java client](http://appium.github.io/java-client/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 new file mode 100644 index 000000000..a08a6ea6e --- /dev/null +++ b/docs/environment.md @@ -0,0 +1,35 @@ +# Appium Environment Troubleshooting + +Quite often there are questions about why Appium throws an error about missing environment variable, for example `ANDROID_HOME`, or about missing binaries, like `idevice_id`, `carthage` or `java`. +This article explains what might be a cause of such problem and how to resolve it. + +## Prerequisites + +In order to understand this topic you should know concept of environment variables, how it works in your operating system and how you can change the system environment if needed. +In particular, it is important to know the meaning of `PATH` environment variable. Read https://en.wikipedia.org/wiki/Environment_variable and https://en.wikipedia.org/wiki/PATH_(variable) for more details. + +## How To Verify What Is Missing + +Appium itself is a NodeJS application and uses the same environment as its host `node` process. If you experience an error related to local environment setup then verify the actual process environment first. +In Mac OS, for example, it is possible to do this via `ps eww ` command, where `PID` is the process identifier of the running Appium's host Node process. +In Windows the [ProcessExplorer](https://docs.microsoft.com/sysinternals/downloads/process-explorer) utility can be used for such purpose. +Then make sure the corresponding variable is there and it is set to a proper value, or, in case there is an error finding some binary, make sure the parent folder of this binary is present in `PATH` list, the binary itself on the local file system and can be executed manually. + +## How To Fix Missing Environment Variables + +Usually, if one starts Appium from the command line, then it inherits all the environment variables from the parent process (`bash`/`cmd`, etc.). +This means that if you start Appium manually or with a script then make sure its parent process has all the necessary environment variables set to proper values. +Use `env` command to check the currently defined environment variables of your *nix shell interpreter or `set` for cmd.exe shell in Windows. + +On *nix system you could add/edit environment variables of your shell either by using the `export` command (only works in scope of the current shell session) or by editing the appropriate shell config if it is necessary to keep the changes. +The path to this config depends on the currently selected interpreter. Use `echo $SHELL` to figure out what your current interpreter is. +Bash, for example, loads the config from `$HOME/.bashrc` or `$HOME/.bash_profile` and ZSH from `$HOME/.zshrc`. +Remember to reload the config after it has been changed either by sourcing it (e.g. `source ~/.zshrc`) or by restarting the terminal session. + +On Windows, you could use the `set` command to add/change environment variables in scope of the current shell session or edit them in [system settings](https://www.java.com/en/download/help/path.xml) to keep the changes. +Remember to reload the config after it has been changed by restarting the command prompt session. + +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 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 34d114d1c..19ebf202f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,5 @@ org.gradle.daemon=true -signing.keyId=YourKeyId -signing.password=YourPublicKeyPassword -signing.secretKeyRingFile=PathToYourKeyRingFile - -ossrhUsername=your-jira-id -ossrhPassword=your-jira-password +selenium.version=4.36.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 d3b83982b..8bdaf60c7 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 13c1c1002..a35649f5f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Sat Jul 23 20:09:50 IST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/gradlew b/gradlew index 27309d923..ef07e0162 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# 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. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# 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="" +# 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 ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +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 +CLASSPATH="\\\"\\\"" + # 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 @@ -81,84 +132,120 @@ 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" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; 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 # 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=$((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 -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -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 f6d5974e7..5eed7ee84 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ 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= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe 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 @@ -35,54 +57,36 @@ 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 -if "%@eval[2+2]" == "4" goto 4NT_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=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= + @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%" -classpath "%CLASSPATH%" -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 new file mode 100644 index 000000000..e5b145a9d --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,4 @@ +jdk: + - openjdk11 +install: + - ./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/e2eAndroidTest/java/io/appium/java_client/android/AndroidConnectionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidConnectionTest.java new file mode 100644 index 000000000..4c8f03cd4 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidConnectionTest.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.android; + +import io.appium.java_client.android.connection.ConnectionState; +import io.appium.java_client.android.connection.ConnectionStateBuilder; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.MethodName.class) +public class AndroidConnectionTest extends BaseAndroidTest { + + @Test + public void test1() { + ConnectionState state = driver.setConnection(new ConnectionStateBuilder() + .withWiFiEnabled() + .build()); + assertTrue(state.isWiFiEnabled()); + } + + @Test + public void test2() { + ConnectionState state = driver.setConnection(new ConnectionStateBuilder() + .withAirplaneModeDisabled() + .build()); + assertFalse(state.isAirplaneModeEnabled()); + assertFalse(state.isWiFiEnabled()); + assertFalse(state.isDataEnabled()); + state = driver.setConnection(new ConnectionStateBuilder(state) + .withAirplaneModeEnabled() + .build()); + assertTrue(state.isAirplaneModeEnabled()); + } + + @Test + public void test3() { + ConnectionState state = driver.setConnection( + new ConnectionStateBuilder(driver.getConnection()) + .withAirplaneModeDisabled() + .withWiFiEnabled() + .withDataEnabled() + .build()); + assertFalse(state.isAirplaneModeEnabled()); + assertTrue(state.isWiFiEnabled()); + assertTrue(state.isDataEnabled()); + } +} 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 1ee5280b7..1a9a5657d 100644 --- a/src/test/java/io/appium/java_client/android/AndroidContextTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidContextTest.java @@ -16,23 +16,23 @@ package io.appium.java_client.android; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - 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() { @@ -43,12 +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"); + driver.context(NATIVE_CONTEXT); + assertEquals(driver.getContext(), NATIVE_CONTEXT); } - @Test(expected = NoSuchContextException.class) public void testContextError() { - driver.context("Planet of the Ape-ium"); - assertTrue(driver.getContext().equals("Planet of the Ape-ium")); + @Test public void testContextError() { + assertThrows(NoSuchContextException.class, () -> driver.context("Planet of the Ape-ium")); } } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDataMatcherTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDataMatcherTest.java new file mode 100644 index 000000000..83d8eabdf --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDataMatcherTest.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; + +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 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, Duration.ofSeconds(10)); + wait.until(ExpectedConditions + .elementToBeClickable(AppiumBy.accessibilityId("Graphics"))); + driver.findElement(AppiumBy.accessibilityId("Graphics")).click(); + + String selector = new Json().toJson(Map.of( + "name", "hasEntry", + "args", List.of("title", "Sweep") + )); + + assertNotNull(wait.until(ExpectedConditions + .presenceOfElementLocated(AppiumBy.androidDataMatcher(selector)))); + } +} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDriverTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDriverTest.java new file mode 100644 index 000000000..76753d75d --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidDriverTest.java @@ -0,0 +1,291 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Location; +import io.appium.java_client.appmanagement.ApplicationState; +import org.apache.commons.io.FileUtils; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.ScreenOrientation; + +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 { + + @Test + public void sendSMSTest() { + try { + driver.sendSMS("11111111", "call"); + } catch (Exception e) { + fail("method works only in emulators"); + } + } + + + @Test + public void getStatusTest() { + assertThat(driver.getStatus().get("build").toString(), Matchers.containsString(".")); + } + + @Test + public void gsmCallTest() { + try { + driver.makeGsmCall("11111111", GsmCallActions.CALL); + driver.makeGsmCall("11111111", GsmCallActions.ACCEPT); + } catch (Exception e) { + fail("method works only in emulators"); + } + } + + @Test + public void toggleWiFi() { + try { + driver.toggleWifi(); + } catch (Exception e) { + fail("Not able to toggle wifi"); + } + } + + @Test + public void toggleAirplane() { + try { + driver.toggleAirplaneMode(); + } catch (Exception e) { + fail("Not able to toggle airplane mode"); + } + } + + @Test + public void toggleData() { + try { + driver.toggleData(); + } catch (Exception e) { + fail("Not able to toggle data"); + } + } + + @Test + public void gsmSignalStrengthTest() { + try { + driver.setGsmSignalStrength(GsmSignalStrength.GREAT); + } catch (Exception e) { + fail("method works only in emulators"); + } + } + + @Test + public void gsmVoiceTest() { + try { + driver.setGsmVoice(GsmVoiceState.OFF); + } catch (Exception e) { + fail("method works only in emulators"); + } + } + + @Test + public void networkSpeedTest() { + try { + driver.setNetworkSpeed(NetworkSpeed.EDGE); + } catch (Exception e) { + fail("method works only in emulators"); + } + } + + @Test + public void powerTest() { + try { + driver.setPowerCapacity(100); + driver.setPowerAC(PowerACState.OFF); + } catch (Exception e) { + fail("method works only in emulators"); + } + } + + @Test + public void getDeviceTimeTest() { + String time = driver.getDeviceTime(); + assertFalse(time.isEmpty()); + } + + @Test + public void isAppInstalledTest() { + assertTrue(driver.isAppInstalled(APP_ID)); + } + + @Test + public void isAppNotInstalledTest() { + assertFalse(driver.isAppInstalled("foo")); + } + + @Test + public void closeAppTest() { + 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.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); + byte[] returnData = driver.pullFile("/data/local/tmp/remote.txt"); + String returnDataDecoded = new String(returnData); + assertEquals( + "The eventual code is no more than the deposit of your understanding. ~E. W. Dijkstra", + returnDataDecoded); + } + + @Test + public void pushTempFileTest() throws Exception { + File temp = File.createTempFile("Temp_", "_test"); + try { + FileUtils.writeStringToFile(temp, "The eventual code is no " + + "more than the deposit of your understanding. ~E. W. Dijkstra", "UTF-8", true); + driver.pushFile("/data/local/tmp/remote2.txt", temp); + byte[] returnData = driver.pullFile("/data/local/tmp/remote2.txt"); + String returnDataDecoded = new String(returnData); + assertEquals( + "The eventual code is no more than the deposit of " + + "your understanding. ~E. W. Dijkstra", + returnDataDecoded); + } finally { + FileUtils.forceDelete(temp); + } + } + + @Test + public void toggleLocationServicesTest() { + driver.toggleLocationServices(); + } + + @Test + public void geolocationTest() { + Location location = new Location(45, 45, 100.0); + driver.setLocation(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 runAppInBackgroundTest() { + long time = System.currentTimeMillis(); + driver.runAppInBackground(Duration.ofSeconds(4)); + long timeAfter = System.currentTimeMillis(); + assert timeAfter - time > 3000; + } + + @Test + public void testApplicationsManagement() throws InterruptedException { + String appId = driver.getCurrentPackage(); + assertThat(driver.queryAppState(appId), equalTo(ApplicationState.RUNNING_IN_FOREGROUND)); + Thread.sleep(500); + driver.runAppInBackground(Duration.ofSeconds(-1)); + assertThat(driver.queryAppState(appId), lessThan(ApplicationState.RUNNING_IN_FOREGROUND)); + Thread.sleep(500); + driver.activateApp(appId); + assertThat(driver.queryAppState(appId), equalTo(ApplicationState.RUNNING_IN_FOREGROUND)); + } + + @Test + public void pullFileTest() { + byte[] data = driver.pullFile("/data/system/users/userlist.xml"); + assert data.length > 0; + } + + @Test + public void deviceDetailsAndKeyboardTest() { + assertFalse(driver.isKeyboardShown()); + assertNotNull(driver.getDisplayDensity()); + assertNotEquals(0, driver.getSystemBars().size()); + } + + @Test + public void getSupportedPerformanceDataTypesTest() { + startActivity(".ApiDemos"); + + List dataTypes = new ArrayList<>(); + dataTypes.add("cpuinfo"); + dataTypes.add("memoryinfo"); + dataTypes.add("batteryinfo"); + dataTypes.add("networkinfo"); + + List supportedPerformanceDataTypes = driver.getSupportedPerformanceDataTypes(); + + assertEquals(4, supportedPerformanceDataTypes.size()); + + for (int i = 0; i < supportedPerformanceDataTypes.size(); ++i) { + assertEquals(dataTypes.get(i), supportedPerformanceDataTypes.get(i)); + } + } + + @Test + public void getPerformanceDataTest() { + startActivity(".ApiDemos"); + + List supportedPerformanceDataTypes = driver.getSupportedPerformanceDataTypes(); + + for (String dataType : supportedPerformanceDataTypes) { + + List> valueTable = driver.getPerformanceData(APP_ID, dataType, 60000); + + for (int j = 1; j < valueTable.size(); ++j) { + assertEquals(valueTable.subList(0, 0).size(), valueTable.subList(j, j).size()); + } + } + + } + + @Test + public void getCurrentPackageTest() { + assertEquals(APP_ID, driver.getCurrentPackage()); + } +} 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 75% 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 ab3b5e84f..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,15 +1,10 @@ package io.appium.java_client.android; -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; @@ -20,10 +15,17 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; -import java.util.concurrent.TimeUnit; 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 -> { @@ -64,23 +66,26 @@ public class AndroidFunctionTest extends BaseAndroidTest { return null; }; - @BeforeClass public static void startWebViewActivity() throws Exception { + @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 public void setUp() { - driver.context("NATIVE_APP"); + @BeforeEach + public void setUp() { + + driver.context(NATIVE_CONTEXT); } - @Test public void complexWaitingTestWithPreCondition() { + @Test + public void complexWaitingTestWithPreCondition() { AppiumFunction> compositeFunction = searchingFunction.compose(contextFunction); Wait wait = new FluentWait<>(Pattern.compile("WEBVIEW")) - .withTimeout(30, TimeUnit.SECONDS); + .withTimeout(ofSeconds(30)); List elements = wait.until(compositeFunction); assertThat("Element size should be 1", elements.size(), is(1)); @@ -115,22 +120,25 @@ public class AndroidFunctionTest extends BaseAndroidTest { }); Wait wait = new FluentWait<>(Pattern.compile("WEBVIEW")) - .withTimeout(30, TimeUnit.SECONDS); + .withTimeout(ofSeconds(30)); List elements = wait.until(compositeFunction); assertThat("Element size should be 1", elements.size(), is(1)); assertThat("WebView is expected", driver.getContext(), containsString("WEBVIEW")); assertThat("There should be 3 calls", calls.size(), is(3)); } - @Test(expected = TimeoutException.class) public void nullPointerExceptionSafetyTestWithPrecondition() { + @Test + public void nullPointerExceptionSafetyTestWithPrecondition() { Wait wait = new FluentWait<>(Pattern.compile("Fake_context")) - .withTimeout(30, TimeUnit.SECONDS).pollingEvery(500, TimeUnit.MILLISECONDS); - assertTrue(wait.until(searchingFunction.compose(contextFunction)).size() > 0); + .withTimeout(ofSeconds(30)).pollingEvery(ofMillis(500)); + assertThrows(TimeoutException.class, () -> wait.until(searchingFunction.compose(contextFunction))); } - @Test(expected = TimeoutException.class) public void nullPointerExceptionSafetyTestWithPostConditions() { + @Test + public void nullPointerExceptionSafetyTestWithPostConditions() { Wait wait = new FluentWait<>(Pattern.compile("Fake_context")) - .withTimeout(30, TimeUnit.SECONDS).pollingEvery(500, TimeUnit.MILLISECONDS); - assertTrue(wait.until(contextFunction.andThen(searchingFunction).andThen(filteringFunction)).size() > 0); + .withTimeout(ofSeconds(30)).pollingEvery(ofMillis(500)); + assertThrows(TimeoutException.class, + () -> wait.until(contextFunction.andThen(searchingFunction).andThen(filteringFunction))); } } diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidLogcatListenerTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidLogcatListenerTest.java new file mode 100644 index 000000000..618da2e32 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidLogcatListenerTest.java @@ -0,0 +1,38 @@ +package io.appium.java_client.android; + +import org.apache.commons.lang3.time.DurationFormatUtils; +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 + public void verifyLogcatListenerCanBeAssigned() { + final Semaphore messageSemaphore = new Semaphore(1); + final Duration timeout = Duration.ofSeconds(15); + + 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); + try { + driver.startLogcatBroadcast(); + messageSemaphore.acquire(); + // This is needed for pushing some internal log messages + driver.runAppInBackground(Duration.ofSeconds(1)); + 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 { + messageSemaphore.release(); + driver.stopLogcatBroadcast(); + } + } +} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java new file mode 100644 index 000000000..5fef68b48 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidScreenRecordTest.java @@ -0,0 +1,40 @@ +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; + +public class AndroidScreenRecordTest extends BaseAndroidTest { + + @BeforeEach + public void setUp() { + startActivity(".ApiDemos"); + } + + @Test + public void verifyBasicScreenRecordingWorks() throws InterruptedException { + try { + driver.startRecordingScreen( + new AndroidStartScreenRecordingOptions() + .withTimeLimit(Duration.ofSeconds(5)) + ); + } catch (WebDriverException e) { + if (e.getMessage() != null && e.getMessage().toLowerCase(ROOT).contains("emulator")) { + // screen recording only works on real devices + return; + } + } + Thread.sleep(5000); + String result = driver.stopRecordingScreen(); + assertThat(result, is(not(emptyString()))); + } + +} 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/e2eAndroidTest/java/io/appium/java_client/android/AndroidViewMatcherTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidViewMatcherTest.java new file mode 100644 index 000000000..80b60ab28 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidViewMatcherTest.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Test; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class AndroidViewMatcherTest extends BaseEspressoTest { + + @Test + public void testFindByViewMatcher() { + String selector = new Json().toJson(Map.of( + "name", "withText", + "args", List.of("Animation"), + "class", "androidx.test.espresso.matcher.ViewMatchers" + )); + final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); + assertNotNull(wait.until(ExpectedConditions + .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/BaseAndroidTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java similarity index 62% rename from src/test/java/io/appium/java_client/android/BaseAndroidTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/BaseEspressoTest.java index 99d379fae..2245b1be3 100644 --- a/src/test/java/io/appium/java_client/android/BaseAndroidTest.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.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 io.appium.java_client.utils.TestUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; -import org.junit.AfterClass; -import org.junit.BeforeClass; +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +public class BaseEspressoTest { -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; - -public class BaseAndroidTest { private static AppiumDriverLocalService service; - protected static AndroidDriver driver; + protected static AndroidDriver driver; /** * initialization. */ - @BeforeClass public static void beforeClass() throws Exception { + @BeforeAll public static void beforeClass() { service = AppiumDriverLocalService.buildDefaultService(); service.start(); @@ -43,18 +41,17 @@ public class BaseAndroidTest { "An appium server node is not started!"); } - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - 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/main/java/io/appium/java_client/android/AndroidElement.java b/src/e2eAndroidTest/java/io/appium/java_client/android/BatteryTest.java similarity index 55% rename from src/main/java/io/appium/java_client/android/AndroidElement.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/BatteryTest.java index 4aad95314..ae9fc9e26 100644 --- a/src/main/java/io/appium/java_client/android/AndroidElement.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/BatteryTest.java @@ -16,19 +16,18 @@ package io.appium.java_client.android; -import static io.appium.java_client.android.AndroidMobileCommandHelper.replaceElementValueCommand; +import org.junit.jupiter.api.Test; -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.FindsByAndroidUIAutomator; -import io.appium.java_client.MobileElement; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; -public class AndroidElement extends MobileElement - implements FindsByAndroidUIAutomator { - /** - * This method replace current text value. - * @param value a new value - */ - public void replaceValue(String value) { - CommandExecutionHelper.execute(this, replaceElementValueCommand(this, value)); +public class BatteryTest extends BaseAndroidTest { + + @Test public void veryGettingBatteryInformation() { + final AndroidBatteryInfo batteryInfo = driver.getBatteryInfo(); + assertThat(batteryInfo.getLevel(), is(greaterThan(0.0))); + assertThat(batteryInfo.getState(), is(not(AndroidBatteryInfo.BatteryState.UNKNOWN))); } } diff --git a/src/main/java/io/appium/java_client/android/AndroidKeyMetastate.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ClipboardTest.java similarity index 52% rename from src/main/java/io/appium/java_client/android/AndroidKeyMetastate.java rename to src/e2eAndroidTest/java/io/appium/java_client/android/ClipboardTest.java index 392c414dd..8de3bda5c 100644 --- a/src/main/java/io/appium/java_client/android/AndroidKeyMetastate.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ClipboardTest.java @@ -16,26 +16,23 @@ package io.appium.java_client.android; -/** - * Metastates for Android Key Events. - */ -public interface AndroidKeyMetastate { +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ClipboardTest extends BaseAndroidTest { + + @BeforeEach public void setUp() { + driver.executeScript("mobile: terminateApp", Map.of("appId", APP_ID)); + driver.executeScript("mobile: activateApp", Map.of("appId", APP_ID)); + } - int META_ALT_LEFT_ON = 16; - int META_ALT_ON = 2; - int META_ALT_RIGHT_ON = 32; - int META_CAPS_LOCK_ON = 1048576; - int META_CTRL_LEFT_ON = 8192; - int META_CTRL_ON = 4096; - int META_CTRL_RIGHT_ON = 16384; - int META_FUNCTION_ON = 8; - int META_META_LEFT_ON = 131072; - int META_META_ON = 65536; - int META_META_RIGHT_ON = 262144; - int META_NUM_LOCK_ON = 2097152; - int META_SCROLL_LOCK_ON = 4194304; - int META_SHIFT_LEFT_ON = 64; - int META_SHIFT_ON = 1; - int META_SHIFT_RIGHT_ON = 128; - int META_SYM_ON = 4; + @Test public void verifySetAndGetClipboardText() { + final String text = "Happy testing"; + driver.setClipboardText(text); + assertEquals(driver.getClipboardText(), text); + } } 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/e2eAndroidTest/java/io/appium/java_client/android/ExecuteDriverScriptTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteDriverScriptTest.java new file mode 100644 index 000000000..d8bf5a65d --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ExecuteDriverScriptTest.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; + +import io.appium.java_client.driverscripts.ScriptOptions; +import io.appium.java_client.driverscripts.ScriptType; +import io.appium.java_client.driverscripts.ScriptValue; +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.jupiter.api.Assertions.assertNotNull; + +public class ExecuteDriverScriptTest extends BaseAndroidTest { + + @Test + public void verifyBasicScriptExecution() { + String script = String.join("\n", Arrays.asList( + "const status = await driver.status();", + "console.warn('warning message');", + "return status;") + ); + ScriptValue value = driver.executeDriverScript(script, new ScriptOptions() + .withTimeout(5000) + .withScriptType(ScriptType.WEBDRIVERIO)); + //noinspection unchecked + assertNotNull(((Map) value.getResult()).get("build")); + //noinspection unchecked + assertThat(((List)value.getLogs().get("warn")).get(0), + is(equalTo("warning message"))); + } +} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/FingerPrintTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/FingerPrintTest.java new file mode 100644 index 000000000..4f1e17551 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/FingerPrintTest.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 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.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 void initDriver() { + 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. + */ + @BeforeAll public static void beforeClass() { + service = AppiumDriverLocalService.buildDefaultService(); + service.start(); + + if (service == null || !service.isRunning()) { + throw new ExceptionInInitializerError("An appium server node is not started!"); + } + } + + /** + * finishing. + */ + @AfterAll public static void afterClass() { + if (service != null) { + service.stop(); + } + } + + 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.findElement(By.id("com.android.settings:id/next_button")).click(); + } + + private void clickFingerPrintNext() { + driver.findElement(By.id("com.android.settings:id/fingerprint_next_button")).click(); + } + + private void clickOKInPopup() { + driver.findElement(By.id("android:id/button1")).click(); + } + + private void enterPasswordAndContinue() { + driver.findElement(By.id("com.android.settings:id/password_entry")).sendKeys("1234\n"); + } + + private void clickOnSecurity() { + driver.findElement(androidUIAutomator("new UiScrollable(new UiSelector()" + + ".scrollable(true)).scrollIntoView(" + + "new UiSelector().text(\"Security & location\"));")).click(); + } + + /** + * enable system security which is required for finger print activation. + */ + @BeforeEach + public void before() { + initDriver(); + clickOnSecurity(); + findElementByText("Screen lock").click(); + findElementByText("PIN").click(); + enterPasswordAndContinue(); + enterPasswordAndContinue(); + clickNext(); + } + + /** + * add a new finger print to security. + */ + @Test + public void fingerPrintTest() { + findElementByText("Fingerprint").click(); + clickFingerPrintNext(); + enterPasswordAndContinue(); + driver.fingerPrint(1234); + driver.fingerPrint(1234); + driver.fingerPrint(1234); + try { + clickNext(); + } catch (Exception e) { + Assertions.fail("fingerprint command fail to execute"); + } + } + + /** + * disabling pin lock mode. + */ + @AfterEach + public void after() { + driver.quit(); + + initDriver(); + clickOnSecurity(); + + findElementByText("Screen lock").click(); + + enterPasswordAndContinue(); + findElementByText("None").click(); + clickOKInPopup(); + driver.quit(); + } +} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/ImagesComparisonTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/ImagesComparisonTest.java new file mode 100644 index 000000000..3632bfbf8 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/ImagesComparisonTest.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.android; + +import io.appium.java_client.imagecomparison.FeatureDetector; +import io.appium.java_client.imagecomparison.FeaturesMatchingOptions; +import io.appium.java_client.imagecomparison.FeaturesMatchingResult; +import io.appium.java_client.imagecomparison.MatchingFunction; +import io.appium.java_client.imagecomparison.OccurrenceMatchingOptions; +import io.appium.java_client.imagecomparison.OccurrenceMatchingResult; +import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; +import io.appium.java_client.imagecomparison.SimilarityMatchingResult; +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.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); + FeaturesMatchingResult result = driver + .matchImagesFeatures(screenshot, screenshot, new FeaturesMatchingOptions() + .withDetectorName(FeatureDetector.ORB) + .withGoodMatchesFactor(40) + .withMatchFunc(MatchingFunction.BRUTE_FORCE_HAMMING) + .withEnabledVisualization()); + assertThat(result.getVisualization().length, is(greaterThan(0))); + assertThat(result.getCount(), is(greaterThan(0))); + assertThat(result.getTotalCount(), is(greaterThan(0))); + assertFalse(result.getPoints1().isEmpty()); + assertNotNull(result.getRect1()); + assertFalse(result.getPoints2().isEmpty()); + assertNotNull(result.getRect2()); + } + + @Test + public void verifyOccurrencesLookup() { + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); + OccurrenceMatchingResult result = driver + .findImageOccurrence(screenshot, screenshot, new OccurrenceMatchingOptions() + .withEnabledVisualization()); + assertThat(result.getVisualization().length, is(greaterThan(0))); + assertNotNull(result.getRect()); + } + + @Test + public void verifySimilarityCalculation() { + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); + SimilarityMatchingResult result = driver + .getImagesSimilarity(screenshot, screenshot, new SimilarityMatchingOptions() + .withEnabledVisualization()); + assertThat(result.getVisualization().length, is(greaterThan(0))); + assertThat(result.getScore(), is(greaterThan(0.0))); + } +} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/KeyCodeTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/KeyCodeTest.java new file mode 100644 index 000000000..7ed431166 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/KeyCodeTest.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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.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"); + + @BeforeEach + public void setUp() { + startActivity(".text.KeyEventText"); + } + + @Test + public void pressKeyCodeTest() { + driver.pressKey(new KeyEvent(AndroidKey.ENTER)); + assertThat(driver.findElement(PRESS_RESULT_VIEW).getText(), + containsString(String.format("KEYCODE_%s", AndroidKey.ENTER.name()))); + } + + @Test + public void pressKeyCodeWithMetastateTest() { + driver.pressKey(new KeyEvent(AndroidKey.SPACE) + .withMetaModifier(KeyEventMetaModifier.SHIFT_ON)); + final String state = driver.findElement(PRESS_RESULT_VIEW).getText(); + assertThat(state, containsString(String.format("KEYCODE_%s", AndroidKey.SPACE.name()))); + assertThat(state, containsString(String.format("META_%s", KeyEventMetaModifier.SHIFT_ON.name()))); + } + + @Test + public void pressKeyAndGenerateIMEActionTest() { + driver.pressKey(new KeyEvent() + .withKey(AndroidKey.ENTER) + .withFlag(KeyEventFlag.SOFT_KEYBOARD) + .withFlag(KeyEventFlag.KEEP_TOUCH_MODE) + .withFlag(KeyEventFlag.EDITOR_ACTION)); + final String state = driver.findElement(PRESS_RESULT_VIEW).getText(); + // This event won't update the view + assertTrue(state.isEmpty()); + } + + @Test + public void longPressKeyCodeTest() { + driver.longPressKey(new KeyEvent(AndroidKey.SPACE)); + final String state = driver.findElement(PRESS_RESULT_VIEW).getText(); + assertThat(state, containsString(String.format("KEYCODE_%s", AndroidKey.SPACE.name()))); + assertThat(state, containsString(String.format("flags=0x%s", + Integer.toHexString(KeyEventFlag.LONG_PRESS.getValue())))); + } + + @Test + public void longPressKeyCodeWithMetastateTest() { + driver.longPressKey(new KeyEvent(AndroidKey.SPACE) + .withMetaModifier(KeyEventMetaModifier.SHIFT_ON)); + final String state = driver.findElement(PRESS_RESULT_VIEW).getText(); + assertThat(state, containsString(String.format("KEYCODE_%s", AndroidKey.SPACE.name()))); + assertThat(state, containsString(String.format("META_%s", KeyEventMetaModifier.SHIFT_ON.name()))); + assertThat(state, containsString(String.format("flags=0x%s", + Integer.toHexString(KeyEventFlag.LONG_PRESS.getValue())))); + } +} diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/LogEventTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/LogEventTest.java new file mode 100644 index 000000000..16d28f31e --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/LogEventTest.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; + +import io.appium.java_client.serverevents.CommandEvent; +import io.appium.java_client.serverevents.CustomEvent; +import io.appium.java_client.serverevents.ServerEvents; +import io.appium.java_client.serverevents.TimedEvent; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LogEventTest extends BaseAndroidTest { + + @Test + public void verifyLoggingCustomEvents() { + CustomEvent evt = new CustomEvent(); + evt.setEventName("funEvent"); + evt.setVendor("appium"); + 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 + ); + boolean hasCommandName = events.commands.stream().anyMatch((CommandEvent event) -> + event.name.equals("logCustomEvent") + ); + assertTrue(hasCustomEvent); + assertTrue(hasCommandName); + assertThat(events.jsonData, Matchers.containsString("\"appium:funEvent\"")); + } +} 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/e2eAndroidTest/java/io/appium/java_client/android/SettingTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/SettingTest.java new file mode 100644 index 000000000..380efa1cb --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/SettingTest.java @@ -0,0 +1,130 @@ +package io.appium.java_client.android; + +import io.appium.java_client.Setting; +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.jupiter.api.Assertions.assertEquals; + +public class SettingTest extends BaseAndroidTest { + + @Test public void ignoreUnimportantViewsTest() { + driver.ignoreUnimportantViews(true); + assertEquals(true, driver.getSettings() + .get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); + + driver.ignoreUnimportantViews(false); + assertEquals(false, driver.getSettings() + .get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); + } + + @Test public void configuratorTest() { + driver.configuratorSetActionAcknowledgmentTimeout(Duration.ofMillis(500)); + assertJSONElementContains(Setting.WAIT_ACTION_ACKNOWLEDGMENT_TIMEOUT, 500); + + driver.configuratorSetKeyInjectionDelay(Duration.ofMillis(400)); + assertJSONElementContains(Setting.KEY_INJECTION_DELAY, 400); + + driver.configuratorSetScrollAcknowledgmentTimeout(Duration.ofMillis(300)); + assertJSONElementContains(Setting.WAIT_SCROLL_ACKNOWLEDGMENT_TIMEOUT, 300); + + driver.configuratorSetWaitForIdleTimeout(Duration.ofMillis(600)); + assertJSONElementContains(Setting.WAIT_FOR_IDLE_TIMEOUT, 600); + + driver.configuratorSetWaitForSelectorTimeout(Duration.ofSeconds(1)); + assertJSONElementContains(Setting.WAIT_FOR_SELECTOR_TIMEOUT, 1000); + } + + @Test public void testNormalizeTagNames() { + assertEquals(false, driver.getSettings() + .get(Setting.NORMALIZE_TAG_NAMES.toString())); + driver.normalizeTagNames(true); + assertEquals(true, driver.getSettings() + .get(Setting.NORMALIZE_TAG_NAMES.toString())); + } + + @Test public void testSetShouldUseCompactResponses() { + assertEquals(true, driver.getSettings() + .get(Setting.SHOULD_USE_COMPACT_RESPONSES.toString())); + driver.setShouldUseCompactResponses(false); + assertEquals(false, driver.getSettings() + .get(Setting.SHOULD_USE_COMPACT_RESPONSES.toString())); + } + + @Test public void testSetElementResponseAttributes() { + assertEquals("", driver.getSettings() + .get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + driver.setElementResponseAttributes("type,label"); + assertEquals("type,label", driver.getSettings() + .get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + } + + @Test public void testAllowInvisibleElements() { + assertEquals(false, driver.getSettings() + .get(Setting.ALLOW_INVISIBLE_ELEMENTS.toString())); + driver.allowInvisibleElements(true); + assertEquals(true, driver.getSettings() + .get(Setting.ALLOW_INVISIBLE_ELEMENTS.toString())); + } + + @Test public void testEnableNotificationListener() { + assertEquals(true, driver.getSettings() + .get(Setting.ENABLE_NOTIFICATION_LISTENER.toString())); + driver.enableNotificationListener(false); + assertEquals(false, driver.getSettings() + .get(Setting.ENABLE_NOTIFICATION_LISTENER.toString())); + } + + @Test public void testShutdownOnPowerDisconnect() { + assertEquals(true, driver.getSettings() + .get(Setting.SHUTDOWN_ON_POWER_DISCONNECT.toString())); + driver.shutdownOnPowerDisconnect(false); + assertEquals(false, driver.getSettings() + .get(Setting.SHUTDOWN_ON_POWER_DISCONNECT.toString())); + } + + @Test public void testSetTrackScrollEvents() { + assertEquals(true, driver.getSettings() + .get(Setting.TRACK_SCROLL_EVENTS.toString())); + driver.setTrackScrollEvents(false); + assertEquals(false, driver.getSettings() + .get(Setting.TRACK_SCROLL_EVENTS.toString())); + } + + @Test public void testSettingByString() { + assertEquals(true, driver.getSettings() + .get("shouldUseCompactResponses")); + driver.setSetting("shouldUseCompactResponses", false); + assertEquals(false, driver.getSettings() + .get("shouldUseCompactResponses")); + driver.setSetting("shouldUseCompactResponses", true); + assertEquals(true, driver.getSettings() + .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/e2eAndroidTest/java/io/appium/java_client/android/UIAutomator2Test.java b/src/e2eAndroidTest/java/io/appium/java_client/android/UIAutomator2Test.java new file mode 100644 index 000000000..47ac3239b --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/UIAutomator2Test.java @@ -0,0 +1,90 @@ +package io.appium.java_client.android; + +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 { + + @AfterEach + public void afterMethod() { + driver.rotate(new DeviceRotation(0, 0, 0)); + } + + @Test + public void testLandscapeRightRotation() { + 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); + } + + @Test + public void testLandscapeLeftRotation() { + 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); + } + + @Test + public void testPortraitUpsideDown() { + 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); + } + + /** + * ignoring. + */ + @Disabled + @Test + public void testToastMSGIsDisplayed() { + final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); + startActivity(".view.PopupMenu1"); + + 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(); + assertNotNull(wait.until(ExpectedConditions.presenceOfElementLocated( + By.xpath("//*[@text='Clicked popup menu item Search']")))); + + wait.until(ExpectedConditions.elementToBeClickable(popUpElement)).click(); + wait.until(ExpectedConditions.visibilityOfElementLocated( + By.xpath(".//*[@text='Add']"))).click(); + assertNotNull(wait.until(ExpectedConditions + .presenceOfElementLocated(By.xpath("//*[@text='Clicked popup menu item Add']")))); + + wait.until(ExpectedConditions.elementToBeClickable(popUpElement)).click(); + wait.until(ExpectedConditions.visibilityOfElementLocated( + By.xpath(".//*[@text='Edit']"))).click(); + assertNotNull(wait.until(ExpectedConditions + .presenceOfElementLocated(By.xpath("//*[@text='Clicked popup menu item Edit']")))); + + wait.until(ExpectedConditions.visibilityOfElementLocated( + By.xpath(".//*[@text='Share']"))).click(); + assertNotNull(wait.until(ExpectedConditions + .presenceOfElementLocated(By.xpath("//*[@text='Clicked popup menu item Share']")))); + } +} 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 68% 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 550b58b6f..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,38 +16,38 @@ package io.appium.java_client.pagefactory_tests; -import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; -import static junit.framework.TestCase.assertNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; - -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 io.appium.java_client.pagefactory.SelendroidFindBy; -import io.appium.java_client.pagefactory.iOSFindBy; -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.internal.WrapsDriver; +import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.remote.RemoteWebElement; import org.openqa.selenium.support.CacheLookup; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; -import java.util.concurrent.TimeUnit; +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; @@ -58,20 +58,17 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(className = "android.widget.TextView") private List androidTextViews; - @iOSFindBy(uiAutomator = ".elements()[0]") - private List iosTextViews; - - @iOSFindBy(uiAutomator = ".elements()[0]") @AndroidFindBy(className = "android.widget.TextView") + @AndroidFindBy(className = "android.widget.TextView") private List androidOriOsTextViews; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") 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; @@ -80,13 +77,9 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(className = "android.widget.TextView") private List chainElementViews; - @iOSFindBy(uiAutomator = ".elements()[0]") @iOSFindBy(xpath = "//someElement") - private List iosChainTextViews; - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")") @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")") @AndroidFindBy(id = "android:id/text1") - @iOSFindBy(uiAutomator = ".elements()[0]") @iOSFindBy(xpath = "//someElement") private List chainAndroidOrIOSUIAutomatorViews; @FindBy(id = "android:id/text1") @@ -95,20 +88,17 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(className = "android.widget.TextView") private WebElement androidTextView; - @iOSFindBy(uiAutomator = ".elements()[0]") - private WebElement iosTextView; - - @AndroidFindBy(className = "android.widget.TextView") @iOSFindBy(uiAutomator = ".elements()[0]") + @AndroidFindBy(className = "android.widget.TextView") private WebElement androidOriOsTextView; @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") 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; @@ -117,24 +107,20 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(className = "android.widget.TextView") private WebElement chainElementView; - @iOSFindBy(uiAutomator = ".elements()[0]") @iOSFindBy(xpath = "//someElement") - private WebElement iosChainTextView; - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/content\")") @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")") @AndroidFindBy(id = "android:id/text1") - @iOSFindBy(uiAutomator = ".elements()[0]") @iOSFindBy(xpath = "//someElement") private WebElement chainAndroidOrIOSUIAutomatorView; @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,13 +138,13 @@ public class AndroidPageObjectTest extends BaseAndroidTest { @AndroidFindBy(id = "android:id/FakeId") private WebElement findAllElementView; - @AndroidFindBy(id = "android:id/text1") @SelendroidFindBy(id = "Invalid Identifier") + @AndroidFindBy(id = "android:id/text1") private WebElement textAndroidId; - @iOSFindBy(uiAutomator = ".elements()[0]") @FindBy(css = "e.e1.e2") + @FindBy(css = "e.e1.e2") private List elementsWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy; - @iOSFindBy(uiAutomator = ".elements()[0]") @FindBy(css = "e.e1.e2") + @FindBy(css = "e.e1.e2") private WebElement elementWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy; @FindBy(id = "fakeId") @@ -167,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\")") @@ -185,64 +175,64 @@ 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() throws Exception { + @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, 5, TimeUnit.SECONDS), this); + PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(5)), this); } populated = true; @@ -265,14 +255,6 @@ public class AndroidPageObjectTest extends BaseAndroidTest { assertNotEquals(null, androidTextView.getAttribute("text")); } - @Test public void checkThatElementsWereNotFoundByIOSUIAutomator() { - assertEquals(0, iosTextViews.size()); - } - - @Test(expected = NoSuchElementException.class) public void checkThatElementWasNotFoundByIOSUIAutomator() { - assertNotNull(iosTextView.getAttribute("text")); - } - @Test public void androidOrIOSFindByElementsTest() { assertNotEquals(0, androidOriOsTextViews.size()); } @@ -321,14 +303,6 @@ public class AndroidPageObjectTest extends BaseAndroidTest { assertNotEquals(null, chainElementView.getAttribute("text")); } - @Test public void checkThatElementsWereNotFoundByIOSUIAutomatorChain() { - assertEquals(0, iosChainTextViews.size()); - } - - @Test(expected = NoSuchElementException.class) public void checkThatElementWasNotFoundByIOSUIAutomatorChain() { - assertNotNull(iosChainTextView.getAttribute("text")); - } - @Test public void androidOrIOSFindByElementsTestChainSearches() { assertNotEquals(0, chainAndroidOrIOSUIAutomatorViews.size()); } @@ -357,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() { @@ -367,23 +341,37 @@ public class AndroidPageObjectTest extends BaseAndroidTest { } @Test public void checkThatClassObjectMethodsDoNotInvokeTheSearching() { - assertEquals(true, AndroidElement.class.isAssignableFrom(fakeElement.getClass())); - assertEquals(false, AndroidElement.class.equals(fakeElement.getClass())); - assertEquals(true, driver.equals(((WrapsDriver) fakeElement).getWrappedDriver())); + assertTrue(WebElement.class.isAssignableFrom(fakeElement.getClass())); + assertNotEquals(WebElement.class, fakeElement.getClass()); + assertEquals(driver, ((WrapsDriver) fakeElement).getWrappedDriver()); } @Test public void checkThatClassObjectMethodsDoNotInvokeTheSearchingOfElementLest() { - assertEquals(true, List.class.isAssignableFrom(fakeElements.getClass())); - assertEquals(false, ArrayList.class.equals(fakeElements.getClass())); + assertTrue(List.class.isAssignableFrom(fakeElements.getClass())); + assertNotEquals(ArrayList.class, fakeElements.getClass()); + } + + @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 checkCached() { - assertEquals(cached.getId(), cached.getId()); + @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() { @@ -398,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 71% 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 450da87cd..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 @@ -17,25 +17,25 @@ package io.appium.java_client.pagefactory_tests; 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; import org.openqa.selenium.support.PageFactory; import java.util.List; -import java.util.concurrent.TimeUnit; + +import static java.time.Duration.ofSeconds; public class MobileBrowserCompatibilityTest { @@ -43,7 +43,8 @@ public class MobileBrowserCompatibilityTest { 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") @@ -52,28 +53,29 @@ 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() throws Exception { + @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, 5, TimeUnit.SECONDS), this); + PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(5)), this); } /** * finishing. */ - @After public void tearDown() throws Exception { + @AfterEach + public void tearDown() { if (driver != null) { driver.quit(); } @@ -83,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/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java new file mode 100644 index 000000000..235e7a5e9 --- /dev/null +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ServerBuilderTest.java @@ -0,0 +1,344 @@ +package io.appium.java_client.service.local; + +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.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.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.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; + +@SuppressWarnings("ResultOfMethodCallIgnored") +class ServerBuilderTest { + + /** + * It may be impossible to find the path to the instance of appium server due to different circumstance. + * So user may use environment variables/system properties to define the full path to the server + * main.js that is supposed to be default. + */ + private static final String PATH_TO_APPIUM_NODE_IN_PROPERTIES = getProperty(APPIUM_PATH); + + /** + * This is the path to the stub main.js file. + */ + private static final Path PATH_T0_TEST_MAIN_JS = TestUtils.resourcePathToAbsolutePath("main.js"); + + private static String testIP; + private AppiumDriverLocalService service; + private File testLogFile; + private OutputStream stream; + private static WebDriverManager chromeManager; + + /** + * initialization. + */ + @BeforeAll + public static void beforeClass() throws Exception { + testIP = getLocalIp4Address(); + chromeManager = chromedriver(); + chromeManager.setup(); + } + + @AfterEach + public void tearDown() throws Exception { + ofNullable(service).ifPresent(AppiumDriverLocalService::stop); + + if (stream != null) { + stream.close(); + } + + ofNullable(testLogFile).ifPresent(savedTestLogFile -> { + if (savedTestLogFile.exists()) { + savedTestLogFile.delete(); + } + }); + + System.clearProperty(APPIUM_PATH); + ofNullable(PATH_TO_APPIUM_NODE_IN_PROPERTIES).ifPresent(s -> setProperty(APPIUM_PATH, s)); + } + + @Test + void checkAbilityToAddLogMessageConsumer() { + List log = new ArrayList<>(); + service = buildDefaultService(); + service.clearOutPutStreams(); + service.addLogMessageConsumer(log::add); + service.start(); + assertTrue(log.size() > 0); + } + + @Test + void checkAbilityToStartDefaultService() { + service = buildDefaultService(); + service.start(); + assertTrue(service.isRunning()); + } + + @Test + 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 + 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 + void checkAbilityToStartServiceOnAFreePort() { + service = new AppiumServiceBuilder().usingAnyFreePort().build(); + service.start(); + assertTrue(service.isRunning()); + } + + @Test + void checkAbilityToStartServiceUsingNonLocalhostIP() { + service = new AppiumServiceBuilder().withIPAddress(testIP).build(); + service.start(); + assertTrue(service.isRunning()); + } + + @Test + void checkAbilityToStartServiceUsingFlags() { + service = new AppiumServiceBuilder() + .withArgument(CALLBACK_ADDRESS, testIP) + .withArgument(SESSION_OVERRIDE) + .build(); + service.start(); + assertTrue(service.isRunning()); + } + + @Test + 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 + 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) + .withCapabilities(options).build(); + service.start(); + assertTrue(service.isRunning()); + } + + @Test + void checkAbilityToChangeOutputStream() throws Exception { + testLogFile = new File("test"); + testLogFile.createNewFile(); + stream = Files.newOutputStream(testLogFile.toPath()); + service = buildDefaultService(); + service.addOutPutStream(stream); + service.start(); + assertThat(testLogFile.length(), greaterThan(0L)); + } + + @Test + void checkAbilityToChangeOutputStreamAfterTheServiceIsStarted() throws Exception { + testLogFile = new File("test"); + testLogFile.createNewFile(); + stream = Files.newOutputStream(testLogFile.toPath()); + service = buildDefaultService(); + service.start(); + service.addOutPutStream(stream); + service.isRunning(); + assertThat(testLogFile.length(), greaterThan(0L)); + } + + @Test + void checkAbilityToShutDownService() { + service = buildDefaultService(); + service.start(); + service.stop(); + assertFalse(service.isRunning()); + } + + @Test + void checkAbilityToStartAndShutDownFewServices() throws Exception { + List services = asList( + new AppiumServiceBuilder().usingAnyFreePort().build(), + new AppiumServiceBuilder().usingAnyFreePort().build(), + new AppiumServiceBuilder().usingAnyFreePort().build(), + new AppiumServiceBuilder().usingAnyFreePort().build()); + services.parallelStream().forEach(AppiumDriverLocalService::start); + assertTrue(services.stream().allMatch(AppiumDriverLocalService::isRunning)); + SECONDS.sleep(1); + services.parallelStream().forEach(AppiumDriverLocalService::stop); + assertTrue(services.stream().noneMatch(AppiumDriverLocalService::isRunning)); + } + + @Test + void checkAbilityToStartServiceWithLogFile() throws Exception { + testLogFile = new File("Log.txt"); + testLogFile.createNewFile(); + service = new AppiumServiceBuilder().withLogFile(testLogFile).build(); + service.start(); + assertTrue(testLogFile.exists()); + assertThat(testLogFile.length(), greaterThan(0L)); + } + + @Test + void checkAbilityToStartServiceWithPortUsingFlag() { + String port = "8996"; + String expectedUrl = String.format("http://0.0.0.0:%s/", port); + + service = new AppiumServiceBuilder() + .withArgument(() -> "--port", port) + .build(); + String actualUrl = service.getUrl().toString(); + assertEquals(expectedUrl, actualUrl); + service.start(); + } + + @Test + void checkAbilityToStartServiceWithPortUsingShortFlag() { + String port = "8996"; + String expectedUrl = String.format("http://0.0.0.0:%s/", port); + + service = new AppiumServiceBuilder() + .withArgument(() -> "-p", port) + .build(); + String actualUrl = service.getUrl().toString(); + assertEquals(expectedUrl, actualUrl); + service.start(); + } + + @Test + void checkAbilityToStartServiceWithIpUsingFlag() { + String expectedUrl = String.format("http://%s:4723/", testIP); + + service = new AppiumServiceBuilder() + .withArgument(() -> "--address", testIP) + .build(); + String actualUrl = service.getUrl().toString(); + assertEquals(expectedUrl, actualUrl); + service.start(); + } + + @Test + void checkAbilityToStartServiceWithIpUsingShortFlag() { + String expectedUrl = String.format("http://%s:4723/", testIP); + + service = new AppiumServiceBuilder() + .withArgument(() -> "-a", testIP) + .build(); + String actualUrl = service.getUrl().toString(); + assertEquals(expectedUrl, actualUrl); + service.start(); + } + + @Test + void checkAbilityToStartServiceWithLogFileUsingFlag() { + testLogFile = new File("Log2.txt"); + + service = new AppiumServiceBuilder() + .withArgument(() -> "--log", testLogFile.getAbsolutePath()) + .build(); + service.start(); + assertTrue(testLogFile.exists()); + } + + @Test + void checkAbilityToStartServiceWithLogFileUsingShortFlag() { + testLogFile = new File("Log3.txt"); + + service = new AppiumServiceBuilder() + .withArgument(() -> "-g", testLogFile.getAbsolutePath()) + .build(); + 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/localserver/ThreadSafetyTest.java b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ThreadSafetyTest.java similarity index 61% rename from src/test/java/io/appium/java_client/localserver/ThreadSafetyTest.java rename to src/e2eAndroidTest/java/io/appium/java_client/service/local/ThreadSafetyTest.java index f4ea4ebe8..8087da057 100644 --- a/src/test/java/io/appium/java_client/localserver/ThreadSafetyTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/service/local/ThreadSafetyTest.java @@ -1,46 +1,46 @@ -package io.appium.java_client.localserver; +package io.appium.java_client.service.local; -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() { - @Override protected Object perform() { + private final Action run = new Action() { + @Override protected String perform() { service.start(); return "OK"; } }; - private final Action run2 = run.clone(); - private final Action isRunning = new Action() { - @Override protected Object perform() { + private final Action run2 = run.clone(); + private final Action isRunning = new Action() { + @Override protected Boolean perform() { return service.isRunning(); } }; - private final Action isRunning2 = isRunning.clone(); - private final Action stop = new Action() { - @Override protected Object perform() { + private final Action isRunning2 = isRunning.clone(); + private final Action stop = new Action() { + @Override protected String perform() { service.stop(); return "OK"; } }; - private final Action stop2 = stop.clone(); + 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); + TestThread runTestThread = new TestThread<>(run); + TestThread runTestThread2 = new TestThread<>(run2); - TestThread isRunningTestThread = new TestThread(isRunning); - TestThread isRunningTestThread2 = new TestThread(isRunning2); + TestThread isRunningTestThread = new TestThread<>(isRunning); + TestThread isRunningTestThread2 = new TestThread<>(isRunning2); - TestThread stopTestThread = new TestThread(stop); - TestThread stopTestThread2 = new TestThread(stop2); + TestThread stopTestThread = new TestThread<>(stop); + TestThread stopTestThread2 = new TestThread<>(stop2); Thread runThread = new Thread(runTestThread); Thread runThread2 = new Thread(runTestThread2); @@ -67,8 +67,8 @@ public class ThreadSafetyTest { throw runTestThread2.throwable; } - assertTrue("OK".equals(runTestThread.result)); - assertTrue("OK".equals(runTestThread2.result)); + assertEquals("OK", runTestThread.result); + assertEquals("OK", runTestThread2.result); assertTrue(service.isRunning()); isRunningThread.start(); @@ -86,8 +86,8 @@ public class ThreadSafetyTest { throw isRunningTestThread2.throwable; } - assertTrue(isRunningTestThread.result.equals(true)); - assertTrue(isRunningTestThread2.result.equals(true)); + assertTrue(isRunningTestThread.result); + assertTrue(isRunningTestThread2.result); stopThread.start(); stopThread2.start(); @@ -104,8 +104,8 @@ public class ThreadSafetyTest { throw stopTestThread2.throwable; } - assertTrue("OK".equals(stopTestThread.result)); - assertTrue("OK".equals(stopTestThread2.result)); + assertEquals("OK", stopTestThread.result); + assertEquals("OK", stopTestThread2.result); assertFalse(service.isRunning()); } finally { if (service.isRunning()) { @@ -115,15 +115,16 @@ public class ThreadSafetyTest { } - @Test public void whenFewTreadsDoDifferentWork() throws Throwable { - TestThread runTestThread = new TestThread(run); - TestThread runTestThread2 = new TestThread(run2); + @Test + void whenFewTreadsDoDifferentWork() throws Throwable { + TestThread runTestThread = new TestThread<>(run); + TestThread runTestThread2 = new TestThread<>(run2); - TestThread isRunningTestThread = new TestThread(isRunning); - TestThread isRunningTestThread2 = new TestThread(isRunning2); + TestThread isRunningTestThread = new TestThread<>(isRunning); + TestThread isRunningTestThread2 = new TestThread<>(isRunning2); - TestThread stopTestThread = new TestThread(stop); - TestThread stopTestThread2 = new TestThread(stop2); + TestThread stopTestThread = new TestThread<>(stop); + TestThread stopTestThread2 = new TestThread<>(stop2); Thread runThread = new Thread(runTestThread); Thread runThread2 = new Thread(runTestThread2); @@ -157,9 +158,9 @@ public class ThreadSafetyTest { throw stopTestThread.throwable; } - assertTrue("OK".equals(runTestThread.result)); //the service had been started firstly (see (1)) - assertTrue(Boolean.TRUE.equals(isRunningTestThread.result)); //it was running (see (2)) - assertTrue("OK".equals(stopTestThread.result)); //and then the test tried to shut down it (see (3)) + assertEquals("OK", runTestThread.result); //the service had been started firstly (see (1)) + assertTrue(isRunningTestThread.result); //it was running (see (2)) + assertEquals("OK", stopTestThread.result); //and then the test tried to shut down it (see (3)) assertFalse(service.isRunning()); isRunningThread2.start(); // (1) @@ -185,10 +186,10 @@ public class ThreadSafetyTest { } //the service wasn'throwable being running (see (1)) - assertTrue(Boolean.FALSE.equals(isRunningTestThread2.result)); + assertFalse(isRunningTestThread2.result); //the service had not been started firstly (see (2)), it is ok - assertTrue("OK".equals(stopTestThread2.result)); - assertTrue("OK".equals(runTestThread2.result)); //and then it was started (see (3)) + assertEquals("OK", stopTestThread2.result); + assertEquals("OK", runTestThread2.result); //and then it was started (see (3)) assertTrue(service.isRunning()); } finally { if (service.isRunning()) { @@ -198,12 +199,12 @@ public class ThreadSafetyTest { } - private abstract static class Action implements Cloneable { - protected abstract Object perform(); + private abstract static class Action implements Cloneable { + protected abstract T perform(); - public Action clone() { + public Action clone() { try { - return (Action) super.clone(); + return (Action) super.clone(); } catch (Throwable t) { throw new AppiumServerHasNotBeenStartedLocallyException(t.getMessage(), t); } @@ -211,12 +212,12 @@ public Action clone() { } - private static class TestThread implements Runnable { - private final Action action; - private Object result; + private static class TestThread implements Runnable { + private final Action action; + private T result; private Throwable throwable; - TestThread(Action action) { + TestThread(Action action) { this.action = action; } diff --git a/src/e2eAndroidTest/resources/main.js b/src/e2eAndroidTest/resources/main.js new file mode 100644 index 000000000..e69de29bb 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/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.java new file mode 100644 index 000000000..baee302da --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSTest.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; + +import io.appium.java_client.service.local.AppiumDriverLocalService; +import io.appium.java_client.service.local.AppiumServiceBuilder; +import io.appium.java_client.service.local.flags.GeneralServerFlag; +import org.junit.jupiter.api.AfterAll; + +import java.time.Duration; +import java.util.Optional; + +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +public class BaseIOSTest { + + protected static AppiumDriverLocalService service; + protected static IOSDriver driver; + protected static final int PORT = 4723; + public static final String DEVICE_NAME = 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 service instance + */ + public static AppiumDriverLocalService startAppiumServer() { + service = new AppiumServiceBuilder() + .withIPAddress("127.0.0.1") + .usingPort(PORT) + .withTimeout(SERVER_START_TIMEOUT) + .withArgument(GeneralServerFlag.ALLOW_INSECURE, "*:session_discovery") + .build(); + service.start(); + return service; + } + + /** + * finishing. + */ + @AfterAll public static void afterClass() { + if (driver != null) { + driver.quit(); + } + if (service != null) { + service.stop(); + } + } +} diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.java new file mode 100644 index 000000000..8c54b30c4 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/BaseIOSWebViewTest.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.ios; + +import io.appium.java_client.ios.options.XCUITestOptions; +import org.junit.jupiter.api.BeforeAll; +import org.openqa.selenium.SessionNotCreatedException; + +import java.io.IOException; +import java.net.URL; +import java.time.Duration; +import java.util.function.Supplier; + +import static io.appium.java_client.utils.TestUtils.IOS_SIM_VODQA_RELEASE_URL; + +public class BaseIOSWebViewTest extends BaseIOSTest { + private static final Duration WEB_VIEW_DETECT_INTERVAL = Duration.ofSeconds(2); + private static final Duration WEB_VIEW_DETECT_DURATION = Duration.ofSeconds(30); + + @BeforeAll + public static void beforeClass() throws IOException { + startAppiumServer(); + + 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); + } + 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 + options.useNewWDA(); + driver = createDriver.get(); + } + } + + protected void findAndSwitchToWebView() throws InterruptedException { + final long msStarted = System.currentTimeMillis(); + while (System.currentTimeMillis() - msStarted <= WEB_VIEW_DETECT_DURATION.toMillis()) { + for (String handle : driver.getContextHandles()) { + if (handle.contains("WEBVIEW")) { + driver.context(handle); + return; + } + } + //noinspection BusyWait + Thread.sleep(WEB_VIEW_DETECT_INTERVAL.toMillis()); + } + throw new IllegalStateException(String.format("No web views have been detected within %sms timeout", + WEB_VIEW_DETECT_DURATION.toMillis())); + } +} 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/main/java/io/appium/java_client/ios/IOSElement.java b/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java similarity index 64% rename from src/main/java/io/appium/java_client/ios/IOSElement.java rename to src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java index 8702497aa..b3a58e588 100644 --- a/src/main/java/io/appium/java_client/ios/IOSElement.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/ClipboardTest.java @@ -16,12 +16,15 @@ package io.appium.java_client.ios; -import io.appium.java_client.FindsByIosClassChain; -import io.appium.java_client.FindsByIosNSPredicate; -import io.appium.java_client.FindsByIosUIAutomation; -import io.appium.java_client.MobileElement; +import org.junit.jupiter.api.Test; -public class IOSElement extends MobileElement - implements FindsByIosUIAutomation, FindsByIosNSPredicate, - FindsByIosClassChain { +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(text, driver.getClipboardText()); + } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java new file mode 100644 index 000000000..eea8899b5 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSAlertTest.java @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.apache.commons.lang3.StringUtils; +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; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; + +@TestMethodOrder(MethodOrderer.MethodName.class) +public class IOSAlertTest extends AppIOSTest { + private static final Duration ALERT_TIMEOUT = Duration.ofSeconds(5); + private static final int CLICK_RETRIES = 2; + private final WebDriverWait waiter = new WebDriverWait(driver, ALERT_TIMEOUT); + + @AfterEach + public void afterEach() { + try { + driver.switchTo().alert().accept(); + } catch (WebDriverException e) { + // ignore + } + } + + @Test + public void acceptAlertTest() { + Supplier acceptAlert = () -> { + ensureAlertPresence(); + driver.switchTo().alert().accept(); + return true; + }; + assertTrue(acceptAlert.get()); + } + + @Test + public void dismissAlertTest() { + Supplier dismissAlert = () -> { + ensureAlertPresence(); + driver.switchTo().alert().dismiss(); + return true; + }; + assertTrue(dismissAlert.get()); + } + + @Test + 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 55b53cbff..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,33 +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 static org.junit.Assert.assertTrue; - 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"); - assertTrue(driver.getContext().equals("Planet of the Ape-ium")); + @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/e2eIosTest/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java new file mode 100644 index 000000000..8c1bc3fee --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java @@ -0,0 +1,37 @@ +package io.appium.java_client.ios; + +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() { + 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.findElement(By.id("i am a link")); + el.click(); + 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, Duration.ofSeconds(30)) + .until(ExpectedConditions.titleIs("I am a page title - Sauce Labs"))); + driver.nativeWebTap(false); + el = driver.findElement(By.id("i am a link")); + el.click(); + assertTrue(new WebDriverWait(driver, Duration.ofSeconds(30)) + .until(ExpectedConditions.titleIs("I am another page title - Sauce Labs"))); + } +} diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSScreenRecordTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSScreenRecordTest.java new file mode 100644 index 000000000..170ea9b6a --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSScreenRecordTest.java @@ -0,0 +1,25 @@ +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; + +public class IOSScreenRecordTest extends AppIOSTest { + + @Test + public void verifyBasicScreenRecordingWorks() throws InterruptedException { + driver.startRecordingScreen( + new IOSStartScreenRecordingOptions() + .withTimeLimit(Duration.ofSeconds(10)) + ); + Thread.sleep(5000); + String result = driver.stopRecordingScreen(); + assertThat(result, is(not(emptyString()))); + } + +} diff --git a/src/test/java/io/appium/java_client/ios/IOSSearchingTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java similarity index 52% rename from src/test/java/io/appium/java_client/ios/IOSSearchingTest.java rename to src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java index ff8ce5dee..e3fa303e6 100644 --- a/src/test/java/io/appium/java_client/ios/IOSSearchingTest.java +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSearchingTest.java @@ -16,27 +16,25 @@ package io.appium.java_client.ios; -import static org.junit.Assert.assertNotEquals; +import org.junit.jupiter.api.Test; -import org.junit.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 findByAccessibilityIdTest() { - assertNotEquals(driver - .findElementByAccessibilityId("ComputeSumButton") - .getText(), null); - assertNotEquals(driver - .findElementsByAccessibilityId("ComputeSumButton") - .size(), 0); + @Test + public void findByByIosPredicatesTest() { + assertNotNull(driver.findElement(USERNAME_EDIT_PREDICATE).getText()); + assertNotEquals(0, driver.findElements(USERNAME_EDIT_PREDICATE).size()); } - @Test public void findByByIosUIAutomationTest() { - assertNotEquals(driver - .findElementByIosUIAutomation(".elements().withName(\"Answer\")") - .getText(), null); - assertNotEquals(driver - .findElementsByIosUIAutomation(".elements().withName(\"Answer\")") - .size(), 0); + @Test public void findByByIosClassChainTest() { + assertNotEquals(0, driver.findElements(VODQA_LOGO_CLASS_CHAIN).size()); } } diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/IOSSyslogListenerTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSyslogListenerTest.java new file mode 100644 index 000000000..b2c4c96bd --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/IOSSyslogListenerTest.java @@ -0,0 +1,38 @@ +package io.appium.java_client.ios; + +import org.apache.commons.lang3.time.DurationFormatUtils; +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 + public void verifySyslogListenerCanBeAssigned() { + final Semaphore messageSemaphore = new Semaphore(1); + final Duration timeout = Duration.ofSeconds(15); + + 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); + try { + driver.startSyslogBroadcast(); + messageSemaphore.acquire(); + // This is needed for pushing some internal log messages + driver.runAppInBackground(Duration.ofSeconds(1)); + 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 { + messageSemaphore.release(); + driver.stopSyslogBroadcast(); + } + } +} 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/e2eIosTest/java/io/appium/java_client/ios/ImagesComparisonTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/ImagesComparisonTest.java new file mode 100644 index 000000000..8534f8f35 --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/ImagesComparisonTest.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; + +import io.appium.java_client.imagecomparison.FeatureDetector; +import io.appium.java_client.imagecomparison.FeaturesMatchingOptions; +import io.appium.java_client.imagecomparison.FeaturesMatchingResult; +import io.appium.java_client.imagecomparison.MatchingFunction; +import io.appium.java_client.imagecomparison.OccurrenceMatchingOptions; +import io.appium.java_client.imagecomparison.OccurrenceMatchingResult; +import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; +import io.appium.java_client.imagecomparison.SimilarityMatchingResult; +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.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); + FeaturesMatchingResult result = driver + .matchImagesFeatures(screenshot, screenshot, new FeaturesMatchingOptions() + .withDetectorName(FeatureDetector.ORB) + .withGoodMatchesFactor(40) + .withMatchFunc(MatchingFunction.BRUTE_FORCE_HAMMING) + .withEnabledVisualization()); + assertThat(result.getVisualization().length, is(greaterThan(0))); + assertThat(result.getCount(), is(greaterThan(0))); + assertThat(result.getTotalCount(), is(greaterThan(0))); + assertFalse(result.getPoints1().isEmpty()); + assertNotNull(result.getRect1()); + assertFalse(result.getPoints2().isEmpty()); + assertNotNull(result.getRect2()); + } + + @Test + public void verifyOccurrencesSearch() { + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); + OccurrenceMatchingResult result = driver + .findImageOccurrence(screenshot, screenshot, new OccurrenceMatchingOptions() + .withEnabledVisualization()); + assertThat(result.getVisualization().length, is(greaterThan(0))); + assertNotNull(result.getRect()); + } + + @Test + public void verifySimilarityCalculation() { + byte[] screenshot = Base64.getEncoder().encode(driver.getScreenshotAs(OutputType.BYTES)); + SimilarityMatchingResult result = driver + .getImagesSimilarity(screenshot, screenshot, new SimilarityMatchingOptions() + .withEnabledVisualization()); + assertThat(result.getVisualization().length, is(greaterThan(0))); + assertThat(result.getScore(), is(greaterThan(0.0))); + } +} diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/RotationTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/RotationTest.java new file mode 100644 index 000000000..1d741845f --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/RotationTest.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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.DeviceRotation; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RotationTest extends AppIOSTest { + + @AfterEach public void afterMethod() { + driver.rotate(new DeviceRotation(0, 0, 0)); + } + + @Test public void testLandscapeRightRotation() { + DeviceRotation landscapeRightRotation = new DeviceRotation(0, 0, 90); + driver.rotate(landscapeRightRotation); + assertEquals(driver.rotation(), landscapeRightRotation); + } + + @Test public void testLandscapeLeftRotation() { + DeviceRotation landscapeLeftRotation = new DeviceRotation(0, 0, 270); + driver.rotate(landscapeLeftRotation); + assertEquals(driver.rotation(), landscapeLeftRotation); + } +} diff --git a/src/e2eIosTest/java/io/appium/java_client/ios/SettingTest.java b/src/e2eIosTest/java/io/appium/java_client/ios/SettingTest.java new file mode 100644 index 000000000..647b93e2d --- /dev/null +++ b/src/e2eIosTest/java/io/appium/java_client/ios/SettingTest.java @@ -0,0 +1,117 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Setting; +import org.junit.jupiter.api.Test; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SettingTest extends AppIOSTest { + + @Test public void testSetShouldUseCompactResponses() { + assertEquals(true, driver.getSettings() + .get(Setting.SHOULD_USE_COMPACT_RESPONSES.toString())); + driver.setShouldUseCompactResponses(false); + assertEquals(false, driver.getSettings() + .get(Setting.SHOULD_USE_COMPACT_RESPONSES.toString())); + } + + @Test public void testSetElementResponseAttributes() { + assertEquals("", driver.getSettings() + .get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + driver.setElementResponseAttributes("type,label"); + assertEquals("type,label", driver.getSettings() + .get(Setting.ELEMENT_RESPONSE_ATTRIBUTES.toString())); + } + + @Test public void testSetMjpegServerScreenshotQuality() { + assertEquals(25L, driver.getSettings() + .get(Setting.MJPEG_SERVER_SCREENSHOT_QUALITY.toString())); + driver.setMjpegServerScreenshotQuality(0); + assertEquals(0L, driver.getSettings() + .get(Setting.MJPEG_SERVER_SCREENSHOT_QUALITY.toString())); + } + + @Test public void testSetMjpegServerFramerate() { + assertEquals(10L, driver.getSettings() + .get(Setting.MJPEG_SERVER_FRAMERATE.toString())); + driver.setMjpegServerFramerate(60); + assertEquals(60L, driver.getSettings() + .get(Setting.MJPEG_SERVER_FRAMERATE.toString())); + } + + @Test public void testSetScreenshotQuality() { + assertEquals(1L, driver.getSettings() + .get(Setting.SCREENSHOT_QUALITY.toString())); + driver.setScreenshotQuality(2); + assertEquals(2L, driver.getSettings() + .get(Setting.SCREENSHOT_QUALITY.toString())); + } + + @Test public void testSetMjpegScalingFactor() { + driver.setMjpegScalingFactor(1); + assertEquals(1L, driver.getSettings() + .get(Setting.MJPEG_SCALING_FACTOR.toString())); + } + + @Test public void testSetKeyboardAutocorrection() { + driver.setKeyboardAutocorrection(true); + assertEquals(true, driver.getSettings() + .get(Setting.KEYBOARD_AUTOCORRECTION.toString())); + } + + @Test public void testSetKeyboardPrediction() { + driver.setKeyboardPrediction(true); + assertEquals(true, driver.getSettings() + .get(Setting.KEYBOARD_PREDICTION.toString())); + } + + @Test public void testSettingByString() { + assertEquals(true, driver.getSettings() + .get("shouldUseCompactResponses")); + driver.setSetting("shouldUseCompactResponses", false); + assertEquals(false, driver.getSettings() + .get("shouldUseCompactResponses")); + driver.setSetting("shouldUseCompactResponses", true); + assertEquals(true, driver.getSettings() + .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 7c6d0d43f..e41ba3699 100644 --- a/src/main/java/io/appium/java_client/AppiumCommandInfo.java +++ b/src/main/java/io/appium/java_client/AppiumCommandInfo.java @@ -16,15 +16,17 @@ package io.appium.java_client; +import lombok.AccessLevel; +import lombok.Getter; import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.http.HttpMethod; public class AppiumCommandInfo extends CommandInfo { - private final String url; - private final HttpMethod method; + @Getter(AccessLevel.PUBLIC) private final String url; + @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 @@ -34,12 +36,4 @@ public AppiumCommandInfo(String url, HttpMethod method) { this.url = url; this.method = method; } - - public String getUrl() { - return url; - } - - public HttpMethod getMethod() { - return method; - } } diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index 0a51b5271..1daf7013e 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -16,277 +16,411 @@ package io.appium.java_client; -import static com.google.common.base.Preconditions.checkNotNull; -import static io.appium.java_client.remote.MobileCapabilityType.AUTOMATION_NAME; -import static io.appium.java_client.remote.MobileCapabilityType.PLATFORM_NAME; -import static java.util.Optional.ofNullable; - -import com.google.common.collect.ImmutableMap; - -import io.appium.java_client.internal.JsonToMobileElementConverter; +import io.appium.java_client.internal.CapabilityHelpers; +import io.appium.java_client.internal.SessionHelpers; import io.appium.java_client.remote.AppiumCommandExecutor; -import io.appium.java_client.remote.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.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.remote.DesiredCapabilities; +import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.BiDiException; +import org.openqa.selenium.bidi.HasBiDi; +import org.openqa.selenium.remote.CapabilityType; +import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.DriverCommand; import org.openqa.selenium.remote.ErrorHandler; import org.openqa.selenium.remote.ExecuteMethod; import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.Response; -import org.openqa.selenium.remote.html5.RemoteLocationContext; +import org.openqa.selenium.remote.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; + /** -* @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} and its subclasses that designed - * specifically - * for each target mobile OS (still Android and iOS) -*/ -@SuppressWarnings("unchecked") -public class AppiumDriver - extends DefaultGenericMobileDriver { - - private static final ErrorHandler errorHandler = new ErrorHandler(new ErrorCodesMobile(), true); + * Default Appium driver implementation. + */ +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; - private final String platformName; - private final String automationName; - + @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; /** - * @param executor is an instance of {@link org.openqa.selenium.remote.HttpCommandExecutor} - * or class that extends it. Default commands or another vendor-specific - * commands may be specified there. - * @param capabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on command {@code executor} and {@code capabilities}. + * + * @param executor is an instance of {@link HttpCommandExecutor} + * or class that extends it. Default commands or another vendor-specific + * commands may be specified there. + * @param capabilities take a look at {@link Capabilities} */ 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(); + } - Object capabilityPlatform1 = getCapabilities().getCapability(PLATFORM_NAME); - Object capabilityAutomation1 = getCapabilities().getCapability(AUTOMATION_NAME); - - Object capabilityPlatform2 = capabilities.getCapability(PLATFORM_NAME); - Object capabilityAutomation2 = capabilities.getCapability(AUTOMATION_NAME); - - platformName = ofNullable(ofNullable(super.getPlatformName()) - .orElse(capabilityPlatform1 != null ? String.valueOf(capabilityPlatform1) : null)) - .orElse(capabilityPlatform2 != null ? String.valueOf(capabilityPlatform2) : null); - automationName = ofNullable(ofNullable(super.getAutomationName()) - .orElse(capabilityAutomation1 != null ? String.valueOf(capabilityAutomation1) : null)) - .orElse(capabilityAutomation2 != null ? String.valueOf(capabilityAutomation2) : null); - - this.setElementConverter(new JsonToMobileElementConverter(this, this)); + public AppiumDriver(AppiumClientConfig clientConfig, Capabilities capabilities) { + this(new AppiumCommandExecutor(MobileCommand.commandRepository, clientConfig), capabilities); } - public AppiumDriver(URL remoteAddress, Capabilities desiredCapabilities) { + 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); } /** - * @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 + * 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. + * @param automationName The name of the target automation. */ - protected static Capabilities substituteMobilePlatform(Capabilities originalCapabilities, - String newPlatform) { - DesiredCapabilities dc = new DesiredCapabilities(originalCapabilities); - dc.setCapability(PLATFORM_NAME, newPlatform); - return dc; - } - - @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 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(); - public List findElementsByLinkText(String using) { - return super.findElementsByLinkText(using); + setSessionId(sessionAddress.getId()); } - public List findElementsByPartialLinkText(String using) { - return super.findElementsByPartialLinkText(using); + @Override + public ExecuteMethod getExecuteMethod() { + return executeMethod; } - public List findElementsByTagName(String using) { - return super.findElementsByTagName(using); + /** + * This method is used to get build version status of running Appium server. + * + * @return map containing version details + */ + public Map getStatus() { + //noinspection unchecked + return (Map) execute(DriverCommand.STATUS).getValue(); } - public List findElementsByName(String using) { - return super.findElementsByName(using); + /** + * 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); } - public List findElementsByClassName(String using) { - return super.findElementsByClassName(using); + @Override + public Response execute(String driverCommand, Map parameters) { + return super.execute(driverCommand, parameters); } - public List findElementsByCssSelector(String using) { - return super.findElementsByCssSelector(using); + @Override + public Response execute(String command) { + return super.execute(command, Collections.emptyMap()); } - public List findElementsByXPath(String using) { - return super.findElementsByXPath(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); + } - @Override public List findElementsByAccessibilityId(String using) { - return super.findElementsByAccessibilityId(using); + @Override + public X convertFromPngBytes(byte[] png) { + return outputType.convertFromPngBytes(png); + } + }); } - @Override public ExecuteMethod getExecuteMethod() { - return executeMethod; + @Override + public AppiumDriver assertExtensionExists(String extName) { + if (absentExtensionNames.contains(extName)) { + throw new UnsupportedCommandException(); + } + return this; } - @Override public WebDriver context(String name) { - checkNotNull(name, "Must supply a context name"); - execute(DriverCommand.SWITCH_TO_CONTEXT, ImmutableMap.of("name", name)); + @Override + public AppiumDriver markExtensionAbsence(String extName) { + absentExtensionNames.add(extName); return this; } - @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); + @Override + public Optional maybeGetBiDi() { + return Optional.ofNullable(this.biDi); + } + + @Override + @NonNull + public BiDi getBiDi() { + var webSocketUrl = ((BaseOptions) this.capabilities).getWebSocketUrl().orElseThrow( + () -> { + var suffix = wasBiDiRequested + ? "Do both the server and the driver declare BiDi support?" + : String.format("Did you set %s to true?", SupportsWebSocketUrlOption.WEB_SOCKET_URL); + return new BiDiException(String.format( + "BiDi is not enabled for this driver session. %s", suffix + )); + } + ); + if (this.biDiUri == null) { + throw new BiDiException( + String.format( + "BiDi is not enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", + SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl + ) + ); } - } - - @Override public String getContext() { - String contextName = - String.valueOf(execute(DriverCommand.GET_CURRENT_CONTEXT_HANDLE).getValue()); - if ("null".equalsIgnoreCase(contextName)) { - return null; + if (this.biDi == null) { + // This should not happen + throw new IllegalStateException(); } - return contextName; - } - - @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 this.biDi; + } + + protected HttpClient getHttpClient() { + return ((HttpCommandExecutor) getCommandExecutor()).client; + } + + @Override + protected void startSession(Capabilities requestCapabilities) { + var response = Optional.ofNullable( + execute(DriverCommand.NEW_SESSION(singleton(requestCapabilities))) + ).orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a null response." + )); + + var rawResponseCapabilities = Optional.ofNullable(response.getValue()) + .map(value -> { + if (!(value instanceof Map)) { + throw new SessionNotCreatedException(String.format( + "The underlying command executor returned a response " + + "with a non well formed payload: %s", response) + ); + } + //noinspection unchecked + return (Map) value; + }) + .orElseThrow(() -> new SessionNotCreatedException( + "The underlying command executor returned a response without payload: " + response) + ); + + // 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); } - 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); + this.capabilities = new BaseOptions<>(rawResponseCapabilities); + this.wasBiDiRequested = Boolean.TRUE.equals( + requestCapabilities.getCapability(SupportsWebSocketUrlOption.WEB_SOCKET_URL) + ); + if (wasBiDiRequested) { + this.initBiDi((BaseOptions) capabilities); } + setSessionId(response.getSessionId()); } - @Override public Location location() { - return locationContext.location(); - } - - @Override public void setLocation(Location location) { - locationContext.setLocation(location); - } - - public URL getRemoteAddress() { - return remoteAddress; + /** + * Changes platform name if it is not set and returns merged capabilities. + * + * @param originalCapabilities the given {@link Capabilities}. + * @param defaultName a platformName value which has to be set up + * @return {@link Capabilities} with changed platform name value or the original capabilities + */ + protected static Capabilities ensurePlatformName( + Capabilities originalCapabilities, String defaultName) { + return originalCapabilities.getPlatformName() == null + ? originalCapabilities.merge(new ImmutableCapabilities(PLATFORM_NAME, defaultName)) + : originalCapabilities; } - @Override public String getPlatformName() { - return platformName; + /** + * 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 String getAutomationName() { - return automationName; + /** + * 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() { - return !getContext().toLowerCase().contains("NATIVE_APP".toLowerCase()); + private void initBiDi(BaseOptions responseCaps) { + var webSocketUrl = responseCaps.getWebSocketUrl(); + if (webSocketUrl.isEmpty()) { + return; + } + URISyntaxException uriSyntaxError = null; + try { + this.biDiUri = new URI(String.valueOf(webSocketUrl.get())); + } catch (URISyntaxException e) { + uriSyntaxError = e; + } + if (uriSyntaxError != null || this.biDiUri.getScheme() == null) { + var message = String.format( + "BiDi cannot be enabled for this driver session. " + + "Is the %s '%s' received from the create session response valid?", + SupportsWebSocketUrlOption.WEB_SOCKET_URL, webSocketUrl.get() + ); + if (uriSyntaxError == null) { + throw new BiDiException(message); + } + throw new BiDiException(message, uriSyntaxError); + } + var executor = getCommandExecutor(); + final HttpClient wsClient; + if (executor instanceof AppiumCommandExecutor) { + var wsConfig = ((AppiumCommandExecutor) executor).getAppiumClientConfig().baseUri(biDiUri); + wsClient = ((AppiumCommandExecutor) executor).getHttpClientFactory().createClient(wsConfig); + } else { + var 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); } } diff --git a/src/main/java/io/appium/java_client/AppiumExecutionMethod.java b/src/main/java/io/appium/java_client/AppiumExecutionMethod.java index 1950a917f..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 bdc04c7dc..a284e1ebb 100644 --- a/src/main/java/io/appium/java_client/AppiumFluentWait.java +++ b/src/main/java/io/appium/java_client/AppiumFluentWait.java @@ -17,83 +17,73 @@ package io.appium.java_client; import com.google.common.base.Throwables; - +import lombok.AccessLevel; +import lombok.Getter; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.support.ui.Clock; -import org.openqa.selenium.support.ui.Duration; 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.concurrent.TimeUnit; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; public class AppiumFluentWait extends FluentWait { private Function pollingStrategy = null; - public static class IterationInfo { - private final long number; - private final Duration elapsed; - private final Duration total; - private final Duration interval; - - /** - * The class is used to represent information about a single loop iteration in {@link #until(Function)} - * method. - * - * @param number loop iteration number, starts from 1 - * @param elapsed the amount of elapsed time since the loop started - * @param total the amount of total time to run the loop - * @param interval the default time interval for each loop iteration - */ - public IterationInfo(long number, Duration elapsed, Duration total, Duration interval) { - this.number = number; - this.elapsed = elapsed; - this.total = total; - this.interval = interval; - } + 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. * * @return current iteration number. It starts from 1 */ - public long getNumber() { - return number; - } - + @Getter(AccessLevel.PUBLIC) private final long number; /** * The amount of elapsed time. * * @return the amount of elapsed time */ - public Duration getElapsed() { - return elapsed; - } - + @Getter(AccessLevel.PUBLIC) private final Duration elapsed; /** * The amount of total time. * * @return the amount of total time */ - public Duration getTotal() { - return total; - } - + @Getter(AccessLevel.PUBLIC) private final Duration total; /** * The current interval. * * @return The actual value of current interval or the default one if it is not set */ - public Duration getInterval() { - return interval; + @Getter(AccessLevel.PUBLIC) private final Duration interval; + + /** + * The class is used to represent information about a single loop iteration in {@link #until(Function)} + * method. + * + * @param number loop iteration number, starts from 1 + * @param elapsed the amount of elapsed time since the loop started + * @param total the amount of total time to run the loop + * @param interval the default time interval for each loop iteration + */ + public IterationInfo(long number, Duration elapsed, Duration total, Duration interval) { + this.number = number; + this.elapsed = elapsed; + this.total = total; + this.interval = interval; } } /** + * The input value to pass to the evaluated conditions. + * * @param input The input value to pass to the evaluated conditions. */ public AppiumFluentWait(T input) { @@ -101,6 +91,8 @@ public AppiumFluentWait(T input) { } /** + * Creates wait object based on {@code input} value, {@code clock} and {@code sleeper}. + * * @param input The input value to pass to the evaluated conditions. * @param clock The clock to use when measuring the timeout. * @param sleeper Used to put the thread to sleep between evaluation loops. @@ -109,61 +101,50 @@ 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; } /** * Sets the strategy for polling. The default strategy is null, * which means, that polling interval is always a constant value and is - * set by {@link #pollingEvery(long, TimeUnit)} method. Otherwise the value set by that + * set by {@link #pollingEvery(Duration)} method. Otherwise the value set by that * method might be just a helper to calculate the actual interval. * Although, by setting an alternative polling strategy you may flexibly control * the duration of this interval for each polling round. @@ -211,10 +192,9 @@ public AppiumFluentWait withPollingStrategy(Function *
    *
  1. the function returns neither null nor false,
  2. *
  3. the function throws an unignored exception,
  4. - *
  5. the timeout expires, - *
  6. + *
  7. the timeout expires,
  8. *
  9. the current thread is interrupted
  10. - *
+ * . * * @param isTrue the parameter to pass to the expected condition * @param The function's expected return type. @@ -224,10 +204,19 @@ public AppiumFluentWait withPollingStrategy(Function */ @Override public V until(Function isTrue) { - final long start = getClock().now(); - final long end = getClock().laterBy(getTimeout().in(TimeUnit.MILLISECONDS)); - 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()); @@ -245,33 +234,52 @@ public V until(Function isTrue) { // Check the timeout after evaluating the function to ensure conditions // with a zero timeout can succeed. - if (!getClock().isNowBefore(end)) { - 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().in(TimeUnit.SECONDS), getInterval()); - throw timeoutException(timeoutMessage, lastException); + if (end.isBefore(getClock().instant())) { + handleTimeoutException(lastException, isTrue); } - try { - Duration interval = getInterval(); - if (pollingStrategy != null) { - final IterationInfo info = new IterationInfo(iterationNumber, - new Duration(getClock().now() - start, TimeUnit.MILLISECONDS), 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 2b6d7e7d0..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())); } - public static T execute(ExecutesMethod executesMethod, String command) { + @Nullable + public static T execute(ExecutesMethod executesMethod, String command) { return handleResponse(executesMethod.execute(command)); } - private static T handleResponse(Response response) { - if (response != null) { - return (T) response.getValue(); - } - return null; + @Nullable + private static T handleResponse(Response response) { + //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 new file mode 100644 index 000000000..4f44d6e0a --- /dev/null +++ b/src/main/java/io/appium/java_client/ComparesImages.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.imagecomparison.ComparisonMode; +import io.appium.java_client.imagecomparison.FeaturesMatchingOptions; +import io.appium.java_client.imagecomparison.FeaturesMatchingResult; +import io.appium.java_client.imagecomparison.OccurrenceMatchingOptions; +import io.appium.java_client.imagecomparison.OccurrenceMatchingResult; +import io.appium.java_client.imagecomparison.SimilarityMatchingOptions; +import io.appium.java_client.imagecomparison.SimilarityMatchingResult; +import org.jspecify.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.compareImagesCommand; + +public interface ComparesImages extends ExecutesMethod { + + /** + * Performs images matching by features with default options. Read + * https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html + * for more details on this topic. + * + * @param base64image1 base64-encoded representation of the first image + * @param base64Image2 base64-encoded representation of the second image + * @return The matching result. + */ + default FeaturesMatchingResult matchImagesFeatures(byte[] base64image1, byte[] base64Image2) { + return matchImagesFeatures(base64image1, base64Image2, null); + } + + /** + * Performs images matching by features. Read + * https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html + * for more details on this topic. + * + * @param base64image1 base64-encoded representation of the first image + * @param base64Image2 base64-encoded representation of the second image + * @param options comparison options + * @return The matching result. The configuration of fields in the result depends on comparison options. + */ + default FeaturesMatchingResult matchImagesFeatures(byte[] base64image1, byte[] base64Image2, + @Nullable FeaturesMatchingOptions options) { + Object response = CommandExecutionHelper.execute(this, + compareImagesCommand(ComparisonMode.MATCH_FEATURES, base64image1, base64Image2, options)); + //noinspection unchecked + return new FeaturesMatchingResult((Map) response); + } + + /** + * Performs images matching by features with default options. Read + * https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html + * for more details on this topic. + * + * @param image1 The location of the first image + * @param image2 The location of the second image + * @return The matching result. + * @throws IOException On file system I/O error. + */ + default FeaturesMatchingResult matchImagesFeatures(File image1, File image2) throws IOException { + return matchImagesFeatures(image1, image2, null); + } + + /** + * Performs images matching by features. Read + * https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html + * for more details on this topic. + * + * @param image1 The location of the first image + * @param image2 The location of the second image + * @param options comparison options + * @return The matching result. The configuration of fields in the result depends on comparison options. + * @throws IOException On file system I/O error. + */ + default FeaturesMatchingResult matchImagesFeatures(File image1, File image2, + @Nullable FeaturesMatchingOptions options) throws IOException { + return matchImagesFeatures(Base64.getEncoder().encode(Files.readAllBytes(image1.toPath())), + Base64.getEncoder().encode(Files.readAllBytes(image2.toPath())), options); + } + + /** + * Performs images matching by template to find possible occurrence of the partial image + * in the full image with default options. Read + * https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html + * for more details on this topic. + * + * @param fullImage base64-encoded representation of the full image + * @param partialImage base64-encoded representation of the partial image + * @return The matching result. + */ + default OccurrenceMatchingResult findImageOccurrence(byte[] fullImage, byte[] partialImage) { + return findImageOccurrence(fullImage, partialImage, null); + } + + /** + * Performs images matching by template to find possible occurrence of the partial image + * in the full image. Read + * https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html + * for more details on this topic. + * + * @param fullImage base64-encoded representation of the full image + * @param partialImage base64-encoded representation of the partial image + * @param options comparison options + * @return The matching result. The configuration of fields in the result depends on comparison options. + */ + default OccurrenceMatchingResult findImageOccurrence(byte[] fullImage, byte[] partialImage, + @Nullable OccurrenceMatchingOptions options) { + Object response = CommandExecutionHelper.execute(this, + compareImagesCommand(ComparisonMode.MATCH_TEMPLATE, fullImage, partialImage, options)); + return new OccurrenceMatchingResult(response); + } + + /** + * Performs images matching by template to find possible occurrence of the partial image + * in the full image with default options. Read + * https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html + * for more details on this topic. + * + * @param fullImage The location of the full image + * @param partialImage The location of the partial image + * @return The matching result. The configuration of fields in the result depends on comparison options. + * @throws IOException On file system I/O error. + */ + default OccurrenceMatchingResult findImageOccurrence(File fullImage, File partialImage) throws IOException { + return findImageOccurrence(fullImage, partialImage, null); + } + + /** + * Performs images matching by template to find possible occurrence of the partial image + * in the full image. Read + * https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html + * for more details on this topic. + * + * @param fullImage The location of the full image + * @param partialImage The location of the partial image + * @param options comparison options + * @return The matching result. The configuration of fields in the result depends on comparison options. + * @throws IOException On file system I/O error. + */ + default OccurrenceMatchingResult findImageOccurrence(File fullImage, File partialImage, + @Nullable OccurrenceMatchingOptions options) + throws IOException { + return findImageOccurrence(Base64.getEncoder().encode(Files.readAllBytes(fullImage.toPath())), + Base64.getEncoder().encode(Files.readAllBytes(partialImage.toPath())), options); + } + + /** + * Performs images matching to calculate the similarity score between them + * with default options. The flow there is similar to the one used in + * {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)}, + * but it is mandatory that both images are of equal size. + * + * @param base64image1 base64-encoded representation of the first image + * @param base64Image2 base64-encoded representation of the second image + * @return Matching result. The configuration of fields in the result depends on comparison options. + */ + default SimilarityMatchingResult getImagesSimilarity(byte[] base64image1, byte[] base64Image2) { + return getImagesSimilarity(base64image1, base64Image2, null); + } + + /** + * Performs images matching to calculate the similarity score between them. + * The flow there is similar to the one used in + * {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)}, + * but it is mandatory that both images are of equal size. + * + * @param base64image1 base64-encoded representation of the first image + * @param base64Image2 base64-encoded representation of the second image + * @param options comparison options + * @return Matching result. The configuration of fields in the result depends on comparison options. + */ + default SimilarityMatchingResult getImagesSimilarity(byte[] base64image1, byte[] base64Image2, + @Nullable SimilarityMatchingOptions options) { + Object response = CommandExecutionHelper.execute(this, + compareImagesCommand(ComparisonMode.GET_SIMILARITY, base64image1, base64Image2, options)); + //noinspection unchecked + return new SimilarityMatchingResult((Map) response); + } + + /** + * Performs images matching to calculate the similarity score between them + * with default options. The flow there is similar to the one used in + * {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)}, + * but it is mandatory that both images are of equal size. + * + * @param image1 The location of the full image + * @param image2 The location of the partial image + * @return Matching result. The configuration of fields in the result depends on comparison options. + * @throws IOException On file system I/O error. + */ + default SimilarityMatchingResult getImagesSimilarity(File image1, File image2) throws IOException { + return getImagesSimilarity(image1, image2, null); + } + + /** + * Performs images matching to calculate the similarity score between them. + * The flow there is similar to the one used in + * {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)}, + * but it is mandatory that both images are of equal size. + * + * @param image1 The location of the full image + * @param image2 The location of the partial image + * @param options comparison options + * @return Matching result. The configuration of fields in the result depends on comparison options. + * @throws IOException On file system I/O error. + */ + default SimilarityMatchingResult getImagesSimilarity(File image1, File image2, + @Nullable SimilarityMatchingOptions options) + throws IOException { + return getImagesSimilarity(Base64.getEncoder().encode(Files.readAllBytes(image1.toPath())), + Base64.getEncoder().encode(Files.readAllBytes(image2.toPath())), options); + } +} 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 e3a27cb9e..000000000 --- a/src/main/java/io/appium/java_client/DefaultGenericMobileDriver.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; - -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.interactions.Mouse; -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); - } - - /** - * @throws WebDriverException his method doesn't work against native app UI. - */ - public T findElementByLinkText(String using) throws WebDriverException { - return (T) super.findElementByLinkText(using); - } - - /** - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByLinkText(String using) throws WebDriverException { - return super.findElementsByLinkText(using); - } - - /** - * @throws WebDriverException his method doesn't work against native app UI. - */ - public T findElementByPartialLinkText(String using) throws WebDriverException { - return (T) super.findElementByPartialLinkText(using); - } - - /** - * @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); - } - - /** - * @throws WebDriverException his method doesn't work against native app UI. - */ - public T findElementByCssSelector(String using) throws WebDriverException { - return (T) super.findElementByCssSelector(using); - } - - /** - * @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); - } - - /** - * Mouse doesn't work on mobile devices and emulators. - */ - @Deprecated public Mouse getMouse() { - return super.getMouse(); - } -} 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 085ad85a0..000000000 --- a/src/main/java/io/appium/java_client/DefaultGenericMobileElement.java +++ /dev/null @@ -1,163 +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); - } - - /** - * @throws WebDriverException his method doesn't work against native app UI. - */ - public T findElementByLinkText(String using) throws WebDriverException { - return (T) super.findElementByLinkText(using); - } - - /** - * @throws WebDriverException This method doesn't work against native app UI. - */ - public List findElementsByLinkText(String using) throws WebDriverException { - return super.findElementsByLinkText(using); - } - - /** - * @throws WebDriverException his method doesn't work against native app UI. - */ - public T findElementByPartialLinkText(String using) throws WebDriverException { - return (T) super.findElementByPartialLinkText(using); - } - - /** - * @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); - } - - /** - * @throws WebDriverException his method doesn't work against native app UI. - */ - public T findElementByCssSelector(String using) throws WebDriverException { - return (T) super.findElementByCssSelector(using); - } - - /** - * @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); - } - - /** - * @throws WebDriverException because it may not work against native app UI. - */ - public void submit() throws WebDriverException { - super.submit(); - } - - /** - * @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 c353e1fdc..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: @@ -54,10 +51,14 @@ public Class getExceptionType(int statusCode) { } /** + * Returns the exception type that corresponds to the given {@code message}or {@code null} if + * there are no matching mobile exceptions. + * * @param message message An error message returned by Appium server * @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())) { @@ -73,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 new file mode 100644 index 000000000..2509dba85 --- /dev/null +++ b/src/main/java/io/appium/java_client/ExecutesDriverScript.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; + +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 java.util.HashMap; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.EXECUTE_DRIVER_SCRIPT; +import static java.util.Objects.requireNonNull; + +public interface ExecutesDriverScript extends ExecutesMethod { + + /** + * Run a set of scripts in scope of the current session. + * This allows multiple web driver commands to be executed within one request + * and may significantly speed up the automation script performance in + * distributed client-server environments with high latency. + * Read http://appium.io/docs/en/commands/session/execute-driver for more details. + * + * @since Appium 1.14 + * @param script the web driver script to execute (it should + * be a valid webdriverio code snippet by default + * unless another option is provided) + * @param options additional scripting options + * @return The script result + * @throws org.openqa.selenium.WebDriverException if there was a failure while executing the script + */ + default ScriptValue executeDriverScript(String script, @Nullable ScriptOptions options) { + Map data = new HashMap<>(); + data.put("script", requireNonNull(script)); + if (options != null) { + data.putAll(options.build()); + } + Response response = execute(EXECUTE_DRIVER_SCRIPT, data); + //noinspection unchecked + Map value = (Map) response.getValue(); + //noinspection unchecked + return new ScriptValue(value.get("result"), (Map) value.get("logs")); + } + + /** + * Run a set of scripts in scope of the current session with default options. + * + * @since Appium 1.14 + * @param script the web driver script to execute (it should + * be a valid webdriverio code snippet) + * @return The script result + */ + default ScriptValue executeDriverScript(String script) { + return executeDriverScript(script, null); + } +} 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 382ed85f3..000000000 --- a/src/main/java/io/appium/java_client/FindsByAccessibilityId.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; - -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByAccessibilityId extends FindsByFluentSelector { - /** - * @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); - } - - /** - * @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 60b8a5fd5..000000000 --- a/src/main/java/io/appium/java_client/FindsByAndroidUIAutomator.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; - -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByAndroidUIAutomator extends FindsByFluentSelector { - - /** - * @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); - } - - /** - * @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/FindsByFluentSelector.java b/src/main/java/io/appium/java_client/FindsByFluentSelector.java deleted file mode 100644 index 037bcf427..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/FindsByIosUIAutomation.java b/src/main/java/io/appium/java_client/FindsByIosUIAutomation.java deleted file mode 100644 index 57c4c54b4..000000000 --- a/src/main/java/io/appium/java_client/FindsByIosUIAutomation.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; - -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public interface FindsByIosUIAutomation extends FindsByFluentSelector { - /** - * @throws WebDriverException This method is not applicable with browser/webview UI. - * @throws NoSuchElementException when no one element is found - */ - default T findElementByIosUIAutomation(String using) { - return findElement(MobileSelector.IOS_UI_AUTOMATION.toString(), using); - } - - /** - * @throws WebDriverException This method is not applicable with browser/webview UI. - */ - default List findElementsByIosUIAutomation(String using) { - return findElements(MobileSelector.IOS_UI_AUTOMATION.toString(), using); - } -} 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 292c83189..e450f28f1 100644 --- a/src/main/java/io/appium/java_client/HasDeviceTime.java +++ b/src/main/java/io/appium/java_client/HasDeviceTime.java @@ -16,16 +16,33 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.GET_DEVICE_TIME; - -import org.openqa.selenium.remote.Response; +import java.util.Map; public interface HasDeviceTime extends ExecutesMethod { - /* - Gets device date and time for both iOS(Supports only real device) and Android devices + + /** + * Gets device date and time for both iOS(host time is returned for simulators) and Android devices. + * + * @param format The set of format specifiers. Read + * https://momentjs.com/docs/ to get the full list of supported + * datetime format specifiers. The default format is + * `YYYY-MM-DDTHH:mm:ssZ`, which complies to ISO-8601 + * @since Appium 1.18 + * @return Device time string + */ + default String getDeviceTime(String format) { + return CommandExecutionHelper.executeScript( + this, "mobile: getDeviceTime", Map.of("format", format) + ); + } + + /** + * Gets device date and time for both iOS(host time is returned for simulators) and Android devices. + * The default format since Appium 1.8.2 is `YYYY-MM-DDTHH:mm:ssZ`, which complies to ISO-8601. + * + * @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 new file mode 100644 index 000000000..b242d2b01 --- /dev/null +++ b/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java @@ -0,0 +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, CanRememberExtensionPresence { + + /** + * 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() { + 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 3106b2905..000000000 --- a/src/main/java/io/appium/java_client/HasSessionDetails.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.appium.java_client; - -import static io.appium.java_client.MobileCommand.GET_SESSION; -import static java.util.Optional.ofNullable; -import static org.apache.commons.lang3.StringUtils.isBlank; - -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.remote.Response; - -import java.util.Map; - -public interface HasSessionDetails extends ExecutesMethod { - /** - * @return a map with values that hold session details. - * - */ - @SuppressWarnings("unchecked") - default Map getSessionDetails() { - Response response = execute(GET_SESSION); - return ImmutableMap.builder() - .putAll(Map.class.cast(response.getValue())).build(); - } - - default Object getSessionDetail(String detail) { - return getSessionDetails().get(detail); - } - - default String getPlatformName() { - Object platformName = getSessionDetail("platformName"); - return ofNullable(platformName != null ? String.valueOf(platformName) : null).orElse(null); - } - - default String getAutomationName() { - Object automationName = getSessionDetail("automationName"); - return ofNullable(automationName != null ? String.valueOf(automationName) : null).orElse(null); - } - - /** - * @return is focus on browser or on native content. - */ - boolean isBrowser(); -} diff --git a/src/main/java/io/appium/java_client/HasSettings.java b/src/main/java/io/appium/java_client/HasSettings.java index 3195bcf58..73befa6f5 100644 --- a/src/main/java/io/appium/java_client/HasSettings.java +++ b/src/main/java/io/appium/java_client/HasSettings.java @@ -16,15 +16,15 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.getSettingsCommand; -import static io.appium.java_client.MobileCommand.setSettingsCommand; - -import com.google.common.collect.ImmutableMap; - 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 { @@ -34,10 +34,50 @@ public interface HasSettings extends ExecutesMethod { * the method for the specific setting you want to change. * * @param setting Setting you wish to set. - * @param value value of the setting. + * @param value Value of the setting. + * @return Self instance for chaining. */ - default void setSetting(Setting setting, Object value) { - CommandExecutionHelper.execute(this, setSettingsCommand(setting, value)); + default HasSettings setSetting(Setting setting, Object value) { + return setSetting(setting.toString(), value); + } + + /** + * Set a setting for this test session It's probably better to use a + * convenience function, rather than use this function directly. Try finding + * the method for the specific setting you want to change. + * + * @param settingName Setting name you wish to set. + * @param value Value of the setting. + * @return Self instance for chaining. + */ + default HasSettings setSetting(String settingName, Object value) { + CommandExecutionHelper.execute(this, setSettingsCommand(settingName, 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; } /** @@ -51,8 +91,6 @@ default void setSetting(Setting setting, Object value) { default Map getSettings() { Map.Entry> keyValuePair = getSettingsCommand(); Response response = execute(keyValuePair.getKey(), keyValuePair.getValue()); - - return ImmutableMap.builder() - .putAll(Map.class.cast(response.getValue())).build(); + return (Map) response.getValue(); } } 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 d03473bd0..c2a84bb11 100644 --- a/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java +++ b/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java @@ -16,6 +16,10 @@ 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; @@ -23,25 +27,23 @@ 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 12a497131..0ca018abb 100644 --- a/src/main/java/io/appium/java_client/InteractsWithApps.java +++ b/src/main/java/io/appium/java_client/InteractsWithApps.java @@ -16,35 +16,64 @@ package io.appium.java_client; -import static io.appium.java_client.MobileCommand.CLOSE_APP; +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.HashMap; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.ACTIVATE_APP; import static io.appium.java_client.MobileCommand.INSTALL_APP; import static io.appium.java_client.MobileCommand.IS_APP_INSTALLED; -import static io.appium.java_client.MobileCommand.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.prepareArguments; +import static io.appium.java_client.MobileCommand.TERMINATE_APP; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; -import com.google.common.collect.ImmutableMap; - -import java.time.Duration; -import java.util.AbstractMap; +@SuppressWarnings({"rawtypes", "unchecked"}) +public interface InteractsWithApps extends ExecutesMethod, CanRememberExtensionPresence { -public interface InteractsWithApps extends ExecutesMethod { /** - * Launch the app which was provided in the capabilities at session creation. + * Install an app on the mobile device. + * + * @param appPath path to app to install. */ - default void launchApp() { - execute(LAUNCH_APP); + default void installApp(String appPath) { + installApp(appPath, null); } /** * Install an app on the mobile device. * - * @param appPath path to app to install. + * @param appPath path to app to install or a remote URL. + * @param options Set of the corresponding installation options for + * the particular platform. */ - default void installApp(String appPath) { - execute(INSTALL_APP, ImmutableMap.of("appPath", appPath)); + default void installApp(String appPath, @Nullable BaseInstallApplicationOptions options) { + 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)); + } } /** @@ -54,42 +83,198 @@ default void installApp(String appPath) { * @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)) + ) + ); + } } /** - * Reset the currently running app for this session. + * 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 unit is one millisecond. + * Passing a negative value will switch to Home screen and return immediately. */ - default void resetApp() { - execute(RESET); + default void runAppInBackground(Duration duration) { + 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)) + ); + } } /** - * Runs the current app as a background app for the time - * requested. This is a synchronous method, it returns after the back has - * been returned to the foreground. + * Remove the specified app from the device (uninstall). * - * @param duration The time to run App in background. Minimum time resolution is one second + * @param bundleId the bundle identifier (or app id) of the app to remove. + * @return true if the uninstall was successful. */ - default void runAppInBackground(Duration duration) { - execute(RUN_APP_IN_BACKGROUND, ImmutableMap.of("seconds", duration.getSeconds())); + default boolean removeApp(String bundleId) { + return removeApp(bundleId, null); } /** * Remove the specified app from the device (uninstall). * - * @param bundleId the bunble identifier (or app id) of the app to remove. + * @param bundleId the bundle identifier (or app id) of the app to remove. + * @param options the set of uninstall options supported by the + * particular platform. + * @return true if the uninstall was successful. */ - default void removeApp(String bundleId) { - execute(REMOVE_APP, ImmutableMap.of("bundleId", bundleId)); + default boolean removeApp(String bundleId, @Nullable BaseRemoveApplicationOptions options) { + 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) + ) + ); + } } /** - * Close the app which was provided in the capabilities at session creation. + * Activates the given app if it installed, but not running or if it is running in the + * background. + * + * @param bundleId the bundle identifier (or app id) of the app to activate. */ - default void closeApp() { - execute(CLOSE_APP); + default void activateApp(String bundleId) { + activateApp(bundleId, null); } + /** + * Activates the given app if it installed, but not running or if it is running in the + * background. + * + * @param bundleId the bundle identifier (or app id) of the app to activate. + * @param options the set of activation options supported by the + * particular platform. + */ + default void activateApp(String bundleId, @Nullable BaseActivateApplicationOptions options) { + 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)); + } + } + + /** + * Queries the state of an application. + * + * @param bundleId the bundle identifier (or app id) of the app to query the state of. + * @return one of possible {@link ApplicationState} values, + */ + default ApplicationState queryAppState(String 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)) + ) + ) + ); + } + } + + /** + * Terminate the particular application if it is running. + * + * @param bundleId the bundle identifier (or app id) of the app to be terminated. + * @return true if the app was running before and has been successfully stopped. + */ + default boolean terminateApp(String bundleId) { + return terminateApp(bundleId, null); + } + + /** + * Terminate the particular application if it is running. + * + * @param bundleId the bundle identifier (or app id) of the app to be terminated. + * @param options the set of termination options supported by the + * particular platform. + * @return true if the app was running before and has been successfully stopped. + */ + default boolean terminateApp(String bundleId, @Nullable BaseTerminateApplicationOptions options) { + 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 2f60a3d66..000000000 --- a/src/main/java/io/appium/java_client/InteractsWithFiles.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; - -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 javax.xml.bind.DatatypeConverter; - -public interface InteractsWithFiles extends ExecutesMethod { - - /** - * @param remotePath On Android and iOS, this is either the path to the file - * (relative to the root of the app's file system). On iOS only, - * if path starts with /AppName.app, which will be replaced with - * the application's .app directory - * @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 DatatypeConverter.parseBase64Binary(base64String); - } - - /** - * Pull a folder from the simulator/device. Does not work on iOS Real - * Devices, but works on simulators - * - * @param remotePath On Android and iOS, this is either the path to the file - * (relative to the root of the app's file system). On iOS only, - * if path starts with /AppName.app, which will be replaced with - * the application's .app directory - * @return A byte array of Base64 encoded data, representing a ZIP ARCHIVE - * of the contents of the requested folder. - */ - default byte[] pullFolder(String remotePath) { - Response response = execute(PULL_FOLDER, ImmutableMap.of("path", remotePath)); - String base64String = response.getValue().toString(); - - return DatatypeConverter.parseBase64Binary(base64String); - } - -} 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 new file mode 100644 index 000000000..bd818b21b --- /dev/null +++ b/src/main/java/io/appium/java_client/LocksDevice.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.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; + +public interface LocksDevice extends ExecutesMethod, CanRememberExtensionPresence { + + /** + * This method locks a device. It will return silently if the device + * is already locked. + */ + default void lockDevice() { + lockDevice(Duration.ofSeconds(0)); + } + + /** + * Lock the device (bring it to the lock screen) for a given number of + * seconds or forever (until the command for unlocking is called). The call + * is ignored if the device has been already locked. + * + * @param duration for how long to lock the screen. Minimum time resolution is one second. + * A negative/zero value will lock the device and return immediately. + */ + default void lockDevice(Duration 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)); + } + } + + /** + * Unlock the device if it is locked. This method will return silently if the device + * is not locked. + */ + default void unlockDevice() { + 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()); + } + } + + /** + * Check if the device is locked. + * + * @return true if the device is locked or false otherwise. + */ + default boolean isDeviceLocked() { + 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 new file mode 100644 index 000000000..6ae80bc0c --- /dev/null +++ b/src/main/java/io/appium/java_client/LogsEvents.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; + +import io.appium.java_client.serverevents.CommandEvent; +import io.appium.java_client.serverevents.CustomEvent; +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 static io.appium.java_client.MobileCommand.GET_EVENTS; +import static io.appium.java_client.MobileCommand.LOG_EVENT; + +public interface LogsEvents extends ExecutesMethod { + + /** + * Log a custom event on the Appium server. + * + * @since Appium 1.16 + * @param event the event to log + * @throws org.openqa.selenium.WebDriverException if there was a failure while executing the script + */ + default void logEvent(CustomEvent event) { + execute(LOG_EVENT, Map.of("vendor", event.getVendor(), "event", event.getEventName())); + } + + /** + * Log a custom event on the Appium server. + * + * @since Appium 1.16 + * @return ServerEvents object wrapping up the various command and event timestamps + * @throws org.openqa.selenium.WebDriverException if there was a failure while executing the script + */ + default ServerEvents getEvents() { + Response response = execute(GET_EVENTS); + String jsonData = new Json().toJson(response.getValue()); + + //noinspection unchecked + Map value = (Map) response.getValue(); + + //noinspection unchecked + List commands = ((List>) value.get("commands")) + .stream() + .map((Map cmd) -> new CommandEvent( + (String) cmd.get("cmd"), + (Long) cmd.get("startTime"), + (Long) cmd.get("endTime") + )) + .collect(Collectors.toList()); + + List events = value.keySet().stream() + .filter((String name) -> !name.equals("commands")) + .map((String name) -> { + //noinspection unchecked + return new TimedEvent(name, (List) value.get(name)); + }) + .collect(Collectors.toList()); + + return new ServerEvents(commands, events, jsonData); + } +} 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 c4209523d..000000000 --- a/src/main/java/io/appium/java_client/MobileBy.java +++ /dev/null @@ -1,469 +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.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."; - - 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; - } - - protected String getLocatorString() { - return locatorString; - } - - @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()); - } - - /** - * Read https://developer.apple.com/library/tvos/documentation/DeveloperTools/ - * Conceptual/InstrumentsUserGuide/UIAutomation.html - * - * @param iOSAutomationText is iOS UIAutomation string - * @return an instance of {@link io.appium.java_client.MobileBy.ByIosUIAutomation} - */ - public static By IosUIAutomation(final String iOSAutomationText) { - return new ByIosUIAutomation(iOSAutomationText); - } - - /** - * Read http://developer.android.com/intl/ru/tools/testing-support-library/ - * index.html#uia-apis - * @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 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); - } - - public static class ByIosUIAutomation extends MobileBy implements Serializable { - - public ByIosUIAutomation(String iOSAutomationText) { - super(MobileSelector.IOS_UI_AUTOMATION, iOSAutomationText); - } - - /** - * @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 (FindsByIosUIAutomation.class.isAssignableFrom(contextClass)) { - return FindsByIosUIAutomation.class.cast(context) - .findElementsByIosUIAutomation(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(context.getClass())) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosUIAutomation.class, - FindsByFluentSelector.class); - } - - /** - * @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 (FindsByIosUIAutomation.class.isAssignableFrom(contextClass)) { - return ((FindsByIosUIAutomation) context) - .findElementByIosUIAutomation(getLocatorString()); - } - - if (FindsByFluentSelector.class.isAssignableFrom(context.getClass())) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosUIAutomation.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.IosUIAutomation: " + getLocatorString(); - } - } - - - public static class ByAndroidUIAutomator extends MobileBy implements Serializable { - - - public ByAndroidUIAutomator(String uiautomatorText) { - super(MobileSelector.ANDROID_UI_AUTOMATOR, uiautomatorText); - } - - /** - * @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(context.getClass())) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAndroidUIAutomator.class, - FindsByFluentSelector.class); - } - - /** - * @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(context.getClass())) { - 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); - } - - /** - * @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(context.getClass())) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByAccessibilityId.class, - FindsByFluentSelector.class); - } - - /** - * @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(context.getClass())) { - 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); - } - - /** - * @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(context.getClass())) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosClassChain.class, - FindsByFluentSelector.class); - } - - /** - * @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(context.getClass())) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosClassChain.class, - FindsByFluentSelector.class); - } - - @Override public String toString() { - return "By.IosClassChain: " + getLocatorString(); - } - } - - public static class ByIosNsPredicate extends MobileBy implements Serializable { - - protected ByIosNsPredicate(String locatorString) { - super(MobileSelector.IOS_PREDICATE_STRING, locatorString); - } - - /** - * @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(context.getClass())) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosNSPredicate.class, - FindsByFluentSelector.class); - } - - /** - * @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(context.getClass())) { - 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); - } - - /** - * @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(context.getClass())) { - return super.findElements(context); - } - - throw formIllegalArgumentException(contextClass, FindsByWindowsAutomation.class, - FindsByFluentSelector.class); - } - - /** - * @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(context.getClass())) { - return super.findElement(context); - } - - throw formIllegalArgumentException(contextClass, FindsByIosNSPredicate.class, - FindsByWindowsAutomation.class); - } - } -} - - diff --git a/src/main/java/io/appium/java_client/MobileCommand.java b/src/main/java/io/appium/java_client/MobileCommand.java index 7ab77cc90..b4df90047 100644 --- a/src/main/java/io/appium/java_client/MobileCommand.java +++ b/src/main/java/io/appium/java_client/MobileCommand.java @@ -17,70 +17,181 @@ package io.appium.java_client; import com.google.common.collect.ImmutableMap; - -import org.apache.commons.lang3.StringUtils; +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.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 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; + @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; - protected static final String LAUNCH_APP; - protected static final String CLOSE_APP; - protected static final String GET_DEVICE_TIME; - protected static final String GET_SESSION; + //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; - protected static final String LONG_PRESS_KEY_CODE; + @Deprecated + public static final String LONG_PRESS_KEY_CODE; + @Deprecated + protected static final String FINGER_PRINT; + @Deprecated protected static final String OPEN_NOTIFICATIONS; - protected static final String PRESS_KEY_CODE; + @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; + @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 Map commandRepository; + public static final String GET_SCREEN_ORIENTATION = "getScreenOrientation"; + public static final String SET_SCREEN_ORIENTATION = "setScreenOrientation"; + public static final String GET_SCREEN_ROTATION = "getScreenRotation"; + public static final String SET_SCREEN_ROTATION = "setScreenRotation"; + + public static final String GET_CONTEXT_HANDLES = "getContextHandles"; + public static final String GET_CURRENT_CONTEXT_HANDLE = "getCurrentContextHandle"; + public static final String SWITCH_TO_CONTEXT = "switchToContext"; + + public static final String GET_LOCATION = "getLocation"; + public static final String SET_LOCATION = "setLocation"; + + public static final Map commandRepository; static { RESET = "reset"; @@ -91,17 +202,33 @@ public class MobileCommand { RUN_APP_IN_BACKGROUND = "runAppInBackground"; PERFORM_TOUCH_ACTION = "performTouchAction"; PERFORM_MULTI_TOUCH = "performMultiTouch"; - IS_APP_INSTALLED = "isAppInstalled"; - INSTALL_APP = "installApp"; - REMOVE_APP = "removeApp"; LAUNCH_APP = "launchApp"; CLOSE_APP = "closeApp"; GET_DEVICE_TIME = "getDeviceTime"; GET_SESSION = "getSession"; + LOG_EVENT = "logCustomEvent"; + GET_EVENTS = "getLogEvents"; + + //region Applications Management + IS_APP_INSTALLED = "isAppInstalled"; + QUERY_APP_STATE = "queryAppState"; + TERMINATE_APP = "terminateApp"; + ACTIVATE_APP = "activateApp"; + REMOVE_APP = "removeApp"; + INSTALL_APP = "installApp"; + //endregion + + //region Clipboard + SET_CLIPBOARD = "setClipboard"; + GET_CLIPBOARD = "getClipboard"; + //endregion GET_PERFORMANCE_DATA = "getPerformanceData"; GET_SUPPORTED_PERFORMANCE_DATA_TYPES = "getSuppportedPerformanceDataTypes"; + START_RECORDING_SCREEN = "startRecordingScreen"; + STOP_RECORDING_SCREEN = "stopRecordingScreen"; + HIDE_KEYBOARD = "hideKeyboard"; LOCK = "lock"; SHAKE = "shake"; @@ -116,6 +243,7 @@ public class MobileCommand { IS_KEYBOARD_SHOWN = "isKeyboardShown"; IS_LOCKED = "isLocked"; LONG_PRESS_KEY_CODE = "longPressKeyCode"; + FINGER_PRINT = "fingerPrint"; OPEN_NOTIFICATIONS = "openNotifications"; PRESS_KEY_CODE = "pressKeyCode"; PUSH_FILE = "pushFile"; @@ -127,6 +255,20 @@ public class MobileCommand { GET_SETTINGS = "getSettings"; SET_SETTINGS = "setSettings"; GET_CURRENT_PACKAGE = "getCurrentPackage"; + SEND_SMS = "sendSMS"; + GSM_CALL = "gsmCall"; + GSM_SIGNAL = "gsmSignal"; + GSM_VOICE = "gsmVoice"; + NETWORK_SPEED = "networkSpeed"; + POWER_CAPACITY = "powerCapacity"; + POWER_AC_STATE = "powerAC"; + TOGGLE_WIFI = "toggleWiFi"; + TOGGLE_AIRPLANE_MODE = "toggleFlightMode"; + TOGGLE_DATA = "toggleData"; + 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")); @@ -138,20 +280,39 @@ public class MobileCommand { commandRepository.put(RUN_APP_IN_BACKGROUND, postC("/session/:sessionId/appium/app/background")); commandRepository.put(PERFORM_TOUCH_ACTION, postC("/session/:sessionId/touch/perform")); commandRepository.put(PERFORM_MULTI_TOUCH, postC("/session/:sessionId/touch/multi/perform")); - commandRepository.put(IS_APP_INSTALLED, postC("/session/:sessionId/appium/device/app_installed")); - commandRepository.put(INSTALL_APP, postC("/session/:sessionId/appium/device/install_app")); - commandRepository.put(REMOVE_APP, postC("/session/:sessionId/appium/device/remove_app")); commandRepository.put(LAUNCH_APP, postC("/session/:sessionId/appium/app/launch")); commandRepository.put(CLOSE_APP, postC("/session/:sessionId/appium/app/close")); commandRepository.put(LOCK, postC("/session/:sessionId/appium/device/lock")); commandRepository.put(GET_SETTINGS, getC("/session/:sessionId/appium/settings")); commandRepository.put(SET_SETTINGS, postC("/session/:sessionId/appium/settings")); commandRepository.put(GET_DEVICE_TIME, getC("/session/:sessionId/appium/device/system_time")); - commandRepository.put(GET_SESSION,getC("/session/:sessionId/")); + commandRepository.put(GET_SESSION, getC("/session/:sessionId/")); commandRepository.put(GET_SUPPORTED_PERFORMANCE_DATA_TYPES, - postC("/session/:sessionId/appium/performanceData/types")); + postC("/session/:sessionId/appium/performanceData/types")); commandRepository.put(GET_PERFORMANCE_DATA, - postC("/session/:sessionId/appium/getPerformanceData")); + postC("/session/:sessionId/appium/getPerformanceData")); + commandRepository.put(START_RECORDING_SCREEN, + postC("/session/:sessionId/appium/start_recording_screen")); + commandRepository.put(STOP_RECORDING_SCREEN, + postC("/session/:sessionId/appium/stop_recording_screen")); + commandRepository.put(GET_EVENTS, + postC("/session/:sessionId/appium/events")); + commandRepository.put(LOG_EVENT, + postC("/session/:sessionId/appium/log_event")); + + //region Applications Management + commandRepository.put(IS_APP_INSTALLED, postC("/session/:sessionId/appium/device/app_installed")); + commandRepository.put(INSTALL_APP, postC("/session/:sessionId/appium/device/install_app")); + commandRepository.put(ACTIVATE_APP, postC("/session/:sessionId/appium/device/activate_app")); + commandRepository.put(REMOVE_APP, postC("/session/:sessionId/appium/device/remove_app")); + commandRepository.put(TERMINATE_APP, postC("/session/:sessionId/appium/device/terminate_app")); + commandRepository.put(QUERY_APP_STATE, postC("/session/:sessionId/appium/device/app_state")); + //endregion + + //region Clipboard + commandRepository.put(GET_CLIPBOARD, postC("/session/:sessionId/appium/device/get_clipboard")); + commandRepository.put(SET_CLIPBOARD, postC("/session/:sessionId/appium/device/set_clipboard")); + //endregion //iOS commandRepository.put(SHAKE, postC("/session/:sessionId/appium/device/shake")); @@ -160,30 +321,57 @@ public class MobileCommand { postC("/session/:sessionId/appium/simulator/toggle_touch_id_enrollment")); //Android commandRepository.put(CURRENT_ACTIVITY, - getC("/session/:sessionId/appium/device/current_activity")); + getC("/session/:sessionId/appium/device/current_activity")); commandRepository.put(END_TEST_COVERAGE, - postC("/session/:sessionId/appium/app/end_test_coverage")); + postC("/session/:sessionId/appium/app/end_test_coverage")); commandRepository.put(GET_DISPLAY_DENSITY, getC("/session/:sessionId/appium/device/display_density")); commandRepository.put(GET_NETWORK_CONNECTION, getC("/session/:sessionId/network_connection")); commandRepository.put(GET_SYSTEM_BARS, getC("/session/:sessionId/appium/device/system_bars")); commandRepository.put(IS_KEYBOARD_SHOWN, getC("/session/:sessionId/appium/device/is_keyboard_shown")); commandRepository.put(IS_LOCKED, postC("/session/:sessionId/appium/device/is_locked")); commandRepository.put(LONG_PRESS_KEY_CODE, - postC("/session/:sessionId/appium/device/long_press_keycode")); + postC("/session/:sessionId/appium/device/long_press_keycode")); + commandRepository.put(FINGER_PRINT, postC("/session/:sessionId/appium/device/finger_print")); commandRepository.put(OPEN_NOTIFICATIONS, - postC("/session/:sessionId/appium/device/open_notifications")); + postC("/session/:sessionId/appium/device/open_notifications")); commandRepository.put(PRESS_KEY_CODE, - postC("/session/:sessionId/appium/device/press_keycode")); + postC("/session/:sessionId/appium/device/press_keycode")); commandRepository.put(PUSH_FILE, postC("/session/:sessionId/appium/device/push_file")); commandRepository.put(SET_NETWORK_CONNECTION, - postC("/session/:sessionId/network_connection")); + postC("/session/:sessionId/network_connection")); commandRepository.put(START_ACTIVITY, - postC("/session/:sessionId/appium/device/start_activity")); + postC("/session/:sessionId/appium/device/start_activity")); commandRepository.put(TOGGLE_LOCATION_SERVICES, - postC("/session/:sessionId/appium/device/toggle_location_services")); + postC("/session/:sessionId/appium/device/toggle_location_services")); commandRepository.put(UNLOCK, postC("/session/:sessionId/appium/device/unlock")); - commandRepository. put(REPLACE_VALUE, postC("/session/:sessionId/appium/element/:id/replace_value")); - commandRepository.put(GET_CURRENT_PACKAGE,getC("/session/:sessionId/appium/device/current_package")); + commandRepository.put(REPLACE_VALUE, postC("/session/:sessionId/appium/element/:id/replace_value")); + commandRepository.put(GET_CURRENT_PACKAGE, getC("/session/:sessionId/appium/device/current_package")); + commandRepository.put(SEND_SMS, postC("/session/:sessionId/appium/device/send_sms")); + commandRepository.put(GSM_CALL, postC("/session/:sessionId/appium/device/gsm_call")); + commandRepository.put(GSM_SIGNAL, postC("/session/:sessionId/appium/device/gsm_signal")); + commandRepository.put(GSM_VOICE, postC("/session/:sessionId/appium/device/gsm_voice")); + commandRepository.put(NETWORK_SPEED, postC("/session/:sessionId/appium/device/network_speed")); + commandRepository.put(POWER_CAPACITY, postC("/session/:sessionId/appium/device/power_capacity")); + commandRepository.put(POWER_AC_STATE, postC("/session/:sessionId/appium/device/power_ac")); + commandRepository.put(TOGGLE_WIFI, postC("/session/:sessionId/appium/device/toggle_wifi")); + commandRepository.put(TOGGLE_AIRPLANE_MODE, postC("/session/:sessionId/appium/device/toggle_airplane_mode")); + commandRepository.put(TOGGLE_DATA, postC("/session/:sessionId/appium/device/toggle_data")); + 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")); } /** @@ -217,59 +405,66 @@ 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 + )); } /** + * Prepares single argument. + * * @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) { + Object value) { ImmutableMap.Builder builder = ImmutableMap.builder(); builder.put(param, value); return builder.build(); } /** + * Prepares collection of arguments. + * * @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) { + 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]); } } @@ -277,84 +472,161 @@ 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 Map} of parameters for the device unlocking. + * + * @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 Map.entry(UNLOCK, Map.of()); + } + + /** + * 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 Map} command arguments. + * @deprecated This helper is deprecated and will be removed in future versions. + */ + @Deprecated + public static Map.Entry> getIsDeviceLockedCommand() { + return Map.entry(IS_LOCKED, Map.of()); } public static Map.Entry> getSettingsCommand() { - return new AbstractMap.SimpleEntry<>(GET_SETTINGS, ImmutableMap.of()); + return Map.entry(GET_SETTINGS, Map.of()); + } + + public static Map.Entry> setSettingsCommand(String setting, Object value) { + return setSettingsCommand(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 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 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) { + return Map.entry(PUSH_FILE, Map.of( + "path", remotePath, + "data", new String(base64Data, StandardCharsets.UTF_8) + )); + } + + public static Map.Entry> startRecordingScreenCommand(BaseStartScreenRecordingOptions opts) { + return Map.entry(START_RECORDING_SCREEN, Map.of("options", opts.build())); + } + + public static Map.Entry> stopRecordingScreenCommand(BaseStopScreenRecordingOptions opts) { + return Map.entry(STOP_RECORDING_SCREEN, Map.of("options", opts.build())); } - public static Map.Entry> setSettingsCommand(Setting setting, Object value) { - return new AbstractMap.SimpleEntry<>(SET_SETTINGS, prepareArguments("settings", - prepareArguments(setting.toString(), value))); + /** + * 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 + * @param img2Data base64-encoded data of the second image + * @param options comparison options + * @return key-value pairs + */ + public static Map.Entry> compareImagesCommand(ComparisonMode mode, + byte[] img1Data, byte[] img2Data, + @Nullable BaseComparisonOptions options) { + 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 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 f2a98536b..000000000 --- a/src/main/java/io/appium/java_client/MobileElement.java +++ /dev/null @@ -1,100 +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 - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - 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/MultiTouchAction.java b/src/main/java/io/appium/java_client/MultiTouchAction.java index f2a3e08be..d82b47b1f 100644 --- a/src/main/java/io/appium/java_client/MultiTouchAction.java +++ b/src/main/java/io/appium/java_client/MultiTouchAction.java @@ -16,9 +16,12 @@ package io.appium.java_client; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.stream.Collectors.toList; /** * Used for Webdriver 3 multi-touch gestures @@ -36,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<>(); } /** @@ -62,28 +75,20 @@ public MultiTouchAction add(TouchAction action) { * Perform the multi-touch action on the mobile performsTouchActions. */ public MultiTouchAction perform() { - int size = actions.build().size(); - if (size > 1) { + checkArgument(!actions.isEmpty(), + "MultiTouch action must have at least one TouchAction added before it can be performed"); + if (actions.size() > 1) { performsTouchActions.performMultiTouchAction(this); - } else if (size == 1) { - //android doesn't like having multi-touch actions with only a single TouchAction... - performsTouchActions.performTouchAction(actions.build().get(0)); - } else { - throw new MissingParameterException( - "MultiTouch action must have at least one TouchAction " - + "added before it can be performed"); - } + return this; + } //android doesn't like having multi-touch actions with only a single TouchAction... + performsTouchActions.performTouchAction(actions.get(0)); return this; } - protected ImmutableMap> getParameters() { - ImmutableList.Builder listOfActionChains = ImmutableList.builder(); - ImmutableList touchActions = actions.build(); - - touchActions.forEach(action -> { - listOfActionChains.add(action.getParameters().get("actions")); - }); - return ImmutableMap.of("actions", listOfActionChains.build()); + protected Map> getParameters() { + return Map.of("actions", + actions.stream().map(touchAction -> touchAction.getParameters().get("actions")).collect(toList()) + ); } /** @@ -92,7 +97,7 @@ protected ImmutableMap> 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 2f01246eb..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 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; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - +/** + * 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,8 +48,9 @@ public interface PerformsTouchActions extends ExecutesMethod { * touch actions to perform * @return the same touch action object */ + @Deprecated default TouchAction performTouchAction(TouchAction touchAction) { - ImmutableMap> parameters = touchAction.getParameters(); + Map> parameters = touchAction.getParameters(); execute(PERFORM_TOUCH_ACTION, parameters); return touchAction.clearParameters(); } @@ -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) { - ImmutableMap> parameters = multiAction.getParameters(); + @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/PressesKeyCode.java b/src/main/java/io/appium/java_client/PressesKeyCode.java deleted file mode 100644 index 4ad06f104..000000000 --- a/src/main/java/io/appium/java_client/PressesKeyCode.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 static io.appium.java_client.MobileCommand.longPressKeyCodeCommand; -import static io.appium.java_client.MobileCommand.pressKeyCodeCommand; - -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/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 1c8675eb7..f51add334 100644 --- a/src/main/java/io/appium/java_client/ScreenshotState.java +++ b/src/main/java/io/appium/java_client/ScreenshotState.java @@ -16,35 +16,42 @@ package io.appium.java_client; -import static nu.pattern.OpenCV.loadShared; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; -import org.opencv.core.Core; -import org.opencv.core.CvType; -import org.opencv.core.Mat; -import org.opencv.core.Size; -import org.opencv.imgproc.Imgproc; - -import java.awt.AlphaComposite; -import java.awt.Graphics2D; +import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.time.Duration; import java.time.LocalDateTime; -import java.util.Optional; +import java.util.Base64; import java.util.function.Function; import java.util.function.Supplier; +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + +@Accessors(chain = true) public class ScreenshotState { private static final Duration DEFAULT_INTERVAL_MS = Duration.ofMillis(500); - private Optional previousScreenshot = Optional.empty(); - private Supplier stateProvider; - - private Duration comparisonInterval = DEFAULT_INTERVAL_MS; + private BufferedImage previousScreenshot; + private final Supplier stateProvider; + private final ComparesImages comparator; + /** + * Gets the interval value in ms between similarity verification rounds in verify* methods. + * + * @param comparisonInterval interval value. 500 ms by default + * @return current interval value in ms + */ + @Getter(AccessLevel.PUBLIC) @Setter(AccessLevel.PUBLIC) private Duration comparisonInterval = DEFAULT_INTERVAL_MS; /** - * The class constructor accepts single argument, which is - * lambda function, that provides the screenshot of the necessary + * The class constructor accepts two arguments. The first one is image comparator, the second + * parameter is lambda function, that provides the screenshot of the necessary * screen area to be verified for similarity. * This lambda method is NOT called upon class creation. * One has to invoke {@link #remember()} method in order to call it. @@ -70,31 +77,18 @@ public class ScreenshotState { * } * * + * @param comparator image comparator * @param stateProvider lambda function, which returns a screenshot for further comparison */ - public ScreenshotState(Supplier stateProvider) { + public ScreenshotState(ComparesImages comparator, Supplier stateProvider) { + this.comparator = requireNonNull(comparator); this.stateProvider = stateProvider; } - /** - * Gets the interval value in ms between similarity verification rounds in verify* methods. - * - * @return current interval value in ms - */ - public Duration getComparisonInterval() { - return comparisonInterval; + public ScreenshotState(ComparesImages comparator) { + this(comparator, null); } - /** - * Sets the interval between similarity verification rounds in verify* methods. - * - * @param comparisonInterval interval value. 500 ms by default - * @return self instance for chaining - */ - public ScreenshotState setComparisonInterval(Duration comparisonInterval) { - this.comparisonInterval = comparisonInterval; - return this; - } /** * Call this method to save the initial screenshot state. @@ -103,7 +97,7 @@ public ScreenshotState setComparisonInterval(Duration comparisonInterval) { * @return self instance for chaining */ public ScreenshotState remember() { - this.previousScreenshot = Optional.of(stateProvider.get()); + this.previousScreenshot = stateProvider.get(); return this; } @@ -116,7 +110,7 @@ public ScreenshotState remember() { * @return self instance for chaining */ public ScreenshotState remember(BufferedImage customInitialState) { - this.previousScreenshot = Optional.of(customInitialState); + this.previousScreenshot = requireNonNull(customInitialState); return this; } @@ -134,7 +128,7 @@ public static class ScreenshotComparisonError extends RuntimeException { public static class ScreenshotComparisonTimeout extends RuntimeException { private static final long serialVersionUID = 6336247721154252476L; - private double currentScore = Double.NaN; + private final double currentScore; ScreenshotComparisonTimeout(String message, double currentScore) { super(message); @@ -147,17 +141,13 @@ public double getCurrentScore() { } private ScreenshotState checkState(Function checkerFunc, Duration timeout) { - return checkState(checkerFunc, timeout, ResizeMode.NO_RESIZE); - } - - private ScreenshotState checkState(Function checkerFunc, Duration timeout, ResizeMode resizeMode) { final LocalDateTime started = LocalDateTime.now(); double score; do { final BufferedImage currentState = stateProvider.get(); - score = getOverlapScore(this.previousScreenshot + score = getOverlapScore(ofNullable(this.previousScreenshot) .orElseThrow(() -> new ScreenshotComparisonError("Initial screenshot state is not set. " - + "Nothing to compare")), currentState, resizeMode); + + "Nothing to compare")), currentState); if (checkerFunc.apply(score)) { return this; } @@ -185,26 +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); - } - - /** - * Verifies whether the state of the screenshot provided by stateProvider lambda function - * is changed within the given timeout. - * - * @param timeout timeout value - * @param minScore the value in range (0.0, 1.0) - * @param resizeMode one of ResizeMode enum values. - * Set it to a value different from NO_RESIZE - * if the actual screenshot is expected to have different - * dimensions in comparison to the previously remembered one - * @return self instance for chaining - * @throws ScreenshotComparisonTimeout if the calculated score is still - * greater or equal to the given score after timeout happens - * @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet - */ - public ScreenshotState verifyChanged(Duration timeout, double minScore, ResizeMode resizeMode) { - return checkState((x) -> x < minScore, timeout, resizeMode); + return checkState(x -> x < minScore, timeout); } /** @@ -219,115 +190,31 @@ public ScreenshotState verifyChanged(Duration timeout, double minScore, ResizeMo * @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet */ public ScreenshotState verifyNotChanged(Duration timeout, double minScore) { - return checkState((x) -> x >= minScore, timeout); - } - - /** - * Verifies whether the state of the screenshot provided by stateProvider lambda function - * is changed within the given timeout. - * - * @param timeout timeout value - * @param minScore the value in range (0.0, 1.0) - * @param resizeMode one of ResizeMode enum values. - * Set it to a value different from NO_RESIZE - * if the actual screenshot is expected to have different - * dimensions in comparison to the previously remembered one - * @return self instance for chaining - * @throws ScreenshotComparisonTimeout if the calculated score is still - * less than the given score after timeout happens - * @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet - */ - public ScreenshotState verifyNotChanged(Duration timeout, double minScore, ResizeMode resizeMode) { - return checkState((x) -> x >= minScore, timeout, resizeMode); - } - - private static Mat prepareImageForComparison(BufferedImage srcImage) { - final BufferedImage normalizedBitmap = new BufferedImage(srcImage.getWidth(), srcImage.getHeight(), - BufferedImage.TYPE_3BYTE_BGR); - final Graphics2D g = normalizedBitmap.createGraphics(); - try { - g.setComposite(AlphaComposite.Src); - g.drawImage(srcImage, 0, 0, null); - } finally { - g.dispose(); - } - final byte[] pixels = ((DataBufferByte) normalizedBitmap.getRaster().getDataBuffer()).getData(); - final Mat result = new Mat(normalizedBitmap.getHeight(), normalizedBitmap.getWidth(), CvType.CV_8UC3); - result.put(0, 0, pixels); - return result; - } - - private static Mat resizeFirstMatrixToSecondMatrixResolution(Mat first, Mat second) { - if (first.width() != second.width() || first.height() != second.height()) { - final Mat result = new Mat(); - final Size sz = new Size(second.width(), second.height()); - Imgproc.resize(first, result, sz); - return result; - } - return first; - } - - /** - * A shortcut to {@link #getOverlapScore(BufferedImage, BufferedImage, ResizeMode)} method - * for the case if both reference and template images are expected to have the same dimensions. - * - * @param refImage reference image - * @param tplImage template - * @return similarity score value in range (-1.0, 1.0). 1.0 is returned if the images are equal - * @throws ScreenshotComparisonError if provided images are not valid or have different resolution - */ - public static double getOverlapScore(BufferedImage refImage, BufferedImage tplImage) { - return getOverlapScore(refImage, tplImage, ResizeMode.NO_RESIZE); + return checkState(x -> x >= minScore, timeout); } /** * Compares two valid java bitmaps and calculates similarity score between them. + * Both images are expected to be of the same size/resolution. The method + * implicitly invokes {@link ComparesImages#getImagesSimilarity(byte[], byte[])}. * * @param refImage reference image * @param tplImage template - * @param resizeMode one of possible enum values. Set it either to TEMPLATE_TO_REFERENCE_RESOLUTION or - * REFERENCE_TO_TEMPLATE_RESOLUTION if given bitmaps have different dimensions - * @return similarity score value in range (-1.0, 1.0). 1.0 is returned if the images are equal + * @return similarity score value in range (-1.0, 1.0]. 1.0 is returned if the images are equal * @throws ScreenshotComparisonError if provided images are not valid or have - * different resolution, but resizeMode has been set to NO_RESIZE + * different resolution */ - public static double getOverlapScore(BufferedImage refImage, BufferedImage tplImage, ResizeMode resizeMode) { - Mat ref = prepareImageForComparison(refImage); - if (ref.empty()) { - throw new ScreenshotComparisonError("Reference image cannot be converted for further comparison"); - } - Mat tpl = prepareImageForComparison(tplImage); - if (tpl.empty()) { - throw new ScreenshotComparisonError("Template image cannot be converted for further comparison"); + public double getOverlapScore(BufferedImage refImage, BufferedImage tplImage) { + try (ByteArrayOutputStream img1 = new ByteArrayOutputStream(); + ByteArrayOutputStream img2 = new ByteArrayOutputStream()) { + ImageIO.write(refImage, "png", img1); + ImageIO.write(tplImage, "png", img2); + return comparator + .getImagesSimilarity(Base64.getEncoder().encode(img1.toByteArray()), + Base64.getEncoder().encode(img2.toByteArray())) + .getScore(); + } catch (IOException e) { + throw new ScreenshotComparisonError(e); } - switch (resizeMode) { - case TEMPLATE_TO_REFERENCE_RESOLUTION: - tpl = resizeFirstMatrixToSecondMatrixResolution(tpl, ref); - break; - case REFERENCE_TO_TEMPLATE_RESOLUTION: - ref = resizeFirstMatrixToSecondMatrixResolution(ref, tpl); - break; - default: - // do nothing - } - - if (ref.width() != tpl.width() || ref.height() != tpl.height()) { - throw new ScreenshotComparisonError( - "Resolutions of template and reference images are expected to be equal. " - + "Try different resizeMode value." - ); - } - - Mat res = new Mat(ref.rows() - tpl.rows() + 1, ref.cols() - tpl.cols() + 1, CvType.CV_32FC1); - Imgproc.matchTemplate(ref, tpl, res, Imgproc.TM_CCOEFF_NORMED); - return Core.minMaxLoc(res).maxVal; - } - - public enum ResizeMode { - NO_RESIZE, TEMPLATE_TO_REFERENCE_RESOLUTION, REFERENCE_TO_TEMPLATE_RESOLUTION - } - - static { - loadShared(); } } diff --git a/src/main/java/io/appium/java_client/Setting.java b/src/main/java/io/appium/java_client/Setting.java index e88acc714..cc1f44cd1 100644 --- a/src/main/java/io/appium/java_client/Setting.java +++ b/src/main/java/io/appium/java_client/Setting.java @@ -18,23 +18,54 @@ /** * Enums defining constants for Appium Settings which can be set and toggled during a test session. + *
+ * + * https://appium.io/docs/en/advanced-concepts/settings/ */ public enum Setting { + // Android IGNORE_UNIMPORTANT_VIEWS("ignoreUnimportantViews"), - WAIT_FOR_IDLE_TIMEOUT("setWaitForIdleTimeout"), - WAIT_FOR_SELECTOR_TIMEOUT("setWaitForSelectorTimeout"), - WAIT_SCROLL_ACKNOWLEDGMENT_TIMEOUT("setScrollAcknowledgmentTimeout"), - WAIT_ACTION_ACKNOWLEDGMENT_TIMEOUT("setActionAcknowledgmentTimeout"), - KEY_INJECTION_DELAY("setKeyInjectionDelay"), - NATIVE_WEB_TAP("nativeWebTap"); + WAIT_FOR_IDLE_TIMEOUT("waitForIdleTimeout"), + WAIT_FOR_SELECTOR_TIMEOUT("waitForSelectorTimeout"), + WAIT_SCROLL_ACKNOWLEDGMENT_TIMEOUT("scrollAcknowledgmentTimeout"), + WAIT_ACTION_ACKNOWLEDGMENT_TIMEOUT("actionAcknowledgmentTimeout"), + ALLOW_INVISIBLE_ELEMENTS("allowInvisibleElements"), + ENABLE_NOTIFICATION_LISTENER("enableNotificationListener"), + NORMALIZE_TAG_NAMES("normalizeTagNames"), + KEY_INJECTION_DELAY("keyInjectionDelay"), + SHUTDOWN_ON_POWER_DISCONNECT("shutdownOnPowerDisconnect"), + TRACK_SCROLL_EVENTS("trackScrollEvents"), + // iOS + MJPEG_SERVER_SCREENSHOT_QUALITY("mjpegServerScreenshotQuality"), + MJPEG_SERVER_FRAMERATE("mjpegServerFramerate"), + SCREENSHOT_QUALITY("screenshotQuality"), + NATIVE_WEB_TAP("nativeWebTap"), + 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"), + // All platforms + IMAGE_ELEMENT_TAP_STRATEGY("imageElementTapStrategy"), + IMAGE_MATCH_THRESHOLD("imageMatchThreshold"), + FIX_IMAGE_FIND_SCREENSHOT_DIMENSIONS("fixImageFindScreenshotDims"), + FIX_IMAGE_TEMPLATE_SIZE("fixImageTemplateSize"), + CHECK_IMAGE_ELEMENT_STALENESS("checkForImageElementStaleness"), + UPDATE_IMAGE_ELEMENT_POSITION("autoUpdateImageElementPosition"), + FIX_IMAGE_TEMPLATE_SCALE("fixImageTemplateScale"), + DEFAULT_IMAGE_TEMPLATE_SCALE("defaultImageTemplateScale"), + GET_MATCHED_IMAGE_RESULT("getMatchedImageResult"); - private String name; + private final String name; Setting(String name) { 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 ca30fde11..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,21 @@ package io.appium.java_client; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -import org.openqa.selenium.WebElement; -import org.openqa.selenium.internal.HasIdentity; - -import java.time.Duration; +import io.appium.java_client.touch.ActionOptions; +import io.appium.java_client.touch.LongPressOptions; +import io.appium.java_client.touch.TapOptions; +import io.appium.java_client.touch.WaitOptions; +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 @@ -33,58 +41,37 @@ * 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. */ -public class TouchAction implements PerformsActions { +@Deprecated +public class TouchAction> implements PerformsActions { - protected ImmutableList.Builder parameterBuilder; + protected List parameters; private PerformsTouchActions performsTouchActions; public TouchAction(PerformsTouchActions performsTouchActions) { - this.performsTouchActions = performsTouchActions; - parameterBuilder = ImmutableList.builder(); - } - - /** - * Press on the center of an element. - * - * @param el element to press on. - * @return this TouchAction, for chaining. - */ - public TouchAction press(WebElement el) { - ActionParameter action = new ActionParameter("press", (HasIdentity) el); - parameterBuilder.add(action); - return this; - } - - /** - * Press on an absolute position on the screen. - * - * @param x x coordinate. - * @param y y coordinate. - * @return this TouchAction, for chaining. - */ - public TouchAction press(int x, int y) { - ActionParameter action = new ActionParameter("press"); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; + this.performsTouchActions = requireNonNull(performsTouchActions); + parameters = new ArrayList<>(); } /** - * Press on an element, offset from upper left corner by a number of pixels. + * Press action on the screen. * - * @param el element to press on. - * @param x x offset. - * @param y y offset. + * @param pressOptions see {@link PointOption} and {@link ElementOption}. * @return this TouchAction, for chaining. */ - public TouchAction press(WebElement el, int x, int y) { - ActionParameter action = new ActionParameter("press", (HasIdentity) el); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; + public T press(PointOption pressOptions) { + parameters.add(new ActionParameter("press", pressOptions)); + //noinspection unchecked + return (T) this; } /** @@ -92,98 +79,48 @@ public TouchAction press(WebElement el, int x, int y) { * * @return this TouchAction, for chaining. */ - public TouchAction release() { - ActionParameter action = new ActionParameter("release"); - parameterBuilder.add(action); - return this; - } - - /** - * Move current touch to center of an element. - * - * @param el element to move to. - * @return this TouchAction, for chaining. - */ - public TouchAction moveTo(WebElement el) { - ActionParameter action = new ActionParameter("moveTo", (HasIdentity) el); - parameterBuilder.add(action); - return this; - } - - /** - * Move current touch to a new position relative to the current position on - * the screen. If the current position of this TouchAction is (xOld, yOld), - * then this method will move the TouchAction to (xOld + x, yOld + y). - * - * @param x change in x coordinate to move through. - * @param y change in y coordinate to move through. - * @return this TouchAction, for chaining. - */ - public TouchAction moveTo(int x, int y) { - ActionParameter action = new ActionParameter("moveTo"); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; + public T release() { + parameters.add(new ActionParameter("release")); + //noinspection unchecked + return (T) this; } /** - * Move current touch to an element, offset from upper left corner. + * Moves current touch to a new position. * - * @param el element to move current touch to. - * @param x x offset. - * @param y y offset. + * @param moveToOptions see {@link PointOption} and {@link ElementOption} + * Important: some older Appium drivers releases have a bug when moveTo + * coordinates are calculated as relative to the recent pointer position + * in the chain instead of being absolute. + * @see Appium Issue #7486 + * for more details. * @return this TouchAction, for chaining. */ - public TouchAction moveTo(WebElement el, int x, int y) { - ActionParameter action = new ActionParameter("moveTo", (HasIdentity) el); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; + public T moveTo(PointOption moveToOptions) { + parameters.add(new ActionParameter("moveTo", moveToOptions)); + return (T) this; } /** - * Tap the center of an element. + * Tap on an element. * - * @param el element to tap. + * @param tapOptions see {@link TapOptions}. * @return this TouchAction, for chaining. */ - public TouchAction tap(WebElement el) { - ActionParameter action = new ActionParameter("tap", (HasIdentity) el); - parameterBuilder.add(action); - return this; + public T tap(TapOptions tapOptions) { + parameters.add(new ActionParameter("tap", tapOptions)); + return (T) this; } /** - * Tap an absolute position on the screen. + * Tap on a position. * - * @param x x coordinate. - * @param y y coordinate. + * @param tapOptions see {@link PointOption} and {@link ElementOption} * @return this TouchAction, for chaining. */ - public TouchAction tap(int x, int y) { - ActionParameter action = new ActionParameter("tap"); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; - } - - /** - * Tap an element, offset from upper left corner. - * - * @param el element to tap. - * @param x x offset. - * @param y y offset. - * @return this TouchAction, for chaining. - */ - public TouchAction tap(WebElement el, int x, int y) { - ActionParameter action = new ActionParameter("tap", (HasIdentity) el); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; + public T tap(PointOption tapOptions) { + parameters.add(new ActionParameter("tap", tapOptions)); + return (T) this; } /** @@ -191,128 +128,53 @@ public TouchAction tap(WebElement el, int x, int y) { * * @return this TouchAction, for chaining. */ - public TouchAction waitAction() { - ActionParameter action = new ActionParameter("wait"); - parameterBuilder.add(action); - return this; + public T waitAction() { + parameters.add(new ActionParameter("wait")); + //noinspection unchecked + return (T) this; } /** * Waits for specified amount of time to pass before continue to next touch action. * - * @param duration of the wait action. Minimum time reolution unit is one millisecond. - * @return this TouchAction, for chaining. - */ - public TouchAction waitAction(Duration duration) { - ActionParameter action = new ActionParameter("wait"); - action.addParameter("ms", duration.toMillis()); - parameterBuilder.add(action); - return this; - } - - /** - * Press and hold the at the center of an element until the contextmenu event has fired. - * - * @param el element to long-press. - * @return this TouchAction, for chaining. - */ - public TouchAction longPress(WebElement el) { - ActionParameter action = new ActionParameter("longPress", (HasIdentity) el); - parameterBuilder.add(action); - return this; - } - - /** - * Press and hold the at the center of an element until the contextmenu event has fired. - * - * @param el element to long-press. - * @param duration of the long-press. Minimum time resolution unit is one millisecond. + * @param waitOptions see {@link WaitOptions}. * @return this TouchAction, for chaining. */ - public TouchAction longPress(WebElement el, Duration duration) { - ActionParameter action = new ActionParameter("longPress", (HasIdentity) el); - action.addParameter("duration", duration.toMillis()); - parameterBuilder.add(action); - return this; + public T waitAction(WaitOptions waitOptions) { + parameters.add(new ActionParameter("wait", waitOptions)); + //noinspection unchecked + return (T) this; } /** - * Press and hold the at an absolute position on the screen - * until the contextmenu event has fired. + * Press and hold the at the center of an element until the context menu event has fired. * - * @param x x coordinate. - * @param y y coordinate. + * @param longPressOptions see {@link LongPressOptions}. * @return this TouchAction, for chaining. */ - public TouchAction longPress(int x, int y) { - ActionParameter action = new ActionParameter("longPress"); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; + public T longPress(LongPressOptions longPressOptions) { + parameters.add(new ActionParameter("longPress", longPressOptions)); + //noinspection unchecked + return (T) this; } /** - * Press and hold the at an absolute position on the screen until the - * contextmenu event has fired. + * Press and hold the at the center of an element until the context menu event has fired. * - * @param x x coordinate. - * @param y y coordinate. - * @param duration of the long-press. Minimum time resolution unit is one millisecond. + * @param longPressOptions see {@link PointOption} and {@link ElementOption}. * @return this TouchAction, for chaining. */ - public TouchAction longPress(int x, int y, Duration duration) { - ActionParameter action = new ActionParameter("longPress"); - action.addParameter("x", x); - action.addParameter("y", y); - action.addParameter("duration", duration.toMillis()); - parameterBuilder.add(action); - return this; - } - - - /** - * Press and hold the at an elements upper-left corner, offset by the given amount, - * until the contextmenu event has fired. - * - * @param el element to long-press. - * @param x x offset. - * @param y y offset. - * @return this TouchAction, for chaining. - */ - public TouchAction longPress(WebElement el, int x, int y) { - ActionParameter action = new ActionParameter("longPress", (HasIdentity) el); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); - return this; - } - - /** - * Press and hold the at an elements upper-left corner, offset by the - * given amount, until the contextmenu event has fired. - * - * @param el element to long-press. - * @param x x offset. - * @param y y offset. - * @param duration of the long-press. Minimum time resolution unit is one millisecond. - * @return this TouchAction, for chaining. - */ - public TouchAction longPress(WebElement el, int x, int y, Duration duration) { - ActionParameter action = new ActionParameter("longPress", (HasIdentity) el); - action.addParameter("x", x); - action.addParameter("y", y); - action.addParameter("duration", duration.toMillis()); - parameterBuilder.add(action); - return this; + public T longPress(PointOption longPressOptions) { + parameters.add(new ActionParameter("longPress", longPressOptions)); + //noinspection unchecked + return (T) this; } /** * 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(); } @@ -321,9 +183,10 @@ public void cancel() { * * @return this TouchAction, for possible segmented-touches. */ - public TouchAction perform() { + public T perform() { performsTouchActions.performTouchAction(this); - return this; + //noinspection unchecked + return (T) this; } /** @@ -331,13 +194,10 @@ public TouchAction perform() { * * @return A map of parameters for this touch action to pass as part of mjsonwp. */ - protected ImmutableMap> getParameters() { - - ImmutableList.Builder parameters = ImmutableList.builder(); - ImmutableList actionList = parameterBuilder.build(); - - actionList.forEach(action -> parameters.add(action.getParameterMap())); - return ImmutableMap.of("actions", parameters.build()); + protected Map> getParameters() { + return Map.of("actions", + parameters.stream().map(ActionParameter::getParameterMap).collect(toList()) + ); } /** @@ -345,37 +205,36 @@ protected ImmutableMap> getParameters() { * * @return this TouchAction, for possible segmented-touches. */ - protected TouchAction clearParameters() { - parameterBuilder = ImmutableList.builder(); - return this; + protected T clearParameters() { + parameters = new ArrayList<>(); + //noinspection unchecked + return (T) this; } /** * 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(); - } - - public ActionParameter(String actionName, HasIdentity el) { - this.actionName = actionName; - optionsBuilder = ImmutableMap.builder(); - addParameter("element", el.getId()); + options = new HashMap<>(); } - public ImmutableMap getParameterMap() { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("action", actionName).put("options", optionsBuilder.build()); - return builder.build(); + public ActionParameter(String actionName, ActionOptions opts) { + this(actionName); + requireNonNull(opts); + //noinspection unchecked + options.putAll(opts.build()); } - public void addParameter(String name, Object value) { - optionsBuilder.put(name, value); + 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 790fad11b..34821f8d4 100644 --- a/src/main/java/io/appium/java_client/android/Activity.java +++ b/src/main/java/io/appium/java_client/android/Activity.java @@ -1,11 +1,16 @@ package io.appium.java_client.android; +import lombok.Data; +import lombok.experimental.Accessors; + import static com.google.common.base.Preconditions.checkArgument; -import static org.apache.commons.lang3.StringUtils.isBlank; +import static com.google.common.base.Strings.isNullOrEmpty; /** * This is a simple POJO class to support the {@link StartsActivity}. */ +@Accessors(chain = true) +@Data public class Activity { private final String appPackage; private final String appActivity; @@ -24,179 +29,13 @@ 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; this.stopApp = true; } - /** - * Gets the app package value. - * - * @return The app package value. - */ - public String getAppPackage() { - return appPackage; - } - - /** - * Gets the app activity value. - * - * @return The app activity value. - */ - public String getAppActivity() { - return appActivity; - } - - /** - * Gets the app wait package value. - * - * @return The app wait package value. - */ - public String getAppWaitPackage() { - return appWaitPackage; - } - - /** - * Sets the app wait package value. - * - * @param appWaitPackage The app wait package value. - * @return self reference - */ - public Activity setAppWaitPackage(String appWaitPackage) { - this.appWaitPackage = appWaitPackage; - return this; - } - - /** - * Gets the app wait activity value. - * - * @return The app wait activity value. - */ - public String getAppWaitActivity() { - return appWaitActivity; - } - - /** - * Sets the app wait activity value. - * - * @param appWaitActivity The app wait activity value. - * @return self reference - */ - public Activity setAppWaitActivity(String appWaitActivity) { - this.appWaitActivity = appWaitActivity; - return this; - } - - /** - * Gets the intent action value. - * - * @return The intent action value. - */ - public String getIntentAction() { - return intentAction; - } - - /** - * Sets the intent action value. - * - * @param intentAction The intent action value. - * @return self reference - */ - public Activity setIntentAction(String intentAction) { - this.intentAction = intentAction; - return this; - } - - /** - * Gets the intent category value. - * - * @return The intent category value. - */ - public String getIntentCategory() { - return intentCategory; - } - - /** - * Sets the intent category value. - * - * @param intentCategory The intent category value. - * @return self reference - */ - public Activity setIntentCategory(String intentCategory) { - this.intentCategory = intentCategory; - return this; - } - - /** - * Gets the intent flags value. - * - * @return The intent flags value. - */ - public String getIntentFlags() { - return intentFlags; - } - - /** - * Sets the intent flags value. - * - * @param intentFlags The intent flags value. - * @return self reference - */ - public Activity setIntentFlags(String intentFlags) { - this.intentFlags = intentFlags; - return this; - } - - /** - * Gets the optional intent arguments value. - * - * @return The optional intent arguments value. - */ - public String getOptionalIntentArguments() { - return optionalIntentArguments; - } - - /** - * Sets the optional intent arguments value. - * - * @param optionalIntentArguments The optional intent arguments value. - * @return self reference - */ - public Activity setOptionalIntentArguments(String optionalIntentArguments) { - this.optionalIntentArguments = optionalIntentArguments; - return this; - } - - /** - * Gets the stop app value. - * - * @return The stop app value. - */ - public boolean isStopApp() { - return stopApp; - } - - /** - * Sets the stop app value. - * - * @param stopApp The stop app value. - * @return self reference - */ - public Activity setStopApp(boolean stopApp) { - this.stopApp = stopApp; - return this; - } - - @Override public String toString() { - return "Activity{" + "appPackage='" + appPackage + '\'' + ", appActivity='" + appActivity - + '\'' + ", appWaitPackage='" + appWaitPackage + '\'' + ", appWaitActivity='" - + appWaitActivity + '\'' + ", intentAction='" + intentAction + '\'' - + ", intentCategory='" + intentCategory + '\'' + ", intentFlags='" + intentFlags + '\'' - + ", optionalIntentArguments='" + optionalIntentArguments + '\'' + ", stopApp=" - + stopApp + '}'; - } } diff --git a/src/main/java/io/appium/java_client/android/AndroidBatteryInfo.java b/src/main/java/io/appium/java_client/android/AndroidBatteryInfo.java new file mode 100644 index 000000000..1c3ff2872 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AndroidBatteryInfo.java @@ -0,0 +1,34 @@ +package io.appium.java_client.android; + +import io.appium.java_client.battery.BatteryInfo; + +import java.util.Map; + +public class AndroidBatteryInfo extends BatteryInfo { + + public AndroidBatteryInfo(Map input) { + super(input); + } + + @SuppressWarnings("unchecked") + @Override + public BatteryState getState() { + final int state = ((Long) getInput().get("state")).intValue(); + switch (state) { + case 2: + return BatteryState.CHARGING; + case 3: + return BatteryState.DISCHARGING; + case 4: + return BatteryState.NOT_CHARGING; + case 5: + return BatteryState.FULL; + default: + return BatteryState.UNKNOWN; + } + } + + public enum BatteryState { + UNKNOWN, CHARGING, DISCHARGING, NOT_CHARGING, FULL + } +} 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 fc29f9ecb..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,161 +16,245 @@ 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 io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.FindsByAndroidUIAutomator; -import io.appium.java_client.PressesKeyCode; -import io.appium.java_client.remote.MobilePlatform; +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.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; /** - * @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} + * Android driver implementation. */ -public class AndroidDriver - extends AppiumDriver - implements PressesKeyCode, HasNetworkConnection, PushesFiles, StartsActivity, - FindsByAndroidUIAutomator, LocksAndroidDevice, HasAndroidSettings, HasDeviceDetails, - HasSupportedPerformanceDataType { +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 static final String ANDROID_PLATFORM = MobilePlatform.ANDROID; + private StringWebSocketClient logcatClient; /** - * @param executor is an instance of {@link org.openqa.selenium.remote.HttpCommandExecutor} + * Creates a new instance based on command {@code executor} and {@code capabilities}. + * + * @param executor is an instance of {@link HttpCommandExecutor} * or class that extends it. Default commands or another vendor-specific * commands may be specified there. - * @param capabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * @param capabilities take a look at {@link Capabilities} */ public AndroidDriver(HttpCommandExecutor executor, Capabilities capabilities) { - super(executor, substituteMobilePlatform(capabilities, ANDROID_PLATFORM)); + super(executor, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param remoteAddress is the address of remotely/locally - * started Appium server - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium server URL and {@code capabilities}. + * + * @param remoteAddress is the address of remotely/locally started Appium server + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(URL remoteAddress, Capabilities desiredCapabilities) { - super(remoteAddress, substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param remoteAddress is the address of remotely/locally - * started Appium server - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium server URL, HTTP client factory and {@code capabilities}. + * + * @param remoteAddress is the address of remotely/locally started Appium server + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(remoteAddress, httpClientFactory, - substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver( + URL remoteAddress, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param service take a look - * at {@link io.appium.java_client.service.local.AppiumDriverLocalService} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium driver local service and {@code capabilities}. + * + * @param service take a look at {@link AppiumDriverLocalService} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(AppiumDriverLocalService service, Capabilities desiredCapabilities) { - super(service, substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param service take a look - * at {@link io.appium.java_client.service.local.AppiumDriverLocalService} - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium driver local service, HTTP client factory and {@code capabilities}. + * + * @param service take a look at {@link AppiumDriverLocalService} + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(service, httpClientFactory, - substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver( + AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param builder take a look - * at {@link io.appium.java_client.service.local.AppiumServiceBuilder} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium service builder and {@code capabilities}. + * + * @param builder take a look at {@link AppiumServiceBuilder} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) { - super(builder, substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param builder take a look - * at {@link io.appium.java_client.service.local.AppiumServiceBuilder} - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium service builder, HTTP client factory and {@code capabilities}. + * + * @param builder take a look at {@link AppiumServiceBuilder} + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param capabilities take a look at {@link Capabilities} */ public AndroidDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(builder, httpClientFactory, - substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + Capabilities capabilities) { + super(builder, httpClientFactory, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on HTTP client factory and {@code capabilities}. + * + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param capabilities take a look at {@link Capabilities} */ - public AndroidDriver(HttpClient.Factory httpClientFactory, Capabilities desiredCapabilities) { - super(httpClientFactory, substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities) { + super(httpClientFactory, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.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} + * + */ + public AndroidDriver(ClientConfig clientConfig, Capabilities capabilities) { + super(AppiumClientConfig.fromClientConfig(clientConfig), ensurePlatformName(capabilities, + ANDROID_PLATFORM)); + } + + /** + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * UiAutomator2Options options = new UiAutomator2Options();
+     * AndroidDriver driver = new AndroidDriver(appiumClientConfig, options);
+     *
+     * 
+ * + * @param appiumClientConfig take a look at {@link AppiumClientConfig} + * @param capabilities take a look at {@link Capabilities} + * */ - public AndroidDriver(Capabilities desiredCapabilities) { - super(substituteMobilePlatform(desiredCapabilities, ANDROID_PLATFORM)); + public AndroidDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * Get test-coverage data. + * Creates a new instance based on {@code capabilities}. * - * @param intent intent to broadcast. - * @param path path to .ec file. + * @param capabilities take a look at {@link Capabilities} */ - public void endTestCoverage(String intent, String path) { - CommandExecutionHelper.execute(this, endTestCoverageCommand(intent, path)); + public AndroidDriver(Capabilities capabilities) { + super(ensurePlatformName(capabilities, ANDROID_PLATFORM)); } /** - * Open the notification shade, on Android devices. + * This is a special constructor used to connect to a running driver instance. + * It does not do any necessary verifications, but rather assumes the given + * driver session is already running at `remoteSessionAddress`. + * The maintenance of driver state(s) is the caller's responsibility. + * !!! This API is supposed to be used for **debugging purposes only**. + * + * @param remoteSessionAddress The address of the **running** session including the session identifier. + * @param automationName The name of the target automation. */ - public void openNotifications() { - CommandExecutionHelper.execute(this, openNotificationsCommand()); + public AndroidDriver(URL remoteSessionAddress, String automationName) { + super(remoteSessionAddress, ANDROID_PLATFORM, automationName); } - public void toggleLocationServices() { - CommandExecutionHelper.execute(this, toggleLocationServicesCommand()); + @Override + public AndroidBatteryInfo getBatteryInfo() { + return new AndroidBatteryInfo(CommandExecutionHelper.executeScript(this, "mobile: batteryInfo")); } + @Override + public synchronized StringWebSocketClient getLogcatClient() { + if (logcatClient == null) { + logcatClient = new StringWebSocketClient(getHttpClient()); + } + return logcatClient; + } } diff --git a/src/main/java/io/appium/java_client/android/AndroidKeyCode.java b/src/main/java/io/appium/java_client/android/AndroidKeyCode.java deleted file mode 100644 index 612485610..000000000 --- a/src/main/java/io/appium/java_client/android/AndroidKeyCode.java +++ /dev/null @@ -1,1377 +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; - -/** - * Some common key codes for Android Key Events. - */ -public interface AndroidKeyCode { - - int BACK = 4; - int BACKSPACE = 67; - int DEL = 67; - int ENTER = 66; - int HOME = 3; - int MENU = 82; - int SETTINGS = 176; - int SPACE = 62; - - // getAction() value: the key has been pressed down. - // (0x00000000) - int ACTION_DOWN = 0; - - // getAction() value: multiple duplicate key events have occurred in a row, or a complex string - // is being delivered. If the key code is not {#link KEYCODE_UNKNOWN then the - // {#link getRepeatCount() method returns the number of times the given key code should be - // executed. Otherwise, if the key code is KEYCODE_UNKNOWN, then this is a sequence of - // characters as returned by getCharacters(). - // (0x00000002) - int ACTION_MULTIPLE = 2; - - // getAction() value: the key has been released. - // (0x00000001) - int ACTION_UP = 1; - - // When associated with up key events, this indicates that the key press has been canceled. - // Typically this is used with virtual touch screen keys, where the user can slide from - // the virtual key area on to the display: in that case, the application will receive a - // canceled up event and should not perform the action normally associated with the key. - // Note that for this to work, the application can not perform an action for a key until - // it receives an up or the long press timeout has expired. - // (0x00000020) - int FLAG_CANCELED = 32; - - // Set when a key event has FLAG_CANCELED set because a long press action - // was executed while it was down. - // (0x00000100) - int FLAG_CANCELED_LONG_PRESS = 256; - - // This mask is used for compatibility, to identify enter keys that are coming - // from an IME whose enter key has been auto-labelled "next" or "done". - // This allows TextView to dispatch these as normal enter keys for old applications, - // but still do the appropriate action when receiving them. - // (0x00000010) - int FLAG_EDITOR_ACTION = 16; - - // Set when a key event has been synthesized to implement default behavior for an - // event that the application did not handle. - // Fallback key events are generated by unhandled trackball motions (to emulate a directional - // keypad) and by certain unhandled key presses that are declared in the key map - // (such as special function numeric keypad keys when numlock is off). - // (0x00000400) - int FLAG_FALLBACK = 1024; - - // This mask is set if an event was known to come from a trusted part of the system. - // That is, the event is known to come from the user, and could not - // have been spoofed by a third party component. - // (0x00000008) - int FLAG_FROM_SYSTEM = 8; - - // This mask is set if we don't want the key event to cause us to leave touch mode. - // (0x00000004) - int FLAG_KEEP_TOUCH_MODE = 4; - - // This flag is set for the first key repeat that occurs after the long press timeout. - // (0x00000080) - int FLAG_LONG_PRESS = 128; - - // This mask is set if the key event was generated by a software keyboard. - // (0x00000002) - int FLAG_SOFT_KEYBOARD = 2; - - // Set for ACTION_UP when this event's key code is still being tracked from its initial down. - // That is, somebody requested that tracking started on the key down and a - // long press has not caused the tracking to be canceled. - // (0x00000200) - int FLAG_TRACKING = 512; - - // This key event was generated by a virtual (on-screen) hard key area. - // Typically this is an area of the touchscreen, outside of the regular display, - // dedicated to "hardware" buttons. - // (0x00000040) - int FLAG_VIRTUAL_HARD_KEY = 64; - - // This constant was deprecated in API level 20. - // This flag will never be set by the system since the system consumes all wake keys itself. - // This mask is set if the device woke because of this key event. - // (0x00000001) - int FLAG_WOKE_HERE = 1; - - // Key code constant: '0' key. - // (0x00000007) - int KEYCODE_0 = 7; - - // Key code constant: '1' key. - // (0x00000008) - int KEYCODE_1 = 8; - - // Key code constant: '11' key. - // (0x000000e3) - int KEYCODE_11 = 227; - - // Key code constant: '12' key. - // (0x000000e4) - int KEYCODE_12 = 228; - - // Key code constant: '2' key. - // (0x00000009) - int KEYCODE_2 = 9; - - // Key code constant: '3' key. - // (0x0000000a) - int KEYCODE_3 = 10; - - // Key code constant: 3D Mode key. Toggles the display between 2D and 3D mode. - // (0x000000ce) - int KEYCODE_3D_MODE = 206; - - // Key code constant: '4' key. - // (0x0000000b) - int KEYCODE_4 = 11; - - // Key code constant: '5' key. - // (0x0000000c) - int KEYCODE_5 = 12; - - // Key code constant: '6' key. - // (0x0000000d) - int KEYCODE_6 = 13; - - // Key code constant: '7' key. - // (0x0000000e) - int KEYCODE_7 = 14; - - // Key code constant: '8' key. - // (0x0000000f) - int KEYCODE_8 = 15; - - // Key code constant: '9' key. - // (0x00000010) - int KEYCODE_9 = 16; - - // Key code constant: 'A' key. - // (0x0000001d) - int KEYCODE_A = 29; - - // Key code constant: Left Alt modifier key. - // (0x00000039) - int KEYCODE_ALT_LEFT = 57; - - // Key code constant: Right Alt modifier key. - // (0x0000003a) - int KEYCODE_ALT_RIGHT = 58; - - // Key code constant: ''' (apostrophe) key. - // (0x0000004b) - int KEYCODE_APOSTROPHE = 75; - - // Key code constant: App switch key. Should bring up the application switcher dialog. - // (0x000000bb) - int KEYCODE_APP_SWITCH = 187; - - // Key code constant: Assist key. Launches the global assist activity. Not delivered to - // applications. - // (0x000000db) - int KEYCODE_ASSIST = 219; - - // Key code constant: '@' key. - // (0x0000004d) - int KEYCODE_AT = 77; - - // Key code constant: A/V Receiver input key. On TV remotes, switches the input mode on an - // external A/V Receiver. - // (0x000000b6) - int KEYCODE_AVR_INPUT = 182; - - // Key code constant: A/V Receiver power key. On TV remotes, toggles the power on an external - // A/V Receiver. - // (0x000000b5) - int KEYCODE_AVR_POWER = 181; - - // Key code constant: 'B' key. - // (0x0000001e) - int KEYCODE_B = 30; - - // Key code constant: Back key. - // (0x00000004) - int KEYCODE_BACK = 4; - - // Key code constant: '\' key. - // (0x00000049) - int KEYCODE_BACKSLASH = 73; - - // Key code constant: Bookmark key. On some TV remotes, bookmarks content or web pages. - // (0x000000ae) - int KEYCODE_BOOKMARK = 174; - - // Key code constant: Break / Pause key. - // (0x00000079) - int KEYCODE_BREAK = 121; - - // Key code constant: Brightness Down key. Adjusts the screen brightness down. - // (0x000000dc) - int KEYCODE_BRIGHTNESS_DOWN = 220; - - // Key code constant: Brightness Up key. Adjusts the screen brightness up. - // (0x000000dd) - int KEYCODE_BRIGHTNESS_UP = 221; - - // Key code constant: Generic Game Pad Button #1. - // (0x000000bc) - int KEYCODE_BUTTON_1 = 188; - - // Key code constant: Generic Game Pad Button #10. - // (0x000000c5) - int KEYCODE_BUTTON_10 = 197; - - // Key code constant: Generic Game Pad Button #11. - // (0x000000c6) - int KEYCODE_BUTTON_11 = 198; - - // Key code constant: Generic Game Pad Button #12. - // (0x000000c7) - int KEYCODE_BUTTON_12 = 199; - - // Key code constant: Generic Game Pad Button #13. - // (0x000000c8) - int KEYCODE_BUTTON_13 = 200; - - // Key code constant: Generic Game Pad Button #14. - // (0x000000c9) - int KEYCODE_BUTTON_14 = 201; - - // Key code constant: Generic Game Pad Button #15. - // (0x000000ca) - int KEYCODE_BUTTON_15 = 202; - - // Key code constant: Generic Game Pad Button #16. - // (0x000000cb) - int KEYCODE_BUTTON_16 = 203; - - // Key code constant: Generic Game Pad Button #2. - // (0x000000bd) - int KEYCODE_BUTTON_2 = 189; - - // Key code constant: Generic Game Pad Button #3. - // (0x000000be) - int KEYCODE_BUTTON_3 = 190; - - // Key code constant: Generic Game Pad Button #4. - // (0x000000bf) - int KEYCODE_BUTTON_4 = 191; - - // Key code constant: Generic Game Pad Button #5. - // (0x000000c0) - int KEYCODE_BUTTON_5 = 192; - - // Key code constant: Generic Game Pad Button #6. - // (0x000000c1) - int KEYCODE_BUTTON_6 = 193; - - // Key code constant: Generic Game Pad Button #7. - // (0x000000c2) - int KEYCODE_BUTTON_7 = 194; - - // Key code constant: Generic Game Pad Button #8. - // (0x000000c3) - int KEYCODE_BUTTON_8 = 195; - - // Key code constant: Generic Game Pad Button #9. - // (0x000000c4) - int KEYCODE_BUTTON_9 = 196; - - // Key code constant: A Button key. On a game controller, the A button should be either the - // button labeled A or the first button on the bottom row of controller buttons. - // (0x00000060) - int KEYCODE_BUTTON_A = 96; - - // Key code constant: B Button key. On a game controller, the B button should be either the - // button labeled B or the second button on the bottom row of controller buttons. - // (0x00000061) - int KEYCODE_BUTTON_B = 97; - - // Key code constant: C Button key. On a game controller, the C button should be either - // the button labeled C or the third button on the bottom row of controller buttons. - // (0x00000062) - int KEYCODE_BUTTON_C = 98; - - // Key code constant: L1 Button key. On a game controller, the L1 button should be either - // the button labeled L1 (or L) or the top left trigger button. - // (0x00000066) - int KEYCODE_BUTTON_L1 = 102; - - // Key code constant: L2 Button key. On a game controller, the L2 button should be either - // the button labeled L2 or the bottom left trigger button. - // (0x00000068) - int KEYCODE_BUTTON_L2 = 104; - - // Key code constant: Mode Button key. On a game controller, the button labeled Mode. - // (0x0000006e) - int KEYCODE_BUTTON_MODE = 110; - - // Key code constant: R1 Button key. On a game controller, the R1 button should be either - // the button labeled R1 (or R) or the top right trigger button. - // (0x00000067) - int KEYCODE_BUTTON_R1 = 103; - - // Key code constant: R2 Button key. On a game controller, the R2 button - // should be either the button labeled R2 or the bottom right trigger button. - // (0x00000069) - int KEYCODE_BUTTON_R2 = 105; - - // Key code constant: Select Button key. On a game controller, the button labeled Select. - // (0x0000006d) - int KEYCODE_BUTTON_SELECT = 109; - - // Key code constant: Start Button key. On a game controller, the button labeled Start. - // (0x0000006c) - int KEYCODE_BUTTON_START = 108; - - // Key code constant: Left Thumb Button key. On a game controller, the left thumb button - // indicates that the left (or only) joystick is pressed. - // (0x0000006a) - int KEYCODE_BUTTON_THUMBL = 106; - - // Key code constant: Right Thumb Button key. On a game controller, the right thumb - // button indicates that the right joystick is pressed. - // (0x0000006b) - int KEYCODE_BUTTON_THUMBR = 107; - - // Key code constant: X Button key. On a game controller, the X button should be - // either the button labeled X or the first button on the upper row of controller buttons. - // (0x00000063) - int KEYCODE_BUTTON_X = 99; - - // Key code constant: Y Button key. On a game controller, the Y button should be either - // the button labeled Y or the second button on the upper row of controller buttons. - // (0x00000064) - int KEYCODE_BUTTON_Y = 100; - - // Key code constant: Z Button key. On a game controller, the Z button should be either - // the button labeled Z or the third button on the upper row of controller buttons. - // (0x00000065) - int KEYCODE_BUTTON_Z = 101; - - // Key code constant: 'C' key. - // (0x0000001f) - int KEYCODE_C = 31; - - // Key code constant: Calculator special function key. Used to launch a calculator application. - // (0x000000d2) - int KEYCODE_CALCULATOR = 210; - - // Key code constant: Calendar special function key. Used to launch a calendar application. - // (0x000000d0) - int KEYCODE_CALENDAR = 208; - - // Key code constant: Call key. - // (0x00000005) - int KEYCODE_CALL = 5; - - // Key code constant: Camera key. Used to launch a camera application or take pictures. - // (0x0000001b) - int KEYCODE_CAMERA = 27; - - // Key code constant: Caps Lock key. - // (0x00000073) - int KEYCODE_CAPS_LOCK = 115; - - // Key code constant: Toggle captions key. Switches the mode for closed-captioning text, - // for example during television shows. - // (0x000000af) - int KEYCODE_CAPTIONS = 175; - - // Key code constant: Channel down key. On TV remotes, decrements the television channel. - // (0x000000a7) - int KEYCODE_CHANNEL_DOWN = 167; - - // Key code constant: Channel up key. On TV remotes, increments the television channel. - // (0x000000a6) - int KEYCODE_CHANNEL_UP = 166; - - // Key code constant: Clear key. - // (0x0000001c) - int KEYCODE_CLEAR = 28; - - // Key code constant: ',' key. - // (0x00000037) - int KEYCODE_COMMA = 55; - - // Key code constant: Contacts special function key. Used to launch an address book application. - // (0x000000cf) - int KEYCODE_CONTACTS = 207; - - // Key code constant: Left Control modifier key. - // (0x00000071) - int KEYCODE_CTRL_LEFT = 113; - - // Key code constant: Right Control modifier key. - // (0x00000072) - int KEYCODE_CTRL_RIGHT = 114; - - // Key code constant: 'D' key. - // (0x00000020) - int KEYCODE_D = 32; - - // Key code constant: Backspace key. Deletes characters before the insertion point, - // unlike KEYCODE_FORWARD_DEL. - // (0x00000043) - int KEYCODE_DEL = 67; - - // Key code constant: Directional Pad Center key. May also be synthesized from - // trackball motions. - // (0x00000017) - int KEYCODE_DPAD_CENTER = 23; - - // Key code constant: Directional Pad Down key. May also be synthesized - // from trackball motions. - // (0x00000014) - int KEYCODE_DPAD_DOWN = 20; - - // Key code constant: Directional Pad Left key. May also be synthesized - // from trackball motions. - // (0x00000015) - int KEYCODE_DPAD_LEFT = 21; - - // Key code constant: Directional Pad Right key. May also be synthesized from trackball motions. - // (0x00000016) - int KEYCODE_DPAD_RIGHT = 22; - - // Key code constant: Directional Pad Up key. May also be synthesized from trackball motions. - // (0x00000013) - int KEYCODE_DPAD_UP = 19; - - // Key code constant: DVR key. On some TV remotes, switches to a DVR mode for recorded shows. - // (0x000000ad) - int KEYCODE_DVR = 173; - - // Key code constant: 'E' key. - // (0x00000021) - int KEYCODE_E = 33; - - // Key code constant: Japanese alphanumeric key. - // (0x000000d4) - int KEYCODE_EISU = 212; - - // Key code constant: End Call key. - // (0x00000006) - int KEYCODE_ENDCALL = 6; - - // Key code constant: Enter key. - // (0x00000042) - int KEYCODE_ENTER = 66; - - // Key code constant: Envelope special function key. Used to launch a mail application. - // (0x00000041) - int KEYCODE_ENVELOPE = 65; - - // Key code constant: '=' key. - // (0x00000046) - int KEYCODE_EQUALS = 70; - - // Key code constant: Escape key. - // (0x0000006f) - int KEYCODE_ESCAPE = 111; - - // Key code constant: Explorer special function key. Used to launch a browser application. - // (0x00000040) - int KEYCODE_EXPLORER = 64; - - // Key code constant: 'F' key. - // (0x00000022) - int KEYCODE_F = 34; - - // Key code constant: F1 key. - // (0x00000083) - int KEYCODE_F1 = 131; - - // Key code constant: F10 key. - // (0x0000008c) - int KEYCODE_F10 = 140; - - // Key code constant: F11 key. - // (0x0000008d) - int KEYCODE_F11 = 141; - - // Key code constant: F12 key. - // (0x0000008e) - int KEYCODE_F12 = 142; - - // Key code constant: F2 key. - // (0x00000084) - int KEYCODE_F2 = 132; - - // Key code constant: F3 key. - // (0x00000085) - int KEYCODE_F3 = 133; - - // Key code constant: F4 key. - // (0x00000086) - int KEYCODE_F4 = 134; - - // Key code constant: F5 key. - // (0x00000087) - int KEYCODE_F5 = 135; - - // Key code constant: F6 key. - // (0x00000088) - int KEYCODE_F6 = 136; - - // Key code constant: F7 key. - // (0x00000089) - int KEYCODE_F7 = 137; - - // Key code constant: F8 key. - // (0x0000008a) - int KEYCODE_F8 = 138; - - // Key code constant: F9 key. - // (0x0000008b) - int KEYCODE_F9 = 139; - - // Key code constant: Camera Focus key. Used to focus the camera. - // (0x00000050) - int KEYCODE_FOCUS = 80; - - // Key code constant: Forward key. Navigates forward in the history stack. - // Complement of KEYCODE_BACK. - // (0x0000007d) - int KEYCODE_FORWARD = 125; - - // Key code constant: Forward Delete key. Deletes characters ahead of the insertion point, - // unlike KEYCODE_DEL. - // (0x00000070) - int KEYCODE_FORWARD_DEL = 112; - - // Key code constant: Function modifier key. - // (0x00000077) - int KEYCODE_FUNCTION = 119; - - // Key code constant: 'G' key. - // (0x00000023) - int KEYCODE_G = 35; - - // Key code constant: '`' (backtick) key. - // (0x00000044) - int KEYCODE_GRAVE = 68; - - // Key code constant: Guide key. On TV remotes, shows a programming guide. - // (0x000000ac) - int KEYCODE_GUIDE = 172; - - // Key code constant: 'H' key. - // (0x00000024) - int KEYCODE_H = 36; - - // Key code constant: Headset Hook key. Used to hang up calls and stop media. - // (0x0000004f) - int KEYCODE_HEADSETHOOK = 79; - - // Key code constant: Help key. - // (0x00000103) - int KEYCODE_HELP = 259; - - // Key code constant: Japanese conversion key. - // (0x000000d6) - int KEYCODE_HENKAN = 214; - - // Key code constant: Home key. This key is handled by the framework - // and is never delivered to applications. - // (0x00000003) - int KEYCODE_HOME = 3; - - // Key code constant: 'I' key. - // (0x00000025) - int KEYCODE_I = 37; - - // Key code constant: Info key. Common on TV remotes to show additional - // information related to what is currently being viewed. - // (0x000000a5) - int KEYCODE_INFO = 165; - - // Key code constant: Insert key. Toggles insert / overwrite edit mode. - // (0x0000007c) - int KEYCODE_INSERT = 124; - - // Key code constant: 'J' key. - // (0x00000026) - int KEYCODE_J = 38; - - // Key code constant: 'K' key. - // (0x00000027) - int KEYCODE_K = 39; - - // Key code constant: Japanese kana key. - // (0x000000da) - int KEYCODE_KANA = 218; - - // Key code constant: Japanese katakana / hiragana key. - // (0x000000d7) - int KEYCODE_KATAKANA_HIRAGANA = 215; - - // Key code constant: 'L' key. - // (0x00000028) - int KEYCODE_L = 40; - - // Key code constant: Language Switch key. Toggles the current input language such as - // switching between English and Japanese on a QWERTY keyboard. - // On some devices, the same function may be performed by pressing Shift+Spacebar. - // (0x000000cc) - int KEYCODE_LANGUAGE_SWITCH = 204; - - // Key code constant: Last Channel key. Goes to the last viewed channel. - // (0x000000e5) - int KEYCODE_LAST_CHANNEL = 229; - - // Key code constant: '[' key. - // (0x00000047) - int KEYCODE_LEFT_BRACKET = 71; - - // Key code constant: 'M' key. - // (0x00000029) - int KEYCODE_M = 41; - - // Key code constant: Manner Mode key. Toggles silent or vibrate mode on and off to make the - // device behave more politely in certain settings such as on a crowded train. - // On some devices, the key may only operate when long-pressed. - // (0x000000cd) - int KEYCODE_MANNER_MODE = 205; - - // Key code constant: Audio Track key. Switches the audio tracks. - // (0x000000de) - int KEYCODE_MEDIA_AUDIO_TRACK = 222; - - // Key code constant: Close media key. May be used to close a CD tray, for example. - // (0x00000080) - int KEYCODE_MEDIA_CLOSE = 128; - - // Key code constant: Eject media key. May be used to eject a CD tray, for example. - // (0x00000081) - int KEYCODE_MEDIA_EJECT = 129; - - // Key code constant: Fast Forward media key. - // (0x0000005a) - int KEYCODE_MEDIA_FAST_FORWARD = 90; - - // Key code constant: Play Next media key. - // (0x00000057) - int KEYCODE_MEDIA_NEXT = 87; - - // Key code constant: Pause media key. - // (0x0000007f) - int KEYCODE_MEDIA_PAUSE = 127; - - // Key code constant: Play media key. - // (0x0000007e) - int KEYCODE_MEDIA_PLAY = 126; - - // Key code constant: Play/Pause media key. - // (0x00000055) - int KEYCODE_MEDIA_PLAY_PAUSE = 85; - - // Key code constant: Play Previous media key. - // (0x00000058) - int KEYCODE_MEDIA_PREVIOUS = 88; - - // Key code constant: Record media key. - // (0x00000082) - int KEYCODE_MEDIA_RECORD = 130; - - // Key code constant: Rewind media key. - // (0x00000059) - int KEYCODE_MEDIA_REWIND = 89; - - // Key code constant: Stop media key. - // (0x00000056) - int KEYCODE_MEDIA_STOP = 86; - - // Key code constant: Media Top Menu key. Goes to the top of media menu. - // (0x000000e2) - int KEYCODE_MEDIA_TOP_MENU = 226; - - // Key code constant: Menu key. - // (0x00000052) - int KEYCODE_MENU = 82; - - // Key code constant: Left Meta modifier key. - // (0x00000075) - int KEYCODE_META_LEFT = 117; - - // Key code constant: Right Meta modifier key. - // (0x00000076) - int KEYCODE_META_RIGHT = 118; - - // Key code constant: '-'. - // (0x00000045) - int KEYCODE_MINUS = 69; - - // Key code constant: End Movement key. Used for scrolling or moving the cursor around to - // the end of a line or to the bottom of a list. - // (0x0000007b) - int KEYCODE_MOVE_END = 123; - - // Key code constant: Home Movement key. Used for scrolling or moving the cursor around to - // the start of a line or to the top of a list. - // (0x0000007a) - int KEYCODE_MOVE_HOME = 122; - - // Key code constant: Japanese non-conversion key. - // (0x000000d5) - int KEYCODE_MUHENKAN = 213; - - // Key code constant: Music special function key. Used to launch a music player application. - // (0x000000d1) - int KEYCODE_MUSIC = 209; - - // Key code constant: Mute key. Mutes the microphone, unlike KEYCODE_VOLUME_MUTE. - // (0x0000005b) - int KEYCODE_MUTE = 91; - - // Key code constant: 'N' key. - // (0x0000002a) - int KEYCODE_N = 42; - - // Key code constant: Notification key. - // (0x00000053) - int KEYCODE_NOTIFICATION = 83; - - // Key code constant: Number modifier key. Used to enter numeric symbols. This key is not - // Num Lock; it is more like KEYCODE_ALT_LEFT and is interpreted as an - // ALT key by MetaKeyKeyListener. - // (0x0000004e) - int KEYCODE_NUM = 78; - - // Key code constant: Numeric keypad '0' key. - // (0x00000090) - int KEYCODE_NUMPAD_0 = 144; - - // Key code constant: Numeric keypad '1' key. - // (0x00000091) - int KEYCODE_NUMPAD_1 = 145; - - // Key code constant: Numeric keypad '2' key. - // (0x00000092) - int KEYCODE_NUMPAD_2 = 146; - - // Key code constant: Numeric keypad '3' key. - // (0x00000093) - int KEYCODE_NUMPAD_3 = 147; - - // Key code constant: Numeric keypad '4' key. - // (0x00000094) - int KEYCODE_NUMPAD_4 = 148; - - // Key code constant: Numeric keypad '5' key. - // (0x00000095) - int KEYCODE_NUMPAD_5 = 149; - - // Key code constant: Numeric keypad '6' key. - // (0x00000096) - int KEYCODE_NUMPAD_6 = 150; - - // Key code constant: Numeric keypad '7' key. - // (0x00000097) - int KEYCODE_NUMPAD_7 = 151; - - // Key code constant: Numeric keypad '8' key. - // (0x00000098) - int KEYCODE_NUMPAD_8 = 152; - - // Key code constant: Numeric keypad '9' key. - // (0x00000099) - int KEYCODE_NUMPAD_9 = 153; - - // Key code constant: Numeric keypad '+' key (for addition). - // (0x0000009d) - int KEYCODE_NUMPAD_ADD = 157; - - // Key code constant: Numeric keypad ',' key (for decimals or digit grouping). - // (0x0000009f) - int KEYCODE_NUMPAD_COMMA = 159; - - // Key code constant: Numeric keypad '/' key (for division). - // (0x0000009a) - int KEYCODE_NUMPAD_DIVIDE = 154; - - // Key code constant: Numeric keypad '.' key (for decimals or digit grouping). - // (0x0000009e) - int KEYCODE_NUMPAD_DOT = 158; - - // Key code constant: Numeric keypad Enter key. - // (0x000000a0) - int KEYCODE_NUMPAD_ENTER = 160; - - // Key code constant: Numeric keypad '=' key. - // (0x000000a1) - int KEYCODE_NUMPAD_EQUALS = 161; - - // Key code constant: Numeric keypad '(' key. - // (0x000000a2) - int KEYCODE_NUMPAD_LEFT_PAREN = 162; - - // Key code constant: Numeric keypad '*' key (for multiplication). - // (0x0000009b) - int KEYCODE_NUMPAD_MULTIPLY = 155; - - // Key code constant: Numeric keypad ')' key. - // (0x000000a3) - int KEYCODE_NUMPAD_RIGHT_PAREN = 163; - - // Key code constant: Numeric keypad '-' key (for subtraction). - // (0x0000009c) - int KEYCODE_NUMPAD_SUBTRACT = 156; - - // Key code constant: Num Lock key. This is the Num Lock key; it is different from KEYCODE_NUM. - // This key alters the behavior of other keys on the numeric keypad. - // (0x0000008f) - int KEYCODE_NUM_LOCK = 143; - - // Key code constant: 'O' key. - // (0x0000002b) - int KEYCODE_O = 43; - - // Key code constant: 'P' key. - // (0x0000002c) - int KEYCODE_P = 44; - - // Key code constant: Page Down key. - // (0x0000005d) - int KEYCODE_PAGE_DOWN = 93; - - // Key code constant: Page Up key. - // (0x0000005c) - int KEYCODE_PAGE_UP = 92; - - // Key code constant: Pairing key. Initiates peripheral pairing mode. Useful for pairing - // remote control devices or game controllers, especially if no other input mode is available. - // (0x000000e1) - int KEYCODE_PAIRING = 225; - - // Key code constant: '.' key. - // (0x00000038) - int KEYCODE_PERIOD = 56; - - // Key code constant: Picture Symbols modifier key. Used to switch symbol sets - // (Emoji, Kao-moji). - // (0x0000005e) - int KEYCODE_PICTSYMBOLS = 94; - - // Key code constant: '+' key. - // (0x00000051) - int KEYCODE_PLUS = 81; - - // Key code constant: '#' key. - // (0x00000012) - int KEYCODE_POUND = 18; - - // Key code constant: Power key. - // (0x0000001a) - int KEYCODE_POWER = 26; - - // Key code constant: Blue "programmable" key. On TV remotes, - // acts as a contextual/programmable key. - // (0x000000ba) - int KEYCODE_PROG_BLUE = 186; - - // Key code constant: Green "programmable" key. On TV remotes, actsas a - // contextual/programmable key. - // (0x000000b8) - int KEYCODE_PROG_GREEN = 184; - - // Key code constant: Red "programmable" key. On TV remotes, acts as - // a contextual/programmable key. - // (0x000000b7) - int KEYCODE_PROG_RED = 183; - - // Key code constant: Yellow "programmable" key. On TV remotes, acts as - // a contextual/programmable key. - // (0x000000b9) - int KEYCODE_PROG_YELLOW = 185; - - // Key code constant: 'Q' key. - // (0x0000002d) - int KEYCODE_Q = 45; - - // Key code constant: 'R' key. - // (0x0000002e) - int KEYCODE_R = 46; - - // Key code constant: ']' key. - // (0x00000048) - int KEYCODE_RIGHT_BRACKET = 72; - - // Key code constant: Japanese Ro key. - // (0x000000d9) - int KEYCODE_RO = 217; - - // Key code constant: 'S' key. - // (0x0000002f) - int KEYCODE_S = 47; - - // Key code constant: Scroll Lock key. - // (0x00000074) - int KEYCODE_SCROLL_LOCK = 116; - - // Key code constant: Search key. - // (0x00000054) - int KEYCODE_SEARCH = 84; - - // Key code constant: ';' key. - // (0x0000004a) - int KEYCODE_SEMICOLON = 74; - - // Key code constant: Settings key. Starts the system settings activity. - // (0x000000b0) - int KEYCODE_SETTINGS = 176; - - // Key code constant: Left Shift modifier key. - // (0x0000003b) - int KEYCODE_SHIFT_LEFT = 59; - - // Key code constant: Right Shift modifier key. - // (0x0000003c) - int KEYCODE_SHIFT_RIGHT = 60; - - // Key code constant: '/' key. - // (0x0000004c) - int KEYCODE_SLASH = 76; - - // Key code constant: Sleep key. Puts the device to sleep. Behaves somewhat like - // KEYCODE_POWER but it has no effect if the device is already asleep. - // (0x000000df) - int KEYCODE_SLEEP = 223; - - // Key code constant: Soft Left key. Usually situated below the display on phones and - // used as a multi-function feature key for selecting a software defined function - // shown on the bottom left of the display. - // (0x00000001) - int KEYCODE_SOFT_LEFT = 1; - - // Key code constant: Soft Right key. Usually situated below the display on phones - // and used as a multi-function feature key for selecting a software defined function - // shown on the bottom right of the display. - // (0x00000002) - int KEYCODE_SOFT_RIGHT = 2; - - // Key code constant: Space key. - // (0x0000003e) - int KEYCODE_SPACE = 62; - - // Key code constant: '*' key. - // (0x00000011) - int KEYCODE_STAR = 17; - - // Key code constant: Set-top-box input key. On TV remotes, switches the input mode - // on an external Set-top-box. - // (0x000000b4) - int KEYCODE_STB_INPUT = 180; - - // Key code constant: Set-top-box power key. On TV remotes, toggles the power on an - // external Set-top-box. - // (0x000000b3) - int KEYCODE_STB_POWER = 179; - - // Key code constant: Switch Charset modifier key. Used to switch character sets - // (Kanji, Katakana). - // (0x0000005f) - int KEYCODE_SWITCH_CHARSET = 95; - - // Key code constant: Symbol modifier key. Used to enter alternate symbols. - // (0x0000003f) - int KEYCODE_SYM = 63; - - // Key code constant: System Request / Print Screen key. - // (0x00000078) - int KEYCODE_SYSRQ = 120; - - // Key code constant: 'T' key. - // (0x00000030) - int KEYCODE_T = 48; - - // Key code constant: Tab key. - // (0x0000003d) - int KEYCODE_TAB = 61; - - // Key code constant: TV key. On TV remotes, switches to viewing live TV. - // (0x000000aa) - int KEYCODE_TV = 170; - - // Key code constant: Antenna/Cable key. Toggles broadcast input source between - // antenna and cable. - // (0x000000f2) - int KEYCODE_TV_ANTENNA_CABLE = 242; - - // Key code constant: Audio description key. Toggles audio description off / on. - // (0x000000fc) - int KEYCODE_TV_AUDIO_DESCRIPTION = 252; - - // Key code constant: Audio description mixing volume down key. Lessen audio description - // volume as compared with normal audio volume. - // (0x000000fe) - int KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254; - - // Key code constant: Audio description mixing volume up key. Louden audio description - // volume as compared with normal audio volume. - // (0x000000fd) - int KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253; - - // Key code constant: Contents menu key. Goes to the title list. Corresponds to Contents - // Menu (0x0B) of CEC User Control Code - // (0x00000100) - int KEYCODE_TV_CONTENTS_MENU = 256; - - // Key code constant: TV data service key. Displays data services like weather, sports. - // (0x000000e6) - int KEYCODE_TV_DATA_SERVICE = 230; - - // Key code constant: TV input key. On TV remotes, switches the input on a television screen. - // (0x000000b2) - int KEYCODE_TV_INPUT = 178; - - // Key code constant: Component #1 key. Switches to component video input #1. - // (0x000000f9) - int KEYCODE_TV_INPUT_COMPONENT_1 = 249; - - // Key code constant: Component #2 key. Switches to component video input #2. - // (0x000000fa) - int KEYCODE_TV_INPUT_COMPONENT_2 = 250; - - // Key code constant: Composite #1 key. Switches to composite video input #1. - // (0x000000f7) - int KEYCODE_TV_INPUT_COMPOSITE_1 = 247; - - // Key code constant: Composite #2 key. Switches to composite video input #2. - // (0x000000f8) - int KEYCODE_TV_INPUT_COMPOSITE_2 = 248; - - // Key code constant: HDMI #1 key. Switches to HDMI input #1. - // (0x000000f3) - int KEYCODE_TV_INPUT_HDMI_1 = 243; - - // Key code constant: HDMI #2 key. Switches to HDMI input #2. - // (0x000000f4) - int KEYCODE_TV_INPUT_HDMI_2 = 244; - - // Key code constant: HDMI #3 key. Switches to HDMI input #3. - // (0x000000f5) - int KEYCODE_TV_INPUT_HDMI_3 = 245; - - // Key code constant: HDMI #4 key. Switches to HDMI input #4. - // (0x000000f6) - int KEYCODE_TV_INPUT_HDMI_4 = 246; - - // Key code constant: VGA #1 key. Switches to VGA (analog RGB) input #1. - // (0x000000fb) - int KEYCODE_TV_INPUT_VGA_1 = 251; - - // Key code constant: Media context menu key. Goes to the context menu of media contents. - // Corresponds to Media Context-sensitive Menu (0x11) of CEC User Control Code. - // (0x00000101) - int KEYCODE_TV_MEDIA_CONTEXT_MENU = 257; - - // Key code constant: Toggle Network key. Toggles selecting broacast services. - // (0x000000f1) - int KEYCODE_TV_NETWORK = 241; - - // Key code constant: Number entry key. Initiates to enter multi-digit channel nubmber when - // each digit key is assigned for selecting separate channel. - // Corresponds to Number Entry Mode (0x1D) of CEC User Control Code. - // (0x000000ea) - int KEYCODE_TV_NUMBER_ENTRY = 234; - - // Key code constant: TV power key. On TV remotes, toggles the power on a television screen. - // (0x000000b1) - int KEYCODE_TV_POWER = 177; - - // Key code constant: Radio key. Toggles TV service / Radio service. - // (0x000000e8) - int KEYCODE_TV_RADIO_SERVICE = 232; - - // Key code constant: Satellite key. Switches to digital satellite broadcast service. - // (0x000000ed) - int KEYCODE_TV_SATELLITE = 237; - - // Key code constant: BS key. Switches to BS digital satellite broadcasting service - // available in Japan. - // (0x000000ee) - int KEYCODE_TV_SATELLITE_BS = 238; - - // Key code constant: CS key. Switches to CS digital satellite broadcasting - // service available in Japan. - // (0x000000ef) - int KEYCODE_TV_SATELLITE_CS = 239; - - // Key code constant: BS/CS key. Toggles between BS and CS digital satellite services. - // (0x000000f0) - int KEYCODE_TV_SATELLITE_SERVICE = 240; - - // Key code constant: Teletext key. Displays Teletext service. - // (0x000000e9) - int KEYCODE_TV_TELETEXT = 233; - - // Key code constant: Analog Terrestrial key. Switches to analog terrestrial broadcast service. - // (0x000000eb) - int KEYCODE_TV_TERRESTRIAL_ANALOG = 235; - - // Key code constant: Digital Terrestrial key. Switches to digital terrestrial - // broadcast service. - // (0x000000ec) - int KEYCODE_TV_TERRESTRIAL_DIGITAL = 236; - - // Key code constant: Timer programming key. Goes to the timer recording menu. - // Corresponds to Timer Programming (0x54) of CEC User Control Code. - // (0x00000102) - int KEYCODE_TV_TIMER_PROGRAMMING = 258; - - // Key code constant: Zoom mode key. Changes Zoom mode (Normal, - // Full, Zoom, Wide-zoom, etc.) - // (0x000000ff) - int KEYCODE_TV_ZOOM_MODE = 255; - - // Key code constant: 'U' key. - // (0x00000031) - int KEYCODE_U = 49; - - // Key code constant: Unknown key code. - // (0x00000000) - int KEYCODE_UNKNOWN = 0; - - // Key code constant: 'V' key. - // (0x00000032) - int KEYCODE_V = 50; - - // Key code constant: Voice Assist key. Launches the global voice assist activity. - // Not delivered to applications. - // (0x000000e7) - int KEYCODE_VOICE_ASSIST = 231; - - // Key code constant: Volume Down key. Adjusts the speaker volume down. - // (0x00000019) - int KEYCODE_VOLUME_DOWN = 25; - - // Key code constant: Volume Mute key. Mutes the speaker, unlike KEYCODE_MUTE. - // This key should normally be implemented as a toggle such that - // the first press mutes the speaker and the second press restores the original volume. - // (0x000000a4) - int KEYCODE_VOLUME_MUTE = 164; - - // Key code constant: Volume Up key. Adjusts the speaker volume up. - // (0x00000018) - int KEYCODE_VOLUME_UP = 24; - - // Key code constant: 'W' key. - // (0x00000033) - int KEYCODE_W = 51; - - // Key code constant: Wakeup key. Wakes up the device. Behaves somewhat like - // KEYCODE_POWER but it has no effect if the device is already awake. - // (0x000000e0) - int KEYCODE_WAKEUP = 224; - - // Key code constant: Window key. On TV remotes, toggles picture-in-picture - // mode or other windowing functions. - // (0x000000ab) - int KEYCODE_WINDOW = 171; - - // Key code constant: 'X' key. - // (0x00000034) - int KEYCODE_X = 52; - - // Key code constant: 'Y' key. - // (0x00000035) - int KEYCODE_Y = 53; - - // Key code constant: Japanese Yen key. - // (0x000000d8) - int KEYCODE_YEN = 216; - - // Key code constant: 'Z' key. - // (0x00000036) - int KEYCODE_Z = 54; - - // Key code constant: Japanese full-width / half-width key. - // (0x000000d3) - int KEYCODE_ZENKAKU_HANKAKU = 211; - - // Key code constant: Zoom in key. - // (0x000000a8) - int KEYCODE_ZOOM_IN = 168; - - // Key code constant: Zoom out key. - // (0x000000a9) - int KEYCODE_ZOOM_OUT = 169; - - // This constant was deprecated in API level 3. - // There are now more than MAX_KEYCODE keycodes. Use getMaxKeyCode() instead. - // (0x00000054) - int MAX_KEYCODE = 84; - - // This mask is used to check whether the left ALT meta key is pressed. - // See Also - // isAltPressed() - // getMetaState() - // KEYCODE_ALT_LEFT - // (0x00000010) - int META_ALT_LEFT_ON = 16; - - // This mask is a combination of META_ALT_ON, META_ALT_LEFT_ON and META_ALT_RIGHT_ON. - // (0x00000032) - int META_ALT_MASK = 50; - - // This mask is used to check whether one of the ALT meta keys is pressed. - // See Also - // isAltPressed() - // getMetaState() - // KEYCODE_ALT_LEFT - // KEYCODE_ALT_RIGHT - // (0x00000002) - int META_ALT_ON = 2; - - // This mask is used to check whether the right the ALT meta key is pressed. - // See Also - // isAltPressed() - // getMetaState() - // KEYCODE_ALT_RIGHT - // (0x00000020) - int META_ALT_RIGHT_ON = 32; - - // This mask is used to check whether the CAPS LOCK meta key is on. - // See Also - // isCapsLockOn() - // getMetaState() - // KEYCODE_CAPS_LOCK - // (0x00100000) - int META_CAPS_LOCK_ON = 1048576; - - // This mask is used to check whether the left CTRL meta key is pressed. - // See Also - // isCtrlPressed() - // getMetaState() - // KEYCODE_CTRL_LEFT - // (0x00002000) - int META_CTRL_LEFT_ON = 8192; - - // This mask is a combination of META_CTRL_ON, META_CTRL_LEFT_ON and META_CTRL_RIGHT_ON. - // (0x00007000) - int META_CTRL_MASK = 28672; - - // This mask is used to check whether one of the CTRL meta keys is pressed. - // See Also - // isCtrlPressed() - // getMetaState() - // KEYCODE_CTRL_LEFT - // KEYCODE_CTRL_RIGHT - // (0x00001000) - int META_CTRL_ON = 4096; - - // This mask is used to check whether the right CTRL meta key is pressed. - // See Also - // isCtrlPressed() - // getMetaState() - // KEYCODE_CTRL_RIGHT - // (0x00004000) - int META_CTRL_RIGHT_ON = 16384; - - // This mask is used to check whether the FUNCTION meta key is pressed. - // See Also - // isFunctionPressed() - // getMetaState() - // (0x00000008) - int META_FUNCTION_ON = 8; - - // This mask is used to check whether the left META meta key is pressed. - // See Also - // isMetaPressed() - // getMetaState() - // KEYCODE_META_LEFT - // (0x00020000) - int META_META_LEFT_ON = 131072; - - // This mask is a combination of META_META_ON, META_META_LEFT_ON and META_META_RIGHT_ON. - // (0x00070000) - int META_META_MASK = 458752; - - // This mask is used to check whether one of the META meta keys is pressed. - // See Also - // isMetaPressed() - // getMetaState() - // KEYCODE_META_LEFT - // KEYCODE_META_RIGHT - // (0x00010000) - int META_META_ON = 65536; - - // This mask is used to check whether the right META meta key is pressed. - // See Also - // isMetaPressed() - // getMetaState() - // KEYCODE_META_RIGHT - // (0x00040000) - int META_META_RIGHT_ON = 262144; - - // This mask is used to check whether the NUM LOCK meta key is on. - // See Also - // isNumLockOn() - // getMetaState() - // KEYCODE_NUM_LOCK - // (0x00200000) - int META_NUM_LOCK_ON = 2097152; - - // This mask is used to check whether the SCROLL LOCK meta key is on. - // See Also - // isScrollLockOn() - // getMetaState() - // KEYCODE_SCROLL_LOCK - // (0x00400000) - int META_SCROLL_LOCK_ON = 4194304; - - // This mask is used to check whether the left SHIFT meta key is pressed. - // See Also - // isShiftPressed() - // getMetaState() - // KEYCODE_SHIFT_LEFT - // (0x00000040) - int META_SHIFT_LEFT_ON = 64; - - // This mask is a combination of META_SHIFT_ON, META_SHIFT_LEFT_ON and META_SHIFT_RIGHT_ON. - // (0x000000c1) - int META_SHIFT_MASK = 193; - - // This mask is used to check whether one of the SHIFT meta keys is pressed. - // See Also - // isShiftPressed() - // getMetaState() - // KEYCODE_SHIFT_LEFT - // KEYCODE_SHIFT_RIGHT - // (0x00000001) - int META_SHIFT_ON = 1; - - // This mask is used to check whether the right SHIFT meta key is pressed. - // See Also - // isShiftPressed() - // getMetaState() - // KEYCODE_SHIFT_RIGHT - // (0x00000080) - int META_SHIFT_RIGHT_ON = 128; - - // This mask is used to check whether the SYM meta key is pressed. - // See Also - // isSymPressed() - // getMetaState() - // (0x00000004) - int META_SYM_ON = 4; - -} 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 3cfa8ff8c..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.openqa.selenium.remote.RemoteWebElement; -import org.apache.commons.lang3.StringUtils; -import org.openqa.selenium.internal.HasIdentity; - -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. @@ -35,44 +30,23 @@ public class AndroidMobileCommandHelper extends MobileCommand { /** - * This method forms a {@link java.util.Map} of parameters for the - * getting of the current activity. + * This method forms a {@link Map} of parameters for the getting of the current activity. * - * @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 public static Map.Entry> currentActivityCommand() { - return new AbstractMap.SimpleEntry<>( - CURRENT_ACTIVITY, ImmutableMap.of()); + return Map.entry(CURRENT_ACTIVITY, Map.of()); } /** - * This method forms a {@link java.util.Map} of parameters for the - * getting of the current package. + * This method forms a {@link Map} of parameters for the getting of the current package. * - * @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 public static Map.Entry> currentPackageCommand() { - return new AbstractMap.SimpleEntry<>( - GET_CURRENT_PACKAGE, ImmutableMap.of()); - } - - /** - * This method forms a {@link java.util.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 java.util.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()); } /** @@ -83,8 +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()); } /** @@ -114,207 +87,268 @@ public class AndroidMobileCommandHelper extends MobileCommand { * [1478095200, null, null, 10079213, 19962, 2487705, 20015, 0, 3600], * [1478098800, null, null, 4444433, 10227, 1430356, 10493, 0, 3600]] * in case of cpu info : [[user, kernel], [0.9, 1.3]] - * @throws Exception if the performance data type is not supported, thows Error */ public static Map.Entry> getPerformanceDataCommand( - String packageName, String dataType, int dataReadTimeout) throws Exception { - String[] parameters = new String[] {"packageName", "dataType", "dataReadTimeout"}; - Object[] values = new Object[] {packageName, dataType, dataReadTimeout}; - return new AbstractMap.SimpleEntry<>( - GET_PERFORMANCE_DATA, prepareArguments(parameters, values)); + String packageName, String dataType, int dataReadTimeout) { + return Map.entry(GET_PERFORMANCE_DATA, Map.of( + "packageName", packageName, + "dataType", dataType, + "dataReadTimeout", dataReadTimeout + )); } /** - * This method forms a {@link java.util.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 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 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 java.util.Map} of parameters for the - * getting of a network connection value. + * 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 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 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 java.util.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 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 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 - * checking of the keyboard state (is it shown or not). + * 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 java.util.Map} command arguments. + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ - public static Map.Entry> isKeyboardShownCommand() { - return new AbstractMap.SimpleEntry<>( - IS_KEYBOARD_SHOWN, ImmutableMap.of()); + @Deprecated + public static Map.Entry> isLockedCommand() { + return Map.entry(IS_LOCKED, Map.of()); } /** - * This method forms a {@link java.util.Map} of parameters for the - * checking of the device state (is it locked or not). + * 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 java.util.Map} command arguments. + * @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. */ - public static Map.Entry> isLockedCommand() { - return new AbstractMap.SimpleEntry<>( - IS_LOCKED, ImmutableMap.of()); + @Deprecated + public static Map.Entry> fingerPrintCommand(int fingerPrintId) { + return Map.entry(FINGER_PRINT, Map.of("fingerprintId", fingerPrintId)); } /** - * This method forms a {@link java.util.Map} of parameters for the - * notification opening. + * This method forms a {@link Map} of parameters for the notification opening. * - * @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 public static Map.Entry> openNotificationsCommand() { - return new AbstractMap.SimpleEntry<>( - OPEN_NOTIFICATIONS, ImmutableMap.of()); + return Map.entry(OPEN_NOTIFICATIONS, Map.of()); } /** - * This method forms a {@link java.util.Map} of parameters for the - * file pushing + * This method forms a {@link Map} of parameters for the setting of device network connection. * - * @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. + * @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. */ - public static Map.Entry> pushFileCommandCommand(String remotePath, - byte[] base64Data) { - String[] parameters = new String[] {"path", "data"}; - Object[] values = new Object[] {remotePath, base64Data}; - return new AbstractMap.SimpleEntry<>(PUSH_FILE, prepareArguments(parameters, values)); + @Deprecated + public static Map.Entry> setConnectionCommand(long bitMask) { + return Map.entry(SET_NETWORK_CONNECTION, Map.of( + "name", "network_connection", + "parameters", Map.of("type", bitMask) + )); } /** - * This method forms a {@link java.util.Map} of parameters for the - * setting of device network connection. + * This method forms a {@link Map} of parameters for the toggling of location services. * - * @param connection The bitmask of the desired connection - * @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. */ - public static Map.Entry> setConnectionCommand(Connection connection) { - String[] parameters = new String[] {"name", "parameters"}; - Object[] values = - new Object[] {"network_connection", ImmutableMap.of("type", connection.bitMask)}; - return new AbstractMap.SimpleEntry<>( - SET_NETWORK_CONNECTION, prepareArguments(parameters, values)); + @Deprecated + public static Map.Entry> toggleLocationServicesCommand() { + return Map.entry(TOGGLE_LOCATION_SERVICES, Map.of()); } /** - * This method forms a {@link java.util.Map} of parameters for the - * activity starting. + * This method forms a {@link Map} of parameters for the element. * - * @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] - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. - * @throws IllegalArgumentException when any required argument is empty + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ - 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 { + @Deprecated + public static Map.Entry> unlockCommand() { + return Map.entry(UNLOCK, Map.of()); + } - 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 : ""; + /** + * This method forms a {@link Map} of parameters for the element + * value replacement. It is used against input elements + * + * @param remoteWebElement an instance which contains an element ID + * @param value a new value + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. + */ + @Deprecated + public static Map.Entry> replaceElementValueCommand( + RemoteWebElement remoteWebElement, String value) { + return Map.entry(REPLACE_VALUE, Map.of( + "id", remoteWebElement.getId(), + "value", value + )); + } - 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); + /** + * This method forms a {@link Map} of parameters for the element + * value replacement. It is used against input elements + * + * @param phoneNumber The phone number of message sender + * @param message The message content + * + * @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) { + return Map.entry(SEND_SMS, Map.of( + "phoneNumber", phoneNumber, + "message", message + )); } /** - * This method forms a {@link java.util.Map} of parameters for the - * toggling of location services. + * This method forms a {@link Map} of parameters for the element + * value replacement. It is used against input elements * - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @param phoneNumber The phone number of message sender + * @param gsmCallActions One of available GSM call actions + * + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ - public static Map.Entry> toggleLocationServicesCommand() { - return new AbstractMap.SimpleEntry<>(TOGGLE_LOCATION_SERVICES, ImmutableMap.of()); + @Deprecated + public static Map.Entry> gsmCallCommand( + String phoneNumber, GsmCallActions gsmCallActions) { + return Map.entry(GSM_CALL, Map.of( + "phoneNumber", phoneNumber, + "action", gsmCallActions.name().toLowerCase(ROOT) + )); } /** - * This method forms a {@link java.util.Map} of parameters for the - * device unlocking. + * This method forms a {@link Map} of parameters for the element + * value replacement. It is used against input elements + * + * @param gsmSignalStrength One of available GSM signal strength * - * @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. */ - public static Map.Entry> unlockCommand() { - return new AbstractMap.SimpleEntry<>(UNLOCK, ImmutableMap.of()); + @Deprecated + public static Map.Entry> gsmSignalStrengthCommand( + GsmSignalStrength gsmSignalStrength) { + return Map.entry(GSM_SIGNAL, + Map.of( + // https://github.com/appium/appium/issues/12234 + "signalStrengh", gsmSignalStrength.ordinal(), + "signalStrength", gsmSignalStrength.ordinal() + )); } + /** + * This method forms a {@link Map} of parameters for the element + * value replacement. It is used against input elements + * + * @param gsmVoiceState One of available GSM voice state + * + * @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 Map.entry(GSM_VOICE, Map.of("state", gsmVoiceState.name().toLowerCase(ROOT))); + } /** - * This method forms a {@link java.util.Map} of parameters for the element + * This method forms a {@link Map} of parameters for the element * value replacement. It is used against input elements * - * @param hasIdentityObject an instance which contains an element ID - * @param value a new value - * @return a key-value pair. The key is the command name. The value is a - * {@link java.util.Map} command arguments. + * @param networkSpeed One of possible NETWORK_SPEED values + * + * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. */ - public static Map.Entry> replaceElementValueCommand( - HasIdentity hasIdentityObject, String value) { - String[] parameters = new String[] {"id", "value"}; - Object[] values = - new Object[] {hasIdentityObject.getId(), value}; + @Deprecated + public static Map.Entry> networkSpeedCommand( + NetworkSpeed networkSpeed) { + return Map.entry(NETWORK_SPEED, Map.of("netspeed", networkSpeed.name().toLowerCase(ROOT))); + } - return new AbstractMap.SimpleEntry<>( - REPLACE_VALUE, prepareArguments(parameters, values)); + /** + * This method forms a {@link Map} of parameters for the element + * value replacement. It is used against input elements + * + * @param percent A number in range [0, 4] + * + * @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 Map.entry(POWER_CAPACITY, Map.of("percent", percent)); + } + + /** + * This method forms a {@link Map} of parameters for the element + * value replacement. It is used against input elements + * + * @param powerACState One of available power AC state + * + * @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 Map.entry(POWER_AC_STATE, Map.of("state", powerACState.name().toLowerCase(ROOT))); + } + + /** + * This method forms a {@link Map} of parameters for the toggling of wifi. + * + * @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 Map.entry(TOGGLE_WIFI, Map.of()); + } + + /** + * This method forms a {@link Map} of parameters for the toggle airplane mode. + * + * @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 Map.entry(TOGGLE_AIRPLANE_MODE, Map.of()); + } + + /** + * This method forms a {@link Map} of parameters for the toggle data. + * + * @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 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 new file mode 100644 index 000000000..dd6ee2b2f --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AndroidStartScreenRecordingOptions.java @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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; + private String videoSize; + private Boolean isBugReportEnabled; + + public static AndroidStartScreenRecordingOptions startScreenRecordingOptions() { + return new AndroidStartScreenRecordingOptions(); + } + + /** + * The video bit rate for the video, in megabits per second. + * The default value is 4000000 (4 Mb/s) for Android API level below 27 + * and 20000000 (20 Mb/s) for API level 27 and above. + * You can increase the bit rate to improve video quality, + * but doing so results in larger movie files. + * + * @param bitRate The actual bit rate (Mb/s). + * @return self instance for chaining. + */ + public AndroidStartScreenRecordingOptions withBitRate(int bitRate) { + this.bitRate = bitRate; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AndroidStartScreenRecordingOptions withUploadOptions(ScreenRecordingUploadOptions uploadOptions) { + return (AndroidStartScreenRecordingOptions) super.withUploadOptions(uploadOptions); + } + + /** + * The video size of the generated media file. The format is WIDTHxHEIGHT. + * The default value is the device's native display resolution (if supported), + * 1280x720 if not. For best results, + * use a size supported by your device's Advanced Video Coding (AVC) encoder. + * + * @param videoSize The actual video size: WIDTHxHEIGHT. + * @return self instance for chaining. + */ + public AndroidStartScreenRecordingOptions withVideoSize(String videoSize) { + this.videoSize = videoSize; + return this; + } + + /** + * Makes the recorder to display an additional information on the video overlay, + * such as a timestamp, that is helpful in videos captured to illustrate bugs. + * This option is only supported since API level 27 (Android P). + * + * @return self instance for chaining. + */ + public AndroidStartScreenRecordingOptions enableBugReport() { + this.isBugReportEnabled = true; + return this; + } + + /** + * The maximum recording time. The default and maximum value is 180 seconds (3 minutes). + * Setting values greater than this or less than zero will cause an exception. The minimum + * time resolution unit is one second. + * + *

Since Appium 1.8.2 the time limit can be up to 1800 seconds (30 minutes). + * Appium will automatically try to merge the 3-minutes chunks recorded + * by the screenrecord utility, however, this requires FFMPEG utility + * to be installed and available in PATH on the server machine. If the utility is not + * present then the most recent screen recording chunk is going to be returned as the result.

+ * + * @param timeLimit The actual time limit of the recorded video. + * @return self instance for chaining. + */ + @Override + public AndroidStartScreenRecordingOptions withTimeLimit(Duration timeLimit) { + return super.withTimeLimit(timeLimit); + } + + @Override + public Map 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/events/api/general/AppiumWebDriverEventListener.java b/src/main/java/io/appium/java_client/android/AndroidStopScreenRecordingOptions.java similarity index 63% rename from src/main/java/io/appium/java_client/events/api/general/AppiumWebDriverEventListener.java rename to src/main/java/io/appium/java_client/android/AndroidStopScreenRecordingOptions.java index 5b249b3f2..83b7ead33 100644 --- a/src/main/java/io/appium/java_client/events/api/general/AppiumWebDriverEventListener.java +++ b/src/main/java/io/appium/java_client/android/AndroidStopScreenRecordingOptions.java @@ -14,12 +14,15 @@ * limitations under the License. */ -package io.appium.java_client.events.api.general; +package io.appium.java_client.android; -import io.appium.java_client.events.api.Listener; -import org.openqa.selenium.support.events.WebDriverEventListener; +import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; + +public class AndroidStopScreenRecordingOptions extends + BaseStopScreenRecordingOptions { + + public static AndroidStopScreenRecordingOptions stopScreenRecordingOptions() { + return new AndroidStopScreenRecordingOptions(); + } -public interface AppiumWebDriverEventListener extends Listener, WebDriverEventListener, ListensToException, - SearchingEventListener, NavigationEventListener, - JavaScriptEventListener, ElementEventListener { } diff --git a/src/main/java/io/appium/java_client/android/AndroidTouchAction.java b/src/main/java/io/appium/java_client/android/AndroidTouchAction.java new file mode 100644 index 000000000..0fa31cffa --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AndroidTouchAction.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.android; + +import io.appium.java_client.PerformsTouchActions; +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) { + super(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 new file mode 100644 index 000000000..611fb30ed --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java @@ -0,0 +1,30 @@ +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.fingerPrintCommand; + +public interface AuthenticatesByFinger extends ExecutesMethod, CanRememberExtensionPresence { + + /** + * Authenticate users by using their finger print scans on supported emulators. + * + * @param fingerPrintId finger prints stored in Android Keystore system (from 1 to 10) + */ + default void fingerPrint(int 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/GsmCallActions.java b/src/main/java/io/appium/java_client/android/GsmCallActions.java new file mode 100644 index 000000000..443d85a09 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/GsmCallActions.java @@ -0,0 +1,5 @@ +package io.appium.java_client.android; + +public enum GsmCallActions { + CALL, ACCEPT, CANCEL, HOLD +} diff --git a/src/main/java/io/appium/java_client/android/GsmSignalStrength.java b/src/main/java/io/appium/java_client/android/GsmSignalStrength.java new file mode 100644 index 000000000..a73513df2 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/GsmSignalStrength.java @@ -0,0 +1,5 @@ +package io.appium.java_client.android; + +public enum GsmSignalStrength { + NONE_OR_UNKNOWN, POOR, MODERATE, GOOD, GREAT +} diff --git a/src/main/java/io/appium/java_client/android/GsmVoiceState.java b/src/main/java/io/appium/java_client/android/GsmVoiceState.java new file mode 100644 index 000000000..e92cde951 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/GsmVoiceState.java @@ -0,0 +1,5 @@ +package io.appium.java_client.android; + +public enum GsmVoiceState { + ON, OFF, DENIED, SEARCHING, ROAMING, HOME, UNREGISTERED +} diff --git a/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java b/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java new file mode 100644 index 000000000..8b7018e76 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/HasAndroidClipboard.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; + +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.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 { + /** + * Set the content of device's clipboard. + * + * @param label clipboard data label. + * @param contentType one of supported content types. + * @param base64Content base64-encoded content to be set. + */ + default void setClipboard(String label, ClipboardContentType contentType, byte[] base64Content) { + 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) + ) + )); + } + + /** + * Set the clipboard text. + * + * @param label clipboard data label. + * @param text The actual text to be set. + */ + default void setClipboardText(String label, String text) { + setClipboard(label, ClipboardContentType.PLAINTEXT, Base64 + .getEncoder() + .encode(text.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java b/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java new file mode 100644 index 000000000..3e230b3a3 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java @@ -0,0 +1,45 @@ +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.getDisplayDensityCommand; +import static io.appium.java_client.android.AndroidMobileCommandHelper.getSystemBarsCommand; + +public interface HasAndroidDeviceDetails extends ExecutesMethod, CanRememberExtensionPresence { + + /** + Retrieve the display density of the Android device. + + @return The density value in dpi + */ + default Long getDisplayDensity() { + 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()); + } + } + + /** + Retrieve visibility and bounds information of the status and navigation bars. + + @return The map where keys are bar types and values are mappings of bar properties. + */ + default Map> getSystemBars() { + 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 220dfcb48..73900e050 100644 --- a/src/main/java/io/appium/java_client/android/HasAndroidSettings.java +++ b/src/main/java/io/appium/java_client/android/HasAndroidSettings.java @@ -16,13 +16,12 @@ package io.appium.java_client.android; - import io.appium.java_client.HasSettings; import io.appium.java_client.Setting; 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()` @@ -31,58 +30,152 @@ interface HasAndroidSettings extends HasSettings { * by the system), in an attempt to make things less confusing or faster. * * @param compress ignores unimportant views if true, doesn't ignore otherwise. + * @return self instance for chaining */ - default void ignoreUnimportantViews(Boolean compress) { - setSetting(Setting.IGNORE_UNIMPORTANT_VIEWS, compress); + default HasAndroidSettings ignoreUnimportantViews(Boolean compress) { + return (HasAndroidSettings) setSetting(Setting.IGNORE_UNIMPORTANT_VIEWS, compress); } /** - * invoke {@code setWaitForIdleTimeout} in {@code com.android.uiautomator.core.Configurator} + * invoke {@code setWaitForIdleTimeout} in {@code com.android.uiautomator.core.Configurator}. * * @param timeout A negative value would reset to its default value. Minimum time unit * resolution is one millisecond + * @return self instance for chaining */ - default void configuratorSetWaitForIdleTimeout(Duration timeout) { - setSetting(Setting.WAIT_FOR_IDLE_TIMEOUT, timeout.toMillis()); + default HasAndroidSettings configuratorSetWaitForIdleTimeout(Duration timeout) { + return (HasAndroidSettings) setSetting(Setting.WAIT_FOR_IDLE_TIMEOUT, timeout.toMillis()); } /** - * invoke {@code setWaitForSelectorTimeout} in {@code com.android.uiautomator.core.Configurator} + * invoke {@code setWaitForSelectorTimeout} in {@code com.android.uiautomator.core.Configurator}. * * @param timeout A negative value would reset to its default value. Minimum time unit * resolution is one millisecond + * @return self instance for chaining */ - default void configuratorSetWaitForSelectorTimeout(Duration timeout) { - setSetting(Setting.WAIT_FOR_SELECTOR_TIMEOUT, timeout.toMillis()); + default HasAndroidSettings configuratorSetWaitForSelectorTimeout(Duration timeout) { + return (HasAndroidSettings) setSetting(Setting.WAIT_FOR_SELECTOR_TIMEOUT, timeout.toMillis()); } /** - * invoke {@code setScrollAcknowledgmentTimeout} in {@code com.android.uiautomator.core.Configurator} + * invoke {@code setScrollAcknowledgmentTimeout} in {@code com.android.uiautomator.core.Configurator}. * * @param timeout A negative value would reset to its default value. Minimum time unit * resolution is one millisecond + * @return self instance for chaining */ - default void configuratorSetScrollAcknowledgmentTimeout(Duration timeout) { - setSetting(Setting.WAIT_SCROLL_ACKNOWLEDGMENT_TIMEOUT, timeout.toMillis()); + default HasAndroidSettings configuratorSetScrollAcknowledgmentTimeout(Duration timeout) { + return (HasAndroidSettings) setSetting(Setting.WAIT_SCROLL_ACKNOWLEDGMENT_TIMEOUT, timeout.toMillis()); } /** - * invoke {@code configuratorSetKeyInjectionDelay} in {@code com.android.uiautomator.core.Configurator} + * invoke {@code configuratorSetKeyInjectionDelay} in {@code com.android.uiautomator.core.Configurator}. * * @param delay A negative value would reset to its default value. Minimum time unit * resolution is one millisecond + * @return self instance for chaining */ - default void configuratorSetKeyInjectionDelay(Duration delay) { - setSetting(Setting.KEY_INJECTION_DELAY, delay.toMillis()); + default HasAndroidSettings configuratorSetKeyInjectionDelay(Duration delay) { + return (HasAndroidSettings) setSetting(Setting.KEY_INJECTION_DELAY, delay.toMillis()); } /** - * invoke {@code setActionAcknowledgmentTimeout} in {@code com.android.uiautomator.core.Configurator} + * invoke {@code setActionAcknowledgmentTimeout} in {@code com.android.uiautomator.core.Configurator}. * * @param timeout A negative value would reset to its default value. Minimum time unit * resolution is one millisecond + * @return self instance for chaining + */ + default HasAndroidSettings configuratorSetActionAcknowledgmentTimeout(Duration timeout) { + return (HasAndroidSettings) setSetting(Setting.WAIT_ACTION_ACKNOWLEDGMENT_TIMEOUT, timeout.toMillis()); + } + + /** + * Setting this value to true will enforce source tree dumper + * to transliterate all class names used as XML tags to the limited + * set of ASCII characters supported by Apache Harmony + * lib and used by default in Android to avoid possible + * XML parsing exceptions caused by XPath lookup. + * The Unicode to ASCII transliteration is based on + * JUnidecode library (https://github.com/gcardone/junidecode). + * Works for UIAutomator2 only. + * + * @param enabled Either true or false. The default value if false. + * @return self instance for chaining + */ + default HasAndroidSettings normalizeTagNames(boolean enabled) { + return (HasAndroidSettings) setSetting(Setting.NORMALIZE_TAG_NAMES, enabled); + } + + /** + * Whether to return compact (standards-compliant) and faster responses in find element/s + * (the default setting). If set to false then the response may also contain other + * available element attributes. + * + * @param enabled Either true or false. The default value if true. + * @return self instance for chaining + */ + default HasAndroidSettings setShouldUseCompactResponses(boolean enabled) { + return (HasAndroidSettings) setSetting(Setting.SHOULD_USE_COMPACT_RESPONSES, enabled); + } + + /** + * Which attributes should be returned if compact responses are disabled. + * It works only if shouldUseCompactResponses is false. Defaults to "" (empty string). + * + * @param attrNames The comma-separated list of fields to return with each element. + * @return self instance for chaining + */ + default HasAndroidSettings setElementResponseAttributes(String attrNames) { + return (HasAndroidSettings) setSetting(Setting.ELEMENT_RESPONSE_ATTRIBUTES, attrNames); + } + + /** + * Set whether the source output/xpath search should consider all elements, visible and invisible. + * Disabling this setting speeds up source and xml search. Works for UIAutomator2 only. + * + * @param enabled Either true or false. The default value if false. + * @return self instance for chaining + */ + default HasAndroidSettings allowInvisibleElements(boolean enabled) { + return (HasAndroidSettings) setSetting(Setting.ALLOW_INVISIBLE_ELEMENTS, enabled); + } + + /** + * Whether to enable or disable the notification listener. + * No toast notifications are going to be added into page source output if + * this setting is disabled. + * Works for UIAutomator2 only. + * + * @param enabled Either true or false. The default value if true. + * @return self instance for chaining + */ + default HasAndroidSettings enableNotificationListener(boolean enabled) { + return (HasAndroidSettings) setSetting(Setting.ENABLE_NOTIFICATION_LISTENER, enabled); + } + + /** + * Whether to enable or disable shutdown the server through + * the broadcast receiver on ACTION_POWER_DISCONNECTED. + * + * @param enabled Either true or false. The default value if true. + * @return self instance for chaining + */ + default HasAndroidSettings shutdownOnPowerDisconnect(boolean enabled) { + return (HasAndroidSettings) setSetting(Setting.SHUTDOWN_ON_POWER_DISCONNECT, enabled); + } + + /** + * Turn on or off the tracking of scroll events as they happen. + * If {@code true}, a field {@code lastScrollData} is added to the results of + * {@code getSession}, which can then be used to check on scroll progress. + * Turning this feature off significantly increases touch action performance. + * + * @param enabled Either true or false. The default value if true. + * @return self instance for chaining */ - default void configuratorSetActionAcknowledgmentTimeout(Duration timeout) { - setSetting(Setting.WAIT_ACTION_ACKNOWLEDGMENT_TIMEOUT, timeout.toMillis()); + default HasAndroidSettings setTrackScrollEvents(boolean enabled) { + return (HasAndroidSettings) setSetting(Setting.TRACK_SCROLL_EVENTS, enabled); } } diff --git a/src/main/java/io/appium/java_client/android/HasDeviceDetails.java b/src/main/java/io/appium/java_client/android/HasDeviceDetails.java deleted file mode 100644 index 30b28cefe..000000000 --- a/src/main/java/io/appium/java_client/android/HasDeviceDetails.java +++ /dev/null @@ -1,35 +0,0 @@ -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 static io.appium.java_client.android.AndroidMobileCommandHelper.isKeyboardShownCommand; - -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; - -import java.util.Map; - -public interface HasDeviceDetails extends ExecutesMethod { - /* - Retrieve the display density of the Android device. - */ - default Long getDisplayDensity() { - return CommandExecutionHelper.execute(this, getDisplayDensityCommand()); - } - - /* - Retrieve visibility and bounds information of the status and navigation bars. - */ - default Map getSystemBars() { - return CommandExecutionHelper.execute(this, getSystemBarsCommand()); - } - - /** - * Check if the keyboard is displayed. - * - * @return true if keyboard is displayed. False otherwise - */ - default boolean isKeyboardShown() { - return CommandExecutionHelper.execute(this, isKeyboardShownCommand()); - } -} diff --git a/src/main/java/io/appium/java_client/android/HasNetworkConnection.java b/src/main/java/io/appium/java_client/android/HasNetworkConnection.java deleted file mode 100644 index fde98c3b5..000000000 --- a/src/main/java/io/appium/java_client/android/HasNetworkConnection.java +++ /dev/null @@ -1,56 +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.getNetworkConnectionCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.setConnectionCommand; - -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.WebDriverException; - -public interface HasNetworkConnection extends ExecutesMethod { - - /** - * Set the network connection of the device. - * - * @param connection The bitmask of the desired connection - */ - default void setConnection(Connection connection) { - CommandExecutionHelper.execute(this, setConnectionCommand(connection)); - } - - - /** - * Get the current network settings of the device. - * - * @return Connection object will let you inspect the status - * of None, AirplaneMode, Wifi, Data and All connections - */ - default Connection getConnection() { - long bitMask = CommandExecutionHelper.execute(this, getNetworkConnectionCommand()); - Connection[] types = Connection.values(); - - for (Connection connection: types) { - if (connection.bitMask == bitMask) { - return connection; - } - } - throw new WebDriverException("The unknown network connection " - + "type has been returned. The bitmask is " + bitMask); - } -} 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 e747acecc..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() + ); + } } /** @@ -48,11 +59,19 @@ default List getSupportedPerformanceDataTypes() { * [1478095200, null, null, 10079213, 19962, 2487705, 20015, 0, 3600], * [1478098800, null, null, 4444433, 10227, 1430356, 10493, 0, 3600]] * in case of cpu info : [[user, kernel], [0.9, 1.3]] - * @throws Exception if the performance data type is not supported, thows Error */ - default List> getPerformanceData(String packageName, String dataType, int dataReadTimeout) - throws Exception { - return CommandExecutionHelper.execute(this, - getPerformanceDataCommand(packageName, dataType, dataReadTimeout)); + default List> getPerformanceData(String packageName, String dataType, int 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 new file mode 100644 index 000000000..d5051da0e --- /dev/null +++ b/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java @@ -0,0 +1,134 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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.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(); + + /** + * Start logcat messages broadcast via web socket. + * This method assumes that Appium server is running on localhost and + * is assigned to the default port (4723). + */ + default void startLogcatBroadcast() { + startLogcatBroadcast("127.0.0.1"); + } + + /** + * Start logcat messages broadcast via web socket. + * This method assumes that Appium server is assigned to the default port (4723). + * + * @param host the name of the host where Appium server is running + */ + default void startLogcatBroadcast(String host) { + startLogcatBroadcast(host, DEFAULT_APPIUM_PORT); + } + + /** + * Start logcat messages broadcast via web socket. + * + * @param host the name of the host where Appium server is running + * @param port the port of the host where Appium server is running + */ + default void startLogcatBroadcast(String host, int port) { + 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)); + } + + /** + * Adds a new log messages broadcasting handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which accepts a single argument, which is the actual log message + */ + default void addLogcatMessagesListener(Consumer handler) { + getLogcatClient().addMessageHandler(handler); + } + + /** + * Adds a new log broadcasting errors handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which accepts a single argument, which is the actual exception instance + */ + default void addLogcatErrorsListener(Consumer handler) { + getLogcatClient().addErrorHandler(handler); + } + + /** + * Adds a new log broadcasting connection handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which is executed as soon as the client is successfully + * connected to the web socket + */ + default void addLogcatConnectionListener(Runnable handler) { + getLogcatClient().addConnectionHandler(handler); + } + + /** + * Adds a new log broadcasting disconnection handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which is executed as soon as the client is successfully + * disconnected from the web socket + */ + default void addLogcatDisconnectionListener(Runnable handler) { + getLogcatClient().addDisconnectionHandler(handler); + } + + /** + * Removes all existing logcat handlers. + */ + default void removeAllLogcatListeners() { + getLogcatClient().removeAllHandlers(); + } + + /** + * Stops logcat messages broadcast via web socket. + */ + default void stopLogcatBroadcast() { + removeAllLogcatListeners(); + CommandExecutionHelper.executeScript(this, "mobile: stopLogsBroadcast"); + } +} diff --git a/src/main/java/io/appium/java_client/android/LocksAndroidDevice.java b/src/main/java/io/appium/java_client/android/LocksAndroidDevice.java deleted file mode 100644 index f5b46d00e..000000000 --- a/src/main/java/io/appium/java_client/android/LocksAndroidDevice.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.android; - -import static io.appium.java_client.MobileCommand.lockDeviceCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.isLockedCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.unlockCommand; -import static java.time.Duration.ofMillis; - -import io.appium.java_client.CommandExecutionHelper; -import io.appium.java_client.ExecutesMethod; - -import java.time.Duration; - -public interface LocksAndroidDevice extends ExecutesMethod { - /** - * Check if the device is locked. - * - * @return true if device is locked. False otherwise - */ - default boolean isLocked() { - return CommandExecutionHelper.execute(this, isLockedCommand()); - } - - /** - * This method locks a device. - */ - default void lockDevice() { - CommandExecutionHelper.execute(this, lockDeviceCommand(ofMillis(0))); - } - - /** - * This method unlocks a device. - */ - default void unlockDevice() { - CommandExecutionHelper.execute(this, unlockCommand()); - } -} diff --git a/src/main/java/io/appium/java_client/android/NetworkSpeed.java b/src/main/java/io/appium/java_client/android/NetworkSpeed.java new file mode 100644 index 000000000..64cdc513d --- /dev/null +++ b/src/main/java/io/appium/java_client/android/NetworkSpeed.java @@ -0,0 +1,5 @@ +package io.appium.java_client.android; + +public enum NetworkSpeed { + GSM, SCSD, GPRS, EDGE, UMTS, HSDPA, LTE, EVDO, FULL +} diff --git a/src/main/java/io/appium/java_client/android/PowerACState.java b/src/main/java/io/appium/java_client/android/PowerACState.java new file mode 100644 index 000000000..ab845c925 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/PowerACState.java @@ -0,0 +1,5 @@ +package io.appium.java_client.android; + +public enum PowerACState { + ON, OFF +} 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 e9708e5fa..000000000 --- a/src/main/java/io/appium/java_client/android/PushesFiles.java +++ /dev/null @@ -1,60 +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.android.AndroidMobileCommandHelper.pushFileCommandCommand; - -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. - * - * @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 - */ - default void pushFile(String remotePath, byte[] base64Data) { - CommandExecutionHelper.execute(this, pushFileCommandCommand(remotePath, base64Data)); - } - - /** - * Saves given file as a file on the remote mobile device. - * - * @param remotePath Path to file to write data to on remote device - * @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 new file mode 100644 index 000000000..2992e5847 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java @@ -0,0 +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; + +public interface SupportsNetworkStateManagement extends ExecutesMethod, CanRememberExtensionPresence { + + /** + * Toggles Wifi on and off. + */ + default void toggleWifi() { + 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 Android versions below + * 6 and above 10. + */ + default void toggleAirplaneMode() { + 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 Emulators and real devices + * running Android version above 10. + */ + default void toggleData() { + 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 new file mode 100644 index 000000000..bb618b8cd --- /dev/null +++ b/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java @@ -0,0 +1,182 @@ +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.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, CanRememberExtensionPresence { + + /** + * Emulate send SMS event on the connected emulator. + * + * @param phoneNumber The phone number of message sender. + * @param message The message content. + */ + default void sendSMS(String phoneNumber, String 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 gsmCallAction One of available {@link GsmCallActions} values. + */ + 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) + )) + ); + } + } + + /** + * Emulate GSM signal strength change event on the connected emulator. + * + * @param gsmSignalStrength One of available {@link GsmSignalStrength} values. + */ + default void setGsmSignalStrength(GsmSignalStrength 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() + )) + ); + } + } + + /** + * Emulate GSM voice event on the connected emulator. + * + * @param gsmVoiceState One of available {@link GsmVoiceState} values. + */ + default void setGsmVoice(GsmVoiceState 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) + )) + ); + } + } + + /** + * Emulate network speed change event on the connected emulator. + * + * @param networkSpeed One of available {@link NetworkSpeed} values. + */ + default void setNetworkSpeed(NetworkSpeed 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) + )) + ); + } + } + + /** + * Emulate power capacity change on the connected emulator. + * + * @param percent Percentage value in range [0, 100]. + */ + default void setPowerCapacity(int 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 + )) + ); + } + } + + /** + * Emulate power state change on the connected emulator. + * + * @param powerACState One of available {@link PowerACState} values. + */ + default void setPowerAC(PowerACState 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 new file mode 100644 index 000000000..216641b84 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/appmanagement/AndroidInstallApplicationOptions.java @@ -0,0 +1,150 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.appmanagement; + +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; + private Duration timeout; + private Boolean allowTestPackages; + private Boolean useSdcard; + private Boolean grantPermissions; + + /** + * Enables the possibility to upgrade/reinstall the application + * if it is already present on the device (the default behavior). + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withReplaceEnabled() { + this.replace = true; + return this; + } + + /** + * Disables the possibility to upgrade/reinstall the application + * if it is already present on the device. + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withReplaceDisabled() { + this.replace = false; + return this; + } + + /** + * The time to wait until the app is installed (60000ms by default). + * + * @param timeout the actual timeout value. The minimum time resolution + * unit is one millisecond. + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withTimeout(Duration timeout) { + checkArgument(!requireNonNull(timeout).isNegative(), "The timeout value cannot be negative"); + this.timeout = timeout; + return this; + } + + /** + * Allows to install packages marked as test in the manifest. + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withAllowTestPackagesEnabled() { + this.allowTestPackages = true; + return this; + } + + /** + * Disables a possibility to install packages marked as test in + * the manifest (the default setting). + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withAllowTestPackagesDisabled() { + this.allowTestPackages = false; + return this; + } + + /** + * Forces the application to be installed of SD card + * instead of the internal memory. + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withUseSdcardEnabled() { + this.useSdcard = true; + return this; + } + + /** + * Forces the application to be installed to the internal memory + * (the default behavior). + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withUseSdcardDisabled() { + this.useSdcard = false; + return this; + } + + /** + * Grants all the permissions requested in the + * application's manifest automatically after the installation + * is completed under Android 6+. + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withGrantPermissionsEnabled() { + this.grantPermissions = true; + return this; + } + + /** + * Does not grant all the permissions requested in the + * application's manifest automatically after the installation + * is completed (the default behavior). + * + * @return self instance for chaining. + */ + public AndroidInstallApplicationOptions withGrantPermissionsDisabled() { + this.grantPermissions = false; + return this; + } + + @Override + public Map 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 new file mode 100644 index 000000000..fe68a0073 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/appmanagement/AndroidRemoveApplicationOptions.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.android.appmanagement; + +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; + private Boolean keepData; + + /** + * The time to wait until the app is removed (20000ms by default). + * + * @param timeout the actual timeout value. The minimum time resolution + * unit is one millisecond. + * @return self instance for chaining. + */ + public AndroidRemoveApplicationOptions withTimeout(Duration timeout) { + checkArgument(!requireNonNull(timeout).isNegative(), + "The timeout value cannot be negative"); + this.timeout = timeout; + return this; + } + + /** + * Forces uninstall to keep the application data and caches. + * + * @return self instance for chaining. + */ + public AndroidRemoveApplicationOptions withKeepDataEnabled() { + this.keepData = true; + return this; + } + + /** + * Forces uninstall to delete the application data and caches + * (the default behavior). + * + * @return self instance for chaining. + */ + public AndroidRemoveApplicationOptions withKeepDataDisabled() { + this.keepData = false; + return this; + } + + @Override + public Map 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 new file mode 100644 index 000000000..b683c5a8f --- /dev/null +++ b/src/main/java/io/appium/java_client/android/appmanagement/AndroidTerminateApplicationOptions.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.appmanagement; + +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; + + /** + * The time to wait until the app is terminated (500ms by default). + * + * @param timeout the actual timeout value. The minimum time resolution + * unit is one millisecond. + * @return self instance for chaining. + */ + public AndroidTerminateApplicationOptions withTimeout(Duration timeout) { + checkArgument(!requireNonNull(timeout).isNegative(), "The timeout value cannot be negative"); + this.timeout = timeout; + return this; + } + + @Override + public Map 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/ConnectionState.java b/src/main/java/io/appium/java_client/android/connection/ConnectionState.java new file mode 100644 index 000000000..7b0134de6 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/connection/ConnectionState.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.connection; + +public class ConnectionState { + public static final long AIRPLANE_MODE_MASK = 0b001; + public static final long WIFI_MASK = 0b010; + public static final long DATA_MASK = 0b100; + + private final long bitMask; + + public long getBitMask() { + return bitMask; + } + + public ConnectionState(long bitMask) { + this.bitMask = bitMask; + } + + /** + * Is airplane mode enabled or not. + * + * @return true if airplane mode is enabled. + */ + public boolean isAirplaneModeEnabled() { + return (bitMask & AIRPLANE_MODE_MASK) != 0; + } + + /** + * Is Wi-Fi connection enabled or not. + * + * @return true if Wi-Fi connection is enabled. + */ + public boolean isWiFiEnabled() { + return (bitMask & WIFI_MASK) != 0; + } + + /** + * Is data connection enabled or not. + * + * @return true if data connection is enabled. + */ + public boolean isDataEnabled() { + return (bitMask & DATA_MASK) != 0; + } +} + diff --git a/src/main/java/io/appium/java_client/android/connection/ConnectionStateBuilder.java b/src/main/java/io/appium/java_client/android/connection/ConnectionStateBuilder.java new file mode 100644 index 000000000..66bbed50e --- /dev/null +++ b/src/main/java/io/appium/java_client/android/connection/ConnectionStateBuilder.java @@ -0,0 +1,127 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.connection; + +import static io.appium.java_client.android.connection.ConnectionState.AIRPLANE_MODE_MASK; +import static io.appium.java_client.android.connection.ConnectionState.DATA_MASK; +import static io.appium.java_client.android.connection.ConnectionState.WIFI_MASK; + +public class ConnectionStateBuilder { + private long bitMask; + + /** + * Initializes connection state builder with the default value (all off). + */ + public ConnectionStateBuilder() { + this.bitMask = 0; + } + + /** + * Initializes connection state builder with the the predefined bit mask. + * This constructor might be handy to change an existing connection state. + * + * @param bitMask the actual initial state bit mask to set + */ + public ConnectionStateBuilder(long bitMask) { + this.bitMask = bitMask; + } + + /** + * Initializes connection state builder with the the predefined bit mask. + * This constructor might be handy to change an existing connection state. + * + * @param state the actual initial state to set + */ + public ConnectionStateBuilder(ConnectionState state) { + this(state.getBitMask()); + } + + /** + * Sets airplane mode to enabled state if it was disabled. + * This option only works up to Android 6. + * Enabling the airplane mode on the device will automatically + * disable Wi-Fi and data connections. + * + * @return self instance for chaining + */ + public ConnectionStateBuilder withAirplaneModeEnabled() { + bitMask |= AIRPLANE_MODE_MASK; + return this; + } + + /** + * Sets airplane mode to disabled state if it was enabled. + * This option only works up to Android 6. + * + * @return self instance for chaining + */ + public ConnectionStateBuilder withAirplaneModeDisabled() { + bitMask &= ~AIRPLANE_MODE_MASK; + return this; + } + + /** + * Sets Wi-Fi connection mode to enabled state if it was disabled. + * + * @return self instance for chaining + */ + public ConnectionStateBuilder withWiFiEnabled() { + bitMask |= WIFI_MASK; + return this; + } + + /** + * Sets Wi-Fi connection mode to disabled state if it was enabled. + * + * @return self instance for chaining + */ + public ConnectionStateBuilder withWiFiDisabled() { + bitMask &= ~WIFI_MASK; + return this; + } + + /** + * Sets data connection mode to enabled state if it was disabled. + * This option only works on rooted devices or on emulators. + * + * @return self instance for chaining + */ + public ConnectionStateBuilder withDataEnabled() { + bitMask |= DATA_MASK; + return this; + } + + /** + * Sets data connection mode to disabled state if it was enabled. + * This option only works on rooted devices or on emulators. + * + * @return self instance for chaining + */ + public ConnectionStateBuilder withDataDisabled() { + bitMask &= ~DATA_MASK; + return this; + } + + /** + * Builds connection state instance, which is ready to be passed as Appium server parameter. + * + * @return ConnectionState instance + */ + public ConnectionState build() { + return new ConnectionState(bitMask); + } +} 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 new file mode 100644 index 000000000..a00693af3 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.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.connection; + +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, CanRememberExtensionPresence { + + /** + * Set the network connection of the device. + * + * @param connection The bitmask of the desired connection + * @return Connection object, which represents the resulting state + */ + default ConnectionState setConnection(ConnectionState connection) { + 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()) + ) + ) + ); + } + } + + /** + * Get the current network settings of the device. + * + * @return Connection object, which lets you to inspect the current status + */ + default ConnectionState getConnection() { + 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/ios/LocksIOSDevice.java b/src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.java similarity index 59% rename from src/main/java/io/appium/java_client/ios/LocksIOSDevice.java rename to src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.java index 007c03c73..0472a5bab 100644 --- a/src/main/java/io/appium/java_client/ios/LocksIOSDevice.java +++ b/src/main/java/io/appium/java_client/android/geolocation/SupportsExtendedGeolocationCommands.java @@ -14,24 +14,25 @@ * limitations under the License. */ -package io.appium.java_client.ios; - -import static io.appium.java_client.MobileCommand.lockDeviceCommand; +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.time.Duration; +import java.util.Map; -public interface LocksIOSDevice extends ExecutesMethod { +public interface SupportsExtendedGeolocationCommands extends ExecutesMethod { /** - * Lock the device (bring it to the lock screen) for a given number of - * seconds. + * Allows to set geo location with extended parameters + * available for Android platform. * - * @param duration for how long to lock the screen. Minimum time resolution is one second + * @param location The location object to set. */ - default void lockDevice(Duration duration) { - CommandExecutionHelper.execute(this, lockDeviceCommand(duration)); + 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 new file mode 100644 index 000000000..4138ea69f --- /dev/null +++ b/src/main/java/io/appium/java_client/android/nativekey/AndroidKey.java @@ -0,0 +1,1438 @@ +package io.appium.java_client.android.nativekey; + +public enum AndroidKey { + /** + * Key code constant: Unknown key code. + */ + UNKNOWN(0), + /** + * Key code constant: Soft Left key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom left + * of the display. + */ + SOFT_LEFT(1), + /** + * Key code constant: Soft Right key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom right + * of the display. + */ + SOFT_RIGHT(2), + /** + * Key code constant: Home key. + * This key is handled by the framework and is never delivered to applications. + */ + HOME(3), + /** + * Key code constant: Back key. + */ + BACK(4), + /** + * Key code constant: Call key. + */ + CALL(5), + /** + * Key code constant: End Call key. + */ + ENDCALL(6), + /** + * Key code constant: '0' key. + */ + DIGIT_0(7), + /** + * Key code constant: '1' key. + */ + DIGIT_1(8), + /** + * Key code constant: '2' key. + */ + DIGIT_2(9), + /** + * Key code constant: '3' key. + */ + DIGIT_3(10), + /** + * Key code constant: '4' key. + */ + DIGIT_4(11), + /** + * Key code constant: '5' key. + */ + DIGIT_5(12), + /** + * Key code constant: '6' key. + */ + DIGIT_6(13), + /** + * Key code constant: '7' key. + */ + DIGIT_7(14), + /** + * Key code constant: '8' key. + */ + DIGIT_8(15), + /** + * Key code constant: '9' key. + */ + DIGIT_9(16), + /** + * Key code constant: '*' key. + */ + STAR(17), + /** + * Key code constant: '#' key. + */ + POUND(18), + /** + * Key code constant: Directional Pad Up key. + * May also be synthesized from trackball motions. + */ + DPAD_UP(19), + /** + * Key code constant: Directional Pad Down key. + * May also be synthesized from trackball motions. + */ + DPAD_DOWN(20), + /** + * Key code constant: Directional Pad Left key. + * May also be synthesized from trackball motions. + */ + DPAD_LEFT(21), + /** + * Key code constant: Directional Pad Right key. + * May also be synthesized from trackball motions. + */ + DPAD_RIGHT(22), + /** + * Key code constant: Directional Pad Center key. + * May also be synthesized from trackball motions. + */ + DPAD_CENTER(23), + /** + * Key code constant: Volume Up key. + * Adjusts the speaker volume up. + */ + VOLUME_UP(24), + /** + * Key code constant: Volume Down key. + * Adjusts the speaker volume down. + */ + VOLUME_DOWN(25), + /** + * Key code constant: Power key. + */ + POWER(26), + /** + * Key code constant: Camera key. + * Used to launch a camera application or take pictures. + */ + CAMERA(27), + /** + * Key code constant: Clear key. + */ + CLEAR(28), + /** + * Key code constant: 'A' key. + */ + A(29), + /** + * Key code constant: 'B' key. + */ + B(30), + /** + * Key code constant: 'C' key. + */ + C(31), + /** + * Key code constant: 'D' key. + */ + D(32), + /** + * Key code constant: 'E' key. + */ + E(33), + /** + * Key code constant: 'F' key. + */ + F(34), + /** + * Key code constant: 'G' key. + */ + G(35), + /** + * Key code constant: 'H' key. + */ + H(36), + /** + * Key code constant: 'I' key. + */ + I(37), + /** + * Key code constant: 'J' key. + */ + J(38), + /** + * Key code constant: 'K' key. + */ + K(39), + /** + * Key code constant: 'L' key. + */ + L(40), + /** + * Key code constant: 'M' key. + */ + M(41), + /** + * Key code constant: 'N' key. + */ + N(42), + /** + * Key code constant: 'O' key. + */ + O(43), + /** + * Key code constant: 'P' key. + */ + P(44), + /** + * Key code constant: 'Q' key. + */ + Q(45), + /** + * Key code constant: 'R' key. + */ + R(46), + /** + * Key code constant: 'S' key. + */ + S(47), + /** + * Key code constant: 'T' key. + */ + T(48), + /** + * Key code constant: 'U' key. + */ + U(49), + /** + * Key code constant: 'V' key. + */ + V(50), + /** + * Key code constant: 'W' key. + */ + W(51), + /** + * Key code constant: 'X' key. + */ + X(52), + /** + * Key code constant: 'Y' key. + */ + Y(53), + /** + * Key code constant: 'Z' key. + */ + Z(54), + /** + * Key code constant: ',' key. + */ + COMMA(55), + /** + * Key code constant: '.' key. + */ + PERIOD(56), + /** + * Key code constant: Left Alt modifier key. + */ + ALT_LEFT(57), + /** + * Key code constant: Right Alt modifier key. + */ + ALT_RIGHT(58), + /** + * Key code constant: Left Shift modifier key. + */ + SHIFT_LEFT(59), + /** + * Key code constant: Right Shift modifier key. + */ + SHIFT_RIGHT(60), + /** + * Key code constant: Tab key. + */ + TAB(61), + /** + * Key code constant: Space key. + */ + SPACE(62), + /** + * Key code constant: Symbol modifier key. + * Used to enter alternate symbols. + */ + SYM(63), + /** + * Key code constant: Explorer special function key. + * Used to launch a browser application. + */ + EXPLORER(64), + /** + * Key code constant: Envelope special function key. + * Used to launch a mail application. + */ + ENVELOPE(65), + /** + * Key code constant: Enter key. + */ + ENTER(66), + /** + * Key code constant: Backspace key. + * Deletes characters before the insertion point, unlike {@link #FORWARD_DEL}. + */ + DEL(67), + /** + * Key code constant: '`' (backtick) key. + */ + GRAVE(68), + /** + * Key code constant: '-'. + */ + MINUS(69), + /** + * Key code constant: '=' key. + */ + EQUALS(70), + /** + * Key code constant: '[' key. + */ + LEFT_BRACKET(71), + /** + * Key code constant: ']' key. + */ + RIGHT_BRACKET(72), + /** + * Key code constant: '\' key. + */ + BACKSLASH(73), + /** + * Key code constant: ';' key. + */ + SEMICOLON(74), + /** + * Key code constant: ''' (apostrophe) key. + */ + APOSTROPHE(75), + /** + * Key code constant: '/' key. + */ + SLASH(76), + /** + * Key code constant: '@' key. + */ + AT(77), + /** + * Key code constant: Number modifier key. + * Used to enter numeric symbols. + * This key is not Num Lock; it is more like {@link #ALT_LEFT} and is + * interpreted as an ALT key + */ + NUM(78), + /** + * Key code constant: Headset Hook key. + * Used to hang up calls and stop media. + */ + HEADSETHOOK(79), + /** + * Key code constant: Camera Focus key. + * Used to focus the camera. + */ + FOCUS(80), // *Camera* focus + /** + * Key code constant: '+' key. + */ + PLUS(81), + /** + * Key code constant: Menu key. + */ + MENU(82), + /** + * Key code constant: Notification key. + */ + NOTIFICATION(83), + /** + * Key code constant: Search key. + */ + SEARCH(84), + /** + * Key code constant: Play/Pause media key. + */ + MEDIA_PLAY_PAUSE(85), + /** + * Key code constant: Stop media key. + */ + MEDIA_STOP(86), + /** + * Key code constant: Play Next media key. + */ + MEDIA_NEXT(87), + /** + * Key code constant: Play Previous media key. + */ + MEDIA_PREVIOUS(88), + /** + * Key code constant: Rewind media key. + */ + MEDIA_REWIND(89), + /** + * Key code constant: Fast Forward media key. + */ + MEDIA_FAST_FORWARD(90), + /** + * Key code constant: Mute key. + * Mutes the microphone, unlike {@link #VOLUME_MUTE}. + */ + MUTE(91), + /** + * Key code constant: Page Up key. + */ + PAGE_UP(92), + /** + * Key code constant: Page Down key. + */ + PAGE_DOWN(93), + /** + * Key code constant: Picture Symbols modifier key. + * Used to switch symbol sets (Emoji, Kao-moji). + */ + PICTSYMBOLS(94), // switch symbol-sets (Emoji,Kao-moji) + /** + * Key code constant: Switch Charset modifier key. + * Used to switch character sets (Kanji, Katakana). + */ + SWITCH_CHARSET(95), // switch char-sets (Kanji,Katakana) + /** + * Key code constant: A Button key. + * On a game controller, the A button should be either the button labeled A + * or the first button on the bottom row of controller buttons. + */ + BUTTON_A(96), + /** + * Key code constant: B Button key. + * On a game controller, the B button should be either the button labeled B + * or the second button on the bottom row of controller buttons. + */ + BUTTON_B(97), + /** + * Key code constant: C Button key. + * On a game controller, the C button should be either the button labeled C + * or the third button on the bottom row of controller buttons. + */ + BUTTON_C(98), + /** + * Key code constant: X Button key. + * On a game controller, the X button should be either the button labeled X + * or the first button on the upper row of controller buttons. + */ + BUTTON_X(99), + /** + * Key code constant: Y Button key. + * On a game controller, the Y button should be either the button labeled Y + * or the second button on the upper row of controller buttons. + */ + BUTTON_Y(100), + /** + * Key code constant: Z Button key. + * On a game controller, the Z button should be either the button labeled Z + * or the third button on the upper row of controller buttons. + */ + BUTTON_Z(101), + /** + * Key code constant: L1 Button key. + * On a game controller, the L1 button should be either the button labeled L1 (or L) + * or the top left trigger button. + */ + BUTTON_L1(102), + /** + * Key code constant: R1 Button key. + * On a game controller, the R1 button should be either the button labeled R1 (or R) + * or the top right trigger button. + */ + BUTTON_R1(103), + /** + * Key code constant: L2 Button key. + * On a game controller, the L2 button should be either the button labeled L2 + * or the bottom left trigger button. + */ + BUTTON_L2(104), + /** + * Key code constant: R2 Button key. + * On a game controller, the R2 button should be either the button labeled R2 + * or the bottom right trigger button. + */ + BUTTON_R2(105), + /** + * Key code constant: Left Thumb Button key. + * On a game controller, the left thumb button indicates that the left (or only) + * joystick is pressed. + */ + BUTTON_THUMBL(106), + /** + * Key code constant: Right Thumb Button key. + * On a game controller, the right thumb button indicates that the right + * joystick is pressed. + */ + BUTTON_THUMBR(107), + /** + * Key code constant: Start Button key. + * On a game controller, the button labeled Start. + */ + BUTTON_START(108), + /** + * Key code constant: Select Button key. + * On a game controller, the button labeled Select. + */ + BUTTON_SELECT(109), + /** + * Key code constant: Mode Button key. + * On a game controller, the button labeled Mode. + */ + BUTTON_MODE(110), + /** + * Key code constant: Escape key. + */ + ESCAPE(111), + /** + * Key code constant: Forward Delete key. + * Deletes characters ahead of the insertion point, unlike {@link #DEL}. + */ + FORWARD_DEL(112), + /** + * Key code constant: Left Control modifier key. + */ + CTRL_LEFT(113), + /** + * Key code constant: Right Control modifier key. + */ + CTRL_RIGHT(114), + /** + * Key code constant: Caps Lock key. + */ + CAPS_LOCK(115), + /** + * Key code constant: Scroll Lock key. + */ + SCROLL_LOCK(116), + /** + * Key code constant: Left Meta modifier key. + */ + META_LEFT(117), + /** + * Key code constant: Right Meta modifier key. + */ + META_RIGHT(118), + /** + * Key code constant: Function modifier key. + */ + FUNCTION(119), + /** + * Key code constant: System Request / Print Screen key. + */ + SYSRQ(120), + /** + * Key code constant: Break / Pause key. + */ + BREAK(121), + /** + * Key code constant: Home Movement key. + * Used for scrolling or moving the cursor around to the start of a line + * or to the top of a list. + */ + MOVE_HOME(122), + /** + * Key code constant: End Movement key. + * Used for scrolling or moving the cursor around to the end of a line + * or to the bottom of a list. + */ + MOVE_END(123), + /** + * Key code constant: Insert key. + * Toggles insert / overwrite edit mode. + */ + INSERT(124), + /** + * Key code constant: Forward key. + * Navigates forward in the history stack. Complement of {@link #BACK}. + */ + FORWARD(125), + /** + * Key code constant: Play media key. + */ + MEDIA_PLAY(126), + /** + * Key code constant: Pause media key. + */ + MEDIA_PAUSE(127), + /** + * Key code constant: Close media key. + * May be used to close a CD tray, for example. + */ + MEDIA_CLOSE(128), + /** + * Key code constant: Eject media key. + * May be used to eject a CD tray, for example. + */ + MEDIA_EJECT(129), + /** + * Key code constant: Record media key. + */ + MEDIA_RECORD(130), + /** + * Key code constant: F1 key. + */ + F1(131), + /** + * Key code constant: F2 key. + */ + F2(132), + /** + * Key code constant: F3 key. + */ + F3(133), + /** + * Key code constant: F4 key. + */ + F4(134), + /** + * Key code constant: F5 key. + */ + F5(135), + /** + * Key code constant: F6 key. + */ + F6(136), + /** + * Key code constant: F7 key. + */ + F7(137), + /** + * Key code constant: F8 key. + */ + F8(138), + /** + * Key code constant: F9 key. + */ + F9(139), + /** + * Key code constant: F10 key. + */ + F10(140), + /** + * Key code constant: F11 key. + */ + F11(141), + /** + * Key code constant: F12 key. + */ + F12(142), + /** + * Key code constant: Num Lock key. + * This is the Num Lock key; it is different from {@link #NUM}. + * This key alters the behavior of other keys on the numeric keypad. + */ + NUM_LOCK(143), + /** + * Key code constant: Numeric keypad '0' key. + */ + NUMPAD_0(144), + /** + * Key code constant: Numeric keypad '1' key. + */ + NUMPAD_1(145), + /** + * Key code constant: Numeric keypad '2' key. + */ + NUMPAD_2(146), + /** + * Key code constant: Numeric keypad '3' key. + */ + NUMPAD_3(147), + /** + * Key code constant: Numeric keypad '4' key. + */ + NUMPAD_4(148), + /** + * Key code constant: Numeric keypad '5' key. + */ + NUMPAD_5(149), + /** + * Key code constant: Numeric keypad '6' key. + */ + NUMPAD_6(150), + /** + * Key code constant: Numeric keypad '7' key. + */ + NUMPAD_7(151), + /** + * Key code constant: Numeric keypad '8' key. + */ + NUMPAD_8(152), + /** + * Key code constant: Numeric keypad '9' key. + */ + NUMPAD_9(153), + /** + * Key code constant: Numeric keypad '/' key (for division). + */ + NUMPAD_DIVIDE(154), + /** + * Key code constant: Numeric keypad '*' key (for multiplication). + */ + NUMPAD_MULTIPLY(155), + /** + * Key code constant: Numeric keypad '-' key (for subtraction). + */ + NUMPAD_SUBTRACT(156), + /** + * Key code constant: Numeric keypad '+' key (for addition). + */ + NUMPAD_ADD(157), + /** + * Key code constant: Numeric keypad '.' key (for decimals or digit grouping). + */ + NUMPAD_DOT(158), + /** + * Key code constant: Numeric keypad ',' key (for decimals or digit grouping). + */ + NUMPAD_COMMA(159), + /** + * Key code constant: Numeric keypad Enter key. + */ + NUMPAD_ENTER(160), + /** + * Key code constant: Numeric keypad '=' key. + */ + NUMPAD_EQUALS(161), + /** + * Key code constant: Numeric keypad '(' key. + */ + NUMPAD_LEFT_PAREN(162), + /** + * Key code constant: Numeric keypad ')' key. + */ + NUMPAD_RIGHT_PAREN(163), + /** + * Key code constant: Volume Mute key. + * Mutes the speaker, unlike {@link #MUTE}. + * This key should normally be implemented as a toggle such that the first press + * mutes the speaker and the second press restores the original volume. + */ + VOLUME_MUTE(164), + /** + * Key code constant: Info key. + * Common on TV remotes to show additional information related to what is + * currently being viewed. + */ + INFO(165), + /** + * Key code constant: Channel up key. + * On TV remotes, increments the television channel. + */ + CHANNEL_UP(166), + /** + * Key code constant: Channel down key. + * On TV remotes, decrements the television channel. + */ + CHANNEL_DOWN(167), + /** + * Key code constant: Zoom in key. + */ + KEYCODE_ZOOM_IN(168), + /** + * Key code constant: Zoom out key. + */ + KEYCODE_ZOOM_OUT(169), + /** + * Key code constant: TV key. + * On TV remotes, switches to viewing live TV. + */ + TV(170), + /** + * Key code constant: Window key. + * On TV remotes, toggles picture-in-picture mode or other windowing functions. + */ + WINDOW(171), + /** + * Key code constant: Guide key. + * On TV remotes, shows a programming guide. + */ + GUIDE(172), + /** + * Key code constant: DVR key. + * On some TV remotes, switches to a DVR mode for recorded shows. + */ + DVR(173), + /** + * Key code constant: Bookmark key. + * On some TV remotes, bookmarks content or web pages. + */ + BOOKMARK(174), + /** + * Key code constant: Toggle captions key. + * Switches the mode for closed-captioning text, for example during television shows. + */ + CAPTIONS(175), + /** + * Key code constant: Settings key. + * Starts the system settings activity. + */ + SETTINGS(176), + /** + * Key code constant: TV power key. + * On TV remotes, toggles the power on a television screen. + */ + TV_POWER(177), + /** + * Key code constant: TV input key. + * On TV remotes, switches the input on a television screen. + */ + TV_INPUT(178), + /** + * Key code constant: Set-top-box power key. + * On TV remotes, toggles the power on an external Set-top-box. + */ + STB_POWER(179), + /** + * Key code constant: Set-top-box input key. + * On TV remotes, switches the input mode on an external Set-top-box. + */ + STB_INPUT(180), + /** + * Key code constant: A/V Receiver power key. + * On TV remotes, toggles the power on an external A/V Receiver. + */ + AVR_POWER(181), + /** + * Key code constant: A/V Receiver input key. + * On TV remotes, switches the input mode on an external A/V Receiver. + */ + AVR_INPUT(182), + /** + * Key code constant: Red "programmable" key. + * On TV remotes, acts as a contextual/programmable key. + */ + PROG_RED(183), + /** + * Key code constant: Green "programmable" key. + * On TV remotes, actsas a contextual/programmable key. + */ + PROG_GREEN(184), + /** + * Key code constant: Yellow "programmable" key. + * On TV remotes, acts as a contextual/programmable key. + */ + PROG_YELLOW(185), + /** + * Key code constant: Blue "programmable" key. + * On TV remotes, acts as a contextual/programmable key. + */ + PROG_BLUE(186), + /** + * Key code constant: App switch key. + * Should bring up the application switcher dialog. + */ + APP_SWITCH(187), + /** + * Key code constant: Generic Game Pad Button #1. + */ + BUTTON_1(188), + /** + * Key code constant: Generic Game Pad Button #2. + */ + BUTTON_2(189), + /** + * Key code constant: Generic Game Pad Button #3. + */ + BUTTON_3(190), + /** + * Key code constant: Generic Game Pad Button #4. + */ + BUTTON_4(191), + /** + * Key code constant: Generic Game Pad Button #5. + */ + BUTTON_5(192), + /** + * Key code constant: Generic Game Pad Button #6. + */ + BUTTON_6(193), + /** + * Key code constant: Generic Game Pad Button #7. + */ + BUTTON_7(194), + /** + * Key code constant: Generic Game Pad Button #8. + */ + BUTTON_8(195), + /** + * Key code constant: Generic Game Pad Button #9. + */ + BUTTON_9(196), + /** + * Key code constant: Generic Game Pad Button #10. + */ + BUTTON_10(197), + /** + * Key code constant: Generic Game Pad Button #11. + */ + BUTTON_11(198), + /** + * Key code constant: Generic Game Pad Button #12. + */ + BUTTON_12(199), + /** + * Key code constant: Generic Game Pad Button #13. + */ + BUTTON_13(200), + /** + * Key code constant: Generic Game Pad Button #14. + */ + BUTTON_14(201), + /** + * Key code constant: Generic Game Pad Button #15. + */ + BUTTON_15(202), + /** + * Key code constant: Generic Game Pad Button #16. + */ + BUTTON_16(203), + /** + * Key code constant: Language Switch key. + * Toggles the current input language such as switching between English and Japanese on + * a QWERTY keyboard. On some devices, the same function may be performed by + * pressing Shift+Spacebar. + */ + LANGUAGE_SWITCH(204), + /** + * Key code constant: Manner Mode key. + * Toggles silent or vibrate mode on and off to make the device behave more politely + * in certain settings such as on a crowded train. On some devices, the key may only + * operate when long-pressed. + */ + MANNER_MODE(205), + /** + * Key code constant: 3D Mode key. + * Toggles the display between 2D and 3D mode. + */ + MODE_3D(206), + /** + * Key code constant: Contacts special function key. + * Used to launch an address book application. + */ + CONTACTS(207), + /** + * Key code constant: Calendar special function key. + * Used to launch a calendar application. + */ + CALENDAR(208), + /** + * Key code constant: Music special function key. + * Used to launch a music player application. + */ + MUSIC(209), + /** + * Key code constant: Calculator special function key. + * Used to launch a calculator application. + */ + CALCULATOR(210), + /** + * Key code constant: Japanese full-width / half-width key. + */ + ZENKAKU_HANKAKU(211), + /** + * Key code constant: Japanese alphanumeric key. + */ + EISU(212), + /** + * Key code constant: Japanese non-conversion key. + */ + MUHENKAN(213), + /** + * Key code constant: Japanese conversion key. + */ + HENKAN(214), + /** + * Key code constant: Japanese katakana / hiragana key. + */ + KATAKANA_HIRAGANA(215), + /** + * Key code constant: Japanese Yen key. + */ + YEN(216), + /** + * Key code constant: Japanese Ro key. + */ + RO(217), + /** + * Key code constant: Japanese kana key. + */ + KANA(218), + /** + * Key code constant: Assist key. + * Launches the global assist activity. Not delivered to applications. + */ + ASSIST(219), + /** + * Key code constant: Brightness Down key. + * Adjusts the screen brightness down. + */ + BRIGHTNESS_DOWN(220), + /** + * Key code constant: Brightness Up key. + * Adjusts the screen brightness up. + */ + BRIGHTNESS_UP(221), + /** + * Key code constant: Audio Track key. + * Switches the audio tracks. + */ + MEDIA_AUDIO_TRACK(222), + /** + * Key code constant: Sleep key. + * Puts the device to sleep. Behaves somewhat like {@link #POWER} but it + * has no effect if the device is already asleep. + */ + SLEEP(223), + /** + * Key code constant: Wakeup key. + * Wakes up the device. Behaves somewhat like {@link #POWER} but it + * has no effect if the device is already awake. + */ + WAKEUP(224), + /** + * Key code constant: Pairing key. + * Initiates peripheral pairing mode. Useful for pairing remote control + * devices or game controllers, especially if no other input mode is + * available. + */ + PAIRING(225), + /** + * Key code constant: Media Top Menu key. + * Goes to the top of media menu. + */ + MEDIA_TOP_MENU(226), + /** + * Key code constant: '11' key. + */ + KEY_11(227), + /** + * Key code constant: '12' key. + */ + KEY_12(228), + /** + * Key code constant: Last Channel key. + * Goes to the last viewed channel. + */ + LAST_CHANNEL(229), + /** + * Key code constant: TV data service key. + * Displays data services like weather, sports. + */ + TV_DATA_SERVICE(230), + /** + * Key code constant: Voice Assist key. + * Launches the global voice assist activity. Not delivered to applications. + */ + VOICE_ASSIST(231), + /** + * Key code constant: Radio key. + * Toggles TV service / Radio service. + */ + TV_RADIO_SERVICE(232), + /** + * Key code constant: Teletext key. + * Displays Teletext service. + */ + TV_TELETEXT(233), + /** + * Key code constant: Number entry key. + * Initiates to enter multi-digit channel nubmber when each digit key is assigned + * for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC + * User Control Code. + */ + TV_NUMBER_ENTRY(234), + /** + * Key code constant: Analog Terrestrial key. + * Switches to analog terrestrial broadcast service. + */ + TV_TERRESTRIAL_ANALOG(235), + /** + * Key code constant: Digital Terrestrial key. + * Switches to digital terrestrial broadcast service. + */ + TV_TERRESTRIAL_DIGITAL(236), + /** + * Key code constant: Satellite key. + * Switches to digital satellite broadcast service. + */ + TV_SATELLITE(237), + /** + * Key code constant: BS key. + * Switches to BS digital satellite broadcasting service available in Japan. + */ + TV_SATELLITE_BS(238), + /** + * Key code constant: CS key. + * Switches to CS digital satellite broadcasting service available in Japan. + */ + TV_SATELLITE_CS(239), + /** + * Key code constant: BS/CS key. + * Toggles between BS and CS digital satellite services. + */ + TV_SATELLITE_SERVICE(240), + /** + * Key code constant: Toggle Network key. + * Toggles selecting broadcast services. + */ + TV_NETWORK(241), + /** + * Key code constant: Antenna/Cable key. + * Toggles broadcast input source between antenna and cable. + */ + TV_ANTENNA_CABLE(242), + /** + * Key code constant: HDMI #1 key. + * Switches to HDMI input #1. + */ + TV_INPUT_HDMI_1(243), + /** + * Key code constant: HDMI #2 key. + * Switches to HDMI input #2. + */ + TV_INPUT_HDMI_2(244), + /** + * Key code constant: HDMI #3 key. + * Switches to HDMI input #3. + */ + TV_INPUT_HDMI_3(245), + /** + * Key code constant: HDMI #4 key. + * Switches to HDMI input #4. + */ + TV_INPUT_HDMI_4(246), + /** + * Key code constant: Composite #1 key. + * Switches to composite video input #1. + */ + TV_INPUT_COMPOSITE_1(247), + /** + * Key code constant: Composite #2 key. + * Switches to composite video input #2. + */ + TV_INPUT_COMPOSITE_2(248), + /** + * Key code constant: Component #1 key. + * Switches to component video input #1. + */ + TV_INPUT_COMPONENT_1(249), + /** + * Key code constant: Component #2 key. + * Switches to component video input #2. + */ + TV_INPUT_COMPONENT_2(250), + /** + * Key code constant: VGA #1 key. + * Switches to VGA (analog RGB) input #1. + */ + TV_INPUT_VGA_1(251), + /** + * Key code constant: Audio description key. + * Toggles audio description off / on. + */ + TV_AUDIO_DESCRIPTION(252), + /** + * Key code constant: Audio description mixing volume up key. + * Louden audio description volume as compared with normal audio volume. + */ + TV_AUDIO_DESCRIPTION_MIX_UP(253), + /** + * Key code constant: Audio description mixing volume down key. + * Lessen audio description volume as compared with normal audio volume. + */ + TV_AUDIO_DESCRIPTION_MIX_DOWN(254), + /** + * Key code constant: Zoom mode key. + * Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) + */ + TV_ZOOM_MODE(255), + /** + * Key code constant: Contents menu key. + * Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control + * Code + */ + TV_CONTENTS_MENU(256), + /** + * Key code constant: Media context menu key. + * Goes to the context menu of media contents. Corresponds to Media Context-sensitive + * Menu (0x11) of CEC User Control Code. + */ + TV_MEDIA_CONTEXT_MENU(257), + /** + * Key code constant: Timer programming key. + * Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of + * CEC User Control Code. + */ + TV_TIMER_PROGRAMMING(258), + /** + * Key code constant: Help key. + */ + HELP(259), + /** + * Key code constant: Navigate to previous key. + * Goes backward by one item in an ordered collection of items. + */ + NAVIGATE_PREVIOUS(260), + /** + * Key code constant: Navigate to next key. + * Advances to the next item in an ordered collection of items. + */ + NAVIGATE_NEXT(261), + /** + * Key code constant: Navigate in key. + * Activates the item that currently has focus or expands to the next level of a navigation + * hierarchy. + */ + NAVIGATE_IN(262), + /** + * Key code constant: Navigate out key. + * Backs out one level of a navigation hierarchy or collapses the item that currently has + * focus. + */ + NAVIGATE_OUT(263), + /** + * Key code constant: Primary stem key for Wear. + * Main power/reset button on watch. + */ + STEM_PRIMARY(264), + /** + * Key code constant: Generic stem key 1 for Wear. + */ + STEM_1(265), + /** + * Key code constant: Generic stem key 2 for Wear. + */ + STEM_2(266), + /** + * Key code constant: Generic stem key 3 for Wear. + */ + STEM_3(267), + /** + * Key code constant: Directional Pad Up-Left. + */ + DPAD_UP_LEFT(268), + /** + * Key code constant: Directional Pad Down-Left. + */ + DPAD_DOWN_LEFT(269), + /** + * Key code constant: Directional Pad Up-Right. + */ + DPAD_UP_RIGHT(270), + /** + * Key code constant: Directional Pad Down-Right. + */ + DPAD_DOWN_RIGHT(271), + /** + * Key code constant: Skip forward media key. + */ + MEDIA_SKIP_FORWARD(272), + /** + * Key code constant: Skip backward media key. + */ + MEDIA_SKIP_BACKWARD(273), + /** + * Key code constant: Step forward media key. + * Steps media forward, one frame at a time. + */ + MEDIA_STEP_FORWARD(274), + /** + * Key code constant: Step backward media key. + * Steps media backward, one frame at a time. + */ + MEDIA_STEP_BACKWARD(275), + /** + * Key code constant: put device to sleep unless a wakelock is held. + */ + SOFT_SLEEP(276), + /** + * Key code constant: Cut key. + */ + CUT(277), + /** + * Key code constant: Copy key. + */ + COPY(278), + /** + * Key code constant: Paste key. + */ + PASTE(279); + + private final int code; + + AndroidKey(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + /** + * Returns true if the specified nativekey is a gamepad button. + * + * @return True if the nativekey is a gamepad button, such as {@link #BUTTON_A}. + */ + public boolean isGamepadButton() { + switch (this) { + case BUTTON_A: + case BUTTON_B: + case BUTTON_C: + case BUTTON_X: + case BUTTON_Y: + case BUTTON_Z: + case BUTTON_L1: + case BUTTON_R1: + case BUTTON_L2: + case BUTTON_R2: + case BUTTON_THUMBL: + case BUTTON_THUMBR: + case BUTTON_START: + case BUTTON_SELECT: + case BUTTON_MODE: + case BUTTON_1: + case BUTTON_2: + case BUTTON_3: + case BUTTON_4: + case BUTTON_5: + case BUTTON_6: + case BUTTON_7: + case BUTTON_8: + case BUTTON_9: + case BUTTON_10: + case BUTTON_11: + case BUTTON_12: + case BUTTON_13: + case BUTTON_14: + case BUTTON_15: + case BUTTON_16: + return true; + default: + return false; + } + } + + /** + * Whether key will, by default, trigger a click on the focused view. + * + * @return true if this is a confirm key. + */ + public boolean isConfirmKey() { + switch (this) { + case DPAD_CENTER: + case ENTER: + case SPACE: + case NUMPAD_ENTER: + return true; + default: + return false; + } + } + + /** + * Whether this key is a media key, which can be send to apps that are + * interested in media key events. + * + * @return true if this is a media key. + */ + public boolean isMediaKey() { + switch (this) { + case MEDIA_PLAY: + case MEDIA_PAUSE: + case MEDIA_PLAY_PAUSE: + case MUTE: + case HEADSETHOOK: + case MEDIA_STOP: + case MEDIA_NEXT: + case MEDIA_PREVIOUS: + case MEDIA_REWIND: + case MEDIA_RECORD: + case MEDIA_FAST_FORWARD: + return true; + default: + return false; + } + } + + + /** + * Is this a system key? System keys can not be used for menu shortcuts. + * + * @return true if this is a system key. + */ + public boolean isSystemKey() { + switch (this) { + case MENU: + case SOFT_RIGHT: + case HOME: + case BACK: + case CALL: + case ENDCALL: + case VOLUME_UP: + case VOLUME_DOWN: + case VOLUME_MUTE: + case MUTE: + case POWER: + case HEADSETHOOK: + case MEDIA_PLAY: + case MEDIA_PAUSE: + case MEDIA_PLAY_PAUSE: + case MEDIA_STOP: + case MEDIA_NEXT: + case MEDIA_PREVIOUS: + case MEDIA_REWIND: + case MEDIA_RECORD: + case MEDIA_FAST_FORWARD: + case CAMERA: + case FOCUS: + case SEARCH: + case BRIGHTNESS_DOWN: + case BRIGHTNESS_UP: + case MEDIA_AUDIO_TRACK: + return true; + default: + return false; + } + } + + /** + * Is it wake key or not. + * + * @return true if this is a wakeup key. + */ + public boolean isWakeKey() { + switch (this) { + case BACK: + case MENU: + case WAKEUP: + case PAIRING: + case STEM_1: + case STEM_2: + case STEM_3: + return true; + default: + return false; + } + } +} 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 new file mode 100644 index 000000000..984c34cbf --- /dev/null +++ b/src/main/java/io/appium/java_client/android/nativekey/KeyEvent.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.nativekey; + +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; + private Integer flags; + + public KeyEvent() { + } + + public KeyEvent(AndroidKey key) { + this.keyCode = key.getCode(); + } + + /** + * Sets the key code. This code is mandatory. + * + * @param key Native Android key. + * @return self instance for chaining + */ + public KeyEvent withKey(AndroidKey key) { + this.keyCode = key.getCode(); + return this; + } + + /** + * Adds the meta modifier. + * + * @param keyEventMetaModifier Native Android modifier value. Multiple modifiers can + * be combined into a single key event. + * @return self instance for chaining + */ + public KeyEvent withMetaModifier(KeyEventMetaModifier keyEventMetaModifier) { + if (this.metaState == null) { + this.metaState = 0; + } + this.metaState |= keyEventMetaModifier.getValue(); + return this; + } + + /** + * Adds the flag. + * + * @param keyEventFlag Native Android flag value. Several flags can + * be combined into a single key event. + * @return self instance for chaining + */ + public KeyEvent withFlag(KeyEventFlag keyEventFlag) { + if (this.flags == null) { + this.flags = 0; + } + this.flags |= keyEventFlag.getValue(); + return this; + } + + /** + * Builds a map, which is ready to be used by the downstream API. + * + * @return API parameters mapping + * @throws IllegalStateException if key code is not set + */ + public Map 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/KeyEventFlag.java b/src/main/java/io/appium/java_client/android/nativekey/KeyEventFlag.java new file mode 100644 index 000000000..787264916 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/nativekey/KeyEventFlag.java @@ -0,0 +1,94 @@ +package io.appium.java_client.android.nativekey; + +public enum KeyEventFlag { + /** + * This mask is set if the key event was generated by a software keyboard. + */ + SOFT_KEYBOARD(0x2), + /** + * This mask is set if we don't want the key event to cause us to leave + * touch mode. + */ + KEEP_TOUCH_MODE(0x4), + /** + * This mask is set if an event was known to come from a trusted part + * of the system. That is, the event is known to come from the user, + * and could not have been spoofed by a third party component. + */ + FROM_SYSTEM(0x8), + /** + * This mask is used for compatibility, to identify enter keys that are + * coming from an IME whose enter key has been auto-labelled "next" or + * "done". This allows TextView to dispatch these as normal enter keys + * for old applications, but still do the appropriate action when + * receiving them. + */ + EDITOR_ACTION(0x10), + /** + * When associated with up key events, this indicates that the key press + * has been canceled. Typically this is used with virtual touch screen + * keys, where the user can slide from the virtual key area on to the + * display: in that case, the application will receive a canceled up + * event and should not perform the action normally associated with the + * key. Note that for this to work, the application can not perform an + * action for a key until it receives an up or the long press timeout has + * expired. + */ + CANCELED(0x20), + /** + * This key event was generated by a virtual (on-screen) hard key area. + * Typically this is an area of the touchscreen, outside of the regular + * display, dedicated to "hardware" buttons. + */ + VIRTUAL_HARD_KEY(0x40), + /** + * This flag is set for the first key repeat that occurs after the + * long press timeout. + */ + LONG_PRESS(0x80), + /** + * Set when a key event has {@link #CANCELED} set because a long + * press action was executed while it was down. + */ + CANCELED_LONG_PRESS(0x100), + /** + * Set for ACTION_UP when this event's key value is still being + * tracked from its initial down. That is, somebody requested that tracking + * started on the key down and a long press has not caused + * the tracking to be canceled. + */ + TRACKING(0x200), + /** + * Set when a key event has been synthesized to implement default behavior + * for an event that the application did not handle. + * Fallback key events are generated by unhandled trackball motions + * (to emulate a directional keypad) and by certain unhandled key presses + * that are declared in the key map (such as special function numeric keypad + * keys when numlock is off). + */ + FALLBACK(0x400), + /** + * Signifies that the key is being predispatched. + */ + PREDISPATCH(0x20000000), + /** + * Private control to determine when an app is tracking a key sequence. + */ + START_TRACKING(0x40000000), + /** + * Private flag that indicates when the system has detected that this key event + * may be inconsistent with respect to the sequence of previously delivered key events, + * such as when a key up event is sent but the key was not down. + */ + TAINTED(0x80000000); + + private final int value; + + KeyEventFlag(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..b32de52bc --- /dev/null +++ b/src/main/java/io/appium/java_client/android/nativekey/KeyEventMetaModifier.java @@ -0,0 +1,157 @@ +package io.appium.java_client.android.nativekey; + +public enum KeyEventMetaModifier { + /** + * SHIFT key locked in CAPS mode. + * Reserved for use by MetaKeyKeyListener for a published constant in its API. + */ + CAP_LOCKED(0x100), + /** + * ALT key locked. + * Reserved for use by MetaKeyKeyListener for a published constant in its API. + */ + ALT_LOCKED(0x200), + /** + * SYM key locked. + * Reserved for use by MetaKeyKeyListener for a published constant in its API. + */ + SYM_LOCKED(0x400), + /** + * Text is in selection mode. + * Reserved for use by MetaKeyKeyListener for a private unpublished constant + * in its API that is currently being retained for legacy reasons. + */ + SELECTING(0x800), + /** + * 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. + * + * @see AndroidKey#ALT_LEFT + */ + ALT_LEFT_ON(0x10), + /** + * 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. + * + * @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. + * + * @see AndroidKey#SHIFT_LEFT + */ + SHIFT_LEFT_ON(0x40), + /** + * 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. + */ + SYM_ON(0x4), + /** + * 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. + * + * @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. + * + * @see AndroidKey#CTRL_LEFT + */ + CTRL_LEFT_ON(0x2000), + /** + * 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. + * + * @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. + * + * @see AndroidKey#META_LEFT + */ + META_LEFT_ON(0x20000), + /** + * 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. + * + * @see AndroidKey#CAPS_LOCK + */ + CAPS_LOCK_ON(0x100000), + /** + * 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. + * + * @see AndroidKey#SCROLL_LOCK + */ + SCROLL_LOCK_ON(0x400000), + /** + * This mask is a combination of {@link #SHIFT_ON}, {@link #SHIFT_LEFT_ON} + * and {@link #SHIFT_RIGHT_ON}. + */ + SHIFT_MASK(SHIFT_ON.getValue() | SHIFT_LEFT_ON.getValue() | SHIFT_RIGHT_ON.getValue()), + /** + * This mask is a combination of {@link #ALT_ON}, {@link #ALT_LEFT_ON} + * and {@link #ALT_RIGHT_ON}. + */ + ALT_MASK(ALT_ON.getValue() | ALT_LEFT_ON.getValue() | ALT_RIGHT_ON.getValue()), + /** + * This mask is a combination of {@link #CTRL_ON}, {@link #CTRL_LEFT_ON} + * and {@link #CTRL_RIGHT_ON}. + */ + CTRL_MASK(CTRL_ON.getValue() | CTRL_LEFT_ON.getValue() | CTRL_RIGHT_ON.getValue()), + /** + * This mask is a combination of {@link #META_ON}, {@link #META_LEFT_ON} + * and {@link #META_RIGHT_ON}. + */ + META_MASK(META_ON.getValue() | META_LEFT_ON .getValue() | META_RIGHT_ON.getValue()); + + private final int value; + + KeyEventMetaModifier(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} 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 new file mode 100644 index 000000000..af633a1d9 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/nativekey/PressesKey.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.nativekey; + +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.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, CanRememberExtensionPresence { + + /** + * Send a key event to the device under test. + * + * @param keyEvent The generated native key event + */ + default void pressKey(KeyEvent keyEvent) { + 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()) + ); + } + } + + /** + * Send a long press key event to the device under test. + * + * @param keyEvent The generated native key event + */ + default void longPressKey(KeyEvent keyEvent) { + 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/android/options/signing/KeystoreConfig.java b/src/main/java/io/appium/java_client/android/options/signing/KeystoreConfig.java new file mode 100644 index 000000000..5ab0d78b6 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/options/signing/KeystoreConfig.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 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/appmanagement/ApplicationState.java b/src/main/java/io/appium/java_client/appmanagement/ApplicationState.java new file mode 100644 index 000000000..8cc9005a3 --- /dev/null +++ b/src/main/java/io/appium/java_client/appmanagement/ApplicationState.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.appmanagement; + +import java.util.Arrays; + +public enum ApplicationState { + NOT_INSTALLED, NOT_RUNNING, RUNNING_IN_BACKGROUND_SUSPENDED, + RUNNING_IN_BACKGROUND, RUNNING_IN_FOREGROUND; + + /** + * Creates {@link ApplicationState} instance based on the code. + * + * @param code the code received from state querying endpoint. + * @return {@link ApplicationState} instance. + */ + public static ApplicationState ofCode(long code) { + return Arrays.stream(ApplicationState.values()) + .filter(x -> code == x.ordinal()) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + String.format("Application state %s is unknown", code)) + ); + } +} diff --git a/src/main/java/io/appium/java_client/windows/WindowsElement.java b/src/main/java/io/appium/java_client/appmanagement/BaseActivateApplicationOptions.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/appmanagement/BaseActivateApplicationOptions.java index 4f7ec7ba2..08158d32b 100644 --- a/src/main/java/io/appium/java_client/windows/WindowsElement.java +++ b/src/main/java/io/appium/java_client/appmanagement/BaseActivateApplicationOptions.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package io.appium.java_client.windows; +package io.appium.java_client.appmanagement; -import io.appium.java_client.FindsByWindowsAutomation; -import io.appium.java_client.MobileElement; +public abstract class BaseActivateApplicationOptions> + extends BaseOptions { -public class WindowsElement extends MobileElement implements FindsByWindowsAutomation { } diff --git a/src/main/java/io/appium/java_client/appmanagement/BaseInstallApplicationOptions.java b/src/main/java/io/appium/java_client/appmanagement/BaseInstallApplicationOptions.java new file mode 100644 index 000000000..ac58ab7dc --- /dev/null +++ b/src/main/java/io/appium/java_client/appmanagement/BaseInstallApplicationOptions.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.appmanagement; + +public abstract class BaseInstallApplicationOptions> + extends BaseOptions { + +} diff --git a/src/main/java/io/appium/java_client/IllegalCoordinatesException.java b/src/main/java/io/appium/java_client/appmanagement/BaseOptions.java similarity index 70% rename from src/main/java/io/appium/java_client/IllegalCoordinatesException.java rename to src/main/java/io/appium/java_client/appmanagement/BaseOptions.java index 8167ad65d..1c1327a86 100644 --- a/src/main/java/io/appium/java_client/IllegalCoordinatesException.java +++ b/src/main/java/io/appium/java_client/appmanagement/BaseOptions.java @@ -14,15 +14,16 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.appmanagement; -import org.openqa.selenium.WebDriverException; +import java.util.Map; -public class IllegalCoordinatesException extends WebDriverException { - private static final long serialVersionUID = 1L; - - public IllegalCoordinatesException(String message) { - super(message); - } +public abstract class BaseOptions> { + /** + * Creates a map based on the provided options. + * + * @return options mapping. + */ + public abstract Map build(); } diff --git a/src/main/java/io/appium/java_client/appmanagement/BaseRemoveApplicationOptions.java b/src/main/java/io/appium/java_client/appmanagement/BaseRemoveApplicationOptions.java new file mode 100644 index 000000000..e43ce5631 --- /dev/null +++ b/src/main/java/io/appium/java_client/appmanagement/BaseRemoveApplicationOptions.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.appmanagement; + +public abstract class BaseRemoveApplicationOptions> + extends BaseOptions { + +} diff --git a/src/main/java/io/appium/java_client/appmanagement/BaseTerminateApplicationOptions.java b/src/main/java/io/appium/java_client/appmanagement/BaseTerminateApplicationOptions.java new file mode 100644 index 000000000..204f87a66 --- /dev/null +++ b/src/main/java/io/appium/java_client/appmanagement/BaseTerminateApplicationOptions.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.appmanagement; + +public abstract class BaseTerminateApplicationOptions> + extends BaseOptions { + +} diff --git a/src/main/java/io/appium/java_client/battery/BatteryInfo.java b/src/main/java/io/appium/java_client/battery/BatteryInfo.java new file mode 100644 index 000000000..1781db8a5 --- /dev/null +++ b/src/main/java/io/appium/java_client/battery/BatteryInfo.java @@ -0,0 +1,36 @@ +package io.appium.java_client.battery; + +import java.util.Map; + +public abstract class BatteryInfo { + private final Map input; + + public BatteryInfo(Map input) { + this.input = input; + } + + /** + * Returns battery level. + * + * @return Battery level in range [0.0, 1.0], where 1.0 means 100% charge. + */ + public double getLevel() { + final Object value = getInput().get("level"); + if (value instanceof Long) { + return ((Long) value).doubleValue(); + } + return (double) value; + } + + /** + * Returns battery state. + * + * @param The type of state data object for the corresponding platform. + * @return Battery state value. + */ + public abstract T getState(); + + protected Map getInput() { + return this.input; + } +} diff --git a/src/main/java/io/appium/java_client/battery/HasBattery.java b/src/main/java/io/appium/java_client/battery/HasBattery.java new file mode 100644 index 000000000..4b25cc7e4 --- /dev/null +++ b/src/main/java/io/appium/java_client/battery/HasBattery.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.battery; + +import io.appium.java_client.ExecutesMethod; + +public interface HasBattery extends ExecutesMethod { + + /** + * Retrieves battery info from the device under test. + * + * @return BatteryInfo instance, containing the battery information + */ + T getBatteryInfo(); +} 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/events/api/Listener.java b/src/main/java/io/appium/java_client/clipboard/ClipboardContentType.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/clipboard/ClipboardContentType.java index e1ea35c1d..006d88e43 100644 --- a/src/main/java/io/appium/java_client/events/api/Listener.java +++ b/src/main/java/io/appium/java_client/clipboard/ClipboardContentType.java @@ -14,10 +14,8 @@ * limitations under the License. */ -package io.appium.java_client.events.api; +package io.appium.java_client.clipboard; -/** - * This interface just marks event listeners. - */ -public interface Listener { +public enum ClipboardContentType { + PLAINTEXT, IMAGE, URL } diff --git a/src/main/java/io/appium/java_client/clipboard/HasClipboard.java b/src/main/java/io/appium/java_client/clipboard/HasClipboard.java new file mode 100644 index 000000000..ae507f940 --- /dev/null +++ b/src/main/java/io/appium/java_client/clipboard/HasClipboard.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.clipboard; + +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.Base64; +import java.util.Map; + +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 base64Content base64-encoded content to be set. + */ + default void setClipboard(ClipboardContentType contentType, byte[] base64Content) { + final String extName = "mobile: setClipboard"; + var args = Map.of( + "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), + "contentType", contentType.name().toLowerCase(ROOT) + ); + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(this, Map.entry(SET_CLIPBOARD, args)); + } + } + + /** + * Get the content of the clipboard. + * + * @param contentType one of supported content types. + * @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) { + 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)); + } + } + + /** + * Set the clipboard text. + * + * @param text The actual text to be set. + */ + default void setClipboardText(String text) { + setClipboard(ClipboardContentType.PLAINTEXT, Base64 + .getMimeEncoder() + .encode(text.getBytes(StandardCharsets.UTF_8))); + } + + /** + * Get the clipboard text. + * + * @return Either the text, which is stored in the clipboard or an empty string if the clipboard is empty + */ + default String getClipboardText() { + byte[] base64decodedBytes = Base64 + .getMimeDecoder() + .decode(getClipboard(ClipboardContentType.PLAINTEXT)); + return new String(base64decodedBytes, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java b/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.java new file mode 100644 index 000000000..7eac89083 --- /dev/null +++ b/src/main/java/io/appium/java_client/driverscripts/ScriptOptions.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.driverscripts; + +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 ScriptOptions { + private ScriptType scriptType; + private Long timeoutMs; + + /** + * Sets the script type. + * + * @param type the actual script type + * @return self instance for chaining + */ + public ScriptOptions withScriptType(ScriptType type) { + this.scriptType = requireNonNull(type); + return this; + } + + /** + * Sets the script execution timeout. + * If this is not set then the maximum duration of the script + * is not limited (e. g. may block forever). + * + * @param timeoutMs the timeout in milliseconds + * @return self instance for chaining + */ + public ScriptOptions withTimeout(long timeoutMs) { + this.timeoutMs = timeoutMs; + return this; + } + + /** + * Builds a values map for further usage in HTTP requests to Appium. + * + * @return The map containing the provided options + */ + public Map 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/driverscripts/ScriptType.java b/src/main/java/io/appium/java_client/driverscripts/ScriptType.java new file mode 100644 index 000000000..42aa78833 --- /dev/null +++ b/src/main/java/io/appium/java_client/driverscripts/ScriptType.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.driverscripts; + +public enum ScriptType { + WEBDRIVERIO +} diff --git a/src/main/java/io/appium/java_client/driverscripts/ScriptValue.java b/src/main/java/io/appium/java_client/driverscripts/ScriptValue.java new file mode 100644 index 000000000..934cd670d --- /dev/null +++ b/src/main/java/io/appium/java_client/driverscripts/ScriptValue.java @@ -0,0 +1,44 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.driverscripts; + +import lombok.AccessLevel; +import lombok.Getter; + +import java.util.Map; + +public class ScriptValue { + /** + * The result of ExecuteDriverScript call. + * + * @return The actual returned value depends on the script content + */ + @Getter(AccessLevel.PUBLIC) private final Object result; + /** + * Retrieves logs mapping from ExecuteDriverScript call. + * + * @return Mapping keys are log levels, for example `warn` or + * `error` and the values are lists of strings that were printed + * by the script into the corresponding logging level + */ + @Getter(AccessLevel.PUBLIC) private final Map logs; + + public ScriptValue(Object result, Map logs) { + this.result = result; + this.logs = logs; + } +} 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 b7184b841..000000000 --- a/src/main/java/io/appium/java_client/events/DefaultAspect.java +++ /dev/null @@ -1,551 +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.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; - -@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_ALERT_AUTHENTICATION = "execution(* org.openqa.selenium." - + "Alert.setCredentials(..)) || " - + "execution(* org.openqa.selenium.Alert.authenticateUsing(..))"; - 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 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.FindsByIosUIAutomation.*(..)) || " - + "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.*(..))"; - - 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) throws Exception { - 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_ALERT_AUTHENTICATION) - public void beforeAlertAuthentication(JoinPoint joinPoint) throws Throwable { - try { - listener.beforeAuthentication(driver, - castTarget(joinPoint), castArgument(joinPoint, 0)); - } catch (Throwable t) { - throw getRootCause(t); - } - } - - @After(EXECUTION_ALERT_AUTHENTICATION) - public void afterAlertAuthentication(JoinPoint joinPoint) throws Throwable { - try { - listener.afterAuthentication(driver, castTarget(joinPoint), - castArgument(joinPoint, 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_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 8a7893d77..000000000 --- a/src/main/java/io/appium/java_client/events/DefaultListener.java +++ /dev/null @@ -1,238 +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.Point; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.security.Credentials; -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); - } - - 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 beforeAuthentication(WebDriver driver, Alert alert, Credentials credentials) { - ((AlertEventListener) dispatcher).beforeAuthentication(driver, alert, credentials); - } - - @Override - public void afterAuthentication(WebDriver driver, Alert alert, Credentials credentials) { - ((AlertEventListener) dispatcher).afterAuthentication(driver, alert, credentials); - } - - @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 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); - } -} \ No newline at end of file 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 a191680a3..000000000 --- a/src/main/java/io/appium/java_client/events/EventFiringObjectFactory.java +++ /dev/null @@ -1,74 +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 d0fb36cd8..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 eae7ebdf7..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/AlertEventListener.java +++ /dev/null @@ -1,101 +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; -import org.openqa.selenium.security.Credentials; - -public interface AlertEventListener extends Listener { - - /** - * This action will be performed each time before {@link org.openqa.selenium.Alert#accept()} - * - * @param driver WebDriver - * @param alert {@link org.openqa.selenium.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 org.openqa.selenium.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 org.openqa.selenium.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 org.openqa.selenium.Alert} which has been dismissed - */ - void beforeAlertDismiss(WebDriver driver, Alert alert); - - /** - * This action will be performed each time before - * {@link org.openqa.selenium.Alert#sendKeys(String)} - * - * @param driver WebDriver - * @param alert {@link org.openqa.selenium.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 org.openqa.selenium.Alert#sendKeys(String)} - * - * @param driver WebDriver - * @param alert {@link org.openqa.selenium.Alert} which has received keys - * @param keys Keys which have been sent - */ - void afterAlertSendKeys(WebDriver driver, Alert alert, String keys); - - /** - * This action will be performed each time before - * {@link org.openqa.selenium.Alert#setCredentials(Credentials)} and - * {@link org.openqa.selenium.Alert#authenticateUsing(Credentials)} - * - * @param driver WebDriver - * @param alert {@link org.openqa.selenium.Alert} which is receiving user credentials - * @param credentials which are being sent - */ - void beforeAuthentication(WebDriver driver, Alert alert, - Credentials credentials); - - /** - * This action will be performed each time after - * {@link org.openqa.selenium.Alert#setCredentials(Credentials)} and - * {@link org.openqa.selenium.Alert#authenticateUsing(Credentials)} - * - * @param driver WebDriver - * @param alert {@link org.openqa.selenium.Alert} which has received user credentials - * @param credentials which have been sent - */ - void afterAuthentication(WebDriver driver, Alert alert, - Credentials credentials); -} 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 960d7f5b6..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/ElementEventListener.java +++ /dev/null @@ -1,78 +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 org.openqa.selenium.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 org.openqa.selenium.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 org.openqa.selenium.WebElement#clear WebElement.clear()}, - * {@link org.openqa.selenium.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 org.openqa.selenium.WebElement#clear WebElement.clear()}, - * {@link org.openqa.selenium.WebElement#sendKeys WebElement.sendKeys(...)}. - * - * @param driver WebDriver - * @param element the WebElement being used for the action - */ - void beforeChangeValueOf(WebElement element, WebDriver driver, CharSequence[] keysToSend); - - /** - * Called after {@link org.openqa.selenium.WebElement#clear WebElement.clear()}, - * {@link org.openqa.selenium.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 org.openqa.selenium.WebElement#clear WebElement.clear()}, - * {@link org.openqa.selenium.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, CharSequence[] keysToSend); -} \ No newline at end of file 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 4edaaf708..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/JavaScriptEventListener.java +++ /dev/null @@ -1,43 +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(java.lang.String, - * java.lang.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(java.lang.String, - * java.lang.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 b9ab08fbc..000000000 --- a/src/main/java/io/appium/java_client/events/api/general/WindowEventListener.java +++ /dev/null @@ -1,87 +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 org.openqa.selenium.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(org.openqa.selenium.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(org.openqa.selenium.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); -} 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/gecko/options/Verbosity.java b/src/main/java/io/appium/java_client/gecko/options/Verbosity.java new file mode 100644 index 000000000..f9a5c599e --- /dev/null +++ b/src/main/java/io/appium/java_client/gecko/options/Verbosity.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.gecko.options; + +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 new file mode 100644 index 000000000..bd20f884a --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/BaseComparisonOptions.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.imagecomparison; + +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; + + /** + * Makes the endpoint to return an image, + * which contains the visualized result of the corresponding + * picture matching operation. This option is disabled by default. + * + * @return self instance for chaining + */ + public T withEnabledVisualization() { + visualize = true; + //noinspection unchecked + return (T) this; + } + + /** + * Builds a map, which is ready to be passed to the subordinated + * Appium API. + * + * @return comparison options mapping. + */ + public Map 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/MissingParameterException.java b/src/main/java/io/appium/java_client/imagecomparison/ComparisonMode.java similarity index 67% rename from src/main/java/io/appium/java_client/MissingParameterException.java rename to src/main/java/io/appium/java_client/imagecomparison/ComparisonMode.java index 1f9782cf4..c2e20f96f 100644 --- a/src/main/java/io/appium/java_client/MissingParameterException.java +++ b/src/main/java/io/appium/java_client/imagecomparison/ComparisonMode.java @@ -14,18 +14,19 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.imagecomparison; -public class MissingParameterException - extends IllegalArgumentException { +public enum ComparisonMode { + MATCH_FEATURES("matchFeatures"), GET_SIMILARITY("getSimilarity"), MATCH_TEMPLATE("matchTemplate"); - private static final long serialVersionUID = 1L; + private final String name; - public MissingParameterException(String reason) { - super(reason); + ComparisonMode(String name) { + this.name = name; } - public MissingParameterException(String reason, Throwable cause) { - super(reason, cause); + @Override + public String toString() { + return name; } } diff --git a/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java b/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java new file mode 100644 index 000000000..e73be71c8 --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/ComparisonResult.java @@ -0,0 +1,119 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.imagecomparison; + +import org.openqa.selenium.Point; +import org.openqa.selenium.Rectangle; + +import java.io.File; +import java.io.FileOutputStream; +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"; + + protected final Object 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 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 (!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)); + } + } + + /** + * Returns the visualization of the matching result. + * + * @return The visualization of the matching result represented as base64-encoded PNG image. + */ + public byte[] getVisualization() { + verifyPropertyPresence(VISUALIZATION); + return ((String) getResultAsMap().get(VISUALIZATION)).getBytes(StandardCharsets.UTF_8); + } + + /** + * Stores visualization image into the given file. + * + * @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.getDecoder().decode(getVisualization()); + try (OutputStream stream = new FileOutputStream(destination)) { + stream.write(data); + } + } + + /** + * Converts float OpenCV coordinates to Selenium-compatible format. + * + * @param openCVCoordinate the original coordinate value + * @return The converted value + */ + private static int toSeleniumCoordinate(Object openCVCoordinate) { + if (openCVCoordinate instanceof Long) { + return ((Long) openCVCoordinate).intValue(); + } + if (openCVCoordinate instanceof Double) { + return ((Double) openCVCoordinate).intValue(); + } + return (int) openCVCoordinate; + } + + /** + * Transforms a map into {@link Rectangle} object. + * + * @param map the source map. + * @return {@link Rectangle} object + */ + public static Rectangle mapToRect(Map map) { + return new Rectangle(toSeleniumCoordinate(map.get("x")), + toSeleniumCoordinate(map.get("y")), + toSeleniumCoordinate(map.get("height")), + toSeleniumCoordinate(map.get("width"))); + } + + /** + * Transforms a map into {@link Point} object. + * + * @param map the source map. + * @return {@link Point} object + */ + public static Point mapToPoint(Map map) { + return new Point(toSeleniumCoordinate(map.get("x")), toSeleniumCoordinate(map.get("y"))); + } +} diff --git a/src/main/java/io/appium/java_client/imagecomparison/FeatureDetector.java b/src/main/java/io/appium/java_client/imagecomparison/FeatureDetector.java new file mode 100644 index 000000000..2e9f50a5d --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/FeatureDetector.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.imagecomparison; + +public enum FeatureDetector { + AKAZE, AGAST, BRISK, FAST, GFTT, KAZE, MSER, SIFT, ORB +} diff --git a/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingOptions.java b/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingOptions.java new file mode 100644 index 000000000..3fd56517c --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingOptions.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.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; + +public class FeaturesMatchingOptions extends BaseComparisonOptions { + private String detectorName; + private String matchFunc; + private Integer goodMatchesFactor; + + /** + * Sets the detector name for features matching + * algorithm. Some of these detectors (FAST, AGAST, GFTT, FAST, SIFT and MSER) are not available + * in the default OpenCV installation and have to be enabled manually before + * library compilation. The default detector name is 'ORB'. + * + * @param name the detector name for features matching. + * @return self instance for chaining. + */ + public FeaturesMatchingOptions withDetectorName(FeatureDetector name) { + this.detectorName = name.name(); + return this; + } + + /** + * Sets the name of the matching function. + * The default one is 'BruteForce'. + * + * @param name the name of the matching function. + * @return self instance for chaining. + */ + public FeaturesMatchingOptions withMatchFunc(MatchingFunction name) { + this.matchFunc = name.toString(); + return this; + } + + /** + * Sets the maximum count of "good" matches (e. g. with minimal distances). + * + * @param factor the "good" matches factor + * @return self instance for chaining. + */ + public FeaturesMatchingOptions withGoodMatchesFactor(int factor) { + checkArgument(factor > 1); + this.goodMatchesFactor = factor; + return this; + } + + @Override + public Map 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 new file mode 100644 index 000000000..0a983e50a --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/FeaturesMatchingResult.java @@ -0,0 +1,109 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.imagecomparison; + +import org.openqa.selenium.Point; +import org.openqa.selenium.Rectangle; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class FeaturesMatchingResult extends ComparisonResult { + private static final String COUNT = "count"; + private static final String TOTAL_COUNT = "totalCount"; + private static final String POINTS1 = "points1"; + private static final String RECT1 = "rect1"; + private static final String POINTS2 = "points2"; + private static final String RECT2 = "rect2"; + + public FeaturesMatchingResult(Map input) { + super(input); + } + + /** + * Returns a count of matched edges on both images. + * + * @return The count of matched edges on both images. + * The more matching edges there are no both images the more similar they are. + */ + public int getCount() { + verifyPropertyPresence(COUNT); + return ((Long) getResultAsMap().get(COUNT)).intValue(); + } + + /** + * Returns a count of matched edges on both images. + * + * @return The total count of matched edges on both images. + * It is equal to `count` if `goodMatchesFactor` does not limit the matches, + * otherwise it contains the total count of matches before `goodMatchesFactor` is + * applied. + */ + public int getTotalCount() { + verifyPropertyPresence(TOTAL_COUNT); + return ((Long) getResultAsMap().get(TOTAL_COUNT)).intValue(); + } + + /** + * Returns a list of matching points on the first image. + * + * @return The list of matching points on the first image. + */ + public List getPoints1() { + verifyPropertyPresence(POINTS1); + //noinspection unchecked + return ((List>) getResultAsMap().get(POINTS1)).stream() + .map(ComparisonResult::mapToPoint) + .collect(Collectors.toList()); + } + + /** + * Returns a rect for the `points1` list. + * + * @return The bounding rect for the `points1` list or a zero rect if not enough matching points were found. + */ + public Rectangle getRect1() { + verifyPropertyPresence(RECT1); + //noinspection unchecked + return mapToRect((Map) getResultAsMap().get(RECT1)); + } + + /** + * Returns a list of matching points on the second image. + * + * @return The list of matching points on the second image. + */ + public List getPoints2() { + verifyPropertyPresence(POINTS2); + //noinspection unchecked + return ((List>) getResultAsMap().get(POINTS2)).stream() + .map(ComparisonResult::mapToPoint) + .collect(Collectors.toList()); + } + + /** + * Returns a rect for the `points2` list. + * + * @return The bounding rect for the `points2` list or a zero rect if not enough matching points were found. + */ + public Rectangle getRect2() { + verifyPropertyPresence(RECT2); + //noinspection unchecked + return mapToRect((Map) getResultAsMap().get(RECT2)); + } +} diff --git a/src/main/java/io/appium/java_client/FindsByIosClassChain.java b/src/main/java/io/appium/java_client/imagecomparison/MatchingFunction.java similarity index 59% rename from src/main/java/io/appium/java_client/FindsByIosClassChain.java rename to src/main/java/io/appium/java_client/imagecomparison/MatchingFunction.java index 92482663a..1fe71738e 100644 --- a/src/main/java/io/appium/java_client/FindsByIosClassChain.java +++ b/src/main/java/io/appium/java_client/imagecomparison/MatchingFunction.java @@ -14,19 +14,21 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.imagecomparison; -import org.openqa.selenium.WebElement; +public enum MatchingFunction { + FLANN_BASED("FlannBased"), BRUTE_FORCE("BruteForce"), BRUTE_FORCE1("BruteForceL1"), + BRUTE_FORCE_HAMMING("BruteForceHamming"), BRUTE_FORCE_HAMMING_LUT("BruteForceHammingLut"), + BRUTE_FORCE_SL2("BruteForceSL2"); -import java.util.List; + private final String name; -public interface FindsByIosClassChain extends FindsByFluentSelector { - - default T findElementByIosClassChain(String using) { - return findElement(MobileSelector.IOS_CLASS_CHAIN.toString(), using); + MatchingFunction(String name) { + this.name = name; } - default List findElementsByIosClassChain(String using) { - return findElements(MobileSelector.IOS_CLASS_CHAIN.toString(), using); + @Override + public String toString() { + return name; } } diff --git a/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingOptions.java b/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingOptions.java new file mode 100644 index 000000000..314a237dc --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingOptions.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.imagecomparison; + +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. + * + * @param threshold value in range 0..1. 0.5 is the default value. + * @return self instance for chaining. + */ + public OccurrenceMatchingOptions withThreshold(double threshold) { + this.threshold = 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() { + 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 new file mode 100644 index 000000000..7b0266f23 --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/OccurrenceMatchingResult.java @@ -0,0 +1,187 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.imagecomparison; + +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"; + + private final boolean hasMultiple; + + public OccurrenceMatchingResult(Object input) { + super(input); + hasMultiple = input instanceof List; + } + + /** + * 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) 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/SimilarityMatchingOptions.java b/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingOptions.java new file mode 100644 index 000000000..69a7a5f7e --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingOptions.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.imagecomparison; + +public class SimilarityMatchingOptions extends BaseComparisonOptions { +} diff --git a/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingResult.java b/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingResult.java new file mode 100644 index 000000000..0806e7b53 --- /dev/null +++ b/src/main/java/io/appium/java_client/imagecomparison/SimilarityMatchingResult.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.imagecomparison; + +import java.util.Map; + +public class SimilarityMatchingResult extends ComparisonResult { + private static final String SCORE = "score"; + + public SimilarityMatchingResult(Map input) { + super(input); + } + + /** + * Returns the similarity score as a float number in range [0.0, 1.0]. + * + * @return The similarity score as a float number in range [0.0, 1.0]. + * 1.0 is the highest score (means both images are totally equal). + */ + public double getScore() { + verifyPropertyPresence(SCORE); + if (getResultAsMap().get(SCORE) instanceof Long) { + return ((Long) getResultAsMap().get(SCORE)).doubleValue(); + } + 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 new file mode 100644 index 000000000..345e60a9c --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/CapabilityHelpers.java @@ -0,0 +1,181 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.jspecify.annotations.Nullable; +import org.openqa.selenium.Capabilities; + +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. + * + * @param The corresponding capability type. + * @param caps driver caps object + * @param name capability name + * @param expectedType the expected capability type + * @return The retrieved capability value or null if the cap either not present has an unexpected type + */ + @Nullable + public static T getCapability(Capabilities caps, String name, Class expectedType) { + List possibleNames = new ArrayList<>(); + possibleNames.add(name); + if (!name.startsWith(APPIUM_PREFIX)) { + possibleNames.add(APPIUM_PREFIX + name); + } + for (String capName : possibleNames) { + if (caps.getCapability(capName) == null) { + continue; + } + + if (expectedType == String.class) { + return expectedType.cast(String.valueOf(caps.getCapability(capName))); + } + if (expectedType.isAssignableFrom(caps.getCapability(capName).getClass())) { + return expectedType.cast(caps.getCapability(capName)); + } + } + 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 new file mode 100644 index 000000000..4413f28ab --- /dev/null +++ b/src/main/java/io/appium/java_client/internal/Config.java @@ -0,0 +1,73 @@ +package io.appium.java_client.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +public class Config { + private static Config mainInstance = null; + private static final String MAIN_CONFIG = "main.properties"; + private static final Map CACHE = new ConcurrentHashMap<>(); + private final String configName; + + /** + * Retrieve a facade for the main config. + * + * @return interaction helper for 'main.properties' config + */ + public static synchronized Config main() { + if (mainInstance == null) { + mainInstance = new Config(MAIN_CONFIG); + } + return mainInstance; + } + + private Config(String configName) { + this.configName = configName; + } + + /** + * Retrieve a value from properties file. + * + * @param the value type. + * @param key the name of the corresponding key which value to retrieve + * @param valueType the expected type of the value to be retrieved + * @return the actual value + * @throws IllegalArgumentException if the given key does not exist + * @throws ClassCastException if the retrieved value cannot be cast to `valueType` type + */ + public T getValue(String key, Class valueType) { + return getOptionalValue(key, valueType) + .orElseThrow(() -> new IllegalArgumentException( + String.format("There is no '%s' key in '%s' config", key, configName) + )); + } + + /** + * Retrieve a value from properties file. + * + * @param the type of the resulting value. + * @param key the name of the corresponding key which value to retrieve + * @param valueType the expected type of the value to be retrieved + * @return the actual value or {@link Optional#empty()} if the key is not present + * @throws UncheckedIOException if the given properties file does not exist/not accessible + * @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 -> { + try (InputStream configFileStream = getClass().getClassLoader().getResourceAsStream(configName)) { + final Properties p = new Properties(); + p.load(configFileStream); + return p; + } catch (IOException e) { + throw new UncheckedIOException(String.format("Configuration file '%s' cannot be loaded", + configName), e); + } + }); + return cachedProps.containsKey(key) ? Optional.of(valueType.cast(cachedProps.get(key))) : Optional.empty(); + } +} 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 0dec31b3e..000000000 --- a/src/main/java/io/appium/java_client/internal/ElementMap.java +++ /dev/null @@ -1,87 +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 com.google.common.collect.ImmutableMap; - -import io.appium.java_client.HasSessionDetails; -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; - - private ElementMap(String platformOrAutomation, Class elementClass) { - this.platformOrAutomation = platformOrAutomation; - this.elementClass = elementClass; - } - - public String getPlatformOrAutomation() { - return platformOrAutomation; - } - - public Class getElementClass() { - return elementClass; - } - - /** - * @param hasSessionDetails something that implements {@link io.appium.java_client.HasSessionDetails}. - * @return subclass of {@link io.appium.java_client.MobileElement} that convenient to current session details. - */ - public static Class getElementClass(HasSessionDetails hasSessionDetails) { - if (hasSessionDetails == null) { - return RemoteWebElement.class; - } - ElementMap element = Optional.ofNullable(mobileElementMap.get(String - .valueOf(hasSessionDetails.getAutomationName()).toLowerCase().trim())) - .orElse(mobileElementMap - .get(String.valueOf(hasSessionDetails.getPlatformName()).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 103963d14..000000000 --- a/src/main/java/io/appium/java_client/internal/JsonToMobileElementConverter.java +++ /dev/null @@ -1,101 +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 com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; - -import io.appium.java_client.HasSessionDetails; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.remote.internal.JsonToWebElementConverter; - -import java.lang.reflect.Constructor; -import java.util.Collection; -import java.util.Map; - -/** - * 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 HasSessionDetails hasSessionDetails; - - /** - * @param driver an instance of {@link org.openqa.selenium.remote.RemoteWebDriver} subclass - * @param hasSessionDetails object that has session details - */ - public JsonToMobileElementConverter(RemoteWebDriver driver, HasSessionDetails hasSessionDetails) { - super(driver); - this.driver = driver; - this.hasSessionDetails = hasSessionDetails; - } - - /** - * This method converts a command result. - * - * @param result is the result of a command execution. - * @return the result - */ - public Object apply(Object result) { - if (result instanceof Collection) { - Collection results = (Collection) result; - return Lists.newArrayList(Iterables.transform(results, this)); - } - - if (result instanceof Map) { - Map resultAsMap = (Map) result; - if (resultAsMap.containsKey("ELEMENT")) { - RemoteWebElement element = newMobileElement(); - element.setId(String.valueOf(resultAsMap.get("ELEMENT"))); - element.setFileDetector(driver.getFileDetector()); - return element; - } else { - return Maps.transformValues(resultAsMap, this); - } - } - - if (result instanceof Number) { - if (result instanceof Float || result instanceof Double) { - return ((Number) result).doubleValue(); - } - return ((Number) result).longValue(); - } - - return result; - } - - protected RemoteWebElement newMobileElement() { - Class target; - target = getElementClass(hasSessionDetails); - try { - Constructor constructor = target.getDeclaredConstructor(); - constructor.setAccessible(true); - RemoteWebElement result = constructor.newInstance(); - result.setParent(driver); - 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 new file mode 100644 index 000000000..32f4c9df4 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/HasIOSClipboard.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static java.util.Objects.requireNonNull; + +public interface HasIOSClipboard extends HasClipboard { + /** + * Set an image to the clipboard. + * + * @param img the actual image to be set. + * @throws IOException if the image cannot be decoded in PNG representation + */ + default void setClipboardImage(BufferedImage img) throws IOException { + try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) { + ImageIO.write(requireNonNull(img), "png", os); + setClipboard(ClipboardContentType.IMAGE, Base64 + .getMimeEncoder() + .encode(os.toByteArray())); + } + } + + /** + * Get an image from the clipboard. + * + * @return the actual image instance. + * @throws IOException If the returned image cannot be decoded or if the clipboard is empty. + */ + default BufferedImage getClipboardImage() throws IOException { + final byte[] base64decodedBytes = Base64 + .getMimeDecoder() + .decode(getClipboard(ClipboardContentType.IMAGE)); + return ImageIO.read(new ByteArrayInputStream(base64decodedBytes)); + } + + /** + * Set an URL to the clipboard. + * + * @param url the actual URL to set. + */ + default void setClipboardUrl(URL url) { + setClipboard(ClipboardContentType.URL, Base64 + .getMimeEncoder() + .encode(requireNonNull(url).toString().getBytes(StandardCharsets.UTF_8))); + } + + /** + * Get an URL from the clipboard. + * + * @return the actual URL instance. + * @throws MalformedURLException if the URL in the clipboard is not valid or if the clipboard is empty. + */ + default URL getClipboardUrl() throws MalformedURLException { + final byte[] base64decodedBytes = Base64 + .getMimeDecoder() + .decode(getClipboard(ClipboardContentType.URL)); + return new URL(new String(base64decodedBytes, 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 f6050923a..0f27380b3 100644 --- a/src/main/java/io/appium/java_client/ios/HasIOSSettings.java +++ b/src/main/java/io/appium/java_client/ios/HasIOSSettings.java @@ -16,17 +16,108 @@ package io.appium.java_client.ios; - 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 + * Sets whether Safari/webviews should convert element taps into x/y taps. + * * @param enabled turns nativeWebTap on if true, off if false + * @return self instance for chaining + */ + default HasIOSSettings nativeWebTap(Boolean enabled) { + return (HasIOSSettings) setSetting(Setting.NATIVE_WEB_TAP, enabled); + } + + /** + * Whether to return compact (standards-compliant) and faster responses from find element/s + * (the default setting). If set to false then the response may also contain other + * available element attributes. + * + * @param enabled Either true or false. The default value if true. + * @return self instance for chaining + */ + default HasIOSSettings setShouldUseCompactResponses(boolean enabled) { + return (HasIOSSettings) setSetting(Setting.SHOULD_USE_COMPACT_RESPONSES, enabled); + } + + /** + * Which attributes should be returned if compact responses are disabled. + * It works only if shouldUseCompactResponses is set to false. Defaults to "type,label" string. + * + * @param attrNames The comma-separated list of fields to return with each element. + * @return self instance for chaining + */ + default HasIOSSettings setElementResponseAttributes(String attrNames) { + return (HasIOSSettings) setSetting(Setting.ELEMENT_RESPONSE_ATTRIBUTES, attrNames); + } + + /** + * The quality of the screenshots generated by the screenshots broadcaster, + * The value of 0 represents the maximum compression + * (or lowest quality) while the value of 100 represents the least compression (or best quality). + * + * @param quality An integer in range 0..100. The default value is 25. + * @return self instance for chaining + */ + default HasIOSSettings setMjpegServerScreenshotQuality(int quality) { + return (HasIOSSettings) setSetting(Setting.MJPEG_SERVER_SCREENSHOT_QUALITY, quality); + } + + /** + * The frame rate at which the background screenshots broadcaster should broadcast screenshots in range 1..60. + * The default value is 10 (Frames Per Second). + * Setting zero value will cause the frame rate to be at its maximum possible value. + * + * @param framerate An integer in range 1..60. The default value is 10. + * @return self instance for chaining + */ + default HasIOSSettings setMjpegServerFramerate(int framerate) { + return (HasIOSSettings) setSetting(Setting.MJPEG_SERVER_FRAMERATE, framerate); + } + + /** + * Changes the quality of phone display screenshots according to XCTest/XCTImageQuality enum. + * Sometimes setting this value to the maximum possible quality may crash XCTest because of + * lack of the memory (lossless screenshot require more space). + * + * @param quality An integer in range 0..2. The default value is 1. + * @return self instance for chaining + */ + default HasIOSSettings setScreenshotQuality(int quality) { + return (HasIOSSettings) setSetting(Setting.SCREENSHOT_QUALITY, quality); + } + + /** + * The scale of screenshots in range 1..100. + * The default value is 100, no scaling + * + * @param scale An integer in range 1..100. The default value is 100. + * @return self instance for chaining + */ + default HasIOSSettings setMjpegScalingFactor(int scale) { + return (HasIOSSettings) setSetting(Setting.MJPEG_SCALING_FACTOR, scale); + } + + /** + * Changes the 'Auto-Correction' preference in Keyboards setting. + * + * @param enabled Either true or false. Defaults to false when WDA starts as xctest. + * @return self instance for chaining + */ + default HasIOSSettings setKeyboardAutocorrection(boolean enabled) { + return (HasIOSSettings) setSetting(Setting.KEYBOARD_AUTOCORRECTION, enabled); + } + + /** + * Changes the 'Predictive' preference in Keyboards setting. + * + * @param enabled Either true or false. Defaults to false when WDA starts as xctest. + * @return self instance for chaining */ - default void nativeWebTap(Boolean enabled) { - setSetting(Setting.NATIVE_WEB_TAP, enabled); + default HasIOSSettings setKeyboardPrediction(boolean enabled) { + return (HasIOSSettings) setSetting(Setting.KEYBOARD_PREDICTION, enabled); } } diff --git a/src/main/java/io/appium/java_client/ios/IOSBatteryInfo.java b/src/main/java/io/appium/java_client/ios/IOSBatteryInfo.java new file mode 100644 index 000000000..3344f9903 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/IOSBatteryInfo.java @@ -0,0 +1,32 @@ +package io.appium.java_client.ios; + +import io.appium.java_client.battery.BatteryInfo; + +import java.util.Map; + +public class IOSBatteryInfo extends BatteryInfo { + + public IOSBatteryInfo(Map input) { + super(input); + } + + @SuppressWarnings("unchecked") + @Override + public BatteryState getState() { + final int state = ((Long) getInput().get("state")).intValue(); + switch (state) { + case 1: + return BatteryState.UNPLUGGED; + case 2: + return BatteryState.CHARGING; + case 3: + return BatteryState.FULL; + default: + return BatteryState.UNKNOWN; + } + } + + public enum BatteryState { + UNKNOWN, UNPLUGGED, CHARGING, FULL + } +} 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 995c6370c..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,172 +16,241 @@ package io.appium.java_client.ios; -import static io.appium.java_client.MobileCommand.RUN_APP_IN_BACKGROUND; -import static io.appium.java_client.MobileCommand.prepareArguments; - +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.FindsByIosUIAutomation; +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.remote.MobilePlatform; +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.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 org.openqa.selenium.security.Credentials; import java.net.URL; -import java.time.Duration; +import java.util.Map; /** - * @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} + * iOS driver implementation. */ -public class IOSDriver - extends AppiumDriver - implements HidesKeyboardWithKeyName, ShakesDevice, HasIOSSettings, - FindsByIosUIAutomation, LocksIOSDevice, PerformsTouchID, FindsByIosNSPredicate, - FindsByIosClassChain { +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 PLATFORM_NAME = Platform.IOS.name(); - private static final String IOS_PLATFORM = MobilePlatform.IOS; + private StringWebSocketClient syslogClient; /** - * @param executor is an instance of {@link org.openqa.selenium.remote.HttpCommandExecutor} + * Creates a new instance based on command {@code executor} and {@code capabilities}. + * + * @param executor is an instance of {@link HttpCommandExecutor} * or class that extends it. Default commands or another vendor-specific * commands may be specified there. - * @param capabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(HttpCommandExecutor executor, Capabilities capabilities) { - super(executor, substituteMobilePlatform(capabilities, IOS_PLATFORM)); + super(executor, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** - * @param remoteAddress is the address - * of remotely/locally started Appium server - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium server URL and {@code capabilities}. + * + * @param remoteAddress is the address of remotely/locally started Appium server + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(URL remoteAddress, Capabilities desiredCapabilities) { - super(remoteAddress, substituteMobilePlatform(desiredCapabilities, IOS_PLATFORM)); + public IOSDriver(URL remoteAddress, Capabilities capabilities) { + super(remoteAddress, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** - * @param remoteAddress is the address - * of remotely/locally started Appium server - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium server URL, HTTP client factory and {@code capabilities}. + * + * @param remoteAddress is the address of remotely/locally started Appium server + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(URL remoteAddress, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(remoteAddress, httpClientFactory, - substituteMobilePlatform(desiredCapabilities, IOS_PLATFORM)); + Capabilities capabilities) { + super(remoteAddress, httpClientFactory, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** - * @param service take a look - * at {@link io.appium.java_client.service.local.AppiumDriverLocalService} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium driver local service and {@code capabilities}. + * + * @param service take a look at {@link AppiumDriverLocalService} + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(AppiumDriverLocalService service, Capabilities desiredCapabilities) { - super(service, substituteMobilePlatform(desiredCapabilities, IOS_PLATFORM)); + public IOSDriver(AppiumDriverLocalService service, Capabilities capabilities) { + super(service, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** - * @param service take a look - * at {@link io.appium.java_client.service.local.AppiumDriverLocalService} - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium driver local service, HTTP client factory and {@code capabilities}. + * + * @param service take a look at {@link AppiumDriverLocalService} + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(AppiumDriverLocalService service, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(service, httpClientFactory, - substituteMobilePlatform(desiredCapabilities, IOS_PLATFORM)); + Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** - * @param builder take a look - * at {@link io.appium.java_client.service.local.AppiumServiceBuilder} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium service builder and {@code capabilities}. + * + * @param builder take a look at {@link AppiumServiceBuilder} + * @param capabilities take a look at {@link Capabilities} */ - public IOSDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) { - super(builder, substituteMobilePlatform(desiredCapabilities, IOS_PLATFORM)); + public IOSDriver(AppiumServiceBuilder builder, Capabilities capabilities) { + super(builder, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** - * @param builder take a look - * at {@link io.appium.java_client.service.local.AppiumServiceBuilder} - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on Appium service builder, HTTP client factory and {@code capabilities}. + * + * @param builder take a look at {@link AppiumServiceBuilder} + * @param httpClientFactory take a look at {@link HttpClient.Factory} + * @param capabilities take a look at {@link Capabilities} */ public IOSDriver(AppiumServiceBuilder builder, HttpClient.Factory httpClientFactory, - Capabilities desiredCapabilities) { - super(builder, httpClientFactory, - substituteMobilePlatform(desiredCapabilities, IOS_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 capabilities take a look at {@link Capabilities} + */ + 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)); } /** - * @param httpClientFactory take a look - * at {@link org.openqa.selenium.remote.http.HttpClient.Factory} - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.Capabilities} + * Creates a new instance based on the given ClientConfig and {@code capabilities}. + * The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}. + * For example: + * + *
+     *
+     * AppiumClientConfig appiumClientConfig = AppiumClientConfig.defaultConfig()
+     *     .directConnect(true)
+     *     .baseUri(URI.create("WebDriver URL"))
+     *     .readTimeout(Duration.ofMinutes(5));
+     * 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(HttpClient.Factory httpClientFactory, Capabilities desiredCapabilities) { - super(httpClientFactory, substituteMobilePlatform(desiredCapabilities, IOS_PLATFORM)); + public IOSDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilities) { + super(appiumClientConfig, ensurePlatformName(capabilities, PLATFORM_NAME)); } /** - * @param desiredCapabilities take a look - * at {@link org.openqa.selenium.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 remoteSessionAddress The address of the **running** session including the session identifier. */ - public IOSDriver(Capabilities desiredCapabilities) { - super(substituteMobilePlatform(desiredCapabilities, IOS_PLATFORM)); + public IOSDriver(URL remoteSessionAddress) { + super(remoteSessionAddress, PLATFORM_NAME, AutomationName.IOS_XCUI_TEST); } /** - * Runs the current app as a background app for the number of seconds - * or minimizes the app + * Creates a new instance based on {@code capabilities}. * - * @param duration The time to run App in background. + * @param capabilities take a look at {@link Capabilities} */ - @Override public void runAppInBackground(Duration duration) { - // timeout parameter is expected to be in milliseconds - // float values are allowed - execute(RUN_APP_IN_BACKGROUND, - prepareArguments("seconds", prepareArguments("timeout", duration.toMillis()))); + public IOSDriver(Capabilities capabilities) { + super(ensurePlatformName(capabilities, PLATFORM_NAME)); } @Override public TargetLocator switchTo() { return new InnerTargetLocator(); } + @Override + public IOSBatteryInfo getBatteryInfo() { + return new IOSBatteryInfo(CommandExecutionHelper.executeScript(this, "mobile: batteryInfo")); + } + private class InnerTargetLocator extends RemoteTargetLocator { @Override public Alert alert() { return new IOSAlert(super.alert()); } } - class IOSAlert implements Alert { private final Alert alert; @@ -204,16 +273,16 @@ class IOSAlert implements Alert { } @Override public void sendKeys(String keysToSend) { - execute(DriverCommand.SET_ALERT_VALUE, prepareArguments("value", keysToSend)); + execute(DriverCommand.SET_ALERT_VALUE, Map.of("value", keysToSend)); } - @Override public void setCredentials(Credentials credentials) { - alert.setCredentials(credentials); - } + } - @Override public void authenticateUsing(Credentials credentials) { - alert.authenticateUsing(credentials); + @Override + public synchronized StringWebSocketClient getSyslogClient() { + if (syslogClient == null) { + 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 d99842d33..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,45 +16,46 @@ 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 java.util.Map} of parameters for the - * device shaking. + * 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 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. */ - 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 java.util.Map} of parameters for the touchId simulator. - * + * 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)); } /** - * This method forms a {@link java.util.Map} of parameters for the toggling touchId + * This method forms a {@link Map} of parameters for the toggling touchId * enrollment in simulator. * + * @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. */ - public static Map.Entry> toggleTouchIdEnrollmentCommand() { - return new AbstractMap.SimpleEntry<>( - TOUCH_ID_ENROLLMENT, ImmutableMap.of()); + @Deprecated + public static Map.Entry> toggleTouchIdEnrollmentCommand(boolean 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 new file mode 100644 index 000000000..5c56cd9a5 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/IOSStartScreenRecordingOptions.java @@ -0,0 +1,146 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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; + private String videoQuality; + private String videoScale; + private String videoFilters; + private Integer fps; + + public static IOSStartScreenRecordingOptions startScreenRecordingOptions() { + return new IOSStartScreenRecordingOptions(); + } + + /** + * {@inheritDoc} + */ + @Override + public IOSStartScreenRecordingOptions withUploadOptions(ScreenRecordingUploadOptions uploadOptions) { + return (IOSStartScreenRecordingOptions) super.withUploadOptions(uploadOptions); + } + + /** + * The video codec type used for encoding of the recorded screen capture. + * Execute `ffmpeg -codecs` in the terminal to see the list of supported video codecs. + * 'mjpeg' by default. + * + * @since Appium 1.10.0 + * @param videoType one of available video codec names, for example 'libx264'. + * @return self instance for chaining. + */ + public IOSStartScreenRecordingOptions withVideoType(String videoType) { + this.videoType = requireNonNull(videoType); + return this; + } + + public enum VideoQuality { + LOW, MEDIUM, HIGH, PHOTO + } + + /** + * The video encoding quality (low, medium, high, photo - defaults to medium). + * Only works for real devices. + * + * @param videoQuality one of possible quality preset names. + * @return self instance for chaining. + */ + public IOSStartScreenRecordingOptions withVideoQuality(VideoQuality videoQuality) { + this.videoQuality = requireNonNull(videoQuality).name().toLowerCase(ROOT); + return this; + } + + /** + * The Frames Per Second rate of the recorded video. Defaults to 10. + * + * @since Appium 1.10.0 + * @param fps frames per second value in range 1..60. + * @return self instance for chaining. + */ + public IOSStartScreenRecordingOptions withFps(int fps) { + this.fps = fps; + return this; + } + + /** + * The scaling value to apply. Read https://trac.ffmpeg.org/wiki/Scaling for possible values. + * No scale is applied by default. + * If filters are set then the scale setting is effectively ignored. + * + * @since Appium 1.10.0 + * @param videoScale ffmpeg-compatible scale format specifier. + * @return self instance for chaining. + */ + public IOSStartScreenRecordingOptions withVideoScale(String videoScale) { + this.videoScale = requireNonNull(videoScale); + return this; + } + + /** + * The maximum recording time. The default value is 180 seconds (3 minutes). + * The maximum value is 30 minutes. + * Setting values greater than this or less than zero will cause an exception. 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 IOSStartScreenRecordingOptions withTimeLimit(Duration timeLimit) { + return super.withTimeLimit(timeLimit); + } + + /** + * The FFMPEG video filters to apply. These filters allow to scale, flip, rotate and do many + * other useful transformations on the source video stream. The format of the property + * must comply with https://ffmpeg.org/ffmpeg-filters.html. + * + * @since Appium 1.15 + * @param filters One or more filters to apply to the resulting video stream, + * for example "transpose=1" to rotate the resulting video 90 degrees clockwise. + * @return self instance for chaining. + */ + public IOSStartScreenRecordingOptions withVideoFilters(String filters) { + this.videoFilters = filters; + return this; + } + + @Override + public Map 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/IOSStopScreenRecordingOptions.java b/src/main/java/io/appium/java_client/ios/IOSStopScreenRecordingOptions.java new file mode 100644 index 000000000..bbe9b33dc --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/IOSStopScreenRecordingOptions.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.ios; + +import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; + +public class IOSStopScreenRecordingOptions extends + BaseStopScreenRecordingOptions { + + public static IOSStopScreenRecordingOptions stopScreenRecordingOptions() { + return new IOSStopScreenRecordingOptions(); + } + +} 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 104fc8f8b..aca87955d 100644 --- a/src/main/java/io/appium/java_client/ios/IOSTouchAction.java +++ b/src/main/java/io/appium/java_client/ios/IOSTouchAction.java @@ -1,42 +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; import io.appium.java_client.PerformsTouchActions; import io.appium.java_client.TouchAction; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.internal.HasIdentity; - +import io.appium.java_client.ios.touch.IOSPressOptions; +import io.appium.java_client.touch.offset.ElementOption; +import io.appium.java_client.touch.offset.PointOption; -public class IOSTouchAction extends TouchAction { +/** + * 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) { super(performsTouchActions); } /** - * Double taps an element, offset from upper left corner. + * Double taps using coordinates. * - * @param el element to tap. - * @param x x offset. - * @param y y offset. - * @return this TouchAction, for chaining. + * @param doubleTapOption see {@link PointOption} and {@link ElementOption}.. + * @return self-reference */ - public IOSTouchAction doubleTap(WebElement el, int x, int y) { - ActionParameter action = new ActionParameter("doubleTap", (HasIdentity) el); - action.addParameter("x", x); - action.addParameter("y", y); - parameterBuilder.add(action); + public IOSTouchAction doubleTap(PointOption doubleTapOption) { + parameters.add(new ActionParameter("doubleTap", doubleTapOption)); return this; } /** - * Double taps an element, offset from upper left corner. + * Press action on the screen. * - * @param el element to tap. + * @param pressOptions see {@link IOSPressOptions} * @return this TouchAction, for chaining. */ - public IOSTouchAction doubleTap(WebElement el) { - ActionParameter action = new ActionParameter("doubleTap", (HasIdentity) el); - parameterBuilder.add(action); + public IOSTouchAction press(IOSPressOptions 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 new file mode 100644 index 000000000..98a75158a --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/ListensToSyslogMessages.java @@ -0,0 +1,134 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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.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(); + + /** + * Start syslog messages broadcast via web socket. + * This method assumes that Appium server is running on localhost and + * is assigned to the default port (4723). + */ + default void startSyslogBroadcast() { + startSyslogBroadcast("localhost"); + } + + /** + * Start syslog messages broadcast via web socket. + * This method assumes that Appium server is assigned to the default port (4723). + * + * @param host the name of the host where Appium server is running + */ + default void startSyslogBroadcast(String host) { + startSyslogBroadcast(host, DEFAULT_APPIUM_PORT); + } + + /** + * Start syslog messages broadcast via web socket. + * + * @param host the name of the host where Appium server is running + * @param port the port of the host where Appium server is running + */ + default void startSyslogBroadcast(String host, int port) { + 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)); + } + + /** + * Adds a new log messages broadcasting handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which accepts a single argument, which is the actual log message + */ + default void addSyslogMessagesListener(Consumer handler) { + getSyslogClient().addMessageHandler(handler); + } + + /** + * Adds a new log broadcasting errors handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which accepts a single argument, which is the actual exception instance + */ + default void addSyslogErrorsListener(Consumer handler) { + getSyslogClient().addErrorHandler(handler); + } + + /** + * Adds a new log broadcasting connection handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which is executed as soon as the client is successfully + * connected to the web socket + */ + default void addSyslogConnectionListener(Runnable handler) { + getSyslogClient().addConnectionHandler(handler); + } + + /** + * Adds a new log broadcasting disconnection handler. + * Several handlers might be assigned to a single server. + * Multiple calls to this method will cause such handler + * to be called multiple times. + * + * @param handler a function, which is executed as soon as the client is successfully + * disconnected from the web socket + */ + default void addSyslogDisconnectionListener(Runnable handler) { + getSyslogClient().addDisconnectionHandler(handler); + } + + /** + * Removes all existing syslog handlers. + */ + default void removeAllSyslogListeners() { + getSyslogClient().removeAllHandlers(); + } + + /** + * Stops syslog messages broadcast via web socket. + */ + default void stopSyslogBroadcast() { + 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 53a3d1bf1..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,28 +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. + * 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() { - CommandExecutionHelper.execute(this, toggleTouchIdEnrollmentCommand()); + default void toggleTouchIDEnrollment(boolean enabled) { + CommandExecutionHelper.executeScript(this, "mobile: enrollBiometric", Map.of( + "isEnabled", enabled + )); } } diff --git a/src/main/java/io/appium/java_client/ios/ShakesDevice.java b/src/main/java/io/appium/java_client/ios/ShakesDevice.java index 208f05bb1..57302ef8a 100644 --- a/src/main/java/io/appium/java_client/ios/ShakesDevice.java +++ b/src/main/java/io/appium/java_client/ios/ShakesDevice.java @@ -16,17 +16,25 @@ package io.appium.java_client.ios; -import static io.appium.java_client.ios.IOSMobileCommandHelper.shakeCommand; - +import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; +import org.openqa.selenium.UnsupportedCommandException; + +import static io.appium.java_client.ios.IOSMobileCommandHelper.shakeCommand; -public interface ShakesDevice extends ExecutesMethod { +public interface ShakesDevice extends ExecutesMethod, CanRememberExtensionPresence { /** - * Simulate shaking the device. + * Simulate shaking the Simulator. This API does not work for real devices. */ default void shake() { - CommandExecutionHelper.execute(this, shakeCommand()); + final String extName = "mobile: shake"; + try { + CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); + } catch (UnsupportedCommandException e) { + // TODO: Remove the fallback + CommandExecutionHelper.execute(markExtensionAbsence(extName), shakeCommand()); + } } } diff --git a/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java b/src/main/java/io/appium/java_client/ios/options/XCUITestOptions.java 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/ios/options/wda/Keychain.java b/src/main/java/io/appium/java_client/ios/options/wda/Keychain.java new file mode 100644 index 000000000..8a2e69891 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/Keychain.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Data; +import lombok.ToString; + +@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/ios/options/wda/XcodeCertificate.java b/src/main/java/io/appium/java_client/ios/options/wda/XcodeCertificate.java new file mode 100644 index 000000000..49d2d4639 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/options/wda/XcodeCertificate.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Data; +import lombok.ToString; + +@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 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 new file mode 100644 index 000000000..a6b8cdf7f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/touch/IOSPressOptions.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.touch; + +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; + + /** + * It creates an empty instance of {@link IOSPressOptions}. + * + * @return an empty instance of {@link IOSPressOptions} + */ + public static IOSPressOptions iosPressOptions() { + return new IOSPressOptions(); + } + + /** + * Set the pressure value. This allows to simulate force/3D touch on + * devices, that support it. + * + * @param pressure the value to set. + * See the description of `force` property on Apple's UITouch class + * (https://developer.apple.com/documentation/uikit/uitouch?language=objc) + * for more details on possible value ranges. + * + * @return this instance for chaining. + */ + public IOSPressOptions withPressure(double pressure) { + this.pressure = pressure; + return this; + } + + @Override + public Map build() { + final Map result = super.build(); + ofNullable(pressure).ifPresent(x -> result.put("pressure", x)); + return result; + } +} 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/AndroidBy.java b/src/main/java/io/appium/java_client/pagefactory/AndroidBy.java index b802cbbbc..b40acdd07 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AndroidBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/AndroidBy.java @@ -21,9 +21,10 @@ */ public @interface AndroidBy { /** - * It is an is Android UIAutomator string. - * Read http://developer.android.com/intl/ru/tools/testing-support-library/ - * index.html#uia-apis + * A String that can build an Android UiSelector or UiScrollable object. + * Refer to https://developer.android.com/training/testing/ui-automator + * + * @return an Android UIAutomator string */ String uiAutomator() default ""; @@ -31,31 +32,43 @@ * It an UI automation accessibility Id which is a convenient to Android. * About Android accessibility * https://developer.android.com/intl/ru/training/accessibility/accessible-app.html + * + * @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 ""; /** - * @return priority of the searching. Higher number means lower priority. + * 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/AndroidFindAll.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindAll.java index 1f411a4a5..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,17 +16,17 @@ 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.AndroidBy} tags + * series of {@link AndroidBy} tags * It will then search for all elements that match any criteria. Note that elements * are not guaranteed to be in document order. */ @@ -34,13 +34,16 @@ @Repeatable(AndroidFindByAllSet.class) public @interface AndroidFindAll { /** - * It is a set of {@link io.appium.java_client.pagefactory.AndroidBy} strategies which may - * be used to find the target element. + * It is a set of {@link AndroidBy} strategies which may be used to find the target element. + * + * @return a collection of strategies which may be used to find the target element */ AndroidBy[] value(); /** - * @return priority of the searching. Higher number means lower priority. + * 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/AndroidFindBy.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindBy.java index bb17d861b..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 @@ -36,9 +36,10 @@ @Repeatable(AndroidFindBySet.class) public @interface AndroidFindBy { /** - * It is an is Android UIAutomator string. - * Read http://developer.android.com/intl/ru/tools/testing-support-library/ - * index.html#uia-apis + * A String that can build an Android UiSelector or UiScrollable object. + * Refer to https://developer.android.com/training/testing/ui-automator + * + * @return an Android UIAutomator string */ String uiAutomator() default ""; @@ -46,31 +47,57 @@ * It an UI automation accessibility Id which is a convenient to Android. * About Android accessibility * https://developer.android.com/intl/ru/training/accessibility/accessible-app.html + * + * @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 desired data matcher expression. + * + * @return a desired data matcher expression + */ + String androidDataMatcher() default ""; + + /** + * It is a desired view matcher expression. + * + * @return a desired view matcher expression + */ + String androidViewMatcher() default ""; + /** * It is a xpath to the target element. + * + * @return a xpath to the target element */ String xpath() default ""; /** - * @return priority of the searching. Higher number means lower priority. + * 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/AndroidFindByAllSet.java b/src/main/java/io/appium/java_client/pagefactory/AndroidFindByAllSet.java index 40e49415a..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} @@ -15,8 +15,10 @@ @Retention(value = RUNTIME) public @interface AndroidFindByAllSet { /** + * An array which builds a sequence of the chained searching for elements or a set of possible locators. + * * @return an array of {@link AndroidFindAll} which builds a sequence of - * the chained searching for elements or a set of possible locators + * the chained searching for elements or a set of possible locators */ AndroidFindAll[] value(); } 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 bf1958767..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} @@ -15,8 +15,10 @@ @Retention(value = RUNTIME) public @interface AndroidFindByChainSet { /** + * An array of which builds a sequence of the chained searching for elements or a set of possible locators. + * * @return an array of {@link io.appium.java_client.pagefactory.AndroidFindBys} which builds a sequence of - * the chained searching for elements or a set of possible locators + * the chained searching for elements or a set of possible locators */ AndroidFindBys[] value(); } 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 08be7d053..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} @@ -31,8 +31,10 @@ @Retention(value = RUNTIME) public @interface AndroidFindBySet { /** + * An array of which builds a sequence of the chained searching for elements or a set of possible locators. + * * @return an array of {@link io.appium.java_client.pagefactory.AndroidFindBy} which builds a sequence of - * the chained searching for elements or a set of possible locators + * the chained searching for elements or a set of possible locators */ AndroidFindBy[] value(); } 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 e8c0d2ae1..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. @@ -34,11 +34,15 @@ /** * It is a set of {@link io.appium.java_client.pagefactory.AndroidBy} 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 */ AndroidBy[] value(); /** - * @return priority of the searching. Higher number means lower priority. + * 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/AppiumElementLocator.java b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocator.java index 6ce4e3b1b..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,41 +16,68 @@ 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 com.google.common.base.Function; - +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; import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.TimeoutException; -import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; 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.concurrent.TimeUnit; +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 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 TimeOutDuration timeOutDuration; - private final TimeOutDuration originalTimeOutDuration; - private final WebDriver originalWebDriver; + private final Duration duration; + private final WeakReference searchContextReference; private final SearchContext searchContext; + private WebElement cachedElement; private List cachedElementList; - private final String exceptionMessageIfElementNotFound; + + /** + * 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 @@ -60,43 +87,57 @@ class AppiumElementLocator implements CacheableLocator { * @param by a By locator strategy * @param shouldCache is the flag that signalizes that elements which * are found once should be cached - * @param duration is a POJO which contains timeout parameters for the element to be searched - * @param originalDuration is a POJO which contains timeout parameters from page object which contains the element - * @param originalWebDriver is an instance of WebDriver that is going to be - * used by a proxied element + * @param duration timeout parameter for the element to be found */ - - public AppiumElementLocator(SearchContext searchContext, By by, boolean shouldCache, - TimeOutDuration duration, TimeOutDuration originalDuration, WebDriver originalWebDriver) { + public AppiumElementLocator( + SearchContext searchContext, + By by, + boolean shouldCache, + Duration duration + ) { + this.searchContextReference = null; this.searchContext = searchContext; this.shouldCache = shouldCache; - this.timeOutDuration = duration; - this.originalTimeOutDuration = originalDuration; + this.duration = duration; this.by = by; - this.originalWebDriver = originalWebDriver; - this.exceptionMessageIfElementNotFound = "Can't locate an element by this strategy: " + by.toString(); } - private void changeImplicitlyWaitTimeOut(long newTimeOut, TimeUnit newTimeUnit) { - originalWebDriver.manage().timeouts().implicitlyWait(newTimeOut, newTimeUnit); + 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} + * then it is switched to the searching for some html or native mobile element. + * Otherwise nothing happens there. + * + * @param currentBy is some locator strategy + * @param currentContent is an instance of some subclass of the {@link SearchContext}. + * @return the corrected {@link By} for the further searching + */ + private static By getBy(By currentBy, SearchContext currentContent) { + if (!ContentMappedBy.class.isAssignableFrom(currentBy.getClass())) { + return currentBy; + } + + return ((ContentMappedBy) currentBy).useContent(getCurrentContentType(currentContent)); } - private T waitFor(Supplier supplier) { + private T waitFor(Supplier supplier) { WaitingFunction function = new WaitingFunction<>(); try { - changeImplicitlyWaitTimeOut(0, TimeUnit.SECONDS); FluentWait> wait = new FluentWait<>(supplier) .ignoring(NoSuchElementException.class); - wait.withTimeout(timeOutDuration.getTime(), timeOutDuration.getTimeUnit()); + wait.withTimeout(duration); return wait.until(function); } catch (TimeoutException e) { if (function.foundStaleElementReferenceException != null) { - throw StaleElementReferenceException - .class.cast(function.foundStaleElementReferenceException); + throw (StaleElementReferenceException) function.foundStaleElementReferenceException; } throw e; - } finally { - changeImplicitlyWaitTimeOut(originalTimeOutDuration.getTime(), originalTimeOutDuration.getTimeUnit()); } } @@ -108,15 +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(by)); + WebElement result = waitFor(() -> searchContext.findElement(bySearching)); if (shouldCache) { cachedElement = result; } return result; } catch (TimeoutException | StaleElementReferenceException e) { - throw new NoSuchElementException(exceptionMessageIfElementNotFound, e); + throw new NoSuchElementException(format(EXCEPTION_MESSAGE_IF_ELEMENT_NOT_FOUND, bySearching.toString()), e); } } @@ -128,14 +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(by); - if (list.size() > 0) { - return list; - } - return null; + List list = searchContext.findElements(getBy(by, searchContext)); + return list.isEmpty() ? null : list; }); } catch (TimeoutException | StaleElementReferenceException e) { result = new ArrayList<>(); @@ -152,7 +202,7 @@ public List findElements() { } @Override public String toString() { - return String.format("Located by %s", by); + return format("Located by %s", by); } @@ -167,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 819bdb263..f423d1dca 100644 --- a/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/AppiumElementLocatorFactory.java @@ -19,57 +19,88 @@ 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 org.openqa.selenium.WebDriver; +import java.lang.ref.WeakReference; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; +import java.time.Duration; + +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 TimeOutDuration timeOutDuration; - private final WebDriver originalWebDriver; + private final WeakReference searchContextReference; + private final Duration duration; private final AppiumByBuilder builder; /** * Creates a new mobile element locator factory. * * @param searchContext The context to use when finding the element - * @param timeOutDuration is a POJO which contains timeout parameters for the element to be searched - * @param originalWebDriver is an instance of WebDriver that is going to be used by a proxied element + * @param duration timeout parameters for the elements to be found * @param builder is handler of Appium-specific page object annotations */ - - public AppiumElementLocatorFactory(SearchContext searchContext, TimeOutDuration timeOutDuration, - WebDriver originalWebDriver, AppiumByBuilder builder) { + public AppiumElementLocatorFactory( + SearchContext searchContext, + Duration duration, + AppiumByBuilder builder + ) { this.searchContext = searchContext; - this.originalWebDriver = originalWebDriver; - this.timeOutDuration = timeOutDuration; + this.searchContextReference = null; + this.duration = duration; + this.builder = builder; + } + + /** + * 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 CacheableLocator createLocator(AnnotatedElement annotatedElement) { - TimeOutDuration customDuration; + @Nullable + @Override + public CacheableLocator createLocator(AnnotatedElement annotatedElement) { + Duration customDuration; if (annotatedElement.isAnnotationPresent(WithTimeout.class)) { WithTimeout withTimeout = annotatedElement.getAnnotation(WithTimeout.class); - customDuration = new TimeOutDuration(withTimeout.time(), withTimeout.unit()); + customDuration = build(withTimeout); } else { - customDuration = timeOutDuration; + customDuration = duration; } - builder.setAnnotated(annotatedElement); - By by = builder.buildBy(); - if (by != null) { - return new AppiumElementLocator(searchContext, by, builder.isLookupCached(), - customDuration, timeOutDuration, originalWebDriver); + 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); } - return 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 bc866a6aa..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,36 +16,40 @@ 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 com.google.common.collect.ImmutableList; - -import io.appium.java_client.HasSessionDetails; -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.internal.CapabilityHelpers; import io.appium.java_client.pagefactory.bys.ContentType; import io.appium.java_client.pagefactory.locator.CacheableLocator; -import io.appium.java_client.windows.WindowsElement; +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; import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.CapabilityType; 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; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; + +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 @@ -53,71 +57,115 @@ * {@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); - public static long DEFAULT_IMPLICITLY_WAIT_TIMEOUT = 1; - public static TimeUnit DEFAULT_TIMEUNIT = TimeUnit.SECONDS; - private final WebDriver originalDriver; - private final DefaultFieldDecorator defaultElementFieldDecoracor; + private static final List> AVAILABLE_ELEMENT_CLASSES = List.of( + WebElement.class, + RemoteWebElement.class + ); + public static final Duration DEFAULT_WAITING_TIMEOUT = ofSeconds(1); + private final WeakReference webDriverReference; + private final DefaultFieldDecorator defaultElementFieldDecorator; private final AppiumElementLocatorFactory widgetLocatorFactory; private final String platform; private final String automation; - private final TimeOutDuration timeOutDuration; - private final HasSessionDetails hasSessionDetails; + private final Duration duration; + + /** + * Creates field decorator based on {@link SearchContext} and timeout {@code duration}. + * + * @param context is an instance of {@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. + */ + public AppiumFieldDecorator(SearchContext context, Duration duration) { + this.webDriverReference = requireWebDriverReference(context); + this.platform = readStringCapability(context, CapabilityType.PLATFORM_NAME); + this.automation = readStringCapability(context, AUTOMATION_NAME_OPTION); + this.duration = duration; + defaultElementFieldDecorator = createFieldDecorator(new AppiumElementLocatorFactory( + context, duration, new DefaultElementByBuilder(platform, automation) + )); + widgetLocatorFactory = new AppiumElementLocatorFactory( + context, duration, new WidgetByBuilder(platform, automation) + ); + } - public AppiumFieldDecorator(SearchContext context, long implicitlyWaitTimeOut, - TimeUnit timeUnit) { - this(context, new TimeOutDuration(implicitlyWaitTimeOut, timeUnit)); + public AppiumFieldDecorator(SearchContext context) { + this(context, DEFAULT_WAITING_TIMEOUT); } /** - * @param context is an instance of {@link org.openqa.selenium.SearchContext} - * It may be the instance of {@link org.openqa.selenium.WebDriver} - * or {@link org.openqa.selenium.WebElement} or - * {@link io.appium.java_client.pagefactory.Widget} or some other user's - * extension/implementation. - * @param timeOutDuration is a desired duration of the waiting for an element presence. + * 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. */ - public AppiumFieldDecorator(SearchContext context, TimeOutDuration timeOutDuration) { - this.originalDriver = unpackWebDriverFromSearchContext(context); - if (originalDriver == null - || !HasSessionDetails.class.isAssignableFrom(originalDriver.getClass())) { - hasSessionDetails = null; - platform = null; - automation = null; - } else { - hasSessionDetails = HasSessionDetails.class.cast(originalDriver); - platform = hasSessionDetails.getPlatformName(); - automation = hasSessionDetails.getAutomationName(); - } + 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; + + defaultElementFieldDecorator = createFieldDecorator(new AppiumElementLocatorFactory( + contextReference, duration, new DefaultElementByBuilder(platform, automation) + )); + widgetLocatorFactory = new AppiumElementLocatorFactory( + contextReference, duration, new WidgetByBuilder(platform, automation) + ); + } - this.timeOutDuration = timeOutDuration; + @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() + ) + )); + } - defaultElementFieldDecoracor = new DefaultFieldDecorator( - new AppiumElementLocatorFactory(context, timeOutDuration, originalDriver, - new DefaultElementByBuilder(platform, automation))) { + @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) { + protected List proxyForListLocator(ClassLoader ignored, ElementLocator locator) { ElementListInterceptor elementInterceptor = new ElementListInterceptor(locator); + //noinspection unchecked return getEnhancedProxy(ArrayList.class, elementInterceptor); } - @Override protected boolean isDecoratableList(Field field) { + @Override + protected boolean isDecoratableList(Field field) { if (!List.class.isAssignableFrom(field.getType())) { return false; } @@ -128,41 +176,28 @@ protected List proxyForListLocator(ClassLoader ignored, } Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0]; - - for (Class webElementClass : availableElementClasses) { - if (webElementClass.equals(listType)) { - return true; - } - } - return false; + List bounds = (listType instanceof TypeVariable) + ? Arrays.asList(((TypeVariable) listType).getBounds()) + : Collections.emptyList(); + return AVAILABLE_ELEMENT_CLASSES.stream() + .anyMatch(webElClass -> webElClass.equals(listType) || bounds.contains(webElClass)); } }; - - widgetLocatorFactory = - new AppiumElementLocatorFactory(context, timeOutDuration, originalDriver, - new WidgetByBuilder(platform, automation)); - } - - public AppiumFieldDecorator(SearchContext context) { - this(context, DEFAULT_IMPLICITLY_WAIT_TIMEOUT, DEFAULT_TIMEUNIT); } /** + * Decorated page object {@code field}. + * * @param ignored class loader is ignored by current implementation - * @param field is {@link java.lang.reflect.Field} of page object which is supposed to be - * decorated. + * @param field is {@link Field} of page object which is supposed to be decorated. * @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,37 +220,41 @@ private Object decorateWidget(Field field) { } if (listType instanceof Class) { - if (!Widget.class.isAssignableFrom((Class) listType)) { + if (!Widget.class.isAssignableFrom((Class) listType)) { return null; } - widgetType = Class.class.cast(listType); + //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, automation); + Map> map = OverrideWidgetReader.read(widgetType, field, platform); if (isAlist) { - return getEnhancedProxy(ArrayList.class, - new WidgetListInterceptor(locator, originalDriver, map, widgetType, - timeOutDuration)); + return getEnhancedProxy( + ArrayList.class, + new WidgetListInterceptor(locator, webDriverReference, map, widgetType, duration) + ); } - Constructor constructor = - WidgetConstructorUtil.findConvenientConstructor(widgetType); - return getEnhancedProxy(widgetType, new Class[] {constructor.getParameterTypes()[0]}, - new Object[] {proxyForAnElement(locator)}, - new WidgetInterceptor(locator, originalDriver, null, map, timeOutDuration)); + Constructor constructor = WidgetConstructorUtil.findConvenientConstructor(widgetType); + return getEnhancedProxy( + widgetType, + new Class[]{constructor.getParameterTypes()[0]}, + new Object[]{proxyForAnElement(locator)}, + new WidgetInterceptor(locator, webDriverReference, null, map, duration) + ); } private WebElement proxyForAnElement(ElementLocator locator) { - ElementInterceptor elementInterceptor = new ElementInterceptor(locator, originalDriver); - return getEnhancedProxy(getElementClass(hasSessionDetails), 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 84e815997..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) { @@ -96,20 +96,20 @@ protected By buildDefaultBy() { By defaultBy = null; FindBy findBy = annotatedElement.getAnnotation(FindBy.class); if (findBy != null) { - defaultBy = super.buildByFromFindBy(findBy); + defaultBy = new FindBy.FindByBuilder().buildIt(findBy, (Field) annotatedElement); } if (defaultBy == null) { FindBys findBys = annotatedElement.getAnnotation(FindBys.class); if (findBys != null) { - defaultBy = super.buildByFromFindBys(findBys); + defaultBy = new FindBys.FindByBuilder().buildIt(findBys, (Field) annotatedElement); } } if (defaultBy == null) { FindAll findAll = annotatedElement.getAnnotation(FindAll.class); if (findAll != null) { - defaultBy = super.buildBysFromFindByOneOf(findAll); + defaultBy = new FindAll.FindByBuilder().buildIt(findAll, (Field) annotatedElement); } } return defaultBy; @@ -155,7 +155,7 @@ private By[] getBys(Class singleLocator, Class howToUseLocatorsOptional = ofNullable(howToUseLocators); - By result = null; - if (isSelendroidAutomation()) { - result = buildMobileBy(howToUseLocatorsOptional - .map(HowToUseLocators::selendroidAutomation).orElse(null), - getBys(SelendroidFindBy.class, SelendroidFindBys.class, SelendroidFindAll.class)); - } - - if (isAndroid() && result == null) { + if (isAndroid()) { return buildMobileBy(howToUseLocatorsOptional.map(HowToUseLocators::androidAutomation).orElse(null), getBys(AndroidFindBy.class, AndroidFindBys.class, AndroidFindAll.class)); } - if (isIOSXcuit()) { - result = buildMobileBy(howToUseLocatorsOptional.map(HowToUseLocators::iOSXCUITAutomation).orElse(null), + if (isIOSXcuit() || isIOS() || isTvOS()) { + return buildMobileBy(howToUseLocatorsOptional.map(HowToUseLocators::iOSXCUITAutomation).orElse(null), getBys(iOSXCUITFindBy.class, iOSXCUITFindBys.class, iOSXCUITFindAll.class)); } - if (isIOS() && result == null) { - return buildMobileBy(howToUseLocatorsOptional.map(HowToUseLocators::iOSAutomation).orElse(null), - getBys(iOSFindBy.class, iOSFindBys.class, iOSFindAll.class)); - } - - if (isWindows()) { - return buildMobileBy(howToUseLocatorsOptional.map(HowToUseLocators::windowsAutomation).orElse(null), - getBys(WindowsFindBy.class, WindowsFindBys.class, WindowsFindAll.class)); - } - - return ofNullable(result).orElse(null); + return null; } @Override public boolean isLookupCached() { AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated(); - return (annotatedElement.getAnnotation(CacheLookup.class) != null); + return annotatedElement.getAnnotation(CacheLookup.class) != null; } private By returnMappedBy(By byDefault, By nativeAppBy) { @@ -218,15 +201,13 @@ public By buildBy() { String idOrName = ((Field) annotatedElementContainer.getAnnotated()).getName(); if (defaultBy == null && mobileNativeBy == null) { - defaultBy = - new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); + defaultBy = new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); mobileNativeBy = new By.ById(idOrName); return returnMappedBy(defaultBy, mobileNativeBy); } if (defaultBy == null) { - defaultBy = - new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); + defaultBy = new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName()); return returnMappedBy(defaultBy, mobileNativeBy); } diff --git a/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java index a95740aff..82b61990b 100644 --- a/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java +++ b/src/main/java/io/appium/java_client/pagefactory/ElementInterceptor.java @@ -16,25 +16,27 @@ package io.appium.java_client.pagefactory; -import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; - import io.appium.java_client.pagefactory.interceptors.InterceptorOfASingleElement; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.pagefactory.ElementLocator; +import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import static io.appium.java_client.pagefactory.ThrowableUtil.extractReadableException; + /** - * Intercepts requests to {@link io.appium.java_client.MobileElement}. + * Intercepts requests to {@link WebElement}. */ -class ElementInterceptor extends InterceptorOfASingleElement { +public class ElementInterceptor extends InterceptorOfASingleElement { - ElementInterceptor(ElementLocator locator, WebDriver driver) { + public ElementInterceptor(ElementLocator locator, WeakReference driver) { super(locator, driver); } - @Override protected Object getObject(WebElement element, Method method, Object[] args) + @Override + protected Object getObject(WebElement element, Method method, Object[] args) throws Throwable { try { return method.invoke(element, args); diff --git a/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java b/src/main/java/io/appium/java_client/pagefactory/ElementListInterceptor.java index 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 5208367b7..cdeb9da1e 100644 --- a/src/main/java/io/appium/java_client/pagefactory/HowToUseLocators.java +++ b/src/main/java/io/appium/java_client/pagefactory/HowToUseLocators.java @@ -24,37 +24,22 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) public @interface HowToUseLocators { /** - * @return the strategy which defines how to use locators which are described by - * the {@link AndroidFindBy} annotation. These annotations can define the chained searching + * The strategy which defines how to use locators which are described by the + * {@link AndroidFindBy} 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 AndroidFindBy} annotation */ LocatorGroupStrategy androidAutomation() default LocatorGroupStrategy.CHAIN; /** - * @return the strategy which defines how to use locators which are described by - * the {@link SelendroidFindBy} annotation. These annotations can define the chained searching - * or the searching by all possible locators. - */ - LocatorGroupStrategy selendroidAutomation() default LocatorGroupStrategy.CHAIN; - - /** - * @return the strategy which defines how to use locators which are described by - * the {@link iOSFindBy} annotation. These annotations can define the chained searching - * or the searching by all possible locators. - */ - LocatorGroupStrategy iOSAutomation() default LocatorGroupStrategy.CHAIN; - - /** - * @return 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. - */ - LocatorGroupStrategy windowsAutomation() default LocatorGroupStrategy.CHAIN; - - /** - * @return the strategy which defines how to use locators which are described by - * the {@link iOSXCUITFindBy} annotation. These annotations can define the chained searching + * 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 */ LocatorGroupStrategy iOSXCUITAutomation() default LocatorGroupStrategy.CHAIN; } diff --git a/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java b/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java index d4d8870d0..53bdee8f0 100644 --- a/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java +++ b/src/main/java/io/appium/java_client/pagefactory/OverrideWidget.java @@ -49,25 +49,6 @@ */ Class androidUIAutomator() default Widget.class; - /** - * This property is designed for iOS native content. - * A declared class should not be abstract. Declared class also should be a subclass - * of an annotated class/class which is declared by an annotated field. - * - * @return a class which extends {@link io.appium.java_client.pagefactory.Widget} - */ - Class iOSUIAutomation() default Widget.class; - - /** - * This property is designed for Android native content when - * {@link io.appium.java_client.remote.AutomationName#SELENDROID} automation is used. - * A declared class should not be abstract. Declared class also should be a subclass - * of an annotated class/class which is declared by an annotated field. - * - * @return a class which extends {@link io.appium.java_client.pagefactory.Widget} - */ - Class selendroid() default Widget.class; - /** * This property is designed for iOS native content when * {@link io.appium.java_client.remote.AutomationName#IOS_XCUI_TEST} automation is used. 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 e814b807b..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,26 +24,33 @@ 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"; private static final String ANDROID_UI_AUTOMATOR = "androidUIAutomator"; - private static final String IOS_UI_AUTOMATION = "iOSUIAutomation"; - private static final String SELENDROID = "selendroid"; 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, new Class[] {}).invoke(overrideWidget)) - .equals(EMPTY)) { + (Class) OverrideWidget.class + .getDeclaredMethod(method).invoke(overrideWidget)) + .equals(EMPTY)) { convenientClass = declaredClass; } } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { @@ -58,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 automation) { - String transformedPlatform = String.valueOf(platform).toUpperCase().trim(); - String transformedAutomation = String.valueOf(automation).toUpperCase().trim(); + AnnotatedElement annotatedElement, String platform) { + String transformedPlatform = String.valueOf(platform).toUpperCase(ROOT).trim(); if (ANDROID.equalsIgnoreCase(transformedPlatform)) { - if (AutomationName.SELENDROID.equalsIgnoreCase(transformedAutomation)) { - return getConvenientClass(declaredClass, annotatedElement, SELENDROID); - } - return getConvenientClass(declaredClass, annotatedElement, ANDROID_UI_AUTOMATOR); } if (IOS.equalsIgnoreCase(transformedPlatform)) { - if (AutomationName.IOS_XCUI_TEST.equalsIgnoreCase(transformedAutomation)) { - return getConvenientClass(declaredClass, annotatedElement, IOS_XCUIT_AUTOMATION); - } - - return getConvenientClass(declaredClass, annotatedElement, IOS_UI_AUTOMATION); + return getConvenientClass(declaredClass, annotatedElement, IOS_XCUIT_AUTOMATION); } if (WINDOWS.equalsIgnoreCase(transformedPlatform)) { @@ -101,29 +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, - String automation) { + Class declaredClass, AnnotatedElement annotatedElement, String platform) { Class clazz = - getMobileNativeWidgetClass(declaredClass, annotatedElement, platform, automation); + getMobileNativeWidgetClass(declaredClass, annotatedElement, platform); return findConvenientConstructor(clazz); } protected static Map> read( - Class declaredClass, AnnotatedElement annotatedElement, String platform, - String automation) { + 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, - automation)); + getConstructorOfAMobileNativeWidgets(declaredClass, annotatedElement, platform)); return result; } } diff --git a/src/main/java/io/appium/java_client/pagefactory/SelendroidBy.java b/src/main/java/io/appium/java_client/pagefactory/SelendroidBy.java deleted file mode 100644 index 75933eba4..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/SelendroidBy.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.pagefactory; - - -/** - * Used to build a complex selendroid locator. - */ -public @interface SelendroidBy { - /** - * It is an id of the target element. - */ - String id() default ""; - - /** - * It is used in Selendroid mode instead of accessibility id. - */ - String name() default ""; - - /** - * It is a className of the target element. - */ - String className() default ""; - - /** - * It is a desired element tag. - */ - String tagName() default ""; - - /** - * It is a xpath to the target element. - */ - String xpath() default ""; - - /** - * It is a text of the desired element. - */ - String linkText() default ""; - - /** - * It is a part of the text of the desired element. - */ - String partialLinkText() default ""; - - /** - * @return priority of the searching. Higher number means lower priority. - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindAll.java b/src/main/java/io/appium/java_client/pagefactory/SelendroidFindAll.java deleted file mode 100644 index 8e2e56a79..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindAll.java +++ /dev/null @@ -1,44 +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; - -/** - * 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.SelendroidBy} 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}) -public @interface SelendroidFindAll { - /** - * It is a set of {@link io.appium.java_client.pagefactory.SelendroidBy} strategies which - * may be used to find the target element. - */ - SelendroidBy[] value(); - - /** - * @return priority of the searching. Higher number means lower priority. - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBy.java b/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBy.java deleted file mode 100644 index 146079a14..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBy.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.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 Selendroid UI selectors like, id, name, class name, tag and xpath - */ -@Retention(RUNTIME) @Target({FIELD, TYPE}) -@Repeatable(SelendroidFindBySet.class) -public @interface SelendroidFindBy { - /** - * It is an id of the target element. - */ - String id() default ""; - - /** - * It is used in Selendroid mode instead of accessibility id. - */ - String name() default ""; - - /** - * It is a className of the target element. - */ - String className() default ""; - - /** - * It is a desired element tag. - */ - String tagName() default ""; - - /** - * It is a xpath to the target element. - */ - String xpath() default ""; - - /** - * It is a text of the desired element. - */ - String linkText() default ""; - - /** - * It is a part of the text of the desired element. - */ - String partialLinkText() default ""; - - /** - * @return priority of the searching. Higher number means lower priority. - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindByAllSet.java b/src/main/java/io/appium/java_client/pagefactory/SelendroidFindByAllSet.java deleted file mode 100644 index 0b83c223c..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindByAllSet.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.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 SelendroidFindAll} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface SelendroidFindByAllSet { - /** - * @return an array of {@link SelendroidFindAll} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - SelendroidFindAll[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindByChainSet.java b/src/main/java/io/appium/java_client/pagefactory/SelendroidFindByChainSet.java deleted file mode 100644 index 5824ed173..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindByChainSet.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.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 SelendroidFindBys} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface SelendroidFindByChainSet { - /** - * @return an array of {@link SelendroidFindBys} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - SelendroidFindBys[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBySet.java b/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBySet.java deleted file mode 100644 index 38e903966..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBySet.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.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 io.appium.java_client.pagefactory.SelendroidFindBy} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface SelendroidFindBySet { - /** - * @return an array of {@link io.appium.java_client.pagefactory.SelendroidFindBy} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - SelendroidFindBy[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBys.java b/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBys.java deleted file mode 100644 index 6a7bd2f3a..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/SelendroidFindBys.java +++ /dev/null @@ -1,42 +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; - -/** - * Used to mark a field on a Page Object to indicate that lookup should - * use a series of {@link io.appium.java_client.pagefactory.SelendroidBy} tags. - */ -@Retention(RUNTIME) @Target({FIELD, TYPE}) -public @interface SelendroidFindBys { - /** - * It is a set of {@link io.appium.java_client.pagefactory.SelendroidBy} strategies which - * build the chain of the searching for the target element. - */ - SelendroidBy[] value(); - - /** - * @return priority of the searching. Higher number means lower priority. - */ - int priority() default 0; -} 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/TimeOutDuration.java b/src/main/java/io/appium/java_client/pagefactory/TimeOutDuration.java deleted file mode 100644 index 73f530451..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/TimeOutDuration.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.pagefactory; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import java.util.concurrent.TimeUnit; - -/** - * Represents an duration of waiting for element rendering. - */ -public class TimeOutDuration { - - private long time; - private TimeUnit unit; - - /** - * @param time The amount of time. - * @param unit The unit of time. - */ - public TimeOutDuration(long time, TimeUnit unit) { - setTime(time, unit); - } - - public long getTime() { - return time; - } - - public TimeUnit getTimeUnit() { - return unit; - } - - public void setTime(TimeUnit newTimeUnit) { - checkNotNull(newTimeUnit); - unit = newTimeUnit; - } - - public void setTime(long newTime) { - checkArgument(newTime >= 0, "Duration < 0: %d", newTime); - time = newTime; - } - - public void setTime(long newTime, TimeUnit newTimeUnit) { - setTime(newTime); - setTime(newTimeUnit); - } -} 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 6ac55e189..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,18 +16,17 @@ 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; import org.openqa.selenium.WebElement; -import org.openqa.selenium.internal.WrapsDriver; -import org.openqa.selenium.internal.WrapsElement; +import org.openqa.selenium.WrapsDriver; +import org.openqa.selenium.WrapsElement; 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 978e4fbaf..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; @@ -70,7 +70,7 @@ private By getByFromDeclaredClass(WhatIsNeeded whatIsNeeded) { } else { convenientClass = getMobileNativeWidgetClass((Class) declaredClass, field, - platform, automation); + platform); } while (result == null && !convenientClass.equals(Object.class)) { @@ -93,12 +93,12 @@ private By getByFromDeclaredClass(WhatIsNeeded whatIsNeeded) { @Override protected By buildDefaultBy() { return ofNullable(super.buildDefaultBy()) - .orElse(getByFromDeclaredClass(WhatIsNeeded.DEFAULT_OR_HTML)); + .orElseGet(() -> getByFromDeclaredClass(WhatIsNeeded.DEFAULT_OR_HTML)); } @Override protected By buildMobileNativeBy() { return ofNullable(super.buildMobileNativeBy()) - .orElse(getByFromDeclaredClass(WhatIsNeeded.MOBILE_NATIVE)); + .orElseGet(() -> getByFromDeclaredClass(WhatIsNeeded.MOBILE_NATIVE)); } private enum WhatIsNeeded { 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 37fb1c21e..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,62 +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 TimeOutDuration duration; - private WebElement cachedElement; + private final Duration duration; + private WeakReference cachedElementReference; - WidgetInterceptor(CacheableLocator locator, WebDriver driver, WebElement cachedElement, - Map> instantiationMap, - TimeOutDuration 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); @@ -81,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 ff4a9f740..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,34 +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 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 TimeOutDuration duration; - private final WebDriver driver; - private List cachedElements; + private final Duration duration; + private final WeakReference driver; + private final List> cachedElementReferences = new ArrayList<>(); - WidgetListInterceptor(CacheableLocator locator, WebDriver driver, - Map> instantiationMap, - Class declaredType, TimeOutDuration 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; @@ -51,21 +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(); - for (WebElement element : cachedElements) { - ContentType type = 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))); + ContentType type = null; + for (WebElement element : elements) { + type = ofNullable(type).orElseGet(() -> getCurrentContentType(element)); + 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 892201cc6..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsBy.java +++ /dev/null @@ -1,58 +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 an is Windows automator string. - */ - String windowsAutomation() default ""; - - /** - * It an UI automation accessibility Id which is a convenient to Windows. - */ - String accessibility() default ""; - - /** - * It is an id of the target element. - */ - String id() default ""; - - /** - * It is a className of the target element. - */ - String className() default ""; - - /** - * It is a desired element tag. - */ - String tagName() default ""; - - /** - * It is a xpath to the target element. - */ - String xpath() default ""; - - /** - * @return priority of the searching. Higher number means lower priority. - */ - 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 5b1c7cf00..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindAll.java +++ /dev/null @@ -1,46 +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. - */ - WindowsBy[] value(); - - /** - * @return priority of the searching. Higher number means lower priority. - */ - 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 667814056..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBy.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; - -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 an is Windows automator string. - */ - String windowsAutomation() default ""; - - /** - * It an UI automation accessibility Id which is a convenient to Windows. - */ - String accessibility() default ""; - - /** - * It is an id of the target element. - */ - String id() default ""; - - /** - * It is a className of the target element. - */ - String className() default ""; - - /** - * It is a desired element tag. - */ - String tagName() default ""; - - /** - * It is a xpath to the target element. - */ - String xpath() default ""; - - /** - * @return priority of the searching. Higher number means lower priority. - */ - 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 adbad0864..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindByAllSet.java +++ /dev/null @@ -1,22 +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 { - /** - * @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 b8a5bc03d..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindByChainSet.java +++ /dev/null @@ -1,22 +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 { - /** - * @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 edf7de758..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBySet.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.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 { - /** - * @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 5bd34f460..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/WindowsFindBys.java +++ /dev/null @@ -1,44 +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. - */ - WindowsBy[] value(); - - /** - * @return priority of the searching. Higher number means lower priority. - */ - 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 80bd2088b..0b04dadf2 100644 --- a/src/main/java/io/appium/java_client/pagefactory/WithTimeout.java +++ b/src/main/java/io/appium/java_client/pagefactory/WithTimeout.java @@ -16,26 +16,39 @@ package io.appium.java_client.pagefactory; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.concurrent.TimeUnit; +import java.time.Duration; +import java.time.temporal.ChronoUnit; -@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) /** - *This annotation is used when some element waits for time - *that differs from defined by default + * This annotation is used when some element waits for time + * that differs from defined by default. */ +@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface WithTimeout { /** - * Desired waiting duration + * Desired waiting duration. + * + * @return waiting duration */ long time(); /** - * Desired time unit + * Desired time unit. + * + * @return time unit. */ - TimeUnit unit(); + 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 95a6659c6..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,23 +1,23 @@ /* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* See the NOTICE file distributed with this work for additional -* information regarding copyright ownership. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT 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 io.appium.java_client.pagefactory.utils.WebDriverUnpackUtility.getCurrentContentType; - +import lombok.EqualsAndHashCode; +import org.jspecify.annotations.NonNull; import org.openqa.selenium.By; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; @@ -25,31 +25,42 @@ import java.util.List; import java.util.Map; +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; public ContentMappedBy(Map map) { this.map = map; } - @Override public WebElement findElement(SearchContext context) { - return context.findElement(map.get(getCurrentContentType(context))); + /** + * 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) { + requireNonNull(type); + currentContent = type; + return this; } - @Override public List findElements(SearchContext context) { - return context.findElements(map.get(getCurrentContentType(context))); + @Override + public WebElement findElement(SearchContext context) { + return context.findElement(map.get(currentContent)); } - @Override public String toString() { - By defaultBy = map.get(ContentType.HTML_OR_DEFAULT); - By nativeBy = map.get(ContentType.NATIVE_MOBILE_SPECIFIC); - - if (defaultBy.equals(nativeBy)) { - return defaultBy.toString(); - } + @Override + public List findElements(SearchContext context) { + return context.findElements(map.get(currentContent)); + } - return "Locator map: " + "\n" - + "- native content: \"" + nativeBy.toString() + "\" \n" - + "- html content: \"" + defaultBy.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/AnnotatedElementContainer.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/AnnotatedElementContainer.java index 6db68c8c9..8b4d4957b 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/AnnotatedElementContainer.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/AnnotatedElementContainer.java @@ -16,20 +16,16 @@ package io.appium.java_client.pagefactory.bys.builder; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + import java.lang.reflect.AnnotatedElement; /** * This is the POJO for the setting/getting of an AnnotatedElement instances. */ public class AnnotatedElementContainer { + @Getter(AccessLevel.PUBLIC) @Setter(AccessLevel.PACKAGE) private AnnotatedElement annotated; - - - public AnnotatedElement getAnnotated() { - return annotated; - } - - void setAnnotated(AnnotatedElement annotated) { - this.annotated = annotated; - } } 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 0b528d57d..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,44 +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.AutomationName.SELENDROID; -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; @@ -65,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, new Object[] {}).toString(); - if (!"".equals(strategyParameter)) { - 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; @@ -162,7 +155,7 @@ protected static By createBy(Annotation[] annotations, HowToUseSelectors howToUs /** * This method should be used for the setting up of * AnnotatedElement instances before the building of - * By-locator strategies + * By-locator strategies. * * @param annotated is an instance of class which type extends * {@link java.lang.reflect.AnnotatedElement} @@ -175,14 +168,14 @@ protected boolean isAndroid() { return ANDROID.equalsIgnoreCase(platform); } - protected boolean isSelendroidAutomation() { - return isAndroid() && SELENDROID.equalsIgnoreCase(automation); - } - 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 fa4285934..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByAll.java +++ /dev/null @@ -1,52 +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(); - } - }; - } - - /** - * @param bys is a set of {@link org.openqa.selenium.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())); - } -} \ No newline at end of file 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 f25f1d3cc..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; @@ -43,11 +46,12 @@ private static AppiumFunction getSearchingFunction(By } /** - * @param bys is a set of {@link org.openqa.selenium.By} which forms the chain of the searching. + * Finds elements that matches each of the locators in sequence. + * + * @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"); } @@ -56,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 35fbfd6e9..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,18 +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 io.appium.java_client.pagefactory.iOSFindBy; 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") { @@ -35,40 +34,34 @@ enum Strategies { String value = getValue(annotation, this); if (annotation.annotationType().equals(AndroidFindBy.class) || annotation.annotationType().equals(AndroidBy.class)) { - return MobileBy.AndroidUIAutomator(value); - } - if (annotation.annotationType().equals(iOSFindBy.class) - || annotation.annotationType().equals(iOSBy.class)) { - return MobileBy.IosUIAutomation(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") { @@ -89,21 +82,27 @@ enum Strategies { .partialLinkText(getValue(annotation, this)); } }, - BYWINDOWSAUTOMATION("windowsAutomation") { + BY_CLASS_CHAIN("iOSClassChain") { @Override By getBy(Annotation annotation) { - return MobileBy - .windowsAutomation(getValue(annotation, this)); + return AppiumBy + .iOSClassChain(getValue(annotation, this)); } }, - BY_CLASS_CHAIN("iOSClassChain") { + BY_DATA_MATCHER("androidDataMatcher") { @Override By getBy(Annotation annotation) { - return MobileBy - .iOSClassChain(getValue(annotation, this)); + return AppiumBy + .androidDataMatcher(getValue(annotation, this)); + } + }, + BY_VIEW_MATCHER("androidViewMatcher") { + @Override By getBy(Annotation annotation) { + return AppiumBy + .androidViewMatcher(getValue(annotation, this)); } }, BY_NS_PREDICATE("iOSNsPredicate") { @Override By getBy(Annotation annotation) { - return MobileBy + return AppiumBy .iOSNsPredicateString(getValue(annotation, this)); } }; @@ -115,19 +114,14 @@ 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) { try { Method m = annotation.getClass() .getMethod(strategy.valueName, AppiumByBuilder.DEFAULT_ANNOTATION_METHOD_ARGUMENTS); - return m.invoke(annotation, new Object[] {}).toString(); + return m.invoke(annotation).toString(); } catch (NoSuchMethodException | SecurityException | IllegalAccessException diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSBy.java b/src/main/java/io/appium/java_client/pagefactory/iOSBy.java index 0b7623bc6..1f79ee9f4 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSBy.java @@ -22,9 +22,11 @@ */ public @interface iOSBy { /** - * It is is iOS UIAutomation string. + * It is an iOS UIAutomation string. * Read https://developer.apple.com/library/tvos/documentation/DeveloperTools/ * Conceptual/InstrumentsUserGuide/UIAutomation.html + * + * @return an iOS UIAutomation string */ String uiAutomator() default ""; @@ -32,31 +34,43 @@ * It an UI automation accessibility Id which is a convenient to iOS. * About iOS accessibility * See UIAccessibilityIdentification + * + * @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 name of a type/class of the target element. + * + * @return a name of a type/class 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 ""; /** - * @return priority of the searching. Higher number means lower priority. + * 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/iOSFindAll.java b/src/main/java/io/appium/java_client/pagefactory/iOSFindAll.java deleted file mode 100644 index 5754583d1..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/iOSFindAll.java +++ /dev/null @@ -1,46 +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 io.appium.java_client.pagefactory.iOSBy} 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(iOSFindByAllSet.class) -public @interface iOSFindAll { - /** - * It is a set of {@link io.appium.java_client.pagefactory.iOSBy} strategies which may be - * used to find the target element. - */ - iOSBy[] value(); - - /** - * @return priority of the searching. Higher number means lower priority. - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSFindBy.java b/src/main/java/io/appium/java_client/pagefactory/iOSFindBy.java deleted file mode 100644 index 5c365fe4a..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/iOSFindBy.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.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 iOS UI selectors, accessibility, id, name, class name, tag and xpath - */ -@Retention(RUNTIME) @Target({FIELD, TYPE}) -@Repeatable(iOSFindBySet.class) -public @interface iOSFindBy { - /** - * It is is iOS UIAutomation string. - * Read https://developer.apple.com/library/tvos/documentation/DeveloperTools/ - * Conceptual/InstrumentsUserGuide/UIAutomation.html - */ - String uiAutomator() default ""; - - /** - * It an UI automation accessibility Id which is a convenient to iOS. - * About iOS accessibility - * See UIAccessibilityIdentification - */ - String accessibility() default ""; - - /** - * It is an id of the target element. - */ - String id() default ""; - - /** - * It is a name of a type/class of the target element. - */ - String className() default ""; - - /** - * It is a desired element tag. - */ - String tagName() default ""; - - /** - * It is a xpath to the target element. - */ - String xpath() default ""; - - /** - * @return priority of the searching. Higher number means lower priority. - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSFindByAllSet.java b/src/main/java/io/appium/java_client/pagefactory/iOSFindByAllSet.java deleted file mode 100644 index cbedac322..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/iOSFindByAllSet.java +++ /dev/null @@ -1,22 +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 io.appium.java_client.pagefactory.iOSFindAll} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface iOSFindByAllSet { - /** - * @return an array of {@link io.appium.java_client.pagefactory.iOSFindAll} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - iOSFindAll[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSFindByChainSet.java b/src/main/java/io/appium/java_client/pagefactory/iOSFindByChainSet.java deleted file mode 100644 index 5b6865dd1..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/iOSFindByChainSet.java +++ /dev/null @@ -1,22 +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 io.appium.java_client.pagefactory.iOSFindBys} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface iOSFindByChainSet { - /** - * @return an array of {@link io.appium.java_client.pagefactory.iOSFindBys} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - iOSFindBys[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSFindBySet.java b/src/main/java/io/appium/java_client/pagefactory/iOSFindBySet.java deleted file mode 100644 index 2f876b878..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/iOSFindBySet.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.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 io.appium.java_client.pagefactory.iOSFindBy} - */ -@Target(value = {TYPE, FIELD}) -@Retention(value = RUNTIME) -public @interface iOSFindBySet { - /** - * @return an array of {@link io.appium.java_client.pagefactory.iOSFindBy} which builds a sequence of - * the chained searching for elements or a set of possible locators - */ - iOSFindBy[] value(); -} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSFindBys.java b/src/main/java/io/appium/java_client/pagefactory/iOSFindBys.java deleted file mode 100644 index 9d2f0a4c3..000000000 --- a/src/main/java/io/appium/java_client/pagefactory/iOSFindBys.java +++ /dev/null @@ -1,44 +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 io.appium.java_client.pagefactory.iOSBy} tags. - */ -@Retention(RUNTIME) @Target({FIELD, TYPE}) -@Repeatable(iOSFindByChainSet.class) -public @interface iOSFindBys { - /** - * It is a set of {@link io.appium.java_client.pagefactory.iOSBy} strategies which build - * the chain of the searching for the target element. - */ - iOSBy[] value(); - - /** - * @return priority of the searching. Higher number means lower priority. - */ - int priority() default 0; -} diff --git a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITBy.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITBy.java index f3a1e8100..c59a1559d 100644 --- a/src/main/java/io/appium/java_client/pagefactory/iOSXCUITBy.java +++ b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITBy.java @@ -24,14 +24,18 @@ /** * The Class Chain locator is similar to xpath, but it's faster and can only * search direct children elements. See the - * + * * documentation for more details. + * + * @return iOS class chain */ String iOSClassChain() default ""; /** * The NSPredicate class is used to define logical conditions used to constrain * a search either for a fetch or for in-memory filtering. + * + * @return iOS NSPredicate */ String iOSNsPredicate() default ""; @@ -39,31 +43,43 @@ * It an UI automation accessibility Id which is a convenient to iOS. * About iOS accessibility * See UIAccessibilityIdentification + * + * @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 name of a type/class of the target element. + * + * @return a name of a type/class 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 ""; /** - * @return priority of the searching. Higher number means lower priority. + * 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/iOSXCUITFindAll.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindAll.java index 95af79c36..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,17 +16,17 @@ 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 + * of {@link io.appium.java_client.pagefactory.iOSXCUITBy} tags. * It will then search for all elements that match any criteria. Note that elements * are not guaranteed to be in document order. */ @@ -34,13 +34,16 @@ @Repeatable(iOSXCUITFindByAllSet.class) public @interface iOSXCUITFindAll { /** - * It is a set of {@link io.appium.java_client.pagefactory.iOSXCUITBy} strategies which may be - * used to find the target element. + * It is a set of {@link iOSXCUITBy} strategies which may be used to find the target element. + * + * @return a collection of strategies which may be used to find the target element */ iOSXCUITBy[] value(); /** - * @return priority of the searching. Higher number means lower priority. + * 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/iOSXCUITFindBy.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindBy.java index 63caf7cf4..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 { @@ -31,14 +31,18 @@ /** * The Class Chain locator is similar to xpath, but it's faster and can only * search direct children elements. See the - * + * * documentation for more details. + * + * @return iOS class chain */ String iOSClassChain() default ""; /** * The NSPredicate class is used to define logical conditions used to constrain * a search either for a fetch or for in-memory filtering. + * + * @return iOS NSPredicate */ String iOSNsPredicate() default ""; @@ -46,31 +50,43 @@ * It an UI automation accessibility Id which is a convenient to iOS. * About iOS accessibility * See UIAccessibilityIdentification + * + * @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 name of a type/class of the target element. + * + * @return a name of a type/class 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 ""; /** - * @return priority of the searching. Higher number means lower priority. + * 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/iOSXCUITFindByAllSet.java b/src/main/java/io/appium/java_client/pagefactory/iOSXCUITFindByAllSet.java index a0adcf8f2..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} @@ -15,8 +15,10 @@ @Retention(value = RUNTIME) public @interface iOSXCUITFindByAllSet { /** + * An array of which builds a sequence of the chained searching for elements or a set of possible locators. + * * @return an array of {@link io.appium.java_client.pagefactory.iOSXCUITFindAll} which builds a sequence of - * the chained searching for elements or a set of possible locators + * the chained searching for elements or a set of possible locators */ iOSXCUITFindAll[] value(); } 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 056931c8a..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} @@ -15,8 +15,10 @@ @Retention(value = RUNTIME) public @interface iOSXCUITFindByChainSet { /** + * An array of which builds a sequence of the chained searching for elements or a set of possible locators. + * * @return an array of {@link io.appium.java_client.pagefactory.iOSXCUITFindBys} which builds a sequence of - * the chained searching for elements or a set of possible locators + * the chained searching for elements or a set of possible locators */ iOSXCUITFindBys[] value(); } 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 2d3b0c02b..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,18 +16,20 @@ 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 { /** + * An array of which builds a sequence of the chained searching for elements or a set of possible locators. + * * @return an array of {@link iOSXCUITFindBy} which builds a sequence of - * the chained searching for elements or a set of possible locators + * the chained searching for elements or a set of possible locators */ iOSXCUITFindBy[] value(); } 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 ad241f445..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. @@ -32,13 +32,16 @@ @Repeatable(iOSXCUITFindByChainSet.class) public @interface iOSXCUITFindBys { /** - * It is a set of {@link io.appium.java_client.pagefactory.iOSXCUITBy} strategies which build - * the chain of the searching for the target element. + * It is a set of {@link iOSXCUITBy} 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 */ iOSXCUITBy[] value(); /** - * @return priority of the searching. Higher number means lower priority. + * 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/interceptors/InterceptorOfAListOfElements.java b/src/main/java/io/appium/java_client/pagefactory/interceptors/InterceptorOfAListOfElements.java index 9130c3cde..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,38 +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; - - /** - * Look at - * {@link net.sf.cglib.proxy.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); + protected abstract Object getObject( + List elements, Method method, Object[] args + ) throws Throwable; + + @Override + public Object call(Object obj, Method method, Object[] args, Callable original) throws Throwable { + if (locator == null || Object.class == method.getDeclaringClass()) { + return original.call(); } - ArrayList realElements = new ArrayList(); - realElements.addAll(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 6322a3c04..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,49 +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.internal.WrapsDriver; +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; + + private static boolean areElementsEqual(Object we1, Object we2) { + if (!(we1 instanceof RemoteWebElement) || !(we2 instanceof RemoteWebElement)) { + return false; + } - /** - * Look at - * {@link net.sf.cglib.proxy.MethodInterceptor#intercept(Object, Method, Object[], MethodProxy)} - */ - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) - throws Throwable { + 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/locator/CacheableElementLocatorFactory.java b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableElementLocatorFactory.java index f360a983f..3fd540bbc 100644 --- a/src/main/java/io/appium/java_client/pagefactory/locator/CacheableElementLocatorFactory.java +++ b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableElementLocatorFactory.java @@ -23,7 +23,7 @@ public interface CacheableElementLocatorFactory extends ElementLocatorFactory { - public CacheableLocator createLocator(Field field); + CacheableLocator createLocator(Field field); - public CacheableLocator createLocator(AnnotatedElement annotatedElement); + CacheableLocator createLocator(AnnotatedElement annotatedElement); } diff --git a/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java index c824d66f2..963a8b63b 100644 --- a/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java +++ b/src/main/java/io/appium/java_client/pagefactory/locator/CacheableLocator.java @@ -19,5 +19,5 @@ import org.openqa.selenium.support.pagefactory.ElementLocator; public interface CacheableLocator extends ElementLocator { - public boolean isLookUpCached(); + boolean isLookUpCached(); } 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 a8b11c707..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 io.appium.java_client.proxy.MethodCallListener; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.MethodInterceptor; +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); + } + + @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); } - public static T getEnhancedProxy(Class requiredClazz, MethodInterceptor interceptor) { - return getEnhancedProxy(requiredClazz, new Class[] {}, new Object[] {}, interceptor); + 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 279ba4f05..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,100 +16,103 @@ 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 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.internal.WrapsDriver; -import org.openqa.selenium.internal.WrapsElement; +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 org.openqa.selenium.WebDriver} from the given - * {@link org.openqa.selenium.SearchContext}. - * @param searchContext is an instance of {@link org.openqa.selenium.SearchContext} - * It may be the instance of {@link org.openqa.selenium.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 org.openqa.selenium.internal.WrapsDriver} or - * {@link org.openqa.selenium.internal.WrapsElement} - * @return the instance of {@link org.openqa.selenium.WebDriver}. - * Note: if the given {@link org.openqa.selenium.SearchContext} is not - * {@link org.openqa.selenium.WebDriver} and it doesn't implement - * {@link org.openqa.selenium.internal.WrapsDriver} or - * {@link org.openqa.selenium.internal.WrapsElement} then this method returns - * null. + * 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} + * @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(); } /** - * @param context is an instance of {@link org.openqa.selenium.SearchContext} - * It may be the instance of {@link org.openqa.selenium.WebDriver} - * or {@link org.openqa.selenium.WebElement} or some other user's - * extension/implementation. + * 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); + } + + /** + * Detects content type by the provided search {@code context}. + * + * @param context 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 org.openqa.selenium.ContextAware} or - * {@link org.openqa.selenium.internal.WrapsDriver} + * 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 io.appium.java_client.pagefactory.bys.ContentType#NATIVE_MOBILE_SPECIFIC}. - * {@link io.appium.java_client.pagefactory.bys.ContentType#HTML_OR_DEFAULT} will be returned - * if the current context is WEB_VIEW. - * {@link io.appium.java_client.pagefactory.bys.ContentType#HTML_OR_DEFAULT} also will be - * returned if the given {@link org.openqa.selenium.SearchContext} - * instance doesn't implement - * {@link org.openqa.selenium.ContextAware} and {@link org.openqa.selenium.internal.WrapsDriver} + * 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 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.class.cast(driver); - - if (hasSessionDetails.isBrowser()) { - return HTML_OR_DEFAULT; - } - return NATIVE_MOBILE_SPECIFIC; - } - - if (!ContextAware.class.isAssignableFrom(driver.getClass())) { //it is desktop browser - return HTML_OR_DEFAULT; - } + var browserCheckHolder = unpackObjectFromSearchContext(context, HasBrowserCheck.class); + if (browserCheckHolder.filter(hbc -> !hbc.isBrowser()).isPresent()) { + return NATIVE_MOBILE_SPECIFIC; + } - ContextAware contextAware = ContextAware.class.cast(driver); - String currentContext = contextAware.getContext(); - if (currentContext.contains(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 a9474f371..000000000 --- a/src/main/java/io/appium/java_client/remote/AndroidMobileCapabilityType.java +++ /dev/null @@ -1,259 +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/1.5/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 . (e.g., .MainActivity instead of MainActivity). - */ - String APP_ACTIVITY = "appActivity"; - - /** - * Java package of the Android app you want to run. - */ - String APP_PACKAGE = "appPackage"; - - /** - * Activity name for the Android activity you want to wait for. - */ - String APP_WAIT_ACTIVITY = "appWaitActivity"; - - /** - * Java package of the Android app you want to wait for. - */ - String APP_WAIT_PACKAGE = "appWaitPackage"; - - /** - * Timeout in milliseconds used to wait for the appWaitActivity to launch (default 20000). - */ - String APP_WAIT_DURATION = "appWaitDuration"; - - /** - * Timeout in seconds while waiting for device to become ready. - */ - String DEVICE_READY_TIMEOUT = "deviceReadyTimeout"; - - /** - * Fully qualified instrumentation class. Passed to -w in adb shell - * am instrument -e coverage true -w. - */ - String ANDROID_COVERAGE = "androidCoverage"; - - /** - * (Chrome and webview only) Enable Chromedriver's performance logging (default false). - */ - 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`. - */ - String ANDROID_INSTALL_TIMEOUT = "androidInstallTimeout"; - - /** - * 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). - */ - String AVD_LAUNCH_TIMEOUT = "avdLaunchTimeout"; - - /** - * How long to wait in milliseconds for an avd to finish its - * boot animations (default 120000). - */ - 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"; - - /** - * Amount of time to wait for Webview context to become active, in ms. Defaults to 2000. - */ - 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: - * http://developer.android.com/reference/android/content/Intent.html - */ - 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 - */ - String DONT_STOP_APP_ON_RESET = "dontStopAppOnReset"; - - /** - * Enable Unicode input, default false. - */ - 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. - */ - 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. - */ - String DISABLE_ANDROID_WATCHERS = "disableAndroidWatchers"; - - /** - * Allows passing chromeOptions capability for ChromeDriver. - * For more information see chromeOptions: - * https://sites.google.com/a/chromium.org/chromedriver/capabilities - */ - 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. - */ - 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. - */ - String ANDROID_SCREENSHOT_PATH = "androidScreenshotPath"; - - /** - * Have Appium automatically determine which permissions your app requires and - * grant them to the app on install. Defaults to false. - */ - String AUTO_GRANT_PERMISSIONS = "autoGrantPermissions"; - - /** - * Add androidNaturalOrientation capability to allow for correct handling of - * orientation on landscape-oriented devices. - */ - String ANDROID_NATURAL_ORIENTATION = "androidNaturalOrientation"; - - String SELENDROID_PORT = "selendroidPort"; - - /** - * The port number, which being used by UIAutomator2. - */ - String SYSTEM_PORT = "systemPort"; -} 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 b62e99e01..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,65 +16,211 @@ package io.appium.java_client.remote; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Throwables.throwIfUnchecked; -import static java.util.Optional.ofNullable; - -import com.google.common.base.Supplier; import com.google.common.base.Throwables; - +import io.appium.java_client.AppiumClientConfig; +import io.appium.java_client.internal.ReflectionHelpers; +import lombok.Getter; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.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; import org.openqa.selenium.remote.HttpCommandExecutor; +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.internal.ApacheHttpClient; +import org.openqa.selenium.remote.http.HttpClient.Factory; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; import org.openqa.selenium.remote.service.DriverService; import java.io.IOException; import java.net.ConnectException; +import java.net.MalformedURLException; import java.net.URL; import java.util.Map; import java.util.Optional; +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 { private final Optional serviceOptional; + @Getter + private final AppiumClientConfig appiumClientConfig; - private AppiumCommandExecutor(Map additionalCommands, DriverService service, - URL addressOfRemoteServer, - HttpClient.Factory httpClientFactory) { + /** + * 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, new ApacheHttpClient.Factory()); + 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, new ApacheHttpClient.Factory()); + DriverService service, AppiumClientConfig appiumClientConfig) { + this(additionalCommands, service, HttpClient.Factory.createDefault(), appiumClientConfig); + } + + @Deprecated + @SuppressWarnings("SameParameterValue") + protected void setPrivateFieldValue( + Class cls, String fieldName, Object newValue) { + ReflectionHelpers.setPrivateFieldValue(cls, this, fieldName, newValue); + } + + public Map getAdditionalCommands() { + return additionalCommands; + } + + public Factory getHttpClientFactory() { + return httpClientFactory; + } + + @Nullable + protected CommandCodec getCommandCodec() { + return this.commandCodec; + } + + public void setCommandCodec(CommandCodec newCodec) { + this.commandCodec = newCodec; + } + + public void setResponseCodec(ResponseCodec codec) { + this.responseCodec = codec; + } + + protected HttpClient getClient() { + return this.client; + } + + /** + * Override the http client in the HttpCommandExecutor class with a new http client instance with the given URL. + * It uses the same http client factory and client config for the new http client instance + * if the constructor got them. + * @param serverUrl A url to override. + */ + protected void overrideServerUrl(URL serverUrl) { + HttpClient newClient = getHttpClientFactory().createClient(appiumClientConfig.baseUrl(serverUrl)); + setPrivateFieldValue(HttpCommandExecutor.class, "client", newClient); + } + + private Response createSession(Command command) throws IOException { + if (getCommandCodec() != null) { + throw new SessionNotCreatedException("Session already exists"); + } + + var result = new ProtocolHandshake().createSession(getClient(), command); + Dialect dialect = result.getDialect(); + 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()); + 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 public Response execute(Command command) throws WebDriverException { + @Override + public Response execute(Command command) throws WebDriverException { if (DriverCommand.NEW_SESSION.equals(command.getName())) { serviceOptional.ifPresent(driverService -> { try { @@ -86,7 +232,7 @@ public AppiumCommandExecutor(Map additionalCommands, } try { - return 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 @@ -97,8 +243,7 @@ public AppiumCommandExecutor(Map additionalCommands, } 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); @@ -108,4 +253,4 @@ public AppiumCommandExecutor(Map additionalCommands, } } } -} \ No newline at end of file +} 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 new file mode 100644 index 000000000..1fc6943a3 --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/AppiumW3CHttpCommandCodec.java @@ -0,0 +1,76 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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_ELEMENT; +import static org.openqa.selenium.remote.DriverCommand.SET_TIMEOUT; +import static org.openqa.selenium.remote.DriverCommand.SUBMIT_ELEMENT; + +public class AppiumW3CHttpCommandCodec extends W3CHttpCommandCodec { + /** + * This class overrides the built-in Selenium W3C commands codec, + * since the latter hardcodes many commands in Javascript, + * which does not work with Appium. + * Check https://www.w3.org/TR/webdriver/ to see all available W3C + * endpoints. + */ + 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")); + } + + @Override + public void alias(String commandName, String isAnAliasFor) { + // This blocks parent constructor from undesirable aliases assigning + switch (commandName) { + case GET_ELEMENT_ATTRIBUTE: + case GET_ELEMENT_LOCATION: + case GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW: + case GET_ELEMENT_SIZE: + case IS_ELEMENT_DISPLAYED: + case SUBMIT_ELEMENT: + case GET_PAGE_SOURCE: + return; + default: + super.alias(commandName, isAnAliasFor); + break; + } + } + + @Override + protected Map amendParameters(String name, Map parameters) { + // This blocks parent constructor from undesirable parameters amending + switch (name) { + case SEND_KEYS_TO_ELEMENT: + case SET_TIMEOUT: + return super.amendParameters(name, parameters); + default: + return 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 1c7cfa8a4..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,11 +16,28 @@ package io.appium.java_client.remote; - public interface AutomationName { - String APPIUM = "Appium"; - 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"; + // 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 2ddae1694..000000000 --- a/src/main/java/io/appium/java_client/remote/IOSMobileCapabilityType.java +++ /dev/null @@ -1,361 +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/1.5/docs/en/writing-running-appium/caps.md#ios-only - */ -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"; - - /** - * (Sim-only) 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"; - - /** - * Capability to pre-authorize a specific SSL cert in the iOS trust store. - */ - 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"; - - /** - * The capability to direct Appium to set the simulator scale. - * The XCUITest specific capability. - */ - 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 - */ - 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"; - - /** - * Forces uninstall of any existing WebDriverAgent app on device. - * This can provide stability in some situations. Defaults to false. - */ - String USE_NEW_WDA = "useNewWDA"; - - /** - * Time, in ms, to wait for WebDriverAgewnt to be pingable. Defaults to 60000ms. - */ - String WDA_LAUNCH_TIMEOUT = "wdaLaunchTimeout"; - - /** - * Timeout, in ms, for waiting for a resonse 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. - */ - 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 - * duratiuon of test session initialization. - * Defaults to true. - */ - String RESET_ON_SESSION_START_ONLY = "resetOnSessionStartOnly"; - - /** - * Custom timeout(s) in milliseconds for WDA backend commands execution. - */ - 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 40077436f..000000000 --- a/src/main/java/io/appium/java_client/remote/MobileCapabilityType.java +++ /dev/null @@ -1,123 +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/1.5/docs/en/ - * writing-running-appium/caps.md#appium-server-capabilities - */ -public interface MobileCapabilityType extends CapabilityType { - - /** - * Which automation engine to use. - */ - String AUTOMATION_NAME = "automationName"; - - /** - * Which mobile OS platform to use. - */ - String PLATFORM_NAME = "platformName"; - - /** - * 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 an .ipa or .apk file, - * or a .zip containing one of these. 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 appPackage and appActivity capabilities (see below). - * Incompatible with browserName. - */ - String APP = "app"; - - /** - * Name of mobile web browser to automate. - * Should be an empty string if automating an app instead. - */ - String BROWSER_NAME = "browserName"; - - /** - * Unique device identifier of the connected physical device. - */ - String UDID = "udid"; - - /** - * Sauce-specific. - */ - String APPIUM_VERSION = "appiumVersion"; - - /** - * (Sim/Emu-only) Language to set for the simulator / emulator. - */ - String LANGUAGE = "language"; - - /** - * (Sim/Emu-only) Locale to set for the simulator / emulator. - */ - 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. Default false. - */ - String NO_RESET = "noReset"; - - /** - * (iOS) Delete the entire simulator folder. - * (Android) Reset app state by uninstalling app instead of clearing app data. - * On Android, this will also remove the app after the session is complete. Default false. - */ - 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 false. - */ - String EVENT_TIMINGS = "eventTimings"; -} 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 c14a1b4e6..97e8deaf3 100644 --- a/src/main/java/io/appium/java_client/remote/MobilePlatform.java +++ b/src/main/java/io/appium/java_client/remote/MobilePlatform.java @@ -22,4 +22,6 @@ public interface MobilePlatform { String IOS = "iOS"; String FIREFOX_OS = "FirefoxOS"; String WINDOWS = "Windows"; + String TVOS = "tvOS"; + String MAC = "Mac"; } 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/test/java/io/appium/java_client/ios/BaseIOSTest.java b/src/main/java/io/appium/java_client/remote/options/CanSetCapability.java similarity index 59% rename from src/test/java/io/appium/java_client/ios/BaseIOSTest.java rename to src/main/java/io/appium/java_client/remote/options/CanSetCapability.java index 63df68d2f..c992569c6 100644 --- a/src/test/java/io/appium/java_client/ios/BaseIOSTest.java +++ b/src/main/java/io/appium/java_client/remote/options/CanSetCapability.java @@ -14,25 +14,21 @@ * limitations under the License. */ -package io.appium.java_client.ios; +package io.appium.java_client.remote.options; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import org.junit.AfterClass; - -public class BaseIOSTest { - - protected static AppiumDriverLocalService service; - protected static IOSDriver driver; +public interface CanSetCapability> { + void setCapability(String key, Object value); /** - * finishing. + * Set a custom option. + * + * @param optionName Option name. + * @param value Option value. + * @return self instance for chaining. */ - @AfterClass public static void afterClass() { - if (driver != null) { - driver.quit(); - } - if (service != null) { - service.stop(); - } + 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/events/api/general/ListensToException.java b/src/main/java/io/appium/java_client/remote/options/SupportsBrowserNameOption.java similarity index 57% 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/SupportsBrowserNameOption.java index 89733e693..b0506feda 100644 --- a/src/main/java/io/appium/java_client/events/api/general/ListensToException.java +++ b/src/main/java/io/appium/java_client/remote/options/SupportsBrowserNameOption.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; +import org.openqa.selenium.Capabilities; + +public interface SupportsBrowserNameOption> extends + Capabilities, CanSetCapability { + String BROWSER_NAME_OPTION = "browserName"; -public interface ListensToException extends Listener { /** - * Called whenever an exception would be thrown. - * @param throwable the exception that will be thrown - * @param driver WebDriver + * Set the browser name to use. + * + * @param browserName One of supported browser names. + * @return self instance for chaining. */ - void onException(Throwable throwable, WebDriver driver); + 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/remote/options/SupportsPostrunOption.java b/src/main/java/io/appium/java_client/remote/options/SupportsPostrunOption.java new file mode 100644 index 000000000..e055cb69f --- /dev/null +++ b/src/main/java/io/appium/java_client/remote/options/SupportsPostrunOption.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 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/MobileSelector.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/MobileSelector.java rename to src/main/java/io/appium/java_client/remote/options/SystemScript.java index d3afac5bd..901d8e220 100644 --- a/src/main/java/io/appium/java_client/MobileSelector.java +++ b/src/main/java/io/appium/java_client/remote/options/SystemScript.java @@ -14,23 +14,32 @@ * limitations under the License. */ -package io.appium.java_client; +package io.appium.java_client.remote.options; -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"); +import java.util.Map; +import java.util.Optional; - private final String selector; +public abstract class SystemScript> extends BaseMapOptionData { + public SystemScript() { + } + + public SystemScript(Map options) { + super(options); + } + + public T withScript(String script) { + return assignOptionValue("script", script); + } + + public Optional getScript() { + return getOptionValue("script"); + } - MobileSelector(String selector) { - this.selector = selector; + public T withCommand(String command) { + return assignOptionValue("command", command); } - @Override public String toString() { - return selector; + 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 new file mode 100644 index 000000000..fd75dc2d6 --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/BaseScreenRecordingOptions.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.screenrecording; + +import java.util.Map; + +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; + +public abstract class BaseScreenRecordingOptions> { + private ScreenRecordingUploadOptions uploadOptions; + + /** + * Upload options set for the recorded screen capture. + * + * @param uploadOptions see the documentation on {@link ScreenRecordingUploadOptions} + * for more details. + * @return self instance for chaining. + */ + protected T withUploadOptions(ScreenRecordingUploadOptions uploadOptions) { + this.uploadOptions = requireNonNull(uploadOptions); + //noinspection unchecked + return (T) this; + } + + /** + * Builds a map, which is ready to be passed to the subordinated + * Appium API. + * + * @return arguments mapping. + */ + public Map 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 new file mode 100644 index 000000000..55716b622 --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/BaseStartScreenRecordingOptions.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.screenrecording; + +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; + private Duration timeLimit; + + /** + * The maximum recording time. + * + * @param timeLimit The actual time limit of the recorded video. + * @return self instance for chaining. + */ + public T withTimeLimit(Duration timeLimit) { + this.timeLimit = requireNonNull(timeLimit); + //noinspection unchecked + return (T) this; + } + + /** + * Whether to ignore the result of previous capture and start a new recording + * immediately. + * + * @return self instance for chaining. + */ + public T enableForcedRestart() { + this.forceRestart = true; + //noinspection unchecked + return (T) this; + } + + /** + * Whether to return silently if there is an active video recording. + * + * @return self instance for chaining. + */ + public T disableForcedRestart() { + this.forceRestart = false; + //noinspection unchecked + return (T) this; + } + + @Override + public Map 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/BaseStopScreenRecordingOptions.java b/src/main/java/io/appium/java_client/screenrecording/BaseStopScreenRecordingOptions.java new file mode 100644 index 000000000..7920b5c76 --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/BaseStopScreenRecordingOptions.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.screenrecording; + +public abstract class BaseStopScreenRecordingOptions> + extends BaseScreenRecordingOptions> { + + /** + * The remotePath upload option is the path to the remote location, + * where the resulting video should be uploaded. + * The following protocols are supported: http/https (multipart), ftp. + * Missing value (the default setting) means the content of resulting + * file should be encoded as Base64 and passed as the endpoint response value, but + * an exception will be thrown if the generated media file is too big to + * fit into the available process memory. + * + * @param uploadOptions see the documentation on {@link ScreenRecordingUploadOptions} + * for more details. + * @return self instance for chaining. + */ + @Override + public T withUploadOptions(ScreenRecordingUploadOptions uploadOptions) { + //noinspection unchecked + return (T) super.withUploadOptions(uploadOptions); + } +} diff --git a/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java b/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.java new file mode 100644 index 000000000..9743edb0a --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/CanRecordScreen.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.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; + +public interface CanRecordScreen extends ExecutesMethod { + + /** + * Start asynchronous screen recording process. + * + * @param The platform-specific {@link BaseStartScreenRecordingOptions} + * @param options see the documentation on the {@link BaseStartScreenRecordingOptions} + * descendant for the particular platform. + * @return `not used`. + */ + @SuppressWarnings("rawtypes") + default String startRecordingScreen(T options) { + return CommandExecutionHelper.execute(this, startRecordingScreenCommand(options)); + } + + /** + * Start asynchronous screen recording process with default options. + * + * @return `not used`. + */ + default String startRecordingScreen() { + return this.execute(START_RECORDING_SCREEN).getValue().toString(); + } + + /** + * Gather the output from the previously started screen recording to a media file. + * + * @param The platform-specific {@link BaseStopScreenRecordingOptions} + * @param options see the documentation on the {@link BaseStopScreenRecordingOptions} + * descendant for the particular platform. + * @return Base-64 encoded content of the recorded media file or an empty string + * if the file has been successfully uploaded to a remote location (depends on the actual options). + */ + @SuppressWarnings("rawtypes") + default String stopRecordingScreen(T options) { + return CommandExecutionHelper.execute(this, stopRecordingScreenCommand(options)); + } + + /** + * Gather the output from the previously started screen recording to a media file + * with default options. + * + * @return Base-64 encoded content of the recorded media file. + */ + default String stopRecordingScreen() { + return this.execute(STOP_RECORDING_SCREEN).getValue().toString(); + } +} diff --git a/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java b/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java new file mode 100644 index 000000000..e018b47ea --- /dev/null +++ b/src/main/java/io/appium/java_client/screenrecording/ScreenRecordingUploadOptions.java @@ -0,0 +1,136 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.screenrecording; + +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; + private String pass; + private String method; + private String fileFieldName; + private Map headers; + private Map formFields; + + public static ScreenRecordingUploadOptions uploadOptions() { + return new ScreenRecordingUploadOptions(); + } + + /** + * The path to the remote location, where the resulting video should be uploaded. + * + * @param remotePath The path to a writable remote location. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withRemotePath(String remotePath) { + this.remotePath = requireNonNull(remotePath); + return this; + } + + /** + * Sets the credentials for remote ftp/http authentication (if needed). + * This option only has an effect if remotePath is provided. + * + * @param user The name of the user for the remote authentication. + * @param pass The password for the remote authentication. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withAuthCredentials(String user, String pass) { + this.user = requireNonNull(user); + this.pass = requireNonNull(pass); + return this; + } + + public enum RequestMethod { + POST, PUT + } + + /** + * Sets the method name for http(s) upload. PUT is used by default. + * This option only has an effect if remotePath is provided. + * + * @param method The HTTP method name. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withHttpMethod(RequestMethod method) { + this.method = requireNonNull(method).name(); + return this; + } + + /** + * Sets the form field name containing the binary payload in multipart/form-data + * requests. + * + * @since Appium 1.18.0 + * @param fileFieldName The name of the form field containing the binary payload. + * "file" by default. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withFileFieldName(String fileFieldName) { + this.fileFieldName = requireNonNull(fileFieldName); + return this; + } + + /** + * Sets additional form fields in multipart/form-data requests. + * + * @since Appium 1.18.0 + * @param formFields form fields mapping. If any entry has the same key as + * `fileFieldName` then it is going to be ignored. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withFormFields(Map formFields) { + this.formFields = requireNonNull(formFields); + return this; + } + + /** + * Sets additional headers in multipart/form-data requests. + * + * @since Appium 1.18.0 + * @param headers headers mapping. + * @return self instance for chaining. + */ + public ScreenRecordingUploadOptions withHeaders(Map headers) { + this.headers = requireNonNull(headers); + return this; + } + + /** + * Builds a map, which is ready to be passed to the subordinated + * Appium API. + * + * @return arguments mapping. + */ + public Map 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/CommandEvent.java b/src/main/java/io/appium/java_client/serverevents/CommandEvent.java new file mode 100644 index 000000000..960b4fddb --- /dev/null +++ b/src/main/java/io/appium/java_client/serverevents/CommandEvent.java @@ -0,0 +1,10 @@ +package io.appium.java_client.serverevents; + +import lombok.Data; + +@Data +public class CommandEvent { + public final String name; + public final long startTimestamp; + public final long endTimestamp; +} diff --git a/src/main/java/io/appium/java_client/serverevents/CustomEvent.java b/src/main/java/io/appium/java_client/serverevents/CustomEvent.java new file mode 100644 index 000000000..66bab4cb5 --- /dev/null +++ b/src/main/java/io/appium/java_client/serverevents/CustomEvent.java @@ -0,0 +1,9 @@ +package io.appium.java_client.serverevents; + +import lombok.Data; + +@Data +public class CustomEvent { + private String vendor; + private String eventName; +} \ No newline at end of file diff --git a/src/main/java/io/appium/java_client/serverevents/ServerEvents.java b/src/main/java/io/appium/java_client/serverevents/ServerEvents.java new file mode 100644 index 000000000..624dd1707 --- /dev/null +++ b/src/main/java/io/appium/java_client/serverevents/ServerEvents.java @@ -0,0 +1,20 @@ +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; + +@Data +public class ServerEvents { + + public final List commands; + public final List events; + public final String jsonData; + + 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 new file mode 100644 index 000000000..999ecbd39 --- /dev/null +++ b/src/main/java/io/appium/java_client/serverevents/TimedEvent.java @@ -0,0 +1,11 @@ +package io.appium.java_client.serverevents; + +import lombok.Data; + +import java.util.List; + +@Data +public class TimedEvent { + public final String name; + public final List occurrences; +} 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 16ce2f3cd..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,56 +16,72 @@ package io.appium.java_client.service.local; -import static com.google.common.base.Preconditions.checkNotNull; - -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.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 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 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 int nodeJSPort; - private final ImmutableList nodeJSArgs; - private final ImmutableMap nodeJSEnvironment; - private final String ipAddress; - 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; + @Getter + private String basePath; + private ExternalProcess process = null; - - private CommandLine process = null; - - AppiumDriverLocalService(String ipAddress, File nodeJSExec, int nodeJSPort, - ImmutableList nodeJSArgs, ImmutableMap nodeJSEnvironment, - long startupTimeout, TimeUnit timeUnit) throws IOException { - super(nodeJSExec, nodeJSPort, nodeJSArgs, nodeJSEnvironment); - this.ipAddress = ipAddress; + 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.nodeJSPort = nodeJSPort; this.nodeJSArgs = nodeJSArgs; this.nodeJSEnvironment = nodeJSEnvironment; this.startupTimeout = startupTimeout; - this.timeUnit = timeUnit; - this.url = new URL(String.format(URL_MASK, this.ipAddress, this.nodeJSPort)); + this.url = new URL(String.format(URL_MASK, ipAddress, nodeJSPort)); } public static AppiumDriverLocalService buildDefaultService() { @@ -76,50 +92,78 @@ 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. + * * @return The base URL for the managed appium server. */ - @Override public URL getUrl() { - return url; + @Override + public URL getUrl() { + return basePath == null ? url : addSuffix(url, basePath); } - @Override public boolean isRunning() { + @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 { - URL status = new URL(url.toString() + "/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 + * 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 { @@ -128,38 +172,83 @@ 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. * * @see #start() */ - @Override public void stop() { + @Override + public void stop() { lock.lock(); try { if (process != null) { @@ -171,44 +260,193 @@ public void start() throws AppiumServerHasNotBeenStartedLocallyException { } } + /** + * Destroys the service. + * This method waits up to `DESTROY_TIMEOUT` seconds for the Appium service + * to exit gracefully. + */ private void destroyProcess() { - if (process.isRunning()) { - process.destroy(); + if (process == null || !process.isAlive()) { + return; } + process.shutdown(DESTROY_TIMEOUT); } /** - * @return String logs if the server has been run. - * null is returned otherwise. + * Logs as string. + * + * @return String logs if the server has been run. Null is returned otherwise. */ + @Nullable public String getStdOut() { - if (process != null) { - return process.getStdOut(); - } - - return null; + return ofNullable(process).map(ExternalProcess::getOutput).orElse(null); } /** - * Adds other output stream which should accept server output data - * @param outputStream is an instance of {@link java.io.OutputStream} + * Adds other output stream which should accept server output data. + * + * @param outputStream is an instance of {@link OutputStream} * 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); } /** - * Adds other output streams which should accept server output data - * @param outputStreams is a list of additional {@link java.io.OutputStream} + * Adds other output streams which should accept server output data. + * + * @param outputStreams is a list of additional {@link 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. + * + * @return true if at least one output stream has been cleared + */ + public boolean clearOutPutStreams() { + return stream.clear(); + } + + /** + * Enables server output data logging through + * SLF4J loggers. This allow server output + * data to be configured with your preferred logging frameworks (e.g. + * java.util.logging, logback, log4j). + * + *

NOTE1: You might want to call method {@link #clearOutPutStreams()} before + * calling this method.
+ * NOTE2: it is required that {@code --log-timestamp} server flag is + * {@code false}. + * + *

By default log messages are: + *

    + *
  • logged at {@code INFO} level, unless log message is pre-fixed by + * {@code [debug]} then logged at {@code DEBUG} level.
  • + *
  • logged by a SLF4J logger instance with + * a name corresponding to the appium sub module as prefixed in log message + * (logger name is transformed to lower case, no spaces and prefixed with + * "appium.service.").
  • + *
+ * Example log-message: "[ADB] Cannot read version codes of " is logged by + * logger: {@code appium.service.adb} at level {@code INFO}. + *
+ * Example log-message: "[debug] [XCUITest] Xcode version set to 'x.y.z' " + * is logged by logger {@code appium.service.xcuitest} at level + * {@code DEBUG}. + *
+ * + * @see #addSlf4jLogMessageConsumer(BiConsumer) + */ + public void enableDefaultSlf4jLoggingOfOutputData() { + addSlf4jLogMessageConsumer((logMessage, ctx) -> { + if (ctx.getLevel().equals(DEBUG)) { + ctx.getLogger().debug(logMessage); + } else { + ctx.getLogger().info(logMessage); + } + }); + } + + /** + * When a complete log message is available (from server output data) that + * message is parsed for its slf4j context (logger name, logger level etc.) + * and the specified {@code BiConsumer} is invoked with the log message and + * slf4j context. + * + *

Use this method only if you want a behavior that differentiates from the + * default behavior as enabled by method + * {@link #enableDefaultSlf4jLoggingOfOutputData()}. + * + *

NOTE: You might want to call method {@link #clearOutPutStreams()} before + * calling this method. + * + *

implementation detail: + *

    + *
  • if log message begins with {@code [debug]} then log level is set to + * {@code DEBUG}, otherwise log level is {@code INFO}.
  • + *
  • the appium sub module name is parsed from the log message and used as + * logger name (prefixed with "appium.service.", all lower case, spaces + * removed). If no appium sub module is detected then "appium.service" is + * used as logger name.
  • + *
+ * Example log-message: "[ADB] Cannot read version codes of " is logged by + * {@code appium.service.adb} at level {@code INFO}
+ * Example log-message: "[debug] [XCUITest] Xcode version set to 'x.y.z' " + * is logged by {@code appium.service.xcuitest} at level {@code DEBUG} + *
+ * + * @param slf4jLogMessageConsumer BiConsumer block to be executed when a log message is + * available. + */ + public void addSlf4jLogMessageConsumer(BiConsumer slf4jLogMessageConsumer) { + requireNonNull(slf4jLogMessageConsumer, "slf4jLogMessageConsumer parameter is NULL!"); + addLogMessageConsumer(logMessage -> { + slf4jLogMessageConsumer.accept(logMessage, parseSlf4jContextFromLogMessage(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(ROOT).replaceAll("\\s+", ""); + if (m.group(1) != null) { + level = DEBUG; + } + } + return new Slf4jLogMessageContext(loggerName, level); + } + + /** + * When a complete log message is available (from server output data), the + * specified {@code Consumer} is invoked with that log message. + * + *

NOTE: You might want to call method {@link #clearOutPutStreams()} before + * calling this method. + * + *

If the Consumer fails and throws an exception the exception is logged (at + * WARN level) and execution continues. + *
+ * + * @param consumer Consumer block to be executed when a log message is available. + */ + public void addLogMessageConsumer(Consumer consumer) { + requireNonNull(consumer, "consumer parameter is NULL!"); + addOutPutStream(new OutputStream() { + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + @Override + public void write(int chr) { + try { + outputStream.write(chr); + if (chr == '\n') { + consumer.accept(outputStream.toString()); + outputStream.reset(); + } + } catch (Exception e) { + // log error and continue + LOG.warn("Log message consumer crashed!", e); + } + } + }); + } } 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 1636bbae1..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,37 +16,45 @@ package io.appium.java_client.service.local; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; - -import io.appium.java_client.remote.AndroidMobileCapabilityType; -import io.appium.java_client.remote.MobileCapabilityType; +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.FileUtils; -import org.apache.commons.lang3.StringUtils; -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.CommandLine; -import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.os.ExecutableFinder; +import org.openqa.selenium.remote.Browser; import org.openqa.selenium.remote.service.DriverService; 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 { + extends DriverService.Builder { /** * The environmental variable used to define @@ -60,155 +68,117 @@ public final class AppiumServiceBuilder * the path to executable NodeJS file (node.exe for WIN and * node for Linux/MacOS X). */ - public static final String NODE_PATH = "NODE_BINARY_PATH"; - public static final String DEFAULT_LOCAL_IP_ADDRESS = "0.0.0.0"; - private static final List PATH_CAPABILITIES = ImmutableList.of(AndroidMobileCapabilityType.KEYSTORE_PATH, - AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE, MobileCapabilityType.APP); - private static final String APPIUM_FOLDER = "appium"; - private static final String BUILD_FOLDER = "build"; - private static final String LIB_FOLDER = "lib"; - private static final String MAIN_JS = "main.js"; - private static final String ERROR_NODE_NOT_FOUND = "There is no installed nodes! Please " - + "install node via NPM (https://www.npmjs.com/package/appium#using-node-js) or download and " - + "install Appium app (http://appium.io/downloads.html)"; - private static final String APPIUM_NODE_MASK = - File.separator + BUILD_FOLDER - + File.separator + LIB_FOLDER - + File.separator + MAIN_JS; - private static final int DEFAULT_APPIUM_PORT = 4723; - private static final String BASH = "bash"; - private static final String CMD_EXE = "cmd.exe"; - private static final String NODE = "node"; - final Map serverArguments = new HashMap<>(); - private File appiumJS; - private String ipAddress = DEFAULT_LOCAL_IP_ADDRESS; - private File npmScript; - private File getNodeJSExecutable; - private DesiredCapabilities capabilities; + private static final String NODE_PATH = "NODE_BINARY_PATH"; - //The first starting is slow sometimes on some - //environment - private long startupTimeout = 120; - private TimeUnit timeUnit = TimeUnit.SECONDS; + 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_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 -> + String.format("The main NodeJS executable does not exist at '%s'", fullPath.getAbsolutePath()); + + 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); + withEnvironment(System.getenv()); } - private static void validateNodeStructure(File node) { - String absoluteNodePath = node.getAbsolutePath(); + /** + * Provides a measure of how strongly this {@link DriverService} supports the given + * {@code capabilities}. A score of 0 or less indicates that this {@link DriverService} does not + * support instances of {@link org.openqa.selenium.WebDriver} that require {@code capabilities}. + * Typically, the score is generated by summing the number of capabilities that the driver + * service directly supports that are unique to the driver service (that is, things like + * "{@code proxy}" don't tend to count to the score). + * Higher the score, higher the possibility of getting grid sessions created sooner. + */ + @Override + public int score(Capabilities capabilities) { + int score = 0; - if (!node.exists()) { - throw new InvalidServerInstanceException( - "The invalid appium node " + absoluteNodePath + " has been defined", - new IOException("The node " + absoluteNodePath + "doesn't exist")); + if (capabilities.getCapability(PLATFORM_NAME) != null) { + score++; } - } - private static void disposeCachedFile(File file) throws Throwable { - if (file != null) { - FileUtils.forceDelete(file); + String browserName = capabilities.getBrowserName(); + if (Browser.CHROME.is(browserName) || browserName.equalsIgnoreCase(MobileBrowserType.ANDROID) + || Browser.SAFARI.is(browserName)) { + score++; } + + return score; } - private void setUpNPMScript() { - if (npmScript != null) { - return; + private static File validatePath(@Nullable String fullPath, String errMsg) { + if (fullPath == null) { + throw new InvalidServerInstanceException(errMsg); } - - if (!Platform.getCurrent().is(Platform.WINDOWS)) { - npmScript = Scripts.GET_PATH_TO_DEFAULT_NODE_UNIX.getScriptFile(); + File result = new File(fullPath); + if (!result.exists()) { + throw new InvalidServerInstanceException(errMsg); } + return result; } - private void setUpGetNodeJSExecutableScript() { - if (getNodeJSExecutable != null) { - return; - } - - getNodeJSExecutable = Scripts.GET_NODE_JS_EXECUTABLE.getScriptFile(); + private static File findBinary(String name, String errMsg) { + return validatePath(new ExecutableFinder().find(name), errMsg); } - private File findNodeInCurrentFileSystem() { - setUpNPMScript(); + private static File findNpm() { + return findBinary("npm", + "Node Package Manager (npm) is either not installed or its executable is not present in PATH"); + } - String instancePath; - CommandLine commandLine; + private static File findMainScript() { + File npm = findNpm(); + 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 { - if (Platform.getCurrent().is(Platform.WINDOWS)) { - commandLine = new CommandLine(CMD_EXE, "/C", "npm root -g"); - } else { - commandLine = new CommandLine(BASH, "-l", npmScript.getAbsolutePath()); - } - commandLine.execute(); - } catch (Throwable e) { - throw new RuntimeException(e); + 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); } - - instancePath = (commandLine.getStdOut()).trim(); - try { - File defaultAppiumNode; - if (StringUtils.isBlank(instancePath) || !(defaultAppiumNode = - new File(instancePath + File.separator - + APPIUM_FOLDER)).exists()) { - String errorOutput = commandLine.getStdOut(); - throw new InvalidServerInstanceException(ERROR_NODE_NOT_FOUND, - new IOException(errorOutput)); - } - //appium servers v1.5.x and higher - File result; - if ((result = new File(defaultAppiumNode, APPIUM_NODE_MASK)).exists()) { - return result; - } - - throw new InvalidServerInstanceException(ERROR_NODE_NOT_FOUND, new IOException( - "Could not find a file " + APPIUM_NODE_MASK - + " in the " - + defaultAppiumNode + " directory")); - } finally { - commandLine.destroy(); + File mainAppiumJs = Paths.get(nodeModulesRoot, APPIUM_PATH_SUFFIX.toString()).toFile(); + if (!mainAppiumJs.exists()) { + throw new InvalidServerInstanceException(APPIUM_JS_NOT_EXIST_ERROR.apply(mainAppiumJs)); } + return mainAppiumJs; } - @Override protected File findDefaultExecutable() { - - String nodeJSExec = System.getProperty(NODE_PATH); - if (StringUtils.isBlank(nodeJSExec)) { - nodeJSExec = System.getenv(NODE_PATH); - } - if (!StringUtils.isBlank(nodeJSExec)) { - File result = new File(nodeJSExec); - if (result.exists()) { - return result; - } + protected File findDefaultExecutable() { + if (this.node != null) { + validatePath(this.node.getAbsolutePath(), NODE_JS_NOT_EXIST_ERROR.apply(this.node)); + return this.node; } - CommandLine commandLine; - setUpGetNodeJSExecutableScript(); - try { - if (Platform.getCurrent().is(Platform.WINDOWS)) { - commandLine = new CommandLine(NODE + ".exe", getNodeJSExecutable.getAbsolutePath()); - } else { - commandLine = new CommandLine(NODE, getNodeJSExecutable.getAbsolutePath()); - } - commandLine.execute(); - } catch (Throwable t) { - throw new InvalidNodeJSInstance("Node.js is not installed!", t); + File node = loadPathFromEnv(NODE_PATH); + if (node != null) { + validatePath(node.getAbsolutePath(), NODE_JS_NOT_EXIST_ERROR.apply(node)); + this.node = node; + return this.node; } - - String filePath = (commandLine.getStdOut()).trim(); - - try { - if (StringUtils.isBlank(filePath) || !new File(filePath).exists()) { - String errorOutput = commandLine.getStdOut(); - String errorMessage = "Can't get a path to the default Node.js instance"; - throw new InvalidNodeJSInstance(errorMessage, new IOException(errorOutput)); - } - return new File(filePath); - } finally { - commandLine.destroy(); - } + this.node = findBinary("node", + "NodeJS is either not installed or its executable not present in PATH"); + return this.node; } /** @@ -225,6 +195,8 @@ public AppiumServiceBuilder withArgument(ServerArgument argument) { } /** + * Adds a server argument. + * * @param argument is an instance which contains the argument name. * @param value A non null string value. (Warn!!!) Boolean arguments have a special moment: * the presence of an arguments means "true". At this case an empty string @@ -232,36 +204,69 @@ public AppiumServiceBuilder withArgument(ServerArgument argument) { * @return the self-reference. */ public AppiumServiceBuilder withArgument(ServerArgument argument, String value) { - String argName = argument.getArgument().trim().toLowerCase(); - if ("--port".equals(argName) || "-p".equals(argName)) { - usingPort(Integer.valueOf(value)); - } else if ("--address".equals(argName) || "-a".equals(argName)) { - withIPAddress(value); - } else if ("--log".equals(argName) || "-g".equals(argName)) { - withLogFile(new File(value)); - } else { - serverArguments.put(argName, value); + String argName = argument.getArgument(); + switch (argName) { + case "--port": + case "-p": + usingPort(Integer.parseInt(value)); + break; + case "--address": + case "-a": + withIPAddress(value); + break; + case "--log": + case "-g": + withLogFile(new File(value)); + break; + case "--base-path": + serverArguments.put(argName, sanitizeBasePath(value)); + break; + default: + serverArguments.put(argName, value); + break; } 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 + "/"; + } + /** - * @param capabilities is an instance of - * {@link org.openqa.selenium.remote.DesiredCapabilities}. + * Adds capabilities. + * + * @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. + * * @param appiumJS an executable appium.js (1.4.x and lower) or * main.js (1.5.x and higher). * @return the self-reference. @@ -276,130 +281,91 @@ public AppiumServiceBuilder withIPAddress(String ipAddress) { return this; } - /** - * @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 (isNullOrEmpty(fullPath)) { + fullPath = System.getenv(envVarName); + } + return isNullOrEmpty(fullPath) ? null : new File(fullPath); } - private void checkAppiumJS() { - if (appiumJS != null) { - validateNodeStructure(appiumJS); + private void loadPathToMainScript() { + if (this.appiumJS != null) { + validatePath(this.appiumJS.getAbsolutePath(), APPIUM_JS_NOT_EXIST_ERROR.apply(this.appiumJS)); return; } - String appiumJS = System.getProperty(APPIUM_PATH); - if (StringUtils.isBlank(appiumJS)) { - appiumJS = System.getenv(APPIUM_PATH); - } - if (!StringUtils.isBlank(appiumJS)) { - File node = new File(appiumJS); - validateNodeStructure(node); - this.appiumJS = node; + File mainScript = loadPathFromEnv(APPIUM_PATH); + if (mainScript != null) { + validatePath(mainScript.getAbsolutePath(), APPIUM_JS_NOT_EXIST_ERROR.apply(mainScript)); + this.appiumJS = mainScript; return; } - this.appiumJS = findNodeInCurrentFileSystem(); + this.appiumJS = findMainScript(); } - private String parseCapabilitiesIfWindows() { - String result = StringUtils.EMPTY; - - if (capabilities != null) { - Map capabilitiesMap = capabilities.asMap(); - Set> entries = capabilitiesMap.entrySet(); - - for (Map.Entry entry : entries) { - Object value = entry.getValue(); - - if (value == null) { - continue; - } - - if (String.class.isAssignableFrom(value.getClass())) { - if (PATH_CAPABILITIES.contains(entry.getKey())) { - value = "\\\"" + String.valueOf(value).replace("\\", "/") + "\\\""; - } else { - value = "\\\"" + String.valueOf(value) + "\\\""; - } - } else { - value = String.valueOf(value); - } - - String key = "\\\"" + String.valueOf(entry.getKey()) + "\\\""; - if (StringUtils.isBlank(result)) { - result = key + ": " + value; - } else { - result = result + ", " + key + ": " + value; - } - } + private String capabilitiesToQuotedCmdlineArg() { + if (capabilities == null) { + return "{}"; } + StringBuilder result = new StringBuilder(); + Map capabilitiesMap = capabilities.asMap(); + Set> entries = capabilitiesMap.entrySet(); - return "{" + result + "}"; - } - - private String parseCapabilitiesIfUNIX() { - String result = StringUtils.EMPTY; - - if (capabilities != null) { - Map capabilitiesMap = capabilities.asMap(); - Set> entries = capabilitiesMap.entrySet(); - - for (Map.Entry entry : entries) { - Object value = entry.getValue(); + for (Map.Entry entry : entries) { + Object value = entry.getValue(); - if (value == null) { - continue; - } + if (value == null) { + continue; + } - if (String.class.isAssignableFrom(value.getClass())) { - value = "\"" + String.valueOf(value) + "\""; + if (value instanceof String) { + String valueString = (String) value; + if (PATH_CAPABILITIES.contains(entry.getKey())) { + value = "\\\"" + valueString.replace("\\", "/") + "\\\""; } else { - value = String.valueOf(value); + value = "\\\"" + valueString + "\\\""; } + } else { + value = String.valueOf(value); + } - String key = "\"" + String.valueOf(entry.getKey()) + "\""; - if (StringUtils.isBlank(result)) { - result = key + ": " + value; - } else { - result = result + ", " + key + ": " + value; - } + String key = "\\\"" + entry.getKey() + "\\\""; + if (result.length() > 0) { + result.append(", "); } + result.append(key).append(": ").append(value); } return "{" + result + "}"; } - - private String parseCapabilities() { - if (Platform.getCurrent().is(Platform.WINDOWS)) { - return parseCapabilitiesIfWindows(); + + private String capabilitiesToCmdlineArg() { + if (autoQuoteCapabilitiesOnWindows && Platform.getCurrent().is(Platform.WINDOWS)) { + return capabilitiesToQuotedCmdlineArg(); } - return parseCapabilitiesIfUNIX(); + Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .serializeNulls() + .create(); + // Selenium internally uses org.apache.commons.exec.CommandLine + // which has the following known bug in its arguments parser: + // https://issues.apache.org/jira/browse/EXEC-54 + return gson.toJson(capabilities.asMap()); } - @Override protected ImmutableList createArgs() { + @Override + protected List createArgs() { List argList = new ArrayList<>(); - checkAppiumJS(); + loadPathToMainScript(); argList.add(appiumJS.getAbsolutePath()); argList.add("--port"); argList.add(String.valueOf(getPort())); - if (StringUtils.isBlank(ipAddress)) { - ipAddress = DEFAULT_LOCAL_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); @@ -414,22 +380,29 @@ private String parseCapabilities() { 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); } } if (capabilities != null) { argList.add("--default-capabilities"); - argList.add(parseCapabilities()); + argList.add(capabilitiesToCmdlineArg()); } - return new ImmutableList.Builder().addAll(argList).build(); + return Collections.unmodifiableList(argList); + } + + @Override + protected void loadSystemProperties() { + if (this.exe == null) { + usingDriverExecutable(findDefaultExecutable()); + } } /** @@ -472,7 +445,8 @@ public AppiumServiceBuilder usingAnyFreePort() { * appium server with. * @return A self reference. */ - @Override public AppiumServiceBuilder withEnvironment(Map environment) { + @Override + public AppiumServiceBuilder withEnvironment(Map environment) { return super.withEnvironment(environment); } @@ -487,23 +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); - } - } - - @Override protected void finalize() throws Throwable { - try { - disposeCachedFile(npmScript); - disposeCachedFile(getNodeJSExecutable); - } finally { - super.finalize(); - } + 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/InvalidServerInstanceException.java b/src/main/java/io/appium/java_client/service/local/InvalidServerInstanceException.java index 9b7498407..6addfbd33 100644 --- a/src/main/java/io/appium/java_client/service/local/InvalidServerInstanceException.java +++ b/src/main/java/io/appium/java_client/service/local/InvalidServerInstanceException.java @@ -21,9 +21,11 @@ public class InvalidServerInstanceException extends RuntimeException { private static final long serialVersionUID = 1L; - private static String MESSAGE_PREFIX = "Invalid server instance exception has occured: "; + public InvalidServerInstanceException(String message, Throwable t) { + super(message, t); + } - public InvalidServerInstanceException(String messege, Throwable t) { - super(MESSAGE_PREFIX + messege, t); + public InvalidServerInstanceException(String message) { + super(message); } } 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 c0d83cef4..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 @@ -16,11 +16,11 @@ package io.appium.java_client.service.local; - import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Optional; class ListOutputStream extends OutputStream { @@ -31,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); @@ -49,15 +53,30 @@ 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(); } } + + /** + * Clears all the existing output streams. + * + * @return true if at least one output stream has been cleared + */ + public boolean clear() { + if (streams.isEmpty()) { + return false; + } + streams.clear(); + return true; + } } diff --git a/src/main/java/io/appium/java_client/service/local/Scripts.java b/src/main/java/io/appium/java_client/service/local/Scripts.java deleted file mode 100644 index 042654e03..000000000 --- a/src/main/java/io/appium/java_client/service/local/Scripts.java +++ /dev/null @@ -1,80 +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 org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - - -enum Scripts { - GET_PATH_TO_DEFAULT_NODE_UNIX("get_path_to_default_node.sh"), - GET_NODE_JS_EXECUTABLE("getExe.js"); - private static final String RESOURCE_FOLDER = "/scripts/"; - private final String script; - - Scripts(String script) { - this.script = script; - } - - public File getScriptFile() { - InputStream inputStream = getClass().getResourceAsStream(RESOURCE_FOLDER + this.script); - byte[] bytes; - try { - bytes = IOUtils.toByteArray(inputStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - - String[] splittedName = this.script.split("\\."); - File scriptFile; - try { - scriptFile = File.createTempFile(splittedName[0], "." + splittedName[1]); - } catch (IOException e) { - throw new RuntimeException(e); - } - - if (!scriptFile.exists()) { - try { - scriptFile.createNewFile(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - FileOutputStream output; - try { - output = new FileOutputStream(scriptFile, true); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } - - try { - output.write(bytes); - output.flush(); - output.close(); - return scriptFile; - } catch (IOException e) { - throw new RuntimeException(e); - } - - } -} diff --git a/src/main/java/io/appium/java_client/service/local/Slf4jLogMessageContext.java b/src/main/java/io/appium/java_client/service/local/Slf4jLogMessageContext.java new file mode 100644 index 000000000..3f7c60e52 --- /dev/null +++ b/src/main/java/io/appium/java_client/service/local/Slf4jLogMessageContext.java @@ -0,0 +1,41 @@ +package io.appium.java_client.service.local; + +import lombok.AccessLevel; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; + +/** + * This class provides context to a Slf4jLogMessageConsumer. + * + */ +public final class Slf4jLogMessageContext { + /** + * Returns the {@link Logger} instance associated with this context. + * + * @return {@link Logger} instance associated with this context. + * + */ + @Getter(AccessLevel.PUBLIC) private final Logger logger; + /** + * Returns log {@link Level} for the log message associated with this context. + * + * @return {@link Level} for log message associated with this context. + */ + @Getter(AccessLevel.PUBLIC) private final Level level; + + Slf4jLogMessageContext(String loggerName, Level level) { + this.level = level; + this.logger = LoggerFactory.getLogger(loggerName); + } + + /** + * Returns the name of the {@link Logger} associated with this context. + * + * @return name of {@link Logger} associated with this context. + */ + public String getName() { + return logger.getName(); + } +} diff --git a/src/main/java/io/appium/java_client/service/local/flags/AndroidServerFlag.java b/src/main/java/io/appium/java_client/service/local/flags/AndroidServerFlag.java index 9321078fb..f04427d6f 100644 --- a/src/main/java/io/appium/java_client/service/local/flags/AndroidServerFlag.java +++ b/src/main/java/io/appium/java_client/service/local/flags/AndroidServerFlag.java @@ -25,11 +25,6 @@ public enum AndroidServerFlag implements ServerArgument { * --bootstrap-port 4724 */ BOOTSTRAP_PORT_NUMBER("--bootstrap-port"), - /** - * Local port used for communication with Selendroid. Sample: - * --selendroid-port 8080 - */ - SELENDROID_PORT("--selendroid-port"), /** * If set, prevents Appium from killing the adb server * instance. Default: false 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 accccbe1e..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,15 +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 */ @@ -107,12 +91,55 @@ public enum GeneralServerFlag implements ServerArgument { * Add exaggerated spacing in logs to help with visual inspection. Default: false */ DEBUG_LOG_SPACING("--debug-log-spacing"), - /** * Add long stack traces to log entries. Recommended for debugging only. * Default: false */ - ASYNC_TRACE("--async-trace"); + ASYNC_TRACE("--async-trace"), + /** + * Disable additional security checks. Only enable it if all the clients are in the trusted network. + * Default: false + */ + RELAXED_SECURITY("--relaxed-security"), + /** + * Enables NodeJS memory dumps collection feature. + */ + ENABLE_HEAP_DUMP("--enable-heapdump"), + /** + * Allow a list of features which are considered insecure and must be turned on + * explicitly by system administrators. + * Default: [] + * Sample: --allow-insecure=foo,bar + */ + ALLOW_INSECURE("--allow-insecure"), + /** + * Specify a list of features which will never be allowed to run, even if --relaxed-security + * is turned on, and even if feature names are listed with --allow-insecure. + * Default: [] + * Sample: --deny-insecure=foo,bar + */ + 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 new file mode 100644 index 000000000..2514a92a5 --- /dev/null +++ b/src/main/java/io/appium/java_client/touch/ActionOptions.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.touch; + +import java.util.HashMap; +import java.util.Map; + +@Deprecated +public abstract class ActionOptions> { + /** + * This method is automatically called before building + * options map to verify the consistency of the instance. + * + * @throws IllegalArgumentException if there are problems with this options map. + */ + protected abstract void verify(); + + /** + * Creates a map based on the provided options. + * + * @return options mapping. + */ + public Map build() { + verify(); + return new HashMap<>(); + } +} diff --git a/src/main/java/io/appium/java_client/touch/LongPressOptions.java b/src/main/java/io/appium/java_client/touch/LongPressOptions.java new file mode 100644 index 000000000..56d2334fb --- /dev/null +++ b/src/main/java/io/appium/java_client/touch/LongPressOptions.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.touch; + +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; + + /** + * It creates an empty instance of {@link LongPressOptions}. + * + * @return an empty instance of {@link LongPressOptions} + */ + public static LongPressOptions longPressOptions() { + return new LongPressOptions(); + } + + /** + * Set the long press duration. + * + * @param duration the value to set. + * Time resolution unit is 1 ms. + * @return this instance for chaining. + */ + public LongPressOptions withDuration(Duration duration) { + requireNonNull(duration); + checkArgument(duration.toMillis() >= 0, + "Duration value should be greater or equal to zero"); + this.duration = duration; + return this; + } + + @Override + public Map build() { + final Map result = super.build(); + ofNullable(duration).ifPresent(durationParam -> + result.put("duration", durationParam.toMillis())); + return result; + } +} diff --git a/src/main/java/io/appium/java_client/touch/TapOptions.java b/src/main/java/io/appium/java_client/touch/TapOptions.java new file mode 100644 index 000000000..7dee99fae --- /dev/null +++ b/src/main/java/io/appium/java_client/touch/TapOptions.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.touch; + +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; + + /** + * It creates an empty instance of {@link TapOptions}. + * + * @return the empty instance of {@link TapOptions} + */ + public static TapOptions tapOptions() { + return new TapOptions(); + } + + /** + * Set the count of taps to perform. + * + * @param tapsCount the taps count to perform. + * The value should be greater than zero. + * @return this instance for chaining. + */ + public TapOptions withTapsCount(int tapsCount) { + checkArgument(tapsCount > 0, "Taps count should be greater than zero"); + this.tapsCount = tapsCount; + return this; + } + + @Override + public Map build() { + final Map result = super.build(); + ofNullable(tapsCount).ifPresent(integer -> result.put("count", integer)); + return result; + } +} diff --git a/src/main/java/io/appium/java_client/touch/WaitOptions.java b/src/main/java/io/appium/java_client/touch/WaitOptions.java new file mode 100644 index 000000000..11eb0ccdc --- /dev/null +++ b/src/main/java/io/appium/java_client/touch/WaitOptions.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.touch; + +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); + + /** + * Creates and instance of {@link WaitOptions}. + * + * @param duration is the duration of the waiting. + * @return a built option. + */ + public static WaitOptions waitOptions(Duration duration) { + return new WaitOptions().withDuration(duration); + } + + /** + * Set the wait duration. + * + * @param duration the value to set. + * Time resolution unit is 1 ms. + * @return this instance for chaining. + */ + public WaitOptions withDuration(Duration duration) { + requireNonNull(duration); + checkArgument(duration.toMillis() >= 0, + "Duration value should be greater or equal to zero"); + this.duration = duration; + return this; + } + + @Override + protected void verify() { + //there is nothing to check + } + + @Override + public Map build() { + final Map result = super.build(); + result.put("ms", duration.toMillis()); + return result; + } +} 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 new file mode 100644 index 000000000..194228eea --- /dev/null +++ b/src/main/java/io/appium/java_client/touch/offset/AbstractOptionCombinedWithPosition.java @@ -0,0 +1,51 @@ +package io.appium.java_client.touch.offset; + +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; + + /** + * Some actions may require coordinates. Invocation of this method + * replaces the result of previous {@link #withElement(ElementOption)} invocation. + * + * @param positionOption required coordinates. * + * @return self-reference + */ + public T withPosition(PointOption positionOption) { + this.positionOption = positionOption; + return (T) this; + } + + /** + * Most of touch action may use position which is relative to some element. In order to unify + * this behaviour this method was added. Invocation of this method + * replaces the result of previous {@link #withPosition(PointOption)} invocation. + * + * @param element required position which is relative to some element + * @return self-reference + */ + public T withElement(ElementOption element) { + positionOption = element; + return (T) this; + } + + protected void verify() { + ofNullable(positionOption).orElseThrow(() -> + new IllegalArgumentException("Some coordinates or an offset from an element should " + + "be defined. Use withPosition or withElement methods")); + } + + @Override + public Map build() { + final Map result = super.build(); + result.putAll(positionOption.build()); + return result; + } +} 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 new file mode 100644 index 000000000..ac5d577a7 --- /dev/null +++ b/src/main/java/io/appium/java_client/touch/offset/ElementOption.java @@ -0,0 +1,112 @@ +package io.appium.java_client.touch.offset; + +import org.openqa.selenium.Point; +import org.openqa.selenium.WebElement; +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; + + /** + * This method creates a build instance of the {@link ElementOption}. + * + * @param element is the element to calculate offset from. + * @param offset is the offset from the upper left corner of the given element. + * @return the built option + */ + public static ElementOption element(WebElement element, Point offset) { + return new ElementOption().withElement(element).withCoordinates(offset); + } + + + /** + * This method creates a build instance of the {@link ElementOption}. + * + * @param element is the element to calculate offset from. + * @param x is the x-offset from the upper left corner of the given element. + * @param y is the y-offset from the upper left corner of the given element. + * @return the built option + */ + public static ElementOption element(WebElement element, int x, int y) { + return new ElementOption().withElement(element).withCoordinates(x, y); + } + + /** + * This method creates a build instance of the {@link ElementOption}. + * + * @param element is the element to calculate offset from. + * @return the built option + */ + public static ElementOption element(WebElement element) { + return new ElementOption().withElement(element); + } + + /** + * It defines x and y offset from the upper left corner of an element. + * + * @param offset is the offset from the upper left corner of the given element. + * @return self-reference + */ + @Override + public ElementOption withCoordinates(Point offset) { + super.withCoordinates(offset); + return this; + } + + /** + * It defines x and y offset from the upper left corner of an element. + * + * @param xOffset is the x-offset from the upper left corner of the given element. + * @param yOffset is the y-offset from the upper left corner of the given element. + * @return self-reference + */ + @Override + public ElementOption withCoordinates(int xOffset, int yOffset) { + super.withCoordinates(xOffset, yOffset); + return this; + } + + /** + * This method sets the element as an option. It means that x/y offset is the offset + * from the upper left corner of the given element. + * + * @param element is the element to calculate offset from. + * @return self-reference + */ + public ElementOption withElement(WebElement element) { + requireNonNull(element); + checkArgument(true, "Element should be an instance of the class which " + + "extends org.openqa.selenium.remote.RemoteWebElement", + element instanceof RemoteWebElement); + elementId = ((RemoteWebElement) element).getId(); + return this; + } + + @Override + protected void verify() { + ofNullable(elementId).orElseThrow(() -> + new IllegalArgumentException("Element should be defined")); + } + + @Override + public Map build() { + verify(); + final Map result = new HashMap<>(); + result.put("element", elementId); + + ofNullable(coordinates).ifPresent(point -> { + result.put("x", point.x); + result.put("y", point.y); + }); + return result; + } +} 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 new file mode 100644 index 000000000..a45d59f9c --- /dev/null +++ b/src/main/java/io/appium/java_client/touch/offset/PointOption.java @@ -0,0 +1,77 @@ +package io.appium.java_client.touch.offset; + +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; + + /** + * It creates a built instance of {@link PointOption} which takes x and y coordinates. + * This is offset from the upper left corner of the screen. + * + * @param offset is an offset value. + * @return a built option + */ + public static PointOption point(Point offset) { + return new PointOption().withCoordinates(offset); + } + + /** + * It creates a built instance of {@link PointOption} which takes x and y coordinates. + * This is offset from the upper left corner of the screen. + * + * @param xOffset is x value. + * @param yOffset is y value. + * @return a built option + */ + public static PointOption point(int xOffset, int yOffset) { + return new PointOption().withCoordinates(xOffset, yOffset); + } + + /** + * It defines x and y coordinates. + * This is offset from the upper left corner of the screen. + * + * @param offset is an offset value. + * @return self-reference + */ + public T withCoordinates(Point offset) { + return withCoordinates(offset.x, offset.y); + } + + /** + * It defines x and y coordinates. + * This is offset from the upper left corner of the screen. + * + * @param xOffset is x value. + * @param yOffset is y value. + * @return self-reference + */ + public T withCoordinates(int xOffset, int yOffset) { + coordinates = new Point(xOffset, yOffset); + //noinspection unchecked + return (T) this; + } + + @Override + protected void verify() { + //noinspection ResultOfMethodCallIgnored + ofNullable(coordinates).orElseThrow(() -> new IllegalArgumentException( + "Coordinate values must be defined")); + } + + @Override + public Map build() { + final Map result = super.build(); + result.put("x", coordinates.x); + result.put("y", coordinates.y); + return result; + } +} 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 5c03692ec..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.PressesKeyCode; +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 { +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, substituteMobilePlatform(capabilities, WINDOWS)); + super(executor, ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } - public WindowsDriver(URL remoteAddress, Capabilities desiredCapabilities) { - super(remoteAddress, substituteMobilePlatform(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, substituteMobilePlatform(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, substituteMobilePlatform(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, substituteMobilePlatform(desiredCapabilities, WINDOWS)); + Capabilities capabilities) { + super(service, httpClientFactory, ensurePlatformAndAutomationNames( + capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } - public WindowsDriver(AppiumServiceBuilder builder, Capabilities desiredCapabilities) { - super(builder, substituteMobilePlatform(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, substituteMobilePlatform(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, substituteMobilePlatform(desiredCapabilities, WINDOWS)); + /** + * Launch the application app under test after it was closed. + */ + public void launchApp() { + execute(MobileCommand.LAUNCH_APP); } - public WindowsDriver(Capabilities desiredCapabilities) { - super(substituteMobilePlatform(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 new file mode 100644 index 000000000..8f5d5bc72 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/WindowsStartScreenRecordingOptions.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.windows; + +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 WindowsStartScreenRecordingOptions + extends BaseStartScreenRecordingOptions { + private Integer fps; + private String videoFilter; + private String preset; + private Boolean captureCursor; + private Boolean captureClicks; + private String audioInput; + + public static WindowsStartScreenRecordingOptions startScreenRecordingOptions() { + return new WindowsStartScreenRecordingOptions(); + } + + /** + * 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 WindowsStartScreenRecordingOptions 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 WindowsStartScreenRecordingOptions 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 WindowsStartScreenRecordingOptions enableClicksCapture() { + this.captureClicks = true; + return this; + } + + /** + * If provided then the given audio input will be used to record the computer audio + * along with the desktop video. The list of available devices could be retrieved using + * `ffmpeg -list_devices true -f dshow -i dummy` command. + * + * @param audioInput One of valid audio input names listed by ffmpeg + * @return self instance for chaining. + */ + public WindowsStartScreenRecordingOptions withAudioInput(String audioInput) { + this.audioInput = audioInput; + 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 WindowsStartScreenRecordingOptions 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 WindowsStartScreenRecordingOptions 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 WindowsStartScreenRecordingOptions 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(audioInput).map(x -> map.put("audioInput", x)); + return Collections.unmodifiableMap(map); + } +} diff --git a/src/main/java/io/appium/java_client/windows/WindowsStopScreenRecordingOptions.java b/src/main/java/io/appium/java_client/windows/WindowsStopScreenRecordingOptions.java new file mode 100644 index 000000000..206e8c644 --- /dev/null +++ b/src/main/java/io/appium/java_client/windows/WindowsStopScreenRecordingOptions.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.windows; + +import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions; + +public class WindowsStopScreenRecordingOptions extends + BaseStopScreenRecordingOptions { + + public static WindowsStopScreenRecordingOptions stopScreenRecordingOptions() { + return new WindowsStopScreenRecordingOptions(); + } + +} 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/CanHandleConnects.java b/src/main/java/io/appium/java_client/ws/CanHandleConnects.java new file mode 100644 index 000000000..033766158 --- /dev/null +++ b/src/main/java/io/appium/java_client/ws/CanHandleConnects.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.ws; + +import java.util.List; + +public interface CanHandleConnects { + + /** + * Returns a list of all registered web socket connection handlers. + * + * @return The list of web socket connection handlers. + */ + List getConnectionHandlers(); + + /** + * Register a new message handler. + * + * @param handler a callback function, which is going to be executed when web socket connection event arrives + */ + default void addConnectionHandler(Runnable handler) { + getConnectionHandlers().add(handler); + } + + /** + * Removes existing web socket connection handlers. + */ + default void removeConnectionHandlers() { + getConnectionHandlers().clear(); + } +} diff --git a/src/main/java/io/appium/java_client/ws/CanHandleDisconnects.java b/src/main/java/io/appium/java_client/ws/CanHandleDisconnects.java new file mode 100644 index 000000000..60f96f276 --- /dev/null +++ b/src/main/java/io/appium/java_client/ws/CanHandleDisconnects.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.ws; + +import java.util.List; + +public interface CanHandleDisconnects { + + /** + * Returns a list of all registered web socket disconnection handlers. + * + * @return The list of web socket disconnection handlers. + */ + List getDisconnectionHandlers(); + + /** + * Register a new web socket disconnect handler. + * + * @param handler a callback function, which is going to be executed when web socket disconnect event arrives + */ + default void addDisconnectionHandler(Runnable handler) { + getDisconnectionHandlers().add(handler); + } + + /** + * Removes existing disconnection handlers. + */ + default void removeDisconnectionHandlers() { + getDisconnectionHandlers().clear(); + } +} diff --git a/src/main/java/io/appium/java_client/ws/CanHandleErrors.java b/src/main/java/io/appium/java_client/ws/CanHandleErrors.java new file mode 100644 index 000000000..5bafb43a4 --- /dev/null +++ b/src/main/java/io/appium/java_client/ws/CanHandleErrors.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.ws; + +import java.util.List; +import java.util.function.Consumer; + +public interface CanHandleErrors { + + /** + * Returns a list of all registered web socket error handlers. + * + * @return The list of web socket error handlers. + */ + List> getErrorHandlers(); + + /** + * Register a new error handler. + * + * @param handler a callback function, which accepts the received exception instance as a parameter + */ + default void addErrorHandler(Consumer handler) { + getErrorHandlers().add(handler); + } + + /** + * Removes existing error handlers. + */ + default void removeErrorHandlers() { + getErrorHandlers().clear(); + } +} diff --git a/src/main/java/io/appium/java_client/ws/CanHandleMessages.java b/src/main/java/io/appium/java_client/ws/CanHandleMessages.java new file mode 100644 index 000000000..6c1b9ffa6 --- /dev/null +++ b/src/main/java/io/appium/java_client/ws/CanHandleMessages.java @@ -0,0 +1,46 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.ws; + +import java.util.List; +import java.util.function.Consumer; + +public interface CanHandleMessages { + /** + * Returns a list of all registered web socket messages handlers. + * + * @return The list of web socket message handlers. + */ + List> getMessageHandlers(); + + /** + * Register a new message handler. + * + * @param handler a callback function, which accepts the received message as a parameter + */ + default void addMessageHandler(Consumer handler) { + getMessageHandlers().add(handler); + } + + /** + * Removes existing message handlers. + */ + default void removeMessageHandlers() { + getMessageHandlers().clear(); + } +} + diff --git a/src/main/java/io/appium/java_client/ws/StringWebSocketClient.java b/src/main/java/io/appium/java_client/ws/StringWebSocketClient.java new file mode 100644 index 000000000..f080c061c --- /dev/null +++ b/src/main/java/io/appium/java_client/ws/StringWebSocketClient.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.java_client.ws; + +import org.jspecify.annotations.Nullable; +import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.http.HttpMethod; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.WebSocket; + +import java.lang.ref.WeakReference; +import java.net.URI; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +public class StringWebSocketClient implements WebSocket.Listener, + CanHandleMessages, CanHandleErrors, CanHandleConnects, CanHandleDisconnects { + private final List> messageHandlers = new CopyOnWriteArrayList<>(); + private final List> errorHandlers = new CopyOnWriteArrayList<>(); + private final List connectHandlers = new CopyOnWriteArrayList<>(); + private final List disconnectHandlers = new CopyOnWriteArrayList<>(); + + 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) { + this.endpoint = endpoint; + } + + @Nullable + public URI getEndpoint() { + return this.endpoint; + } + + public boolean isListening() { + return isListening; + } + + /** + * Connects web socket client. + * + * @param endpoint The full address of an endpoint to connect to. + * Usually starts with 'ws://'. + */ + public void connect(URI endpoint) { + if (endpoint.equals(this.getEndpoint()) && isListening) { + return; + } + + HttpRequest request = new HttpRequest(HttpMethod.GET, endpoint.toString()); + Objects.requireNonNull(httpClient.get()).openSocket(request, this); + onOpen(); + + setEndpoint(endpoint); + } + + /** + * The callback method invoked on websocket opening. + */ + public void onOpen() { + try { + getConnectionHandlers().forEach(Runnable::run); + } finally { + isListening = true; + } + } + + @Override + public void onClose(int code, String reason) { + try { + getDisconnectionHandlers().forEach(Runnable::run); + } finally { + isListening = false; + } + } + + @Override + public void onError(Throwable t) { + getErrorHandlers().forEach(x -> x.accept(t)); + } + + @Override + public void onText(CharSequence data) { + String text = data.toString(); + getMessageHandlers().forEach(x -> x.accept(text)); + } + + @Override + public List> getMessageHandlers() { + return messageHandlers; + } + + @Override + public List> getErrorHandlers() { + return errorHandlers; + } + + @Override + public List getConnectionHandlers() { + return connectHandlers; + } + + @Override + public List getDisconnectionHandlers() { + return disconnectHandlers; + } + + /** + * Remove all the registered handlers. + */ + public void removeAllHandlers() { + removeMessageHandlers(); + removeErrorHandlers(); + removeConnectionHandlers(); + removeDisconnectionHandlers(); + } +} 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 79501d6d8..000000000 --- a/src/main/java/org/openqa/selenium/SearchContext.java +++ /dev/null @@ -1,41 +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 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 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 new file mode 100644 index 000000000..9875b0c49 --- /dev/null +++ b/src/main/resources/main.properties @@ -0,0 +1,2 @@ +selenium.version=@selenium.version@ +appiumClient.version=@appiumClient.version@ diff --git a/src/main/resources/scripts/getExe.js b/src/main/resources/scripts/getExe.js deleted file mode 100644 index 15cfdb975..000000000 --- a/src/main/resources/scripts/getExe.js +++ /dev/null @@ -1 +0,0 @@ -console.log(process.execPath); \ No newline at end of file diff --git a/src/main/resources/scripts/get_path_to_default_node.sh b/src/main/resources/scripts/get_path_to_default_node.sh deleted file mode 100644 index fce4693ea..000000000 --- a/src/main/resources/scripts/get_path_to_default_node.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!bin/sh -OUTPUT="$(npm root -g)" -echo "${OUTPUT}" -exit \ No newline at end of file diff --git a/src/test/java/io/appium/java_client/ApiDemos-debug.apk b/src/test/java/io/appium/java_client/ApiDemos-debug.apk deleted file mode 100644 index 62a1fd607..000000000 Binary files a/src/test/java/io/appium/java_client/ApiDemos-debug.apk and /dev/null differ diff --git a/src/test/java/io/appium/java_client/IntentExample.apk b/src/test/java/io/appium/java_client/IntentExample.apk deleted file mode 100644 index b86d5e9b7..000000000 Binary files a/src/test/java/io/appium/java_client/IntentExample.apk and /dev/null differ diff --git a/src/test/java/io/appium/java_client/TestApp.app.zip b/src/test/java/io/appium/java_client/TestApp.app.zip deleted file mode 100755 index ffe0ea709..000000000 Binary files a/src/test/java/io/appium/java_client/TestApp.app.zip and /dev/null differ diff --git a/src/test/java/io/appium/java_client/UICatalog.app.zip b/src/test/java/io/appium/java_client/UICatalog.app.zip deleted file mode 100644 index 911811635..000000000 Binary files a/src/test/java/io/appium/java_client/UICatalog.app.zip and /dev/null differ diff --git a/src/test/java/io/appium/java_client/WebViewApp.app.zip b/src/test/java/io/appium/java_client/WebViewApp.app.zip deleted file mode 100755 index 45266abef..000000000 Binary files a/src/test/java/io/appium/java_client/WebViewApp.app.zip and /dev/null differ 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 607f84a7f..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidAbilityToUseSupplierTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.appium.java_client.android; - -import static org.junit.Assert.assertNotEquals; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.TouchAction; -import io.appium.java_client.functions.ActionSupplier; -import org.junit.Test; -import org.openqa.selenium.Point; - -import java.time.Duration; -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(); - - return new TouchAction(driver).press(images.get(2), -10, center.y - location.y) - .waitAction(Duration.ofSeconds(2)).moveTo(gallery, 10, center.y - location.y).release(); - }; - - private final ActionSupplier verticalSwiping = () -> - new TouchAction(driver).press(driver.findElementByAccessibilityId("Gallery")) - .waitAction(Duration.ofSeconds(2)).moveTo(driver.findElementByAccessibilityId("Auto Complete")) - .release(); - - @Test public void horizontalSwipingWithSupplier() throws Exception { - 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 7d6693944..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidActivityTest.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.android; - -import static org.junit.Assert.assertEquals; - -import org.junit.Before; -import org.junit.Test; - -public class AndroidActivityTest extends BaseAndroidTest { - - @Before public void setUp() throws Exception { - 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.contacts", ".ContactsListActivity"); - driver.startActivity(activity); - assertEquals(driver.currentActivity(), ".ContactsListActivity"); - driver.pressKeyCode(AndroidKeyCode.BACK); - assertEquals(driver.currentActivity(), ".ContactsListActivity"); - } - - @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.contacts", ".ContactsListActivity") - .setAppWaitPackage("com.android.contacts") - .setAppWaitActivity(".ContactsListActivity") - .setStopApp(false); - driver.startActivity(newActivity); - assertEquals(driver.currentActivity(), ".ContactsListActivity"); - driver.pressKeyCode(AndroidKeyCode.BACK); - assertEquals(driver.currentActivity(), ".accessibility.AccessibilityNodeProviderActivity"); - } -} diff --git a/src/test/java/io/appium/java_client/android/AndroidConnectionTest.java b/src/test/java/io/appium/java_client/android/AndroidConnectionTest.java deleted file mode 100644 index f6df58129..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidConnectionTest.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.android; - -import static org.junit.Assert.assertEquals; - -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class AndroidConnectionTest extends BaseAndroidTest { - - @Test public void test1() { - driver.setConnection(Connection.WIFI); - assertEquals(Connection.WIFI, - driver.getConnection()); - } - - @Test public void test2() { - driver.setConnection(Connection.NONE); - assertEquals(Connection.NONE, - driver.getConnection()); - driver.setConnection(Connection.AIRPLANE); - assertEquals(Connection.AIRPLANE, - driver.getConnection()); - } - - @Test public void test3() { - driver.setConnection(Connection.ALL); - assertEquals(Connection.ALL, - driver.getConnection()); - } -} diff --git a/src/test/java/io/appium/java_client/android/AndroidDriverTest.java b/src/test/java/io/appium/java_client/android/AndroidDriverTest.java deleted file mode 100644 index 037af0775..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidDriverTest.java +++ /dev/null @@ -1,188 +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.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; -import org.junit.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.List; -import java.util.Map; - - -public class AndroidDriverTest extends BaseAndroidTest { - - @Test public void getDeviceTimeTest() { - String time = driver.getDeviceTime(); - assertTrue(time.length() == 28); - } - - @Test public void isAppInstalledTest() { - assertTrue(driver.isAppInstalled("com.example.android.apis")); - } - - @Test public void isAppNotInstalledTest() { - assertFalse(driver.isAppInstalled("foo")); - } - - @Test public void closeAppTest() throws InterruptedException { - driver.closeApp(); - driver.launchApp(); - assertEquals(".ApiDemos", driver.currentActivity()); - } - - @Test public void pushFileTest() { - byte[] data = Base64.encodeBase64( - "The eventual code is no more than the deposit of your understanding. ~E. W. Dijkstra" - .getBytes()); - driver.pushFile("/data/local/tmp/remote.txt", data); - byte[] returnData = driver.pullFile("/data/local/tmp/remote.txt"); - String returnDataDecoded = new String(Base64.decodeBase64(returnData)); - assertEquals( - "The eventual code is no more than the deposit of your understanding. ~E. W. Dijkstra", - returnDataDecoded); - } - - @Test public void pushTempFileTest() throws Exception { - File temp = File.createTempFile("Temp_", "_test"); - try { - FileUtils.writeStringToFile(temp, "The eventual code is no " - + "more than the deposit of your understanding. ~E. W. Dijkstra", "UTF-8", true); - driver.pushFile("/data/local/tmp/remote2.txt", temp); - byte[] returnData = driver.pullFile("/data/local/tmp/remote2.txt"); - String returnDataDecoded = new String(Base64.decodeBase64(returnData)); - assertEquals( - "The eventual code is no more than the deposit of " - + "your understanding. ~E. W. Dijkstra", - returnDataDecoded); - } finally { - FileUtils.forceDelete(temp); - } - } - - @Test public void toggleLocationServicesTest() { - driver.toggleLocationServices(); - } - - @Test public void geolocationTest() { - Location location = new Location(45, 45, 100); - driver.setLocation(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() { - driver.lockDevice(); - assertEquals(true, driver.isLocked()); - driver.unlockDevice(); - assertEquals(false, driver.isLocked()); - } - - @Test public void runAppInBackgroundTest() { - long time = System.currentTimeMillis(); - driver.runAppInBackground(Duration.ofSeconds(4)); - long timeAfter = System.currentTimeMillis(); - assert (timeAfter - time > 3000); - } - - @Test public void pullFileTest() { - byte[] data = - driver.pullFile("data/system/registered_services/android.content.SyncAdapter.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); - } - - @Test public void deviceDetailsAndKeyboardTest() { - assertFalse(driver.isKeyboardShown()); - assertNotNull(driver.getDisplayDensity()); - assertNotEquals(0, driver.getSystemBars().size()); - } - - @Test public void getSupportedPerformanceDataTypesTest() { - driver.startActivity(new Activity("io.appium.android.apis", ".ApiDemos")); - - List dataTypes = new ArrayList(); - dataTypes.add("cpuinfo"); - dataTypes.add("memoryinfo"); - dataTypes.add("batteryinfo"); - dataTypes.add("networkinfo"); - - List supportedPerformanceDataTypes = driver.getSupportedPerformanceDataTypes(); - - assertEquals(4, supportedPerformanceDataTypes.size()); - - for ( int i = 0 ; i < supportedPerformanceDataTypes.size() ; ++i) { - assertEquals(dataTypes.get(i), supportedPerformanceDataTypes.get(i)); - } - - - } - - @Test public void getPerformanceDataTest() throws Exception { - driver.startActivity(new Activity("io.appium.android.apis", ".ApiDemos")); - - List supportedPerformanceDataTypes = driver.getSupportedPerformanceDataTypes(); - - for (String dataType : supportedPerformanceDataTypes) { - - List> valueTable = driver.getPerformanceData("com.example.android.apis", dataType, 60000); - - for (int j = 1; j < valueTable.size(); ++j) { - assertEquals(valueTable.subList(0, 0).size(), valueTable.subList(j, j).size()); - } - } - - } - - @Test public void getCurrentPackageTest() { - assertEquals("io.appium.android.apis",driver.getCurrentPackage()); - } - -} 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 32cd82d57..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() throws Exception { - 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/AndroidSearchingTest.java b/src/test/java/io/appium/java_client/android/AndroidSearchingTest.java deleted file mode 100644 index 3b0ef3eb2..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() throws Exception { - 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 3a4cc3651..000000000 --- a/src/test/java/io/appium/java_client/android/AndroidTouchTest.java +++ /dev/null @@ -1,179 +0,0 @@ -package io.appium.java_client.android; - -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.time.Duration; -import java.util.List; - -public class AndroidTouchTest extends BaseAndroidTest { - - @Before - public void setUp() throws Exception { - driver.resetApp(); - } - - @Test public void dragNDropByElementTest() throws Exception { - 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(dragDot1).moveTo(dragDot3).release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void dragNDropByElementAndDurationTest() throws Exception { - 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(dragDot1, Duration.ofSeconds(2)).moveTo(dragDot3).release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void dragNDropByCoordinatesTest() throws Exception { - 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(center1.x, center1.y).moveTo(center2.x, center2.y).release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void dragNDropByCoordinatesAndDurationTest() throws Exception { - 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(center1.x, center1.y, Duration.ofSeconds(2)) - .moveTo(center2.x, center2.y).release(); - dragNDrop.perform(); - assertNotEquals("Drag text empty", "", dragText.getText()); - } - - @Test public void pressByCoordinatesTest() throws Exception { - 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.x + 20, point.y + 30).waitAction(Duration.ofSeconds(1)) - .release().perform(); - assertEquals("ON" ,driver - .findElementById("io.appium.android.apis:id/button_toggle").getText()); - } - - @Test public void pressByElementTest() throws Exception { - Activity activity = new Activity("io.appium.android.apis", ".view.Buttons1"); - driver.startActivity(activity); - new TouchAction(driver).press(driver.findElementById("io.appium.android.apis:id/button_toggle")) - .waitAction(Duration.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(driver.findElementById("io.appium.android.apis:id/start")).waitAction(Duration.ofSeconds(2)) - .tap(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(center1.x, center1.y) - .tap(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() throws Exception { - 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(images.get(2), -10, center.y - location.y) - .waitAction(Duration.ofSeconds(2)).moveTo(gallery, 10, center.y - location.y).release(); - swipe.perform(); - assertNotEquals(originalImageCount, gallery - .findElementsByClassName("android.widget.ImageView").size()); - } - - @Test public void multiTouchTest() throws Exception { - Activity activity = new Activity("io.appium.android.apis", ".view.Buttons1"); - driver.startActivity(activity); - TouchAction press = new TouchAction(driver); - press.press(driver.findElementById("io.appium.android.apis:id/button_toggle")).waitAction(Duration.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/IntentTest.java b/src/test/java/io/appium/java_client/android/IntentTest.java deleted file mode 100644 index b32e5db6b..000000000 --- a/src/test/java/io/appium/java_client/android/IntentTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.appium.java_client.android; - -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.io.File; -import java.util.function.Predicate; - -public class IntentTest { - private static AppiumDriverLocalService service; - protected static AndroidDriver driver; - - /** - * initialization. - */ - @BeforeClass public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new RuntimeException("An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "IntentExample.apk"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - 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() throws Exception { - 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/KeyCodeTest.java b/src/test/java/io/appium/java_client/android/KeyCodeTest.java deleted file mode 100644 index a95a5b87a..000000000 --- a/src/test/java/io/appium/java_client/android/KeyCodeTest.java +++ /dev/null @@ -1,43 +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 org.junit.Before; -import org.junit.Test; - -public class KeyCodeTest extends BaseAndroidTest { - - @Before public void setup() throws Exception { - driver.resetApp(); - } - - @Test public void pressKeyCodeTest() { - driver.pressKeyCode(AndroidKeyCode.HOME); - } - - @Test public void pressKeyCodeWithMetastateTest() { - driver.pressKeyCode(AndroidKeyCode.SPACE, AndroidKeyMetastate.META_SHIFT_ON); - } - - @Test public void longPressKeyCodeTest() { - driver.longPressKeyCode(AndroidKeyCode.HOME); - } - - @Test public void longPressKeyCodeWithMetastateTest() { - driver.longPressKeyCode(AndroidKeyCode.HOME, AndroidKeyMetastate.META_SHIFT_ON); - } -} 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 a0aaaf2fc..000000000 --- a/src/test/java/io/appium/java_client/android/OpenNotificationsTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.appium.java_client.android; - -import static org.junit.Assert.assertNotEquals; - -import com.google.common.base.Function; - -import org.junit.Test; - -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.util.List; - -public class OpenNotificationsTest extends BaseAndroidTest { - @Test - public void openNotification() throws Exception { - driver.closeApp(); - driver.openNotifications(); - WebDriverWait wait = new WebDriverWait(driver, 20); - assertNotEquals(0, wait.until(new Function>() { - @Override - public List apply(WebDriver input) { - List result = driver - .findElementsById("com.android.systemui:id/carrier_label"); - - if (result.size() == 0) { - return null; - } - - return result; - } - }).size()); - } -} diff --git a/src/test/java/io/appium/java_client/android/SettingTest.java b/src/test/java/io/appium/java_client/android/SettingTest.java deleted file mode 100644 index 9680eafb2..000000000 --- a/src/test/java/io/appium/java_client/android/SettingTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.appium.java_client.android; - -import static org.junit.Assert.assertEquals; - -import io.appium.java_client.Setting; -import org.junit.Test; - -import java.time.Duration; - -public class SettingTest extends BaseAndroidTest { - - @Test public void ignoreUnimportantViewsTest() { - driver.ignoreUnimportantViews(true); - assertEquals(true, driver.getSettings() - .get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); - - driver.ignoreUnimportantViews(false); - assertEquals(false, driver.getSettings() - .get(Setting.IGNORE_UNIMPORTANT_VIEWS.toString())); - } - - @Test public void configuratorTest() { - driver.configuratorSetActionAcknowledgmentTimeout(Duration.ofMillis(500)); - assertJSONElementContains(Setting.WAIT_ACTION_ACKNOWLEDGMENT_TIMEOUT, 500); - - driver.configuratorSetKeyInjectionDelay(Duration.ofMillis(400)); - assertJSONElementContains(Setting.KEY_INJECTION_DELAY, 400); - - driver.configuratorSetScrollAcknowledgmentTimeout(Duration.ofMillis(300)); - assertJSONElementContains(Setting.WAIT_SCROLL_ACKNOWLEDGMENT_TIMEOUT, 300); - - driver.configuratorSetWaitForIdleTimeout(Duration.ofMillis(600)); - assertJSONElementContains(Setting.WAIT_FOR_IDLE_TIMEOUT, 600); - - driver.configuratorSetWaitForSelectorTimeout(Duration.ofSeconds(1)); - assertJSONElementContains(Setting.WAIT_FOR_SELECTOR_TIMEOUT, 1000); - } - - 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/test/java/io/appium/java_client/android/UIAutomator2Test.java deleted file mode 100644 index de480866e..000000000 --- a/src/test/java/io/appium/java_client/android/UIAutomator2Test.java +++ /dev/null @@ -1,111 +0,0 @@ -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 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.After; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.DeviceRotation; -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.concurrent.TimeUnit; - -public class UIAutomator2Test { - private static AppiumDriverLocalService service; - protected static AndroidDriver driver; - - /** - * initialization. - */ - @BeforeClass public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException( - "An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2); - driver = new AndroidDriver<>(service.getUrl(), capabilities); - driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); - } - - /** - * finishing. - */ - @AfterClass public static void afterClass() { - if (driver != null) { - driver.quit(); - } - if (service != null) { - service.stop(); - } - } - - - @After public void afterMethod() { - driver.rotate(new DeviceRotation(0, 0, 0)); - } - - @Test public void testLandscapeRightRotation() { - DeviceRotation landscapeRightRotation = new DeviceRotation(0, 0, 90); - driver.rotate(landscapeRightRotation); - assertEquals(driver.rotation(), landscapeRightRotation); - } - - @Test public void testLandscapeLeftRotation() { - DeviceRotation landscapeLeftRotation = new DeviceRotation(0, 0, 270); - driver.rotate(landscapeLeftRotation); - assertEquals(driver.rotation(), landscapeLeftRotation); - } - - @Test public void testPortraitUpsideDown() { - DeviceRotation landscapeRightRotation = new DeviceRotation(0, 0, 180); - driver.rotate(landscapeRightRotation); - assertEquals(driver.rotation(), landscapeRightRotation); - } - - @Test public void testToastMSGIsDisplayed() throws InterruptedException { - final WebDriverWait wait = new WebDriverWait(driver, 10); - Activity activity = new Activity("io.appium.android.apis", ".view.PopupMenu1"); - driver.startActivity(activity); - - MobileElement popUpElement = driver.findElement(MobileBy.AccessibilityId("Make a Popup!")); - popUpElement.click(); - driver.findElement(By.xpath(".//*[@text='Search']")).click(); - assertNotNull(wait.until(ExpectedConditions.presenceOfElementLocated( - By.xpath("//*[@text='Clicked popup menu item Search']")))); - - popUpElement.click(); - driver.findElement(By.xpath(".//*[@text='Add']")).click(); - assertNotNull(wait.until(ExpectedConditions - .presenceOfElementLocated(By.xpath("//*[@text='Clicked popup menu item Add']")))); - - popUpElement.click(); - driver.findElement(By.xpath(".//*[@text='Edit']")).click(); - assertNotNull(wait.until(ExpectedConditions - .presenceOfElementLocated(By.xpath("//*[@text='Clicked popup menu item Edit']")))); - - driver.findElement(By.xpath(".//*[@text='Share']")).click(); - assertNotNull(wait.until(ExpectedConditions - .presenceOfElementLocated(By.xpath("//*[@text='Clicked popup menu item Share']")))); - } -} 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 b73645df4..000000000 --- a/src/test/java/io/appium/java_client/appium/AndroidTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package io.appium.java_client.appium; - -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.io.File; -import java.util.Map; - -public class AndroidTest { - - private static AppiumDriverLocalService service; - private static AppiumDriver driver; - private StartsActivity startsActivity; - - /** - * initialization. - */ - @BeforeClass - public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException( - "An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - 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() throws Exception { - 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 dffc566e3..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 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.Duration; -import org.openqa.selenium.support.ui.SystemClock; -import org.openqa.selenium.support.ui.Wait; - -import java.util.concurrent.TimeUnit; -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, new SystemClock(), duration -> { - assertThat(duration.in(TimeUnit.SECONDS), is(equalTo(1L))); - Thread.sleep(duration.in(TimeUnit.MILLISECONDS)); - }).withPollingStrategy(AppiumFluentWait.IterationInfo::getInterval) - .withTimeout(3, TimeUnit.SECONDS) - .pollingEvery(1, TimeUnit.SECONDS); - 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, new SystemClock(), duration -> { - callsCounter.incrementAndGet(); - assertThat(duration.in(TimeUnit.SECONDS), is(equalTo(2L))); - Thread.sleep(duration.in(TimeUnit.MILLISECONDS)); - }).withPollingStrategy(info -> new Duration(2, TimeUnit.SECONDS)) - .withTimeout(3, TimeUnit.SECONDS) - .pollingEvery(1, TimeUnit.SECONDS); - 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, new SystemClock(), duration -> { - int callNumber = callsCounter.incrementAndGet(); - assertThat(duration.in(TimeUnit.SECONDS), is(equalTo(pollingStrategy.apply((long) callNumber)))); - Thread.sleep(duration.in(TimeUnit.MILLISECONDS)); - }).withPollingStrategy(info -> new Duration(pollingStrategy.apply(info.getNumber()), TimeUnit.SECONDS)) - .withTimeout(4, TimeUnit.SECONDS) - .pollingEvery(1, TimeUnit.SECONDS); - 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/IOSTest.java b/src/test/java/io/appium/java_client/appium/IOSTest.java deleted file mode 100644 index 8469b6bd0..000000000 --- a/src/test/java/io/appium/java_client/appium/IOSTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.appium.java_client.appium; - -import static org.junit.Assert.assertNotEquals; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.MobileBy; -import io.appium.java_client.ios.IOSElement; -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.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; - -public class IOSTest { - private static AppiumDriverLocalService service; - private static AppiumDriver driver; - - /** - * initialization. - */ - @BeforeClass - public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "TestApp.app.zip"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); - 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); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - driver = new AppiumDriver<>(service.getUrl(), capabilities); - } - - /** - * finishing. - */ - @AfterClass - public static void afterClass() { - if (driver != null) { - driver.quit(); - } - if (service != null) { - service.stop(); - } - } - - @Test - public void findByAccessibilityIdFromDriverTest() { - assertNotEquals(driver - .findElementByAccessibilityId("ComputeSumButton") - .getText(), null); - assertNotEquals(driver - .findElementsByAccessibilityId("ComputeSumButton") - .size(), 0); - } - - @Test public void findByByIosUIAutomationFromDriverTest() { - assertNotEquals(driver - .findElement(MobileBy.IosUIAutomation(".elements().withName(\"Answer\")")) - .getText(), null); - assertNotEquals(driver - .findElements(MobileBy.IosUIAutomation(".elements().withName(\"Answer\")")) - .size(), 0); - } - - @Test public void findByAccessibilityIdFromElementTest() { - assertNotEquals(driver.findElementsByClassName("UIAWindow").get(1) - .findElementsByAccessibilityId("ComputeSumButton").size(), 0); - } - - @Test public void findByByIosUIAutomationTest() { - assertNotEquals((driver.findElementsByClassName("UIAWindow") - .get(1)) - .findElementByIosUIAutomation(".elements().withName(\"Answer\")").getText(), null); - } -} 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 0e4bb6a7f..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.assertTrue; - -import io.appium.java_client.AppiumDriver; -import io.appium.java_client.service.local.AppiumServiceBuilder; -import org.junit.After; -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 BaseElementGenerationTest { - protected AppiumDriver driver; - protected final BiPredicate> commonPredicate = (by, aClass) -> { - WebElement element = driver.findElement(by); - assertTrue(element.getClass().equals(aClass)); - return true; - }; - - @After - public void tearDown() { - if (driver != null) { - driver.quit(); - } - driver = null; - } - - protected boolean check(Supplier serverCapabilitiesSupplier, - Supplier clientCapabilitiesSupplier, - BiPredicate> filter, - By by, Class clazz) { - AppiumServiceBuilder builder = new AppiumServiceBuilder() - .withCapabilities(serverCapabilitiesSupplier.get()); - driver = new AppiumDriver<>(builder, clientCapabilitiesSupplier.get()); - return filter.test(by, clazz); - } - -} 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 1e8223af3..000000000 --- a/src/test/java/io/appium/java_client/appium/element/generation/android/AndroidElementGeneratingTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.appium.java_client.appium.element.generation.android; - -import static io.appium.java_client.MobileBy.AndroidUIAutomator; -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.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 org.junit.Test; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; -import java.util.function.Supplier; - -public class AndroidElementGeneratingTest extends BaseElementGenerationTest { - - private final File app = new File(new File("src/test/java/io/appium/java_client"), - "ApiDemos-debug.apk"); - private final Supplier serverCapabilitiesSupplier = () -> { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - serverCapabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - return serverCapabilities; - }; - - @Test public void whenAndroidNativeAppIsLaunched() { - assertTrue(check(serverCapabilitiesSupplier, () -> { - DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - clientCapabilities.setCapability(MobileCapabilityType.FULL_RESET, true); - clientCapabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 60); - return clientCapabilities; - }, commonPredicate, AndroidUIAutomator("new UiSelector().clickable(true)"), - AndroidElement.class)); - } - - @Test public void whenAndroidHybridAppIsLaunched() { - assertTrue(check(serverCapabilitiesSupplier, () -> { - DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - 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"), AndroidElement.class)); - } - - @Test public void whenAndroidBrowserIsLaunched() { - assertTrue(check(() -> { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); - serverCapabilities.setCapability(MobileCapabilityType.BROWSER_NAME, MobileBrowserType.BROWSER); - return serverCapabilities; - }, () -> { - DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - clientCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - return clientCapabilities; - }, (by, aClass) -> { - driver.get("https://www.google.com"); - return commonPredicate.test(by, aClass); - }, name("q"), 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 2cbf3ba4a..000000000 --- a/src/test/java/io/appium/java_client/appium/element/generation/ios/IOSElementGenerationTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package io.appium.java_client.appium.element.generation.ios; - -import static io.appium.java_client.MobileBy.IosUIAutomation; -import static org.junit.Assert.assertTrue; -import static org.openqa.selenium.By.id; -import static org.openqa.selenium.By.name; - -import io.appium.java_client.appium.element.generation.BaseElementGenerationTest; -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 org.junit.Test; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; -import java.util.function.Function; -import java.util.function.Supplier; - -public class IOSElementGenerationTest extends BaseElementGenerationTest { - - private final File testApp = new File(new File("src/test/java/io/appium/java_client"), - "TestApp.app.zip"); - - private final File webViewApp = new File(new File("src/test/java/io/appium/java_client"), - "WebViewApp.app.zip"); - - private Supplier serverAppCapabilitiesSupplier = () -> { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - serverCapabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, - 500000); //some environment is too slow - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "9.2"); - 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, "9.2"); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - //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(serverAppCapabilitiesSupplier, - appFileSupplierFunction.apply(testApp), - commonPredicate, - IosUIAutomation(".elements().withName(\"Answer\")"), - IOSElement.class)); - } - - @Test public void whenIOSHybridAppIsLaunched() throws Exception { - assertTrue(check(serverAppCapabilitiesSupplier, - appFileSupplierFunction.apply(webViewApp), - (by, aClass) -> { - IOSElement element1 = (IOSElement) driver.findElementByXPath("//UIATextField[@value='Enter URL']"); - element1.sendKeys("www.google.com"); - driver.findElementByClassName("UIAButton").click(); - 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); - }, name("q"), IOSElement.class)); - } - - @Test public void whenIOSBrowserIsLaunched() { - assertTrue(check(serverBrowserCapabilitiesSupplier, - clientBrowserCapabilitiesSupplier, (by, aClass) -> { - driver.get("https://www.google.com"); - return commonPredicate.test(by, aClass); - }, name("q"), IOSElement.class)); - } - - @Test - public void whenIOSNativeAppIsLaunched2() { - assertTrue(check(() -> { - DesiredCapabilities serverCapabilities = serverAppCapabilitiesSupplier.get(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "10.1"); - return serverCapabilities; - }, appFileSupplierFunction.apply(testApp), commonPredicate, id("IntegerA"), IOSElement.class)); - } - - @Test public void whenIOSBrowserIsLaunched2() { - assertTrue(check(() -> { - DesiredCapabilities serverCapabilities = serverBrowserCapabilitiesSupplier.get(); - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "10.1"); - return serverCapabilities; - }, clientBrowserCapabilitiesSupplier, (by, aClass) -> { - driver.get("https://www.google.com"); - return commonPredicate.test(by, aClass); - }, name("q"), 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 452b67c2f..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() throws Exception { - 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 99f11f139..000000000 --- a/src/test/java/io/appium/java_client/events/BaseListenerTest.java +++ /dev/null @@ -1,326 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsCollectionContaining.hasItems; -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.OutputType; -import org.openqa.selenium.Point; -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.security.Credentials; - -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, - hasItems(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, - hasItems(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, - hasItems(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.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, - hasItems(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.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.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")); - - assertThat(listener.messages.size(), is(12)); - 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")); - els.get(0).findElementByAccessibilityId("SomeAccessibility") - .findElement(MobileBy.IosUIAutomation("iOS UI Automation")); - - assertThat(listener.messages, - hasItems(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", - prefix + "Attempt to find something using By.IosUIAutomation: iOS UI Automation. " - + "The root element is io.appium.java_client.events.StubWebElement", - prefix + "The searching for something using By.IosUIAutomation: iOS UI Automation " - + "has beed finished. " - + "The root element was io.appium.java_client.events.StubWebElement")); - assertThat(listener.messages.size(), is(4)); - 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, - hasItems(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")); - assertThat(listener.messages.size(), is(12)); - 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, - hasItems(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")); - assertThat(listener.messages.size(), is(4)); - 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, - hasItems(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")); - assertThat(listener.messages.size(), is(4)); - return true; - } finally { - listener.messages.clear(); - } - } - - protected boolean assertThatExceptionListenerWorks(EmptyWebDriver driver, TestListener listener, String prefix) { - try { - try { - driver.getWindowHandle(); - } catch (Exception ignored) { - ignored.printStackTrace(); - } - - try { - driver.findElementByXPath("Some Path").getScreenshotAs(OutputType.BASE64); - } catch (Exception ignored) { - ignored.printStackTrace(); - } - - assertThat(listener.messages, - hasItems(prefix + "The exception was thrown: " - + WebDriverException.class, - prefix + "The exception was thrown: " - + WebDriverException.class)); - assertThat(listener.messages.size(), is(2)); - 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"); - Credentials credentials = new Credentials() { - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - return "Test credentials 1"; - } - }; - - Credentials credentials2 = new Credentials() { - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - return "Test credentials 2"; - } - }; - - alert.setCredentials(credentials); - alert.authenticateUsing(credentials2); - - assertThat(listener.messages, - hasItems(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", - prefix + "Attempt to send credentials " + credentials.toString() + " to alert", - prefix + "Credentials " + credentials.toString() + " were sent to alert", - prefix + "Attempt to send credentials " + credentials2.toString() + " to alert", - prefix + "Credentials " + credentials2.toString() + " were sent to alert")); - assertThat(listener.messages.size(), is(10)); - 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, - hasItems(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")); - assertThat(listener.messages.size(), is(4)); - 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, - hasItems(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())); - assertThat(listener.messages.size(), is(4)); - 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(); - - assertThat(listener.messages, - hasItems(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")); - assertThat(listener.messages.size(), is(6)); - 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 a2dfc33ae..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() throws Exception { - 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 3caa873e3..000000000 --- a/src/test/java/io/appium/java_client/events/EmptyWebDriver.java +++ /dev/null @@ -1,340 +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 io.appium.java_client.FindsByIosUIAutomation; - -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.Rotatable; -import org.openqa.selenium.ScreenOrientation; -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, - FindsByIosUIAutomation, JavascriptExecutor, HasCapabilities, FindsByFluentSelector { - - 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() { - return null; - } - - @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 StubWebElement findElementByIosUIAutomation(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByIosUIAutomation(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); - } - - 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 d1e9af493..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.IsCollectionContaining.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() throws Exception { - 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 d4bd7cf52..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() throws Exception { - 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 497e6270e..000000000 --- a/src/test/java/io/appium/java_client/events/ListenableObjectTest.java +++ /dev/null @@ -1,175 +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.core.Is.is; -import static org.hamcrest.core.IsCollectionContaining.hasItems; -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 org.openqa.selenium.security.Credentials; - -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 Credentials credentials = new Credentials() { - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - return "Test credentials 1"; - } - }; - - private final Credentials credentials2 = new Credentials() { - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - return "Test credentials 2"; - } - }; - - private final Predicate contextAwarePredicate = (contextAware) -> { - contextAware.context("WEB_VIEW"); - - assertThat(contextListener.messages, - hasItems(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")); - assertThat(contextListener.messages.size(), is(4)); - - ContextListener singleContextListener = (ContextListener) - listeners.get(ContextListener.class); - assertThat(singleContextListener.messages, - hasItems("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")); - assertThat(singleContextListener.messages.size(), is(4)); - return true; - }; - - private final Predicate alertPredicate = alert -> { - alert.accept(); - alert.dismiss(); - alert.sendKeys("Keys"); - - alert.setCredentials(credentials); - alert.authenticateUsing(credentials2); - - assertThat(alertListener.messages, - hasItems(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", - PREFIX + "Attempt to send credentials " + credentials.toString() + " to alert", - PREFIX + "Credentials " + credentials.toString() + " were sent to alert", - PREFIX + "Attempt to send credentials " + credentials2.toString() + " to alert", - PREFIX + "Credentials " + credentials2.toString() + " were sent to alert")); - assertThat(alertListener.messages.size(), is(10)); - - AlertListener singleAlertListener = (AlertListener) - listeners.get(AlertListener.class); - - assertThat(singleAlertListener.messages, - hasItems("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", - "Attempt to send credentials " + credentials.toString() + " to alert", - "Credentials " + credentials.toString() + " were sent to alert", - "Attempt to send credentials " + credentials2.toString() + " to alert", - "Credentials " + credentials2.toString() + " were sent to alert")); - assertThat(singleAlertListener.messages.size(), is(10)); - return true; - }; - - private final Predicate webDriverPredicate = driver -> { - driver.findElement(By.id("someId")); - assertThat(searchingListener.messages, - hasItems(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")); - assertThat(searchingListener.messages.size(), is(2)); - - SearchingListener singleSearchingListener = (SearchingListener) - listeners.get(SearchingListener.class); - - assertThat(singleSearchingListener.messages, - hasItems("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")); - assertThat(singleSearchingListener.messages.size(), is(2)); - 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 b3b20fa42..000000000 --- a/src/test/java/io/appium/java_client/events/StubAlert.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.appium.java_client.events; - -import org.apache.commons.lang3.StringUtils; -import org.openqa.selenium.Alert; -import org.openqa.selenium.security.Credentials; - -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 - } - - @Override public void setCredentials(Credentials credentials) { - //STUB it does nothing - } - - @Override public void authenticateUsing(Credentials credentials) { - //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 acc50837d..000000000 --- a/src/test/java/io/appium/java_client/events/StubWebElement.java +++ /dev/null @@ -1,199 +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 io.appium.java_client.FindsByIosUIAutomation; -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, FindsByIosUIAutomation, FindsByFluentSelector { - - private static List createStubSubElementList() { - List result = new ArrayList<>(); - result.addAll(ImmutableList.of(new StubWebElement(), new StubWebElement())); - return result; - } - - - @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 findElementByIosUIAutomation(String using) { - return new StubWebElement(); - } - - @Override public List findElementsByIosUIAutomation(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() { - return null; - } - - @Override public String getCssValue(String propertyName) { - return null; - } - - @Override public X getScreenshotAs(OutputType target) throws WebDriverException { - throw new WebDriverException(); - } - - @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 acc3563a8..000000000 --- a/src/test/java/io/appium/java_client/events/WebDriverEventListenerCompatibilityTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.appium.java_client.events; - -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsCollectionContaining.hasItems; -import static org.junit.Assert.assertThat; - -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; -import org.openqa.selenium.security.Credentials; - -@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() throws Exception { - 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"); - Credentials credentials = new Credentials() { - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - return "Test credentials 1"; - } - }; - - Credentials credentials2 = new Credentials() { - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - return "Test credentials 2"; - } - }; - - alert.setCredentials(credentials); - alert.authenticateUsing(credentials2); - - 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")); - assertThat(listener.messages.size(), is(4)); - } finally { - listener.messages.clear(); - } - } - - @Test - public void exceptionEventTest() { - assertThat(super.assertThatExceptionListenerWorks(driver, listener, WEBDRIVER_EVENT_LISTENER), - is(true)); - } -} 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 3fc61782d..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/AlertListener.java +++ /dev/null @@ -1,46 +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; -import org.openqa.selenium.security.Credentials; - -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 - public void beforeAuthentication(WebDriver driver, Alert alert, Credentials credentials) { - messages.add("Attempt to send credentials " + credentials.toString() + " to alert"); - } - - @Override - public void afterAuthentication(WebDriver driver, Alert alert, Credentials credentials) { - messages.add("Credentials " + credentials.toString() + " 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 abeff2d80..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/AlertListener2.java +++ /dev/null @@ -1,48 +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; -import org.openqa.selenium.security.Credentials; - -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 - public void beforeAuthentication(WebDriver driver, Alert alert, Credentials credentials) { - messages.add("Externally defined listener: Attempt to send credentials " - + credentials.toString() + " to alert"); - } - - @Override - public void afterAuthentication(WebDriver driver, Alert alert, Credentials credentials) { - messages.add("Externally defined listener: Credentials " + credentials.toString() - + " 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 89740977b..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/AppiumListener.java +++ /dev/null @@ -1,115 +0,0 @@ -package io.appium.java_client.events.listeners; - -import io.appium.java_client.events.api.general.AppiumWebDriverEventListener; -import org.openqa.selenium.By; -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()); - } -} 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 c33caf990..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ElementListener.java +++ /dev/null @@ -1,38 +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 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 6fb5cc55c..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/ElementListener2.java +++ /dev/null @@ -1,38 +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 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 193e5b5fd..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/WindowListener.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.appium.java_client.events.listeners; - -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"); - } -} 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 3f0c802fd..000000000 --- a/src/test/java/io/appium/java_client/events/listeners/WindowListener2.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.appium.java_client.events.listeners; - -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"); - } -} 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/android/Connection.java b/src/test/java/io/appium/java_client/events/stubs/StubAlert.java similarity index 68% rename from src/main/java/io/appium/java_client/android/Connection.java rename to src/test/java/io/appium/java_client/events/stubs/StubAlert.java index 7c22a592d..c89ec4bb7 100644 --- a/src/main/java/io/appium/java_client/android/Connection.java +++ b/src/test/java/io/appium/java_client/events/stubs/StubAlert.java @@ -14,21 +14,24 @@ * limitations under the License. */ -package io.appium.java_client.android; +package io.appium.java_client.events.stubs; -/** - * for use with setting Network Connections on a mobile device. - */ -public enum Connection { - NONE(0), - AIRPLANE(1), - WIFI(2), - DATA(4), - ALL(6); +import org.openqa.selenium.Alert; + +public class StubAlert implements Alert { + public StubAlert() { + } + + public void dismiss() { + } + + public void accept() { + } - final int bitMask; + public String getText() { + return ""; + } - Connection(int bitMask) { - this.bitMask = bitMask; + 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 new file mode 100644 index 000000000..f509a4d23 --- /dev/null +++ b/src/test/java/io/appium/java_client/internal/ConfigTest.java @@ -0,0 +1,41 @@ +package io.appium.java_client.internal; + +import io.appium.java_client.internal.filters.AppiumUserAgentFilter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConfigTest { + private static final String SELENIUM_EXISTING_KEY = "selenium.version"; + + private static final String MISSING_KEY = "bla"; + + @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 + void verifyGettingNonExistingValue() { + assertThrows(IllegalArgumentException.class, () -> Config.main().getValue(MISSING_KEY, String.class)); + } + + @ParameterizedTest + @ValueSource(strings = {SELENIUM_EXISTING_KEY, AppiumUserAgentFilter.VERSION_KEY}) + void verifyGettingExistingValueWithWrongClass(String key) { + assertThrows(ClassCastException.class, () -> Config.main().getValue(key, Integer.class)); + } + + @Test + 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/FindsByIosNSPredicate.java b/src/test/java/io/appium/java_client/internal/SessionConnectTest.java similarity index 50% rename from src/main/java/io/appium/java_client/FindsByIosNSPredicate.java rename to src/test/java/io/appium/java_client/internal/SessionConnectTest.java index 84bc3ff67..a97653882 100644 --- a/src/main/java/io/appium/java_client/FindsByIosNSPredicate.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 FindsByIosNSPredicate extends FindsByFluentSelector { +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; - default T findElementByIosNsPredicate(String using) { - return findElement(MobileSelector.IOS_PREDICATE_STRING.toString(), using); - } +public class SessionConnectTest { - default List findElementsByIosNsPredicate(String using) { - return findElements(MobileSelector.IOS_PREDICATE_STRING.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 e6bda042f..000000000 --- a/src/test/java/io/appium/java_client/ios/AppIOSTest.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.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; - -public class AppIOSTest extends BaseIOSTest { - - @BeforeClass - public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "TestApp.app.zip"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - 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); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - driver = new IOSDriver<>(service.getUrl(), capabilities); - } -} diff --git a/src/test/java/io/appium/java_client/ios/AppXCUITTest.java b/src/test/java/io/appium/java_client/ios/AppXCUITTest.java deleted file mode 100644 index efbb81210..000000000 --- a/src/test/java/io/appium/java_client/ios/AppXCUITTest.java +++ /dev/null @@ -1,40 +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.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; - -public class AppXCUITTest extends BaseIOSTest { - - /** - * initialization. - */ - @BeforeClass public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "TestApp.app.zip"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "10.1"); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone 6"); - capabilities.setCapability(IOSMobileCapabilityType.ALLOW_TOUCHID_ENROLL, "true"); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities - .setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.IOS_XCUI_TEST); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - driver = new IOSDriver<>(service.getUrl(), capabilities); - } -} diff --git a/src/test/java/io/appium/java_client/ios/BaseIOSWebViewTest.java b/src/test/java/io/appium/java_client/ios/BaseIOSWebViewTest.java deleted file mode 100644 index c83262c71..000000000 --- a/src/test/java/io/appium/java_client/ios/BaseIOSWebViewTest.java +++ /dev/null @@ -1,58 +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.IOSMobileCapabilityType; -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.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; - -public class BaseIOSWebViewTest extends BaseIOSTest { - - @BeforeClass public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException("An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "WebViewApp.app.zip"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "9.2"); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - driver = new IOSDriver<>(service.getUrl(), capabilities); - } - - protected void findAndSwitchToWebView() throws InterruptedException { - Thread.sleep(10000); - driver.getContextHandles().forEach((handle) -> { - if (handle.contains("WEBVIEW")) { - driver.context(handle); - } - }); - } -} 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 bca6468a3..000000000 --- a/src/test/java/io/appium/java_client/ios/BaseSafariTest.java +++ /dev/null @@ -1,46 +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.MobileCapabilityType; -import io.appium.java_client.service.local.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -public class BaseSafariTest extends BaseIOSTest { - - @BeforeClass public static void beforeClass() throws Exception { - 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.BROWSER_NAME, "Safari"); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.IOS_XCUI_TEST); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "10.2"); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - driver = new IOSDriver<>(service.getUrl(), capabilities); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSAlertTest.java b/src/test/java/io/appium/java_client/ios/IOSAlertTest.java deleted file mode 100644 index 830a68a3a..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSAlertTest.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.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.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.util.function.Supplier; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class IOSAlertTest extends AppIOSTest { - - private WebDriverWait waiting = new WebDriverWait(driver, 10000); - private static final String iOSAutomationText = ".elements().withName(\"show alert\")"; - - @Test public void acceptAlertTest() { - Supplier acceptAlert = () -> { - driver.findElement(MobileBy - .IosUIAutomation(iOSAutomationText)).click(); - waiting.until(alertIsPresent()); - driver.switchTo().alert().accept(); - return true; - }; - assertTrue(acceptAlert.get()); - } - - @Test public void dismissAlertTest() { - Supplier dismissAlert = () -> { - driver.findElement(MobileBy - .IosUIAutomation(iOSAutomationText)).click(); - waiting.until(alertIsPresent()); - driver.switchTo().alert().dismiss(); - return true; - }; - assertTrue(dismissAlert.get()); - } - - @Test public void getAlertTextTest() { - driver.findElement(MobileBy - .IosUIAutomation(iOSAutomationText)).click(); - waiting.until(alertIsPresent()); - assertFalse(StringUtils.isBlank(driver.switchTo().alert().getText())); - } -} 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 e768f227e..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSDriverTest.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 org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.MobileElement; -import io.appium.java_client.remote.HideKeyboardStrategy; - -import org.junit.Test; - -import org.openqa.selenium.ScreenOrientation; -import org.openqa.selenium.html5.Location; - -import java.time.Duration; -import java.util.function.Supplier; - -public class IOSDriverTest extends AppIOSTest { - - //TODO There is no ability to check this function usibg simulators. - // When CI will have been set up then this test will be returned - public void getDeviceTimeTest() { - String time = driver.getDeviceTime(); - assertTrue(time.length() == 28); - } - - @Test public void resetTest() { - driver.resetApp(); - } - - @Test public void hideKeyboardWithParametersTest() { - MobileElement element = driver.findElementById("IntegerA"); - element.click(); - driver.hideKeyboard(HideKeyboardStrategy.PRESS_KEY, "Done"); - } - - @Test public void geolocationTest() { - Location location = new Location(45, 45, 100); - driver.setLocation(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() { - Supplier lock = () -> { - driver.lockDevice(Duration.ofSeconds(20)); - return true; - }; - assertTrue(lock.get()); - } - - @Test public void pullFileTest() { - byte[] data = driver.pullFile("Library/AddressBook/AddressBook.sqlitedb"); - assert (data.length > 0); - } -} 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 b3305d720..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSElementTest.java +++ /dev/null @@ -1,48 +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 com.google.common.base.Function; - -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.util.List; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class IOSElementTest extends UICatalogIOSTest { - - @Test public void findByAccessibilityIdTest() { - assertThat((driver.findElementsByClassName("UIAWindow").get(1)) - .findElementsByAccessibilityId("Sliders").size(), - not(is(0))); - } - - @Test public void setValueTest() { - driver.findElementsByClassName("UIAWindow").get(1) - .findElementByAccessibilityId("Sliders").click(); - - WebDriverWait wait = new WebDriverWait(driver, 20); - - IOSElement slider = (IOSElement) wait.until(new Function>() { - @Override - public List apply(WebDriver input) { - List result = input.findElements(By.className("UIASlider")); - if (result.size() == 0) { - return null; - } - return result; - } - }).get(1); - slider.setValue("0%"); - assertEquals("0%", slider.getAttribute("value")); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java b/src/test/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java deleted file mode 100644 index 1369d3d5b..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSNativeWebTapSettingTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.appium.java_client.ios; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; -import org.openqa.selenium.WebElement; - -public class IOSNativeWebTapSettingTest extends BaseSafariTest { - - @Test public void nativeWebTapSettingTest() throws InterruptedException { - 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"); - el.click(); - assertEquals(true, driver.getTitle().contains("I am another page title")); - driver.navigate().back(); - - // now do a click with it turned off and assert the same behavior - assertEquals(true, driver.getTitle().contains("I am a page title")); - driver.nativeWebTap(false); - el = driver.findElementById("i am a link"); - el.click(); - assertEquals(true, driver.getTitle().contains("I am another page title")); - } -} diff --git a/src/test/java/io/appium/java_client/ios/IOSScrollingSearchingTest.java b/src/test/java/io/appium/java_client/ios/IOSScrollingSearchingTest.java deleted file mode 100644 index c17f4b854..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSScrollingSearchingTest.java +++ /dev/null @@ -1,42 +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.assertEquals; - -import io.appium.java_client.MobileBy; -import io.appium.java_client.MobileElement; -import org.junit.Test; - -public class IOSScrollingSearchingTest extends UICatalogIOSTest { - - @Test public void scrollByDriver() { - MobileElement slider = driver - .findElement(MobileBy - .IosUIAutomation(".tableViews()[0]" - + ".scrollToElementWithPredicate(\"name CONTAINS 'Slider'\")")); - assertEquals(slider.getAttribute("name"), "Sliders"); - } - - @Test public void scrollByElement() { - MobileElement table = driver.findElement(MobileBy - .IosUIAutomation(".tableViews()[0]")); - MobileElement slider = table.findElement(MobileBy - .IosUIAutomation(".scrollToElementWithPredicate(\"name CONTAINS 'Slider'\")")); - assertEquals(slider.getAttribute("name"), "Sliders"); - } -} 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 ddd5b8e8c..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSTouchTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.appium.java_client.ios; - -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.MobileBy; -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.Dimension; -import org.openqa.selenium.support.ui.WebDriverWait; - -import java.time.Duration; - -@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 TouchAction(driver).tap(e).perform(); - assertEquals(driver.findElementByXPath("//*[@name = \"Answer\"]").getText(), "6"); - } - - @Test public void swipeTest() { - MobileElement slider = driver.findElementByClassName("UIASlider"); - Dimension size = slider.getSize(); - - TouchAction swipe = new TouchAction(driver).press(slider, size.width / 2 + 2, size.height / 2) - .waitAction(Duration.ofSeconds(2)).moveTo(slider, 1, size.height / 2).release(); - swipe.perform(); - assertEquals("0%", slider.getAttribute("value")); - } - - @Test public void multiTouchTest() { - MobileElement e = driver.findElementByAccessibilityId("ComputeSumButton"); - TouchAction tap1 = new TouchAction(driver).tap(e); - TouchAction tap2 = new TouchAction(driver).tap(driver.findElement(MobileBy - .IosUIAutomation(".elements().withName(\"show alert\")"))); - - new MultiTouchAction(driver).add(tap1).add(tap2).perform(); - - WebDriverWait waiting = new WebDriverWait(driver, 10000); - assertNotNull(waiting.until(alertIsPresent())); - driver.switchTo().alert().accept(); - } -} 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 ddacf8d3f..000000000 --- a/src/test/java/io/appium/java_client/ios/IOSWebViewTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.appium.java_client.ios; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; -import org.openqa.selenium.Keys; -import org.openqa.selenium.WebElement; - -public class IOSWebViewTest extends BaseIOSWebViewTest { - - @Test public void webViewPageTestCase() throws InterruptedException { - driver.findElementByXPath("//UIATextField[@value='Enter URL']") - .sendKeys("www.google.com"); - driver.findElementByClassName("UIAButton").click(); - findAndSwitchToWebView(); - WebElement el = driver.findElementByClassName("gsfi"); - el.sendKeys("Appium"); - el.sendKeys(Keys.ENTER); - Thread.sleep(1000); - assertEquals(true, driver.getTitle().contains("Appium")); - } -} 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 29962fc0f..000000000 --- a/src/test/java/io/appium/java_client/ios/UICatalogIOSTest.java +++ /dev/null @@ -1,35 +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.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException; -import org.junit.BeforeClass; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; - -public class UICatalogIOSTest extends BaseIOSTest { - - @BeforeClass - public static void beforeClass() throws Exception { - service = AppiumDriverLocalService.buildDefaultService(); - service.start(); - - if (service == null || !service.isRunning()) { - throw new AppiumServerHasNotBeenStartedLocallyException( - "An appium server node is not started!"); - } - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "UICatalog.app.zip"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); - 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); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - driver = new IOSDriver<>(service.getUrl(), capabilities); - } -} diff --git a/src/test/java/io/appium/java_client/ios/XCUIAutomationTest.java b/src/test/java/io/appium/java_client/ios/XCUIAutomationTest.java deleted file mode 100644 index f4b2c99e0..000000000 --- a/src/test/java/io/appium/java_client/ios/XCUIAutomationTest.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.ios; - -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; - -import org.junit.After; -import org.junit.Test; -import org.openqa.selenium.DeviceRotation; - -import java.time.Duration; - -public class XCUIAutomationTest extends AppXCUITTest { - - @After public void afterMethod() { - driver.rotate(new DeviceRotation(0, 0, 0)); - } - - @Test public void testLandscapeRightRotation() { - DeviceRotation landscapeRightRotation = new DeviceRotation(0, 0, 90); - driver.rotate(landscapeRightRotation); - assertEquals(driver.rotation(), landscapeRightRotation); - } - - @Test public void testLandscapeLeftRotation() { - DeviceRotation landscapeLeftRotation = new DeviceRotation(0, 0, 270); - driver.rotate(landscapeLeftRotation); - assertEquals(driver.rotation(), landscapeLeftRotation); - } - - @Test public void testTouchId() { - try { - driver.toggleTouchIDEnrollment(); - driver.performTouchID(true); - driver.performTouchID(false); - assertEquals(true, true); - } catch (Exception e) { - throw e; - } - } - - @Test public void testPutIntoBackgroundAndRestore() { - final long msStarted = System.currentTimeMillis(); - driver.runAppInBackground(Duration.ofSeconds(4)); - assertThat(System.currentTimeMillis() - msStarted, greaterThan(3000L)); - } - - @Test public void testPutIntoBackgroundWithoutRestore() { - assertThat(driver.findElementsById("IntegerA"), is(not(empty()))); - driver.runAppInBackground(Duration.ofSeconds(-1)); - assertThat(driver.findElementsById("IntegerA"), is(empty())); - } - - @Test public void doubleTapTest() { - IOSElement firstField = driver.findElementById("IntegerA"); - firstField.sendKeys("2"); - - IOSTouchAction iosTouchAction = new IOSTouchAction(driver); - iosTouchAction.doubleTap(firstField); - IOSElement editingMenu = driver.findElementByClassName("UIAEditingMenu"); - assertNotNull(editingMenu); - } -} diff --git a/src/test/java/io/appium/java_client/localserver/ServerBuilderTest.java b/src/test/java/io/appium/java_client/localserver/ServerBuilderTest.java deleted file mode 100644 index a3e8ec945..000000000 --- a/src/test/java/io/appium/java_client/localserver/ServerBuilderTest.java +++ /dev/null @@ -1,311 +0,0 @@ -package io.appium.java_client.localserver; - -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 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.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 java.lang.System.setProperty; -import static java.util.Arrays.asList; -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.service.local.AppiumDriverLocalService; -import io.appium.java_client.service.local.AppiumServiceBuilder; -import org.apache.commons.validator.routines.InetAddressValidator; -import org.junit.After; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.Platform; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.util.Enumeration; -import java.util.List; -import java.util.Properties; - -public class ServerBuilderTest { - - private static Properties properties; - private static String testIP; - private AppiumDriverLocalService service; - private File file; - private OutputStream stream; - - - /** - * initialization. - */ - @BeforeClass public static void beforeClass() throws Exception { - File file = - new File("src/test/java/io/appium/java_client/localserver/custom_node_path.properties"); - FileInputStream fileInput = new FileInputStream(file); - properties = new Properties(); - properties.load(fileInput); - fileInput.close(); - - for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en - .hasMoreElements(); ) { - NetworkInterface intf = en.nextElement(); - for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr - .hasMoreElements(); ) { - InetAddress inetAddress = enumIpAddr.nextElement(); - if (!inetAddress.isLoopbackAddress()) { - InetAddressValidator validator = InetAddressValidator.getInstance(); - testIP = inetAddress.getHostAddress().toString(); - if (validator.isValid(testIP)) { - break; - } - testIP = null; - } - } - if (testIP != null) { - break; - } - } - } - - @After - public void tearDown() throws Exception { - if (service != null) { - service.stop(); - } - if (stream != null) { - stream.close(); - } - if (file != null && file.exists()) { - file.delete(); - } - System.clearProperty(APPIUM_PATH); - } - - private static File findCustomNode() { - Platform current = Platform.getCurrent(); - if (current.is(Platform.WINDOWS)) { - return new File(String.valueOf(properties.get("path.to.custom.node.win"))); - } - - if (current.is(Platform.MAC)) { - return new File(String.valueOf(properties.get("path.to.custom.node.macos"))); - } - - return new File(String.valueOf(properties.get("path.to.custom.node.linux"))); - } - - @Test public void checkAbilityToStartDefaultService() { - service = buildDefaultService(); - service.start(); - assertEquals(true, service.isRunning()); - } - - @Test public void checkAbilityToStartServiceUsingNodeDefinedInProperties() { - String definedNode = findCustomNode().getAbsolutePath(); - setProperty(APPIUM_PATH, definedNode); - service = buildDefaultService(); - service.start(); - assertTrue(service.isRunning()); - } - - @Test public void checkAbilityToStartServiceUsingNodeDefinedExplicitly() { - File node = findCustomNode(); - service = new AppiumServiceBuilder().withAppiumJS(node).build(); - service.start(); - assertTrue(service.isRunning()); - } - - @Test public void checkAbilityToStartServiceOnAFreePort() { - service = new AppiumServiceBuilder().usingAnyFreePort().build(); - service.start(); - assertTrue(service.isRunning()); - } - - @Test public void checkAbilityToStartServiceUsingNonLocalhostIP() throws Exception { - service = new AppiumServiceBuilder().withIPAddress(testIP).build(); - service.start(); - assertTrue(service.isRunning()); - } - - @Test public void checkAbilityToStartServiceUsingFlags() throws Exception { - service = new AppiumServiceBuilder() - .withArgument(CALLBACK_ADDRESS, testIP) - .withArgument(SESSION_OVERRIDE) - .withArgument(PRE_LAUNCH) - .build(); - service.start(); - assertTrue(service.isRunning()); - } - - @Test public void checkAbilityToStartServiceUsingCapabilities() throws Exception { - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - File pageFactoryDir = new File("src/test/java/io/appium/java_client/pagefactory_tests"); - File chrome = new File(pageFactoryDir, "chromedriver.exe"); - - 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(CHROMEDRIVER_EXECUTABLE, chrome.getAbsolutePath()); - - service = new AppiumServiceBuilder().withCapabilities(caps).build(); - service.start(); - assertTrue(service.isRunning()); - } - - @Test public void checkAbilityToStartServiceUsingCapabilitiesAndFlags() throws Exception { - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - File pageFactoryDir = new File("src/test/java/io/appium/java_client/pagefactory_tests"); - File chrome = new File(pageFactoryDir, "chromedriver.exe"); - - 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(CHROMEDRIVER_EXECUTABLE, chrome.getAbsolutePath()); - - service = new AppiumServiceBuilder() - .withArgument(CALLBACK_ADDRESS, testIP) - .withArgument(SESSION_OVERRIDE) - .withArgument(PRE_LAUNCH) - .withCapabilities(caps).build(); - service.start(); - assertTrue(service.isRunning()); - } - - @Test public void checkAbilityToChangeOutputStream() throws Exception { - file = new File("test"); - file.createNewFile(); - stream = new FileOutputStream(file); - service = buildDefaultService(); - service.addOutPutStream(stream); - service.start(); - assertTrue(file.length() > 0); - } - - @Test public void checkAbilityToChangeOutputStreamAfterTheServiceIsStarted() throws Exception { - file = new File("test"); - file.createNewFile(); - stream = new FileOutputStream(file); - service = buildDefaultService(); - service.start(); - service.addOutPutStream(stream); - service.isRunning(); - assertTrue(file.length() > 0); - } - - @Test public void checkAbilityToShutDownService() { - service = buildDefaultService(); - service.start(); - service.stop(); - assertFalse(service.isRunning()); - } - - @Test public void checkAbilityToStartAndShutDownFewServices() throws Exception { - List services = asList( - new AppiumServiceBuilder().usingAnyFreePort().build(), - new AppiumServiceBuilder().usingAnyFreePort().build(), - new AppiumServiceBuilder().usingAnyFreePort().build(), - new AppiumServiceBuilder().usingAnyFreePort().build()); - services.parallelStream().forEach(AppiumDriverLocalService::start); - assertTrue(services.stream().allMatch(AppiumDriverLocalService::isRunning)); - SECONDS.sleep(1); - services.parallelStream().forEach(AppiumDriverLocalService::stop); - assertTrue(services.stream().noneMatch(AppiumDriverLocalService::isRunning)); - } - - @Test public void checkAbilityToStartServiceWithLogFile() throws Exception { - file = new File("Log.txt"); - file.createNewFile(); - service = new AppiumServiceBuilder().withLogFile(file).build(); - service.start(); - assertTrue(file.exists()); - assertTrue(file.length() > 0); - } - - @Test public void checkAbilityToStartServiceWithPortUsingFlag() throws Exception { - String port = "8996"; - String expectedUrl = String.format("http://0.0.0.0:%s/wd/hub", port); - - service = new AppiumServiceBuilder() - .withArgument(() -> "--port", port) - .build(); - String actualUrl = service.getUrl().toString(); - assertEquals(expectedUrl, actualUrl); - service.start(); - } - - @Test public void checkAbilityToStartServiceWithPortUsingShortFlag() throws Exception { - String port = "8996"; - String expectedUrl = String.format("http://0.0.0.0:%s/wd/hub", port); - - service = new AppiumServiceBuilder() - .withArgument(() -> "-p", port) - .build(); - String actualUrl = service.getUrl().toString(); - assertEquals(expectedUrl, actualUrl); - service.start(); - } - - @Test public void checkAbilityToStartServiceWithIpUsingFlag() throws Exception { - String expectedUrl = String.format("http://%s:4723/wd/hub", testIP); - - service = new AppiumServiceBuilder() - .withArgument(() -> "--address", testIP) - .build(); - String actualUrl = service.getUrl().toString(); - assertEquals(expectedUrl, actualUrl); - service.start(); - } - - @Test public void checkAbilityToStartServiceWithIpUsingShortFlag() throws Exception { - String expectedUrl = String.format("http://%s:4723/wd/hub", testIP); - - service = new AppiumServiceBuilder() - .withArgument(() -> "-a", testIP) - .build(); - String actualUrl = service.getUrl().toString(); - assertEquals(expectedUrl, actualUrl); - service.start(); - } - - @Test public void checkAbilityToStartServiceWithLogFileUsingFlag() throws Exception { - file = new File("Log2.txt"); - - service = new AppiumServiceBuilder() - .withArgument(() -> "--log", file.getAbsolutePath()) - .build(); - service.start(); - assertTrue(file.exists()); - } - - @Test public void checkAbilityToStartServiceWithLogFileUsingShortFlag() throws Exception { - file = new File("Log3.txt"); - - service = new AppiumServiceBuilder() - .withArgument(() -> "-g", file.getAbsolutePath()) - .build(); - service.start(); - assertTrue(file.exists()); - } -} diff --git a/src/test/java/io/appium/java_client/localserver/StartingAppLocallyTest.java b/src/test/java/io/appium/java_client/localserver/StartingAppLocallyTest.java deleted file mode 100644 index a7c037bf3..000000000 --- a/src/test/java/io/appium/java_client/localserver/StartingAppLocallyTest.java +++ /dev/null @@ -1,208 +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.localserver; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; - -import io.appium.java_client.android.AndroidDriver; -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.AppiumServiceBuilder; -import io.appium.java_client.service.local.flags.GeneralServerFlag; -import org.junit.Test; -import org.openqa.selenium.Capabilities; -import org.openqa.selenium.remote.DesiredCapabilities; - -import java.io.File; - -public class StartingAppLocallyTest { - - @Test public void startingAndroidAppWithCapabilitiesOnlyTest() { - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); - - AndroidDriver driver = new AndroidDriver<>(capabilities); - try { - Capabilities caps = driver.getCapabilities(); - - assertEquals(true, caps.getCapability(MobileCapabilityType.AUTOMATION_NAME) - .equals(AutomationName.APPIUM)); - assertEquals(true, caps.getCapability(MobileCapabilityType.PLATFORM_NAME) - .equals(MobilePlatform.ANDROID)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - assertEquals(true, - caps.getCapability(MobileCapabilityType.APP).equals(app.getAbsolutePath())); - } finally { - driver.quit(); - } - } - - @Test public void startingAndroidAppWithCapabilitiesAndServiceTest() { - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - - AppiumServiceBuilder builder = - new AppiumServiceBuilder().withArgument(GeneralServerFlag.SESSION_OVERRIDE) - .withArgument(GeneralServerFlag.STRICT_CAPS); - - AndroidDriver driver = new AndroidDriver<>(builder, capabilities); - try { - Capabilities caps = driver.getCapabilities(); - - assertEquals(true, caps.getCapability(MobileCapabilityType.PLATFORM_NAME) - .equals(MobilePlatform.ANDROID)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - } finally { - driver.quit(); - } - } - - @Test public void startingAndroidAppWithCapabilitiesAndFlagsOnServerSideTest() { - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - - File pageFactoryDir = new File("src/test/java/io/appium/java_client/pagefactory_tests"); - File chrome = new File(pageFactoryDir, "chromedriver.exe"); - - 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, app.getAbsolutePath()); - serverCapabilities.setCapability(AndroidMobileCapabilityType.CHROMEDRIVER_EXECUTABLE, - chrome.getAbsolutePath()); - - 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(true, caps.getCapability(MobileCapabilityType.PLATFORM_NAME) - .equals(MobilePlatform.ANDROID)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - } finally { - driver.quit(); - } - } - - @Test public void startingIOSAppWithCapabilitiesOnlyTest() { - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "UICatalog.app.zip"); - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "9.2"); - //sometimes environment has performance problems - capabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, 500000); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); - - IOSDriver driver = new IOSDriver<>(capabilities); - try { - Capabilities caps = driver.getCapabilities(); - - assertEquals(true, caps.getCapability(MobileCapabilityType.AUTOMATION_NAME) - .equals(AutomationName.APPIUM)); - assertEquals(true, - caps.getCapability(MobileCapabilityType.PLATFORM_NAME).equals(MobilePlatform.IOS)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - assertEquals(true, - caps.getCapability(MobileCapabilityType.PLATFORM_VERSION).equals("9.2")); - assertEquals(true, - caps.getCapability(MobileCapabilityType.APP).equals(app.getAbsolutePath())); - } finally { - driver.quit(); - } - } - - - @Test public void startingIOSAppWithCapabilitiesAndServiseTest() { - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "UICatalog.app.zip"); - - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.APPIUM); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "9.2"); - //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(); - assertEquals(true, - caps.getCapability(MobileCapabilityType.PLATFORM_NAME).equals(MobilePlatform.IOS)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - } finally { - driver.quit(); - } - } - - @Test public void startingIOSAppWithCapabilitiesAndFlagsOnServerSideTest() { - DesiredCapabilities serverCapabilities = new DesiredCapabilities(); - serverCapabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); - serverCapabilities.setCapability(IOSMobileCapabilityType.LAUNCH_TIMEOUT, - 500000); //some environment is too slow - serverCapabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "9.2"); - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "UICatalog.app.zip"); - DesiredCapabilities clientCapabilities = new DesiredCapabilities(); - clientCapabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - - 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(); - assertEquals(true, - caps.getCapability(MobileCapabilityType.PLATFORM_NAME).equals(MobilePlatform.IOS)); - assertNotEquals(null, caps.getCapability(MobileCapabilityType.DEVICE_NAME)); - } finally { - driver.quit(); - } - } -} diff --git a/src/test/java/io/appium/java_client/localserver/custom_node_path.properties b/src/test/java/io/appium/java_client/localserver/custom_node_path.properties deleted file mode 100644 index 15534a1cb..000000000 --- a/src/test/java/io/appium/java_client/localserver/custom_node_path.properties +++ /dev/null @@ -1,19 +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. -# - -path.to.custom.node.win=C:/Program Files (x86)/Appium/node_modules/appium/bin/appium.js -path.to.custom.node.macos=/Applications/Appium.app/Contents/Resources/node_modules/appium/bin/appium.js -path.to.custom.node.linux=specify your path on your own 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 eb1f563e2..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,68 +16,62 @@ package io.appium.java_client.pagefactory_tests; -import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; - 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.iOSFindBy; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.Platform; +import io.appium.java_client.pagefactory.Widget; +import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.utils.TestUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeDriverService; +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.io.File; import java.util.List; -import java.util.concurrent.TimeUnit; -public class DesktopBrowserCompatibilityTest { +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(); - private static final Platform current = Platform.getCurrent(); - @HowToUseLocators(iOSAutomation = ALL_POSSIBLE) + @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) @AndroidFindBy(className = "someClass") - @iOSFindBy(xpath = "//selector[1]") @iOSFindBy(xpath = "//someTag") + @iOSXCUITFindBy(xpath = "//selector[1]") @iOSXCUITFindBy(xpath = "//someTag") @FindBys({@FindBy(id = "main"), @FindBy(tagName = "p")}) 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() { - if (current.is(Platform.WINDOWS)) { - System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, - "src/test/java/io/appium/java_client/pagefactory_tests/chromedriver.exe"); - } else { - System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, - "src/test/java/io/appium/java_client/pagefactory_tests/chromedriver"); - } + @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, 15, TimeUnit.SECONDS), - this); - driver.get(new File("src/test/java/io/appium/java_client/hello appium - saved page.htm") - .toURI().toString()); + PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(15)), this); + driver.get(HELLO_APPIUM_HTML); assertNotEquals(0, foundLinks.size()); assertNotEquals(0, main.size()); - assertEquals(null, trap1); - assertEquals(null, trap2); + 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 7c06ba2a5..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/GenericTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package io.appium.java_client.pagefactory_tests; - -import static org.junit.Assert.assertTrue; - -import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.PageFactory; - -import java.util.List; -import java.util.Set; -import java.util.function.Supplier; - -public class GenericTest { - - static class TempGenericPage { - - public List items; - - public List getItems() { - return items; - } - } - - static class MockWebDriver implements WebDriver { - - @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) { - return null; - } - - @Override - public WebElement findElement(By by) { - return null; - } - - @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; - } - } - - @Test - public void genericTestCse() { - Supplier result = () -> { - PageFactory - .initElements(new AppiumFieldDecorator(new MockWebDriver()), - new TempGenericPage<>()); - return true; - }; - assertTrue(result.get()); - } -} \ No newline at end of file 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 d8568c40e..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/IOSMobileBrowserCompatibilityTest.java +++ /dev/null @@ -1,95 +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 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.iOSFindBy; -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; -import java.util.concurrent.TimeUnit; - -public class IOSMobileBrowserCompatibilityTest { - - private WebDriver driver; - private AppiumDriverLocalService service; - - @FindBy(name = "q") - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/someId\")") - @iOSFindBy(className = "someClass") private WebElement searchTextField; - - @AndroidFindBy(className = "someClass") - @FindBys({@FindBy(className = "r"), @FindBy(tagName = "a")}) @iOSFindBy(className = "someClass") - private List foundLinks; - - /** - * The setting up. - */ - @Before public void setUp() throws Exception { - 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, 5, TimeUnit.SECONDS), this); - } - - /** - * finishing. - */ - @After public void tearDown() throws Exception { - 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/IOSPageFactoryTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/IOSPageFactoryTest.java deleted file mode 100644 index b5ff650ce..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/IOSPageFactoryTest.java +++ /dev/null @@ -1,245 +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 org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; - -import io.appium.java_client.MobileElement; - -import io.appium.java_client.ios.AppIOSTest; -import io.appium.java_client.ios.IOSElement; -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.iOSFindBy; - -import org.junit.Before; -import org.junit.Test; - -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.RemoteWebElement; -import org.openqa.selenium.support.CacheLookup; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; - -import java.util.List; - -public class IOSPageFactoryTest extends AppIOSTest { - - private boolean populated = false; - - @FindBy(className = "UIAButton") - private List uiButtons; - - @FindBy(className = "UIAButton") - private List iosUIButtons; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private List iosUIAutomatorButtons; - - @iOSFindBy(uiAutomator = ".elements()[0]") - @AndroidFindBy(className = "android.widget.TextView") - private List androidOriOsTextViews; - - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") - private List androidUIAutomatorViews; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private List mobileButtons; - - @FindBy(className = "UIAButton") - private List mobiletFindByButtons; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private List remoteElementViews; - - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")") - @AndroidFindBy(className = "android.widget.TextView") - private List chainElementViews; - - - @FindBy(className = "UIAButton") - private WebElement uiButton; - - @CacheLookup - @FindBy(className = "UIAButton") - private MobileElement cached; - - @FindBy(className = "UIAButton") - private WebElement iosUIButton; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private WebElement iosUIAutomatorButton; - - @AndroidFindBy(className = "android.widget.TextView") @iOSFindBy(uiAutomator = ".elements()[0]") - private WebElement androidOriOsTextView; - - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/text1\")") - private WebElement androidUIAutomatorView; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private MobileElement mobileButton; - - @FindBy(className = "UIAButton") - private MobileElement mobiletFindByButton; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private RemoteWebElement remotetextVieW; - - @AndroidFindBy(uiAutomator = "new UiSelector().resourceId(\"android:id/list\")") - @AndroidFindBy(className = "android.widget.TextView") - private WebElement chainElementView; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private IOSElement iosButton; - - @iOSFindBy(uiAutomator = ".elements()[0]") - private List iosButtons; - - @HowToUseLocators(iOSAutomation = ALL_POSSIBLE) - @iOSFindBy(id = "ComputeSumButton_Test") - @iOSFindBy(xpath = "//*[@name = \"ComputeSumButton\"]") - private WebElement findAllElement; - - @HowToUseLocators(iOSAutomation = ALL_POSSIBLE) - @iOSFindBy(id = "ComputeSumButton_Test") - @iOSFindBy(xpath = "//*[@name = \"ComputeSumButton\"]") - private List findAllElements; - - @AndroidFindBy(className = "android.widget.TextView") @FindBy(css = "e.e1.e2") - private List elementsWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy; - - @AndroidFindBy(className = "android.widget.TextView") @FindBy(css = "e.e1.e2") - private WebElement elementWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy; - - /** - * The setting up. - */ - @Before public void setUp() throws Exception { - if (!populated) { - PageFactory.initElements(new AppiumFieldDecorator(driver), this); - } - - populated = true; - } - - @Test public void findByElementsTest() { - assertNotEquals(0, uiButtons.size()); - } - - @Test public void findByElementTest() { - assertNotEquals(null, uiButton.getText()); - } - - @Test public void checkCached() { - assertEquals(cached.getId(), cached.getId()); - } - - - @Test public void iosFindByElementsTest() { - assertNotEquals(0, iosUIButtons.size()); - } - - @Test public void iosFindByElementTest() { - assertNotEquals(null, iosUIButton.getText()); - } - - @Test public void checkThatElementsWereNotFoundByAndroidUIAutomator() { - assertEquals(0, androidUIAutomatorViews.size()); - } - - @Test(expected = NoSuchElementException.class) public void checkThatElementWasNotFoundByAndroidUIAutomator() { - assertNotNull(androidUIAutomatorView.getText()); - } - - @Test public void androidOriOSFindByElementsTest() { - assertNotEquals(0, androidOriOsTextViews.size()); - } - - @Test public void androidOrIOSFindByElementTest() { - assertNotEquals(null, androidOriOsTextView.getText()); - } - - @Test public void iosFindByUIAutomatorElementsTest() { - assertNotEquals(0, iosUIAutomatorButtons.size()); - } - - @Test public void iosFindByUIAutomatorElementTest() { - assertNotEquals(null, iosUIAutomatorButton.getText()); - } - - @Test public void areMobileElementsTest() { - assertNotEquals(0, mobileButtons.size()); - } - - @Test public void isMobileElementTest() { - assertNotEquals(null, mobileButton.getText()); - } - - @Test public void areMobileElementsFindByTest() { - assertNotEquals(0, mobiletFindByButtons.size()); - } - - @Test public void isMobileElementFindByTest() { - assertNotEquals(null, mobiletFindByButton.getText()); - } - - @Test public void areRemoteElementsTest() { - assertNotEquals(0, remoteElementViews.size()); - } - - @Test public void isRemoteElementTest() { - assertNotEquals(null, remotetextVieW.getText()); - } - - @Test public void checkThatElementsWereNotFoundByAndroidUIAutomatorChain() { - assertEquals(0, chainElementViews.size()); - } - - @Test(expected = NoSuchElementException.class) public void checkThatElementWasNotFoundByAndroidUIAutomatorChain() { - assertNotNull(chainElementView.getText()); - } - - @Test public void isIOSElementTest() { - assertNotEquals(null, iosButton.getText()); - } - - @Test public void areIOSElementsFindByTest() { - assertNotEquals(0, iosButtons.size()); - } - - @Test public void findAllElementsTest() { - assertNotEquals(0, findAllElements.size()); - } - - @Test public void findAllElementTest() { - assertNotEquals(null, findAllElement.getText()); - } - - @Test(expected = NoSuchElementException.class) public void checkThatTestWillNotBeFailedBecauseOfInvalidFindBy() { - assertNotNull(elementWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy - .getAttribute("text")); - } - - @Test public void checkThatTestWillNotBeFailedBecauseOfInvalidFindByList() { - assertEquals(0, elementsWhenAndroidLocatorIsNotDefinedAndThereIsInvalidFindBy.size()); - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/SelendroidModeTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/SelendroidModeTest.java deleted file mode 100644 index a6ac38e70..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/SelendroidModeTest.java +++ /dev/null @@ -1,194 +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 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.AppiumDriver; -import io.appium.java_client.android.AndroidDriver; - -import io.appium.java_client.android.AndroidElement; -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.SelendroidFindBy; -import io.appium.java_client.remote.AndroidMobileCapabilityType; -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.AppiumServiceBuilder; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.DesiredCapabilities; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; - -import java.io.File; -import java.util.List; -import java.util.concurrent.TimeUnit; - -public class SelendroidModeTest { - private static int SELENDROID_PORT = 9999; - - private static AppiumDriver driver; - private static AppiumDriverLocalService service; - private boolean populated = false; - - @SelendroidFindBy(id = "text1") private WebElement textId; - - @AndroidFindBy(id = "Invalid Identifier") @SelendroidFindBy(id = "text1") - private WebElement textSelendroidId; - - @SelendroidFindBy(name = "Accessibility") - private WebElement textName; - - @AndroidFindBy(xpath = ".//*[@name = 'Accessibility']") - private WebElement textNameAndroid; - - @FindBy(name = "Accessibility") - private WebElement textNameDefault; - - @SelendroidFindBy(xpath = "//TextView[@value='Accessibility']") - private WebElement textXpath; - - @SelendroidFindBy(id = "content") - @SelendroidFindBy(id = "text1") - private WebElement textIds; - - @HowToUseLocators(selendroidAutomation = ALL_POSSIBLE) - @SelendroidFindBy(id = "text999") @SelendroidFindBy(id = "text1") - private WebElement textAll; - - @HowToUseLocators(selendroidAutomation = ALL_POSSIBLE) - @SelendroidFindBy(id = "text999") @SelendroidFindBy(id = "text1") - private List textsAll; - - @SelendroidFindBy(className = "android.widget.TextView") private WebElement textClass; - - @SelendroidFindBy(tagName = "TextView") private WebElement textTag; - - @SelendroidFindBy(linkText = "Accessibility") private WebElement textLink; - - @SelendroidFindBy(partialLinkText = "ccessibilit") private WebElement textPartialLink; - - /** - * initialization. - */ - @BeforeClass public static void beforeClass() throws Exception { - AppiumServiceBuilder builder = new AppiumServiceBuilder(); - service = builder.build(); - service.start(); - - File appDir = new File("src/test/java/io/appium/java_client"); - File app = new File(appDir, "ApiDemos-debug.apk"); - DesiredCapabilities capabilities = new DesiredCapabilities(); - capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - capabilities.setCapability(AndroidMobileCapabilityType.SELENDROID_PORT, SELENDROID_PORT); - capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.SELENDROID); - driver = new AndroidDriver<>(service.getUrl(), capabilities); - driver.context("NATIVE_APP"); - } - - /** - * finishing. - */ - @AfterClass public static void afterClass() throws Exception { - if (driver != null) { - driver.quit(); - } - - if (service != null) { - service.stop(); - } - } - - /** - * The setting up. - */ - @Before public void setUp() throws Exception { - if (!populated) { - //This time out is set because test can be run on slow Android SDK emulator - PageFactory.initElements(new AppiumFieldDecorator(driver, 5, TimeUnit.SECONDS), this); - } - - populated = true; - } - - @Test public void findByIdElementTest() { - assertNotEquals(null, textId.getAttribute("text")); - } - - @Test public void findBySelendroidSelectorTest() { - assertNotEquals(null, textSelendroidId.getAttribute("text")); - } - - @Test public void findByElementByNameTest() { - assertEquals("Accessibility", textName.getText()); - } - - @Test public void findByElementByNameAndroidTest() { - assertEquals("Accessibility", textNameAndroid.getText()); - } - - @Test public void findByElementByNameDefaultTest() { - assertEquals("Accessibility", textNameDefault.getText()); - } - - @Test public void findByElementByXpathTest() { - assertEquals("Accessibility", textXpath.getText()); - } - - @Test public void findByElementByIdsTest() { - assertNotNull(textIds.getText()); - } - - @Test public void findByElementByTestAllTest() { - assertNotNull(textAll.getText()); - } - - @Test public void findByElementByTextsAllTest() { - assertTrue(textsAll.size() > 1); - } - - @Test public void findByElementByCalssTest() { - assertNotEquals(null, textClass.getAttribute("text")); - } - - @Test public void findByElementByTagTest() { - assertNotEquals(null, textTag.getAttribute("text")); - } - - @Test public void findBySelendroidAnnotationOnlyTest() { - assertNotEquals(null, textSelendroidId.getAttribute("text")); - } - - @Test public void findBySelendroidLinkTextTest() { - assertEquals("Accessibility", textLink.getText()); - } - - @Test public void findBySelendroidPartialLinkTextTest() { - assertEquals("Accessibility", textPartialLink.getText()); - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/TimeOutResetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/TimeOutResetTest.java deleted file mode 100644 index c404d91a3..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/TimeOutResetTest.java +++ /dev/null @@ -1,155 +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 org.junit.Assert.assertTrue; - -import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import io.appium.java_client.pagefactory.TimeOutDuration; -import io.appium.java_client.pagefactory.WithTimeout; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.openqa.selenium.By; -import org.openqa.selenium.Platform; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeDriverService; -import org.openqa.selenium.support.FindAll; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.PageFactory; - -import java.util.Calendar; -import java.util.List; -import java.util.concurrent.TimeUnit; - -public class TimeOutResetTest { - private static final long ACCEPTABLE_DELTA_MILLS = 1500; - private WebDriver driver; - @FindAll({@FindBy(className = "ClassWhichDoesNotExist"), - @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) private List - stubElements; - - @WithTimeout(time = 5, unit = TimeUnit.SECONDS) - @FindAll({@FindBy(className = "ClassWhichDoesNotExist"), - @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) private List - stubElements2; - - private TimeOutDuration timeOutDuration; - - private static boolean checkTimeDifference(long expectedTime, TimeUnit expectedTimeUnit, - long currentMillis) { - long expectedMillis = TimeUnit.MILLISECONDS.convert(expectedTime, expectedTimeUnit); - try { - Assert.assertEquals(true, - ((currentMillis - expectedMillis) < ACCEPTABLE_DELTA_MILLS) && ( - (currentMillis - expectedMillis) >= 0)); - } catch (Error e) { - String message = String.valueOf(expectedTime) + " " - + expectedTimeUnit.toString() - + " current duration in millis " - + String.valueOf(currentMillis) + " Failed"; - throw new AssertionError(message, e); - } - return true; - } - - /** - * The setting up. - */ - @Before public void setUp() throws Exception { - if (Platform.getCurrent().is(Platform.WINDOWS)) { - System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, - "src/test/java/io/appium/java_client/pagefactory_tests/chromedriver.exe"); - } else { - System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, - "src/test/java/io/appium/java_client/pagefactory_tests/chromedriver"); - } - driver = new ChromeDriver(); - timeOutDuration = new TimeOutDuration(AppiumFieldDecorator.DEFAULT_IMPLICITLY_WAIT_TIMEOUT, - AppiumFieldDecorator.DEFAULT_TIMEUNIT); - PageFactory.initElements(new AppiumFieldDecorator(driver, timeOutDuration), this); - } - - /** - * finishing. - */ - @After public void tearDown() throws Exception { - driver.quit(); - } - - private long getBenchMark(List stubElements) { - long startMark = Calendar.getInstance().getTimeInMillis(); - stubElements.size(); - long endMark = Calendar.getInstance().getTimeInMillis(); - return endMark - startMark; - } - - @Test public void test() { - assertTrue(checkTimeDifference(AppiumFieldDecorator.DEFAULT_IMPLICITLY_WAIT_TIMEOUT, - AppiumFieldDecorator.DEFAULT_TIMEUNIT, getBenchMark(stubElements))); - System.out.println( - String.valueOf(AppiumFieldDecorator.DEFAULT_IMPLICITLY_WAIT_TIMEOUT) + " " - + AppiumFieldDecorator.DEFAULT_TIMEUNIT.toString() + ": Fine"); - - timeOutDuration.setTime(15500000, TimeUnit.MICROSECONDS); - assertTrue(checkTimeDifference(15500000, TimeUnit.MICROSECONDS, getBenchMark(stubElements))); - System.out.println( - "Change time: " + String.valueOf(15500000) + " " + TimeUnit.MICROSECONDS.toString() - + ": Fine"); - - timeOutDuration.setTime(3, TimeUnit.SECONDS); - assertTrue(checkTimeDifference(3, TimeUnit.SECONDS, getBenchMark(stubElements))); - System.out.println( - "Change time: " + String.valueOf(3) + " " + TimeUnit.SECONDS.toString() + ": Fine"); - - } - - @Test public void test2() { - assertTrue(checkTimeDifference(AppiumFieldDecorator.DEFAULT_IMPLICITLY_WAIT_TIMEOUT, - AppiumFieldDecorator.DEFAULT_TIMEUNIT, getBenchMark(stubElements))); - System.out.println( - String.valueOf(AppiumFieldDecorator.DEFAULT_IMPLICITLY_WAIT_TIMEOUT) + " " - + AppiumFieldDecorator.DEFAULT_TIMEUNIT.toString() + ": Fine"); - - assertTrue(checkTimeDifference(5, TimeUnit.SECONDS, getBenchMark(stubElements2))); - System.out.println(String.valueOf(5) + " " + TimeUnit.SECONDS.toString() + ": Fine"); - - - timeOutDuration.setTime(15500000, TimeUnit.MICROSECONDS); - assertTrue(checkTimeDifference(15500000, TimeUnit.MICROSECONDS, getBenchMark(stubElements))); - System.out.println( - "Change time: " + String.valueOf(15500000) + " " + TimeUnit.MICROSECONDS.toString() - + ": Fine"); - - assertTrue(checkTimeDifference(5, TimeUnit.SECONDS, getBenchMark(stubElements2))); - System.out.println(String.valueOf(5) + " " + TimeUnit.SECONDS.toString() + ": Fine"); - } - - @Test public void checkThatCustomizedTimeOutDoesNotChangeGeneralValue() { - stubElements2.size(); //time out differs from the general value there - - long startMark = Calendar.getInstance().getTimeInMillis(); - driver.findElements(By.id("FakeId")); - long endMark = Calendar.getInstance().getTimeInMillis(); - assertTrue(checkTimeDifference(AppiumFieldDecorator.DEFAULT_IMPLICITLY_WAIT_TIMEOUT, - AppiumFieldDecorator.DEFAULT_TIMEUNIT, endMark - startMark)); - } - -} 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 new file mode 100644 index 000000000..32d23c874 --- /dev/null +++ b/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java @@ -0,0 +1,121 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.pagefactory.AppiumFieldDecorator; +import io.appium.java_client.pagefactory.WithTimeout; +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; + + private WebDriver driver; + + @FindAll({ + @FindBy(className = "ClassWhichDoesNotExist"), + @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) + private List stubElements; + + @WithTimeout(time = 5, chronoUnit = SECONDS) + @FindAll({@FindBy(className = "ClassWhichDoesNotExist"), + @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) + private List stubElements2; + + private Duration timeOutDuration; + + private static long getExpectedMillis(Duration duration) { + return duration.toMillis(); + } + + private static long getPerformanceDiff(long expectedMs, Runnable runnable) { + long startMark = currentTimeMillis(); + runnable.run(); + long endMark = currentTimeMillis(); + return abs(expectedMs - (endMark - startMark)); + } + + private static String assertionMessage(Duration expectedDuration) { + return format("Check difference from the expected waiting duration %s", + formatDuration(expectedDuration.toMillis(), "H:mm:ss:SSS", true)); + } + + @BeforeAll + public static void beforeAll() { + chromedriver().setup(); + } + + /** + * The setting up. + */ + @BeforeEach public void setUp() { + driver = new ChromeDriver(new ChromeOptions().addArguments("--headless=new")); + timeOutDuration = DEFAULT_WAITING_TIMEOUT; + initElements(new AppiumFieldDecorator(driver, timeOutDuration), this); + } + + /** + * finishing. + */ + @AfterEach public void tearDown() { + driver.quit(); + } + + @Test public void withCustomizedTimeOutTest() { + assertThat(assertionMessage(DEFAULT_WAITING_TIMEOUT), + getPerformanceDiff(getExpectedMillis(DEFAULT_WAITING_TIMEOUT), () -> stubElements.size()), + lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); + + assertThat(assertionMessage(ofSeconds(5)), + getPerformanceDiff(getExpectedMillis(ofSeconds(5)), () -> stubElements2.size()), + lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); + + timeOutDuration.plus(ofSeconds(10)); + + assertThat(assertionMessage(timeOutDuration), + getPerformanceDiff(getExpectedMillis(timeOutDuration), () -> stubElements.size()), + lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); + + assertThat(assertionMessage(ofSeconds(5)), + getPerformanceDiff(getExpectedMillis(ofSeconds(5)), () -> stubElements2.size()), + lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); + } +} 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 13f6d66f7..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/XCUITModeTest.java +++ /dev/null @@ -1,171 +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.assertNull; -import static org.junit.Assert.assertTrue; -import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; - -import io.appium.java_client.MobileBy; -import io.appium.java_client.MobileElement; -import io.appium.java_client.ios.AppXCUITTest; -import io.appium.java_client.pagefactory.AppiumFieldDecorator; -import io.appium.java_client.pagefactory.HowToUseLocators; -import io.appium.java_client.pagefactory.iOSFindBy; -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; -import java.util.function.Supplier; - -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class XCUITModeTest extends AppXCUITTest { - - private boolean populated = false; - private WebDriverWait waiting = new WebDriverWait(driver, 10000); - - @HowToUseLocators(iOSAutomation = ALL_POSSIBLE) - @iOSXCUITFindBy(iOSNsPredicate = "label contains 'Compute'") - @iOSXCUITFindBy(className = "XCUIElementTypeButton") - private MobileElement computeButton; - - @HowToUseLocators(iOSAutomation = CHAIN) - @iOSXCUITFindBy(className = "XCUIElementTypeOther") - @iOSXCUITFindBy(iOSNsPredicate = "name like 'Answer'") - private WebElement answer; - - @iOSXCUITFindBy(iOSNsPredicate = "name = 'IntegerA'") - private MobileElement textField1; - - @HowToUseLocators(iOSAutomation = 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; - - @iOSFindBy(id = "locationStatus") - private MobileElement locationStatus; - - @HowToUseLocators(iOSAutomation = CHAIN) - @iOSFindBy(id = "TestApp") @iOSXCUITFindBy(iOSNsPredicate = "name BEGINSWITH 'contact'") - private MobileElement contactAlert; - - @HowToUseLocators(iOSAutomation = ALL_POSSIBLE) - @iOSFindBy(uiAutomator = ".elements()[0]") - @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() throws Exception { - if (!populated) { - PageFactory.initElements(new AppiumFieldDecorator(driver), this); - } - - populated = true; - } - - @Test public void dismissAlertTest() { - Supplier dismissAlert = () -> { - driver.findElement(MobileBy - .iOSNsPredicateString("name CONTAINS 'Slow Down'")).click(); - waiting.until(alertIsPresent()); - driver.switchTo().alert().dismiss(); - return true; - }; - assertTrue(dismissAlert.get()); - } - - @Test public void findByXCUITSelectorTest() { - assertNotEquals(null, computeButton.getText()); - } - - @Test public void findElementByNameTest() { - assertNull(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("Test Gesture")); - } - - @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/chromedriver b/src/test/java/io/appium/java_client/pagefactory_tests/chromedriver deleted file mode 100755 index 2e17cecf2..000000000 Binary files a/src/test/java/io/appium/java_client/pagefactory_tests/chromedriver and /dev/null differ diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/chromedriver.exe b/src/test/java/io/appium/java_client/pagefactory_tests/chromedriver.exe deleted file mode 100644 index c312e1bab..000000000 Binary files a/src/test/java/io/appium/java_client/pagefactory_tests/chromedriver.exe and /dev/null differ 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 d5c5af560..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,28 +1,34 @@ 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.AutomationName.SELENDROID; -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.Response; +import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -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) { return null; @@ -33,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; @@ -60,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 @@ -104,6 +104,22 @@ public Navigation navigate() { return null; } + public String getPlatformName() { + return ""; + } + + public String getAutomationName() { + return ""; + } + + @Override + public Capabilities getCapabilities() { + return new ImmutableCapabilities( + "appium:platformName", getPlatformName(), + "appium:automationName", getAutomationName() + ); + } + @Override public Options manage() { return new Options() { @@ -140,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 @@ -183,33 +246,7 @@ public String getPlatformName() { @Override public String getAutomationName() { - return APPIUM; - } - } - - public static class StubSelendroidDriver extends AbstractStubWebDriver { - - @Override - public String getPlatformName() { - return ANDROID; - } - - @Override - public String getAutomationName() { - return SELENDROID; - } - } - - public static class StubIOSDriver extends AbstractStubWebDriver { - - @Override - public String getPlatformName() { - return IOS; - } - - @Override - public String getAutomationName() { - return APPIUM; + return ANDROID_UIAUTOMATOR2; } } @@ -235,7 +272,7 @@ public String getPlatformName() { @Override public String getAutomationName() { - return APPIUM; + return WINDOWS; } } @@ -261,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 2523c017b..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 checkCaseWhenBothWidgetFieldAndClassHaveDelaredAnnotations(); - - 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 859ac475d..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; @@ -11,17 +8,19 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; -import org.openqa.selenium.internal.WrapsDriver; +import org.openqa.selenium.WrapsDriver; 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/AndroidApp.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AndroidApp.java index 419bd4fba..e33832265 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AndroidApp.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AndroidApp.java @@ -1,7 +1,6 @@ package io.appium.java_client.pagefactory_tests.widget.tests.android; import io.appium.java_client.pagefactory.AndroidFindBy; -import io.appium.java_client.pagefactory.SelendroidFindBy; import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; import java.util.List; @@ -9,67 +8,61 @@ public class AndroidApp implements ExtendedApp { public static String ANDROID_DEFAULT_WIDGET_LOCATOR = "SOME_ANDROID_DEFAULT_LOCATOR"; - public static String ANDROID_SELENDROID_WIDGET_LOCATOR = "SOME_SELENDROID_DEFAULT_LOCATOR"; public static String ANDROID_EXTERNALLY_DEFINED_WIDGET_LOCATOR = "SOME_ANDROID_EXTERNALLY_DEFINED_LOCATOR"; - public static String SELENDROID_EXTERNALLY_DEFINED_WIDGET_LOCATOR = "SOME_SELENDROID_EXTERNALLY_DEFINED_LOCATOR"; @AndroidFindBy(uiAutomator = "SOME_ANDROID_DEFAULT_LOCATOR") - @SelendroidFindBy(linkText = "SOME_SELENDROID_DEFAULT_LOCATOR") private DefaultAndroidWidget singleAndroidWidget; @AndroidFindBy(uiAutomator = "SOME_ANDROID_DEFAULT_LOCATOR") - @SelendroidFindBy(linkText = "SOME_SELENDROID_DEFAULT_LOCATOR") private List multipleAndroidWidgets; /** - * This class is annotated by {@link AndroidFindBy} and @{@link SelendroidFindBy}. + * This class is annotated by {@link AndroidFindBy} * This field was added to check that locator is created correctly according to current platform * and current automation. */ private AnnotatedAndroidWidget singleAnnotatedAndroidWidget; /** - * This class is annotated by {@link AndroidFindBy} and @{@link SelendroidFindBy}. + * This class is annotated by {@link AndroidFindBy} * This field was added to check that locator is created correctly according to current platform * and current automation. */ private List multipleAnnotatedAndroidWidgets; /** - * This class is not annotated by {@link AndroidFindBy} and {@link SelendroidFindBy}. + * This class is not annotated by {@link AndroidFindBy} * But the superclass is annotated by these annotations. This field was added to check that locator is * created correctly according to current platform and current automation. */ private ExtendedAndroidWidget singleExtendedAndroidWidget; /** - * This class is not annotated by {@link AndroidFindBy} and {@link SelendroidFindBy}. + * This class is not annotated by {@link AndroidFindBy} * But the superclass is annotated by these annotations. This field was added to check that locator is * created correctly according to current platform and current automation. */ private List multipleExtendedAndroidWidgets; /** - * The superclass is annotated by {@link AndroidFindBy} and {@link SelendroidFindBy}. + * The superclass is annotated by {@link AndroidFindBy} * However there is the field which is annotated by this annotations. * This field was added to check that locator is * created correctly according to current platform and current automation and * annotations that mark the field. */ @AndroidFindBy(uiAutomator = "SOME_ANDROID_EXTERNALLY_DEFINED_LOCATOR") - @SelendroidFindBy(linkText = "SOME_SELENDROID_EXTERNALLY_DEFINED_LOCATOR") private ExtendedAndroidWidget singleOverriddenAndroidWidget; /** - * The superclass is annotated by {@link AndroidFindBy} and {@link SelendroidFindBy}. + * The superclass is annotated by {@link AndroidFindBy} * However there is the field which is annotated by this annotations. * This field was added to check that locator is * created correctly according to current platform and current automation and * annotations that mark the field. */ @AndroidFindBy(uiAutomator = "SOME_ANDROID_EXTERNALLY_DEFINED_LOCATOR") - @SelendroidFindBy(linkText = "SOME_SELENDROID_EXTERNALLY_DEFINED_LOCATOR") private List multipleOverriddenAndroidWidgets; @Override 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 7985e67c4..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 checkCaseWhenBothWidgetFieldAndClassHaveDelaredAnnotations() { + 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/android/AnnotatedAndroidWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AnnotatedAndroidWidget.java index 2b1706e78..b64b3361d 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AnnotatedAndroidWidget.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/AnnotatedAndroidWidget.java @@ -1,14 +1,11 @@ package io.appium.java_client.pagefactory_tests.widget.tests.android; import io.appium.java_client.pagefactory.AndroidFindBy; -import io.appium.java_client.pagefactory.SelendroidFindBy; import org.openqa.selenium.WebElement; @AndroidFindBy(uiAutomator = "SOME_ROOT_LOCATOR") -@SelendroidFindBy(linkText = "SELENDROID_SOME_ROOT_LOCATOR") public class AnnotatedAndroidWidget extends DefaultAndroidWidget { public static String ANDROID_ROOT_WIDGET_LOCATOR = "SOME_ROOT_LOCATOR"; - public static String SELENDROID_ROOT_WIDGET_LOCATOR = "SELENDROID_SOME_ROOT_LOCATOR"; protected AnnotatedAndroidWidget(WebElement element) { super(element); diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/DefaultAndroidWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/DefaultAndroidWidget.java index 461699939..1fcd02771 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/DefaultAndroidWidget.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/DefaultAndroidWidget.java @@ -1,7 +1,6 @@ package io.appium.java_client.pagefactory_tests.widget.tests.android; import io.appium.java_client.pagefactory.AndroidFindBy; -import io.appium.java_client.pagefactory.SelendroidFindBy; import io.appium.java_client.pagefactory_tests.widget.tests.DefaultStubWidget; import org.openqa.selenium.WebElement; @@ -10,14 +9,11 @@ public class DefaultAndroidWidget extends DefaultStubWidget { public static String ANDROID_SUB_WIDGET_LOCATOR = "SOME_SUB_LOCATOR"; - public static String SELENDROID_SUB_WIDGET_LOCATOR = "SELENDROID_SOME_SUB_LOCATOR"; @AndroidFindBy(uiAutomator = "SOME_SUB_LOCATOR") - @SelendroidFindBy(linkText = "SELENDROID_SOME_SUB_LOCATOR") private DefaultAndroidWidget singleWidget; @AndroidFindBy(uiAutomator = "SOME_SUB_LOCATOR") - @SelendroidFindBy(linkText = "SELENDROID_SOME_SUB_LOCATOR") private List multipleWidgets; protected DefaultAndroidWidget(WebElement element) { diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/SelendroidWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/SelendroidWidgetTest.java deleted file mode 100644 index 0166b4f16..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/android/SelendroidWidgetTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.android; - -import static io.appium.java_client.pagefactory_tests.widget.tests.android.AndroidApp.ANDROID_SELENDROID_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.android.AndroidApp.SELENDROID_EXTERNALLY_DEFINED_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.android.AnnotatedAndroidWidget.SELENDROID_ROOT_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.android.DefaultAndroidWidget.SELENDROID_SUB_WIDGET_LOCATOR; -import static org.openqa.selenium.By.linkText; - -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 SelendroidWidgetTest extends ExtendedWidgetTest { - public SelendroidWidgetTest() { - super(new AndroidApp(), new AbstractStubWebDriver.StubSelendroidDriver()); - } - - @Override - public void checkThatWidgetsAreCreatedCorrectly() { - checkThatLocatorsAreCreatedCorrectly(app.getWidget(), app.getWidgets(), - linkText(ANDROID_SELENDROID_WIDGET_LOCATOR), linkText(SELENDROID_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getAnnotatedWidget(), - ((ExtendedApp) app).getAnnotatedWidgets(), - linkText(SELENDROID_ROOT_WIDGET_LOCATOR), linkText(SELENDROID_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidget(), - ((ExtendedApp) app).getExtendedWidgets(), - linkText(SELENDROID_ROOT_WIDGET_LOCATOR), linkText(SELENDROID_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenBothWidgetFieldAndClassHaveDelaredAnnotations() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidgetWithOverriddenLocators(), - ((ExtendedApp) app).getExtendedWidgetsWithOverriddenLocators(), - linkText(SELENDROID_EXTERNALLY_DEFINED_WIDGET_LOCATOR), linkText(SELENDROID_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 c74a0847e..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,76 +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.ios.DefaultIosWidget; -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.StubSelendroidDriver(), - DefaultSelendroidWidget.class), - dataArray(new CombinedApp(), new AbstractStubWebDriver.StubIOSDriver(), DefaultIosWidget.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.StubSelendroidDriver(), - DefaultSelendroidWidget.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()) @@ -83,20 +74,12 @@ public static class CombinedApp implements AbstractApp { @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class, - iOSUIAutomation = DefaultIosWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class - ) + iOSXCUITAutomation = DefaultIosXCUITWidget.class) private DefaultStubWidget singleWidget; @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class, - iOSUIAutomation = DefaultIosWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class - ) + iOSXCUITAutomation = DefaultIosXCUITWidget.class) private List multipleWidget; @Override @@ -113,15 +96,11 @@ public List getWidgets() { public static class PartiallyCombinedApp implements AbstractApp { @OverrideWidget(html = DefaultFindByWidget.class, - androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class - ) + androidUIAutomator = DefaultAndroidWidget.class) private DefaultStubWidget singleWidget; @OverrideWidget(html = DefaultFindByWidget.class, - androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class - ) + androidUIAutomator = DefaultAndroidWidget.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 4d6740f4a..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,81 +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.ios.DefaultIosWidget; -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(), - new AbstractStubWebDriver.StubSelendroidDriver(), DefaultSelendroidWidget.class), - dataArray(new AppWithCombinedWidgets(), - new AbstractStubWebDriver.StubIOSDriver(), DefaultIosWidget.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.StubSelendroidDriver(), DefaultSelendroidWidget.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)); @@ -93,19 +86,13 @@ public static class CombinedWidget extends DefaultStubWidget { @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class, - iOSUIAutomation = DefaultIosWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class + iOSXCUITAutomation = DefaultIosXCUITWidget.class ) private DefaultStubWidget singleWidget; @OverrideWidget(html = DefaultFindByWidget.class, androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class, - iOSUIAutomation = DefaultIosWidget.class, - iOSXCUITAutomation = DefaultIosXCUITWidget.class, - windowsAutomation = DefaultWindowsWidget.class + iOSXCUITAutomation = DefaultIosXCUITWidget.class ) private List multipleWidget; @@ -128,15 +115,11 @@ public List getSubWidgets() { public static class PartiallyCombinedWidget extends DefaultStubWidget { @OverrideWidget(html = DefaultFindByWidget.class, - androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class - ) + androidUIAutomator = DefaultAndroidWidget.class) private DefaultStubWidget singleWidget; @OverrideWidget(html = DefaultFindByWidget.class, - androidUIAutomator = DefaultAndroidWidget.class, - selendroid = DefaultSelendroidWidget.class - ) + androidUIAutomator = DefaultAndroidWidget.class) private List multipleWidget; @@ -188,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/combined/DefaultIosXCUITWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultIosXCUITWidget.java index e82cebe37..bb4f58037 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultIosXCUITWidget.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultIosXCUITWidget.java @@ -8,6 +8,8 @@ public class DefaultIosXCUITWidget extends DefaultStubWidget { + public static String XCUIT_SUB_WIDGET_LOCATOR = "XCUIT_SOME_SUB_LOCATOR"; + @iOSXCUITFindBy(iOSNsPredicate = "XCUIT_SOME_SUB_LOCATOR") private DefaultIosXCUITWidget singleWidget; diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultSelendroidWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultSelendroidWidget.java deleted file mode 100644 index 34ff23f68..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/combined/DefaultSelendroidWidget.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.combined; - -import io.appium.java_client.pagefactory.SelendroidFindBy; -import io.appium.java_client.pagefactory_tests.widget.tests.DefaultStubWidget; -import org.openqa.selenium.WebElement; - -import java.util.List; - -public class DefaultSelendroidWidget extends DefaultStubWidget { - - @SelendroidFindBy(linkText = "SELENDROID_SOME_SUB_LOCATOR") - private DefaultSelendroidWidget singleWidget; - - @SelendroidFindBy(linkText = "SELENDROID_SOME_SUB_LOCATOR") - private List multipleWidgets; - - protected DefaultSelendroidWidget(WebElement element) { - super(element); - } - - @Override - public DefaultSelendroidWidget getSubWidget() { - return singleWidget; - } - - @Override - public List getSubWidgets() { - return multipleWidgets; - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/AnnotatedIosWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/AnnotatedIosWidget.java index f1eaa796e..b8978d89f 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/AnnotatedIosWidget.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/AnnotatedIosWidget.java @@ -1,13 +1,11 @@ package io.appium.java_client.pagefactory_tests.widget.tests.ios; -import io.appium.java_client.pagefactory.iOSFindBy; import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.pagefactory_tests.widget.tests.combined.DefaultIosXCUITWidget; import org.openqa.selenium.WebElement; -@iOSFindBy(uiAutomator = "SOME_ROOT_LOCATOR") @iOSXCUITFindBy(iOSNsPredicate = "XCUIT_SOME_ROOT_LOCATOR") -public class AnnotatedIosWidget extends DefaultIosWidget { - public static String IOS_ROOT_WIDGET_LOCATOR = "SOME_ROOT_LOCATOR"; +public class AnnotatedIosWidget extends DefaultIosXCUITWidget { public static String XCUIT_ROOT_WIDGET_LOCATOR = "XCUIT_SOME_ROOT_LOCATOR"; protected AnnotatedIosWidget(WebElement element) { diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/DefaultIosWidget.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/DefaultIosWidget.java deleted file mode 100644 index 61c75880a..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/DefaultIosWidget.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.ios; - -import io.appium.java_client.pagefactory.iOSFindBy; -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 DefaultIosWidget extends DefaultStubWidget { - - public static String IOS_SUB_WIDGET_LOCATOR = "SOME_SUB_LOCATOR"; - public static String XCUIT_SUB_WIDGET_LOCATOR = "XCUIT_SOME_SUB_LOCATOR"; - - @iOSFindBy(uiAutomator = "SOME_SUB_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "XCUIT_SOME_SUB_LOCATOR") - private DefaultIosWidget singleWidget; - - @iOSFindBy(uiAutomator = "SOME_SUB_LOCATOR") - @iOSXCUITFindBy(iOSNsPredicate = "XCUIT_SOME_SUB_LOCATOR") - private List multipleWidgets; - - protected DefaultIosWidget(WebElement element) { - super(element); - } - - @Override - public DefaultIosWidget getSubWidget() { - return singleWidget; - } - - @Override - public List getSubWidgets() { - return multipleWidgets; - } -} diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/IosApp.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/IosApp.java index 8fbc1643a..beaa141c0 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/IosApp.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/IosApp.java @@ -1,8 +1,8 @@ package io.appium.java_client.pagefactory_tests.widget.tests.ios; -import io.appium.java_client.pagefactory.iOSFindBy; import io.appium.java_client.pagefactory.iOSXCUITFindBy; import io.appium.java_client.pagefactory_tests.widget.tests.ExtendedApp; +import io.appium.java_client.pagefactory_tests.widget.tests.combined.DefaultIosXCUITWidget; import java.util.List; @@ -14,82 +14,72 @@ public class IosApp implements ExtendedApp { public static String IOS_EXTERNALLY_DEFINED_WIDGET_LOCATOR = "IOS_EXTERNALLY_DEFINED_WIDGET_LOCATOR"; public static String XCUIT_EXTERNALLY_DEFINED_WIDGET_LOCATOR = "SOME_XCUIT_EXTERNALLY_DEFINED_LOCATOR"; - @iOSFindBy(uiAutomator = "SOME_IOS_DEFAULT_LOCATOR") @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_DEFAULT_LOCATOR") - private DefaultIosWidget singleIosWidget; + private DefaultIosXCUITWidget singleIosWidget; - @iOSFindBy(uiAutomator = "SOME_IOS_DEFAULT_LOCATOR") @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_DEFAULT_LOCATOR") - private List multipleIosWidgets; + private List multipleIosWidgets; /** - * This class is annotated by {@link io.appium.java_client.pagefactory.iOSFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. + * This class is annotated by {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. * This field was added to check that locator is created correctly according to current platform * and current automation. */ private AnnotatedIosWidget singleAnnotatedIosWidget; /** - * This class is annotated by {@link io.appium.java_client.pagefactory.iOSFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. + * This class is annotated by {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. * This field was added to check that locator is created correctly according to current platform * and current automation. */ private List multipleIosIosWidgets; /** - * This class is not annotated by {@link io.appium.java_client.pagefactory.iOSFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. + * This class is not annotated by {@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 and current automation. */ private ExtendedIosWidget singleExtendedIosWidget; /** - * This class is not annotated by {@link io.appium.java_client.pagefactory.iOSFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. + * This class is not annotated by {@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 and current automation. */ private List multipleExtendedIosWidgets; /** - * The superclass is annotated by {@link io.appium.java_client.pagefactory.iOSFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. + * The superclass is annotated by {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. * However there is the field which is annotated by this annotations. * This field was added to check that locator is * created correctly according to current platform and current automation and * annotations that mark the field. */ - @iOSFindBy(uiAutomator = "IOS_EXTERNALLY_DEFINED_WIDGET_LOCATOR") @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_EXTERNALLY_DEFINED_LOCATOR") private ExtendedIosWidget singleOverriddenIosWidget; /** - * The superclass is annotated by {@link io.appium.java_client.pagefactory.iOSFindBy} and - * {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. + * The superclass is annotated by {@link io.appium.java_client.pagefactory.iOSXCUITFindBy}. * However there is the field which is annotated by this annotations. * This field was added to check that locator is * created correctly according to current platform and current automation and * annotations that mark the field. */ - @iOSFindBy(uiAutomator = "IOS_EXTERNALLY_DEFINED_WIDGET_LOCATOR") @iOSXCUITFindBy(iOSNsPredicate = "SOME_XCUIT_EXTERNALLY_DEFINED_LOCATOR") private List multipleOverriddenIosWidgets; @Override - public DefaultIosWidget getWidget() { + public DefaultIosXCUITWidget getWidget() { return singleIosWidget; } @Override - public List getWidgets() { + public List getWidgets() { return multipleIosWidgets; } @Override - public DefaultIosWidget getAnnotatedWidget() { + public DefaultIosXCUITWidget getAnnotatedWidget() { return singleAnnotatedIosWidget; } @@ -99,7 +89,7 @@ public List getAnnotatedWidgets() { } @Override - public DefaultIosWidget getExtendedWidget() { + public DefaultIosXCUITWidget getExtendedWidget() { return singleExtendedIosWidget; } @@ -109,7 +99,7 @@ public List getExtendedWidgets() { } @Override - public DefaultIosWidget getExtendedWidgetWithOverriddenLocators() { + public DefaultIosXCUITWidget getExtendedWidgetWithOverriddenLocators() { return singleOverriddenIosWidget; } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/IosWidgetTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/IosWidgetTest.java deleted file mode 100644 index ad84fef59..000000000 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/ios/IosWidgetTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.appium.java_client.pagefactory_tests.widget.tests.ios; - -import static io.appium.java_client.MobileBy.IosUIAutomation; -import static io.appium.java_client.pagefactory_tests.widget.tests.ios.AnnotatedIosWidget.IOS_ROOT_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.ios.DefaultIosWidget.IOS_SUB_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.ios.IosApp.IOS_DEFAULT_WIDGET_LOCATOR; -import static io.appium.java_client.pagefactory_tests.widget.tests.ios.IosApp.IOS_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 IosWidgetTest extends ExtendedWidgetTest { - - public IosWidgetTest() { - super(new IosApp(), new AbstractStubWebDriver.StubIOSDriver()); - } - - @Override - public void checkThatWidgetsAreCreatedCorrectly() { - checkThatLocatorsAreCreatedCorrectly(app.getWidget(), app.getWidgets(), - IosUIAutomation(IOS_DEFAULT_WIDGET_LOCATOR), IosUIAutomation(IOS_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenWidgetClassHasDeclaredLocatorAnnotation() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getAnnotatedWidget(), - ((ExtendedApp) app).getAnnotatedWidgets(), - IosUIAutomation(IOS_ROOT_WIDGET_LOCATOR), IosUIAutomation(IOS_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidget(), - ((ExtendedApp) app).getExtendedWidgets(), - IosUIAutomation(IOS_ROOT_WIDGET_LOCATOR), IosUIAutomation(IOS_SUB_WIDGET_LOCATOR)); - } - - @Override - public void checkCaseWhenBothWidgetFieldAndClassHaveDelaredAnnotations() { - checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidgetWithOverriddenLocators(), - ((ExtendedApp) app).getExtendedWidgetsWithOverriddenLocators(), - IosUIAutomation(IOS_EXTERNALLY_DEFINED_WIDGET_LOCATOR), IosUIAutomation(IOS_SUB_WIDGET_LOCATOR)); - } -} 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 c55365610..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.DefaultIosWidget.XCUIT_SUB_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,8 +40,9 @@ public void checkCaseWhenWidgetClassHasNoDeclaredAnnotationButItHasSuperclass() iOSNsPredicateString(XCUIT_ROOT_WIDGET_LOCATOR), iOSNsPredicateString(XCUIT_SUB_WIDGET_LOCATOR)); } + @Test @Override - public void checkCaseWhenBothWidgetFieldAndClassHaveDelaredAnnotations() { + public void checkCaseWhenBothWidgetFieldAndClassHaveDeclaredAnnotations() { checkThatLocatorsAreCreatedCorrectly(((ExtendedApp) app).getExtendedWidgetWithOverriddenLocators(), ((ExtendedApp) app).getExtendedWidgetsWithOverriddenLocators(), iOSNsPredicateString(XCUIT_EXTERNALLY_DEFINED_WIDGET_LOCATOR), 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 5358b29dc..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 checkCaseWhenBothWidgetFieldAndClassHaveDelaredAnnotations() { - 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/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/utils/ScreenshotStateTests.java b/src/test/java/io/appium/java_client/utils/ScreenshotStateTests.java deleted file mode 100644 index 1f730a60e..000000000 --- a/src/test/java/io/appium/java_client/utils/ScreenshotStateTests.java +++ /dev/null @@ -1,150 +0,0 @@ -package io.appium.java_client.utils; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.lessThan; -import static org.junit.Assert.assertThat; - -import io.appium.java_client.ScreenshotState; -import org.junit.Before; -import org.junit.Test; - -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.time.Duration; -import java.util.Random; - -public class ScreenshotStateTests { - private static final Random rand = new Random(); - private static final Duration ONE_SECOND = Duration.ofSeconds(1); - private static final double MAX_SCORE = 0.999; - - private ImagesGenerator randomImageOfStaticSize; - private ImagesGenerator randomImageOfRandomSize; - private ImagesGenerator staticImage; - - private static class ImagesGenerator { - private boolean isRandom; - private boolean isSizeStatic; - - private static final int DEFAULT_WIDTH = 100; - private static final int MIN_WIDTH = 50; - private static final int DEFAULT_HEIGHT = 100; - private static final int MIN_HEIGHT = 50; - - ImagesGenerator(boolean isRandom, boolean isSizeStatic) { - this.isRandom = isRandom; - this.isSizeStatic = isSizeStatic; - } - - private BufferedImage generate() { - final int width = isSizeStatic ? DEFAULT_WIDTH : MIN_WIDTH + rand.nextInt(DEFAULT_WIDTH); - final int height = isSizeStatic ? DEFAULT_HEIGHT : MIN_HEIGHT + rand.nextInt(DEFAULT_HEIGHT); - final BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - final Graphics2D g2 = result.createGraphics(); - try { - g2.setColor(isRandom ? new Color(rand.nextInt(256), rand.nextInt(256), - rand.nextInt(256)) : Color.red); - g2.fill(new Rectangle2D.Float(0, 0, - isRandom ? rand.nextInt(DEFAULT_WIDTH) : DEFAULT_WIDTH / 2, - isRandom ? rand.nextInt(DEFAULT_HEIGHT) : DEFAULT_HEIGHT / 2)); - } finally { - g2.dispose(); - } - return result; - } - } - - @Before - public void setUp() { - randomImageOfStaticSize = new ImagesGenerator(true, true); - randomImageOfRandomSize = new ImagesGenerator(true, false); - staticImage = new ImagesGenerator(false, true); - } - - //region Positive Tests - @Test - public void testBasicComparisonScenario() { - final ScreenshotState currentState = new ScreenshotState(staticImage::generate) - .remember(); - assertThat(currentState.verifyNotChanged(ONE_SECOND, MAX_SCORE), is(notNullValue())); - } - - @Test - public void testChangedImageVerification() { - final ScreenshotState currentState = new ScreenshotState(randomImageOfStaticSize::generate) - .remember(); - assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE), is(notNullValue())); - } - - @Test - public void testChangedImageVerificationWithDifferentSize() { - final ScreenshotState currentState = new ScreenshotState(randomImageOfRandomSize::generate) - .remember(); - assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE, - ScreenshotState.ResizeMode.REFERENCE_TO_TEMPLATE_RESOLUTION), is(notNullValue())); - } - - @Test - public void testChangedImageVerificationWithCustomRememberedImage() { - final ScreenshotState currentState = new ScreenshotState(randomImageOfRandomSize::generate) - .remember(randomImageOfRandomSize.generate()); - assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE, - ScreenshotState.ResizeMode.REFERENCE_TO_TEMPLATE_RESOLUTION), is(notNullValue())); - } - - @Test - public void testChangedImageVerificationWithCustomInterval() { - final ScreenshotState currentState = new ScreenshotState(randomImageOfRandomSize::generate) - .setComparisonInterval(Duration.ofMillis(100)).remember(); - assertThat(currentState.verifyChanged(ONE_SECOND, MAX_SCORE, - ScreenshotState.ResizeMode.REFERENCE_TO_TEMPLATE_RESOLUTION), is(notNullValue())); - } - - @Test - public void testDirectOverlapScoreCalculation() { - final BufferedImage anImage = staticImage.generate(); - final double score = ScreenshotState.getOverlapScore(anImage, anImage); - assertThat(score, is(greaterThanOrEqualTo(MAX_SCORE))); - } - - @Test - public void testScreenshotComparisonUsingStaticMethod() { - BufferedImage img1 = randomImageOfStaticSize.generate(); - // ImageIO.write(img1, "png", new File("img1.png")); - BufferedImage img2 = randomImageOfStaticSize.generate(); - // ImageIO.write(img2, "png", new File("img2.png")); - assertThat(ScreenshotState.getOverlapScore(img1, img2), is(lessThan(MAX_SCORE))); - } - //endregion - - //region Negative Tests - @Test(expected = ScreenshotState.ScreenshotComparisonError.class) - public void testDifferentSizeOfTemplates() { - new ScreenshotState(randomImageOfRandomSize::generate).remember().verifyChanged(ONE_SECOND, MAX_SCORE); - } - - @Test(expected = NullPointerException.class) - public void testInvalidProvider() { - new ScreenshotState(() -> null).remember(); - } - - @Test(expected = ScreenshotState.ScreenshotComparisonTimeout.class) - public void testImagesComparisonExpectationFailed() { - new ScreenshotState(randomImageOfStaticSize::generate).remember().verifyNotChanged(ONE_SECOND, MAX_SCORE); - } - - @Test(expected = ScreenshotState.ScreenshotComparisonTimeout.class) - public void testImagesComparisonExpectationFailed2() { - new ScreenshotState(staticImage::generate).remember().verifyChanged(ONE_SECOND, MAX_SCORE); - } - - @Test(expected = ScreenshotState.ScreenshotComparisonError.class) - public void testScreenshotInitialStateHasNotBeenRemembered() { - new ScreenshotState(staticImage::generate).verifyNotChanged(ONE_SECOND, MAX_SCORE); - } - //endregion -} 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/java/io/appium/java_client/hello appium - saved page.htm b/src/test/resources/html/hello appium - saved page.htm similarity index 100% rename from src/test/java/io/appium/java_client/hello appium - saved page.htm rename to src/test/resources/html/hello appium - saved page.htm