diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 0d99a23f2..a324a9b7d 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -1,23 +1,30 @@ -# https://github.com/actions/virtual-environments/tree/main/images/macos +# https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml variables: - MIN_VM_IMAGE: macOS-10.15 - MIN_XCODE_VERSION: 12.1 - # Xcode 10.0 did not have Apple TV - MIN_TV_XCODE_VERSION: 12.1 - MIN_TV_PLATFORM_VERSION: 14.0 - MIN_TV_DEVICE_NAME: Apple TV 4K - MIN_PLATFORM_VERSION: 14.1 + MIN_VM_IMAGE: macOS-12 + MIN_XCODE_VERSION: 13.1 + MIN_PLATFORM_VERSION: 15.0 + MIN_TV_PLATFORM_VERSION: 15.0 + MIN_TV_DEVICE_NAME: Apple TV 4K (2nd generation) MIN_IPHONE_DEVICE_NAME: iPhone 11 - MIN_IPAD_DEVICE_NAME: iPad Pro (11-inch) (2nd generation) - MAX_VM_IMAGE: macOS-11 - MAX_XCODE_VERSION: 13.2 - MAX_PLATFORM_VERSION: 15.2 - MAX_PLATFORM_VERSION_TV: 15.2 - MAX_IPHONE_DEVICE_NAME: iPhone 11 Pro Max - MAX_TV_DEVICE_NAME: Apple TV 4K - MAX_IPAD_DEVICE_NAME: iPad Pro (11-inch) (2nd generation) - DEFAULT_NODE_VERSION: "16.x" + MIN_IPAD_DEVICE_NAME: iPad Pro (11-inch) (3rd generation) + MAX_VM_IMAGE: macOS-12 + MAX_XCODE_VERSION: 14.2 + MAX_PLATFORM_VERSION: 16.2 + MAX_PLATFORM_VERSION_TV: 16.1 + MAX_IPHONE_DEVICE_NAME: iPhone 13 + MAX_TV_DEVICE_NAME: Apple TV 4K (2nd generation) + MAX_IPAD_DEVICE_NAME: iPad Pro (11-inch) (3rd generation) + DEFAULT_NODE_VERSION: "18.x" +trigger: + batch: true + branches: + include: [master] + +pr: + autoCancel: true + branches: + include: [master] pool: vmImage: "$(MAX_VM_IMAGE)" @@ -44,14 +51,6 @@ parameters: stages: - stage: Unit_Tests_And_Linters jobs: - - job: Node_Unit_Tests - steps: - - template: azure-templates/node_setup_steps.yml - - script: | - npm install -g appium@next - npm install - - script: npm run test - # region Build - template: ./azure-templates/base_job.yml parameters: @@ -135,7 +134,7 @@ stages: dest: tv tvModel: $(MIN_TV_DEVICE_NAME) tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_TV_XCODE_VERSION) + xcodeVersion: $(MIN_XCODE_VERSION) vmImage: $(MIN_VM_IMAGE) # endregion @@ -212,7 +211,7 @@ stages: sdk: tv_sim tvModel: $(MIN_TV_DEVICE_NAME) tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_TV_XCODE_VERSION) + xcodeVersion: $(MIN_XCODE_VERSION) vmImage: $(MIN_VM_IMAGE) - template: ./azure-templates/base_job.yml parameters: @@ -222,7 +221,7 @@ stages: sdk: tv_sim tvModel: $(MIN_TV_DEVICE_NAME) tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_TV_XCODE_VERSION) + xcodeVersion: $(MIN_XCODE_VERSION) vmImage: $(MIN_VM_IMAGE) # endregion @@ -295,24 +294,13 @@ stages: sdk: tv_sim tvModel: $(MIN_TV_DEVICE_NAME) tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_TV_XCODE_VERSION) + xcodeVersion: $(MIN_XCODE_VERSION) vmImage: $(MIN_VM_IMAGE) # endregion - stage: Integration_Tests jobs: - - job: Node_Integration_Tests - variables: - XCODE_VERSION: $(MAX_XCODE_VERSION) - DEVICE_NAME: $(MAX_IPHONE_DEVICE_NAME) - PLATFORM_VERSION: $(MAX_PLATFORM_VERSION) - steps: - - template: azure-templates/node_setup_steps.yml - - script: npm install - - template: azure-templates/bootstrap_steps.yml - - script: npm run e2e-test - # region Integration Tests Max Xcode - ${{ each job in parameters.integrationJobs }}: - template: ./azure-templates/base_job.yml diff --git a/.eslintignore b/.eslintignore index 4671072fa..84930aead 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ Resources coverage +build diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 47415a57c..000000000 --- a/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@appium/eslint-config-appium" -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..3d7db0846 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "extends": ["@appium/eslint-config-appium-ts"], + "overrides": [ + { + "files": "test/**/*.js", + "rules": { + "func-names": "off", + "@typescript-eslint/no-var-requires": "off" + } + }, + { + "files": "Scripts/**/*.js", + "parserOptions": {"sourceType": "script"}, + "rules": { + "@typescript-eslint/no-var-requires": "off" + } + }, + { + "files": "ci-jobs/scripts/*.js", + "parserOptions": {"sourceType": "script"}, + "rules": { + "@typescript-eslint/no-var-requires": "off" + } + } + ], + "rules": { + "require-await": "error" + } +} diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml new file mode 100644 index 000000000..e5c1ebad9 --- /dev/null +++ b/.github/workflows/functional-test.yml @@ -0,0 +1,36 @@ +name: Functional Tests + +on: [pull_request] + + +jobs: + test: + env: + CI: true + _FORCE_LOGS: 1 + XCODE_VERSION: 13.4 + DEVICE_NAME: iPhone 11 + PLATFORM_VERSION: 15.5 + # https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md + runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "${{ env.XCODE_VERSION }}" + - run: | + npm install + mkdir -p ./Resources/WebDriverAgent.bundle + name: Install dev dependencies + + - run: | + target_sim_id=$(xcrun simctl list devices available | grep "$DEVICE_NAME (" | cut -d "(" -f2 | cut -d ")" -f1) + open -Fn "$(xcode-select -p)/Applications/Simulator.app" + xcrun simctl bootstatus $target_sim_id -b + name: Preboot Simulator + + - run: npm run e2e-test + name: Run functional tests diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 000000000..9a192aec5 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,15 @@ +name: Conventional Commits +on: + pull_request: + + +jobs: + lint: + name: https://www.conventionalcommits.org + runs-on: ubuntu-latest + steps: + - uses: beemojs/conventional-pr-action@v2 + with: + config-preset: angular + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.js.yml b/.github/workflows/publish.js.yml index 89c0ab731..92299e7a8 100644 --- a/.github/workflows/publish.js.yml +++ b/.github/workflows/publish.js.yml @@ -10,21 +10,67 @@ on: jobs: build: - runs-on: macos-latest + runs-on: macos-13 + + env: + XCODE_VERSION: 14.3.1 + ZIP_PKG_NAME_IOS: "WebDriverAgentRunner-Runner.zip" + PKG_PATH_IOS: "appium_wda_ios" + ZIP_PKG_NAME_TVOS: "WebDriverAgentRunner_tvOS-Runner.zip" + PKG_PATH_TVOS: "appium_wda_tvos" + steps: - uses: actions/checkout@v2 - - name: Use Node.js 16.x - uses: actions/setup-node@v1 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: lts/* + - uses: maxim-lobanov/setup-xcode@v1 with: - node-version: 16.x - - run: | - npm install -g appium@next - npm install --no-package-lock + xcode-version: "${{ env.XCODE_VERSION }}" + - run: npm install --no-package-lock name: Install dev dependencies - - run: npm test - name: Run NPM Test + - run: npm run build + name: Run build + - run: npm run test + name: Run test + + # building WDA packages + - name: Build iOS + run: | + xcodebuild clean build-for-testing \ + -project WebDriverAgent.xcodeproj \ + -derivedDataPath $PKG_PATH_IOS \ + -scheme WebDriverAgentRunner \ + -destination generic/platform=iOS \ + CODE_SIGNING_ALLOWED=NO ARCHS=arm64 + - name: Creating a zip of WebDriverAgentRunner-Runner.app for iOS after removing test frameworks + run: | + pushd appium_wda_ios/Build/Products/Debug-iphoneos + rm -rf WebDriverAgentRunner-Runner.app/Frameworks/XC*.framework + zip -r $ZIP_PKG_NAME_IOS WebDriverAgentRunner-Runner.app + popd + mv $PKG_PATH_IOS/Build/Products/Debug-iphoneos/$ZIP_PKG_NAME_IOS ./ + - name: Build tvOS + run: | + xcodebuild clean build-for-testing \ + -project WebDriverAgent.xcodeproj \ + -derivedDataPath $PKG_PATH_TVOS \ + -scheme WebDriverAgentRunner_tvOS \ + -destination generic/platform=tvOS \ + CODE_SIGNING_ALLOWED=NO ARCHS=arm64 + - name: Creating a zip of WebDriverAgentRunner-Runner.app for tvOS after removing test frameworks + run: | + pushd appium_wda_tvos/Build/Products/Debug-appletvos + rm -rf WebDriverAgentRunner_tvOS-Runner.app/Frameworks/XC*.framework + zip -r $ZIP_PKG_NAME_TVOS WebDriverAgentRunner_tvOS-Runner.app + popd + mv $PKG_PATH_TVOS/Build/Products/Debug-appletvos/$ZIP_PKG_NAME_TVOS ./ + + # release tasks - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} name: Release + diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 000000000..ef98e4ca6 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,33 @@ +name: Unit Tests + +on: [pull_request, push] + + +jobs: + prepare_matrix: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.generate-matrix.outputs.versions }} + steps: + - name: Select 3 most recent LTS versions of Node.js + id: generate-matrix + run: echo "versions=$(curl -s https://endoflife.date/api/nodejs.json | jq -c '[[.[] | select(.lts != false)][:3] | .[].cycle | tonumber]')" >> "$GITHUB_OUTPUT" + + test: + needs: + - prepare_matrix + strategy: + matrix: + node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }} + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm install --no-package-lock + name: Install dev dependencies + - run: npm run lint + name: Run linter + - run: npm run test + name: Run unit tests diff --git a/.github/workflows/wda-package.yml b/.github/workflows/wda-package.yml new file mode 100644 index 000000000..b4b4223f2 --- /dev/null +++ b/.github/workflows/wda-package.yml @@ -0,0 +1,95 @@ +name: Building WebDriverAgent + +on: + workflow_dispatch: + workflow_run: + workflows: ["Release"] + types: + - completed + +env: + HOST: macos-13 + XCODE_VERSION: 14.3.1 + DESTINATION_SIM: platform=iOS Simulator,name=iPhone 14 Pro + DESTINATION_SIM_tvOS: platform=tvOS Simulator,name=Apple TV + +jobs: + host_machine: + runs-on: ubuntu-latest + outputs: + host: ${{ steps.macos_host.outputs.host }} + steps: + - run: | + echo "host=${{ env.HOST }}" >> $GITHUB_OUTPUT + id: macos_host + + for_real_devices: + needs: [host_machine] + name: Build WDA for real iOS and tvOS devices + runs-on: ${{ needs.host_machine.outputs.host }} + + env: + ZIP_PKG_NAME_IOS: "WebDriverAgentRunner-Runner.zip" + ZIP_PKG_NAME_TVOS: "WebDriverAgentRunner_tvOS-Runner.zip" + + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "${{ env.XCODE_VERSION }}" + - name: Create a zip file of WebDriverAgentRunner-Runner.app for iOS + run: sh $GITHUB_WORKSPACE/Scripts/ci/build-real.sh + env: + DERIVED_DATA_PATH: appium_wda_ios + SCHEME: WebDriverAgentRunner + DESTINATION: generic/platform=iOS + WD: appium_wda_ios/Build/Products/Debug-iphoneos + ZIP_PKG_NAME: "${{ env.ZIP_PKG_NAME_IOS }}" + + - name: Create a zip file of WebDriverAgentRunner-Runner.app for tvOS + run: sh $GITHUB_WORKSPACE/Scripts/ci/build-real.sh + env: + DERIVED_DATA_PATH: appium_wda_tvos + SCHEME: WebDriverAgentRunner_tvOS + DESTINATION: generic/platform=tvOS + WD: appium_wda_tvos/Build/Products/Debug-appletvos + ZIP_PKG_NAME: "${{ env.ZIP_PKG_NAME_TVOS }}" + + - name: Upload the built generic app package for iOS + uses: actions/upload-artifact@v3.1.0 + with: + path: "${{ env.ZIP_PKG_NAME_IOS }}" + - name: Upload the built generic app package for tvOS + uses: actions/upload-artifact@v3.1.0 + with: + path: "${{ env.ZIP_PKG_NAME_TVOS }}" + + for_simulator_devices: + needs: [host_machine] + name: Build WDA for ${{ matrix.target }} simulators + runs-on: ${{ needs.host_machine.outputs.host }} + + strategy: + matrix: + # '' is for iOS + target: ['', '_tvOS'] + arch: [x86_64, arm64] + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "${{ env.XCODE_VERSION }}" + - name: Create a zip of WebDriverAgentRunner${{ matrix.target }} for simulator for ${{ matrix.arch }} + run: | + DESTINATION=$DESTINATION_SIM${{ matrix.target }} sh $GITHUB_WORKSPACE/Scripts/ci/build-sim.sh + env: + TARGET: ${{ matrix.target }} + SCHEME: WebDriverAgentRunner${{ matrix.target }} + ARCHS: ${{ matrix.arch }} + ZIP_PKG_NAME: "WebDriverAgentRunner${{ matrix.target }}-Build-Sim-${{ matrix.arch }}.zip" + - name: Upload the built generic app package for WebDriverAgentRunner${{ matrix.target }} with ${{ matrix.arch }} + uses: actions/upload-artifact@v3.1.0 + with: + path: "WebDriverAgentRunner${{ matrix.target }}-Build-Sim-${{ matrix.arch }}.zip" diff --git a/.mocharc.js b/.mocharc.js new file mode 100644 index 000000000..40599e9df --- /dev/null +++ b/.mocharc.js @@ -0,0 +1,4 @@ +module.exports = { + require: ['ts-node/register'], + forbidOnly: Boolean(process.env.CI) +}; diff --git a/.releaserc b/.releaserc index 997c559d8..0cb17f220 100644 --- a/.releaserc +++ b/.releaserc @@ -4,15 +4,38 @@ "preset": "angular", "releaseRules": [ {"type": "chore", "release": "patch"} - ] }], + ["@semantic-release/release-notes-generator", { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + {"type": "feat", "section": "Features"}, + {"type": "fix", "section": "Bug Fixes"}, + {"type": "perf", "section": "Performance Improvements"}, + {"type": "revert", "section": "Reverts"}, + {"type": "chore", "section": "Miscellaneous Chores"}, + {"type": "refactor", "section": "Code Refactoring"}, + {"type": "docs", "section": "Documentation", "hidden": true}, + {"type": "style", "section": "Styles", "hidden": true}, + {"type": "test", "section": "Tests", "hidden": true}, + {"type": "build", "section": "Build System", "hidden": true}, + {"type": "ci", "section": "Continuous Integration", "hidden": true} + ] + } + }], + ["@semantic-release/changelog", { + "changelogFile": "CHANGELOG.md" + }], "@semantic-release/npm", - "@semantic-release/release-notes-generator", ["@semantic-release/git", { - "assets": ["docs", "package.json"], + "assets": ["docs", "package.json", "CHANGELOG.md"], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" }], - "@semantic-release/github", + ["@semantic-release/github", { + "assets": [ + "WebDriverAgentRunner-Runner.zip", + "WebDriverAgentRunner_tvOS-Runner.zip" + ]}] ] } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e4eb6d86a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,607 @@ +## [5.15.5](https://github.com/appium/WebDriverAgent/compare/v5.15.4...v5.15.5) (2023-12-13) + + +### Miscellaneous Chores + +* use appearance for get as well if available ([#825](https://github.com/appium/WebDriverAgent/issues/825)) ([89e233d](https://github.com/appium/WebDriverAgent/commit/89e233d8aef5a19491785fee0823fd8eddbd5fcc)) + +## [5.15.4](https://github.com/appium/WebDriverAgent/compare/v5.15.3...v5.15.4) (2023-12-07) + + +### Bug Fixes + +* set appearance in iOS 17+ ([#818](https://github.com/appium/WebDriverAgent/issues/818)) ([357a2cb](https://github.com/appium/WebDriverAgent/commit/357a2cbca106daf42bc892b251802bfa00895598)) + +## [5.15.3](https://github.com/appium/WebDriverAgent/compare/v5.15.2...v5.15.3) (2023-11-24) + + +### Miscellaneous Chores + +* Make xcodebuild error message more helpful ([#816](https://github.com/appium/WebDriverAgent/issues/816)) ([2d7fc03](https://github.com/appium/WebDriverAgent/commit/2d7fc0370b30e5e3adc9a13002fa95f607c4c160)) + +## [5.15.2](https://github.com/appium/WebDriverAgent/compare/v5.15.1...v5.15.2) (2023-11-23) + + +### Bug Fixes + +* fix run test ci ([#814](https://github.com/appium/WebDriverAgent/issues/814)) ([014d04d](https://github.com/appium/WebDriverAgent/commit/014d04df956e47fef67938b089511e80d344f007)) + + +### Miscellaneous Chores + +* a dummy commit to check a package release ([08388fd](https://github.com/appium/WebDriverAgent/commit/08388fd602ee9d588a8780e8d141d748813782ed)) + +## [5.15.1](https://github.com/appium/WebDriverAgent/compare/v5.15.0...v5.15.1) (2023-11-16) + + +### Bug Fixes + +* Content-Type of the MJPEG server ([b4704da](https://github.com/appium/WebDriverAgent/commit/b4704dafc4567e1f0dc8675facfc48a195aae4bf)) + + +### Code Refactoring + +* Optimize screenshots preprocessing ([#812](https://github.com/appium/WebDriverAgent/issues/812)) ([0b41757](https://github.com/appium/WebDriverAgent/commit/0b41757c0d21004afab32860b4e510d4bc426018)) + +## [5.15.0](https://github.com/appium/WebDriverAgent/compare/v5.14.0...v5.15.0) (2023-11-16) + + +### Features + +* Add element attributes to the performAccessibilityAudit output ([#808](https://github.com/appium/WebDriverAgent/issues/808)) ([0d7e4a6](https://github.com/appium/WebDriverAgent/commit/0d7e4a697adb7355279583eaa05118f396056e6f)) + +## [5.14.0](https://github.com/appium/WebDriverAgent/compare/v5.13.3...v5.14.0) (2023-11-10) + + +### Features + +* use khidusage_keyboardclear to `clear` for iOS/iPad as the 1st attempt, tune tvOS ([#811](https://github.com/appium/WebDriverAgent/issues/811)) ([dd093ea](https://github.com/appium/WebDriverAgent/commit/dd093ea0b7209c3d2f3d0b1fa7f3a7b58507dd2d)) + +## [5.13.3](https://github.com/appium/WebDriverAgent/compare/v5.13.2...v5.13.3) (2023-11-10) + + +### Bug Fixes + +* unrecognized selector sent to instance 0x2829adb20 error in clear ([#809](https://github.com/appium/WebDriverAgent/issues/809)) ([79832bc](https://github.com/appium/WebDriverAgent/commit/79832bc6c69e289091fbbb97aee6a1f1d17ca4c3)) + +## [5.13.2](https://github.com/appium/WebDriverAgent/compare/v5.13.1...v5.13.2) (2023-11-06) + + +### Miscellaneous Chores + +* **deps-dev:** bump @types/sinon from 10.0.20 to 17.0.0 ([#805](https://github.com/appium/WebDriverAgent/issues/805)) ([824f74c](https://github.com/appium/WebDriverAgent/commit/824f74c69769973858350bd5db0061510c546b09)) + +## [5.13.1](https://github.com/appium/WebDriverAgent/compare/v5.13.0...v5.13.1) (2023-11-01) + + +### Miscellaneous Chores + +* **deps:** bump asyncbox from 2.9.4 to 3.0.0 ([#803](https://github.com/appium/WebDriverAgent/issues/803)) ([0f2305d](https://github.com/appium/WebDriverAgent/commit/0f2305d2559dc0807d7df0d0e06f7fc3c549701c)) + +## [5.13.0](https://github.com/appium/WebDriverAgent/compare/v5.12.3...v5.13.0) (2023-10-31) + + +### Features + +* Add "elementDescription" property to audit issues containing the debug description of an element ([#802](https://github.com/appium/WebDriverAgent/issues/802)) ([9925af4](https://github.com/appium/WebDriverAgent/commit/9925af44ec5fbfb66e6f034dfd93a6c25de48661)) + +## [5.12.3](https://github.com/appium/WebDriverAgent/compare/v5.12.2...v5.12.3) (2023-10-31) + + +### Miscellaneous Chores + +* Return better error on WDA startup timeout ([#801](https://github.com/appium/WebDriverAgent/issues/801)) ([796d5e7](https://github.com/appium/WebDriverAgent/commit/796d5e743676b174221e27e739a0164f4b91533c)) + +## [5.12.2](https://github.com/appium/WebDriverAgent/compare/v5.12.1...v5.12.2) (2023-10-29) + + +### Miscellaneous Chores + +* return operation error in `handleKeyboardInput` ([#799](https://github.com/appium/WebDriverAgent/issues/799)) ([247ace6](https://github.com/appium/WebDriverAgent/commit/247ace68f373c09054fabc3be088061089946806)) + +## [5.12.1](https://github.com/appium/WebDriverAgent/compare/v5.12.0...v5.12.1) (2023-10-28) + + +### Bug Fixes + +* when 0 is given for handleKeyboardInput ([#798](https://github.com/appium/WebDriverAgent/issues/798)) ([58ebe8e](https://github.com/appium/WebDriverAgent/commit/58ebe8eb52966963ee30a5c066beb3bf9fed3161)) + +## [5.12.0](https://github.com/appium/WebDriverAgent/compare/v5.11.7...v5.12.0) (2023-10-26) + + +### Features + +* Add an endpoint for keyboard input ([#797](https://github.com/appium/WebDriverAgent/issues/797)) ([aaf70c9](https://github.com/appium/WebDriverAgent/commit/aaf70c9196e4dcb2073da151cda23b2b221d4dae)) + +## [5.11.7](https://github.com/appium/WebDriverAgent/compare/v5.11.6...v5.11.7) (2023-10-25) + + +### Miscellaneous Chores + +* **deps-dev:** bump @typescript-eslint/eslint-plugin from 5.62.0 to 6.9.0 ([#796](https://github.com/appium/WebDriverAgent/issues/796)) ([dabf141](https://github.com/appium/WebDriverAgent/commit/dabf141acd3186b1c27231ef52826fa42208c980)) + +## [5.11.6](https://github.com/appium/WebDriverAgent/compare/v5.11.5...v5.11.6) (2023-10-25) + + +### Miscellaneous Chores + +* disable debugger for wda ([#768](https://github.com/appium/WebDriverAgent/issues/768)) ([e2f4405](https://github.com/appium/WebDriverAgent/commit/e2f4405a3449f1f4d390eae06bf91a220e81b58b)) + +## [5.11.5](https://github.com/appium/WebDriverAgent/compare/v5.11.4...v5.11.5) (2023-10-23) + + +### Miscellaneous Chores + +* **deps-dev:** bump eslint-config-prettier from 8.10.0 to 9.0.0 ([#791](https://github.com/appium/WebDriverAgent/issues/791)) ([f130961](https://github.com/appium/WebDriverAgent/commit/f130961f189f2746d4a2b0a18105fc10203312ca)) +* **deps-dev:** bump lint-staged from 14.0.1 to 15.0.2 ([#792](https://github.com/appium/WebDriverAgent/issues/792)) ([440279d](https://github.com/appium/WebDriverAgent/commit/440279d4f6d069e440180faf4bee8e5dc1758787)) +* **deps-dev:** bump semantic-release from 21.1.2 to 22.0.5 ([#781](https://github.com/appium/WebDriverAgent/issues/781)) ([a967183](https://github.com/appium/WebDriverAgent/commit/a96718308dbd6b13feb30e6ce8f01a7d9b74b146)) + +## [5.11.4](https://github.com/appium/WebDriverAgent/compare/v5.11.3...v5.11.4) (2023-10-23) + + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 16.1.3 to 17.0.0 ([#795](https://github.com/appium/WebDriverAgent/issues/795)) ([4921899](https://github.com/appium/WebDriverAgent/commit/4921899d96800dbcd59a9c27ba793ad16d0c715b)) + +## [5.11.3](https://github.com/appium/WebDriverAgent/compare/v5.11.2...v5.11.3) (2023-10-21) + + +### Miscellaneous Chores + +* use PRODUCT_BUNDLE_IDENTIFIER to info.plist ([#794](https://github.com/appium/WebDriverAgent/issues/794)) ([543c498](https://github.com/appium/WebDriverAgent/commit/543c49860d2d35148bcbaa33e14d3e1dab058cef)) + +## [5.11.2](https://github.com/appium/WebDriverAgent/compare/v5.11.1...v5.11.2) (2023-10-19) + + +### Miscellaneous Chores + +* Use latest teen_process types ([895cdfc](https://github.com/appium/WebDriverAgent/commit/895cdfc1a316117bb7c8b5be0265b439c1e911bc)) + +## [5.11.1](https://github.com/appium/WebDriverAgent/compare/v5.11.0...v5.11.1) (2023-10-19) + + +### Miscellaneous Chores + +* Use latest types version ([123eefb](https://github.com/appium/WebDriverAgent/commit/123eefba5e5e30100cb3cdff09a516179f78afe7)) + +## [5.11.0](https://github.com/appium/WebDriverAgent/compare/v5.10.1...v5.11.0) (2023-10-05) + + +### Features + +* Add /calibrate endpoint ([#785](https://github.com/appium/WebDriverAgent/issues/785)) ([ae1603a](https://github.com/appium/WebDriverAgent/commit/ae1603a3b5b5c4828ed4959c63d6274254f832a2)) + +## [5.10.1](https://github.com/appium/WebDriverAgent/compare/v5.10.0...v5.10.1) (2023-10-05) + + +### Miscellaneous Chores + +* Remove the hardcoded keyboard wait delay from handleKeys ([#784](https://github.com/appium/WebDriverAgent/issues/784)) ([f043d67](https://github.com/appium/WebDriverAgent/commit/f043d67dd90fbfca00b8cf53ccae63dbd67fa150)) + +## [5.10.0](https://github.com/appium/WebDriverAgent/compare/v5.9.1...v5.10.0) (2023-09-25) + + +### Features + +* remove test frameworks in Frameworks and add device local references as rpath for real devices ([#780](https://github.com/appium/WebDriverAgent/issues/780)) ([ae6c842](https://github.com/appium/WebDriverAgent/commit/ae6c842f3c4e7deb51fcc7a1a1045d4eeede69fd)) + +## [5.9.1](https://github.com/appium/WebDriverAgent/compare/v5.9.0...v5.9.1) (2023-09-22) + + +### Bug Fixes + +* Provide signing arguments as command line parameters ([#779](https://github.com/appium/WebDriverAgent/issues/779)) ([51ba527](https://github.com/appium/WebDriverAgent/commit/51ba527b6cde3773ebcd5323cfa7e0890b2563aa)) + +## [5.9.0](https://github.com/appium/WebDriverAgent/compare/v5.8.7...v5.9.0) (2023-09-22) + + +### Features + +* do not get active process information in a new session request ([#774](https://github.com/appium/WebDriverAgent/issues/774)) ([2784ce4](https://github.com/appium/WebDriverAgent/commit/2784ce440f8b5ab9710db08d9ffda704697ac07c)) + +## [5.8.7](https://github.com/appium/WebDriverAgent/compare/v5.8.6...v5.8.7) (2023-09-22) + + +### Miscellaneous Chores + +* tweak device in currentCapabilities ([#773](https://github.com/appium/WebDriverAgent/issues/773)) ([8481b02](https://github.com/appium/WebDriverAgent/commit/8481b02fc84de1147e1254ea7fd114f8735b0226)) + +## [5.8.6](https://github.com/appium/WebDriverAgent/compare/v5.8.5...v5.8.6) (2023-09-21) + + +### Miscellaneous Chores + +* add log to leave it in the system log ([#772](https://github.com/appium/WebDriverAgent/issues/772)) ([012af21](https://github.com/appium/WebDriverAgent/commit/012af21383829397c7265daa0513829cc4e93aee)) + +## [5.8.5](https://github.com/appium/WebDriverAgent/compare/v5.8.4...v5.8.5) (2023-09-15) + + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 15.2.0 to 16.0.0 ([#766](https://github.com/appium/WebDriverAgent/issues/766)) ([2ffd187](https://github.com/appium/WebDriverAgent/commit/2ffd187b2e8b3c1ed04537320179bdfe9f9635df)) + +## [5.8.4](https://github.com/appium/WebDriverAgent/compare/v5.8.3...v5.8.4) (2023-09-14) + + +### Miscellaneous Chores + +* **deps-dev:** bump @types/teen_process from 2.0.0 to 2.0.1 ([#765](https://github.com/appium/WebDriverAgent/issues/765)) ([1af64b8](https://github.com/appium/WebDriverAgent/commit/1af64b8834371a3fdb3d0aab82fdfdeff6194555)) + +## [5.8.3](https://github.com/appium/WebDriverAgent/compare/v5.8.2...v5.8.3) (2023-09-01) + + +### Bug Fixes + +* Address some typing-related issues ([#759](https://github.com/appium/WebDriverAgent/issues/759)) ([87e8704](https://github.com/appium/WebDriverAgent/commit/87e87044d6216513f755c5184d61514a76cb0179)) + +## [5.8.2](https://github.com/appium/WebDriverAgent/compare/v5.8.1...v5.8.2) (2023-08-28) + + +### Miscellaneous Chores + +* **deps-dev:** bump conventional-changelog-conventionalcommits ([#757](https://github.com/appium/WebDriverAgent/issues/757)) ([a3047ea](https://github.com/appium/WebDriverAgent/commit/a3047ea70b7a9fd5ccb2a2c93b0964d7de609d38)) + +## [5.8.1](https://github.com/appium/WebDriverAgent/compare/v5.8.0...v5.8.1) (2023-08-25) + + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 20.1.3 to 21.1.0 ([#754](https://github.com/appium/WebDriverAgent/issues/754)) ([d86d9a6](https://github.com/appium/WebDriverAgent/commit/d86d9a64ca75ad40273cfa10855f49b967d9fd95)) + +## [5.8.0](https://github.com/appium/WebDriverAgent/compare/v5.7.0...v5.8.0) (2023-08-24) + + +### Features + +* Add wdHittable property ([#756](https://github.com/appium/WebDriverAgent/issues/756)) ([075298b](https://github.com/appium/WebDriverAgent/commit/075298b286c83ab5d4a2855e9e0bb915790b3f43)) + +## [5.7.0](https://github.com/appium/WebDriverAgent/compare/v5.6.2...v5.7.0) (2023-08-24) + + +### Features + +* Switch babel to typescript ([#753](https://github.com/appium/WebDriverAgent/issues/753)) ([76a4c7f](https://github.com/appium/WebDriverAgent/commit/76a4c7f066e1895acbb153ab035d6a08604277e4)) + +## [5.6.2](https://github.com/appium/WebDriverAgent/compare/v5.6.1...v5.6.2) (2023-08-23) + + +### Miscellaneous Chores + +* Remove unused glob dependency ([ee7655e](https://github.com/appium/WebDriverAgent/commit/ee7655e0a2aa39dd1f0c6d80d89065b4f34f264d)) + +## [5.6.1](https://github.com/appium/WebDriverAgent/compare/v5.6.0...v5.6.1) (2023-08-14) + + +### Miscellaneous Chores + +* **deps-dev:** bump lint-staged from 13.3.0 to 14.0.0 ([#750](https://github.com/appium/WebDriverAgent/issues/750)) ([0b74bf5](https://github.com/appium/WebDriverAgent/commit/0b74bf5befaa6d87c93a5306beb690a5a0e1843d)) + +## [5.6.0](https://github.com/appium/WebDriverAgent/compare/v5.5.2...v5.6.0) (2023-07-15) + + +### Features + +* apply shouldWaitForQuiescence for activate in /wda/apps/launch ([#739](https://github.com/appium/WebDriverAgent/issues/739)) ([#740](https://github.com/appium/WebDriverAgent/issues/740)) ([66ab695](https://github.com/appium/WebDriverAgent/commit/66ab695f9fa1850145a1d94ef15978b70bc1b032)) + +## [5.5.2](https://github.com/appium/WebDriverAgent/compare/v5.5.1...v5.5.2) (2023-07-07) + + +### Miscellaneous Chores + +* **deps-dev:** bump prettier from 2.8.8 to 3.0.0 ([#735](https://github.com/appium/WebDriverAgent/issues/735)) ([15614d0](https://github.com/appium/WebDriverAgent/commit/15614d030975f2b1eac5919d2353bc015f194d4c)) + +## [5.5.1](https://github.com/appium/WebDriverAgent/compare/v5.5.0...v5.5.1) (2023-06-16) + + +### Bug Fixes + +* Update strongbox API name ([4977032](https://github.com/appium/WebDriverAgent/commit/49770328aeeebacd76011ff1caf13d5b4ed71420)) + +## [5.5.0](https://github.com/appium/WebDriverAgent/compare/v5.4.1...v5.5.0) (2023-06-12) + + +### Features + +* Add accessibility audit extension ([#727](https://github.com/appium/WebDriverAgent/issues/727)) ([78321dd](https://github.com/appium/WebDriverAgent/commit/78321dd3dafdb142eed136b48ec101f1daed50a4)) + +## [5.4.1](https://github.com/appium/WebDriverAgent/compare/v5.4.0...v5.4.1) (2023-06-09) + + +### Bug Fixes + +* Return default testmanagerd version if the info is not available ([#728](https://github.com/appium/WebDriverAgent/issues/728)) ([e6e2dbd](https://github.com/appium/WebDriverAgent/commit/e6e2dbd86fc0c48ae146905f0e69a6223360e856)) + +## [5.4.0](https://github.com/appium/WebDriverAgent/compare/v5.3.3...v5.4.0) (2023-06-09) + + +### Features + +* Drop older screenshoting APIs ([#721](https://github.com/appium/WebDriverAgent/issues/721)) ([4a08d7a](https://github.com/appium/WebDriverAgent/commit/4a08d7a843af6b93b378b4e3dc10f123d2e56359)) + + +### Bug Fixes + +* Streamline errors handling for async block calls ([#725](https://github.com/appium/WebDriverAgent/issues/725)) ([364b779](https://github.com/appium/WebDriverAgent/commit/364b7791393ffae9c048c5cac023e3e7d1813a14)) + +## [5.3.3](https://github.com/appium/WebDriverAgent/compare/v5.3.2...v5.3.3) (2023-06-08) + + +### Miscellaneous Chores + +* Disable automatic screen recording by default ([#726](https://github.com/appium/WebDriverAgent/issues/726)) ([a070223](https://github.com/appium/WebDriverAgent/commit/a070223e0ef43be8dd54d16ee3e3b96603ad5f3a)) + +## [5.3.2](https://github.com/appium/WebDriverAgent/compare/v5.3.1...v5.3.2) (2023-06-07) + + +### Miscellaneous Chores + +* **deps-dev:** bump conventional-changelog-conventionalcommits ([#723](https://github.com/appium/WebDriverAgent/issues/723)) ([b22f61e](https://github.com/appium/WebDriverAgent/commit/b22f61eda142ee6ec1db8c74a4788e0270ac7740)) + +## [5.3.1](https://github.com/appium/WebDriverAgent/compare/v5.3.0...v5.3.1) (2023-06-06) + + +### Bug Fixes + +* remove Parameter of overriding method should be annotated with __attribute__((noescape)) ([#720](https://github.com/appium/WebDriverAgent/issues/720)) ([5f811ac](https://github.com/appium/WebDriverAgent/commit/5f811ac65ba3ac770e42bd7f8614815df8ec990f)) + +## [5.3.0](https://github.com/appium/WebDriverAgent/compare/v5.2.0...v5.3.0) (2023-05-22) + + +### Features + +* Use strongbox to persist the previous version of the module ([#714](https://github.com/appium/WebDriverAgent/issues/714)) ([4611792](https://github.com/appium/WebDriverAgent/commit/4611792ee5d5d7f39d188b5ebc31017f436c5ace)) + +## [5.2.0](https://github.com/appium/WebDriverAgent/compare/v5.1.6...v5.2.0) (2023-05-20) + + +### Features + +* Replace non-encodable characters in the resulting JSON ([#713](https://github.com/appium/WebDriverAgent/issues/713)) ([cdfae40](https://github.com/appium/WebDriverAgent/commit/cdfae408be0bcf6607f0ca4462925eed2c300f5e)) + +## [5.1.6](https://github.com/appium/WebDriverAgent/compare/v5.1.5...v5.1.6) (2023-05-18) + + +### Miscellaneous Chores + +* **deps:** bump @appium/support from 3.1.11 to 4.0.0 ([#710](https://github.com/appium/WebDriverAgent/issues/710)) ([3e49523](https://github.com/appium/WebDriverAgent/commit/3e495230674a46db29ecea3b36c2ed0ea1bf2842)) + +## [5.1.5](https://github.com/appium/WebDriverAgent/compare/v5.1.4...v5.1.5) (2023-05-18) + + +### Miscellaneous Chores + +* Drop obsolete workarounds for coordinates calculation ([#701](https://github.com/appium/WebDriverAgent/issues/701)) ([259f731](https://github.com/appium/WebDriverAgent/commit/259f7319305b15a3f541957d3ccaa3cb12c9e1a3)) + +## [5.1.4](https://github.com/appium/WebDriverAgent/compare/v5.1.3...v5.1.4) (2023-05-16) + + +### Bug Fixes + +* Prevent freeze on launch/activate of a missing app ([#706](https://github.com/appium/WebDriverAgent/issues/706)) ([c4976e3](https://github.com/appium/WebDriverAgent/commit/c4976e3e99afa4d471bd39c3dccfc7d9f58d8bfc)) + +## [5.1.3](https://github.com/appium/WebDriverAgent/compare/v5.1.2...v5.1.3) (2023-05-16) + + +### Bug Fixes + +* Revert "fix: Assert app is installed before launching or activating it ([#704](https://github.com/appium/WebDriverAgent/issues/704))" ([#705](https://github.com/appium/WebDriverAgent/issues/705)) ([00baeb2](https://github.com/appium/WebDriverAgent/commit/00baeb2045b9aac98d27fe2e96cedce0dde5e8be)) + +## [5.1.2](https://github.com/appium/WebDriverAgent/compare/v5.1.1...v5.1.2) (2023-05-15) + + +### Miscellaneous Chores + +* remove code for old os versions ([#694](https://github.com/appium/WebDriverAgent/issues/694)) ([4a9faa5](https://github.com/appium/WebDriverAgent/commit/4a9faa5f85e0615c18a5f35090335bdbc7d56ebe)) + +## [5.1.1](https://github.com/appium/WebDriverAgent/compare/v5.1.0...v5.1.1) (2023-05-15) + + +### Bug Fixes + +* Assert app is installed before launching or activating it ([#704](https://github.com/appium/WebDriverAgent/issues/704)) ([94e5c51](https://github.com/appium/WebDriverAgent/commit/94e5c51bce1d4518418e999b4ac466cd46ca3bc3)) + +## [5.1.0](https://github.com/appium/WebDriverAgent/compare/v5.0.0...v5.1.0) (2023-05-14) + + +### Features + +* Add a possibility to provide a target picker value ([#699](https://github.com/appium/WebDriverAgent/issues/699)) ([fc76aee](https://github.com/appium/WebDriverAgent/commit/fc76aeecb087429974b7b52b725173186e6f0246)) + + +### Code Refactoring + +* Remove obsolete coordinate calculation workarounds needed for older xCode SDKs ([#698](https://github.com/appium/WebDriverAgent/issues/698)) ([025b42c](https://github.com/appium/WebDriverAgent/commit/025b42c8a34ff0beba4379f4cb0c1d79d2b222ed)) + +## [5.0.0](https://github.com/appium/WebDriverAgent/compare/v4.15.1...v5.0.0) (2023-05-14) + + +### ⚠ BREAKING CHANGES + +* The minimum supported xCode/iOS version is now 13/15.0 + +### Code Refactoring + +* Drop workarounds for legacy iOS versions ([#696](https://github.com/appium/WebDriverAgent/issues/696)) ([bb562b9](https://github.com/appium/WebDriverAgent/commit/bb562b96db6aad476970ef7bd352cb8df4f1e6c2)) + +## [4.15.1](https://github.com/appium/WebDriverAgent/compare/v4.15.0...v4.15.1) (2023-05-06) + + +### Performance Improvements + +* tune webDriverAgentUrl case by skiping xcodebuild stuff ([#691](https://github.com/appium/WebDriverAgent/issues/691)) ([d8f1457](https://github.com/appium/WebDriverAgent/commit/d8f1457b591b2dd00040f8336c1a7a728af871d2)) + +## [4.15.0](https://github.com/appium/WebDriverAgent/compare/v4.14.0...v4.15.0) (2023-05-04) + + +### Features + +* Make isFocused attribute available for iOS elements ([#692](https://github.com/appium/WebDriverAgent/issues/692)) ([0ec74ce](https://github.com/appium/WebDriverAgent/commit/0ec74ce32c817a5884228ccb2ec31f0a5a4de9c3)) + +## [4.14.0](https://github.com/appium/WebDriverAgent/compare/v4.13.2...v4.14.0) (2023-05-02) + + +### Features + +* start wda process via Xctest in a real device ([#687](https://github.com/appium/WebDriverAgent/issues/687)) ([e1c0f83](https://github.com/appium/WebDriverAgent/commit/e1c0f836a68ad2efbedfc77343794d0d97ef6090)) + +## [4.13.2](https://github.com/appium/WebDriverAgent/compare/v4.13.1...v4.13.2) (2023-04-28) + + +### Miscellaneous Chores + +* add withoutSession for pasteboard for debug ([#688](https://github.com/appium/WebDriverAgent/issues/688)) ([edcbf9e](https://github.com/appium/WebDriverAgent/commit/edcbf9e6af903c6f490ca90ff915497ad53bb8b5)) + +## [4.13.1](https://github.com/appium/WebDriverAgent/compare/v4.13.0...v4.13.1) (2023-04-04) + + +### Bug Fixes + +* Fixed Xpath lookup for Xcode 14.3 ([#681](https://github.com/appium/WebDriverAgent/issues/681)) ([3e0b191](https://github.com/appium/WebDriverAgent/commit/3e0b1914f87585ed69ba20d960502eabb058941c)) + +## [4.13.0](https://github.com/appium/WebDriverAgent/compare/v4.12.2...v4.13.0) (2023-02-23) + + +### Features + +* Increase Xpath Lookup Performance ([#666](https://github.com/appium/WebDriverAgent/issues/666)) ([1696f4b](https://github.com/appium/WebDriverAgent/commit/1696f4bb879152ef04408940849708654072c797)) + +## [4.12.2](https://github.com/appium/WebDriverAgent/compare/v4.12.1...v4.12.2) (2023-02-22) + + +### Miscellaneous Chores + +* Make sure the test is never going to be unexpectedly interrupted ([#664](https://github.com/appium/WebDriverAgent/issues/664)) ([cafe47e](https://github.com/appium/WebDriverAgent/commit/cafe47e9bea9649a0e9b4a2b96ca44434bbac411)) + +## [4.12.1](https://github.com/appium/WebDriverAgent/compare/v4.12.0...v4.12.1) (2023-02-20) + + +### Bug Fixes + +* Return null if no simulated location has been previously set ([#663](https://github.com/appium/WebDriverAgent/issues/663)) ([6a5c48b](https://github.com/appium/WebDriverAgent/commit/6a5c48bd2ffc43c0f0d9bf781671bbcf171f9375)) + +## [4.12.0](https://github.com/appium/WebDriverAgent/compare/v4.11.0...v4.12.0) (2023-02-20) + + +### Features + +* Add support of the simulated geolocation setting ([#662](https://github.com/appium/WebDriverAgent/issues/662)) ([ebb9e60](https://github.com/appium/WebDriverAgent/commit/ebb9e60d56c0e0db9f509437ed639a3a39f6011b)) + +## [4.11.0](https://github.com/appium/WebDriverAgent/compare/v4.10.24...v4.11.0) (2023-02-19) + + +### Features + +* Add openUrl handler available since Xcode 14.3 ([#661](https://github.com/appium/WebDriverAgent/issues/661)) ([bee564e](https://github.com/appium/WebDriverAgent/commit/bee564e8c6b975aff07fd1244583f0727a0f5470)) + +## [4.10.24](https://github.com/appium/WebDriverAgent/compare/v4.10.23...v4.10.24) (2023-02-17) + + +### Bug Fixes + +* Catch unexpected exceptions thrown by the alerts monitor ([#660](https://github.com/appium/WebDriverAgent/issues/660)) ([aa22555](https://github.com/appium/WebDriverAgent/commit/aa22555f0dcf98de43c95cb20be73e911a97741e)) + +## [4.10.23](https://github.com/appium/WebDriverAgent/compare/v4.10.22...v4.10.23) (2023-02-05) + + +### Miscellaneous Chores + +* bundle:tv for tvOS ([#657](https://github.com/appium/WebDriverAgent/issues/657)) ([9d2d047](https://github.com/appium/WebDriverAgent/commit/9d2d047fba57a33787c66a1e8a8449b9538c67be)) + +## [4.10.22](https://github.com/appium/WebDriverAgent/compare/v4.10.21...v4.10.22) (2023-01-30) + + +### Bug Fixes + +* Pull defaultAdditionalRequestParameters dynamically ([#658](https://github.com/appium/WebDriverAgent/issues/658)) ([d7c397b](https://github.com/appium/WebDriverAgent/commit/d7c397b0260a71568edd6d99ecf7b39ca3503083)) + +## [4.10.21](https://github.com/appium/WebDriverAgent/compare/v4.10.20...v4.10.21) (2023-01-26) + + +### Bug Fixes + +* Properly update maxDepth while fetching snapshots ([#655](https://github.com/appium/WebDriverAgent/issues/655)) ([6f99bab](https://github.com/appium/WebDriverAgent/commit/6f99bab5fbdbf65c9ef74c42b5f1b4c658aeaafb)) + +## [4.10.20](https://github.com/appium/WebDriverAgent/compare/v4.10.19...v4.10.20) (2023-01-17) + + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 19.0.5 to 20.0.2 ([#651](https://github.com/appium/WebDriverAgent/issues/651)) ([e96c367](https://github.com/appium/WebDriverAgent/commit/e96c367cb0d9461bb5e443740504969a4cb857e1)) + +## [4.10.19](https://github.com/appium/WebDriverAgent/compare/v4.10.18...v4.10.19) (2023-01-13) + + +### Miscellaneous Chores + +* **deps-dev:** bump appium-xcode from 4.0.5 to 5.0.0 ([#652](https://github.com/appium/WebDriverAgent/issues/652)) ([75c247f](https://github.com/appium/WebDriverAgent/commit/75c247fe82ebe7b2b8ba0d79528cadeda871e229)) + +## [4.10.18](https://github.com/appium/WebDriverAgent/compare/v4.10.17...v4.10.18) (2022-12-30) + + +### Miscellaneous Chores + +* simplify Script/build-webdriveragent.js ([#647](https://github.com/appium/WebDriverAgent/issues/647)) ([81dab6c](https://github.com/appium/WebDriverAgent/commit/81dab6ca0645f9925a8515abfb4851d6e85da7e9)) + +## [4.10.17](https://github.com/appium/WebDriverAgent/compare/v4.10.16...v4.10.17) (2022-12-30) + + +### Miscellaneous Chores + +* add ARCHS=arm64 for a release package build ([#649](https://github.com/appium/WebDriverAgent/issues/649)) ([08612aa](https://github.com/appium/WebDriverAgent/commit/08612aade1833c384914bb618675b5653d5f5118)) + +## [4.10.16](https://github.com/appium/WebDriverAgent/compare/v4.10.15...v4.10.16) (2022-12-29) + + +### Miscellaneous Chores + +* build only arm64 for generic build in a release ([#648](https://github.com/appium/WebDriverAgent/issues/648)) ([63e175d](https://github.com/appium/WebDriverAgent/commit/63e175d56526d9fb74d9053dbe60fd0c80b9c670)) + +## [4.10.15](https://github.com/appium/WebDriverAgent/compare/v4.10.14...v4.10.15) (2022-12-16) + + +### Miscellaneous Chores + +* **deps:** bump @appium/base-driver from 8.7.3 to 9.0.0 ([#645](https://github.com/appium/WebDriverAgent/issues/645)) ([35dd981](https://github.com/appium/WebDriverAgent/commit/35dd98111f1d8222bc0cb412c11cb1442d10295e)) +* **deps:** bump appium-ios-simulator from 4.2.1 to 5.0.1 ([#646](https://github.com/appium/WebDriverAgent/issues/646)) ([7911cbb](https://github.com/appium/WebDriverAgent/commit/7911cbb3607b1d75091bdf3dc436baae3868854a)) + +## [4.10.14](https://github.com/appium/WebDriverAgent/compare/v4.10.13...v4.10.14) (2022-12-14) + + +### Miscellaneous Chores + +* **deps-dev:** bump @appium/test-support from 2.0.2 to 3.0.0 ([#644](https://github.com/appium/WebDriverAgent/issues/644)) ([ab84580](https://github.com/appium/WebDriverAgent/commit/ab8458027457563b7faaeef36d9019b7ac1921b0)) +* **deps:** bump @appium/support from 2.61.1 to 3.0.0 ([#643](https://github.com/appium/WebDriverAgent/issues/643)) ([3ca197a](https://github.com/appium/WebDriverAgent/commit/3ca197ac7526036e408584207b26129847a615ca)) + +## [4.10.13](https://github.com/appium/WebDriverAgent/compare/v4.10.12...v4.10.13) (2022-12-13) + +## [4.10.12](https://github.com/appium/WebDriverAgent/compare/v4.10.11...v4.10.12) (2022-12-08) + + +### Bug Fixes + +* Provide proper xcodebuild argument for tvOS ([#640](https://github.com/appium/WebDriverAgent/issues/640)) ([72bd327](https://github.com/appium/WebDriverAgent/commit/72bd32780f26ae0f60b30e0cee8fc585aea600fe)) + +## [4.10.11](https://github.com/appium/WebDriverAgent/compare/v4.10.10...v4.10.11) (2022-11-29) + +## [4.10.10](https://github.com/appium/WebDriverAgent/compare/v4.10.9...v4.10.10) (2022-11-25) + + +### Bug Fixes + +* Only check existence if firstMatch is applied ([#638](https://github.com/appium/WebDriverAgent/issues/638)) ([5394fe8](https://github.com/appium/WebDriverAgent/commit/5394fe8cc2eda3d1668685bd00f9f7383e122627)) + +## [4.10.9](https://github.com/appium/WebDriverAgent/compare/v4.10.8...v4.10.9) (2022-11-25) + +## [4.10.8](https://github.com/appium/WebDriverAgent/compare/v4.10.7...v4.10.8) (2022-11-24) + +## [4.10.7](https://github.com/appium/WebDriverAgent/compare/v4.10.6...v4.10.7) (2022-11-24) + +## [4.10.6](https://github.com/appium/WebDriverAgent/compare/v4.10.5...v4.10.6) (2022-11-23) + +## [4.10.5](https://github.com/appium/WebDriverAgent/compare/v4.10.4...v4.10.5) (2022-11-22) + +## [4.10.4](https://github.com/appium/WebDriverAgent/compare/v4.10.3...v4.10.4) (2022-11-22) + +## [4.10.3](https://github.com/appium/WebDriverAgent/compare/v4.10.2...v4.10.3) (2022-11-22) + +## [4.10.2](https://github.com/appium/WebDriverAgent/compare/v4.10.1...v4.10.2) (2022-11-06) diff --git a/Fastlane/Fastfile b/Fastlane/Fastfile index 39bb9f491..6e657a74e 100644 --- a/Fastlane/Fastfile +++ b/Fastlane/Fastfile @@ -1,34 +1,13 @@ XC_PROJECT = File.absolute_path('../WebDriverAgent.xcodeproj') -RETRIES = 3 lane :test do - test_run_block = lambda do |testrun_info| - failed_test_count = testrun_info[:failed].size - - if failed_test_count > 0 - UI.important("Failed tests count: #{failed_test_count}") - - try_attempt = testrun_info[:try_count] - if try_attempt < RETRIES - UI.header("Re-running failing tests (attempt #{try_attempt} of #{RETRIES})") - reset_simulator_contents - end - end - end - # https://docs.fastlane.tools/actions/scan/ - result = multi_scan( + run_tests( project: XC_PROJECT, - try_count: RETRIES, - fail_build: false, + fail_build: true, scheme: ENV['SCHEME'], sdk: ENV['SDK'], destination: ENV['DEST'], - testrun_completed_block: test_run_block + number_of_retries: 3 ) - unless result[:failed_testcount].zero? - msg = "There are #{result[:failed_testcount]} legitimate failing tests" - UI.message(msg) - raise msg - end -end \ No newline at end of file +end diff --git a/Fastlane/Pluginfile b/Fastlane/Pluginfile deleted file mode 100644 index dc2f2aa91..000000000 --- a/Fastlane/Pluginfile +++ /dev/null @@ -1,5 +0,0 @@ -# Autogenerated by fastlane -# -# Ensure this file is checked in to source control! - -gem 'fastlane-plugin-test_center', '3.15.3' diff --git a/Gemfile b/Gemfile index f8456a949..90c93561d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,3 @@ source "https://rubygems.org" -gem "fastlane", '2.162.0' -plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') -eval_gemfile(plugins_path) if File.exist?(plugins_path) +gem "fastlane", '2.217.0' diff --git a/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h b/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h index 4ddeff04d..7473b2e0a 100644 --- a/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h +++ b/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h @@ -10,6 +10,9 @@ #import @class NSMutableDictionary, NSXPCConnection, XCSynthesizedEventRecord; +#if !TARGET_OS_TV // tvOS does not provide relevant APIs +@class CLLocation; +#endif @protocol XCTUIApplicationMonitor, XCTAXClient, XCTestManager_ManagerInterface; // iOS since 10.3 @@ -67,4 +70,21 @@ - (void)_reportInvalidation; - (id)initWithConnection:(id)arg1; +// Since Xcode 14.3 +- (void)openURL:(NSURL *)arg1 usingApplication:(NSString *)arg2 completion:(void (^)(_Bool, NSError *))arg3; +- (void)openDefaultApplicationForURL:(NSURL *)arg1 completion:(void (^)(_Bool, NSError *))arg2; +#if !TARGET_OS_TV // tvOS does not provide relevant APIs +- (void)setSimulatedLocation:(CLLocation *)arg1 completion:(void (^)(_Bool, NSError *))arg2; +- (void)getSimulatedLocationWithReply:(void (^)(CLLocation *, NSError *))arg1; +- (void)clearSimulatedLocationWithReply:(void (^)(_Bool, NSError *))arg1; +@property(readonly) _Bool supportsLocationSimulation; +#endif + +// Since Xcode 10.2 +- (void)launchApplicationWithPath:(NSString *)arg1 + bundleID:(NSString *)arg2 + arguments:(NSArray *)arg3 + environment:(NSDictionary *)arg4 + completion:(void (^)(_Bool, NSError *))arg5; + @end diff --git a/PrivateHeaders/XCTest/XCTestCase.h b/PrivateHeaders/XCTest/XCTestCase.h index f00038ef9..d68d949c5 100644 --- a/PrivateHeaders/XCTest/XCTestCase.h +++ b/PrivateHeaders/XCTest/XCTestCase.h @@ -51,7 +51,6 @@ - (void)reportMeasurements:(id)arg1 forMetricID:(id)arg2 reportFailures:(BOOL)arg3; - (void)_recordValues:(id)arg1 forPerformanceMetricID:(id)arg2 name:(id)arg3 unitsOfMeasurement:(id)arg4 baselineName:(id)arg5 baselineAverage:(id)arg6 maxPercentRegression:(id)arg7 maxPercentRelativeStandardDeviation:(id)arg8 maxRegression:(id)arg9 maxStandardDeviation:(id)arg10 file:(id)arg11 line:(unsigned long long)arg12; - (id)_symbolicationRecordForTestCodeInAddressStack:(id)arg1; -- (void)measureBlock:(CDUnknownBlockType)arg1; - (void)stopMeasuring; - (void)startMeasuring; - (BOOL)_isMeasuringMetrics; diff --git a/PrivateHeaders/XCTest/XCUICoordinate.h b/PrivateHeaders/XCTest/XCUICoordinate.h index 173118951..a7f7122e2 100644 --- a/PrivateHeaders/XCTest/XCUICoordinate.h +++ b/PrivateHeaders/XCTest/XCUICoordinate.h @@ -34,5 +34,11 @@ - (void)pressWithPressure:(double)arg1 duration:(double)arg2; - (void)forcePress; +// Since Xcode 12 +- (void)pressForDuration:(double)duration + thenDragToCoordinate:(XCUICoordinate *)otherCoordinate + withVelocity:(CGFloat)velocity + thenHoldForDuration:(double)holdDuration; + @end #endif diff --git a/PrivateHeaders/XCTest/XCUIElement.h b/PrivateHeaders/XCTest/XCUIElement.h index 7280b2ccf..d5dc18802 100644 --- a/PrivateHeaders/XCTest/XCUIElement.h +++ b/PrivateHeaders/XCTest/XCUIElement.h @@ -65,4 +65,10 @@ - (void)swipeDownWithVelocity:(double)arg1; - (void)swipeUpWithVelocity:(double)arg1; +// Since Xcode 12 +- (void)pressForDuration:(double)duration + thenDragToElement:(XCUIElement *)otherElement + withVelocity:(CGFloat)velocity + thenHoldForDuration:(double)holdDuration; + @end diff --git a/README.md b/README.md index 0b9e0e488..547559ebc 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,10 @@ If you are having some issues please checkout [wiki](https://github.com/facebook If you want to help us out, you are more than welcome to. However please make sure you have followed the guidelines in [CONTRIBUTING](CONTRIBUTING.md). ## Creating Bundles -Follow [this doc](docs/CREATING_BUNDLES.md) + +`npm run bundle` + +Then, you find `WebDriverAgentRunner-Runner-sim-.zip` for iOS and `WebDriverAgentRunner-Runner-tv_sim-.zip` for tvOS files in the current directory. ## License @@ -51,4 +54,3 @@ projects has been integrated directly in the WebDriverAgent source tree. You can find the source files and their licenses in the `WebDriverAgentLib/Vendor` directory. Have fun! - diff --git a/Scripts/build-webdriveragent.js b/Scripts/build-webdriveragent.js index d93fccd26..1958b876d 100644 --- a/Scripts/build-webdriveragent.js +++ b/Scripts/build-webdriveragent.js @@ -1,88 +1,75 @@ const path = require('path'); -const os = require('os'); const { asyncify } = require('asyncbox'); -const { logger, fs, mkdirp, zip } = require('appium/support'); +const { logger, fs } = require('@appium/support'); const { exec } = require('teen_process'); const xcode = require('appium-xcode'); -const log = new logger.getLogger('WDABuild'); -const rootDir = path.resolve(__dirname, '..'); +const LOG = new logger.getLogger('WDABuild'); +const ROOT_DIR = path.resolve(__dirname, '..'); +const DERIVED_DATA_PATH = `${ROOT_DIR}/wdaBuild`; +const WDA_BUNDLE = 'WebDriverAgentRunner-Runner.app'; +const WDA_BUNDLE_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'Debug-iphonesimulator'); + +const WDA_BUNDLE_TV = 'WebDriverAgentRunner_tvOS-Runner.app'; +const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'Debug-appletvsimulator'); + +const TARGETS = ['runner', 'tv_runner']; +const SDKS = ['sim', 'tv_sim']; async function buildWebDriverAgent (xcodeVersion) { - // Get Xcode version - xcodeVersion = xcodeVersion || await xcode.getVersion(); - log.info(`Building bundle for Xcode version '${xcodeVersion}'`); + const target = process.env.TARGET; + const sdk = process.env.SDK; - // Clear WebDriverAgent from derived data - const derivedDataPath = path.resolve(os.homedir(), 'Library', 'Developer', - 'Xcode', 'DerivedData'); - log.info(`Clearing contents of '${derivedDataPath}/WebDriverAgent-*'`); - for (const wdaPath of - await fs.glob('WebDriverAgent-*', {cwd: derivedDataPath, absolute: true}) - ) { - log.info(`Deleting existing WDA: '${wdaPath}'`); - await fs.rimraf(wdaPath); + if (!TARGETS.includes(target)) { + throw Error(`Please set TARGETS environment variable from the supported targets ${JSON.stringify(TARGETS)}`); } - // Clean and build - log.info('Running ./Scripts/build.sh'); - let env = {TARGET: 'runner', SDK: 'sim'}; - try { - await exec('/bin/bash', ['./Scripts/build.sh'], {env, cwd: rootDir}); - } catch (e) { - log.error(`===FAILED TO BUILD FOR ${xcodeVersion}`); - log.error(e.stdout); - log.error(e.stderr); - log.error(e.message); - throw e; + if (!SDKS.includes(sdk)) { + throw Error(`Please set SDK environment variable from the supported SDKs ${JSON.stringify(SDKS)}`); } - // Create bundles folder - const pathToBundles = path.resolve(rootDir, 'bundles'); - await mkdirp(pathToBundles); - // Start creating zip - const uncompressedDir = path.resolve(rootDir, 'uncompressed'); - await fs.rimraf(uncompressedDir); - await mkdirp(uncompressedDir); - log.info('Creating zip'); + LOG.info(`Cleaning ${DERIVED_DATA_PATH} if exists`); + try { + await exec('xcodebuild', ['clean', '-derivedDataPath', DERIVED_DATA_PATH, '-scheme', 'WebDriverAgentRunner'], { + cwd: ROOT_DIR + }); + } catch (ign) {} - // Move contents of the root to folder called "uncompressed" - await exec('rsync', [ - '-av', '.', uncompressedDir, - '--exclude', 'node_modules', - '--exclude', 'build', - '--exclude', 'ci-jobs', - '--exclude', 'lib', - '--exclude', 'test', - '--exclude', 'bundles', - '--exclude', 'azure-templates', - ], {cwd: rootDir}); + // Get Xcode version + xcodeVersion = xcodeVersion || await xcode.getVersion(); + LOG.info(`Building WebDriverAgent for iOS using Xcode version '${xcodeVersion}'`); - // Move DerivedData/WebDriverAgent-* from Library to "uncompressed" folder - const wdaPath = (await fs.glob(`${derivedDataPath}/WebDriverAgent-*`))[0]; - await mkdirp(path.resolve(uncompressedDir, 'DerivedData')); - await fs.rename(wdaPath, path.resolve(uncompressedDir, 'DerivedData', 'WebDriverAgent')); + // Clean and build + try { + await exec('/bin/bash', ['./Scripts/build.sh'], { + env: {TARGET: target, SDK: sdk, DERIVED_DATA_PATH}, + cwd: ROOT_DIR + }); + } catch (e) { + LOG.error(`===FAILED TO BUILD FOR ${xcodeVersion}`); + LOG.error(e.stderr); + throw e; + } - // Compress the "uncompressed" bundle as a Zip - const pathToZip = path.resolve(pathToBundles, `webdriveragent-xcode_${xcodeVersion}.zip`); - await zip.toArchive( - pathToZip, {cwd: path.join(rootDir, 'uncompressed')} - ); - log.info(`Zip bundled at "${pathToZip}"`); + const isTv = target === 'tv_runner'; + const bundle = isTv ? WDA_BUNDLE_TV : WDA_BUNDLE; + const bundle_path = isTv ? WDA_BUNDLE_TV_PATH : WDA_BUNDLE_PATH; - // Now just zip the .app and place it in the root directory - // This zip file will be published to NPM - const wdaAppBundle = 'WebDriverAgentRunner-Runner.app'; - const appBundlePath = path.join(uncompressedDir, 'DerivedData', 'WebDriverAgent', - 'Build', 'Products', 'Debug-iphonesimulator', wdaAppBundle); - const appBundleZipPath = path.join(rootDir, `${wdaAppBundle}.zip`); + const zipName = `WebDriverAgentRunner-Runner-${sdk}-${xcodeVersion}.zip`; + LOG.info(`Creating ${zipName} which includes ${bundle}`); + const appBundleZipPath = path.join(ROOT_DIR, zipName); await fs.rimraf(appBundleZipPath); - log.info(`Created './${wdaAppBundle}.zip'`); - await zip.toArchive(appBundleZipPath, {cwd: appBundlePath}); - log.info(`Zip bundled at "${appBundleZipPath}"`); - // Clean up the uncompressed directory - await fs.rimraf(uncompressedDir); + LOG.info(`Created './${zipName}'`); + try { + await exec('xattr', ['-cr', bundle], {cwd: bundle_path}); + await exec('zip', ['-qr', appBundleZipPath, bundle], {cwd: bundle_path}); + } catch (e) { + LOG.error(`===FAILED TO ZIP ARCHIVE`); + LOG.error(e.stderr); + throw e; + } + LOG.info(`Zip bundled at "${appBundleZipPath}"`); } if (require.main === module) { diff --git a/Scripts/ci/build-real.sh b/Scripts/ci/build-real.sh new file mode 100755 index 000000000..93eec1145 --- /dev/null +++ b/Scripts/ci/build-real.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# To run build script for CI + +xcodebuild clean build-for-testing \ + -project WebDriverAgent.xcodeproj \ + -derivedDataPath $DERIVED_DATA_PATH \ + -scheme $SCHEME \ + -destination "$DESTINATION" \ + CODE_SIGNING_ALLOWED=NO ARCHS=arm64 + +# Only .app is needed. + +pushd $WD + +# to remove test packages to refer to the device local instead of embedded ones +# XCTAutomationSupport.framework, XCTest.framewor, XCTestCore.framework, +# XCUIAutomation.framework, XCUnit.framework +rm -rf $SCHEME-Runner.app/Frameworks/XC*.framework + +zip -r $ZIP_PKG_NAME $SCHEME-Runner.app +popd +mv $WD/$ZIP_PKG_NAME ./ diff --git a/Scripts/ci/build-sim.sh b/Scripts/ci/build-sim.sh new file mode 100755 index 000000000..de52abccf --- /dev/null +++ b/Scripts/ci/build-sim.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# To run build script for CI + +xcodebuild clean build-for-testing \ + -project WebDriverAgent.xcodeproj \ + -derivedDataPath wda_build \ + -scheme $SCHEME \ + -destination "$DESTINATION" \ + CODE_SIGNING_ALLOWED=NO ARCHS=$ARCHS + +# simulator needs to build entire build files + +pushd wda_build +# to remove unnecessary space consuming files +rm -rf Build/Intermediates.noindex +zip -r $ZIP_PKG_NAME Build +popd +mv wda_build/$ZIP_PKG_NAME ./ diff --git a/Scripts/fetch-prebuilt-wda.js b/Scripts/fetch-prebuilt-wda.js index a7ad5101f..8bedd9a3d 100644 --- a/Scripts/fetch-prebuilt-wda.js +++ b/Scripts/fetch-prebuilt-wda.js @@ -1,7 +1,7 @@ const path = require('path'); const axios = require('axios'); const { asyncify } = require('asyncbox'); -const { logger, fs, mkdirp, net } = require('appium/support'); +const { logger, fs, mkdirp, net } = require('@appium/support'); const _ = require('lodash'); const B = require('bluebird'); diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index fa3ee211b..4579803c8 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -64,11 +64,11 @@ 315A15072518CC2800A3A064 /* TouchSpotView.m in Sources */ = {isa = PBXBuildFile; fileRef = 315A15062518CC2800A3A064 /* TouchSpotView.m */; }; 315A150A2518D6F400A3A064 /* TouchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 315A15092518D6F400A3A064 /* TouchViewController.m */; }; 6385F4A7220A40760095BBDB /* XCUIApplicationProcessDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */; }; - 63CCF91221ECE4C700E94ABD /* FBImageIOScaler.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageIOScaler.h */; }; - 63CCF91321ECE4C700E94ABD /* FBImageIOScaler.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CCF91121ECE4C700E94ABD /* FBImageIOScaler.m */; }; - 63FD950221F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageIOScalerTests.m */; }; - 63FD950321F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageIOScalerTests.m */; }; - 63FD950421F9D06200A3E356 /* FBImageIOScalerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageIOScalerTests.m */; }; + 63CCF91221ECE4C700E94ABD /* FBImageProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */; }; + 63CCF91321ECE4C700E94ABD /* FBImageProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */; }; + 63FD950221F9D06100A3E356 /* FBImageProcessorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageProcessorTests.m */; }; + 63FD950321F9D06100A3E356 /* FBImageProcessorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageProcessorTests.m */; }; + 63FD950421F9D06200A3E356 /* FBImageProcessorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 631B523421F6174300625362 /* FBImageProcessorTests.m */; }; 641EE3452240C1C800173FCB /* UITestingUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */; }; 641EE5D72240C5CA00173FCB /* FBScreenshotCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB75F1CAEDF0C008C271F /* FBScreenshotCommands.m */; }; 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */; }; @@ -100,20 +100,18 @@ 641EE5F52240C5CA00173FCB /* XCUIElement+FBUID.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */; }; 641EE5F62240C5CA00173FCB /* FBRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */; }; 641EE5F72240C5CA00173FCB /* FBResponseJSONPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7811CAEDF0C008C271F /* FBResponseJSONPayload.m */; }; - 641EE5F82240C5CA00173FCB /* FBXCTestCaseImplementationFailureHoldingProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = EE7E271B1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.m */; }; 641EE5F92240C5CA00173FCB /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; }; 641EE5FA2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */; }; 641EE5FD2240C5CA00173FCB /* FBBaseActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */; }; 641EE5FE2240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */; }; 641EE5FF2240C5CA00173FCB /* XCUIElement+FBForceTouch.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */; }; 641EE6002240C5CA00173FCB /* FBTouchActionCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D7A1FAE3D2500B9559F /* FBTouchActionCommands.m */; }; - 641EE6012240C5CA00173FCB /* FBImageIOScaler.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CCF91121ECE4C700E94ABD /* FBImageIOScaler.m */; }; + 641EE6012240C5CA00173FCB /* FBImageProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */; }; 641EE6022240C5CA00173FCB /* FBTouchIDCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7631CAEDF0C008C271F /* FBTouchIDCommands.m */; }; 641EE6032240C5CA00173FCB /* FBDebugCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7551CAEDF0C008C271F /* FBDebugCommands.m */; }; 641EE6042240C5CA00173FCB /* NSString+FBXMLSafeString.m in Sources */ = {isa = PBXBuildFile; fileRef = 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */; }; 641EE6052240C5CA00173FCB /* FBUnknownCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7651CAEDF0C008C271F /* FBUnknownCommands.m */; }; 641EE6062240C5CA00173FCB /* FBOrientationCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB75D1CAEDF0C008C271F /* FBOrientationCommands.m */; }; - 641EE6072240C5CA00173FCB /* XCUICoordinate+FBFix.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC9EED520064FAA00BC0D5B /* XCUICoordinate+FBFix.m */; }; 641EE6082240C5CA00173FCB /* FBRuntimeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */; }; 641EE6092240C5CA00173FCB /* XCUIElement+FBUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */; }; 641EE60A2240C5CA00173FCB /* FBLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9B76A41CF7A43900275851 /* FBLogger.m */; }; @@ -140,7 +138,6 @@ 641EE6222240C5CA00173FCB /* FBApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7671CAEDF0C008C271F /* FBApplication.m */; }; 641EE6232240C5CA00173FCB /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; }; 641EE6242240C5CA00173FCB /* FBXCTestDaemonsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */; }; - 641EE6252240C5CA00173FCB /* XCUIElement+FBTap.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB74C1CAEDF0C008C271F /* XCUIElement+FBTap.m */; }; 641EE6262240C5CA00173FCB /* FBMathUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1888391DA661C400307AA8 /* FBMathUtils.m */; }; 641EE6272240C5CA00173FCB /* FBXCAXClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */; }; 641EE6312240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -187,10 +184,8 @@ 641EE65B2240C5CA00173FCB /* XCUIElementHitPointCoordinate.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35AD011E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE65C2240C5CA00173FCB /* XCTDarwinNotificationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCE1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE65D2240C5CA00173FCB /* XCTRunnerAutomationSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEE1E3B77D600A02D78 /* XCTRunnerAutomationSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 641EE65E2240C5CA00173FCB /* XCUICoordinate+FBFix.h in Headers */ = {isa = PBXBuildFile; fileRef = EEC9EED420064FAA00BC0D5B /* XCUICoordinate+FBFix.h */; }; 641EE65F2240C5CA00173FCB /* XCSourceCodeTreeNodeEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACC61E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6602240C5CA00173FCB /* XCUIElement+FBIsVisible.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7471CAEDF0C008C271F /* XCUIElement+FBIsVisible.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 641EE6612240C5CA00173FCB /* XCUIElement+FBTap.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB74B1CAEDF0C008C271F /* XCUIElement+FBTap.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6622240C5CA00173FCB /* FBResponsePayload.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7821CAEDF0C008C271F /* FBResponsePayload.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6632240C5CA00173FCB /* FBUnknownCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */; }; 641EE6642240C5CA00173FCB /* NSPredicate+FBFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */; }; @@ -253,7 +248,7 @@ 641EE6A32240C5CA00173FCB /* XCUIApplication+FBTouchAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */; }; 641EE6A42240C5CA00173FCB /* FBCommandHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7751CAEDF0C008C271F /* FBCommandHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6A52240C5CA00173FCB /* FBSessionCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7601CAEDF0C008C271F /* FBSessionCommands.h */; }; - 641EE6A62240C5CA00173FCB /* FBImageIOScaler.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageIOScaler.h */; }; + 641EE6A62240C5CA00173FCB /* FBImageProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */; }; 641EE6A72240C5CA00173FCB /* FBSession-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7891CAEDF0C008C271F /* FBSession-Private.h */; }; 641EE6A82240C5CA00173FCB /* NSString+FBXMLSafeString.h in Headers */ = {isa = PBXBuildFile; fileRef = 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */; }; 641EE6A92240C5CA00173FCB /* FBCommandStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7761CAEDF0C008C271F /* FBCommandStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -301,7 +296,6 @@ 641EE6D72240C5CA00173FCB /* XCTestObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACE21E3B77D600A02D78 /* XCTestObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6D82240C5CA00173FCB /* XCUIElement.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFE1E3B77D600A02D78 /* XCUIElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6D92240C5CA00173FCB /* XCKeyboardInputSolver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBE1E3B77D600A02D78 /* XCKeyboardInputSolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 641EE6DA2240C5CA00173FCB /* FBXCTestCaseImplementationFailureHoldingProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E271A1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.h */; }; 641EE6DB2240C5CA00173FCB /* FBPasteboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 71930C4020662E1F00D3AFEC /* FBPasteboard.h */; }; 641EE6DC2240C5CA00173FCB /* FBAppiumActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097451FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h */; }; 641EE6DD2240C5CA00173FCB /* FBDebugLogDelegateDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -344,6 +338,8 @@ 64B26508228C5514002A5025 /* XCUIElementDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = 64B26507228C5514002A5025 /* XCUIElementDouble.m */; }; 64B2650A228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */; }; 64B2650B228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */; }; + 64E3502E2AC0B6EB005F3ACB /* NSDictionary+FBUtf8SafeDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */; }; + 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; 711084441DA3AA7500F913D6 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 711084421DA3AA7500F913D6 /* FBXPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; 7119097C2152580600BA3C7E /* XCUIScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 7119097B2152580600BA3C7E /* XCUIScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -385,6 +381,10 @@ 714D88CD2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */; }; 714D88CE2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */; }; 714D88CF2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */; }; + 714E14B829805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */; }; + 714E14B929805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */; }; + 714E14BA29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */; }; + 714E14BB29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */; }; 714EAA0D2673FDFE005C5B47 /* FBCapabilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */; }; 714EAA0E2673FDFE005C5B47 /* FBCapabilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */; }; 714EAA0F2673FDFE005C5B47 /* FBCapabilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 714EAA0C2673FDFE005C5B47 /* FBCapabilities.m */; }; @@ -427,6 +427,9 @@ 716E0BCE1E917E810087A825 /* NSString+FBXMLSafeString.h in Headers */ = {isa = PBXBuildFile; fileRef = 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */; }; 716E0BCF1E917E810087A825 /* NSString+FBXMLSafeString.m in Sources */ = {isa = PBXBuildFile; fileRef = 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */; }; 716E0BD11E917F260087A825 /* FBXMLSafeStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 716E0BD01E917F260087A825 /* FBXMLSafeStringTests.m */; }; + 716F0DA12A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; + 716F0DA32A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */; }; + 716F0DA62A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */; }; 718226CA2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */; }; 718226CB2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */; }; 718226CC2587443700661B83 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; @@ -471,12 +474,15 @@ 71A224E51DE2F56600844D55 /* NSPredicate+FBFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */; }; 71A224E61DE2F56600844D55 /* NSPredicate+FBFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A224E41DE2F56600844D55 /* NSPredicate+FBFormat.m */; }; 71A224E81DE326C500844D55 /* NSPredicateFBFormatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A224E71DE326C500844D55 /* NSPredicateFBFormatTests.m */; }; + 71A5C67329A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 71A5C67429A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 71A5C67529A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */; }; + 71A5C67629A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */; }; 71A7EAF51E20516B001DA4F2 /* XCUIElement+FBClassChain.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF31E20516B001DA4F2 /* XCUIElement+FBClassChain.h */; }; 71A7EAF61E20516B001DA4F2 /* XCUIElement+FBClassChain.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF41E20516B001DA4F2 /* XCUIElement+FBClassChain.m */; }; 71A7EAF91E224648001DA4F2 /* FBClassChainQueryParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF71E224648001DA4F2 /* FBClassChainQueryParser.h */; }; 71A7EAFA1E224648001DA4F2 /* FBClassChainQueryParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */; }; 71A7EAFC1E229302001DA4F2 /* FBClassChainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */; }; - 71AB82B21FDAE8C000D1D7C3 /* FBElementScreenshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AB82B11FDAE8C000D1D7C3 /* FBElementScreenshotTests.m */; }; 71ACF5B8242F2FDC00F0AAD4 /* FBSafariAlertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */; }; 71B155DA23070ECF00646AFB /* FBHTTPStatusCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DB230711E900646AFB /* FBCommandStatus.m */; }; @@ -594,8 +600,6 @@ EE158AB11CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */; }; EE158AB21CBD456F00A3E3F0 /* XCUIElement+FBScrolling.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7491CAEDF0C008C271F /* XCUIElement+FBScrolling.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE158AB31CBD456F00A3E3F0 /* XCUIElement+FBScrolling.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */; }; - EE158AB41CBD456F00A3E3F0 /* XCUIElement+FBTap.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB74B1CAEDF0C008C271F /* XCUIElement+FBTap.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE158AB51CBD456F00A3E3F0 /* XCUIElement+FBTap.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB74C1CAEDF0C008C271F /* XCUIElement+FBTap.m */; }; EE158AB81CBD456F00A3E3F0 /* FBAlertViewCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7501CAEDF0C008C271F /* FBAlertViewCommands.h */; }; EE158AB91CBD456F00A3E3F0 /* FBAlertViewCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7511CAEDF0C008C271F /* FBAlertViewCommands.m */; }; EE158ABA1CBD456F00A3E3F0 /* FBCustomCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7521CAEDF0C008C271F /* FBCustomCommands.h */; }; @@ -787,8 +791,6 @@ EE6B64FE1D0F86EF00E85F5D /* XCTestPrivateSymbols.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */; }; EE7E271C1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE7E271D1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m in Sources */ = {isa = PBXBuildFile; fileRef = EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */; }; - EE7E271E1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E271A1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.h */; }; - EE7E271F1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = EE7E271B1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.m */; }; EE8BA97A1DCCED9A00A9DEF8 /* FBNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8BA9791DCCED9A00A9DEF8 /* FBNavigationController.m */; }; EE8DDD7E20C5733C004D4925 /* XCUIElement+FBForceTouch.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */; }; EE8DDD7F20C5733C004D4925 /* XCUIElement+FBForceTouch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -807,9 +809,6 @@ EE9B76AA1CF7A43900275851 /* FBMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A51CF7A43900275851 /* FBMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; EEBBD48B1D47746D00656A81 /* XCUIElement+FBFind.h in Headers */ = {isa = PBXBuildFile; fileRef = EEBBD4891D47746D00656A81 /* XCUIElement+FBFind.h */; settings = {ATTRIBUTES = (Public, ); }; }; EEBBD48C1D47746D00656A81 /* XCUIElement+FBFind.m in Sources */ = {isa = PBXBuildFile; fileRef = EEBBD48A1D47746D00656A81 /* XCUIElement+FBFind.m */; }; - EEC9EED620064FAA00BC0D5B /* XCUICoordinate+FBFix.h in Headers */ = {isa = PBXBuildFile; fileRef = EEC9EED420064FAA00BC0D5B /* XCUICoordinate+FBFix.h */; }; - EEC9EED720064FAA00BC0D5B /* XCUICoordinate+FBFix.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC9EED520064FAA00BC0D5B /* XCUICoordinate+FBFix.m */; }; - EEC9EED920077D8E00BC0D5B /* XCUICoordinateFix.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC9EED820077D8E00BC0D5B /* XCUICoordinateFix.m */; }; EEDFE1211D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; EEDFE1221D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */; }; EEE16E971D33A25500172525 /* FBConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE16E961D33A25500172525 /* FBConfigurationTests.m */; }; @@ -988,11 +987,11 @@ 315A15082518D6F400A3A064 /* TouchViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchViewController.h; sourceTree = ""; }; 315A15092518D6F400A3A064 /* TouchViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchViewController.m; sourceTree = ""; }; 44757A831D42CE8300ECF35E /* XCUIDeviceRotationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIDeviceRotationTests.m; sourceTree = ""; }; - 631B523421F6174300625362 /* FBImageIOScalerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageIOScalerTests.m; sourceTree = ""; }; + 631B523421F6174300625362 /* FBImageProcessorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageProcessorTests.m; sourceTree = ""; }; 633E904A220DEE7F007CADF9 /* XCUIApplicationProcessDelay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationProcessDelay.h; sourceTree = ""; }; 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCUIApplicationProcessDelay.m; sourceTree = ""; }; - 63CCF91021ECE4C700E94ABD /* FBImageIOScaler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBImageIOScaler.h; sourceTree = ""; }; - 63CCF91121ECE4C700E94ABD /* FBImageIOScaler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageIOScaler.m; sourceTree = ""; }; + 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBImageProcessor.h; sourceTree = ""; }; + 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBImageProcessor.m; sourceTree = ""; }; 641EE2DA2240BBE300173FCB /* WebDriverAgentRunner_tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WebDriverAgentRunner_tvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WebDriverAgentLib_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBTVFocuse.h"; sourceTree = ""; }; @@ -1044,6 +1043,8 @@ 714CA3C61DC23186000F12C9 /* FBXPathIntegrationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPathIntegrationTests.m; sourceTree = ""; }; 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXMLGenerationOptions.h; sourceTree = ""; }; 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXMLGenerationOptions.m; sourceTree = ""; }; + 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCAXClient_iOS+FBSnapshotReqParams.h"; sourceTree = ""; }; + 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCAXClient_iOS+FBSnapshotReqParams.m"; sourceTree = ""; }; 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBCapabilities.h; sourceTree = ""; }; 714EAA0C2673FDFE005C5B47 /* FBCapabilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBCapabilities.m; sourceTree = ""; }; 7150348521A6DAD600A0F4BA /* FBImageUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBImageUtils.h; sourceTree = ""; }; @@ -1077,6 +1078,9 @@ 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+FBXMLSafeString.h"; sourceTree = ""; }; 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+FBXMLSafeString.m"; sourceTree = ""; }; 716E0BD01E917F260087A825 /* FBXMLSafeStringTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXMLSafeStringTests.m; sourceTree = ""; }; + 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+FBUtf8SafeDictionary.h"; sourceTree = ""; }; + 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+FBUtf8SafeDictionary.m"; sourceTree = ""; }; + 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSDictionaryFBUtf8SafeTests.m; sourceTree = ""; }; 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TVOSSettings.xcconfig; sourceTree = ""; }; 717C0D862518ED7000CAA6EC /* TVOSTestSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TVOSTestSettings.xcconfig; sourceTree = ""; }; 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GCDAsyncUdpSocket.h; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h; sourceTree = SOURCE_ROOT; }; @@ -1099,12 +1103,13 @@ 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSPredicate+FBFormat.h"; path = "../Utilities/NSPredicate+FBFormat.h"; sourceTree = ""; }; 71A224E41DE2F56600844D55 /* NSPredicate+FBFormat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSPredicate+FBFormat.m"; path = "../Utilities/NSPredicate+FBFormat.m"; sourceTree = ""; }; 71A224E71DE326C500844D55 /* NSPredicateFBFormatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSPredicateFBFormatTests.m; sourceTree = ""; }; + 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTIssue+FBPatcher.h"; sourceTree = ""; }; + 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCTIssue+FBPatcher.m"; sourceTree = ""; }; 71A7EAF31E20516B001DA4F2 /* XCUIElement+FBClassChain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBClassChain.h"; sourceTree = ""; }; 71A7EAF41E20516B001DA4F2 /* XCUIElement+FBClassChain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBClassChain.m"; sourceTree = ""; }; 71A7EAF71E224648001DA4F2 /* FBClassChainQueryParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBClassChainQueryParser.h; sourceTree = ""; }; 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainQueryParser.m; sourceTree = ""; }; 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainTests.m; sourceTree = ""; }; - 71AB82B11FDAE8C000D1D7C3 /* FBElementScreenshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementScreenshotTests.m; sourceTree = ""; }; 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSafariAlertTests.m; sourceTree = ""; }; 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBHTTPStatusCodes.h; sourceTree = ""; }; 71B155DB230711E900646AFB /* FBCommandStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBCommandStatus.m; sourceTree = ""; }; @@ -1328,8 +1333,6 @@ EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCTestPrivateSymbols.m; sourceTree = ""; }; EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBDebugLogDelegateDecorator.h; sourceTree = ""; }; EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBDebugLogDelegateDecorator.m; sourceTree = ""; }; - EE7E271A1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBXCTestCaseImplementationFailureHoldingProxy.h; sourceTree = ""; }; - EE7E271B1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXCTestCaseImplementationFailureHoldingProxy.m; sourceTree = ""; }; EE836C021C0F118600D87246 /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EE8BA9781DCCED9A00A9DEF8 /* FBNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBNavigationController.h; sourceTree = ""; }; EE8BA9791DCCED9A00A9DEF8 /* FBNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBNavigationController.m; sourceTree = ""; }; @@ -1342,8 +1345,6 @@ EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBIsVisible.m"; sourceTree = ""; }; EE9AB7491CAEDF0C008C271F /* XCUIElement+FBScrolling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBScrolling.h"; sourceTree = ""; }; EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBScrolling.m"; sourceTree = ""; }; - EE9AB74B1CAEDF0C008C271F /* XCUIElement+FBTap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBTap.h"; sourceTree = ""; }; - EE9AB74C1CAEDF0C008C271F /* XCUIElement+FBTap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBTap.m"; sourceTree = ""; }; EE9AB7501CAEDF0C008C271F /* FBAlertViewCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBAlertViewCommands.h; sourceTree = ""; }; EE9AB7511CAEDF0C008C271F /* FBAlertViewCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBAlertViewCommands.m; sourceTree = ""; }; EE9AB7521CAEDF0C008C271F /* FBCustomCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBCustomCommands.h; sourceTree = ""; }; @@ -1416,9 +1417,6 @@ EEC088E41CB56AC000B65968 /* FBElementCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementCache.m; sourceTree = ""; }; EEC088E61CB56DA400B65968 /* FBExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBExceptionHandler.h; sourceTree = ""; }; EEC088E71CB56DA400B65968 /* FBExceptionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBExceptionHandler.m; sourceTree = ""; }; - EEC9EED420064FAA00BC0D5B /* XCUICoordinate+FBFix.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUICoordinate+FBFix.h"; sourceTree = ""; }; - EEC9EED520064FAA00BC0D5B /* XCUICoordinate+FBFix.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUICoordinate+FBFix.m"; sourceTree = ""; }; - EEC9EED820077D8E00BC0D5B /* XCUICoordinateFix.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCUICoordinateFix.m; sourceTree = ""; }; EEDBEBBA1CB2681900A790A2 /* WebDriverAgent.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = WebDriverAgent.bundle; sourceTree = ""; }; EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBHealthCheck.h"; sourceTree = ""; }; EEDFE1201D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIDevice+FBHealthCheck.m"; sourceTree = ""; }; @@ -1825,6 +1823,8 @@ EE9AB73E1CAEDF0C008C271F /* Categories */ = { isa = PBXGroup; children = ( + 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */, + 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */, 71555A3B1DEC460A007D4A8B /* NSExpression+FBFormat.h */, 71555A3C1DEC460A007D4A8B /* NSExpression+FBFormat.m */, 71A224E31DE2F56600844D55 /* NSPredicate+FBFormat.h */, @@ -1833,6 +1833,10 @@ EE0D1F601EBCDCF7006A3123 /* NSString+FBVisualLength.m */, 716E0BCC1E917E810087A825 /* NSString+FBXMLSafeString.h */, 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */, + 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */, + 714E14B729805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m */, + 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */, + 71A5C67229A4F39600421C37 /* XCTIssue+FBPatcher.m */, AD6C269A1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h */, AD6C269B1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m */, 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */, @@ -1841,8 +1845,6 @@ 716C9DFF27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.m */, 71D475C02538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h */, 71D475C12538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m */, - EEC9EED420064FAA00BC0D5B /* XCUICoordinate+FBFix.h */, - EEC9EED520064FAA00BC0D5B /* XCUICoordinate+FBFix.m */, 719CD8FA2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h */, 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */, 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */, @@ -1873,8 +1875,6 @@ EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */, 71F5BE21252E576C00EE9EBA /* XCUIElement+FBSwiping.h */, 71F5BE22252E576C00EE9EBA /* XCUIElement+FBSwiping.m */, - EE9AB74B1CAEDF0C008C271F /* XCUIElement+FBTap.h */, - EE9AB74C1CAEDF0C008C271F /* XCUIElement+FBTap.m */, AD76723B1D6B7CC000610457 /* XCUIElement+FBTyping.h */, AD76723C1D6B7CC000610457 /* XCUIElement+FBTyping.m */, 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */, @@ -2009,8 +2009,8 @@ EE3A18611CDE618F00DE4205 /* FBErrorBuilder.m */, EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */, EE6A89391D0B38640083E92B /* FBFailureProofTestCase.m */, - 63CCF91021ECE4C700E94ABD /* FBImageIOScaler.h */, - 63CCF91121ECE4C700E94ABD /* FBImageIOScaler.m */, + 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */, + 63CCF91121ECE4C700E94ABD /* FBImageProcessor.m */, 7150348521A6DAD600A0F4BA /* FBImageUtils.h */, 7150348621A6DAD600A0F4BA /* FBImageUtils.m */, EE9B76A31CF7A43900275851 /* FBLogger.h */, @@ -2051,8 +2051,6 @@ 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */, EE5A24401F136C8D0078B1D9 /* FBXCodeCompatibility.h */, EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */, - EE7E271A1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.h */, - EE7E271B1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.m */, EE35AD791E3B80C000A02D78 /* FBXCTestDaemonsProxy.h */, EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */, 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */, @@ -2096,7 +2094,6 @@ 719A97AB1F88E7370063B4BD /* FBAppiumMultiTouchActionsIntegrationTests.m */, 719CD8FE2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m */, EE26409C1D0EBA25009BE6B0 /* FBElementAttributeTests.m */, - 71AB82B11FDAE8C000D1D7C3 /* FBElementScreenshotTests.m */, 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */, EE006EAC1EB99B15006900A4 /* FBElementVisibilityTests.m */, EE6A89361D0B35920083E92B /* FBFailureProofTestCaseTests.m */, @@ -2126,7 +2123,7 @@ 71E504941DF59BAD0020C32A /* XCUIElementAttributesTests.m */, EEBBD48D1D4785FC00656A81 /* XCUIElementFBFindTests.m */, EE1E06E11D181CC9007CF043 /* XCUIElementHelperIntegrationTests.m */, - 631B523421F6174300625362 /* FBImageIOScalerTests.m */, + 631B523421F6174300625362 /* FBImageProcessorTests.m */, 644D9CCD230E1F1A00C90459 /* FBConfigurationTests.m */, ); path = IntegrationTests; @@ -2154,9 +2151,9 @@ 716E0BD01E917F260087A825 /* FBXMLSafeStringTests.m */, 712A0C841DA3E459007D02E5 /* FBXPathTests.m */, EE9B76581CF7987300275851 /* Info.plist */, + 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */, 7139145B1DF01A12005896C2 /* NSExpressionFBFormatTests.m */, 71A224E71DE326C500844D55 /* NSPredicateFBFormatTests.m */, - EEC9EED820077D8E00BC0D5B /* XCUICoordinateFix.m */, 713914591DF01989005896C2 /* XCUIElementHelpersTests.m */, ); path = UnitTests; @@ -2404,6 +2401,7 @@ 641EE64B2240C5CA00173FCB /* XCTAsyncActivity.h in Headers */, 641EE64C2240C5CA00173FCB /* XCTestMisuseObserver.h in Headers */, 641EE64D2240C5CA00173FCB /* XCTRunnerDaemonSession.h in Headers */, + 714E14B929805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */, 64B2650B228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */, 641EE64E2240C5CA00173FCB /* FBApplication.h in Headers */, 641EE64F2240C5CA00173FCB /* XCTestExpectationWaiter.h in Headers */, @@ -2422,10 +2420,8 @@ 641EE65B2240C5CA00173FCB /* XCUIElementHitPointCoordinate.h in Headers */, 641EE65C2240C5CA00173FCB /* XCTDarwinNotificationExpectation.h in Headers */, 641EE65D2240C5CA00173FCB /* XCTRunnerAutomationSession.h in Headers */, - 641EE65E2240C5CA00173FCB /* XCUICoordinate+FBFix.h in Headers */, 641EE65F2240C5CA00173FCB /* XCSourceCodeTreeNodeEnumerator.h in Headers */, 641EE6602240C5CA00173FCB /* XCUIElement+FBIsVisible.h in Headers */, - 641EE6612240C5CA00173FCB /* XCUIElement+FBTap.h in Headers */, 641EE6622240C5CA00173FCB /* FBResponsePayload.h in Headers */, 641EE6632240C5CA00173FCB /* FBUnknownCommands.h in Headers */, 641EE7062240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */, @@ -2460,6 +2456,7 @@ 641EE67B2240C5CA00173FCB /* XCUIRecorderUtilities.h in Headers */, 6496A5DA230D6EB30087F8CB /* AXSettings.h in Headers */, 641EE67C2240C5CA00173FCB /* XCTestCaseRun.h in Headers */, + 71A5C67429A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */, 641EE67D2240C5CA00173FCB /* XCTestConfiguration.h in Headers */, 641EE67E2240C5CA00173FCB /* _XCTDarwinNotificationExpectationImplementation.h in Headers */, 641EE67F2240C5CA00173FCB /* XCTestExpectation.h in Headers */, @@ -2504,9 +2501,10 @@ 641EE6A42240C5CA00173FCB /* FBCommandHandler.h in Headers */, 641EE6A52240C5CA00173FCB /* FBSessionCommands.h in Headers */, 641EE70C2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */, - 641EE6A62240C5CA00173FCB /* FBImageIOScaler.h in Headers */, + 641EE6A62240C5CA00173FCB /* FBImageProcessor.h in Headers */, 641EE6A72240C5CA00173FCB /* FBSession-Private.h in Headers */, 641EE6A82240C5CA00173FCB /* NSString+FBXMLSafeString.h in Headers */, + 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, 641EE6A92240C5CA00173FCB /* FBCommandStatus.h in Headers */, 71822702258744A400661B83 /* HTTPResponseProxy.h in Headers */, 71822741258744BB00661B83 /* HTTPLogging.h in Headers */, @@ -2567,7 +2565,6 @@ 641EE6D72240C5CA00173FCB /* XCTestObserver.h in Headers */, 641EE6D82240C5CA00173FCB /* XCUIElement.h in Headers */, 641EE6D92240C5CA00173FCB /* XCKeyboardInputSolver.h in Headers */, - 641EE6DA2240C5CA00173FCB /* FBXCTestCaseImplementationFailureHoldingProxy.h in Headers */, 718226CB2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, 641EE6DB2240C5CA00173FCB /* FBPasteboard.h in Headers */, 711CD03525ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */, @@ -2648,6 +2645,7 @@ 6496A5D9230D6EB30087F8CB /* AXSettings.h in Headers */, EE35AD301E3B77D600A02D78 /* XCKeyboardKeyMap.h in Headers */, EE35AD5D1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h in Headers */, + 714E14B829805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */, EE158B5F1CBD47A000A3E3F0 /* WebDriverAgentLib.h in Headers */, EE158AC01CBD456F00A3E3F0 /* FBFindElementCommands.h in Headers */, 71D475C22538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h in Headers */, @@ -2662,11 +2660,9 @@ EE35AD3F1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h in Headers */, EE35AD5F1E3B77D600A02D78 /* XCTRunnerAutomationSession.h in Headers */, 71C9EAAC25E8415A00470CD8 /* FBScreenshot.h in Headers */, - EEC9EED620064FAA00BC0D5B /* XCUICoordinate+FBFix.h in Headers */, EE35AD371E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h in Headers */, EE158AB01CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.h in Headers */, 71414ED42670A1EE003A8C5D /* LRUCache.h in Headers */, - EE158AB41CBD456F00A3E3F0 /* XCUIElement+FBTap.h in Headers */, EE158ADC1CBD456F00A3E3F0 /* FBResponsePayload.h in Headers */, 13815F6F2328D20400CDAB61 /* FBActiveAppDetectionPoint.h in Headers */, EE158ACC1CBD456F00A3E3F0 /* FBUnknownCommands.h in Headers */, @@ -2736,6 +2732,8 @@ E444DC83249131B10060D7EB /* DDNumber.h in Headers */, EE35AD271E3B77D600A02D78 /* XCApplicationMonitor.h in Headers */, EE8DDD7F20C5733C004D4925 /* XCUIElement+FBForceTouch.h in Headers */, + 71A5C67329A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */, + 716F0DA12A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, EE158AEA1CBD456F00A3E3F0 /* FBRuntimeUtils.h in Headers */, 7136A4791E8918E60024FC3D /* XCUIElement+FBPickerWheel.h in Headers */, E444DCB324913C220060D7EB /* RoutingHTTPServer.h in Headers */, @@ -2753,7 +2751,7 @@ EE158AC81CBD456F00A3E3F0 /* FBSessionCommands.h in Headers */, 71C8E55125399A6B008572C1 /* XCUIApplication+FBQuiescence.h in Headers */, 641EE70B2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */, - 63CCF91221ECE4C700E94ABD /* FBImageIOScaler.h in Headers */, + 63CCF91221ECE4C700E94ABD /* FBImageProcessor.h in Headers */, EE158AE31CBD456F00A3E3F0 /* FBSession-Private.h in Headers */, 716E0BCE1E917E810087A825 /* NSString+FBXMLSafeString.h in Headers */, EE158ACF1CBD456F00A3E3F0 /* FBCommandStatus.h in Headers */, @@ -2818,7 +2816,6 @@ EE35AD531E3B77D600A02D78 /* XCTestObserver.h in Headers */, EE35AD6F1E3B77D600A02D78 /* XCUIElement.h in Headers */, EE35AD2F1E3B77D600A02D78 /* XCKeyboardInputSolver.h in Headers */, - EE7E271E1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.h in Headers */, 71930C4220662E1F00D3AFEC /* FBPasteboard.h in Headers */, 714097471FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h in Headers */, EE7E271C1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h in Headers */, @@ -3187,6 +3184,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 64E3502E2AC0B6EB005F3ACB /* NSDictionary+FBUtf8SafeDictionary.m in Sources */, 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */, E444DCBC24917A5E0060D7EB /* HTTPResponseProxy.m in Sources */, 71D3B3D8267FC7260076473D /* XCUIElement+FBResolve.m in Sources */, @@ -3215,6 +3213,7 @@ 641EE70F2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, 714D88CF2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */, 641EE5DE2240C5CA00173FCB /* XCUIApplication+FBTouchAction.m in Sources */, + 714E14BB29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */, 641EE5DF2240C5CA00173FCB /* FBWebServer.m in Sources */, 641EE5E02240C5CA00173FCB /* FBTCPSocket.m in Sources */, 641EE5E12240C5CA00173FCB /* FBErrorBuilder.m in Sources */, @@ -3246,7 +3245,6 @@ 641EE5F62240C5CA00173FCB /* FBRouteRequest.m in Sources */, 641EE5F72240C5CA00173FCB /* FBResponseJSONPayload.m in Sources */, 718226D12587443700661B83 /* GCDAsyncUdpSocket.m in Sources */, - 641EE5F82240C5CA00173FCB /* FBXCTestCaseImplementationFailureHoldingProxy.m in Sources */, 641EE5F92240C5CA00173FCB /* FBMjpegServer.m in Sources */, 641EE5FA2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.m in Sources */, 641EE5FD2240C5CA00173FCB /* FBBaseActionsSynthesizer.m in Sources */, @@ -3257,14 +3255,13 @@ 641EE6002240C5CA00173FCB /* FBTouchActionCommands.m in Sources */, 719DCF182601EAFB000E765F /* FBNotificationsHelper.m in Sources */, 714EAA102673FDFE005C5B47 /* FBCapabilities.m in Sources */, - 641EE6012240C5CA00173FCB /* FBImageIOScaler.m in Sources */, + 641EE6012240C5CA00173FCB /* FBImageProcessor.m in Sources */, 641EE6022240C5CA00173FCB /* FBTouchIDCommands.m in Sources */, 641EE6032240C5CA00173FCB /* FBDebugCommands.m in Sources */, 641EE6042240C5CA00173FCB /* NSString+FBXMLSafeString.m in Sources */, 641EE6052240C5CA00173FCB /* FBUnknownCommands.m in Sources */, 641EE6062240C5CA00173FCB /* FBOrientationCommands.m in Sources */, 641EE7092240CDEB00173FCB /* XCUIElement+FBTVFocuse.m in Sources */, - 641EE6072240C5CA00173FCB /* XCUICoordinate+FBFix.m in Sources */, 641EE6082240C5CA00173FCB /* FBRuntimeUtils.m in Sources */, 641EE6092240C5CA00173FCB /* XCUIElement+FBUtilities.m in Sources */, 641EE60A2240C5CA00173FCB /* FBLogger.m in Sources */, @@ -3276,6 +3273,7 @@ 641EE6102240C5CA00173FCB /* FBImageUtils.m in Sources */, 641EE6112240C5CA00173FCB /* FBSession.m in Sources */, 641EE6122240C5CA00173FCB /* FBFindElementCommands.m in Sources */, + 71A5C67629A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, 641EE6132240C5CA00173FCB /* FBDebugLogDelegateDecorator.m in Sources */, 641EE6142240C5CA00173FCB /* FBAlertViewCommands.m in Sources */, 71414EDB2670A1EE003A8C5D /* LRUCacheNode.m in Sources */, @@ -3298,7 +3296,6 @@ 641EE6232240C5CA00173FCB /* FBScreen.m in Sources */, 71D04DCB25356C43008A052C /* XCUIElement+FBCaching.m in Sources */, 641EE6242240C5CA00173FCB /* FBXCTestDaemonsProxy.m in Sources */, - 641EE6252240C5CA00173FCB /* XCUIElement+FBTap.m in Sources */, 13DE7A58287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */, 641EE6262240C5CA00173FCB /* FBMathUtils.m in Sources */, 641EE6272240C5CA00173FCB /* FBXCAXClientProxy.m in Sources */, @@ -3324,6 +3321,7 @@ 7136A47A1E8918E60024FC3D /* XCUIElement+FBPickerWheel.m in Sources */, E444DC84249131B10060D7EB /* DDRange.m in Sources */, 6385F4A7220A40760095BBDB /* XCUIApplicationProcessDelay.m in Sources */, + 71A5C67529A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */, 719CD8FD2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m in Sources */, 188940702153835800491471 /* UURandom.m in Sources */, @@ -3371,7 +3369,6 @@ EE158AE21CBD456F00A3E3F0 /* FBRouteRequest.m in Sources */, EE158ADB1CBD456F00A3E3F0 /* FBResponseJSONPayload.m in Sources */, 714EAA0F2673FDFE005C5B47 /* FBCapabilities.m in Sources */, - EE7E271F1D06C69F001BEC7B /* FBXCTestCaseImplementationFailureHoldingProxy.m in Sources */, 7155D704211DCEF400166C20 /* FBMjpegServer.m in Sources */, EEDFE1221D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m in Sources */, 714D88CE2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */, @@ -3380,7 +3377,7 @@ EEE3764A1D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m in Sources */, EE8DDD7E20C5733C004D4925 /* XCUIElement+FBForceTouch.m in Sources */, 71241D7C1FAE3D2500B9559F /* FBTouchActionCommands.m in Sources */, - 63CCF91321ECE4C700E94ABD /* FBImageIOScaler.m in Sources */, + 63CCF91321ECE4C700E94ABD /* FBImageProcessor.m in Sources */, EE158ACB1CBD456F00A3E3F0 /* FBTouchIDCommands.m in Sources */, 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */, EE158ABD1CBD456F00A3E3F0 /* FBDebugCommands.m in Sources */, @@ -3388,9 +3385,9 @@ EE158ACD1CBD456F00A3E3F0 /* FBUnknownCommands.m in Sources */, EE158AC51CBD456F00A3E3F0 /* FBOrientationCommands.m in Sources */, 1889406A2153835800491471 /* UUMonkey.m in Sources */, + 716F0DA32A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m in Sources */, 71D475C42538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m in Sources */, 641EE7082240CDEB00173FCB /* XCUIElement+FBTVFocuse.m in Sources */, - EEC9EED720064FAA00BC0D5B /* XCUICoordinate+FBFix.m in Sources */, 71E75E6F254824230099FC87 /* XCUIElementQuery+FBHelpers.m in Sources */, 71D04DCA25356C43008A052C /* XCUIElement+FBCaching.m in Sources */, EE158AEB1CBD456F00A3E3F0 /* FBRuntimeUtils.m in Sources */, @@ -3404,6 +3401,7 @@ EE6B64FE1D0F86EF00E85F5D /* XCTestPrivateSymbols.m in Sources */, AD76723E1D6B7CC000610457 /* XCUIElement+FBTyping.m in Sources */, EE158AAF1CBD456F00A3E3F0 /* XCUIElement+FBAccessibility.m in Sources */, + 714E14BA29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */, 7150348821A6DAD600A0F4BA /* FBImageUtils.m in Sources */, E444DCAB24913C220060D7EB /* HTTPResponseProxy.m in Sources */, E444DC6D249131890060D7EB /* HTTPErrorResponse.m in Sources */, @@ -3444,7 +3442,6 @@ 715AFAC21FFA29180053896D /* FBScreen.m in Sources */, 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */, EE35AD7C1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m in Sources */, - EE158AB51CBD456F00A3E3F0 /* XCUIElement+FBTap.m in Sources */, EE18883B1DA661C400307AA8 /* FBMathUtils.m in Sources */, 7157B292221DADD2001C348C /* FBXCAXClientProxy.m in Sources */, ); @@ -3456,7 +3453,7 @@ files = ( 71241D801FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m in Sources */, 71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */, - 63FD950221F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */, + 63FD950221F9D06100A3E356 /* FBImageProcessorTests.m in Sources */, 719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */, EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */, 71BD20781F869E0F00B36EC2 /* FBAppiumTouchActionsIntegrationTests.m in Sources */, @@ -3464,7 +3461,6 @@ 715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */, EE22021E1ECC618900A29571 /* FBTapTest.m in Sources */, 71930C472066434000D3AFEC /* FBPasteboardTests.m in Sources */, - 71AB82B21FDAE8C000D1D7C3 /* FBElementScreenshotTests.m in Sources */, 7150FFF722476B3A00B2EE28 /* FBForceTouchTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3474,7 +3470,7 @@ buildActionMask = 2147483647; files = ( EE5095E51EBCC9090028E2FE /* FBTypingTest.m in Sources */, - 63FD950321F9D06100A3E356 /* FBImageIOScalerTests.m in Sources */, + 63FD950321F9D06100A3E356 /* FBImageProcessorTests.m in Sources */, EE5095EB1EBCC9090028E2FE /* XCElementSnapshotHitPointTests.m in Sources */, EE5095EC1EBCC9090028E2FE /* XCUIApplicationHelperTests.m in Sources */, 7136C0F9243A182400921C76 /* FBW3CTypeActionsTests.m in Sources */, @@ -3511,12 +3507,12 @@ 7139145C1DF01A12005896C2 /* NSExpressionFBFormatTests.m in Sources */, 71A224E81DE326C500844D55 /* NSPredicateFBFormatTests.m in Sources */, EE6A892B1D0B25820083E92B /* FBApplicationDouble.m in Sources */, + 716F0DA62A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m in Sources */, EE6A892D1D0B2AF40083E92B /* FBErrorBuilderTests.m in Sources */, 712A0C851DA3E459007D02E5 /* FBXPathTests.m in Sources */, ADBC39981D07842800327304 /* XCUIElementDouble.m in Sources */, 7139145A1DF01989005896C2 /* XCUIElementHelpersTests.m in Sources */, EE6A89261D0B19E60083E92B /* FBSessionTests.m in Sources */, - EEC9EED920077D8E00BC0D5B /* XCUICoordinateFix.m in Sources */, 71A7EAFC1E229302001DA4F2 /* FBClassChainTests.m in Sources */, EE18883D1DA663EB00307AA8 /* FBMathUtilsTests.m in Sources */, ); @@ -3547,7 +3543,7 @@ 7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */, 71ACF5B8242F2FDC00F0AAD4 /* FBSafariAlertTests.m in Sources */, EE1E06DA1D1808C2007CF043 /* FBIntegrationTestCase.m in Sources */, - 63FD950421F9D06200A3E356 /* FBImageIOScalerTests.m in Sources */, + 63FD950421F9D06200A3E356 /* FBImageProcessorTests.m in Sources */, EE05BAFA1D13003C00A3EB00 /* FBKeyboardTests.m in Sources */, EE55B3271D1D54CF003AAAEC /* FBScrollingTests.m in Sources */, EE6A89371D0B35920083E92B /* FBFailureProofTestCaseTests.m in Sources */, @@ -3653,12 +3649,17 @@ "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 8SC497C43L; + ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -3709,12 +3710,17 @@ "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 7J7278KZZ5; + ENABLE_TESTING_SEARCH_PATHS = YES; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, ); MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3764,6 +3770,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", @@ -3775,6 +3782,10 @@ "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, ); OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; @@ -3782,6 +3793,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WARNING_CFLAGS = ( @@ -3810,6 +3822,7 @@ "-Wno-objc-messaging-id", "-Wno-direct-ivar-access", "-Wno-cast-qual", + "-Wno-declaration-after-statement", ); }; name = Debug; @@ -3827,6 +3840,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", @@ -3838,6 +3852,10 @@ "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /System/Developer/Library/Frameworks, + /System/Developer/Library/PrivateFrameworks, + /Developer/Library/PrivateFrameworks, + /Developer/Library/Frameworks, ); ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ""; @@ -3846,6 +3864,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WARNING_CFLAGS = ( @@ -3874,6 +3893,7 @@ "-Wno-objc-messaging-id", "-Wno-direct-ivar-access", "-Wno-cast-qual", + "-Wno-declaration-after-statement", ); }; name = Release; @@ -4076,6 +4096,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", @@ -4088,6 +4109,10 @@ "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, ); OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.WebDriverAgentLib; @@ -4123,6 +4148,7 @@ "-Wno-objc-messaging-id", "-Wno-direct-ivar-access", "-Wno-cast-qual", + "-Wno-declaration-after-statement", ); }; name = Debug; @@ -4139,6 +4165,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(PLATFORM_DIR)/Developer/Library/Frameworks", "$(PLATFORM_DIR)/Developer/Library/PrivateFrameworks", @@ -4151,6 +4178,10 @@ "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, ); ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ""; @@ -4187,6 +4218,7 @@ "-Wno-objc-messaging-id", "-Wno-direct-ivar-access", "-Wno-cast-qual", + "-Wno-declaration-after-statement", ); }; name = Release; @@ -4316,7 +4348,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 7J7278KZZ5; + DEVELOPMENT_TEAM = VDZ6WAVGC2; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -4336,7 +4368,7 @@ CLANG_ANALYZER_NONNULL = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7J7278KZZ5; + DEVELOPMENT_TEAM = VDZ6WAVGC2; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -4397,17 +4429,19 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 7J7278KZZ5; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); + DEVELOPMENT_TEAM = VDZ6WAVGC2; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = UUSENSE; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, ); OTHER_LDFLAGS = ( "$(inherited)", @@ -4453,16 +4487,18 @@ CLANG_STATIC_ANALYZER_MODE = deep; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7J7278KZZ5; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); + DEVELOPMENT_TEAM = VDZ6WAVGC2; + ENABLE_TESTING_SEARCH_PATHS = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = WebDriverAgentRunner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", + /Developer/Library/Frameworks, + /Developer/Library/PrivateFrameworks, + /System/Developer/Library/PrivateFrameworks, + /System/Developer/Library/Frameworks, ); ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme index 82f803a90..8857b934c 100644 --- a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner.xcscheme @@ -24,8 +24,8 @@ diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme index 198d2e6cc..e55655225 100644 --- a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/WebDriverAgentRunner_tvOS.xcscheme @@ -24,8 +24,8 @@ diff --git a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h new file mode 100644 index 000000000..0f657ab6f --- /dev/null +++ b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (FBUtf8SafeString) + +/** + Converts the string, so it could be properly represented in UTF-8 encoding. All non-encodable characters are replaced with + the given `replacement` + + @param replacement The character to use a a replacement for the lossy encoding + @returns Either the same string or a string with non-encodable chars replaced + */ +- (instancetype)fb_utf8SafeStringWithReplacement:(unichar)replacement; + +@end + +@interface NSDictionary (FBUtf8SafeDictionary) + +/** + Converts the dictionary, so it could be properly represented in UTF-8 encoding. All non-encodable characters + in string values are replaced with the Unocde question mark characters. Nested dictionaries and arrays are + processed recursively. + + @returns Either the same dictionary or a dictionary with non-encodable chars in string values replaced + */ +- (instancetype)fb_utf8SafeDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m new file mode 100644 index 000000000..399769142 --- /dev/null +++ b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "NSDictionary+FBUtf8SafeDictionary.h" + +const unichar REPLACER = 0xfffd; + +@implementation NSString (FBUtf8SafeString) + +- (instancetype)fb_utf8SafeStringWithReplacement:(unichar)replacement +{ + if ([self canBeConvertedToEncoding:NSUTF8StringEncoding]) { + return self; + } + + NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + NSString *convertedString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSMutableString *result = [NSMutableString string]; + NSString *replacementStr = [NSString stringWithCharacters:&replacement length:1]; + NSUInteger originalIdx = 0; + NSUInteger convertedIdx = 0; + while (originalIdx < [self length] && convertedIdx < [convertedString length]) { + unichar originalChar = [self characterAtIndex:originalIdx]; + unichar convertedChar = [convertedString characterAtIndex:convertedIdx]; + + if (originalChar == convertedChar) { + [result appendString:[NSString stringWithCharacters:&originalChar length:1]]; + originalIdx++; + convertedIdx++; + continue; + } + + while (originalChar != convertedChar && originalIdx < [self length]) { + [result appendString:replacementStr]; + originalChar = [self characterAtIndex:++originalIdx]; + } + } + return result.copy; +} + +@end + +@implementation NSArray (FBUtf8SafeArray) + +- (instancetype)fb_utf8SafeArray +{ + NSMutableArray *result = [NSMutableArray array]; + for (id item in self) { + if ([item isKindOfClass:NSString.class]) { + [result addObject:[(NSString *)item fb_utf8SafeStringWithReplacement:REPLACER]]; + } else if ([item isKindOfClass:NSDictionary.class]) { + [result addObject:[(NSDictionary *)item fb_utf8SafeDictionary]]; + } else if ([item isKindOfClass:NSArray.class]) { + [result addObject:[(NSArray *)item fb_utf8SafeArray]]; + } else { + [result addObject:item]; + } + } + return result.copy; +} + +@end + +@implementation NSDictionary (FBUtf8SafeDictionary) + +- (instancetype)fb_utf8SafeDictionary +{ + NSMutableDictionary *result = [self mutableCopy]; + for (id key in self) { + id value = result[key]; + if ([value isKindOfClass:NSString.class]) { + result[key] = [(NSString *)value fb_utf8SafeStringWithReplacement:REPLACER]; + } else if ([value isKindOfClass:NSArray.class]) { + result[key] = [(NSArray *)value fb_utf8SafeArray]; + } else if ([value isKindOfClass:NSDictionary.class]) { + result[key] = [(NSDictionary *)value fb_utf8SafeDictionary]; + } + } + return result.copy; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h new file mode 100644 index 000000000..8080f2097 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "XCAXClient_iOS.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const FBSnapshotMaxDepthKey; + +void FBSetCustomParameterForElementSnapshot (NSString* name, id value); + +id __nullable FBGetCustomParameterForElementSnapshot (NSString *name); + +@interface XCAXClient_iOS (FBSnapshotReqParams) + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m new file mode 100644 index 000000000..356a1ea05 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "XCAXClient_iOS+FBSnapshotReqParams.h" + +#import + +/** + Available parameters with their default values for XCTest: + @"maxChildren" : (int)2147483647 + @"traverseFromParentsToChildren" : YES + @"maxArrayCount" : (int)2147483647 + @"snapshotKeyHonorModalViews" : NO + @"maxDepth" : (int)2147483647 + */ +NSString *const FBSnapshotMaxDepthKey = @"maxDepth"; + +static id (*original_defaultParameters)(id, SEL); +static id (*original_snapshotParameters)(id, SEL); +static NSDictionary *defaultRequestParameters; +static NSDictionary *defaultAdditionalRequestParameters; +static NSMutableDictionary *customRequestParameters; + +void FBSetCustomParameterForElementSnapshot (NSString *name, id value) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + customRequestParameters = [NSMutableDictionary new]; + }); + customRequestParameters[name] = value; +} + +id FBGetCustomParameterForElementSnapshot (NSString *name) +{ + return customRequestParameters[name]; +} + +static id swizzledDefaultParameters(id self, SEL _cmd) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultRequestParameters = original_defaultParameters(self, _cmd); + }); + NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:defaultRequestParameters]; + [result addEntriesFromDictionary:defaultAdditionalRequestParameters ?: @{}]; + [result addEntriesFromDictionary:customRequestParameters ?: @{}]; + return result.copy; +} + +static id swizzledSnapshotParameters(id self, SEL _cmd) +{ + NSDictionary *result = original_snapshotParameters(self, _cmd); + defaultAdditionalRequestParameters = result; + return result; +} + + +@implementation XCAXClient_iOS (FBSnapshotReqParams) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" + ++ (void)load +{ + Method original_defaultParametersMethod = class_getInstanceMethod(self.class, @selector(defaultParameters)); + IMP swizzledDefaultParametersImp = (IMP)swizzledDefaultParameters; + original_defaultParameters = (id (*)(id, SEL)) method_setImplementation(original_defaultParametersMethod, swizzledDefaultParametersImp); + + Method original_snapshotParametersMethod = class_getInstanceMethod(NSClassFromString(@"XCTElementQuery"), NSSelectorFromString(@"snapshotParameters")); + IMP swizzledSnapshotParametersImp = (IMP)swizzledSnapshotParameters; + original_snapshotParameters = (id (*)(id, SEL)) method_setImplementation(original_snapshotParametersMethod, swizzledSnapshotParametersImp); +} + +#pragma clang diagnostic pop + +@end diff --git a/WebDriverAgentLib/Categories/XCUICoordinate+FBFix.h b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h similarity index 80% rename from WebDriverAgentLib/Categories/XCUICoordinate+FBFix.h rename to WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h index 14c8750eb..b4cdf1d44 100644 --- a/WebDriverAgentLib/Categories/XCUICoordinate+FBFix.h +++ b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h @@ -9,10 +9,10 @@ #import -#if !TARGET_OS_TV -@interface XCUICoordinate (FBFix) +NS_ASSUME_NONNULL_BEGIN -- (CGPoint)fb_screenPoint; +@interface XCTIssue (AMPatcher) @end -#endif + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m new file mode 100644 index 000000000..dc27e86a6 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "XCTIssue+FBPatcher.h" + +#import + +static _Bool swizzledShouldInterruptTest(id self, SEL _cmd) +{ + return NO; +} + +@implementation XCTIssue (AMPatcher) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" ++ (void)load +{ + SEL originalShouldInterruptTest = NSSelectorFromString(@"shouldInterruptTest"); + if (nil == originalShouldInterruptTest) return; + Method originalShouldInterruptTestMethod = class_getInstanceMethod(self.class, originalShouldInterruptTest); + if (nil == originalShouldInterruptTestMethod) return; + method_setImplementation(originalShouldInterruptTestMethod, (IMP)swizzledShouldInterruptTest); +} +#pragma clang diagnostic pop + +@end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m index 9130bb3b3..38b6586ef 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m @@ -36,26 +36,15 @@ - (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement * }]; NSPredicate *dstViewContainPredicate1 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeTextView]; NSPredicate *dstViewContainPredicate2 = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeButton]; - XCUIElement *candidate = nil; - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) { - // Find the first XCUIElementTypeOther which is the grandchild of the web view - // and is horizontally aligned to the center of the screen - candidate = [[[[[[scrollView descendantsMatchingType:XCUIElementTypeAny] - matchingIdentifier:@"WebView"] - descendantsMatchingType:XCUIElementTypeOther] - matchingPredicate:dstViewMatchPredicate] - containingPredicate:dstViewContainPredicate1] - containingPredicate:dstViewContainPredicate2].fb_firstMatch; - } else { - NSPredicate *webViewPredicate = [NSPredicate predicateWithFormat:@"elementType == %lu", XCUIElementTypeWebView]; - // Find the first XCUIElementTypeOther which is the descendant of the scroll view - // and is horizontally aligned to the center of the screen - candidate = [[[[[scrollView.fb_query containingPredicate:webViewPredicate] - descendantsMatchingType:XCUIElementTypeOther] - matchingPredicate:dstViewMatchPredicate] - containingPredicate:dstViewContainPredicate1] - containingPredicate:dstViewContainPredicate2].fb_firstMatch; - } + // Find the first XCUIElementTypeOther which is the grandchild of the web view + // and is horizontally aligned to the center of the screen + XCUIElement *candidate = [[[[[[scrollView descendantsMatchingType:XCUIElementTypeAny] + matchingIdentifier:@"WebView"] + descendantsMatchingType:XCUIElementTypeOther] + matchingPredicate:dstViewMatchPredicate] + containingPredicate:dstViewContainPredicate1] + containingPredicate:dstViewContainPredicate2].allElementsBoundByIndex.firstObject; + if (nil == candidate) { return nil; } @@ -80,7 +69,7 @@ - (XCUIElement *)fb_alertElement NSPredicate *alertCollectorPredicate = [NSPredicate predicateWithFormat:@"elementType IN {%lu,%lu,%lu}", XCUIElementTypeAlert, XCUIElementTypeSheet, XCUIElementTypeScrollView]; XCUIElement *alert = [[self descendantsMatchingType:XCUIElementTypeAny] - matchingPredicate:alertCollectorPredicate].fb_firstMatch; + matchingPredicate:alertCollectorPredicate].allElementsBoundByIndex.firstObject; if (nil == alert) { return nil; } diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h index 9d190a381..13fabcf88 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h @@ -103,6 +103,26 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames error:(NSError **)error; +/** + A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc + + @param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return List of found issues or nil if there was a failure + */ +- (nullable NSArray *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet *)auditTypes + error:(NSError **)error; + +/** + A wrapper over https://developer.apple.com/documentation/xctest/xcuiapplication/4190847-performaccessibilityauditwithaud?language=objc + + @param auditTypes Combination of https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return List of found issues or nil if there was a failure + */ +- (nullable NSArray *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes + error:(NSError **)error; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m index 6c9dde467..2656de7d6 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m @@ -12,6 +12,7 @@ #import "FBElementTypeTransformer.h" #import "FBKeyboard.h" #import "FBLogger.h" +#import "FBExceptions.h" #import "FBMacros.h" #import "FBMathUtils.h" #import "FBActiveAppDetectionPoint.h" @@ -31,9 +32,56 @@ #import "XCTestPrivateSymbols.h" #import "XCTRunnerDaemonSession.h" -const static NSTimeInterval FBMinimumAppSwitchWait = 3.0; static NSString* const FBUnknownBundleId = @"unknown"; +_Nullable id extractIssueProperty(id issue, NSString *propertyName) { + SEL selector = NSSelectorFromString(propertyName); + NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector]; + if (nil == methodSignature) { + return nil; + } + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation invokeWithTarget:issue]; + id __unsafe_unretained result; + [invocation getReturnValue:&result]; + return result; +} + +NSDictionary *auditTypeNamesToValues(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + // https://developer.apple.com/documentation/xctest/xcuiaccessibilityaudittype?language=objc + result = @{ + @"XCUIAccessibilityAuditTypeAction": @(1UL << 32), + @"XCUIAccessibilityAuditTypeAll": @(~0UL), + @"XCUIAccessibilityAuditTypeContrast": @(1UL << 0), + @"XCUIAccessibilityAuditTypeDynamicType": @(1UL << 16), + @"XCUIAccessibilityAuditTypeElementDetection": @(1UL << 1), + @"XCUIAccessibilityAuditTypeHitRegion": @(1UL << 2), + @"XCUIAccessibilityAuditTypeParentChild": @(1UL << 33), + @"XCUIAccessibilityAuditTypeSufficientElementDescription": @(1UL << 3), + @"XCUIAccessibilityAuditTypeTextClipped": @(1UL << 17), + @"XCUIAccessibilityAuditTypeTrait": @(1UL << 18), + }; + }); + return result; +} + +NSDictionary *auditTypeValuesToNames(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *inverted = [NSMutableDictionary new]; + [auditTypeNamesToValues() enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSNumber *value, BOOL *stop) { + inverted[value] = key; + }]; + result = inverted.copy; + }); + return result; +} + @implementation XCUIApplication (FBHelpers) @@ -92,7 +140,7 @@ - (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)err if(![[XCUIDevice sharedDevice] fb_goToHomescreenWithError:error]) { return NO; } - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MAX(duration, FBMinimumAppSwitchWait)]]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MAX(duration, .0)]]; [self fb_activate]; return YES; } @@ -127,9 +175,7 @@ + (NSDictionary *)dictionaryForElement:(id)snapshot recursi info[@"isEnabled"] = [@([wrappedSnapshot isWDEnabled]) stringValue]; info[@"isVisible"] = [@([wrappedSnapshot isWDVisible]) stringValue]; info[@"isAccessible"] = [@([wrappedSnapshot isWDAccessible]) stringValue]; -#if TARGET_OS_TV info[@"isFocused"] = [@([wrappedSnapshot isWDFocused]) stringValue]; -#endif if (!recursive) { return info.copy; @@ -193,7 +239,7 @@ - (NSString *)fb_xmlRepresentationWithOptions:(FBXMLGenerationOptions *)options - (NSString *)fb_descriptionRepresentation { NSMutableArray *childrenDescriptions = [NSMutableArray array]; - for (XCUIElement *child in [self.fb_query childrenMatchingType:XCUIElementTypeAny].allElementsBoundByAccessibilityElement) { + for (XCUIElement *child in [self.fb_query childrenMatchingType:XCUIElementTypeAny].allElementsBoundByIndex) { [childrenDescriptions addObject:child.debugDescription]; } // debugDescription property of XCUIApplication instance shows descendants addresses in memory @@ -259,7 +305,7 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames continue; } - [matchedKey fb_tapWithError:nil]; + [matchedKey tap]; if (isKeyboardInvisible()) { return YES; } @@ -272,7 +318,7 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames @[@(XCUIElementTypeKey), @(XCUIElementTypeButton)]]; NSArray *matchedKeys = findMatchingKeys(searchPredicate); if (matchedKeys.count > 0) { - [matchedKeys[matchedKeys.count - 1] fb_tapWithError:nil]; + [matchedKeys[matchedKeys.count - 1] tap]; } } #endif @@ -284,4 +330,67 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames error:error]; } +- (NSArray *> *)fb_performAccessibilityAuditWithAuditTypesSet:(NSSet *)auditTypes + error:(NSError **)error; +{ + uint64_t numTypes = 0; + NSDictionary *namesMap = auditTypeNamesToValues(); + for (NSString *value in auditTypes) { + NSNumber *typeValue = namesMap[value]; + if (nil == typeValue) { + NSString *reason = [NSString stringWithFormat:@"Audit type value '%@' is not known. Only the following audit types are supported: %@", value, namesMap.allKeys]; + @throw [NSException exceptionWithName:FBInvalidArgumentException reason:reason userInfo:@{}]; + } + numTypes |= [typeValue unsignedLongLongValue]; + } + return [self fb_performAccessibilityAuditWithAuditTypes:numTypes error:error]; +} + +- (NSArray *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes + error:(NSError **)error; +{ + SEL selector = NSSelectorFromString(@"performAccessibilityAuditWithAuditTypes:issueHandler:error:"); + if (![self respondsToSelector:selector]) { + [[[FBErrorBuilder alloc] + withDescription:@"Accessibility audit is only supported since iOS 17/Xcode 15"] + buildError:error]; + return nil; + } + + NSMutableArray *resultArray = [NSMutableArray array]; + NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation setArgument:&auditTypes atIndex:2]; + BOOL (^issueHandler)(id) = ^BOOL(id issue) { + NSString *auditType = @""; + NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); + NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; + if (nil != auditTypeValue) { + auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; + } + + id extractedElement = extractIssueProperty(issue, @"element"); + + id elementSnapshot = [extractedElement fb_takeSnapshot]; + NSDictionary *elementAttributes = elementSnapshot ? [self.class dictionaryForElement:elementSnapshot recursive:NO] : @{}; + + [resultArray addObject:@{ + @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", + @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", + @"auditType": auditType, + @"element": [extractedElement description] ?: @"", + @"elementDescription": [extractedElement debugDescription] ?: @"", + @"elementAttributes": elementAttributes ?: @{}, + }]; + return YES; + }; + [invocation setArgument:&issueHandler atIndex:3]; + [invocation setArgument:&error atIndex:4]; + [invocation invokeWithTarget:self]; + BOOL isSuccessful; + [invocation getReturnValue:&isSuccessful]; + return isSuccessful ? resultArray.copy : nil; +} + @end diff --git a/WebDriverAgentLib/Categories/XCUICoordinate+FBFix.m b/WebDriverAgentLib/Categories/XCUICoordinate+FBFix.m deleted file mode 100644 index 3313cda64..000000000 --- a/WebDriverAgentLib/Categories/XCUICoordinate+FBFix.m +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "XCUICoordinate+FBFix.h" - -#import "XCUICoordinate.h" -#import "XCUIElement+FBUtilities.h" - -# if !TARGET_OS_TV -@implementation XCUICoordinate (FBFix) - -- (CGPoint)fb_screenPoint -{ - CGPoint referencePoint = CGPointMake(0, 0); - NSValue *referencedElementFrame = nil; - if (self.element) { - CGRect elementFrame = self.element.frame; - if (self.referencedElement == self.element) { - referencedElementFrame = [NSValue valueWithCGRect:elementFrame]; - } - referencePoint = CGPointMake( - CGRectGetMinX(elementFrame) + CGRectGetWidth(elementFrame) * self.normalizedOffset.dx, - CGRectGetMinY(elementFrame) + CGRectGetHeight(elementFrame) * self.normalizedOffset.dy); - } else if (self.coordinate) { - referencePoint = self.coordinate.fb_screenPoint; - } - - CGPoint screenPoint = CGPointMake( - referencePoint.x + self.pointsOffset.dx, - referencePoint.y + self.pointsOffset.dy); - if (nil == referencedElementFrame) { - referencedElementFrame = [NSValue valueWithCGRect:self.referencedElement.frame]; - } - return CGPointMake( - MIN(CGRectGetMaxX(referencedElementFrame.CGRectValue), screenPoint.x), - MIN(CGRectGetMaxY(referencedElementFrame.CGRectValue), screenPoint.y)); -} - -@end -#endif diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h index d5e440b68..09f6e627f 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h @@ -9,6 +9,10 @@ #import +#if !TARGET_OS_TV +#import +#endif + NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { @@ -74,8 +78,9 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { - (nullable NSString *)fb_wifiIPAddress; /** - Opens the particular url scheme using Siri voice recognition helpers. - This will only work since XCode 8.3/iOS 10.3 + Opens the particular url scheme using the default application assigned to it. + This API only works since XCode 14.3/iOS 16.4 + Older Xcode/iOS version try to use Siri fallback. @param url The url scheme represented as a string, for example https://apple.com @param error If there is an error, upon return contains an NSError object that describes the problem. @@ -83,6 +88,17 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { */ - (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error; +/** + Opens the particular url scheme using the given application + This API only works since XCode 14.3/iOS 16.4 + + @param url The url scheme represented as a string, for example https://apple.com + @param bundleId The bundle identifier of an application to use in order to open the given URL + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation was successful + */ +- (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error; + /** Presses the corresponding hardware button on the device with duration. @@ -145,6 +161,37 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { */ - (nullable NSNumber *)fb_getAppearance; +#if !TARGET_OS_TV +/** + Allows to set a simulated geolocation coordinates. + Only works since Xcode 14.3/iOS 16.4 + + @param location The simlated location coordinates to set + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the simulated location has been successfully set + */ +- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error; + +/** + Allows to get a simulated geolocation coordinates. + Only works since Xcode 14.3/iOS 16.4 + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return The current simulated location or nil in case of failure or if no location has previously been seet + (the returned error will be nil in the latter case) + */ +- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error; + +/** + Allows to clear a previosuly set simulated geolocation coordinates. + Only works since Xcode 14.3/iOS 16.4 + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the simulated location has been successfully cleared + */ +- (BOOL)fb_clearSimulatedLocation:(NSError **)error; +#endif + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index e7ab2ac19..29532da39 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -21,6 +21,7 @@ #import "FBScreenshot.h" #import "FBXCDeviceEvent.h" #import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" #import "XCUIDevice.h" static const NSTimeInterval FBHomeButtonCoolOffTime = 1.; @@ -85,11 +86,7 @@ - (BOOL)fb_unlockScreen:(NSError **)error [self pressButton:XCUIDeviceButtonHome]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:FBHomeButtonCoolOffTime]]; #if !TARGET_OS_TV - if (SYSTEM_VERSION_LESS_THAN(@"10.0")) { - [[FBApplication fb_activeApplication] swipeRight]; - } else { - [self pressButton:XCUIDeviceButtonHome]; - } + [self pressButton:XCUIDeviceButtonHome]; #else [self pressButton:XCUIDeviceButtonHome]; #endif @@ -236,20 +233,38 @@ - (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error buildError:error]; } + NSError *err; + if ([FBXCTestDaemonsProxy openDefaultApplicationForURL:parsedUrl error:&err]) { + return YES; + } + if (![err.description containsString:@"does not support"]) { + if (error) { + *error = err; + } + return NO; + } + id siriService = [self valueForKey:@"siriService"]; if (nil != siriService) { return [self fb_activateSiriVoiceRecognitionWithText:[NSString stringWithFormat:@"Open {%@}", url] error:error]; } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // The link never gets opened by this method: https://forums.developer.apple.com/thread/25355 - if (![[UIApplication sharedApplication] openURL:parsedUrl]) { -#pragma clang diagnostic pop + + NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. Consider upgrading to Xcode 14.3+/iOS 16.4+", url]; + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"%@", description] + buildError:error];; +} + +- (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error +{ + NSURL *parsedUrl = [NSURL URLWithString:url]; + if (nil == parsedUrl) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The URL %@ cannot be opened", url] + withDescriptionFormat:@"'%@' is not a valid URL", url] buildError:error]; } - return YES; + + return [FBXCTestDaemonsProxy openURL:parsedUrl usingApplication:bundleId error:error]; } - (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError **)error @@ -403,6 +418,16 @@ - (BOOL)fb_setAppearance:(FBUIInterfaceAppearance)appearance error:(NSError **)e [invocation invoke]; return YES; } + +#if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3) + // Xcode 14.3.1 can build these values. + // For iOS 17+ + if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) { + self.appearance = (XCUIDeviceAppearance) appearance; + return YES; + } +#endif + return [[[FBErrorBuilder builder] withDescriptionFormat:@"Current Xcode SDK does not support appearance changing"] buildError:error]; @@ -410,9 +435,34 @@ - (BOOL)fb_setAppearance:(FBUIInterfaceAppearance)appearance error:(NSError **)e - (NSNumber *)fb_getAppearance { +#if __clang_major__ >= 15 || (__clang_major__ >= 14 && __clang_minor__ >= 0 && __clang_patchlevel__ >= 3) + // Xcode 14.3.1 can build these values. + // For iOS 17+ + if ([self respondsToSelector:NSSelectorFromString(@"appearance")]) { + return [NSNumber numberWithLongLong:[self appearance]]; + } +#endif + return [self respondsToSelector:@selector(appearanceMode)] ? [NSNumber numberWithLongLong:[self appearanceMode]] : nil; } +#if !TARGET_OS_TV +- (BOOL)fb_setSimulatedLocation:(CLLocation *)location error:(NSError **)error +{ + return [FBXCTestDaemonsProxy setSimulatedLocation:location error:error]; +} + +- (nullable CLLocation *)fb_getSimulatedLocation:(NSError **)error +{ + return [FBXCTestDaemonsProxy getSimulatedLocation:error]; +} + +- (BOOL)fb_clearSimulatedLocation:(NSError **)error +{ + return [FBXCTestDaemonsProxy clearSimulatedLocation:error]; +} +#endif + @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m index 9d1d2dc0d..faa42ea86 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m @@ -48,7 +48,7 @@ - (NSString *)fb_cacheId uid = self.fb_uid; } else { id snapshot = self.fb_cachedSnapshot ?: self.fb_takeSnapshot; - uid = [FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdUID; + uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; } objc_setAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY, uid, OBJC_ASSOCIATION_RETAIN_NONATOMIC); diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBFind.m b/WebDriverAgentLib/Categories/XCUIElement+FBFind.m index f2f55f2dc..4d46f4c8f 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBFind.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBFind.m @@ -16,6 +16,7 @@ #import "FBXCElementSnapshotWrapper+Helpers.h" #import "FBXCodeCompatibility.h" #import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBUID.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" #import "XCUIElementQuery.h" @@ -48,7 +49,8 @@ @implementation XCUIElement (FBFind) XCUIElementType type = [FBElementTypeTransformer elementTypeWithTypeName:className]; XCUIElementQuery *query = [self.fb_query descendantsMatchingType:type]; NSMutableArray *result = [NSMutableArray array]; - [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]]; + [result addObjectsFromArray:[self.class fb_extractMatchingElementsFromQuery:query + shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]]; id cachedSnapshot = [self fb_cachedSnapshotWithQuery:query]; if (type == XCUIElementTypeAny || cachedSnapshot.elementType == type) { if (shouldReturnAfterFirstMatch || result.count == 0) { @@ -66,40 +68,10 @@ @implementation XCUIElement (FBFind) value:(NSString *)value partialSearch:(BOOL)partialSearch { - NSMutableArray *elements = [NSMutableArray array]; - [self descendantsWithProperty:property value:value partial:partialSearch results:elements]; - return elements; + NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:(partialSearch ? @"%K CONTAINS %@" : @"%K == %@"), property, value]; + return [self fb_descendantsMatchingPredicate:searchPredicate shouldReturnAfterFirstMatch:NO]; } -- (void)descendantsWithProperty:(NSString *)property value:(NSString *)value - partial:(BOOL)partialSearch - results:(NSMutableArray *)results -{ - if (partialSearch) { - NSString *text = [self fb_valueForWDAttributeName:property]; - BOOL isString = [text isKindOfClass:[NSString class]]; - if (isString && [text rangeOfString:value].location != NSNotFound) { - [results addObject:self]; - } - } else { - if ([[self fb_valueForWDAttributeName:property] isEqual:value]) { - [results addObject:self]; - } - } - - property = [FBElementUtils wdAttributeNameForAttributeName:property]; - NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, - NSDictionary * _Nullable bindings) { - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSString *propertyValue = [NSString stringWithFormat:@"%@", [wrappedSnapshot fb_valueForWDAttributeName:property]]; - return partialSearch ? [value containsString:propertyValue] : [value isEqualToString:propertyValue]; - }]; - XCUIElementQuery *query = [[self.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:predicate]; - NSArray *childElements = query.fb_allMatches; - [results addObjectsFromArray:childElements]; -} - - #pragma mark - Search by Predicate String - (NSArray *)fb_descendantsMatchingPredicate:(NSPredicate *)predicate @@ -138,7 +110,7 @@ - (void)descendantsWithProperty:(NSString *)property value:(NSString *)value matchingSnapshots = @[snapshot]; } return [self fb_filterDescendantsWithSnapshots:matchingSnapshots - selfUID:[FBXCElementSnapshotWrapper ensureWrapped:self.lastSnapshot].wdUID + selfUID:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot] onlyChildren:NO]; } @@ -150,8 +122,7 @@ - (void)descendantsWithProperty:(NSString *)property value:(NSString *)value { NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary * _Nullable bindings) { - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - return [wrappedSnapshot.wdName isEqualToString:accessibilityId]; + return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId]; }]; return [self fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m index e7629c6bc..1c7a9ce0e 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m @@ -35,10 +35,10 @@ - (BOOL)fb_forceTouchCoordinate:(NSValue *)relativeCoordinate [self pressWithPressure:[pressure doubleValue] duration:[duration doubleValue]]; } } else { - CGSize size = self.frame.size; - CGVector offset = CGVectorMake(size.width > 0 ? relativeCoordinate.CGPointValue.x / size.width : 0, - size.height > 0 ? relativeCoordinate.CGPointValue.y / size.height : 0); - XCUICoordinate *hitPoint = [self coordinateWithNormalizedOffset:offset]; + CGVector offset = CGVectorMake(relativeCoordinate.CGPointValue.x, + relativeCoordinate.CGPointValue.y); + XCUICoordinate *hitPoint = [[self coordinateWithNormalizedOffset:CGVectorMake(0, 0)] + coordinateWithOffset:offset]; if (nil == pressure || nil == duration) { [hitPoint forcePress]; } else { diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m index 1ec2e9ac9..87841fc38 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m @@ -34,9 +34,9 @@ - (BOOL)fb_isVisible @implementation FBXCElementSnapshotWrapper (FBIsVisible) -- (NSString *)fb_uniqId ++ (NSString *)fb_uniqIdWithSnapshot:(id)snapshot { - return self.fb_uid ?: [NSString stringWithFormat:@"%p", (void *)self]; + return [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot] ?: [NSString stringWithFormat:@"%p", (void *)snapshot]; } - (nullable NSNumber *)fb_cachedVisibilityValue @@ -46,14 +46,14 @@ - (nullable NSNumber *)fb_cachedVisibilityValue return nil; } - NSDictionary *result = [cache objectForKey:@(self.generation)]; + NSDictionary *result = cache[@(self.generation)]; if (nil == result) { // There is no need to keep the cached data for the previous generations [cache removeAllObjects]; - [cache setObject:[NSMutableDictionary dictionary] forKey:@(self.generation)]; + cache[@(self.generation)] = [NSMutableDictionary dictionary]; return nil; } - return [result objectForKey:self.fb_uniqId]; + return result[[self.class fb_uniqIdWithSnapshot:self.snapshot]]; } - (BOOL)fb_cacheVisibilityWithValue:(BOOL)isVisible @@ -63,19 +63,19 @@ - (BOOL)fb_cacheVisibilityWithValue:(BOOL)isVisible if (nil == cache) { return isVisible; } - NSMutableDictionary *destination = [cache objectForKey:@(self.generation)]; + NSMutableDictionary *destination = cache[@(self.generation)]; if (nil == destination) { return isVisible; } NSNumber *visibleObj = [NSNumber numberWithBool:isVisible]; - [destination setObject:visibleObj forKey:self.fb_uniqId]; + destination[[self.class fb_uniqIdWithSnapshot:self.snapshot]] = visibleObj; if (isVisible && nil != ancestors) { // if an element is visible then all its ancestors must be visible as well for (id ancestor in ancestors) { - NSString *ancestorId = [FBXCElementSnapshotWrapper ensureWrapped:ancestor].fb_uniqId; - if (nil == [destination objectForKey:ancestorId]) { - [destination setObject:visibleObj forKey:ancestorId]; + NSString *ancestorId = [self.class fb_uniqIdWithSnapshot:ancestor]; + if (nil == destination[ancestorId]) { + destination[ancestorId] = visibleObj; } } } @@ -186,18 +186,6 @@ - (BOOL)fb_isVisible } CGPoint midPoint = CGPointMake(visibleRect.origin.x + visibleRect.size.width / 2, visibleRect.origin.y + visibleRect.size.height / 2); -#if !TARGET_OS_TV // TV has no orientation, so it does not need to coordinate - id appElement = ancestors.count > 0 ? [ancestors lastObject] : self; - CGRect appFrame = appElement.frame; - CGRect windowFrame = nil == parentWindow ? selfFrame : parentWindow.frame; - if ((appFrame.size.height > appFrame.size.width && windowFrame.size.height < windowFrame.size.width) || - (appFrame.size.height < appFrame.size.width && windowFrame.size.height > windowFrame.size.width)) { - // This is the indication of the fact that transformation is broken and coordinates should be - // recalculated manually. - // However, upside-down case cannot be covered this way, which is not important for Appium - midPoint = FBInvertPointForApplication(midPoint, appFrame.size, FBApplication.fb_activeApplication.interfaceOrientation); - } -#endif id hitElement = [FBActiveAppDetectionPoint axElementWithPoint:midPoint]; if (nil != hitElement) { if (FBIsAXElementEqualToOther(self.accessibilityElement, hitElement)) { diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m index 7740dbc6b..71ebaf652 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m @@ -12,10 +12,9 @@ #import "FBRunLoopSpinner.h" #import "FBXCElementSnapshot.h" #import "FBXCodeCompatibility.h" -#import "XCUIApplication+FBTouchAction.h" #import "XCUICoordinate.h" -#import "XCUICoordinate+FBFix.h" #import "XCUIElement+FBCaching.h" +#import "XCUIElement+FBResolve.h" #if !TARGET_OS_TV @implementation XCUIElement (FBPickerWheel) @@ -30,24 +29,19 @@ - (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)erro NSString *previousValue = snapshot.value; XCUICoordinate *startCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)]; XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, relativeHeightOffset * snapshot.frame.size.height)]; - CGPoint tapPoint = endCoord.fb_screenPoint; - NSArray *> *gesture = - @[@{ - @"action": @"tap", - @"options": @{ - @"x": @(tapPoint.x), - @"y": @(tapPoint.y), - } - } - ]; - if (![self.application fb_performAppiumTouchActions:gesture elementCache:nil error:error]) { - return NO; - } + // If picker value is reflected in its accessiblity id + // then fetching of the next snapshot may fail with StaleElementReferenceError + // because we bound elements by their accessbility ids by default. + // Fetching stable instance of an element allows it to be bounded to the + // unique element identifier (UID), so it could be found next time even if its + // id is different from the initial one. See https://github.com/appium/appium/issues/17569 + XCUIElement *stableInstance = self.fb_stableInstance; + [endCoord tap]; return [[[[FBRunLoopSpinner new] timeout:VALUE_CHANGE_TIMEOUT] timeoutErrorMessage:[NSString stringWithFormat:@"Picker wheel value has not been changed after %@ seconds timeout", @(VALUE_CHANGE_TIMEOUT)]] spinUntilTrue:^BOOL{ - return ![self.fb_takeSnapshot.value isEqualToString:previousValue]; + return ![stableInstance.value isEqualToString:previousValue]; } error:error]; } diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m index a0fb0af9a..f52c72bf6 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m @@ -41,15 +41,14 @@ - (XCUIElement *)fb_stableInstance XCUIElementQuery *query = [self isKindOfClass:XCUIApplication.class] ? self.application.fb_query : [self.application.fb_query descendantsMatchingType:XCUIElementTypeAny]; - FBXCElementSnapshotWrapper *cachedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:self.fb_cachedSnapshot]; - NSString *uid = nil == cachedSnapshot ? self.fb_uid : cachedSnapshot.fb_uid; + NSString *uid = nil == self.fb_cachedSnapshot + ? self.fb_uid + : [FBXCElementSnapshotWrapper wdUIDWithSnapshot:(id)self.fb_cachedSnapshot]; if (nil == uid) { return self; } - NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary *bindings) { - return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_uid isEqualToString:uid]; - }]; - return (XCUIElement *)[query matchingPredicate:predicate].fb_firstMatch; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@",FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid]; + return [query matchingPredicate:predicate].allElementsBoundByIndex.firstObject ?: self; } @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m index f9582b52a..52be1a557 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m @@ -16,11 +16,9 @@ #import "FBXCodeCompatibility.h" #import "FBXCElementSnapshotWrapper.h" #import "FBXCElementSnapshotWrapper+Helpers.h" -#import "XCUIApplication+FBTouchAction.h" #import "XCUIElement+FBCaching.h" #import "XCUIApplication.h" #import "XCUICoordinate.h" -#import "XCUICoordinate+FBFix.h" #import "XCUIElement+FBIsVisible.h" #import "XCUIElement.h" #import "XCUIElement+FBUtilities.h" @@ -28,7 +26,8 @@ const CGFloat FBFuzzyPointThreshold = 20.f; //Smallest determined value that is not interpreted as touch const CGFloat FBScrollToVisibleNormalizedDistance = .5f; -const CGFloat FBTouchEventDelay = 1.f; +const CGFloat FBTouchEventDelay = 0.5f; +const CGFloat FBTouchVelocity = 300; // pixels per sec const CGFloat FBScrollTouchProportion = 0.75f; #if !TARGET_OS_TV @@ -321,45 +320,16 @@ - (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vecto XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:application normalizedOffset:CGVectorMake(0.0, 0.0)]; XCUICoordinate *startCoordinate = [[XCUICoordinate alloc] initWithCoordinate:appCoordinate pointsOffset:hitpointOffset]; - CGPoint startPoint = startCoordinate.fb_screenPoint; XCUICoordinate *endCoordinate = [[XCUICoordinate alloc] initWithCoordinate:startCoordinate pointsOffset:vector]; - CGPoint endPoint = endCoordinate.fb_screenPoint; - if (FBPointFuzzyEqualToPoint(startPoint, endPoint, FBFuzzyPointThreshold)) { + if (FBPointFuzzyEqualToPoint(startCoordinate.screenPoint, endCoordinate.screenPoint, FBFuzzyPointThreshold)) { return YES; } - NSArray *> *gesture = - @[@{ - @"action": @"press", - @"options": @{ - @"x": @(startPoint.x), - @"y": @(startPoint.y), - } - }, - @{ - @"action": @"wait", - @"options": @{ - @"ms": @(FBTouchEventDelay * 1000), - } - }, - @{ - @"action": @"moveTo", - @"options": @{ - @"x": @(endPoint.x), - @"y": @(endPoint.y), - } - }, - @{ - @"action": @"release" - } - ]; - if (![application fb_performAppiumTouchActions:gesture - elementCache:nil - error:error]) { - return NO; - } - + [startCoordinate pressForDuration:FBTouchEventDelay + thenDragToCoordinate:endCoordinate + withVelocity:FBTouchVelocity + thenHoldForDuration:FBTouchEventDelay]; return YES; } diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTap.h b/WebDriverAgentLib/Categories/XCUIElement+FBTap.h deleted file mode 100644 index ed92bb6d7..000000000 --- a/WebDriverAgentLib/Categories/XCUIElement+FBTap.h +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface XCUIElement (FBTap) - -/** - Waits for element to become stable (not move) and performs sync tap on element - - @param error If there is an error, upon return contains an NSError object that describes the problem. - @return YES if the operation succeeds, otherwise NO. -*/ -- (BOOL)fb_tapWithError:(NSError **)error; - -/** - Waits for element to become stable (not move) and performs sync tap on element - - @param relativeCoordinate hit point coordinate relative to the current element position - @param error If there is an error, upon return contains an NSError object that describes the problem. - @return YES if the operation succeeds, otherwise NO. - */ -- (BOOL)fb_tapCoordinate:(CGPoint)relativeCoordinate error:(NSError **)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTap.m b/WebDriverAgentLib/Categories/XCUIElement+FBTap.m deleted file mode 100644 index 311e13d8e..000000000 --- a/WebDriverAgentLib/Categories/XCUIElement+FBTap.m +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "XCUIElement+FBTap.h" - -#import "FBMacros.h" -#import "XCUIApplication+FBTouchAction.h" -#import "XCUIElement+FBUtilities.h" - - -#if !TARGET_OS_TV -@implementation XCUIElement (FBTap) - -- (BOOL)fb_tapWithError:(NSError **)error -{ - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"13.0")) { - [self tap]; - return YES; - } - - NSArray *> *tapGesture = - @[ - @{@"action": @"tap", - @"options": @{@"element": self} - } - ]; - return [self.application fb_performAppiumTouchActions:tapGesture elementCache:nil error:error]; -} - -- (BOOL)fb_tapCoordinate:(CGPoint)relativeCoordinate error:(NSError **)error -{ - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"13.0")) { - // Coordinates calculation issues have been fixed - // for different device orientations since Xcode 11 - XCUICoordinate *startCoordinate = [self coordinateWithNormalizedOffset:CGVectorMake(0, 0)]; - CGVector offset = CGVectorMake(relativeCoordinate.x, relativeCoordinate.y); - XCUICoordinate *dstCoordinate = [startCoordinate coordinateWithOffset:offset]; - [dstCoordinate tap]; - return YES; - } - - NSArray *> *tapGesture = - @[ - @{@"action": @"tap", - @"options": @{@"element": self, - @"x": @(relativeCoordinate.x), - @"y": @(relativeCoordinate.y) - } - } - ]; - return [self.application fb_performAppiumTouchActions:tapGesture elementCache:nil error:error]; -} - -@end -#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m index aabc6e8bf..c6e31f4fc 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m @@ -15,12 +15,12 @@ #import "NSString+FBVisualLength.h" #import "FBXCElementSnapshotWrapper.h" #import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIDevice+FBHelpers.h" #import "XCUIElement+FBCaching.h" -#import "XCUIElement+FBTap.h" #import "XCUIElement+FBUtilities.h" #import "FBXCodeCompatibility.h" -#define MAX_CLEAR_RETRIES 2 +#define MAX_CLEAR_RETRIES 3 @interface NSString (FBRepeat) @@ -72,7 +72,7 @@ - (void)fb_prepareForTextInputWithSnapshot:(FBXCElementSnapshotWrapper *)snapsho // There is no possibility to open the keyboard by tapping a field in TvOS #if !TARGET_OS_TV [FBLogger logFmt:@"Trying to tap the \"%@\" element to have it focused", snapshot.fb_description]; - [self fb_tapWithError:nil]; + [self tap]; // It might take some time to update the UI [self fb_takeSnapshot]; #endif @@ -137,22 +137,32 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot backspaceDeleteSequence = [[NSString alloc] initWithData:(NSData *)[@"\\u0008\\u007F" dataUsingEncoding:NSASCIIStringEncoding] encoding:NSNonLossyASCIIStringEncoding]; }); - + + NSUInteger preClearTextLength = [currentValue fb_visualLength]; + NSString *backspacesToType = [backspaceDeleteSequence fb_repeatTimes:preClearTextLength]; + +#if TARGET_OS_IOS NSUInteger retry = 0; NSString *placeholderValue = snapshot.placeholderValue; - NSUInteger preClearTextLength = [currentValue fb_visualLength]; do { - if (retry >= MAX_CLEAR_RETRIES - 1) { - // Last chance retry. Tripple-tap the field to select its content - [self tapWithNumberOfTaps:3 numberOfTouches:1]; - return [FBKeyboard typeText:backspaceDeleteSequence error:error]; - } - - NSString *textToType = [backspaceDeleteSequence fb_repeatTimes:preClearTextLength]; + // the ios needs to have keyboard focus to clear text if (shouldPrepareForInput && 0 == retry) { [self fb_prepareForTextInputWithSnapshot:snapshot]; } - if (![FBKeyboard typeText:textToType error:error]) { + + if (retry == 0) { + // 1st attempt is via the IOHIDEvent as the fastest operation + // https://github.com/appium/appium/issues/19389 + [[XCUIDevice sharedDevice] fb_performIOHIDEventWithPage:0x07 // kHIDPage_KeyboardOrKeypad + usage:0x9c // kHIDUsage_KeyboardClear + duration:0.01 + error:nil]; + } else if (retry >= MAX_CLEAR_RETRIES - 1) { + // Last chance retry. Tripple-tap the field to select its content + [self tapWithNumberOfTaps:3 numberOfTouches:1]; + return [FBKeyboard typeText:backspaceDeleteSequence error:error]; + } else if (![FBKeyboard typeText:backspacesToType error:error]) { + // 2nd operation return NO; } @@ -166,6 +176,13 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot retry++; } while (preClearTextLength > 0); return YES; +#else + // tvOS does not need a focus. + // kHIDPage_KeyboardOrKeypad did not work for tvOS's search field. (tvOS 17 at least) + // Tested XCUIElementTypeSearchField and XCUIElementTypeTextView whch were + // common search field and email/passowrd input in tvOS apps. + return [FBKeyboard typeText:backspacesToType error:error]; +#endif } @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUID.h b/WebDriverAgentLib/Categories/XCUIElement+FBUID.h index 3ca0bae2b..8b1a2da64 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUID.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUID.h @@ -30,6 +30,14 @@ NS_ASSUME_NONNULL_BEGIN /*! Represents unique internal element identifier, which is the same for an element and its snapshot */ @property (nonatomic, readonly) unsigned long long fb_accessibiltyId; +/** + Fetches wdUID attribute value for the given snapshot instance + + @param snapshot snapshot instance + @return UID attribute value + */ ++ (nullable NSString *)wdUIDWithSnapshot:(id)snapshot; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUID.m b/WebDriverAgentLib/Categories/XCUIElement+FBUID.m index cb8ef1396..765b410c1 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUID.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUID.m @@ -7,9 +7,12 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "XCUIElement+FBUID.h" #import "FBElementUtils.h" +#import "FBLogger.h" #import "XCUIApplication.h" #import "XCUIElement+FBUtilities.h" @@ -33,13 +36,49 @@ - (NSString *)fb_uid @implementation FBXCElementSnapshotWrapper (FBUID) +static void swizzled_validatePredicateWithExpressionsAllowed(id self, SEL _cmd, id predicate, BOOL withExpressionsAllowed) +{ +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" ++ (void)load +{ + Class XCElementSnapshotCls = objc_lookUpClass("XCElementSnapshot"); + NSAssert(XCElementSnapshotCls != nil, @"Could not locate XCElementSnapshot class"); + Method uidMethod = class_getInstanceMethod(self.class, @selector(fb_uid)); + class_addMethod(XCElementSnapshotCls, @selector(fb_uid), method_getImplementation(uidMethod), method_getTypeEncoding(uidMethod)); + + // Support for Xcode 14.3 requires disabling the new predicate validator, see https://github.com/appium/appium/issues/18444 + Class XCTElementQueryTransformerPredicateValidatorCls = objc_lookUpClass("XCTElementQueryTransformerPredicateValidator"); + if (XCTElementQueryTransformerPredicateValidatorCls == nil) { + return; + } + Method validatePredicateMethod = class_getClassMethod(XCTElementQueryTransformerPredicateValidatorCls, NSSelectorFromString(@"validatePredicate:withExpressionsAllowed:")); + if (validatePredicateMethod == nil) { + [FBLogger log:@"Could not find method +[XCTElementQueryTransformerPredicateValidator validatePredicate:withExpressionsAllowed:]"]; + return; + } + IMP swizzledImp = (IMP)swizzled_validatePredicateWithExpressionsAllowed; + method_setImplementation(validatePredicateMethod, swizzledImp); +} +#pragma diagnostic pop + - (unsigned long long)fb_accessibiltyId { return [FBElementUtils idWithAccessibilityElement:self.accessibilityElement]; } ++ (nullable NSString *)wdUIDWithSnapshot:(id)snapshot +{ + return [FBElementUtils uidWithAccessibilityElement:[snapshot accessibilityElement]]; +} + - (NSString *)fb_uid { + if ([self isKindOfClass:FBXCElementSnapshotWrapper.class]) { + return [self.class wdUIDWithSnapshot:self.snapshot]; + } return [FBElementUtils uidWithAccessibilityElement:[self accessibilityElement]]; } diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h index de79e563c..66b9ee2c9 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h @@ -98,13 +98,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout; -/** - Returns screenshot of the particular element - @param error If there is an error, upon return contains an NSError object that describes the problem. - @return Element screenshot as PNG-encoded data or nil in case of failure - */ -- (nullable NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error; - @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m index 17837b019..b5a072c06 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m @@ -132,22 +132,27 @@ @implementation XCUIElement (FBUtilities) if (0 == snapshots.count) { return @[]; } - NSMutableArray *sortedIds = [NSMutableArray new]; + NSMutableArray *matchedIds = [NSMutableArray new]; for (id snapshot in snapshots) { - [sortedIds addObject:(NSString *)[FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_uid]; + NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; + if (nil != uid) { + [matchedIds addObject:uid]; + } } NSMutableArray *matchedElements = [NSMutableArray array]; NSString *uid = selfUID; if (nil == uid) { uid = self.fb_isResolvedFromCache.boolValue - ? [FBXCElementSnapshotWrapper ensureWrapped:self.lastSnapshot].fb_uid + ? [FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot] : self.fb_uid; } - if ([sortedIds containsObject:uid]) { + if (nil != uid && [matchedIds containsObject:uid]) { + XCUIElement *stableSelf = self.fb_stableInstance; + stableSelf.fb_isResolvedNatively = @NO; if (1 == snapshots.count) { - return @[self]; + return @[stableSelf]; } - [matchedElements addObject:self]; + [matchedElements addObject:stableSelf]; } XCUIElementType type = XCUIElementTypeAny; NSArray *uniqueTypes = [snapshots valueForKeyPath:[NSString stringWithFormat:@"@distinctUnionOfObjects.%@", FBStringify(XCUIElement, elementType)]]; @@ -157,35 +162,14 @@ @implementation XCUIElement (FBUtilities) XCUIElementQuery *query = onlyChildren ? [self.fb_query childrenMatchingType:type] : [self.fb_query descendantsMatchingType:type]; - NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary *bindings) { - return [sortedIds containsObject:(NSString *)[FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_uid]; - }]; - query = [query matchingPredicate:predicate]; - if (1 == snapshots.count) { - XCUIElement *result = query.fb_firstMatch; - result.fb_isResolvedNatively = @NO; - return result ? @[result] : @[]; - } - // Rely here on the fact, that XPath always returns query results in the same - // order they appear in the document, which means we don't need to resort the resulting - // array. Although, if it turns out this is still not the case then we could always - // uncomment the sorting procedure below: - // query = [query sorted:(id)^NSComparisonResult(XCElementSnapshot *a, XCElementSnapshot *b) { - // NSUInteger first = [sortedIds indexOfObject:a.wdUID]; - // NSUInteger second = [sortedIds indexOfObject:b.wdUID]; - // if (first < second) { - // return NSOrderedAscending; - // } - // if (first > second) { - // return NSOrderedDescending; - // } - // return NSOrderedSame; - // }]; - NSArray *result = query.fb_allMatches; - for (XCUIElement *el in result) { + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K IN %@",FBStringify(FBXCElementSnapshotWrapper, fb_uid), matchedIds]; + [matchedElements addObjectsFromArray:[query matchingPredicate:predicate].allElementsBoundByIndex]; + + for (XCUIElement *el in matchedElements) { el.fb_isResolvedNatively = @NO; } - return result; + return matchedElements.copy; } - (void)fb_waitUntilStable @@ -213,53 +197,4 @@ - (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout FBConfiguration.waitForIdleTimeout = previousTimeout; } -- (NSData *)fb_screenshotWithError:(NSError **)error -{ - id selfSnapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; - if (CGRectIsEmpty(selfSnapshot.frame)) { - if (error) { - *error = [[FBErrorBuilder.builder withDescription:@"Cannot get a screenshot of zero-sized element"] build]; - } - return nil; - } - - CGRect elementRect = selfSnapshot.frame; -#if !TARGET_OS_TV - UIInterfaceOrientation orientation = self.application.interfaceOrientation; - if (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) { - // Workaround XCTest bug when element frame is returned as in portrait mode even if the screenshot is rotated - NSArray> *ancestors = [FBXCElementSnapshotWrapper ensureWrapped:selfSnapshot].fb_ancestors; - id parentWindow = nil; - if (1 == ancestors.count) { - parentWindow = selfSnapshot; - } else if (ancestors.count > 1) { - parentWindow = [ancestors objectAtIndex:ancestors.count - 2]; - } - if (nil != parentWindow) { - CGRect appFrame = ancestors.lastObject.frame; - CGRect parentWindowFrame = parentWindow.frame; - if (CGRectEqualToRect(appFrame, parentWindowFrame) - || (appFrame.size.width > appFrame.size.height && parentWindowFrame.size.width > parentWindowFrame.size.height) - || (appFrame.size.width < appFrame.size.height && parentWindowFrame.size.width < parentWindowFrame.size.height)) { - CGPoint fixedOrigin = orientation == UIInterfaceOrientationLandscapeLeft ? - CGPointMake(appFrame.size.height - elementRect.origin.y - elementRect.size.height, elementRect.origin.x) : - CGPointMake(elementRect.origin.y, appFrame.size.width - elementRect.origin.x - elementRect.size.width); - elementRect = CGRectMake(fixedOrigin.x, fixedOrigin.y, elementRect.size.height, elementRect.size.width); - } - } - } -#endif - - // adjust element rect for the actual screen scale - XCUIScreen *mainScreen = XCUIScreen.mainScreen; - elementRect = CGRectMake(elementRect.origin.x * mainScreen.scale, elementRect.origin.y * mainScreen.scale, - elementRect.size.width * mainScreen.scale, elementRect.size.height * mainScreen.scale); - - return [FBScreenshot takeInOriginalResolutionWithQuality:FBConfiguration.screenshotQuality - rect:elementRect - error:error]; -} - @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h index 903eeb432..fdeedaf91 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h @@ -20,6 +20,14 @@ NS_ASSUME_NONNULL_BEGIN @interface FBXCElementSnapshotWrapper (WebDriverAttributes) +/** + Fetches wdName attribute value for the given snapshot instance + + @param snapshot snapshot instance + @return wdName attribute value or nil + */ ++ (nullable NSString *)wdNameWithSnapshot:(id)snapshot; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m index 024563dcc..b29c5a8b2 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m @@ -9,8 +9,6 @@ #import "XCUIElement+FBWebDriverAttributes.h" -#import - #import "FBElementTypeTransformer.h" #import "FBLogger.h" #import "FBMacros.h" @@ -22,6 +20,7 @@ #import "XCUIElement+FBUtilities.h" #import "FBElementUtils.h" #import "XCTestPrivateSymbols.h" +#import "XCUIHitPointResult.h" #define BROKEN_RECT CGRectMake(-1, -1, 0, 0) @@ -58,11 +57,15 @@ - (id)fb_valueForWDAttributeName:(NSString *)name - (id)forwardingTargetForSelector:(SEL)aSelector { - struct objc_method_description descr = protocol_getMethodDescription(@protocol(FBElement), aSelector, YES, YES); - SEL webDriverAttributesSelector = descr.name; - return nil == webDriverAttributesSelector - ? nil - : [FBXCElementSnapshotWrapper ensureWrapped:[self fb_snapshotForAttributeName:NSStringFromSelector(webDriverAttributesSelector)]]; + static dispatch_once_t onceToken; + static NSSet *fbElementAttributeNames; + dispatch_once(&onceToken, ^{ + fbElementAttributeNames = [FBElementUtils selectorNamesWithProtocol:@protocol(FBElement)]; + }); + NSString* attributeName = NSStringFromSelector(aSelector); + return [fbElementAttributeNames containsObject:attributeName] + ? [FBXCElementSnapshotWrapper ensureWrapped:[self fb_snapshotForAttributeName:attributeName]] + : nil; } @end @@ -98,18 +101,23 @@ - (NSString *)wdValue value = [NSString stringWithFormat:@"%@", value]; } return value; - } +} -- (NSString *)wdName ++ (NSString *)wdNameWithSnapshot:(id)snapshot { - NSString *identifier = self.identifier; + NSString *identifier = snapshot.identifier; if (nil != identifier && identifier.length != 0) { return identifier; } - NSString *label = self.label; + NSString *label = snapshot.label; return FBTransferEmptyStringToNil(label); } +- (NSString *)wdName +{ + return [self.class wdNameWithSnapshot:self.snapshot]; +} + - (NSString *)wdLabel { NSString *label = self.label; @@ -147,12 +155,10 @@ - (BOOL)isWDVisible return self.fb_isVisible; } -#if TARGET_OS_TV - (BOOL)isWDFocused { return self.hasFocus; } -#endif - (BOOL)isWDAccessible { @@ -221,6 +227,12 @@ - (NSUInteger)wdIndex return 0; } +- (BOOL)isWDHittable +{ + XCUIHitPointResult *result = [self hitPoint:nil]; + return nil == result ? NO : result.hittable; +} + - (NSDictionary *)wdRect { CGRect frame = self.wdFrame; diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m index 0f0ad3ebe..f6db5bc42 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -55,10 +55,13 @@ + (NSArray *)routes [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession respondWithTarget:self action:@selector(handleActiveAppInfo:)], #if !TARGET_OS_TV // tvOS does not provide relevant APIs [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)], + [[FBRoute POST:@"/wda/setPasteboard"].withoutSession respondWithTarget:self action:@selector(handleSetPasteboard:)], [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)], + [[FBRoute POST:@"/wda/getPasteboard"].withoutSession respondWithTarget:self action:@selector(handleGetPasteboard:)], [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)], #endif [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)], + [[FBRoute POST:@"/wda/performAccessibilityAudit"] respondWithTarget:self action:@selector(handlePerformAccessibilityAudit:)], [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)], [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)], [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)], @@ -73,6 +76,17 @@ + (NSArray *)routes [[FBRoute POST:@"/wda/device/appearance"].withoutSession respondWithTarget:self action:@selector(handleSetDeviceAppearance:)], [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)], [[FBRoute GET:@"/wda/device/location"].withoutSession respondWithTarget:self action:@selector(handleGetLocation:)], +#if !TARGET_OS_TV // tvOS does not provide relevant APIs +#if __clang_major__ >= 15 + [[FBRoute POST:@"/wda/element/:uuid/keyboardInput"] respondWithTarget:self action:@selector(handleKeyboardInput:)], +#endif + [[FBRoute GET:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleGetSimulatedLocation:)], + [[FBRoute GET:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleGetSimulatedLocation:)], + [[FBRoute POST:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleSetSimulatedLocation:)], + [[FBRoute POST:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleSetSimulatedLocation:)], + [[FBRoute DELETE:@"/wda/simulatedLocation"] respondWithTarget:self action:@selector(handleClearSimulatedLocation:)], + [[FBRoute DELETE:@"/wda/simulatedLocation"].withoutSession respondWithTarget:self action:@selector(handleClearSimulatedLocation:)], +#endif [[FBRoute OPTIONS:@"/*"].withoutSession respondWithTarget:self action:@selector(handlePingCommand:)], ]; } @@ -113,9 +127,9 @@ + (NSArray *)routes BOOL isDismissed = [request.session.activeApplication fb_dismissKeyboardWithKeyNames:request.arguments[@"keyNames"] error:&error]; return isDismissed - ? FBResponseWithOK() - : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description - traceback:nil]); + ? FBResponseWithOK() + : FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description + traceback:nil]); } + (id)handlePingCommand:(FBRouteRequest *)request @@ -130,12 +144,12 @@ + (NSArray *)routes FBSession *session = request.session; CGSize statusBarSize = [FBScreen statusBarSizeForApplication:session.activeApplication]; return FBResponseWithObject( - @{ + @{ @"statusBarSize": @{@"width": @(statusBarSize.width), @"height": @(statusBarSize.height), - }, + }, @"scale": @([FBScreen scale]), - }); + }); } + (id)handleLock:(FBRouteRequest *)request @@ -233,7 +247,7 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app if (nil == result) { return FBResponseWithUnknownError(error); } - return FBResponseWithObject([result base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]); + return FBResponseWithObject([result base64EncodedStringWithOptions:0]); } + (id)handleGetBatteryInfo:(FBRouteRequest *)request @@ -332,7 +346,7 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app CLAuthorizationStatus authStatus; if ([locationManager respondsToSelector:@selector(authorizationStatus)]) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[locationManager class] - instanceMethodSignatureForSelector:@selector(authorizationStatus)]]; + instanceMethodSignatureForSelector:@selector(authorizationStatus)]]; [invocation setSelector:@selector(authorizationStatus)]; [invocation setTarget:locationManager]; [invocation invoke]; @@ -386,8 +400,8 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app } FBUIInterfaceAppearance appearance = [name isEqualToString:@"light"] - ? FBUIInterfaceAppearanceLight - : FBUIInterfaceAppearanceDark; + ? FBUIInterfaceAppearanceLight + : FBUIInterfaceAppearanceDark; NSError *error; if (![XCUIDevice.sharedDevice fb_setAppearance:appearance error:&error]) { return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description @@ -404,7 +418,7 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app NSString *currentLocale = [[NSLocale autoupdatingCurrentLocale] localeIdentifier]; NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionaryWithDictionary: - @{ + @{ @"currentLocale": currentLocale, @"timeZone": self.timeZone, @"name": UIDevice.currentDevice.name, @@ -420,10 +434,8 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app #endif }]; - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) { - // https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate - deviceInfo[@"thermalState"] = @(NSProcessInfo.processInfo.thermalState); - } + // https://developer.apple.com/documentation/foundation/nsprocessinfothermalstate + deviceInfo[@"thermalState"] = @(NSProcessInfo.processInfo.thermalState); return FBResponseWithObject(deviceInfo); } @@ -642,4 +654,119 @@ + (NSString *)timeZone return FBResponseWithObject(@{@"response": ret ?: @""}); } } + +#if !TARGET_OS_TV // tvOS does not provide relevant APIs ++ (id)handleGetSimulatedLocation:(FBRouteRequest *)request +{ + NSError *error; + CLLocation *location = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; + if (nil != error) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithObject(@{ + @"latitude": location ? @(location.coordinate.latitude) : NSNull.null, + @"longitude": location ? @(location.coordinate.longitude) : NSNull.null, + @"altitude": location ? @(location.altitude) : NSNull.null, + }); +} + ++ (id)handleSetSimulatedLocation:(FBRouteRequest *)request +{ + NSNumber *longitude = request.arguments[@"longitude"]; + NSNumber *latitude = request.arguments[@"latitude"]; + + if (nil == longitude || nil == latitude) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both latitude and longitude must be provided" + traceback:nil]); + } + NSError *error; + CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude.doubleValue + longitude:longitude.doubleValue]; + if (![XCUIDevice.sharedDevice fb_setSimulatedLocation:location error:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + ++ (id)handleClearSimulatedLocation:(FBRouteRequest *)request +{ + NSError *error; + if (![XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithOK(); +} + +#if __clang_major__ >= 15 ++ (id)handleKeyboardInput:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + BOOL hasElement = ![request.parameters[@"uuid"] isEqual:@"0"]; + XCUIElement *destination = hasElement + ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]] + : request.session.activeApplication; + id keys = request.arguments[@"keys"]; + + if (![destination respondsToSelector:@selector(typeKey:modifierFlags:)]) { + NSString *message = @"typeKey API is only supported since Xcode15 and iPadOS 17"; + return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:message + traceback:nil]); + } + + if (![keys isKindOfClass:NSArray.class]) { + NSString *message = @"The 'keys' argument must be an array"; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + for (id item in (NSArray *)keys) { + if ([item isKindOfClass:NSString.class]) { + NSString *keyValue = [FBKeyboard keyValueForName:item] ?: item; + [destination typeKey:keyValue modifierFlags:XCUIKeyModifierNone]; + } else if ([item isKindOfClass:NSDictionary.class]) { + id key = [(NSDictionary *)item objectForKey:@"key"]; + if (![key isKindOfClass:NSString.class]) { + NSString *message = [NSString stringWithFormat:@"All dictionaries of 'keys' array must have the 'key' item of type string. Got '%@' instead in the item %@", key, item]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + id modifiers = [(NSDictionary *)item objectForKey:@"modifierFlags"]; + NSUInteger modifierFlags = XCUIKeyModifierNone; + if ([modifiers isKindOfClass:NSNumber.class]) { + modifierFlags = [(NSNumber *)modifiers unsignedIntValue]; + } + NSString *keyValue = [FBKeyboard keyValueForName:item] ?: key; + [destination typeKey:keyValue modifierFlags:modifierFlags]; + } else { + NSString *message = @"All items of the 'keys' array must be either dictionaries or strings"; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + } + return FBResponseWithOK(); +} +#endif +#endif + ++ (id)handlePerformAccessibilityAudit:(FBRouteRequest *)request +{ + NSError *error; + NSArray *requestedTypes = request.arguments[@"auditTypes"]; + NSMutableSet *typesSet = [NSMutableSet set]; + if (nil == requestedTypes || 0 == [requestedTypes count]) { + [typesSet addObject:@"XCUIAccessibilityAuditTypeAll"]; + } else { + [typesSet addObjectsFromArray:requestedTypes]; + } + NSArray *result = [request.session.activeApplication fb_performAccessibilityAuditWithAuditTypesSet:typesSet.copy + error:&error]; + if (nil == result) { + return FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:error.description + traceback:nil]); + } + return FBResponseWithObject(result); +} + @end diff --git a/WebDriverAgentLib/Commands/FBElementCommands.m b/WebDriverAgentLib/Commands/FBElementCommands.m index 59ce7f7f6..0f163521b 100644 --- a/WebDriverAgentLib/Commands/FBElementCommands.m +++ b/WebDriverAgentLib/Commands/FBElementCommands.m @@ -30,7 +30,6 @@ #import "XCUIElement+FBIsVisible.h" #import "XCUIElement+FBPickerWheel.h" #import "XCUIElement+FBScrolling.h" -#import "XCUIElement+FBTap.h" #import "XCUIElement+FBForceTouch.h" #import "XCUIElement+FBSwiping.h" #import "XCUIElement+FBTyping.h" @@ -55,6 +54,7 @@ + (NSArray *)routes return @[ [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)], + [[FBRoute GET:@"/window/size"].withoutSession respondWithTarget:self action:@selector(handleGetWindowSize:)], [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)], [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)], [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)], @@ -85,8 +85,10 @@ + (NSArray *)routes [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)], [[FBRoute POST:@"/wda/element/:uuid/scrollTo"] respondWithTarget:self action:@selector(handleScrollTo:)], [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)], + [[FBRoute POST:@"/wda/element/:uuid/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragWithVelocity:)], [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)], [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)], + [[FBRoute POST:@"/wda/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragCoordinateWithVelocity:)], [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)], [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)], [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)], @@ -230,14 +232,14 @@ + (NSArray *)routes { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - NSError *error = nil; #if TARGET_OS_IOS - if (![element fb_tapWithError:&error]) { + [element tap]; #elif TARGET_OS_TV + NSError *error = nil; if (![element fb_selectWithError:&error]) { -#endif return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); } +#endif return FBResponseWithOK(); } @@ -293,9 +295,10 @@ + (NSArray *)routes + (id)handleDoubleTapCoordinate:(FBRouteRequest *)request { - CGPoint doubleTapPoint = CGPointMake((CGFloat)[request.arguments[@"x"] doubleValue], (CGFloat)[request.arguments[@"y"] doubleValue]); - XCUICoordinate *doubleTapCoordinate = [self.class gestureCoordinateWithCoordinate:doubleTapPoint - application:request.session.activeApplication]; + CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue], + [request.arguments[@"y"] doubleValue]); + XCUICoordinate *doubleTapCoordinate = [self.class gestureCoordinateWithOffset:offset + element:request.session.activeApplication]; [doubleTapCoordinate doubleTap]; return FBResponseWithOK(); } @@ -331,13 +334,51 @@ + (NSArray *)routes + (id)handleTouchAndHoldCoordinate:(FBRouteRequest *)request { - CGPoint touchPoint = CGPointMake((CGFloat)[request.arguments[@"x"] doubleValue], (CGFloat)[request.arguments[@"y"] doubleValue]); - XCUICoordinate *pressCoordinate = [self.class gestureCoordinateWithCoordinate:touchPoint - application:request.session.activeApplication]; + CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue], + [request.arguments[@"y"] doubleValue]); + XCUICoordinate *pressCoordinate = [self.class gestureCoordinateWithOffset:offset + element:request.session.activeApplication]; [pressCoordinate pressForDuration:[request.arguments[@"duration"] doubleValue]]; return FBResponseWithOK(); } ++ (id)handlePressAndDragWithVelocity:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + if (![element respondsToSelector:@selector(pressForDuration:thenDragToElement:withVelocity:thenHoldForDuration:)]) { + return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above" + traceback:nil]); + } + [element pressForDuration:[request.arguments[@"pressDuration"] doubleValue] + thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"]] + withVelocity:[request.arguments[@"velocity"] doubleValue] + thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]]; + return FBResponseWithOK(); +} + ++ (id)handlePressAndDragCoordinateWithVelocity:(FBRouteRequest *)request +{ + FBSession *session = request.session; + CGVector startOffset = CGVectorMake((CGFloat)[request.arguments[@"fromX"] doubleValue], + (CGFloat)[request.arguments[@"fromY"] doubleValue]); + XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset + element:session.activeApplication]; + if (![startCoordinate respondsToSelector:@selector(pressForDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:)]) { + return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above" + traceback:nil]); + } + CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue], + (CGFloat)[request.arguments[@"toY"] doubleValue]); + XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset + element:session.activeApplication]; + [startCoordinate pressForDuration:[request.arguments[@"pressDuration"] doubleValue] + thenDragToCoordinate:endCoordinate + withVelocity:[request.arguments[@"velocity"] doubleValue] + thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]]; + return FBResponseWithOK(); +} + + (id)handleScroll:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; @@ -346,9 +387,11 @@ + (NSArray *)routes // what ios-driver did and sadly, we must copy them. NSString *const name = request.arguments[@"name"]; if (name) { - XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingIdentifier:name] allElementsBoundByAccessibilityElement] lastObject]; + XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny] + matchingIdentifier:name] allElementsBoundByIndex] lastObject]; if (!childElement) { - return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' identifier didn't match any elements", name] traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' identifier didn't match any elements", name] + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); } return [self.class handleScrollElementToVisible:childElement withRequest:request]; } @@ -373,9 +416,11 @@ + (NSArray *)routes if (predicateString) { NSPredicate *formattedPredicate = [NSPredicate fb_snapshotBlockPredicateWithPredicate:[NSPredicate predicateWithFormat:predicateString]]; - XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny] matchingPredicate:formattedPredicate] allElementsBoundByAccessibilityElement] lastObject]; + XCUIElement *childElement = [[[[element.fb_query descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:formattedPredicate] allElementsBoundByIndex] lastObject]; if (!childElement) { - return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' predicate didn't match any elements", predicateString] traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + return FBResponseWithStatus([FBCommandStatus noSuchElementErrorWithMessage:[NSString stringWithFormat:@"'%@' predicate didn't match any elements", predicateString] + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); } return [self.class handleScrollElementToVisible:childElement withRequest:request]; } @@ -400,13 +445,15 @@ + (NSArray *)routes + (id)handleDragCoordinate:(FBRouteRequest *)request { FBSession *session = request.session; - CGPoint startPoint = CGPointMake((CGFloat)[request.arguments[@"fromX"] doubleValue], (CGFloat)[request.arguments[@"fromY"] doubleValue]); - CGPoint endPoint = CGPointMake((CGFloat)[request.arguments[@"toX"] doubleValue], (CGFloat)[request.arguments[@"toY"] doubleValue]); + CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue], + [request.arguments[@"fromY"] doubleValue]); + XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset + element:session.activeApplication]; + CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue], + [request.arguments[@"toY"] doubleValue]); + XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset + element:session.activeApplication]; NSTimeInterval duration = [request.arguments[@"duration"] doubleValue]; - XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithCoordinate:endPoint - application:session.activeApplication]; - XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithCoordinate:startPoint - application:session.activeApplication]; [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate]; return FBResponseWithOK(); } @@ -416,14 +463,13 @@ + (NSArray *)routes FBSession *session = request.session; FBElementCache *elementCache = session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - CGRect frame = [(id)element.lastSnapshot frame]; - CGPoint startPoint = CGPointMake((CGFloat)(frame.origin.x + [request.arguments[@"fromX"] doubleValue]), (CGFloat)(frame.origin.y + [request.arguments[@"fromY"] doubleValue])); - CGPoint endPoint = CGPointMake((CGFloat)(frame.origin.x + [request.arguments[@"toX"] doubleValue]), (CGFloat)(frame.origin.y + [request.arguments[@"toY"] doubleValue])); + CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue], + [request.arguments[@"fromY"] doubleValue]); + XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:element]; + CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue], + [request.arguments[@"toY"] doubleValue]); + XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:element]; NSTimeInterval duration = [request.arguments[@"duration"] doubleValue]; - XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithCoordinate:endPoint - application:session.activeApplication]; - XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithCoordinate:startPoint - application:session.activeApplication]; [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate]; return FBResponseWithOK(); } @@ -449,19 +495,13 @@ + (NSArray *)routes + (id)handleTap:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - CGPoint tapPoint = CGPointMake((CGFloat)[request.arguments[@"x"] doubleValue], (CGFloat)[request.arguments[@"y"] doubleValue]); - if ([elementCache hasElementWithUUID:request.parameters[@"uuid"]]) { - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - NSError *error; - if (![element fb_tapCoordinate:tapPoint error:&error]) { - return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description - traceback:nil]); - } - } else { - XCUICoordinate *tapCoordinate = [self.class gestureCoordinateWithCoordinate:tapPoint - application:request.session.activeApplication]; - [tapCoordinate tap]; - } + CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue], + [request.arguments[@"y"] doubleValue]); + XCUIElement *element = [elementCache hasElementWithUUID:request.parameters[@"uuid"]] + ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]] + : request.session.activeApplication; + XCUICoordinate *tapCoordinate = [self.class gestureCoordinateWithOffset:offset element:element]; + [tapCoordinate tap]; return FBResponseWithOK(); } @@ -517,11 +557,6 @@ + (NSArray *)routes { NSString *textToType = [request.arguments[@"value"] componentsJoinedByString:@""]; NSUInteger frequency = [request.arguments[@"frequency"] unsignedIntegerValue] ?: [FBConfiguration maxTypingFrequency]; - if (![FBKeyboard waitUntilVisibleForApplication:request.session.activeApplication - timeout:1 - error:nil]) { - [FBLogger log:@"The on-screen keyboard seems to not exist. Continuing with typing anyway"]; - } NSError *error; if (![FBKeyboard typeText:textToType frequency:frequency error:&error]) { return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description @@ -532,11 +567,13 @@ + (NSArray *)routes + (id)handleGetWindowSize:(FBRouteRequest *)request { + XCUIApplication *app = request.session.activeApplication ?: FBApplication.fb_activeApplication; + #if TARGET_OS_TV - CGSize screenSize = request.session.activeApplication.frame.size; + CGSize screenSize = app.frame.size; #else - CGRect frame = request.session.activeApplication.wdFrame; - CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, request.session.activeApplication.interfaceOrientation); + CGRect frame = app.wdFrame; + CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation); #endif return FBResponseWithObject(@{ @"width": @(screenSize.width), @@ -548,48 +585,66 @@ + (NSArray *)routes { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - NSError *error; - NSData *screenshotData = [element fb_screenshotWithError:&error]; + NSData *screenshotData = [element.screenshot PNGRepresentation]; if (nil == screenshotData) { - return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:error.description + NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description]; + return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg traceback:nil]); } - NSString *screenshot = [screenshotData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; return FBResponseWithObject(screenshot); } #if !TARGET_OS_TV -static const CGFloat DEFAULT_OFFSET = (CGFloat)0.2; +static const CGFloat DEFAULT_PICKER_OFFSET = (CGFloat)0.2; +static const NSInteger DEFAULT_MAX_PICKER_ATTEMPTS = 25; + + (id)handleWheelSelect:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; if ([element.lastSnapshot elementType] != XCUIElementTypePickerWheel) { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:[NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType] traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + NSString *errMsg = [NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); } NSString* order = [request.arguments[@"order"] lowercaseString]; - CGFloat offset = DEFAULT_OFFSET; + CGFloat offset = DEFAULT_PICKER_OFFSET; if (request.arguments[@"offset"]) { offset = (CGFloat)[request.arguments[@"offset"] doubleValue]; if (offset <= 0.0 || offset > 0.5) { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:[NSString stringWithFormat:@"'offset' value is expected to be in range (0.0, 0.5]. '%@' was given instead", request.arguments[@"offset"]] traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + NSString *errMsg = [NSString stringWithFormat:@"'offset' value is expected to be in range (0.0, 0.5]. '%@' was given instead", request.arguments[@"offset"]]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); } } - BOOL isSuccessful = false; - NSError *error; - if ([order isEqualToString:@"next"]) { - isSuccessful = [element fb_selectNextOptionWithOffset:offset error:&error]; - } else if ([order isEqualToString:@"previous"]) { - isSuccessful = [element fb_selectPreviousOptionWithOffset:offset error:&error]; - } else { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:[NSString stringWithFormat:@"Only 'previous' and 'next' order values are supported. '%@' was given instead", request.arguments[@"order"]] traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); - } - if (!isSuccessful) { - return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); + NSNumber *maxAttempts = request.arguments[@"maxAttempts"] ?: @(DEFAULT_MAX_PICKER_ATTEMPTS); + NSString *expectedValue = request.arguments[@"value"]; + NSInteger attempt = 0; + while (attempt < [maxAttempts integerValue]) { + BOOL isSuccessful = false; + NSError *error; + if ([order isEqualToString:@"next"]) { + isSuccessful = [element fb_selectNextOptionWithOffset:offset error:&error]; + } else if ([order isEqualToString:@"previous"]) { + isSuccessful = [element fb_selectPreviousOptionWithOffset:offset error:&error]; + } else { + NSString *errMsg = [NSString stringWithFormat:@"Only 'previous' and 'next' order values are supported. '%@' was given instead", request.arguments[@"order"]]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg + traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); + } + if (!isSuccessful) { + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); + } + if (nil == expectedValue || [element.wdValue isEqualToString:expectedValue]) { + return FBResponseWithOK(); + } + attempt++; } - return FBResponseWithOK(); + NSString *errMsg = [NSString stringWithFormat:@"Cannot select the expected picker wheel value '%@' after %ld attempts", expectedValue, attempt]; + return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:errMsg traceback:nil]); } #pragma mark - Helpers @@ -608,57 +663,18 @@ + (NSArray *)routes } /** - Returns gesture coordinate for the application based on absolute coordinate + Returns gesture coordinate for the element based on absolute coordinate - @param coordinate absolute screen coordinates - @param application the instance of current application under test + @param offset absolute screen offset for the given application + @param element the element instance to perform the gesture on @return translated gesture coordinates ready to be passed to XCUICoordinate methods */ -+ (XCUICoordinate *)gestureCoordinateWithCoordinate:(CGPoint)coordinate - application:(XCUIApplication *)application -{ - CGPoint point = coordinate; - /** - If SDK >= 11, the tap coordinate based on application is not correct when - the application orientation is landscape and - tapX > application portrait width or tapY > application portrait height. - Pass the window element to the method [FBElementCommands gestureCoordinateWithCoordinate:element:] - will resolve the problem. - More details about the bug, please see the following issues: - #705: https://github.com/facebook/WebDriverAgent/issues/705 - #798: https://github.com/facebook/WebDriverAgent/issues/798 - #856: https://github.com/facebook/WebDriverAgent/issues/856 - Notice: On iOS 10, if the application is not launched by wda, no elements will be found. - See issue #732: https://github.com/facebook/WebDriverAgent/issues/732 - */ - XCUIElement *element = application; - if (isSDKVersionGreaterThanOrEqualTo(@"11.0")) { - XCUIElement *window = application.windows.fb_firstMatch; - if (window) { - element = window; - id snapshot = element.fb_cachedSnapshot ?: element.fb_takeSnapshot; - point.x -= snapshot.frame.origin.x; - point.y -= snapshot.frame.origin.y; - } - } - return [self gestureCoordinateWithCoordinate:point element:element]; -} - -/** - Returns gesture coordinate based on the specified element. - - @param coordinate absolute coordinates based on the element - @param element the element in the current application under test - @return translated gesture coordinates ready to be passed to XCUICoordinate methods - */ -+ (XCUICoordinate *)gestureCoordinateWithCoordinate:(CGPoint)coordinate - element:(XCUIElement *)element ++ (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset + element:(XCUIElement *)element { - XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:element - normalizedOffset:CGVectorMake(0, 0)]; - return [[XCUICoordinate alloc] initWithCoordinate:appCoordinate - pointsOffset:CGVectorMake(coordinate.x, coordinate.y)]; + return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset]; } + #endif @end diff --git a/WebDriverAgentLib/Commands/FBFindElementCommands.m b/WebDriverAgentLib/Commands/FBFindElementCommands.m index debc389be..037dbae15 100644 --- a/WebDriverAgentLib/Commands/FBFindElementCommands.m +++ b/WebDriverAgentLib/Commands/FBFindElementCommands.m @@ -22,6 +22,7 @@ #import "XCUIElement+FBClassChain.h" #import "XCUIElement+FBFind.h" #import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBUID.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" @@ -88,7 +89,7 @@ + (NSArray *)routes && [FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdVisible; }]; NSArray *cells = [element fb_filterDescendantsWithSnapshots:visibleCellSnapshots - selfUID:[FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdUID + selfUID:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:element.lastSnapshot] onlyChildren:NO]; return FBResponseWithCachedElements(cells, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); } diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.m b/WebDriverAgentLib/Commands/FBScreenshotCommands.m index 679a3305c..a39d1cc30 100644 --- a/WebDriverAgentLib/Commands/FBScreenshotCommands.m +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.m @@ -24,9 +24,9 @@ #import "FBLogger.h" #import "FBScreenshot.h" #import "FBMacros.h" -#import "FBImageIOScaler.h" #import "FBConfiguration.h" #import "FBErrorBuilder.h" +#import "FBImageProcessor.h" static const NSTimeInterval SCREENSHOT_TIMEOUT = 0.5; @@ -58,7 +58,7 @@ + (NSArray *)routes if (nil == screenshotData) { return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:error.description traceback:nil]); } - NSString *screenshot = [screenshotData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; return FBResponseWithObject(screenshot); } @@ -94,7 +94,7 @@ + (NSArray *)routes rect = CGRectNull; } - if ([FBScreenshot isNewScreenshotAPISupported]) { + if ([FBScreenshotCommands isNewScreenshotAPISupported]) { id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; __block NSError *innerError = nil; dispatch_semaphore_t sem = dispatch_semaphore_create(0); @@ -306,5 +306,15 @@ + (nullable NSData *)jpegDataWithImage:(CGImageRef)imageRef return newImageData; } ++ (BOOL)isNewScreenshotAPISupported +{ + static dispatch_once_t newScreenshotAPISupported; + static BOOL result; + dispatch_once(&newScreenshotAPISupported, ^{ + result = [(NSObject *)[FBXCTestDaemonsProxy testRunnerProxy] respondsToSelector:@selector(_XCT_requestScreenshotOfScreenWithID:withRect:uti:compressionQuality:withReply:)]; + }); + return result; +} + @end diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m index 75e0eb245..849a84ddc 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -68,9 +68,16 @@ + (NSArray *)routes if (!urlString) { return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"URL is required" traceback:nil]); } + NSString* bundleId = request.arguments[@"bundleId"]; NSError *error; - if (![XCUIDevice.sharedDevice fb_openUrl:urlString error:&error]) { - return FBResponseWithUnknownError(error); + if (nil == bundleId) { + if (![XCUIDevice.sharedDevice fb_openUrl:urlString error:&error]) { + return FBResponseWithUnknownError(error); + } + } else { + if (![XCUIDevice.sharedDevice fb_openUrl:urlString withApplication:bundleId error:&error]) { + return FBResponseWithUnknownError(error); + } } return FBResponseWithOK(); } @@ -90,6 +97,8 @@ + (NSArray *)routes if (nil == (capabilities = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error))) { return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.description traceback:nil]); } + + [FBConfiguration resetSessionSettings]; [FBConfiguration setShouldUseTestManagerForVisibilityDetection:[capabilities[FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION] boolValue]]; if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) { [FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; @@ -98,8 +107,8 @@ + (NSArray *)routes if (elementResponseAttributes) { [FBConfiguration setElementResponseAttributes:elementResponseAttributes]; } - if (capabilities[FB_CAP_MAX_TYPING_FREQUNCY]) { - [FBConfiguration setMaxTypingFrequency:[capabilities[FB_CAP_MAX_TYPING_FREQUNCY] unsignedIntegerValue]]; + if (capabilities[FB_CAP_MAX_TYPING_FREQUENCY]) { + [FBConfiguration setMaxTypingFrequency:[capabilities[FB_CAP_MAX_TYPING_FREQUENCY] unsignedIntegerValue]]; } if (capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER]) { [FBConfiguration setShouldUseSingletonTestManager:[capabilities[FB_CAP_USE_SINGLETON_TEST_MANAGER] boolValue]]; @@ -272,7 +281,8 @@ + (NSArray *)routes #endif @"ip" : [XCUIDevice sharedDevice].fb_wifiIPAddress ?: [NSNull null] }, - @"build" : buildInfo.copy + @"build" : buildInfo.copy, + @"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom] } ); } @@ -294,6 +304,7 @@ + (NSArray *)routes FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY: @([FBConfiguration mjpegServerScreenshotQuality]), FB_SETTING_MJPEG_SERVER_FRAMERATE: @([FBConfiguration mjpegServerFramerate]), FB_SETTING_MJPEG_SCALING_FACTOR: @([FBConfiguration mjpegScalingFactor]), + FB_SETTING_MJPEG_FIX_ORIENTATION: @([FBConfiguration mjpegShouldFixOrientation]), FB_SETTING_SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]), FB_SETTING_KEYBOARD_AUTOCORRECTION: @([FBConfiguration keyboardAutocorrection]), FB_SETTING_KEYBOARD_PREDICTION: @([FBConfiguration keyboardPrediction]), @@ -341,6 +352,9 @@ + (NSArray *)routes if (nil != [settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR]) { [FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] unsignedIntegerValue]]; } + if (nil != [settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION]) { + [FBConfiguration setMjpegShouldFixOrientation:[[settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION] boolValue]]; + } if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION]) { [FBConfiguration setKeyboardAutocorrection:[[settings objectForKey:FB_SETTING_KEYBOARD_AUTOCORRECTION] boolValue]]; } @@ -404,10 +418,9 @@ + (NSArray *)routes NSError *error; if (![FBConfiguration setScreenshotOrientation:(NSString *)[settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION] error:&error]) { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.description traceback:nil]); + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.description + traceback:nil]); } - - } #endif @@ -425,6 +438,10 @@ + (NSString *)buildTimestamp ]; } +/** + Return current session information. + This response does not have any active application information. +*/ + (NSDictionary *)sessionInformation { return @@ -434,15 +451,29 @@ + (NSDictionary *)sessionInformation }; } +/* + Return the device kind as lower case +*/ ++ (NSString *)deviceNameByUserInterfaceIdiom:(UIUserInterfaceIdiom) userInterfaceIdiom +{ + if (userInterfaceIdiom == UIUserInterfaceIdiomPad) { + return @"ipad"; + } else if (userInterfaceIdiom == UIUserInterfaceIdiomTV) { + return @"apple tv"; + } else if (userInterfaceIdiom == UIUserInterfaceIdiomPhone) { + return @"iphone"; + } + // CarPlay, Mac, Vision UI or unknown are possible + return @"Unknown"; + +} + + (NSDictionary *)currentCapabilities { - FBApplication *application = [FBSession activeSession].activeApplication; return @{ - @"device": ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) ? @"ipad" : @"iphone", - @"sdkVersion": [[UIDevice currentDevice] systemVersion], - @"browserName": application.label ?: [NSNull null], - @"CFBundleIdentifier": application.bundleID ?: [NSNull null], + @"device": [self.class deviceNameByUserInterfaceIdiom:[UIDevice currentDevice].userInterfaceIdiom], + @"sdkVersion": [[UIDevice currentDevice] systemVersion] }; } diff --git a/WebDriverAgentLib/Commands/UUElementCommands.m b/WebDriverAgentLib/Commands/UUElementCommands.m index 15df64a98..e10ca698f 100644 --- a/WebDriverAgentLib/Commands/UUElementCommands.m +++ b/WebDriverAgentLib/Commands/UUElementCommands.m @@ -31,7 +31,6 @@ #import "XCUIElement+FBIsVisible.h" #import "XCUIElement+FBPickerWheel.h" #import "XCUIElement+FBScrolling.h" -#import "XCUIElement+FBTap.h" #import "XCUIElement+FBTyping.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" @@ -203,46 +202,6 @@ + (NSArray *)routes { return FBResponseWithOK(); } -+ (id)uuDealAlert:(FBRouteRequest *)request { - FBApplication *application = request.session.activeApplication ?: [FBApplication fb_activeApplication]; - FBAlert *alert = [FBAlert alertWithApplication:application]; - NSError *error; - NSInteger counts = 0; - while (alert.isPresent && counts < 10) { - [alert uuAcceptWithError:&error]; - alert = [FBAlert alertWithApplication:application]; - counts += 1; - } - if (error) { - return FBResponseWithUnknownError(error); - } - return FBResponseWithOK(); -} - -+ (id)uuDealAlertWithParam:(FBRouteRequest *)request { - FBApplication *application = request.session.activeApplication ?: [FBApplication fb_activeApplication]; - BOOL accept = [request.arguments[@"accept"] boolValue]; - FBAlert *alert = [FBAlert alertWithApplication:application]; - if (nil == alert) { - return FBResponseWithOK(); - } - NSError *error; - NSInteger counts = 0; - while (alert.isPresent && counts < 10) { - if (accept) { - [alert uuAcceptWithError:&error]; - } else { - [alert uuDismissWithError:&error]; - } - alert = [FBAlert alertWithApplication:application]; - counts += 1; - } - if (error) { - return FBResponseWithUnknownError(error); - } - return FBResponseWithOK(); -} - + (id)uuGetSysInfo:(FBRouteRequest *)request { [[BatteryInfoManager sharedManager] startBatteryMonitoring]; @@ -581,9 +540,6 @@ screen orientation is different from the default one (which is portrait). */ + (XCUICoordinate *)uuGestureCoordinateWithCoordinate:(CGPoint)coordinate application:(XCUIApplication *)application shouldApplyOrientationWorkaround:(BOOL)shouldApplyOrientationWorkaround { CGPoint point = coordinate; - if (shouldApplyOrientationWorkaround) { - point = FBInvertPointForApplication(coordinate, application.frame.size, application.interfaceOrientation); - } XCUICoordinate *appCoordinate = [[XCUICoordinate alloc] initWithElement:application normalizedOffset:CGVectorMake(0, 0)]; return [[XCUICoordinate alloc] initWithCoordinate:appCoordinate pointsOffset:CGVectorMake(point.x, point.y)]; } diff --git a/WebDriverAgentLib/FBAlert.h b/WebDriverAgentLib/FBAlert.h index 2d9bf48e4..22f28a710 100644 --- a/WebDriverAgentLib/FBAlert.h +++ b/WebDriverAgentLib/FBAlert.h @@ -56,8 +56,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)acceptWithError:(NSError **)error; -- (BOOL)uuAcceptWithError:(NSError **)error; - /** Dismisses alert, if present @@ -66,8 +64,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)dismissWithError:(NSError **)error; -- (BOOL)uuDismissWithError:(NSError **)error; - /** Clicks on an alert button, if present diff --git a/WebDriverAgentLib/FBAlert.m b/WebDriverAgentLib/FBAlert.m index 6159ddc39..ff6b98c74 100644 --- a/WebDriverAgentLib/FBAlert.m +++ b/WebDriverAgentLib/FBAlert.m @@ -17,7 +17,6 @@ #import "FBXCodeCompatibility.h" #import "XCUIApplication+FBAlert.h" #import "XCUIElement+FBClassChain.h" -#import "XCUIElement+FBTap.h" #import "XCUIElement+FBTyping.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" @@ -190,11 +189,13 @@ - (BOOL)acceptWithError:(NSError **)error ? buttons.lastObject : buttons.firstObject; } - return nil == acceptButton - ? [[[FBErrorBuilder builder] + if (nil == acceptButton) { + return [[[FBErrorBuilder builder] withDescriptionFormat:@"Failed to find accept button for alert: %@", self.alertElement] - buildError:error] - : [acceptButton fb_tapWithError:error]; + buildError:error]; + } + [acceptButton tap]; + return YES; } - (BOOL)dismissWithError:(NSError **)error @@ -230,11 +231,13 @@ - (BOOL)dismissWithError:(NSError **)error : buttons.lastObject; } - return nil == dismissButton - ? [[[FBErrorBuilder builder] + if (nil == dismissButton) { + return [[[FBErrorBuilder builder] withDescriptionFormat:@"Failed to find dismiss button for alert: %@", self.alertElement] - buildError:error] - : [dismissButton fb_tapWithError:error]; + buildError:error]; + } + [dismissButton tap]; + return YES; } - (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error @@ -245,13 +248,14 @@ - (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error NSPredicate *predicate = [NSPredicate predicateWithFormat:@"label == %@", label]; XCUIElement *requestedButton = [[self.alertElement descendantsMatchingType:XCUIElementTypeButton] - matchingPredicate:predicate].fb_firstMatch; + matchingPredicate:predicate].allElementsBoundByIndex.firstObject; if (!requestedButton) { return [[[FBErrorBuilder builder] withDescriptionFormat:@"Failed to find button with label '%@' for alert: %@", label, self.alertElement] buildError:error]; } - return [requestedButton fb_tapWithError:error]; + [requestedButton tap]; + return YES; } - (XCUIElement *)alertElement @@ -272,45 +276,4 @@ - (XCUIElement *)alertElement } -- (BOOL)uuAcceptWithError:(NSError **)error -{ - XCUIElement *alertElement = self.alertElement; - NSArray *buttons = [alertElement descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex; - - XCUIElement *defaultButton; - if (alertElement.elementType == XCUIElementTypeAlert && [buttons count] < 3) { - defaultButton = buttons.lastObject; - } else { - defaultButton = buttons.firstObject; - } - if (!defaultButton) { - return - [[[FBErrorBuilder builder] - withDescriptionFormat:@"Failed to find accept button for alert: %@", alertElement] - buildError:error]; - } - return [defaultButton fb_tapWithError:error]; -} - -- (BOOL)uuDismissWithError:(NSError **)error -{ - XCUIElement *cancelButton; - XCUIElement *alertElement = self.alertElement; - NSArray *buttons = [alertElement descendantsMatchingType:XCUIElementTypeButton].allElementsBoundByIndex; - - if (alertElement.elementType == XCUIElementTypeAlert) { - cancelButton = buttons.firstObject; - } else { - cancelButton = buttons.lastObject; - } - if (!cancelButton) { - return - [[[FBErrorBuilder builder] - withDescriptionFormat:@"Failed to find dismiss button for alert: %@", alertElement] - buildError:error]; - return NO; - } - return [cancelButton fb_tapWithError:error]; -} - @end diff --git a/WebDriverAgentLib/FBApplication.m b/WebDriverAgentLib/FBApplication.m index 8a9a48ba0..fe46393fb 100644 --- a/WebDriverAgentLib/FBApplication.m +++ b/WebDriverAgentLib/FBApplication.m @@ -11,6 +11,7 @@ #import "FBXCAccessibilityElement.h" #import "FBLogger.h" +#import "FBExceptions.h" #import "FBRunLoopSpinner.h" #import "FBMacros.h" #import "FBActiveAppDetectionPoint.h" diff --git a/WebDriverAgentLib/Routing/FBElement.h b/WebDriverAgentLib/Routing/FBElement.h index 67e4593f9..7cc8f269c 100644 --- a/WebDriverAgentLib/Routing/FBElement.h +++ b/WebDriverAgentLib/Routing/FBElement.h @@ -53,10 +53,11 @@ NS_ASSUME_NONNULL_BEGIN /*! Whether element is an accessibility container (contains children of any depth that are accessible) */ @property (nonatomic, readonly, getter = isWDAccessibilityContainer) BOOL wdAccessibilityContainer; -#if TARGET_OS_TV /*! Whether element is focused */ @property (nonatomic, readonly, getter = isWDFocused) BOOL wdFocused; -#endif + +/*! Whether element is hittable */ +@property (nonatomic, readonly, getter = isWDHittable) BOOL wdHittable; /*! Element's index relatively to its parent. Starts from zero */ @property (nonatomic, readonly) NSUInteger wdIndex; diff --git a/WebDriverAgentLib/Routing/FBElementCache.m b/WebDriverAgentLib/Routing/FBElementCache.m index 1c91a8fb6..1f069bec6 100644 --- a/WebDriverAgentLib/Routing/FBElementCache.m +++ b/WebDriverAgentLib/Routing/FBElementCache.m @@ -38,7 +38,7 @@ - (instancetype)init return nil; } _elementCache = [[LRUCache alloc] initWithCapacity:ELEMENT_CACHE_SIZE]; - _elementsNeedReset = YES; + _elementsNeedReset = NO; return self; } @@ -50,8 +50,8 @@ - (NSString *)storeElement:(XCUIElement *)element } @synchronized (self.elementCache) { [self.elementCache setObject:element forKey:uuid]; + self.elementsNeedReset = YES; } - self.elementsNeedReset = YES; return uuid; } diff --git a/WebDriverAgentLib/Routing/FBElementUtils.h b/WebDriverAgentLib/Routing/FBElementUtils.h index 84de384ae..a0b6da839 100644 --- a/WebDriverAgentLib/Routing/FBElementUtils.h +++ b/WebDriverAgentLib/Routing/FBElementUtils.h @@ -64,6 +64,14 @@ extern NSString *const FBUnknownAttributeException; */ + (unsigned long long)idWithAccessibilityElement:(id)element; +/** + Retrieves the list of required instance methods of the given protocol + + @param protocol target protocol reference + @return set of selector names + */ ++ (NSSet *)selectorNamesWithProtocol:(Protocol *)protocol; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBElementUtils.m b/WebDriverAgentLib/Routing/FBElementUtils.m index d97ee1688..8aec9d8a8 100644 --- a/WebDriverAgentLib/Routing/FBElementUtils.m +++ b/WebDriverAgentLib/Routing/FBElementUtils.m @@ -20,11 +20,26 @@ @implementation FBElementUtils ++ (NSSet *)selectorNamesWithProtocol:(Protocol *)protocol +{ + unsigned int count; + struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, YES, YES, &count); + NSMutableSet *result = [NSMutableSet set]; + for (unsigned int i = 0; i < count; i++) { + SEL sel = methods[i].name; + if (nil != sel) { + [result addObject:NSStringFromSelector(sel)]; + } + } + free(methods); + return result.copy; +} + + (NSString *)wdAttributeNameForAttributeName:(NSString *)name { NSAssert(name.length > 0, @"Attribute name cannot be empty", nil); NSDictionary *attributeNamesMapping = [self.class wdAttributeNamesMapping]; - NSString *result = [attributeNamesMapping valueForKey:name]; + NSString *result = attributeNamesMapping[name]; if (nil == result) { NSString *description = [NSString stringWithFormat:@"The attribute '%@' is unknown. Valid attribute names are: %@", name, [attributeNamesMapping.allKeys sortedArrayUsingSelector:@selector(compare:)]]; @throw [NSException exceptionWithName:FBUnknownAttributeException reason:description userInfo:@{}]; @@ -107,7 +122,7 @@ + (NSString *)wdAttributeNameForAttributeName:(NSString *)name } attributeNamesMapping = resultCache.copy; }); - return attributeNamesMapping.copy; + return attributeNamesMapping; } + (NSString *)uidWithAccessibilityElement:(id)element diff --git a/WebDriverAgentLib/Routing/FBExceptionHandler.m b/WebDriverAgentLib/Routing/FBExceptionHandler.m index 203fcf0e7..7b7a8263f 100644 --- a/WebDriverAgentLib/Routing/FBExceptionHandler.m +++ b/WebDriverAgentLib/Routing/FBExceptionHandler.m @@ -24,7 +24,8 @@ - (void)handleException:(NSException *)exception forResponse:(RouteResponse *)re commandStatus = [FBCommandStatus noSuchDriverErrorWithMessage:exception.reason traceback:traceback]; } else if ([exception.name isEqualToString:FBInvalidArgumentException] - || [exception.name isEqualToString:FBElementAttributeUnknownException]) { + || [exception.name isEqualToString:FBElementAttributeUnknownException] + || [exception.name isEqualToString:FBApplicationMissingException]) { commandStatus = [FBCommandStatus invalidArgumentErrorWithMessage:exception.reason traceback:traceback]; } else if ([exception.name isEqualToString:FBApplicationCrashedException] diff --git a/WebDriverAgentLib/Routing/FBExceptions.h b/WebDriverAgentLib/Routing/FBExceptions.h index 4cbc3e5c4..1c9507a19 100644 --- a/WebDriverAgentLib/Routing/FBExceptions.h +++ b/WebDriverAgentLib/Routing/FBExceptions.h @@ -49,4 +49,7 @@ extern NSString *const FBClassChainQueryParseException; /*! Exception used to notify about application crash */ extern NSString *const FBApplicationCrashedException; +/*! Exception used to notify about the application is not installed */ +extern NSString *const FBApplicationMissingException; + NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBExceptions.m b/WebDriverAgentLib/Routing/FBExceptions.m index 5ff6fed85..571cea4fb 100644 --- a/WebDriverAgentLib/Routing/FBExceptions.m +++ b/WebDriverAgentLib/Routing/FBExceptions.m @@ -20,3 +20,4 @@ NSString *const FBXPathQueryEvaluationException = @"FBXPathQueryEvaluationException"; NSString *const FBClassChainQueryParseException = @"FBClassChainQueryParseException"; NSString *const FBApplicationCrashedException = @"FBApplicationCrashedException"; +NSString *const FBApplicationMissingException = @"FBApplicationMissingException"; diff --git a/WebDriverAgentLib/Routing/FBResponseJSONPayload.m b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m index d00ed38a7..4ad1c37ad 100644 --- a/WebDriverAgentLib/Routing/FBResponseJSONPayload.m +++ b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m @@ -9,6 +9,8 @@ #import "FBResponseJSONPayload.h" +#import "FBLogger.h" +#import "NSDictionary+FBUtf8SafeDictionary.h" #import "RouteResponse.h" @interface FBResponseJSONPayload () @@ -43,6 +45,13 @@ - (void)dispatchWithResponse:(RouteResponse *)response options:NSJSONWritingPrettyPrinted error:&error]; NSCAssert(jsonData, @"Valid JSON must be responded, error of %@", error); + if (nil == [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]) { + [FBLogger log:@"The incoming data cannot be encoded to UTF-8 JSON. Applying lossy conversion as a workaround."]; + jsonData = [NSJSONSerialization dataWithJSONObject:[self.dictionary fb_utf8SafeDictionary] + options:NSJSONWritingPrettyPrinted + error:&error]; + } + NSCAssert(jsonData, @"Valid JSON must be responded, error of %@", error); [response setHeader:@"Content-Type" value:@"application/json;charset=UTF-8"]; [response setStatusCode:self.httpStatusCode]; [response respondWithData:jsonData]; diff --git a/WebDriverAgentLib/Routing/FBResponsePayload.m b/WebDriverAgentLib/Routing/FBResponsePayload.m index 7d3c1c1ac..4c1b53cbb 100644 --- a/WebDriverAgentLib/Routing/FBResponsePayload.m +++ b/WebDriverAgentLib/Routing/FBResponsePayload.m @@ -18,6 +18,7 @@ #import "FBProtocolHelpers.h" #import "XCUIElementQuery.h" #import "XCUIElement+FBResolve.h" +#import "XCUIElement+FBUID.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" @@ -35,7 +36,7 @@ return [[UUResponsePicPayload alloc] initWithData:object andType:@"jpg"]; } -id FBResponseWithOK() +id FBResponseWithOK(void) { return FBResponseWithStatus(FBCommandStatus.ok); } @@ -51,7 +52,7 @@ ? YES : FBSession.activeSession.useNativeCachingStrategy; [elementCache storeElement:(useNativeCachingStrategy ? element : element.fb_stableInstance)]; - return FBResponseWithStatus([FBCommandStatus okWithValue: FBDictionaryResponseWithElement(element, compact)]); + return FBResponseWithStatus([FBCommandStatus okWithValue:FBDictionaryResponseWithElement(element, compact)]); } id FBResponseWithCachedElements(NSArray *elements, FBElementCache *elementCache, BOOL compact) @@ -77,7 +78,8 @@ va_list argList; va_start(argList, format); NSString *errorMessage = [[NSString alloc] initWithFormat:format arguments:argList]; - id payload = FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:errorMessage traceback:nil]); + id payload = FBResponseWithStatus([FBCommandStatus unknownErrorWithMessage:errorMessage + traceback:nil]); va_end(argList); return payload; } @@ -89,13 +91,12 @@ if (nil == status.error) { response[@"value"] = status.value ?: NSNull.null; } else { - NSMutableDictionary* value = [NSMutableDictionary dictionary]; - value[@"error"] = status.error; - value[@"message"] = status.message ?: @""; - value[@"traceback"] = status.traceback ?: @""; - response[@"value"] = value.copy; + response[@"value"] = @{ + @"error": (id)status.error, + @"message": status.message ?: @"", + @"traceback": status.traceback ?: @"" + }; } - return [[FBResponseJSONPayload alloc] initWithDictionary:response.copy httpStatusCode:status.statusCode]; } @@ -109,31 +110,34 @@ if (nil == snapshot) { snapshot = element.lastSnapshot ?: element.fb_takeSnapshot; } + NSDictionary *compactResult = FBToElementDict((NSString *)[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]); + if (compact) { + return compactResult; + } + + NSMutableDictionary *result = compactResult.mutableCopy; FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSMutableDictionary *dictionary = FBInsertElement(@{}, (NSString *)wrappedSnapshot.wdUID).mutableCopy; - if (!compact) { - NSArray *fields = [FBConfiguration.elementResponseAttributes componentsSeparatedByString:@","]; - for (NSString *field in fields) { - // 'name' here is the w3c-approved identifier for what we mean by 'type' - if ([field isEqualToString:@"name"] || [field isEqualToString:@"type"]) { - dictionary[field] = wrappedSnapshot.wdType; - } else if ([field isEqualToString:@"text"]) { - dictionary[field] = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel) ?: [NSNull null]; - } else if ([field isEqualToString:@"rect"]) { - dictionary[field] = wrappedSnapshot.wdRect; - } else if ([field isEqualToString:@"enabled"]) { - dictionary[field] = @(wrappedSnapshot.wdEnabled); - } else if ([field isEqualToString:@"displayed"]) { - dictionary[field] = @(wrappedSnapshot.wdVisible); - } else if ([field isEqualToString:@"selected"]) { - dictionary[field] = @(wrappedSnapshot.wdSelected); - } else if ([field isEqualToString:@"label"]) { - dictionary[field] = wrappedSnapshot.wdLabel ?: [NSNull null]; - } else if ([field hasPrefix:arbitraryAttrPrefix]) { - NSString *attributeName = [field substringFromIndex:[arbitraryAttrPrefix length]]; - dictionary[field] = [wrappedSnapshot fb_valueForWDAttributeName:attributeName] ?: [NSNull null]; - } + NSArray *fields = [FBConfiguration.elementResponseAttributes componentsSeparatedByString:@","]; + for (NSString *field in fields) { + // 'name' here is the w3c-approved identifier for what we mean by 'type' + if ([field isEqualToString:@"name"] || [field isEqualToString:@"type"]) { + result[field] = wrappedSnapshot.wdType; + } else if ([field isEqualToString:@"text"]) { + result[field] = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel) ?: [NSNull null]; + } else if ([field isEqualToString:@"rect"]) { + result[field] = wrappedSnapshot.wdRect; + } else if ([field isEqualToString:@"enabled"]) { + result[field] = @(wrappedSnapshot.wdEnabled); + } else if ([field isEqualToString:@"displayed"]) { + result[field] = @(wrappedSnapshot.wdVisible); + } else if ([field isEqualToString:@"selected"]) { + result[field] = @(wrappedSnapshot.wdSelected); + } else if ([field isEqualToString:@"label"]) { + result[field] = wrappedSnapshot.wdLabel ?: [NSNull null]; + } else if ([field hasPrefix:arbitraryAttrPrefix]) { + NSString *attributeName = [field substringFromIndex:[arbitraryAttrPrefix length]]; + result[field] = [wrappedSnapshot fb_valueForWDAttributeName:attributeName] ?: [NSNull null]; } } - return dictionary.copy; + return result.copy; } diff --git a/WebDriverAgentLib/Routing/FBSession.m b/WebDriverAgentLib/Routing/FBSession.m index d0e5013be..4d0d32db2 100644 --- a/WebDriverAgentLib/Routing/FBSession.m +++ b/WebDriverAgentLib/Routing/FBSession.m @@ -179,13 +179,13 @@ - (FBApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier environment:(nullable NSDictionary *)environment { FBApplication *app = [[FBApplication alloc] initWithBundleIdentifier:bundleIdentifier]; + if (nil == shouldWaitForQuiescence) { + // Iherit the quiescence check setting from the main app under test by default + app.fb_shouldWaitForQuiescence = nil != self.testedApplicationBundleId && self.shouldAppsWaitForQuiescence; + } else { + app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue]; + } if (app.fb_state < 2) { - if (nil == shouldWaitForQuiescence) { - // Iherit the quiescence check setting from the main app under test by default - app.fb_shouldWaitForQuiescence = nil != self.testedApplicationBundleId && self.shouldAppsWaitForQuiescence; - } else { - app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue]; - } app.launchArguments = arguments ?: @[]; app.launchEnvironment = environment ?: @{}; [app launch]; diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index a9b9a0625..7e991e0e2 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -220,6 +220,16 @@ - (void)registerServerKeyRouteHandlers [response respondWithString:@"I-AM-ALIVE"]; }]; + NSString *calibrationPage = @"" + "{\"x\":null,\"y\":null}" + "
" + "" + "
" + ""; + [self.server get:@"/calibrate" withBlock:^(RouteRequest *request, RouteResponse *response) { + [response respondWithString:calibrationPage]; + }]; + [self.server get:@"/wda/shutdown" withBlock:^(RouteRequest *request, RouteResponse *response) { [response respondWithString:@"Shutting down"]; [self.delegate webServerDidRequestShutdown:self]; diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m index cbc6e46b8..1beb87bb2 100644 --- a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m @@ -9,7 +9,7 @@ #import "FBXCElementSnapshotWrapper.h" -#import +#import "FBElementUtils.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-protocol-property-synthesis" @@ -33,11 +33,81 @@ + (instancetype)ensureWrapped:(id)snapshot : [[FBXCElementSnapshotWrapper alloc] initWithSnapshot:snapshot]; } +// Attributes are queried most often, +// so we prefer them to have direct accessors defined here +// rather than to use message forwarding via forwardingTargetForSelector, +// which is slow + +- (NSString *)identifier +{ + return self.snapshot.identifier; +} + +- (CGRect)frame +{ + return self.snapshot.frame; +} + +- (id)value +{ + return self.snapshot.value; +} + +- (NSString *)title +{ + return self.snapshot.title; +} + +- (NSString *)label +{ + return self.snapshot.label; +} + +- (XCUIElementType)elementType +{ + return self.snapshot.elementType; +} + +- (BOOL)isEnabled +{ + return self.snapshot.enabled; +} + +- (XCUIUserInterfaceSizeClass)horizontalSizeClass +{ + return self.snapshot.horizontalSizeClass; +} + +- (XCUIUserInterfaceSizeClass)verticalSizeClass +{ + return self.snapshot.verticalSizeClass; +} + +- (NSString *)placeholderValue +{ + return self.snapshot.placeholderValue; +} + +- (BOOL)isSelected +{ + return self.snapshot.selected; +} + +#if !TARGET_OS_OSX +- (BOOL)hasFocus +{ + return self.snapshot.hasFocus; +} +#endif + - (id)forwardingTargetForSelector:(SEL)aSelector { - struct objc_method_description descr = protocol_getMethodDescription(@protocol(FBXCElementSnapshot), aSelector, YES, YES); - SEL selector = descr.name; - return nil == selector ? nil : self.snapshot; + static dispatch_once_t onceToken; + static NSSet *names; + dispatch_once(&onceToken, ^{ + names = [FBElementUtils selectorNamesWithProtocol:@protocol(FBXCElementSnapshot)]; + }); + return [names containsObject:NSStringFromSelector(aSelector)] ? self.snapshot : nil; } @end diff --git a/WebDriverAgentLib/Utilities/FBAlertsMonitor.m b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m index cdb4a9b3e..c2c625084 100644 --- a/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +++ b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m @@ -11,6 +11,7 @@ #import "FBAlert.h" #import "FBApplication.h" +#import "FBLogger.h" #import "XCUIApplication+FBAlert.h" static const NSTimeInterval FB_MONTORING_INTERVAL = 2.0; @@ -50,9 +51,16 @@ - (void)scheduleNextTick dispatch_async(dispatch_get_main_queue(), ^{ NSArray *activeApps = FBApplication.fb_activeApplications; for (FBApplication *activeApp in activeApps) { - XCUIElement *alertElement = activeApp.fb_alertElement; + XCUIElement *alertElement = nil; + @try { + alertElement = activeApp.fb_alertElement; + if (nil != alertElement) { + [self.delegate didDetectAlert:[FBAlert alertWithElement:alertElement]]; + } + } @catch (NSException *e) { + [FBLogger logFmt:@"Got an unexpected exception while monitoring alerts: %@\n%@", e.reason, e.callStackSymbols]; + } if (nil != alertElement) { - [self.delegate didDetectAlert:[FBAlert alertWithElement:alertElement]]; break; } } diff --git a/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.m index 0dfe8fa51..235d97e63 100644 --- a/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.m +++ b/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.m @@ -80,7 +80,7 @@ @implementation FBAppiumGestureItem - (nullable instancetype)initWithActionItem:(NSDictionary *)item application:(XCUIApplication *)application - atPosition:(nullable NSValue *)atPosition + atPosition:(nullable XCUICoordinate *)atPosition offset:(double)offset error:(NSError **)error { @@ -90,14 +90,14 @@ - (nullable instancetype)initWithActionItem:(NSDictionary *)item self.application = application; self.offset = offset; id options = [item objectForKey:FB_OPTIONS_KEY]; - if (atPosition) { - self.atPosition = [atPosition CGPointValue]; + if (nil != atPosition) { + self.atPosition = (id) atPosition; } else { - NSValue *result = [self coordinatesWithOptions:options error:error]; + XCUICoordinate *result = [self coordinatesWithOptions:options error:error]; if (nil == result) { return nil; } - self.atPosition = [result CGPointValue]; + self.atPosition = result; } self.duration = [self durationWithOptions:options]; if (self.duration < 0) { @@ -124,7 +124,8 @@ - (double)durationWithOptions:(nullable NSDictionary *)options 0.0; } -- (nullable NSValue *)coordinatesWithOptions:(nullable NSDictionary *)options error:(NSError **)error +- (nullable XCUICoordinate *)coordinatesWithOptions:(nullable NSDictionary *)options + error:(NSError **)error { if (![options isKindOfClass:NSDictionary.class]) { NSString *description = [NSString stringWithFormat:@"'%@' key is mandatory for '%@' action", FB_OPTIONS_KEY, self.class.actionName]; @@ -169,7 +170,7 @@ + (BOOL)hasAbsolutePositioning NSTimeInterval currentOffset = FBMillisToSeconds(self.offset); NSMutableArray *result = [NSMutableArray array]; XCPointerEventPath *currentPath = [[XCPointerEventPath alloc] - initForTouchAtPoint:self.atPosition + initForTouchAtPoint:self.atPosition.screenPoint offset:currentOffset]; [result addObject:currentPath]; currentOffset += FBMillisToSeconds(FB_TAP_DURATION_MS); @@ -180,7 +181,8 @@ + (BOOL)hasAbsolutePositioning NSNumber *tapCount = [options objectForKey:FB_OPTION_COUNT] ?: @1; for (NSInteger times = 1; times < tapCount.integerValue; ++times) { currentOffset += FBMillisToSeconds(FB_INTERTAP_MIN_DURATION_MS); - XCPointerEventPath *nextPath = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition offset:currentOffset]; + XCPointerEventPath *nextPath = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint + offset:currentOffset]; [result addObject:nextPath]; currentOffset += FBMillisToSeconds(FB_TAP_DURATION_MS); [nextPath liftUpAtOffset:currentOffset]; @@ -204,7 +206,7 @@ @implementation FBPressItem - (nullable instancetype)initWithActionItem:(NSDictionary *)item application:(XCUIApplication *)application - atPosition:(nullable NSValue *)atPosition + atPosition:(nullable XCUICoordinate *)atPosition offset:(double)offset error:(NSError **)error { @@ -239,7 +241,7 @@ + (BOOL)hasAbsolutePositioning error:(NSError **)error { XCPointerEventPath *result = [[XCPointerEventPath alloc] - initForTouchAtPoint:self.atPosition + initForTouchAtPoint:self.atPosition.screenPoint offset:FBMillisToSeconds(self.offset)]; if (nil != self.pressure && nil != result.pointerEvents.lastObject) { XCPointerEvent *pointerEvent = (XCPointerEvent *)result.pointerEvents.lastObject; @@ -272,7 +274,7 @@ + (BOOL)hasAbsolutePositioning currentItemIndex:(NSUInteger)currentItemIndex error:(NSError **)error { - return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition + return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint offset:FBMillisToSeconds(self.offset)]]; } @@ -312,7 +314,8 @@ + (BOOL)hasAbsolutePositioning } } NSTimeInterval currentOffset = FBMillisToSeconds(self.offset + self.duration); - XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition offset:currentOffset]; + XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint + offset:currentOffset]; if (currentItemIndex == allItems.count - 1) { [result liftUpAtOffset:currentOffset]; } @@ -353,7 +356,8 @@ + (BOOL)hasAbsolutePositioning return nil; } - [eventPath moveToPoint:self.atPosition atOffset:FBMillisToSeconds(self.offset)]; + [eventPath moveToPoint:self.atPosition.screenPoint + atOffset:FBMillisToSeconds(self.offset)]; return @[]; } @@ -448,9 +452,10 @@ @implementation FBAppiumActionsSynthesizer [result addObject:touchItem]; continue; } + NSMutableDictionary *elementDict = FBCleanupElements(options).mutableCopy; + [elementDict addEntriesFromDictionary:FBToElementDict(element)]; NSMutableDictionary *processedItem = touchItem.mutableCopy; - [processedItem setObject:FBInsertElement(FBCleanupElements(options), element) - forKey:FB_OPTIONS_KEY]; + processedItem[FB_OPTIONS_KEY] = elementDict.copy; [result addObject:processedItem.copy]; } return [[result reverseObjectEnumerator] allObjects]; @@ -508,7 +513,11 @@ @implementation FBAppiumActionsSynthesizer return nil; } FBAppiumGestureItem *lastItem = [chain.items lastObject]; - gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem application:self.application atPosition:[NSValue valueWithCGPoint:lastItem.atPosition] offset:chain.durationOffset error:error]; + gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem + application:self.application + atPosition:lastItem.atPosition + offset:chain.durationOffset + error:error]; } if (nil == gestureItem) { return nil; @@ -526,7 +535,7 @@ - (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error BOOL isMultiTouch = [self.actions.firstObject isKindOfClass:NSArray.class]; eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:(isMultiTouch ? @"Multi-Finger Touch Action" : @"Single-Finger Touch Action") - interfaceOrientation:[FBXCTestDaemonsProxy orientationWithApplication:self.application]]; + interfaceOrientation:self.application.interfaceOrientation]; for (NSArray *> *action in (isMultiTouch ? self.actions : @[self.actions])) { NSArray *> *preprocessedAction = [self preprocessAction:action]; NSArray *eventPaths = [self eventPathsWithAction:preprocessedAction error:error]; diff --git a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h index d50408215..5f779194f 100644 --- a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h +++ b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h @@ -51,19 +51,10 @@ NS_ASSUME_NONNULL_BEGIN @interface FBBaseGestureItem : FBBaseActionItem /*! Absolute position on the screen where the gesure should be performed */ -@property (nonatomic) CGPoint atPosition; +@property (nonatomic) XCUICoordinate *atPosition; /*! Gesture duration in milliseconds */ @property (nonatomic) double duration; -/** - Returns fixed hit point coordinates for the case when XCTest fails to transform element snaapshot properly on screen rotation. - - @param hitPoint The initial hitpoint coordinates - @param snapshot Element's snapshot instance - @return The fixed hit point coordinates, if there is a need to fix them, or the unchanged hit point value - */ -- (CGPoint)fixedHitPointWith:(CGPoint)hitPoint forSnapshot:(id)snapshot; - /** Calculate absolute gesture position on the screen based on provided element and positionOffset values. @@ -72,9 +63,9 @@ NS_ASSUME_NONNULL_BEGIN @param error If there is an error, upon return contains an NSError object that describes the problem @return Adbsolute gesture position on the screen or nil if the calculation fails (for example, the element is invisible) */ -- (nullable NSValue *)hitpointWithElement:(nullable XCUIElement *)element - positionOffset:(nullable NSValue *)positionOffset - error:(NSError **)error; +- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element + positionOffset:(nullable NSValue *)positionOffset + error:(NSError **)error; @end diff --git a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m index 24cc054fa..6d4c046a5 100644 --- a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m +++ b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m @@ -31,7 +31,10 @@ + (NSString *)actionName return nil; } -- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath allItems:(NSArray *)allItems currentItemIndex:(NSUInteger)currentItemIndex error:(NSError **)error +- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath + allItems:(NSArray *)allItems + currentItemIndex:(NSUInteger)currentItemIndex + error:(NSError **)error { @throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build]; return nil; @@ -41,84 +44,40 @@ + (NSString *)actionName @implementation FBBaseGestureItem -- (CGPoint)fixedHitPointWith:(CGPoint)hitPoint forSnapshot:(id)snapshot +- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element + positionOffset:(nullable NSValue *)positionOffset + error:(NSError **)error { - UIInterfaceOrientation interfaceOrientation = self.application.interfaceOrientation; - if (interfaceOrientation == UIInterfaceOrientationPortrait) { - // There is no need to recalculate anything for portrait orientation - return hitPoint; - } - CGRect appFrame = self.application.frame; - if (@available(iOS 13.0, *)) { - // For Xcode11 it is always necessary to adjust the tap point coordinates - return FBInvertPointForApplication(hitPoint, appFrame.size, interfaceOrientation); - } - NSArray> *ancestors = [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_ancestors; - id parentWindow = ancestors.count > 1 ? [ancestors objectAtIndex:ancestors.count - 2] : nil; - CGRect parentWindowFrame = nil == parentWindow ? snapshot.frame : parentWindow.frame; - if ((appFrame.size.height > appFrame.size.width && parentWindowFrame.size.height < parentWindowFrame.size.width) || - (appFrame.size.height < appFrame.size.width && parentWindowFrame.size.height > parentWindowFrame.size.width)) { - /* - This is the indication of the fact that transformation is broken and coordinates should be - recalculated manually. - However, upside-down case cannot be covered this way, which is not important for Appium - */ - return FBInvertPointForApplication(hitPoint, appFrame.size, interfaceOrientation); - } - return hitPoint; -} - -- (nullable NSValue *)hitpointWithElement:(nullable XCUIElement *)element positionOffset:(nullable NSValue *)positionOffset error:(NSError **)error -{ - CGPoint hitPoint; if (nil == element) { + CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y); // Only absolute offset is defined - hitPoint = [positionOffset CGPointValue]; - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0")) { - /* - Since iOS 10.0 XCTest has a bug when it always returns portrait coordinates for UI elements - even if the device is not in portait mode. That is why we need to recalculate them manually - based on the current orientation value - */ - hitPoint = FBInvertPointForApplication(hitPoint, self.application.frame.size, self.application.interfaceOrientation); - } - } else { - // The offset relative to the element is defined - - id snapshot = element.fb_isResolvedFromCache.boolValue - ? element.lastSnapshot - : element.fb_takeSnapshot; - if (nil == positionOffset) { - NSValue *hitPointValue = [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_hitPoint; - if (nil != hitPointValue) { - // short circuit element hitpoint - return hitPointValue; - } - [FBLogger logFmt:@"Will use the frame of '%@' for hit point calculation instead", element.debugDescription]; - } - CGRect visibleFrame = snapshot.visibleFrame; - CGRect frame = CGRectIsEmpty(visibleFrame) ? element.frame : visibleFrame; - if (CGRectIsEmpty(frame)) { - [FBLogger log:self.application.fb_descriptionRepresentation]; - NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable", element.description]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; + return [[self.application coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset]; + } + + // The offset relative to the element is defined + if (nil == positionOffset) { + if (element.hittable) { + // short circuit element hitpoint + return element.hitPointCoordinate; } - if (nil == positionOffset) { - hitPoint = CGPointMake(frame.origin.x + frame.size.width / 2, - frame.origin.y + frame.size.height / 2); - } else { - CGPoint origin = frame.origin; - hitPoint = CGPointMake(origin.x, origin.y); - CGPoint offsetValue = [positionOffset CGPointValue]; - hitPoint = CGPointMake(hitPoint.x + offsetValue.x, hitPoint.y + offsetValue.y); - // TODO: Shall we throw an exception if hitPoint is out of the element frame? + [FBLogger logFmt:@"Will use the frame of '%@' for hit point calculation instead", element.debugDescription]; + } + if (CGRectIsEmpty(element.frame)) { + [FBLogger log:self.application.fb_descriptionRepresentation]; + NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable", + element.description]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; } - hitPoint = [self fixedHitPointWith:hitPoint forSnapshot:snapshot]; + return nil; } - return [NSValue valueWithCGPoint:hitPoint]; + if (nil == positionOffset) { + return [element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)]; + } + + CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y); + // TODO: Shall we throw an exception if hitPoint is out of the element frame? + return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset]; } @end @@ -179,7 +138,10 @@ - (void)addItem:(FBBaseActionItem *)item __attribute__((noreturn)) @implementation FBBaseActionsSynthesizer -- (instancetype)initWithActions:(NSArray *)actions forApplication:(XCUIApplication *)application elementCache:(nullable FBElementCache *)elementCache error:(NSError **)error +- (instancetype)initWithActions:(NSArray *)actions + forApplication:(XCUIApplication *)application + elementCache:(nullable FBElementCache *)elementCache + error:(NSError **)error { self = [super init]; if (self) { diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.h b/WebDriverAgentLib/Utilities/FBCapabilities.h index 76b9f1203..116c408df 100644 --- a/WebDriverAgentLib/Utilities/FBCapabilities.h +++ b/WebDriverAgentLib/Utilities/FBCapabilities.h @@ -10,7 +10,7 @@ #import extern NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION; -extern NSString* const FB_CAP_MAX_TYPING_FREQUNCY; +extern NSString* const FB_CAP_MAX_TYPING_FREQUENCY; extern NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER; extern NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS; extern NSString* const FB_CAP_SHOULD_TERMINATE_APP; diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.m b/WebDriverAgentLib/Utilities/FBCapabilities.m index 0cef52273..cafd0f168 100644 --- a/WebDriverAgentLib/Utilities/FBCapabilities.m +++ b/WebDriverAgentLib/Utilities/FBCapabilities.m @@ -10,7 +10,7 @@ #import "FBCapabilities.h" NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION = @"shouldUseTestManagerForVisibilityDetection"; -NSString* const FB_CAP_MAX_TYPING_FREQUNCY = @"maxTypingFrequency"; +NSString* const FB_CAP_MAX_TYPING_FREQUENCY = @"maxTypingFrequency"; NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER = @"shouldUseSingletonTestManager"; NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS = @"disableAutomaticScreenshots"; NSString* const FB_CAP_SHOULD_TERMINATE_APP = @"shouldTerminateApp"; diff --git a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m index cb4ba3077..f04240d0d 100644 --- a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m +++ b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m @@ -502,7 +502,9 @@ + (NSError *)compilationErrorWithQuery:(NSString *)originalQuery description:(NS return [[FBErrorBuilder.builder withDescription:fullDescription] build]; } -+ (nullable FBClassChain*)compiledQueryWithTokenizedQuery:(NSArray *)tokenizedQuery originalQuery:(NSString *)originalQuery error:(NSError **)error ++ (nullable FBClassChain*)compiledQueryWithTokenizedQuery:(NSArray *)tokenizedQuery + originalQuery:(NSString *)originalQuery + error:(NSError **)error { NSMutableArray *result = [NSMutableArray array]; XCUIElementType chainElementType = XCUIElementTypeAny; @@ -514,8 +516,10 @@ + (nullable FBClassChain*)compiledQueryWithTokenizedQuery:(NSArray -#import -#import - -#import "FBConfiguration.h" -#import "FBErrorBuilder.h" -#import "FBLogger.h" - -const CGFloat FBMinScalingFactor = 0.01f; -const CGFloat FBMaxScalingFactor = 1.0f; -const CGFloat FBMinCompressionQuality = 0.0f; -const CGFloat FBMaxCompressionQuality = 1.0f; - -@interface FBImageIOScaler () - -@property (nonatomic) NSData *nextImage; -@property (nonatomic, readonly) NSLock *nextImageLock; -@property (nonatomic, readonly) dispatch_queue_t scalingQueue; - -@end - -@implementation FBImageIOScaler - -- (id)init -{ - self = [super init]; - if (self) { - _nextImageLock = [[NSLock alloc] init]; - _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL); - } - return self; -} - -- (void)submitImage:(NSData *)image - uti:(NSString *)uti - scalingFactor:(CGFloat)scalingFactor - compressionQuality:(CGFloat)compressionQuality - completionHandler:(void (^)(NSData *))completionHandler -{ - [self.nextImageLock lock]; - if (self.nextImage != nil) { - [FBLogger verboseLog:@"Discarding screenshot"]; - } - scalingFactor = MAX(FBMinScalingFactor, MIN(FBMaxScalingFactor, scalingFactor)); - compressionQuality = MAX(FBMinCompressionQuality, MIN(FBMaxCompressionQuality, compressionQuality)); - self.nextImage = image; - [self.nextImageLock unlock]; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wcompletion-handler" - dispatch_async(self.scalingQueue, ^{ - [self.nextImageLock lock]; - NSData *next = self.nextImage; - self.nextImage = nil; - [self.nextImageLock unlock]; - if (next == nil) { - return; - } - - NSError *error; - NSData *scaled = [self scaledJpegImageWithImage:next - scalingFactor:scalingFactor - compressionQuality:compressionQuality - error:&error]; - if (scaled == nil) { - [FBLogger logFmt:@"%@", error.description]; - return; - } - completionHandler(scaled); - }); -#pragma clang diagnostic pop -} - -// This method is more optimized for JPEG scaling -// and should be used in `submitImage` API, while the `scaledImageWithImage` -// one is more generic -- (nullable NSData *)scaledJpegImageWithImage:(NSData *)image - scalingFactor:(CGFloat)scalingFactor - compressionQuality:(CGFloat)compressionQuality - error:(NSError **)error -{ - CGImageSourceRef imageData = CGImageSourceCreateWithData((CFDataRef)image, nil); - CGSize size = [self.class imageSizeWithImage:imageData]; - CGFloat scaledMaxPixelSize = MAX(size.width, size.height) * scalingFactor; - CFDictionaryRef params = (__bridge CFDictionaryRef)@{ - (const NSString *)kCGImageSourceCreateThumbnailWithTransform: @(YES), - (const NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent: @(YES), - (const NSString *)kCGImageSourceThumbnailMaxPixelSize: @(scaledMaxPixelSize) - }; - CGImageRef scaled = CGImageSourceCreateThumbnailAtIndex(imageData, 0, params); - CFRelease(imageData); - if (nil == scaled) { - [[[FBErrorBuilder builder] - withDescriptionFormat:@"Failed to scale the image"] - buildError:error]; - return nil; - } - NSData *resData = [self jpegDataWithImage:scaled - compressionQuality:compressionQuality]; - if (nil == resData) { - [[[FBErrorBuilder builder] - withDescriptionFormat:@"Failed to compress the image to JPEG format"] - buildError:error]; - } - CGImageRelease(scaled); - return resData; -} - -- (nullable NSData *)scaledImageWithImage:(NSData *)image - uti:(NSString *)uti - rect:(CGRect)rect - scalingFactor:(CGFloat)scalingFactor - compressionQuality:(CGFloat)compressionQuality - error:(NSError **)error -{ - UIImage *uiImage = [UIImage imageWithData:image]; - CGSize size = uiImage.size; - CGSize scaledSize = CGSizeMake(size.width * scalingFactor, size.height * scalingFactor); - UIGraphicsBeginImageContext(scaledSize); - UIImageOrientation orientation = uiImage.imageOrientation; -#if !TARGET_OS_TV - if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortrait) { - orientation = UIImageOrientationUp; - } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortraitUpsideDown) { - orientation = UIImageOrientationDown; - } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeLeft) { - orientation = UIImageOrientationRight; - } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeRight) { - orientation = UIImageOrientationLeft; - } -#endif - uiImage = [UIImage imageWithCGImage:(CGImageRef)uiImage.CGImage - scale:uiImage.scale - orientation:orientation]; - [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)]; - UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - if (!CGRectIsNull(rect)) { - UIGraphicsBeginImageContext(rect.size); - [resultImage drawAtPoint:CGPointMake(-rect.origin.x, -rect.origin.y)]; - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - } - - return [uti isEqualToString:(__bridge id)kUTTypePNG] - ? UIImagePNGRepresentation(resultImage) - : UIImageJPEGRepresentation(resultImage, compressionQuality); -} - -- (nullable NSData *)jpegDataWithImage:(CGImageRef)imageRef - compressionQuality:(CGFloat)compressionQuality -{ - NSMutableData *newImageData = [NSMutableData data]; - CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((CFMutableDataRef)newImageData, kUTTypeJPEG, 1, NULL); - CFDictionaryRef compressionOptions = (__bridge CFDictionaryRef)@{ - (const NSString *)kCGImageDestinationLossyCompressionQuality: @(compressionQuality) - }; - CGImageDestinationAddImage(imageDestination, imageRef, compressionOptions); - if(!CGImageDestinationFinalize(imageDestination)) { - [FBLogger log:@"Failed to write the image"]; - newImageData = nil; - } - CFRelease(imageDestination); - return newImageData; -} - -+ (CGSize)imageSizeWithImage:(CGImageSourceRef)imageSource -{ - NSDictionary *options = @{ - (const NSString *)kCGImageSourceShouldCache: @(NO) - }; - CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (CFDictionaryRef)options); - NSNumber *width = [(__bridge NSDictionary *)properties objectForKey:(const NSString *)kCGImagePropertyPixelWidth]; - NSNumber *height = [(__bridge NSDictionary *)properties objectForKey:(const NSString *)kCGImagePropertyPixelHeight]; - CGSize size = CGSizeMake([width floatValue], [height floatValue]); - CFRelease(properties); - return size; -} - -@end diff --git a/WebDriverAgentLib/Utilities/FBImageIOScaler.h b/WebDriverAgentLib/Utilities/FBImageProcessor.h similarity index 61% rename from WebDriverAgentLib/Utilities/FBImageIOScaler.h rename to WebDriverAgentLib/Utilities/FBImageProcessor.h index d5801142a..a350bd9ce 100644 --- a/WebDriverAgentLib/Utilities/FBImageIOScaler.h +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.h @@ -10,6 +10,8 @@ #import #import +@class UTType; + NS_ASSUME_NONNULL_BEGIN // Those values define the allowed ranges for the scaling factor and compression quality settings @@ -18,45 +20,36 @@ extern const CGFloat FBMaxScalingFactor; extern const CGFloat FBMinCompressionQuality; extern const CGFloat FBMaxCompressionQuality; -@interface FBImageIOScaler : NSObject +@interface FBImageProcessor : NSObject /** Puts the passed image on the queue and dispatches a scaling operation. If there is already a image on the queue it will be replaced with the new one @param image The image to scale down - @param uti Either kUTTypePNG or kUTTypeJPEG @param completionHandler called after successfully scaling down an image @param scalingFactor the scaling factor in range 0.01..1.0. A value of 1.0 won't perform scaling at all - @param compressionQuality the compression quality in range 0.0..1.0 (0.0 for max. compression and 1.0 for lossless compression) - Only applicable for kUTTypeJPEG */ -- (void)submitImage:(NSData *)image - uti:(NSString *)uti - scalingFactor:(CGFloat)scalingFactor - compressionQuality:(CGFloat)compressionQuality - completionHandler:(void (^)(NSData *))completionHandler; +- (void)submitImageData:(NSData *)image + scalingFactor:(CGFloat)scalingFactor + completionHandler:(void (^)(NSData *))completionHandler; /** Scales and crops the source image @param image The source image data - @param uti Either kUTTypePNG or kUTTypeJPEG - @param rect The cropping rectange for the screenshot. The value is expected to be non-scaled one - since it happens after scaling/orientation change. - CGRectNull could be used to take a screenshot of the full screen. + @param uti Either UTTypePNG or UTTypeJPEG @param scalingFactor Scaling factor in range 0.01..1.0. A value of 1.0 won't perform scaling at all @param compressionQuality the compression quality in range 0.0..1.0 (0.0 for max. compression and 1.0 for lossless compression). Only works if UTI is set to kUTTypeJPEG @param error The actual error instance if the returned result is nil @returns Processed image data compressed according to the given UTI or nil in case of a failure */ -- (nullable NSData *)scaledImageWithImage:(NSData *)image - uti:(NSString *)uti - rect:(CGRect)rect - scalingFactor:(CGFloat)scalingFactor - compressionQuality:(CGFloat)compressionQuality - error:(NSError **)error; +- (nullable NSData *)scaledImageWithData:(NSData *)image + uti:(UTType *)uti + scalingFactor:(CGFloat)scalingFactor + compressionQuality:(CGFloat)compressionQuality + error:(NSError **)error; @end diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m new file mode 100644 index 000000000..16601ce8e --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "FBImageProcessor.h" + +#import +#import +@import UniformTypeIdentifiers; + +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBImageUtils.h" +#import "FBLogger.h" + +const CGFloat FBMinScalingFactor = 0.01f; +const CGFloat FBMaxScalingFactor = 1.0f; +const CGFloat FBMinCompressionQuality = 0.0f; +const CGFloat FBMaxCompressionQuality = 1.0f; + +@interface FBImageProcessor () + +@property (nonatomic) NSData *nextImage; +@property (nonatomic, readonly) NSLock *nextImageLock; +@property (nonatomic, readonly) dispatch_queue_t scalingQueue; + +@end + +@implementation FBImageProcessor + +- (id)init +{ + self = [super init]; + if (self) { + _nextImageLock = [[NSLock alloc] init]; + _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL); + } + return self; +} + +- (void)submitImageData:(NSData *)image + scalingFactor:(CGFloat)scalingFactor + completionHandler:(void (^)(NSData *))completionHandler +{ + [self.nextImageLock lock]; + if (self.nextImage != nil) { + [FBLogger verboseLog:@"Discarding screenshot"]; + } + self.nextImage = image; + [self.nextImageLock unlock]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcompletion-handler" + dispatch_async(self.scalingQueue, ^{ + [self.nextImageLock lock]; + NSData *nextImageData = self.nextImage; + self.nextImage = nil; + [self.nextImageLock unlock]; + if (nextImageData == nil) { + return; + } + + // We do not want this value to be too high because then we get images larger in size than original ones + // Although, we also don't want to lose too much of the quality on recompression + CGFloat recompressionQuality = MAX(0.9, + MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData + scalingFactor:scalingFactor + uti:UTTypeJPEG + compressionQuality:recompressionQuality + // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata + // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 + fixOrientation:FBConfiguration.mjpegShouldFixOrientation + desiredOrientation:nil]; + completionHandler(thumbnailData ?: nextImageData); + }); +#pragma clang diagnostic pop +} + ++ (nullable NSData *)fixedImageDataWithImageData:(NSData *)imageData + scalingFactor:(CGFloat)scalingFactor + uti:(UTType *)uti + compressionQuality:(CGFloat)compressionQuality + fixOrientation:(BOOL)fixOrientation + desiredOrientation:(nullable NSNumber *)orientation +{ + scalingFactor = MAX(FBMinScalingFactor, MIN(FBMaxScalingFactor, scalingFactor)); + BOOL usesScaling = scalingFactor > 0.0 && scalingFactor < FBMaxScalingFactor; + @autoreleasepool { + if (!usesScaling && !fixOrientation) { + return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality); + } + + UIImage *image = [UIImage imageWithData:imageData]; + if (nil == image + || ((image.imageOrientation == UIImageOrientationUp || !fixOrientation) && !usesScaling)) { + return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality); + } + + CGSize scaledSize = CGSizeMake(image.size.width * scalingFactor, image.size.height * scalingFactor); + if (!fixOrientation && usesScaling) { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block UIImage *result = nil; + [image prepareThumbnailOfSize:scaledSize + completionHandler:^(UIImage * _Nullable thumbnail) { + result = thumbnail; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (nil == result) { + return [uti conformsToType:UTTypePNG] ? FBToPngData(imageData) : FBToJpegData(imageData, compressionQuality); + } + return [uti conformsToType:UTTypePNG] + ? UIImagePNGRepresentation(result) + : UIImageJPEGRepresentation(result, compressionQuality); + } + + UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init]; + format.scale = scalingFactor; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize + format:format]; + UIImageOrientation desiredOrientation = orientation == nil + ? image.imageOrientation + : (UIImageOrientation)orientation.integerValue; + UIImage *uiImage = [UIImage imageWithCGImage:(CGImageRef)image.CGImage + scale:image.scale + orientation:desiredOrientation]; + return [uti conformsToType:UTTypePNG] + ? [renderer PNGDataWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { + [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)]; + }] + : [renderer JPEGDataWithCompressionQuality:compressionQuality + actions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { + [uiImage drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)]; + }]; + } +} + +- (nullable NSData *)scaledImageWithData:(NSData *)imageData + uti:(UTType *)uti + scalingFactor:(CGFloat)scalingFactor + compressionQuality:(CGFloat)compressionQuality + error:(NSError **)error +{ + NSNumber *orientation = nil; +#if !TARGET_OS_TV + if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortrait) { + orientation = @(UIImageOrientationUp); + } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationPortraitUpsideDown) { + orientation = @(UIImageOrientationDown); + } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeLeft) { + orientation = @(UIImageOrientationRight); + } else if (FBConfiguration.screenshotOrientation == UIInterfaceOrientationLandscapeRight) { + orientation = @(UIImageOrientationLeft); + } +#endif + NSData *resultData = [self.class fixedImageDataWithImageData:imageData + scalingFactor:scalingFactor + uti:uti + compressionQuality:compressionQuality + fixOrientation:YES + desiredOrientation:orientation]; + return resultData ?: imageData; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBImageUtils.h b/WebDriverAgentLib/Utilities/FBImageUtils.h index 96ea15a6b..15a04f6fe 100644 --- a/WebDriverAgentLib/Utilities/FBImageUtils.h +++ b/WebDriverAgentLib/Utilities/FBImageUtils.h @@ -17,4 +17,10 @@ BOOL FBIsPngImage(NSData *imageData); /*! Converts the given image data to a PNG representation if necessary */ NSData *_Nullable FBToPngData(NSData *imageData); +/*! Returns YES if the data contains a JPG image */ +BOOL FBIsJpegImage(NSData *imageData); + +/*! Converts the given image data to a JPG representation if necessary */ +NSData *_Nullable FBToJpegData(NSData *imageData, CGFloat compressionQuality); + NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBImageUtils.m b/WebDriverAgentLib/Utilities/FBImageUtils.m index 5983ddc30..b1d2f9b5b 100644 --- a/WebDriverAgentLib/Utilities/FBImageUtils.m +++ b/WebDriverAgentLib/Utilities/FBImageUtils.m @@ -12,8 +12,11 @@ #import "FBMacros.h" #import "FBConfiguration.h" +// https://en.wikipedia.org/wiki/List_of_file_signatures static uint8_t PNG_MAGIC[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; static const NSUInteger PNG_MAGIC_LEN = 8; +static uint8_t JPG_MAGIC[] = { 0xff, 0xd8, 0xff }; +static const NSUInteger JPG_MAGIC_LEN = 3; BOOL FBIsPngImage(NSData *imageData) { @@ -45,3 +48,34 @@ BOOL FBIsPngImage(NSData *imageData) UIImage *image = [UIImage imageWithData:imageData]; return nil == image ? nil : (NSData *)UIImagePNGRepresentation(image); } + +BOOL FBIsJpegImage(NSData *imageData) +{ + if (nil == imageData || [imageData length] < JPG_MAGIC_LEN) { + return NO; + } + + static NSData* jpgMagicStartData = nil; + static dispatch_once_t onceJpgToken; + dispatch_once(&onceJpgToken, ^{ + jpgMagicStartData = [NSData dataWithBytesNoCopy:(void*)JPG_MAGIC length:JPG_MAGIC_LEN freeWhenDone:NO]; + }); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wassign-enum" + NSRange range = [imageData rangeOfData:jpgMagicStartData options:kNilOptions range:NSMakeRange(0, JPG_MAGIC_LEN)]; +#pragma clang diagnostic pop + return range.location != NSNotFound; +} + +NSData *FBToJpegData(NSData *imageData, CGFloat compressionQuality) { + if (nil == imageData || [imageData length] < JPG_MAGIC_LEN) { + return nil; + } + if (FBIsJpegImage(imageData)) { + return imageData; + } + + UIImage *image = [UIImage imageWithData:imageData]; + return nil == image ? nil : (NSData *)UIImageJPEGRepresentation(image, compressionQuality); +} diff --git a/WebDriverAgentLib/Utilities/FBKeyboard.h b/WebDriverAgentLib/Utilities/FBKeyboard.h index cbe1180cd..5a16e1aed 100644 --- a/WebDriverAgentLib/Utilities/FBKeyboard.h +++ b/WebDriverAgentLib/Utilities/FBKeyboard.h @@ -13,6 +13,16 @@ NS_ASSUME_NONNULL_BEGIN @interface FBKeyboard : NSObject +#if (!TARGET_OS_TV && __clang_major__ >= 15) +/** + Transforms key name to its string representation, which could be used with XCTest + + @param name one of available keyboard key names defined in https://developer.apple.com/documentation/xctest/xcuikeyboardkey?language=objc + @return Either the key value or nil if no matches have been found + */ ++ (nullable NSString *)keyValueForName:(NSString *)name; +#endif + /** Types a string into active element. There must be element with keyboard focus; otherwise an error is raised. diff --git a/WebDriverAgentLib/Utilities/FBKeyboard.m b/WebDriverAgentLib/Utilities/FBKeyboard.m index 3f3a0b016..0cf9964c6 100644 --- a/WebDriverAgentLib/Utilities/FBKeyboard.m +++ b/WebDriverAgentLib/Utilities/FBKeyboard.m @@ -63,8 +63,7 @@ + (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInt }]; XCUIElement *firstKey = [[app.keyboard descendantsMatchingType:XCUIElementTypeKey] matchingPredicate:keySearchPredicate].allElementsBoundByIndex.firstObject; - return firstKey.exists - && (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"13.0") ? firstKey.hittable : firstKey.fb_isVisible); + return firstKey.exists && firstKey.hittable; }; NSString* errMessage = @"The on-screen keyboard must be present to send keys"; if (timeout <= 0) { @@ -81,4 +80,69 @@ + (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInt error:error]; } +#if (!TARGET_OS_TV && __clang_major__ >= 15) + ++ (NSString *)keyValueForName:(NSString *)name +{ + static dispatch_once_t onceKeys; + static NSDictionary *keysMapping; + dispatch_once(&onceKeys, ^{ + keysMapping = @{ + @"XCUIKeyboardKeyDelete": XCUIKeyboardKeyDelete, + @"XCUIKeyboardKeyReturn": XCUIKeyboardKeyReturn, + @"XCUIKeyboardKeyEnter": XCUIKeyboardKeyEnter, + @"XCUIKeyboardKeyTab": XCUIKeyboardKeyTab, + @"XCUIKeyboardKeySpace": XCUIKeyboardKeySpace, + @"XCUIKeyboardKeyEscape": XCUIKeyboardKeyEscape, + + @"XCUIKeyboardKeyUpArrow": XCUIKeyboardKeyUpArrow, + @"XCUIKeyboardKeyDownArrow": XCUIKeyboardKeyDownArrow, + @"XCUIKeyboardKeyLeftArrow": XCUIKeyboardKeyLeftArrow, + @"XCUIKeyboardKeyRightArrow": XCUIKeyboardKeyRightArrow, + + @"XCUIKeyboardKeyF1": XCUIKeyboardKeyF1, + @"XCUIKeyboardKeyF2": XCUIKeyboardKeyF2, + @"XCUIKeyboardKeyF3": XCUIKeyboardKeyF3, + @"XCUIKeyboardKeyF4": XCUIKeyboardKeyF4, + @"XCUIKeyboardKeyF5": XCUIKeyboardKeyF5, + @"XCUIKeyboardKeyF6": XCUIKeyboardKeyF6, + @"XCUIKeyboardKeyF7": XCUIKeyboardKeyF7, + @"XCUIKeyboardKeyF8": XCUIKeyboardKeyF8, + @"XCUIKeyboardKeyF9": XCUIKeyboardKeyF9, + @"XCUIKeyboardKeyF10": XCUIKeyboardKeyF10, + @"XCUIKeyboardKeyF11": XCUIKeyboardKeyF11, + @"XCUIKeyboardKeyF12": XCUIKeyboardKeyF12, + @"XCUIKeyboardKeyF13": XCUIKeyboardKeyF13, + @"XCUIKeyboardKeyF14": XCUIKeyboardKeyF14, + @"XCUIKeyboardKeyF15": XCUIKeyboardKeyF15, + @"XCUIKeyboardKeyF16": XCUIKeyboardKeyF16, + @"XCUIKeyboardKeyF17": XCUIKeyboardKeyF17, + @"XCUIKeyboardKeyF18": XCUIKeyboardKeyF18, + @"XCUIKeyboardKeyF19": XCUIKeyboardKeyF19, + + @"XCUIKeyboardKeyForwardDelete": XCUIKeyboardKeyForwardDelete, + @"XCUIKeyboardKeyHome": XCUIKeyboardKeyHome, + @"XCUIKeyboardKeyEnd": XCUIKeyboardKeyEnd, + @"XCUIKeyboardKeyPageUp": XCUIKeyboardKeyPageUp, + @"XCUIKeyboardKeyPageDown": XCUIKeyboardKeyPageDown, + @"XCUIKeyboardKeyClear": XCUIKeyboardKeyClear, + @"XCUIKeyboardKeyHelp": XCUIKeyboardKeyHelp, + + @"XCUIKeyboardKeyCapsLock": XCUIKeyboardKeyCapsLock, + @"XCUIKeyboardKeyShift": XCUIKeyboardKeyShift, + @"XCUIKeyboardKeyControl": XCUIKeyboardKeyControl, + @"XCUIKeyboardKeyOption": XCUIKeyboardKeyOption, + @"XCUIKeyboardKeyCommand": XCUIKeyboardKeyCommand, + @"XCUIKeyboardKeyRightShift": XCUIKeyboardKeyRightShift, + @"XCUIKeyboardKeyRightControl": XCUIKeyboardKeyRightControl, + @"XCUIKeyboardKeyRightOption": XCUIKeyboardKeyRightOption, + @"XCUIKeyboardKeyRightCommand": XCUIKeyboardKeyRightCommand, + @"XCUIKeyboardKeySecondaryFn": XCUIKeyboardKeySecondaryFn + }; + }); + return keysMapping[name]; +} + +#endif + @end diff --git a/WebDriverAgentLib/Utilities/FBMacros.h b/WebDriverAgentLib/Utilities/FBMacros.h index ed4e6f7a0..dae0c5faf 100644 --- a/WebDriverAgentLib/Utilities/FBMacros.h +++ b/WebDriverAgentLib/Utilities/FBMacros.h @@ -52,3 +52,7 @@ /*! Converts the given number of milliseconds into seconds */ #define FBMillisToSeconds(ms) ((ms) / 1000.0) + +/*! Converts boolean value to its string representation */ +#define FBBoolToString(b) ((b) ? @"true" : @"false") + diff --git a/WebDriverAgentLib/Utilities/FBMathUtils.h b/WebDriverAgentLib/Utilities/FBMathUtils.h index 222a2ef67..2d52d3e6d 100644 --- a/WebDriverAgentLib/Utilities/FBMathUtils.h +++ b/WebDriverAgentLib/Utilities/FBMathUtils.h @@ -32,12 +32,6 @@ BOOL FBSizeFuzzyEqualToSize(CGSize size1, CGSize size2, CGFloat threshold); BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold); #if !TARGET_OS_TV -/*! Inverts point if necessary to match location on screen */ -CGPoint FBInvertPointForApplication(CGPoint point, CGSize screenSize, UIInterfaceOrientation orientation); - -/*! Inverts offset if necessary to match screen orientation */ -CGPoint FBInvertOffsetForOrientation(CGPoint offset, UIInterfaceOrientation orientation); - /*! Inverts size if necessary to match current screen orientation */ CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation); #endif diff --git a/WebDriverAgentLib/Utilities/FBMathUtils.m b/WebDriverAgentLib/Utilities/FBMathUtils.m index 0cff32fcf..44ee257dc 100644 --- a/WebDriverAgentLib/Utilities/FBMathUtils.m +++ b/WebDriverAgentLib/Utilities/FBMathUtils.m @@ -46,35 +46,6 @@ BOOL FBRectFuzzyEqualToRect(CGRect rect1, CGRect rect2, CGFloat threshold) } #if !TARGET_OS_TV -CGPoint FBInvertPointForApplication(CGPoint point, CGSize screenSize, UIInterfaceOrientation orientation) -{ - switch (orientation) { - case UIInterfaceOrientationUnknown: - case UIInterfaceOrientationPortrait: - return point; - case UIInterfaceOrientationPortraitUpsideDown: - return CGPointMake(screenSize.width - point.x, screenSize.height - point.y); - case UIInterfaceOrientationLandscapeLeft: - return CGPointMake(point.y, MAX(screenSize.width, screenSize.height) - point.x); - case UIInterfaceOrientationLandscapeRight: - return CGPointMake(MIN(screenSize.width, screenSize.height) - point.y, point.x); - } -} - -CGPoint FBInvertOffsetForOrientation(CGPoint offset, UIInterfaceOrientation orientation) -{ - switch (orientation) { - case UIInterfaceOrientationUnknown: - case UIInterfaceOrientationPortrait: - return offset; - case UIInterfaceOrientationPortraitUpsideDown: - return CGPointMake(-offset.x, -offset.y); - case UIInterfaceOrientationLandscapeLeft: - return CGPointMake(offset.y, -offset.x); - case UIInterfaceOrientationLandscapeRight: - return CGPointMake(-offset.y, offset.x); - } -} CGSize FBAdjustDimensionsForApplication(CGSize actualSize, UIInterfaceOrientation orientation) { diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index 2dbcf04b8..69dd8649d 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -11,14 +11,15 @@ #import -#import +@import UniformTypeIdentifiers; #import "GCDAsyncSocket.h" #import "FBApplication.h" #import "FBConfiguration.h" #import "FBLogger.h" #import "FBScreenshot.h" -#import "FBImageIOScaler.h" +#import "FBImageProcessor.h" +#import "FBImageUtils.h" #import "XCUIScreen.h" static const NSUInteger MAX_FPS = 60; @@ -41,8 +42,7 @@ @interface FBMjpegServer() @property (nonatomic, readonly) dispatch_queue_t backgroundQueue; @property (nonatomic, readonly) NSMutableArray *listeningClients; -@property (nonatomic, readonly) mach_timebase_info_data_t timebaseInfo; -@property (nonatomic, readonly) FBImageIOScaler *imageScaler; +@property (nonatomic, readonly) FBImageProcessor *imageProcessor; @property (nonatomic, readonly) long long mainScreenID; @end @@ -247,11 +247,10 @@ - (instancetype)init _listeningClients = [NSMutableArray array]; dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0); _backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes); - mach_timebase_info(&_timebaseInfo); dispatch_async(_backgroundQueue, ^{ [self streamScreenshot]; }); - _imageScaler = [[FBImageIOScaler alloc] init]; + _imageProcessor = [[FBImageProcessor alloc] init]; _mainScreenID = [XCUIScreen.mainScreen displayID]; } return self; @@ -259,8 +258,8 @@ - (instancetype)init - (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted { - uint64_t timeElapsed = mach_absolute_time() - timeStarted; - int64_t nextTickDelta = timerInterval - timeElapsed * self.timebaseInfo.numer / self.timebaseInfo.denom; + uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; + int64_t nextTickDelta = timerInterval - timeElapsed; if (nextTickDelta > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{ [self streamScreenshot]; @@ -275,14 +274,9 @@ - (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:( - (void)streamScreenshot { - if (![self.class canStreamScreenshots]) { - [FBLogger log:@"MJPEG server cannot start because the current iOS version is not supported"]; - return; - } - NSUInteger framerate = FBConfiguration.mjpegServerFramerate; uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC); - uint64_t timeStarted = mach_absolute_time(); + uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); @synchronized (self.listeningClients) { if (0 == self.listeningClients.count) { [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; @@ -290,16 +284,12 @@ - (void)streamScreenshot } } - CGFloat scalingFactor = [FBConfiguration mjpegScalingFactor] / 100.0f; - BOOL usesScaling = fabs(FBMaxScalingFactor - scalingFactor) > DBL_EPSILON; - CGFloat compressionQuality = FBConfiguration.mjpegServerScreenshotQuality / 100.0f; - // If scaling is applied we perform another JPEG compression after scaling - // To get the desired compressionQuality we need to do a lossless compression here - CGFloat screenshotCompressionQuality = usesScaling ? FBMaxCompressionQuality : compressionQuality; NSError *error; + CGFloat compressionQuality = MAX(FBMinCompressionQuality, + MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); NSData *screenshotData = [FBScreenshot takeInOriginalResolutionWithScreenID:self.mainScreenID - compressionQuality:screenshotCompressionQuality - uti:(__bridge id)kUTTypeJPEG + compressionQuality:compressionQuality + uti:UTTypeJPEG timeout:FRAME_TIMEOUT error:&error]; if (nil == screenshotData) { @@ -308,23 +298,18 @@ - (void)streamScreenshot return; } - if (usesScaling) { - [self.imageScaler submitImage:screenshotData - uti:(__bridge id)kUTTypeJPEG - scalingFactor:scalingFactor - compressionQuality:compressionQuality - completionHandler:^(NSData * _Nonnull scaled) { - [self sendScreenshot:scaled]; - }]; - } else { - [self sendScreenshot:screenshotData]; - } + CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0; + [self.imageProcessor submitImageData:screenshotData + scalingFactor:scalingFactor + completionHandler:^(NSData * _Nonnull scaled) { + [self sendScreenshot:scaled]; + }]; [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; } - (void)sendScreenshot:(NSData *)screenshotData { - NSString *chunkHeader = [NSString stringWithFormat:@"--BoundaryString\r\nContent-type: image/jpg\r\nContent-Length: %@\r\n\r\n", @(screenshotData.length)]; + NSString *chunkHeader = [NSString stringWithFormat:@"--BoundaryString\r\nContent-type: image/jpeg\r\nContent-Length: %@\r\n\r\n", @(screenshotData.length)]; NSMutableData *chunk = [[chunkHeader dataUsingEncoding:NSUTF8StringEncoding] mutableCopy]; // NSString *screenshot = [screenshotData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; // screenshotData = [screenshot dataUsingEncoding:kCFStringEncodingUTF8]; @@ -337,11 +322,6 @@ - (void)sendScreenshot:(NSData *)screenshotData { } } -+ (BOOL)canStreamScreenshots -{ - return [FBScreenshot isNewScreenshotAPISupported]; -} - - (void)didClientConnect:(GCDAsyncSocket *)newClient { [FBLogger logFmt:@"Got screenshots broadcast client connection at %@:%d", newClient.connectedHost, newClient.connectedPort]; diff --git a/WebDriverAgentLib/Utilities/FBPasteboard.m b/WebDriverAgentLib/Utilities/FBPasteboard.m index 149ad4f01..6340efff3 100644 --- a/WebDriverAgentLib/Utilities/FBPasteboard.m +++ b/WebDriverAgentLib/Utilities/FBPasteboard.m @@ -9,7 +9,16 @@ #import "FBPasteboard.h" +#import +#import "FBAlert.h" +#import "FBApplication.h" #import "FBErrorBuilder.h" +#import "FBMacros.h" +#import "XCUIApplication+FBAlert.h" + +#define ALERT_TIMEOUT_SEC 30 +// Must not be less than FB_MONTORING_INTERVAL in FBAlertsMonitor +#define ALERT_CHECK_INTERVAL_SEC 2 #if !TARGET_OS_TV @implementation FBPasteboard @@ -50,21 +59,100 @@ + (BOOL)setData:(NSData *)data forType:(NSString *)type error:(NSError **)error return YES; } ++ (nullable id)pasteboardContentForItem:(NSString *)item + instance:(UIPasteboard *)pbInstance + timeout:(NSTimeInterval)timeout + error:(NSError **)error +{ + SEL selector = NSSelectorFromString(item); + NSMethodSignature *methodSignature = [pbInstance methodSignatureForSelector:selector]; + if (nil == methodSignature) { + NSString *description = [NSString stringWithFormat:@"Cannot retrieve '%@' from a UIPasteboard instance", item]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:selector]; + [invocation setTarget:pbInstance]; + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + [invocation invoke]; + id __unsafe_unretained result; + [invocation getReturnValue:&result]; + return result; + } + + // https://github.com/appium/appium/issues/17392 + __block id pasteboardContent; + dispatch_queue_t backgroundQueue = dispatch_queue_create("GetPasteboard", NULL); + __block BOOL didFinishGetPasteboard = NO; + dispatch_async(backgroundQueue, ^{ + [invocation invoke]; + id __unsafe_unretained result; + [invocation getReturnValue:&result]; + pasteboardContent = result; + didFinishGetPasteboard = YES; + }); + uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); + while (!didFinishGetPasteboard) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:ALERT_CHECK_INTERVAL_SEC]]; + if (didFinishGetPasteboard) { + break; + } + + XCUIElement *alertElement = FBApplication.fb_systemApplication.fb_alertElement; + if (nil != alertElement) { + FBAlert *alert = [FBAlert alertWithElement:alertElement]; + [alert acceptWithError:nil]; + } + uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; + if (timeElapsed / NSEC_PER_SEC > timeout) { + NSString *description = [NSString stringWithFormat:@"Cannot handle pasteboard alert within %@s timeout", @(timeout)]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; + } + } + return pasteboardContent; +} + + (NSData *)dataForType:(NSString *)type error:(NSError **)error { UIPasteboard *pb = UIPasteboard.generalPasteboard; if ([type.lowercaseString isEqualToString:@"plaintext"]) { if (pb.hasStrings) { - return [[pb.strings componentsJoinedByString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]; + id result = [self.class pasteboardContentForItem:@"strings" + instance:pb + timeout:ALERT_TIMEOUT_SEC + error:error + ]; + return nil == result + ? nil + : [[(NSArray *)result componentsJoinedByString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]; } } else if ([type.lowercaseString isEqualToString:@"image"]) { if (pb.hasImages) { - return UIImagePNGRepresentation((UIImage *)pb.image); + id result = [self.class pasteboardContentForItem:@"image" + instance:pb + timeout:ALERT_TIMEOUT_SEC + error:error + ]; + return nil == result ? nil : UIImagePNGRepresentation((UIImage *)result); } } else if ([type.lowercaseString isEqualToString:@"url"]) { if (pb.hasURLs) { + id result = [self.class pasteboardContentForItem:@"URLs" + instance:pb + timeout:ALERT_TIMEOUT_SEC + error:error + ]; + if (nil == result) { + return nil; + } NSMutableArray *urls = [NSMutableArray array]; - for (NSURL *url in pb.URLs) { + for (NSURL *url in (NSArray *)result) { if (nil != url.absoluteString) { [urls addObject:(id)url.absoluteString]; } diff --git a/WebDriverAgentLib/Utilities/FBProtocolHelpers.h b/WebDriverAgentLib/Utilities/FBProtocolHelpers.h index 5db7bb756..eff0a518b 100644 --- a/WebDriverAgentLib/Utilities/FBProtocolHelpers.h +++ b/WebDriverAgentLib/Utilities/FBProtocolHelpers.h @@ -12,13 +12,12 @@ NS_ASSUME_NONNULL_BEGIN /** - Inserts element uuid into the response dictionary + Prepares an element dictionary, which could be then used in hybrid W3C/JWP responses - @param dst The target dictionary. It is NOT mutated @param element Either element identifier or element object itself - @returns The changed dictionary + @returns The resulting dictionary */ -NSDictionary *FBInsertElement(NSDictionary *dst, id element); +NSDictionary *FBToElementDict(id element); /** Extracts element uuid from dictionary diff --git a/WebDriverAgentLib/Utilities/FBProtocolHelpers.m b/WebDriverAgentLib/Utilities/FBProtocolHelpers.m index 287cae575..3cd037586 100644 --- a/WebDriverAgentLib/Utilities/FBProtocolHelpers.m +++ b/WebDriverAgentLib/Utilities/FBProtocolHelpers.m @@ -20,12 +20,12 @@ static NSString *const FIRST_MATCH_KEY = @"firstMatch"; -NSDictionary *FBInsertElement(NSDictionary *dst, id element) +NSDictionary *FBToElementDict(id element) { - NSMutableDictionary *result = dst.mutableCopy; - result[W3C_ELEMENT_KEY] = element; - result[JSONWP_ELEMENT_KEY] = element; - return result.copy; + return @{ + W3C_ELEMENT_KEY: element, + JSONWP_ELEMENT_KEY: element + }; } id FBExtractElement(NSDictionary *src) diff --git a/WebDriverAgentLib/Utilities/FBRuntimeUtils.m b/WebDriverAgentLib/Utilities/FBRuntimeUtils.m index ef4e23d97..bb08de7e3 100644 --- a/WebDriverAgentLib/Utilities/FBRuntimeUtils.m +++ b/WebDriverAgentLib/Utilities/FBRuntimeUtils.m @@ -47,7 +47,7 @@ static NSString *sdkVersion = nil; static dispatch_once_t onceSdkVersionToken; -NSString * _Nullable FBSDKVersion() +NSString * _Nullable FBSDKVersion(void) { dispatch_once(&onceSdkVersionToken, ^{ NSString *sdkName = [[NSBundle mainBundle] infoDictionary][@"DTSDKName"]; diff --git a/WebDriverAgentLib/Utilities/FBScreen.m b/WebDriverAgentLib/Utilities/FBScreen.m index 1ca356b83..e2b25855b 100644 --- a/WebDriverAgentLib/Utilities/FBScreen.m +++ b/WebDriverAgentLib/Utilities/FBScreen.m @@ -22,17 +22,10 @@ + (double)scale + (CGSize)statusBarSizeForApplication:(XCUIApplication *)application { - XCUIApplication *app = application; - BOOL expectVisibleBar = YES; - + XCUIApplication *app = FBApplication.fb_systemApplication; // Since iOS 13 the status bar is no longer part of the application, it’s part of the SpringBoard - if (@available(iOS 13.0, *)) { - app = FBApplication.fb_systemApplication; - expectVisibleBar = NO; - } - - XCUIElement *mainStatusBar = app.statusBars.fb_firstMatch; - if (!mainStatusBar || (expectVisibleBar && !mainStatusBar.fb_isVisible)) { + XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject; + if (nil == mainStatusBar) { return CGSizeZero; } CGSize result = mainStatusBar.frame.size; diff --git a/WebDriverAgentLib/Utilities/FBScreenshot.h b/WebDriverAgentLib/Utilities/FBScreenshot.h index 8a86d735a..8adc1eb6a 100644 --- a/WebDriverAgentLib/Utilities/FBScreenshot.h +++ b/WebDriverAgentLib/Utilities/FBScreenshot.h @@ -8,37 +8,20 @@ */ #import +@class UTType; NS_ASSUME_NONNULL_BEGIN @interface FBScreenshot : NSObject -/** - Returns YES if the current OS SDK supports advanced screenshoting APIs (added since Xcode SDK 10) - */ -+ (BOOL)isNewScreenshotAPISupported; - /** Retrieves non-scaled screenshot of the whole screen - @param quality The number in range 0-2, where 2 (JPG) is the lowest and 0 (PNG) is the highest quality. - @param error If there is an error, upon return contains an NSError object that describes the problem. - @return Device screenshot as PNG- or JPG-encoded data or nil in case of failure - */ -+ (nullable NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality - error:(NSError **)error; - -/** - Retrieves non-scaled screenshot of the particular screen rectangle - - @param quality The number in range 0-2, where 2 (JPG) is the lowest and 0 (PNG) is the highest quality. - @param rect The bounding rectange for the screenshot. The value is expected be non-scaled one. - CGRectNull could be used to take a screenshot of the full screen. + @param quality The number in range 0-3, where 0 is PNG (lossless), 3 is HEIC (lossless), 1- low quality JPEG and 2 - high quality JPEG @param error If there is an error, upon return contains an NSError object that describes the problem. - @return Device screenshot as PNG- or JPG-encoded data or nil in case of failure + @return Device screenshot as PNG-encoded data or nil in case of failure */ + (nullable NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality - rect:(CGRect)rect error:(NSError **)error; /** @@ -46,14 +29,14 @@ NS_ASSUME_NONNULL_BEGIN @param screenID The screen identifier to take the screenshot from @param compressionQuality Normalized screenshot quality value in range 0..1, where 1 is the best quality - @param uti kUTType... constant, which defines the type of the returned screenshot image + @param uti UTType... constant, which defines the type of the returned screenshot image @param timeout how much time to allow for the screenshot to be taken @param error If there is an error, upon return contains an NSError object that describes the problem. - @return Device screenshot as PNG- or JPG-encoded data or nil in case of failure + @return Device screenshot as PNG-, HEIC- or JPG-encoded data or nil in case of failure */ + (nullable NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID compressionQuality:(CGFloat)compressionQuality - uti:(NSString *)uti + uti:(UTType *)uti timeout:(NSTimeInterval)timeout error:(NSError **)error; diff --git a/WebDriverAgentLib/Utilities/FBScreenshot.m b/WebDriverAgentLib/Utilities/FBScreenshot.m index 60e467173..3d9716dea 100644 --- a/WebDriverAgentLib/Utilities/FBScreenshot.m +++ b/WebDriverAgentLib/Utilities/FBScreenshot.m @@ -9,11 +9,11 @@ #import "FBScreenshot.h" -#import +@import UniformTypeIdentifiers; #import "FBConfiguration.h" #import "FBErrorBuilder.h" -#import "FBImageIOScaler.h" +#import "FBImageProcessor.h" #import "FBLogger.h" #import "FBMacros.h" #import "FBXCodeCompatibility.h" @@ -33,16 +33,6 @@ @implementation FBScreenshot -+ (BOOL)isNewScreenshotAPISupported -{ - static dispatch_once_t newScreenshotAPISupported; - static BOOL result; - dispatch_once(&newScreenshotAPISupported, ^{ - result = [(NSObject *)[FBXCTestDaemonsProxy testRunnerProxy] respondsToSelector:@selector(_XCT_requestScreenshotOfScreenWithID:withRect:uti:compressionQuality:withReply:)]; - }); - return result; -} - + (CGFloat)compressionQualityWithQuality:(NSUInteger)quality { switch (quality) { @@ -55,79 +45,34 @@ + (CGFloat)compressionQualityWithQuality:(NSUInteger)quality } } -+ (NSString *)imageUtiWithQuality:(NSUInteger)quality ++ (UTType *)imageUtiWithQuality:(NSUInteger)quality { switch (quality) { case 1: case 2: - return (__bridge id)kUTTypeJPEG; + return UTTypeJPEG; + case 3: + return UTTypeHEIC; default: - return (__bridge id)kUTTypePNG; + return UTTypePNG; } } + (NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality - rect:(CGRect)rect error:(NSError **)error { - if ([self.class isNewScreenshotAPISupported]) { - XCUIScreen *mainScreen = XCUIScreen.mainScreen; - return [self.class takeWithScreenID:mainScreen.displayID - scale:SCREENSHOT_SCALE - compressionQuality:[self.class compressionQualityWithQuality:FBConfiguration.screenshotQuality] - rect:rect - sourceUTI:[self.class imageUtiWithQuality:FBConfiguration.screenshotQuality] - error:error]; - } - - [[[FBErrorBuilder builder] - withDescription:@"Screenshots of limited areas are only available for newer OS versions"] - buildError:error]; - return nil; -} - -+ (NSData *)takeInOriginalResolutionWithQuality:(NSUInteger)quality - error:(NSError **)error -{ - if ([self.class isNewScreenshotAPISupported]) { - XCUIScreen *mainScreen = XCUIScreen.mainScreen; - return [self.class takeWithScreenID:mainScreen.displayID - scale:SCREENSHOT_SCALE - compressionQuality:[self.class compressionQualityWithQuality:FBConfiguration.screenshotQuality] - rect:CGRectNull - sourceUTI:[self.class imageUtiWithQuality:FBConfiguration.screenshotQuality] - error:error]; - } - - id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; - __block NSData *screenshotData = nil; - __block NSError *innerError = nil; - dispatch_semaphore_t sem = dispatch_semaphore_create(0); - [proxy _XCT_requestScreenshotWithReply:^(NSData *data, NSError *screenshotError) { - if (nil == screenshotError) { - screenshotData = data; - } else { - innerError = screenshotError; - } - dispatch_semaphore_signal(sem); - }]; - if (nil != innerError && error) { - *error = innerError; - } - int64_t timeoutNs = (int64_t)(SCREENSHOT_TIMEOUT * NSEC_PER_SEC); - if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { - [[[FBErrorBuilder builder] - withDescription:[NSString stringWithFormat:@"Cannot take a screenshot within %@ timeout", formatTimeInterval(SCREENSHOT_TIMEOUT)]] - buildError:error]; - }; - return screenshotData; + XCUIScreen *mainScreen = XCUIScreen.mainScreen; + return [self.class takeWithScreenID:mainScreen.displayID + scale:SCREENSHOT_SCALE + compressionQuality:[self.class compressionQualityWithQuality:quality] + sourceUTI:[self.class imageUtiWithQuality:quality] + error:error]; } + (NSData *)takeWithScreenID:(long long)screenID scale:(CGFloat)scale compressionQuality:(CGFloat)compressionQuality - rect:(CGRect)rect - sourceUTI:(NSString *)uti + sourceUTI:(UTType *)uti error:(NSError **)error { NSData *screenshotData = [self.class takeInOriginalResolutionWithScreenID:screenID @@ -138,9 +83,8 @@ + (NSData *)takeWithScreenID:(long long)screenID if (nil == screenshotData) { return nil; } - return [[[FBImageIOScaler alloc] init] scaledImageWithImage:screenshotData - uti:(__bridge id)kUTTypePNG - rect:rect + return [[[FBImageProcessor alloc] init] scaledImageWithData:screenshotData + uti:UTTypePNG scalingFactor:1.0 / scale compressionQuality:FBMaxCompressionQuality error:error]; @@ -148,7 +92,7 @@ + (NSData *)takeWithScreenID:(long long)screenID + (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID compressionQuality:(CGFloat)compressionQuality - uti:(NSString *)uti + uti:(UTType *)uti timeout:(NSTimeInterval)timeout error:(NSError **)error { @@ -156,38 +100,23 @@ + (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID __block NSData *screenshotData = nil; __block NSError *innerError = nil; dispatch_semaphore_t sem = dispatch_semaphore_create(0); - if ([self.class shouldUseScreenshotRequestApiForProxy:(NSObject *)proxy]) { - id screnshotRequest = [self.class screenshotRequestWithScreenID:screenID - rect:CGRectNull - uti:uti - compressionQuality:compressionQuality - error:error]; - if (nil == screnshotRequest) { - return nil; - } - [proxy _XCT_requestScreenshot:screnshotRequest - withReply:^(id image, NSError *err) { - if (nil != err) { - innerError = err; - } else { - screenshotData = [image data]; - } - dispatch_semaphore_signal(sem); - }]; - } else { - [proxy _XCT_requestScreenshotOfScreenWithID:screenID - withRect:CGRectNull - uti:uti - compressionQuality:compressionQuality - withReply:^(NSData *data, NSError *err) { - if (nil != err) { - innerError = err; - } else { - screenshotData = data; - } - dispatch_semaphore_signal(sem); - }]; + id screnshotRequest = [self.class screenshotRequestWithScreenID:screenID + rect:CGRectNull + uti:uti + compressionQuality:compressionQuality + error:error]; + if (nil == screnshotRequest) { + return nil; } + [proxy _XCT_requestScreenshot:screnshotRequest + withReply:^(id image, NSError *err) { + if (nil != err) { + innerError = err; + } else { + screenshotData = [image data]; + } + dispatch_semaphore_signal(sem); + }]; int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC); if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { NSString *timeoutMsg = [NSString stringWithFormat:@"Cannot take a screenshot within %@ timeout", formatTimeInterval(SCREENSHOT_TIMEOUT)]; @@ -205,30 +134,10 @@ + (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID return screenshotData; } -+ (BOOL)shouldUseScreenshotRequestApiForProxy:(NSObject *)proxy -{ - static dispatch_once_t shouldUseSRApi; - static BOOL result; - dispatch_once(&shouldUseSRApi, ^{ - if ([proxy respondsToSelector:@selector(_XCT_requestScreenshot:withReply:)]) { -#if TARGET_OS_SIMULATOR - // Required to support simulators running iOS 14.4 and below with Xcode 12.5 and above due to unsupported API on the simulator side. - result = SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"14.5"); -#else - result = SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0"); -#endif - } else { - result = NO; - } - }); - return result; -} - -+ (nullable id)imageEncodingWithUniformTypeIdentifier:(NSString *)uti ++ (nullable id)imageEncodingWithUniformTypeIdentifier:(UTType *)uti compressionQuality:(CGFloat)compressionQuality error:(NSError **)error { - // TODO: Use native accessors after we drop the support of Xcode 12.4 and below Class imageEncodingClass = NSClassFromString(@"XCTImageEncoding"); if (nil == imageEncodingClass) { [[[FBErrorBuilder builder] @@ -236,6 +145,26 @@ + (nullable id)imageEncodingWithUniformTypeIdentifier:(NSString *)uti buildError:error]; return nil; } + + if ([uti conformsToType:UTTypeHEIC]) { + static BOOL isHeicSuppported = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SEL selector = NSSelectorFromString(@"supportsHEICImageEncoding"); + NSMethodSignature *signature = [imageEncodingClass methodSignatureForSelector:selector]; + if (nil != signature) { + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + [invocation invokeWithTarget:imageEncodingClass]; + [invocation getReturnValue:&isHeicSuppported]; + } + }); + if (!isHeicSuppported) { + [FBLogger logFmt:@"The device under test does not support HEIC image encoding. Falling back to PNG"]; + uti = UTTypePNG; + } + } + id imageEncodingAllocated = [imageEncodingClass alloc]; SEL imageEncodingConstructorSelector = NSSelectorFromString(@"initWithUniformTypeIdentifier:compressionQuality:"); if (![imageEncodingAllocated respondsToSelector:imageEncodingConstructorSelector]) { @@ -247,7 +176,8 @@ + (nullable id)imageEncodingWithUniformTypeIdentifier:(NSString *)uti NSMethodSignature *imageEncodingContructorSignature = [imageEncodingAllocated methodSignatureForSelector:imageEncodingConstructorSelector]; NSInvocation *imageEncodingInitInvocation = [NSInvocation invocationWithMethodSignature:imageEncodingContructorSignature]; [imageEncodingInitInvocation setSelector:imageEncodingConstructorSelector]; - [imageEncodingInitInvocation setArgument:&uti atIndex:2]; + NSString *utiIdentifier = uti.identifier; + [imageEncodingInitInvocation setArgument:&utiIdentifier atIndex:2]; [imageEncodingInitInvocation setArgument:&compressionQuality atIndex:3]; [imageEncodingInitInvocation invokeWithTarget:imageEncodingAllocated]; id __unsafe_unretained imageEncoding; @@ -257,11 +187,10 @@ + (nullable id)imageEncodingWithUniformTypeIdentifier:(NSString *)uti + (nullable id)screenshotRequestWithScreenID:(long long)screenID rect:(struct CGRect)rect - uti:(NSString *)uti + uti:(UTType *)uti compressionQuality:(CGFloat)compressionQuality error:(NSError **)error { - // TODO: Use native accessors after we drop the support of Xcode 12.4 and below id imageEncoding = [self.class imageEncodingWithUniformTypeIdentifier:uti compressionQuality:compressionQuality error:error]; diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h index 8235cb337..dd82d740c 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.h +++ b/WebDriverAgentLib/Utilities/FBSettings.h @@ -17,6 +17,7 @@ extern NSString* const FB_SETTING_USE_COMPACT_RESPONSES; extern NSString* const FB_SETTING_ELEMENT_RESPONSE_ATTRIBUTES; extern NSString* const FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY; extern NSString* const FB_SETTING_MJPEG_SERVER_FRAMERATE; +extern NSString* const FB_SETTING_MJPEG_FIX_ORIENTATION; extern NSString* const FB_SETTING_MJPEG_SCALING_FACTOR; extern NSString* const FB_SETTING_SCREENSHOT_QUALITY; extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION; diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m index 26bae07e0..6b12a4cd4 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.m +++ b/WebDriverAgentLib/Utilities/FBSettings.m @@ -14,6 +14,7 @@ NSString* const FB_SETTING_MJPEG_SERVER_SCREENSHOT_QUALITY = @"mjpegServerScreenshotQuality"; NSString* const FB_SETTING_MJPEG_SERVER_FRAMERATE = @"mjpegServerFramerate"; NSString* const FB_SETTING_MJPEG_SCALING_FACTOR = @"mjpegScalingFactor"; +NSString* const FB_SETTING_MJPEG_FIX_ORIENTATION = @"mjpegFixOrientation"; NSString* const FB_SETTING_SCREENSHOT_QUALITY = @"screenshotQuality"; NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION = @"keyboardAutocorrection"; NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction"; diff --git a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m index 40b2278ab..f877b67c6 100644 --- a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m +++ b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m @@ -8,6 +8,9 @@ */ #import "FBUnattachedAppLauncher.h" + +#import + #import "LSApplicationWorkspace.h" @implementation FBUnattachedAppLauncher diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m index 33cb559b9..dc5060568 100644 --- a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +++ b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m @@ -133,16 +133,16 @@ - (nullable instancetype)initWithActionItem:(NSDictionary *)acti return nil; } self.duration = durationObj.doubleValue; - NSValue *position = [self positionWithError:error]; + XCUICoordinate *position = [self positionWithError:error]; if (nil == position) { return nil; } - self.atPosition = [position CGPointValue]; + self.atPosition = position; } return self; } -- (nullable NSValue *)positionWithError:(NSError **)error +- (nullable XCUICoordinate *)positionWithError:(NSError **)error { if (nil == self.previousItem) { NSString *errorDescription = [NSString stringWithFormat:@"The '%@' action item must be preceded by %@ item", self.actionItem, FB_ACTION_ITEM_TYPE_POINTER_MOVE]; @@ -151,39 +151,32 @@ - (nullable NSValue *)positionWithError:(NSError **)error } return nil; } - return [NSValue valueWithCGPoint:self.previousItem.atPosition]; + return self.previousItem.atPosition; } -- (nullable NSValue *)hitpointWithElement:(nullable XCUIElement *)element - positionOffset:(nullable NSValue *)positionOffset - error:(NSError **)error +- (nullable XCUICoordinate *)hitpointWithElement:(nullable XCUIElement *)element + positionOffset:(nullable NSValue *)positionOffset + error:(NSError **)error { if (nil == element || nil == positionOffset) { return [super hitpointWithElement:element positionOffset:positionOffset error:error]; } // An offset relative to the element is defined - id snapshot = element.fb_isResolvedFromCache.boolValue - ? element.lastSnapshot - : element.fb_takeSnapshot; - CGRect frame = snapshot.frame; - if (CGRectIsEmpty(frame)) { + if (CGRectIsEmpty(element.frame)) { [FBLogger log:self.application.fb_descriptionRepresentation]; - NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable", [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_description]; + NSString *description = [NSString stringWithFormat:@"The element '%@' is not visible on the screen and thus is not interactable", + element.description]; if (error) { *error = [[FBErrorBuilder.builder withDescription:description] build]; } return nil; } - CGRect visibleFrame = snapshot.visibleFrame; - frame = CGRectIsEmpty(visibleFrame) ? frame : visibleFrame; + // W3C standard requires that relative element coordinates start at the center of the element's rectangle - CGPoint hitPoint = CGPointMake(frame.origin.x + frame.size.width / 2, frame.origin.y + frame.size.height / 2); - CGPoint offsetValue = [positionOffset CGPointValue]; - hitPoint = CGPointMake(hitPoint.x + offsetValue.x, hitPoint.y + offsetValue.y); + CGVector offset = CGVectorMake(positionOffset.CGPointValue.x, positionOffset.CGPointValue.y); // TODO: Shall we throw an exception if hitPoint is out of the element frame? - hitPoint = [self fixedHitPointWith:hitPoint forSnapshot:snapshot]; - return [NSValue valueWithCGPoint:hitPoint]; + return [[element coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] coordinateWithOffset:offset]; } @end @@ -220,7 +213,7 @@ + (NSString *)actionName } } if (nil == self.pressure) { - XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition + XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint offset:FBMillisToSeconds(self.offset)]; return @[result]; } @@ -247,7 +240,7 @@ + (NSString *)actionName @implementation FBPointerMoveItem -- (nullable NSValue *)positionWithError:(NSError **)error +- (nullable XCUICoordinate *)positionWithError:(NSError **)error { static NSArray *supportedOriginTypes; static dispatch_once_t onceToken; @@ -295,12 +288,9 @@ - (nullable NSValue *)positionWithError:(NSError **)error } return nil; } - CGPoint recentPosition = self.previousItem.atPosition; - CGPoint offsetRelativeToRecentPosition = (nil == x && nil == y) ? CGPointMake(0.0, 0.0) : CGPointMake(x.floatValue, y.floatValue); - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0")) { - offsetRelativeToRecentPosition = FBInvertOffsetForOrientation(offsetRelativeToRecentPosition, self.application.interfaceOrientation); - } - return [NSValue valueWithCGPoint:CGPointMake(recentPosition.x + offsetRelativeToRecentPosition.x, recentPosition.y + offsetRelativeToRecentPosition.y)]; + XCUICoordinate *recentPosition = self.previousItem.atPosition; + CGVector offsetRelativeToRecentPosition = (nil == x && nil == y) ? CGVectorMake(0, 0) : CGVectorMake(x.floatValue, y.floatValue); + return [recentPosition coordinateWithOffset:offsetRelativeToRecentPosition]; } + (NSString *)actionName @@ -314,9 +304,11 @@ + (NSString *)actionName error:(NSError **)error { if (nil == eventPath) { - return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition offset:FBMillisToSeconds(self.offset + self.duration)]]; + return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint + offset:FBMillisToSeconds(self.offset + self.duration)]]; } - [eventPath moveToPoint:self.atPosition atOffset:FBMillisToSeconds(self.offset + self.duration)]; + [eventPath moveToPoint:self.atPosition.screenPoint + atOffset:FBMillisToSeconds(self.offset + self.duration)]; return @[]; } @@ -890,7 +882,7 @@ - (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error { XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:@"W3C Touch Action" - interfaceOrientation:[FBXCTestDaemonsProxy orientationWithApplication:self.application]]; + interfaceOrientation:self.application.interfaceOrientation]; NSMutableDictionary *> *actionsMapping = [NSMutableDictionary new]; NSMutableArray *actionIds = [NSMutableArray new]; for (NSDictionary *action in self.actions) { diff --git a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m index f001ea91d..907bb25b1 100644 --- a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m +++ b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m @@ -9,47 +9,14 @@ #import "FBXCAXClientProxy.h" -#import - #import "FBXCAccessibilityElement.h" -#import "FBConfiguration.h" #import "FBLogger.h" #import "FBMacros.h" -#import "FBReflectionUtils.h" -#import "XCAXClient_iOS.h" +#import "XCAXClient_iOS+FBSnapshotReqParams.h" #import "XCUIDevice.h" static id FBAXClient = nil; -@implementation XCAXClient_iOS (WebDriverAgent) - -/** - Parameters for traversing elements tree from parents to children while requesting XCElementSnapshot. - - @return dictionary with parameters for element's snapshot request - */ -- (NSDictionary *)fb_getParametersForElementSnapshot -{ - return FBConfiguration.snapshotRequestParameters; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wobjc-load-method" - -+ (void)load -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - SEL originalParametersSelector = @selector(defaultParameters); - SEL swizzledParametersSelector = @selector(fb_getParametersForElementSnapshot); - FBReplaceMethod([self class], originalParametersSelector, swizzledParametersSelector); - }); -} - -#pragma clang diagnostic pop - -@end - @implementation FBXCAXClientProxy + (instancetype)sharedClient @@ -73,14 +40,8 @@ - (BOOL)setAXTimeout:(NSTimeInterval)timeout error:(NSError **)error maxDepth:(nullable NSNumber *)maxDepth error:(NSError **)error { - NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init]; - // Mimicking XCTest framework behavior (this attribute is added by default unless it is an excludingNonModalElements query) - // See https://github.com/appium/WebDriverAgent/pull/523 - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"13.0")) { - parameters[@"snapshotKeyHonorModalViews"] = @(NO); - } + NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:self.defaultParameters]; if (nil != maxDepth) { - [parameters addEntriesFromDictionary:self.defaultParameters]; parameters[FBSnapshotMaxDepthKey] = maxDepth; } diff --git a/WebDriverAgentLib/Utilities/FBXCTestCaseImplementationFailureHoldingProxy.h b/WebDriverAgentLib/Utilities/FBXCTestCaseImplementationFailureHoldingProxy.h deleted file mode 100644 index 8bf06b2a2..000000000 --- a/WebDriverAgentLib/Utilities/FBXCTestCaseImplementationFailureHoldingProxy.h +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -@class _XCTestCaseImplementation; - -NS_ASSUME_NONNULL_BEGIN - -/** - Class that can be used to proxy existing _XCTestCaseImplementation and - prevent currently running test from being terminated on any XCTest failure - */ -@interface FBXCTestCaseImplementationFailureHoldingProxy : NSProxy - -/** - Constructor for given existing _XCTestCaseImplementation instance - */ -+ (instancetype)proxyWithXCTestCaseImplementation:(_XCTestCaseImplementation *)internalImplementation; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXCTestCaseImplementationFailureHoldingProxy.m b/WebDriverAgentLib/Utilities/FBXCTestCaseImplementationFailureHoldingProxy.m deleted file mode 100644 index 23f257416..000000000 --- a/WebDriverAgentLib/Utilities/FBXCTestCaseImplementationFailureHoldingProxy.m +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "FBXCTestCaseImplementationFailureHoldingProxy.h" - -#import - -@interface FBXCTestCaseImplementationFailureHoldingProxy () -@property (nonatomic, strong) _XCTestCaseImplementation *internalImplementation; -@end - -@implementation FBXCTestCaseImplementationFailureHoldingProxy - -+ (instancetype)proxyWithXCTestCaseImplementation:(_XCTestCaseImplementation *)internalImplementation -{ - FBXCTestCaseImplementationFailureHoldingProxy *proxy = [super alloc]; - proxy.internalImplementation = internalImplementation; - return proxy; -} - -- (id)forwardingTargetForSelector:(SEL)aSelector -{ - return self.internalImplementation; -} - -// This will prevent test from quiting on app crash or any other test failure -- (BOOL)shouldHaltWhenReceivesControl -{ - return NO; -} - -@end diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h index dd7499bcf..307edb40d 100644 --- a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h @@ -8,26 +8,33 @@ */ #import + +#if !TARGET_OS_TV +#import +#endif + #import "XCSynthesizedEventRecord.h" NS_ASSUME_NONNULL_BEGIN @protocol XCTestManager_ManagerInterface; -/** - Temporary class used to abstract interactions with TestManager daemon between Xcode 8.2.1 and Xcode 8.3-beta - */ @interface FBXCTestDaemonsProxy : NSObject + (id)testRunnerProxy; -#if !TARGET_OS_TV -+ (UIInterfaceOrientation)orientationWithApplication:(XCUIApplication *)application; -#endif - + (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error; ++ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError **)error; ++ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError **)error; + +#if !TARGET_OS_TV ++ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError **)error; ++ (nullable CLLocation *)getSimulatedLocation:(NSError **)error; ++ (BOOL)clearSimulatedLocation:(NSError **)error; +#endif + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m index 708e027f2..180689800 100644 --- a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m @@ -12,6 +12,8 @@ #import #import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBExceptions.h" #import "FBLogger.h" #import "FBRunLoopSpinner.h" #import "XCTestDriver.h" @@ -19,24 +21,61 @@ #import "XCUIApplication.h" #import "XCUIDevice.h" -@implementation FBXCTestDaemonsProxy +#define LAUNCH_APP_TIMEOUT_SEC 300 -static Class FBXCTRunnerDaemonSessionClass = nil; -static dispatch_once_t onceTestRunnerDaemonClass; +static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *)); + +static void swizzledLaunchApp(id self, SEL _cmd, NSString *path, NSString *bundleID, + NSArray *arguments, NSDictionary *environment, + void (^reply)(_Bool, NSError *)) +{ + __block BOOL isSuccessful; + __block NSError *error; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + originalLaunchAppMethod(self, _cmd, path, bundleID, arguments, environment, ^(BOOL passed, NSError *innerError) { + isSuccessful = passed; + error = innerError; + dispatch_semaphore_signal(sem); + }); + int64_t timeoutNs = (int64_t)(LAUNCH_APP_TIMEOUT_SEC * NSEC_PER_SEC); + if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { + NSString *message = [NSString stringWithFormat:@"The application '%@' cannot be launched within %d seconds timeout", + bundleID ?: path, LAUNCH_APP_TIMEOUT_SEC]; + @throw [NSException exceptionWithName:FBTimeoutException reason:message userInfo:nil]; + } + if (!isSuccessful || nil != error) { + [FBLogger logFmt:@"%@", error.description]; + NSString *message = error.description ?: [NSString stringWithFormat:@"The application '%@' is not installed on the device under test", + bundleID ?: path]; + @throw [NSException exceptionWithName:FBApplicationMissingException reason:message userInfo:nil]; + } + reply(isSuccessful, error); +} + +@implementation FBXCTestDaemonsProxy #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-load-method" + (void)load { - // XCTRunnerDaemonSession class is only available since Xcode 8.3 - dispatch_once(&onceTestRunnerDaemonClass, ^{ - FBXCTRunnerDaemonSessionClass = objc_lookUpClass("XCTRunnerDaemonSession"); - }); + [self.class swizzleLaunchApp]; } #pragma clang diagnostic pop ++ (void)swizzleLaunchApp { + Method original = class_getInstanceMethod([XCTRunnerDaemonSession class], + @selector(launchApplicationWithPath:bundleID:arguments:environment:completion:)); + if (original == nil) { + [FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"]; + return; + } + // Workaround for https://github.com/appium/WebDriverAgent/issues/702 + originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original); + method_setImplementation(original, (IMP)swizzledLaunchApp); +} + + (id)testRunnerProxy { static id proxy = nil; @@ -56,44 +95,198 @@ + (void)load + (id)retrieveTestRunnerProxy { - return ((XCTRunnerDaemonSession *)[FBXCTRunnerDaemonSessionClass sharedSession]).daemonProxy; + return ((XCTRunnerDaemonSession *)[XCTRunnerDaemonSession sharedSession]).daemonProxy; } -#if !TARGET_OS_TV -+ (UIInterfaceOrientation)orientationWithApplication:(XCUIApplication *)application ++ (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error +{ + __block NSError *innerError = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + void (^errorHandler)(NSError *) = ^(NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } + completion(); + }; + + XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) { + errorHandler(invokeError); + }; + [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) { + handlerBlock(record, invokeError); + }]; + }]; + if (nil != innerError) { + if (error) { + *error = innerError; + } + return NO; + } + return YES; +} + ++ (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError *__autoreleasing*)error { - if (nil == FBXCTRunnerDaemonSessionClass || - [[FBXCTRunnerDaemonSessionClass sharedSession] useLegacyEventCoordinateTransformationPath]) { - return application.interfaceOrientation; + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"] + buildError:error]; + return NO; } - return UIInterfaceOrientationPortrait; + + __block NSError *innerError = nil; + __block BOOL didSucceed = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session openURL:url usingApplication:bundleId completion:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return didSucceed; } -#endif -+ (BOOL)synthesizeEventWithRecord:(XCSynthesizedEventRecord *)record error:(NSError *__autoreleasing*)error ++ (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasing*)error { + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; + return NO; + } + + __block NSError *innerError = nil; __block BOOL didSucceed = NO; [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ - void (^errorHandler)(NSError *) = ^(NSError *invokeError) { - if (error) { - *error = invokeError; + [session openDefaultApplicationForURL:url completion:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; } - didSucceed = (invokeError == nil); completion(); - }; - - if (nil == FBXCTRunnerDaemonSessionClass) { - [[self testRunnerProxy] _XCT_synthesizeEvent:record completion:errorHandler]; - } else { - XCEventGeneratorHandler handlerBlock = ^(XCSynthesizedEventRecord *innerRecord, NSError *invokeError) { - errorHandler(invokeError); - }; - [[XCUIDevice.sharedDevice eventSynthesizer] synthesizeEvent:record completion:(id)^(BOOL result, NSError *invokeError) { - handlerBlock(record, invokeError); - }]; + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return didSucceed; +} + +#if !TARGET_OS_TV ++ (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; + return NO; + } + if (![session supportsLocationSimulation]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support location simulation"] + buildError:error]; + return NO; + } + + __block NSError *innerError = nil; + __block BOOL didSucceed = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session setSimulatedLocation:location completion:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return didSucceed; +} + ++ (nullable CLLocation *)getSimulatedLocation:(NSError *__autoreleasing*)error; +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(getSimulatedLocationWithReply:)]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; + return nil; + } + if (![session supportsLocationSimulation]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support location simulation"] + buildError:error]; + return nil; + } + + __block NSError *innerError = nil; + __block CLLocation *location = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session getSimulatedLocationWithReply:^(CLLocation *reply, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + location = reply; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return location; +} + ++ (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) { + if (error) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] + buildError:error]; } + return NO; + } + if (![session supportsLocationSimulation]) { + if (error) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support location simulation"] + buildError:error]; + } + return NO; + } + + __block NSError *innerError = nil; + __block BOOL didSucceed = NO; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session clearSimulatedLocationWithReply:^(bool result, NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } else { + didSucceed = result; + } + completion(); + }]; }]; + if (nil != innerError && error) { + *error = innerError; + } return didSucceed; } +#endif @end diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m index 999802aab..4d7bb621d 100644 --- a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m @@ -65,10 +65,11 @@ - (XCElementSnapshot *)fb_uniqueSnapshotWithError:(NSError **)error - (XCUIElement *)fb_firstMatch { - XCUIElement* match = FBConfiguration.useFirstMatch - ? self.firstMatch - : self.fb_allMatches.firstObject; - return [match exists] ? match : nil; + if (FBConfiguration.useFirstMatch) { + XCUIElement* match = self.firstMatch; + return [match exists] ? match : nil; + } + return self.fb_allMatches.firstObject; } - (NSArray *)fb_allMatches @@ -122,12 +123,16 @@ NSInteger FBTestmanagerdVersion(void) static NSInteger testmanagerdVersion; dispatch_once(&getTestmanagerdVersion, ^{ id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; - dispatch_semaphore_t sem = dispatch_semaphore_create(0); - [proxy _XCT_exchangeProtocolVersion:testmanagerdVersion reply:^(unsigned long long code) { - testmanagerdVersion = (NSInteger) code; - dispatch_semaphore_signal(sem); - }]; - dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC))); + if ([(NSObject *)proxy respondsToSelector:@selector(_XCT_exchangeProtocolVersion:reply:)]) { + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [proxy _XCT_exchangeProtocolVersion:testmanagerdVersion reply:^(unsigned long long code) { + testmanagerdVersion = (NSInteger) code; + completion(); + }]; + }]; + } else { + testmanagerdVersion = 0xFFFF; + } }); return testmanagerdVersion; } diff --git a/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentLib/Utilities/FBXPath.m index 00c5d44e7..eae3ee438 100644 --- a/WebDriverAgentLib/Utilities/FBXPath.m +++ b/WebDriverAgentLib/Utilities/FBXPath.m @@ -12,6 +12,7 @@ #import "FBConfiguration.h" #import "FBExceptions.h" #import "FBLogger.h" +#import "FBMacros.h" #import "FBXMLGenerationOptions.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "NSString+FBXMLSafeString.h" @@ -87,6 +88,10 @@ @interface FBIndexAttribute : FBElementAttribute @end +@interface FBHittableAttribute : FBElementAttribute + +@end + @interface FBInternalIndexAttribute : FBElementAttribute @property (nonatomic, nonnull, readonly) NSString* indexValue; @@ -273,6 +278,9 @@ + (int)xmlRepresentationWithRootElement:(id)root NSMutableSet *includedAttributes; if (nil == query) { includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes]; + // The hittable attribute is expensive to calculate for each snapshot item + // thus we only include it when requested by an xPath query + [includedAttributes removeObject:FBHittableAttribute.class]; if (nil != excludedAttributes) { for (NSString *excludedAttributeName in excludedAttributes) { for (Class supportedAttribute in FBElementAttribute.supportedAttributes) { @@ -481,6 +489,7 @@ + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)eleme FBWidthAttribute.class, FBHeightAttribute.class, FBIndexAttribute.class, + FBHittableAttribute.class, ]; } @@ -557,7 +566,7 @@ + (NSString *)name + (NSString *)valueForElement:(id)element { - return element.wdEnabled ? @"true" : @"false"; + return FBBoolToString(element.wdEnabled); } @end @@ -571,7 +580,7 @@ + (NSString *)name + (NSString *)valueForElement:(id)element { - return element.wdVisible ? @"true" : @"false"; + return FBBoolToString(element.wdVisible); } @end @@ -585,7 +594,7 @@ + (NSString *)name + (NSString *)valueForElement:(id)element { - return element.wdAccessible ? @"true" : @"false"; + return FBBoolToString(element.wdAccessible); } @end @@ -601,7 +610,7 @@ + (NSString *)name + (NSString *)valueForElement:(id)element { - return element.wdFocused ? @"true" : @"false"; + return FBBoolToString(element.wdFocused); } @end @@ -667,6 +676,20 @@ + (NSString *)valueForElement:(id)element @end +@implementation FBHittableAttribute + ++ (NSString *)name +{ + return @"hittable"; +} + ++ (NSString *)valueForElement:(id)element +{ + return FBBoolToString(element.wdHittable); +} + +@end + @implementation FBInternalIndexAttribute + (NSString *)name diff --git a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m index 96c4d71b1..8a033e707 100644 --- a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m +++ b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m @@ -52,6 +52,10 @@ + (instancetype)fb_formatSearchPredicate:(NSPredicate *)input + (instancetype)fb_snapshotBlockPredicateWithPredicate:(NSPredicate *)input { + if ([NSStringFromClass(input.class) isEqualToString:@"NSBlockPredicate"]) { + return input; + } + NSPredicate *wdPredicate = [self.class fb_formatSearchPredicate:input]; return [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * _Nullable bindings) { diff --git a/WebDriverAgentLib/WebDriverAgentLib.h b/WebDriverAgentLib/WebDriverAgentLib.h index df9b139ed..6d9daa1f6 100644 --- a/WebDriverAgentLib/WebDriverAgentLib.h +++ b/WebDriverAgentLib/WebDriverAgentLib.h @@ -46,7 +46,7 @@ FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; #import #import #import -#import #import #import #import +#import diff --git a/WebDriverAgentRunner/UITestingUITests.m b/WebDriverAgentRunner/UITestingUITests.m index c98fbd622..cc7c78073 100644 --- a/WebDriverAgentRunner/UITestingUITests.m +++ b/WebDriverAgentRunner/UITestingUITests.m @@ -26,6 +26,11 @@ + (void)setUp [FBConfiguration disableRemoteQueryEvaluation]; [FBConfiguration configureDefaultKeyboardPreferences]; [FBConfiguration disableApplicationUIInterruptionsHandling]; + if (NSProcessInfo.processInfo.environment[@"ENABLE_AUTOMATIC_SCREEN_RECORDINGS"]) { + [FBConfiguration enableScreenRecordings]; + } else { + [FBConfiguration disableScreenRecordings]; + } if (NSProcessInfo.processInfo.environment[@"ENABLE_AUTOMATIC_SCREENSHOTS"]) { [FBConfiguration enableScreenshots]; } else { diff --git a/WebDriverAgentTests/IntegrationTests/FBAlertTests.m b/WebDriverAgentTests/IntegrationTests/FBAlertTests.m index e94c4109e..a7cf04418 100644 --- a/WebDriverAgentTests/IntegrationTests/FBAlertTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBAlertTests.m @@ -15,7 +15,6 @@ #import "FBIntegrationTestCase.h" #import "FBTestMacros.h" #import "FBMacros.h" -#import "XCUIElement+FBTap.h" @interface FBAlertTests : FBIntegrationTestCase @end @@ -42,13 +41,13 @@ - (void)tearDown - (void)showApplicationAlert { - [self.testedApplication.buttons[FBShowAlertButtonName] fb_tapWithError:nil]; + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count != 0); } - (void)showApplicationSheet { - [self.testedApplication.buttons[FBShowSheetAlertButtonName] fb_tapWithError:nil]; + [self.testedApplication.buttons[FBShowSheetAlertButtonName] tap]; FBAssertWaitTillBecomesTrue(self.testedApplication.sheets.count != 0); } @@ -155,6 +154,7 @@ - (void)testNotificationAlert XCTAssertTrue([alert.text containsString:@"Notifications may include"]); } +// This test case depends on the local app permission state. - (void)testCameraRollAlert { FBAlert *alert = [FBAlert alertWithApplication:self.testedApplication]; @@ -163,10 +163,13 @@ - (void)testCameraRollAlert [self.testedApplication.buttons[@"Create Camera Roll Alert"] tap]; FBAssertWaitTillBecomesTrue(alert.isPresent); - XCTAssertTrue([alert.text containsString:@"Would Like to Access Your Photos"]); + // "Would Like to Access Your Photos" or "Would Like to Access Your Photo Library" displayes on the alert button. + XCTAssertTrue([alert.text containsString:@"Would Like to Access Your Photo"]); // iOS 15 has different UI flow if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { [[FBAlert alertWithApplication:self.testedApplication] dismissWithError:nil]; + // CI env could take longer time to show up the button, thus it needs to wait a bit. + XCTAssertTrue([self.testedApplication.buttons[@"Cancel"] waitForExistenceWithTimeout:30.0]); [self.testedApplication.buttons[@"Cancel"] tap]; } } diff --git a/WebDriverAgentTests/IntegrationTests/FBAppiumTouchActionsIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBAppiumTouchActionsIntegrationTests.m index bcfef8741..51525e435 100644 --- a/WebDriverAgentTests/IntegrationTests/FBAppiumTouchActionsIntegrationTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBAppiumTouchActionsIntegrationTests.m @@ -194,14 +194,6 @@ - (void)testTapByCoordinates - (void)testDoubleTap { - if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad && - SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - // "tap the element" does not work on iPadOS simulator after change the rotation in iOS 15 - // while selecting the element worked. (I confirmed with getting an element screenshot that - // the FBShowAlertButtonName was actually selected after changing the orientation.) - return; - } - NSArray *> *gesture = @[@{ @"action": @"tap", @@ -214,16 +206,8 @@ - (void)testDoubleTap [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeLeft]; } -// TODO: UIDeviceOrientationLandscapeRight is not so good in iOS 16? (Especially simulators) - (void)testPress { - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - // Does not work on iOS 15.0. - // The same action tap the FBShowAlertButtonName after rotating the screen - // worked with W3C actions. `.click` action did not work. - return; - } - NSArray *> *gesture = @[@{ @"action": @"press", @@ -260,6 +244,9 @@ - (void)testPress - (void)testLongPress { + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } UIDeviceOrientation orientation = UIDeviceOrientationLandscapeLeft; [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; CGRect elementFrame = self.testedApplication.buttons[FBShowAlertButtonName].frame; @@ -298,7 +285,7 @@ - (void)setUp [self launchApplication]; [self goToAttributesPage]; }); - self.pickerWheel = self.testedApplication.pickerWheels.fb_firstMatch; + self.pickerWheel = self.testedApplication.pickerWheels.allElementsBoundByIndex.firstObject; } - (void)tearDown diff --git a/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m b/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m index f0385c05f..25dc39d21 100644 --- a/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBAutoAlertsHandlerTests.m @@ -49,15 +49,11 @@ - (void)tearDown // The test is flaky on slow Travis CI - (void)disabled_testAutoAcceptingOfAlerts { - if (SYSTEM_VERSION_LESS_THAN(@"11.0")) { - return; - } - self.session = [FBSession initWithApplication:FBApplication.fb_activeApplication defaultAlertAction:@"accept"]; for (int i = 0; i < 2; i++) { - [self.testedApplication.buttons[FBShowAlertButtonName] fb_tapWithError:nil]; + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; [self.testedApplication fb_waitUntilStable]; FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); } @@ -66,15 +62,11 @@ - (void)disabled_testAutoAcceptingOfAlerts // The test is flaky on slow Travis CI - (void)disabled_testAutoDismissingOfAlerts { - if (SYSTEM_VERSION_LESS_THAN(@"11.0")) { - return; - } - self.session = [FBSession initWithApplication:FBApplication.fb_activeApplication defaultAlertAction:@"dismiss"]; for (int i = 0; i < 2; i++) { - [self.testedApplication.buttons[FBShowAlertButtonName] fb_tapWithError:nil]; + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; [self.testedApplication fb_waitUntilStable]; FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count == 0); } diff --git a/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m b/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m index 77aaae14c..ca47cdd74 100644 --- a/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBConfigurationTests.m @@ -33,11 +33,7 @@ - (void)testReduceMotion XCTAssertTrue([FBConfiguration reduceMotionEnabled]); [FBConfiguration setReduceMotionEnabled:defaultReduceMotionEnabled]; - if (isSDKVersionLessThan(@"10.0")) { - XCTAssertFalse([FBConfiguration reduceMotionEnabled]); - } else { - XCTAssertEqual([FBConfiguration reduceMotionEnabled], defaultReduceMotionEnabled); - } + XCTAssertEqual([FBConfiguration reduceMotionEnabled], defaultReduceMotionEnabled); } @end diff --git a/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m b/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m index fe0ffb1b1..13cba4e57 100644 --- a/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBElementAttributeTests.m @@ -61,7 +61,7 @@ - (void)testContainerAccessibilityAttributes - (void)testIgnoredAccessibilityAttributes { // Images are neither accessibility elements nor contain them, so both checks should fail - XCUIElement *imageElement = self.testedApplication.images.fb_firstMatch; + XCUIElement *imageElement = self.testedApplication.images.allElementsBoundByIndex.firstObject; XCTAssertTrue(imageElement.exists); XCTAssertFalse(imageElement.fb_isAccessibilityElement); XCTAssertFalse(imageElement.isWDAccessibilityContainer); @@ -160,7 +160,8 @@ - (void)testSwitchAttributes XCTAssertNil(element.wdLabel); XCTAssertEqualObjects(element.wdValue, @"1"); XCTAssertFalse(element.wdSelected); - XCTAssertTrue([element fb_tapWithError:nil]); + XCTAssertTrue(element.wdHittable); + [element tap]; XCTAssertEqualObjects(element.wdValue, @"0"); XCTAssertFalse(element.wdSelected); } diff --git a/WebDriverAgentTests/IntegrationTests/FBElementScreenshotTests.m b/WebDriverAgentTests/IntegrationTests/FBElementScreenshotTests.m deleted file mode 100644 index 08c6a9bcf..000000000 --- a/WebDriverAgentTests/IntegrationTests/FBElementScreenshotTests.m +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -#import "FBMacros.h" -#import "FBIntegrationTestCase.h" -#import "XCUIDevice+FBRotation.h" -#import "XCUIElement+FBUtilities.h" -#import "XCUIScreen.h" - -@interface FBElementScreenshotTests : FBIntegrationTestCase -@end - -@implementation FBElementScreenshotTests - -- (void)setUp -{ - [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAlertsPage]; - }); -} - -- (void)tearDown -{ - [self resetOrientation]; - [super tearDown]; -} - -- (void)testElementScreenshot -{ - [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:UIDeviceOrientationLandscapeLeft]; - XCUIElement *button = self.testedApplication.buttons[FBShowAlertButtonName]; - NSError *error = nil; - NSData *screenshotData = [button fb_screenshotWithError:&error]; - XCTAssertNotNil(screenshotData); - XCTAssertNil(error); - UIImage *image = [UIImage imageWithData:screenshotData]; - XCTAssertNotNil(image); - XCTAssertTrue(image.size.width > image.size.height); -} - -@end diff --git a/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m b/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m index caf5d9020..0a9d2f5c4 100644 --- a/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBElementSwipingTests.m @@ -67,6 +67,9 @@ - (void)testSwipeDown - (void)testSwipeDownWithVelocity { + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } [self.scrollView fb_swipeWithDirection:@"up" velocity:@2500]; FBAssertInvisibleCell(@"0"); [self.scrollView fb_swipeWithDirection:@"down" velocity:@2500]; @@ -113,6 +116,9 @@ - (void)testSwipeDown - (void)testSwipeDownWithVelocity { + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } [self.testedApplication fb_swipeWithDirection:@"up" velocity:@2500]; FBAssertInvisibleCell(@"0"); [self.testedApplication fb_swipeWithDirection:@"down" velocity:@2500]; diff --git a/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m b/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m index d63b3b3b6..ea4fa2cee 100644 --- a/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBElementVisibilityTests.m @@ -36,7 +36,7 @@ - (void)testSpringBoardIcons XCTAssertTrue(self.springboard.icons[@"Reminders"].fb_isVisible); // Check Icons on second screen screen - XCTAssertFalse(self.springboard.icons[@"IntegrationApp"].query.fb_firstMatch.fb_isVisible); + XCTAssertFalse(self.springboard.icons[@"IntegrationApp"].firstMatch.fb_isVisible); } - (void)testSpringBoardSubfolder @@ -59,7 +59,7 @@ - (void)disabled_testIconsFromSearchDashboard XCTAssertFalse(self.springboard.icons[@"Reminders"].fb_isVisible); XCTAssertFalse([[[self.springboard descendantsMatchingType:XCUIElementTypeIcon] matchingIdentifier:@"IntegrationApp"] - fb_firstMatch].fb_isVisible); + firstMatch].fb_isVisible); } - (void)testTableViewCells @@ -72,8 +72,8 @@ - (void)testTableViewCells [self launchApplication]; [self goToScrollPageWithCells:YES]; for (int i = 0 ; i < 10 ; i++) { - FBAssertWaitTillBecomesTrue(self.testedApplication.cells.allElementsBoundByAccessibilityElement[i].fb_isVisible); - FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts.allElementsBoundByAccessibilityElement[i].fb_isVisible); + FBAssertWaitTillBecomesTrue(self.testedApplication.cells.allElementsBoundByIndex[i].fb_isVisible); + FBAssertWaitTillBecomesTrue(self.testedApplication.staticTexts.allElementsBoundByIndex[i].fb_isVisible); } } diff --git a/WebDriverAgentTests/IntegrationTests/FBImageIOScalerTests.m b/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m similarity index 58% rename from WebDriverAgentTests/IntegrationTests/FBImageIOScalerTests.m rename to WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m index 491e2cfd2..28fa9d7ee 100644 --- a/WebDriverAgentTests/IntegrationTests/FBImageIOScalerTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBImageProcessorTests.m @@ -8,65 +8,63 @@ */ #import -#import -#import "FBImageIOScaler.h" +#import "FBImageProcessor.h" #import "FBIntegrationTestCase.h" -@interface FBImageIOScalerTests : FBIntegrationTestCase +@interface FBImageProcessorTests : FBIntegrationTestCase @property (nonatomic) NSData *originalImage; @property (nonatomic) CGSize originalSize; @end -@implementation FBImageIOScalerTests +@implementation FBImageProcessorTests - (void)setUp { XCUIApplication *app = [[XCUIApplication alloc] init]; [app launch]; XCUIScreenshot *screenshot = app.screenshot; self.originalImage = UIImageJPEGRepresentation(screenshot.image, 1.0); - self.originalSize = [FBImageIOScalerTests scaledSizeFromImage:screenshot.image]; + self.originalSize = [FBImageProcessorTests scaledSizeFromImage:screenshot.image]; } - (void)testScaling { CGFloat halfScale = 0.5; - CGSize expectedHalfScaleSize = [FBImageIOScalerTests sizeFromSize:self.originalSize scalingFactor:0.5]; + CGSize expectedHalfScaleSize = [FBImageProcessorTests sizeFromSize:self.originalSize scalingFactor:0.5]; [self scaleImageWithFactor:halfScale expectedSize:expectedHalfScaleSize]; - // 1 is the smalles scaling factor we accept + // 0 is the smalles scaling factor we accept CGFloat minScale = 0.0; - CGSize expectedMinScaleSize = [FBImageIOScalerTests sizeFromSize:self.originalSize scalingFactor:0.01]; + CGSize expectedMinScaleSize = [FBImageProcessorTests sizeFromSize:self.originalSize scalingFactor:0.01]; [self scaleImageWithFactor:minScale expectedSize:expectedMinScaleSize]; // For scaling factors above 100 we don't perform any scaling and just return the unmodified image - CGFloat unscaled = 2.0; - [self scaleImageWithFactor:unscaled + [self scaleImageWithFactor:1.0 + expectedSize:self.originalSize]; + [self scaleImageWithFactor:2.0 expectedSize:self.originalSize]; } - (void)scaleImageWithFactor:(CGFloat)scalingFactor expectedSize:(CGSize)excpectedSize { - FBImageIOScaler *scaler = [[FBImageIOScaler alloc] init]; + FBImageProcessor *scaler = [[FBImageProcessor alloc] init]; id expScaled = [self expectationWithDescription:@"Receive scaled image"]; - [scaler submitImage:self.originalImage - uti:(__bridge id)kUTTypeJPEG - scalingFactor:scalingFactor - compressionQuality:1.0 - completionHandler:^(NSData *scaled) { - UIImage *scaledImage = [UIImage imageWithData:scaled]; - CGSize scaledSize = [FBImageIOScalerTests scaledSizeFromImage:scaledImage]; - - XCTAssertEqualWithAccuracy(scaledSize.width, excpectedSize.width, 1.0); - XCTAssertEqualWithAccuracy(scaledSize.height, excpectedSize.height, 1.0); - - [expScaled fulfill]; - }]; + [scaler submitImageData:self.originalImage + scalingFactor:scalingFactor + completionHandler:^(NSData *scaled) { + UIImage *scaledImage = [UIImage imageWithData:scaled]; + CGSize scaledSize = [FBImageProcessorTests scaledSizeFromImage:scaledImage]; + + XCTAssertEqualWithAccuracy(scaledSize.width, excpectedSize.width, 1.0); + XCTAssertEqualWithAccuracy(scaledSize.height, excpectedSize.height, 1.0); + + [expScaled fulfill]; + }]; [self waitForExpectations:@[expScaled] timeout:0.5]; diff --git a/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m b/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m index 3da52e370..beca0c791 100644 --- a/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBKeyboardTests.m @@ -29,6 +29,9 @@ - (void)setUp - (void)testTextTyping { + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } NSString *text = @"Happy typing"; XCUIElement *textField = self.testedApplication.textFields[@"aIdentifier"]; [textField tap]; diff --git a/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m b/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m index 81e1c50cc..af3951f2c 100644 --- a/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBPasteboardTests.m @@ -44,7 +44,7 @@ - (void)testSetPasteboard if (![pastItemsQuery.firstMatch waitForExistenceWithTimeout:2.0]) { XCTFail(@"No matched element named 'Paste'"); } - XCUIElement *pasteItem = pastItemsQuery.fb_firstMatch; + XCUIElement *pasteItem = pastItemsQuery.firstMatch; XCTAssertNotNil(pasteItem); [pasteItem tap]; FBAssertWaitTillBecomesTrue([textField.value isEqualToString:text]); @@ -71,11 +71,7 @@ - (void)testGetPasteboard FBWaitExact(1.0); NSData *result = [FBPasteboard dataForType:@"plaintext" error:&error]; XCTAssertNil(error); - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"16.0")) { - // Pasteboard permission appears in a simulator. Not in a real device. - } else { - XCTAssertEqualObjects(textField.value, [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]); - } + XCTAssertEqualObjects(textField.value, [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding]); } - (void)testUrlCopyPaste diff --git a/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m b/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m index acea0b284..b9db49243 100644 --- a/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBPickerWheelSelectTests.m @@ -33,7 +33,7 @@ - (void)setUp - (void)testSelectNextPickerValue { - XCUIElement *element = self.testedApplication.pickerWheels.fb_firstMatch; + XCUIElement *element = self.testedApplication.pickerWheels.allElementsBoundByIndex.firstObject; XCTAssertTrue(element.exists); XCTAssertEqualObjects(element.wdType, @"XCUIElementTypePickerWheel"); NSError *error; diff --git a/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m b/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m index 16c9df00e..f1c5e15a0 100644 --- a/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBScrollingTests.m @@ -49,6 +49,11 @@ - (void)testCellVisibility - (void)testSimpleScroll { + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + // This test is unstable in CI env + return; + } + FBAssertVisibleCell(@"0"); FBAssertVisibleCell(@"10"); [self.scrollView fb_scrollDownByNormalizedDistance:1.0]; @@ -82,6 +87,11 @@ - (void)testFarScrollToVisible - (void)testNativeFarScrollToVisible { + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + // This test is unstable in CI env + return; + } + NSString *cellName = @"80"; NSError *error; FBAssertInvisibleCell(cellName); @@ -100,6 +110,12 @@ - (void)testAttributeWithNullScrollToVisible [element fb_scrollToVisibleWithError:&error]; XCTAssertNil(error); XCTAssertTrue(element.fb_isVisible); + + if (SYSTEM_VERSION_LESS_THAN(@"16.0")) { + // This test is unstable in CI env + return; + } + [element tap]; XCTAssertTrue(element.wdSelected); } diff --git a/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m index dc9a9b1af..ea9fb4cfe 100644 --- a/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBSessionIntegrationTests.m @@ -11,6 +11,7 @@ #import "FBIntegrationTestCase.h" #import "FBApplication.h" +#import "FBExceptions.h" #import "FBMacros.h" #import "FBSession.h" #import "FBXCodeCompatibility.h" @@ -107,4 +108,26 @@ - (void)testLaunchUnattachedApp XCTAssertEqualObjects(SETTINGS_BUNDLE_ID, FBApplication.fb_activeApplication.bundleID); } +- (void)testAppWithInvalidBundleIDCannotBeStarted +{ + FBApplication *testedApp = [[FBApplication alloc] initWithBundleIdentifier:@"yolo"]; + @try { + [testedApp launch]; + XCTFail(@"An exception is expected to be thrown"); + } @catch (NSException *exception) { + XCTAssertEqualObjects(FBApplicationMissingException, exception.name); + } +} + +- (void)testAppWithInvalidBundleIDCannotBeActivated +{ + FBApplication *testedApp = [[FBApplication alloc] initWithBundleIdentifier:@"yolo"]; + @try { + [testedApp activate]; + XCTFail(@"An exception is expected to be thrown"); + } @catch (NSException *exception) { + XCTAssertEqualObjects(FBApplicationMissingException, exception.name); + } +} + @end diff --git a/WebDriverAgentTests/IntegrationTests/FBTapTest.m b/WebDriverAgentTests/IntegrationTests/FBTapTest.m index 30b5f0f9e..5b79711fc 100644 --- a/WebDriverAgentTests/IntegrationTests/FBTapTest.m +++ b/WebDriverAgentTests/IntegrationTests/FBTapTest.m @@ -15,7 +15,6 @@ #import "FBElementCache.h" #import "FBTestMacros.h" #import "XCUIDevice+FBRotation.h" -#import "XCUIElement+FBTap.h" #import "XCUIElement+FBIsVisible.h" @interface FBTapTest : FBIntegrationTestCase @@ -28,19 +27,16 @@ @implementation FBTapTest - (void)verifyTapWithOrientation:(UIDeviceOrientation)orientation { [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - NSError *error; - [self.testedApplication.buttons[FBShowAlertButtonName] fb_tapWithError:&error]; + [self.testedApplication.buttons[FBShowAlertButtonName] tap]; FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); } - (void)setUp { + // Launch the app everytime to ensure the orientation for each test. [super setUp]; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self launchApplication]; - [self goToAlertsPage]; - }); + [self launchApplication]; + [self goToAlertsPage]; [self clearAlert]; } @@ -63,23 +59,23 @@ - (void)testTapInLandscapeLeft - (void)testTapInLandscapeRight { + [self verifyTapWithOrientation:UIDeviceOrientationLandscapeRight]; } -// Visibility detection for upside-down orientation is broken -// and cannot be workarounded properly, but this is not very important for Appium, since -// We don't support such orientation anyway -- (void)disabled_testTapInPortraitUpsideDown +- (void)testTapInPortraitUpsideDown { + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } [self verifyTapWithOrientation:UIDeviceOrientationPortraitUpsideDown]; } - (void)verifyTapByCoordinatesWithOrientation:(UIDeviceOrientation)orientation { [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; - NSError *error; XCUIElement *dstButton = self.testedApplication.buttons[FBShowAlertButtonName]; - [dstButton fb_tapCoordinate:CGPointMake(dstButton.frame.size.width / 2, dstButton.frame.size.height / 2) error:&error]; + [[dstButton coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)] tap]; FBAssertWaitTillBecomesTrue(self.testedApplication.alerts.count > 0); } @@ -98,11 +94,11 @@ - (void)testTapCoordinatesInLandscapeRight [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationLandscapeRight]; } -// Visibility detection for upside-down orientation is broken -// and cannot be workarounded properly, but this is not very important for Appium, since -// We don't support such orientation anyway -- (void)disabled_testTapCoordinatesInPortraitUpsideDown +- (void)testTapCoordinatesInPortraitUpsideDown { + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } [self verifyTapByCoordinatesWithOrientation:UIDeviceOrientationPortraitUpsideDown]; } diff --git a/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m index 75ec24bed..598cc66c9 100644 --- a/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBW3CTouchActionsIntegrationTests.m @@ -294,18 +294,8 @@ - (void)testTap [self verifyGesture:gesture orientation:UIDeviceOrientationPortrait]; } -// TODO: UIDeviceOrientationLandscapeLeft did not work in iOS 16 simumlator. -// UIDeviceOrientationPortrait was ok. - (void)testDoubleTap { - if ([UIDevice.currentDevice userInterfaceIdiom] == UIUserInterfaceIdiomPad && - SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - // "tap the element" does not work on iPadOS simulator after change the rotation in iOS 15 - // while selecting the element worked. (I confirmed with getting an element screenshot that - // the FBShowAlertButtonName was actually selected after changing the orientation.) - return; - } - NSArray *> *gesture = @[@{ @"type": @"pointer", @@ -326,17 +316,8 @@ - (void)testDoubleTap [self verifyGesture:gesture orientation:UIDeviceOrientationLandscapeLeft]; } -// TODO: UIDeviceOrientationLandscapeRight did not work in iOS 16 simumlator. -// UIDeviceOrientationPortrait was ok. - (void)testLongPressWithCombinedPause { - if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"15.0")) { - // Xcode 13 x iOS 14 also does not work while Xcode 12.5 x iOS 14 worked. - // Skip this test for iOS 15 since iOS 15 works with Xcode 13 at least. - return; - } - - NSArray *> *gesture = @[@{ @"type": @"pointer", @@ -357,6 +338,9 @@ - (void)testLongPressWithCombinedPause - (void)testLongPress { + if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + XCTSkip(@"Failed on Azure Pipeline. Local run succeeded."); + } UIDeviceOrientation orientation = UIDeviceOrientationLandscapeLeft; [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientation]; CGRect elementFrame = self.testedApplication.buttons[FBShowAlertButtonName].frame; @@ -415,7 +399,7 @@ - (void)setUp [self launchApplication]; [self goToAttributesPage]; }); - self.pickerWheel = self.testedApplication.pickerWheels.fb_firstMatch; + self.pickerWheel = self.testedApplication.pickerWheels.allElementsBoundByIndex.firstObject; } - (void)tearDown diff --git a/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m index 87a66fb25..aa1ed56c2 100644 --- a/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m +++ b/WebDriverAgentTests/IntegrationTests/FBXPathIntegrationTests.m @@ -10,6 +10,7 @@ #import #import "FBIntegrationTestCase.h" +#import "FBMacros.h" #import "FBTestMacros.h" #import "FBXPath.h" #import "FBXCodeCompatibility.h" @@ -41,7 +42,7 @@ - (void)setUp - (id)destinationSnapshot { - XCUIElement *matchingElement = self.testedView.buttons.fb_firstMatch; + XCUIElement *matchingElement = self.testedView.buttons.allElementsBoundByIndex.firstObject; FBAssertWaitTillBecomesTrue(nil != matchingElement.fb_takeSnapshot); id snapshot = matchingElement.fb_takeSnapshot; @@ -58,7 +59,7 @@ - (void)testSingleDescendantXMLRepresentation NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot options:nil]; XCTAssertNotNil(xmlStr); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, wrappedSnapshot.wdEnabled ? @"true" : @"false", wrappedSnapshot.wdVisible ? @"true" : @"false", wrappedSnapshot.wdAccessible ? @"true" : @"false", [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex]; + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex]; XCTAssertEqualObjects(xmlStr, expectedXml); } @@ -71,7 +72,7 @@ - (void)testSingleDescendantXMLRepresentationWithScope NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot options:options]; XCTAssertNotNil(xmlStr); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@>\n <%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\"/>\n\n", scope, wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, wrappedSnapshot.wdEnabled ? @"true" : @"false", wrappedSnapshot.wdVisible ? @"true" : @"false", wrappedSnapshot.wdAccessible ? @"true" : @"false", [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, scope]; + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@>\n <%@ type=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\"/>\n\n", scope, wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdEnabled), FBBoolToString(wrappedSnapshot.wdVisible), FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue], wrappedSnapshot.wdIndex, scope]; XCTAssertEqualObjects(xmlStr, expectedXml); } @@ -84,7 +85,7 @@ - (void)testSingleDescendantXMLRepresentationWithoutAttributes NSString *xmlStr = [FBXPath xmlStringWithRootElement:wrappedSnapshot options:options]; XCTAssertNotNil(xmlStr); - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, wrappedSnapshot.wdAccessible ? @"true" : @"false", [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue]]; + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" name=\"%@\" label=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\"/>\n", wrappedSnapshot.wdType, wrappedSnapshot.wdType, wrappedSnapshot.wdName, wrappedSnapshot.wdLabel, FBBoolToString(wrappedSnapshot.wdAccessible), [wrappedSnapshot.wdRect[@"x"] stringValue], [wrappedSnapshot.wdRect[@"y"] stringValue], [wrappedSnapshot.wdRect[@"width"] stringValue], [wrappedSnapshot.wdRect[@"height"] stringValue]]; XCTAssertEqualObjects(xmlStr, expectedXml); } diff --git a/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m index 3f47d4ae9..41ee134a1 100644 --- a/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m +++ b/WebDriverAgentTests/IntegrationTests/XCUIApplicationHelperTests.m @@ -14,6 +14,7 @@ #import "FBApplication.h" #import "FBIntegrationTestCase.h" #import "FBElement.h" +#import "FBMacros.h" #import "FBTestMacros.h" #import "XCUIApplication+FBHelpers.h" #import "XCUIElement+FBIsVisible.h" @@ -46,10 +47,10 @@ - (void)testApplicationTree - (void)testDeactivateApplication { NSError *error; - uint64_t timeStarted = mach_absolute_time(); + uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); NSTimeInterval backgroundDuration = 5.0; XCTAssertTrue([self.testedApplication fb_deactivateWithDuration:backgroundDuration error:&error]); - NSTimeInterval timeElapsed = (mach_absolute_time() - timeStarted) / NSEC_PER_SEC; + NSTimeInterval timeElapsed = (clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted) / NSEC_PER_SEC; XCTAssertNil(error); XCTAssertEqualWithAccuracy(timeElapsed, backgroundDuration, 3.0); XCTAssertTrue(self.testedApplication.buttons[@"Alerts"].exists); @@ -94,4 +95,24 @@ - (void)testTestmanagerdVersion XCTAssertGreaterThan(FBTestmanagerdVersion(), 0); } +- (void)testAccessbilityAudit +{ + if (SYSTEM_VERSION_LESS_THAN(@"17.0")) { + return; + } + + NSError *error; + NSArray *auditIssues1 = [FBApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypes:~0UL + error:&error]; + XCTAssertNotNil(auditIssues1); + XCTAssertNil(error); + + NSMutableSet *set = [NSMutableSet new]; + [set addObject:@"XCUIAccessibilityAuditTypeAll"]; + NSArray *auditIssues2 = [FBApplication.fb_activeApplication fb_performAccessibilityAuditWithAuditTypesSet:set.copy + error:&error]; + XCTAssertEqualObjects(auditIssues1, auditIssues2); + XCTAssertNil(error); +} + @end diff --git a/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m b/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m index bfa2470a3..ca79b2fa4 100644 --- a/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m +++ b/WebDriverAgentTests/IntegrationTests/XCUIDeviceHelperTests.m @@ -78,14 +78,12 @@ - (void)testLandscapeScreenshot XCTAssertTrue(screenshot.size.width > screenshot.size.height); XCUIScreen *mainScreen = XCUIScreen.mainScreen; - // TODO: This screenshot rotation was not landscape in an iOS 16 beta simulator. UIImage *screenshotExact = ((XCUIScreenshot *)mainScreen.screenshot).image; - XCTAssertEqualWithAccuracy(screenshotExact.size.height * mainScreen.scale, - screenshot.size.height, - FLT_EPSILON); - XCTAssertEqualWithAccuracy(screenshotExact.size.width * mainScreen.scale, - screenshot.size.width, - FLT_EPSILON); + CGSize realMainScreenSize = screenshotExact.size.height > screenshot.size.width + ? CGSizeMake(screenshotExact.size.height * mainScreen.scale, screenshotExact.size.width * mainScreen.scale) + : CGSizeMake(screenshotExact.size.width * mainScreen.scale, screenshotExact.size.height * mainScreen.scale); + XCTAssertEqualWithAccuracy(realMainScreenSize.height, screenshot.size.height, FLT_EPSILON); + XCTAssertEqualWithAccuracy(realMainScreenSize.width, screenshot.size.width, FLT_EPSILON); } - (void)testWifiAddress @@ -118,15 +116,57 @@ - (void)testLockUnlockScreen XCTAssertNil(error); } -- (void)disabled_testUrlSchemeActivation +- (void)testUrlSchemeActivation { - // This test is not stable on CI because of system slowness + if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { + return; + } + NSError *error; XCTAssertTrue([XCUIDevice.sharedDevice fb_openUrl:@"https://apple.com" error:&error]); FBAssertWaitTillBecomesTrue([FBApplication.fb_activeApplication.bundleID isEqualToString:@"com.apple.mobilesafari"]); XCTAssertNil(error); } +- (void)testUrlSchemeActivationWithApp +{ + if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { + return; + } + + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_openUrl:@"https://apple.com" + withApplication:@"com.apple.mobilesafari" + error:&error]); + FBAssertWaitTillBecomesTrue([FBApplication.fb_activeApplication.bundleID isEqualToString:@"com.apple.mobilesafari"]); + XCTAssertNil(error); +} + +#if !TARGET_OS_TV +- (void)testSimulatedLocationSetup +{ + if (SYSTEM_VERSION_LESS_THAN(@"16.4")) { + return; + } + + CLLocation *simulatedLocation = [[CLLocation alloc] initWithLatitude:50 longitude:50]; + NSError *error; + XCTAssertTrue([XCUIDevice.sharedDevice fb_setSimulatedLocation:simulatedLocation error:&error]); + XCTAssertNil(error); + CLLocation *currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; + XCTAssertNil(error); + XCTAssertNotNil(currentLocation); + XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1); + XCTAssertEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1); + XCTAssertTrue([XCUIDevice.sharedDevice fb_clearSimulatedLocation:&error]); + XCTAssertNil(error); + currentLocation = [XCUIDevice.sharedDevice fb_getSimulatedLocation:&error]; + XCTAssertNil(error); + XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.latitude, currentLocation.coordinate.latitude, 0.1); + XCTAssertNotEqualWithAccuracy(simulatedLocation.coordinate.longitude, currentLocation.coordinate.longitude, 0.1); +} +#endif + - (void)testPressingUnsupportedButton { NSError *error; diff --git a/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m b/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m index b727a20a0..243ebec5d 100644 --- a/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m +++ b/WebDriverAgentTests/IntegrationTests/XCUIElementFBFindTests.m @@ -48,7 +48,8 @@ - (void)testDescendantsWithClassName @"Deadlock app", @"Touch", ]]; - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, expectedLabels.count); NSArray *labels = [matchingSnapshots valueForKeyPath:@"@distinctUnionOfObjects.label"]; XCTAssertEqualObjects([NSSet setWithArray:labels], expectedLabels); @@ -60,14 +61,16 @@ - (void)testDescendantsWithClassName - (void)testSingleDescendantWithClassName { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" shouldReturnAfterFirstMatch:YES]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassName:@"XCUIElementTypeButton" + shouldReturnAfterFirstMatch:YES]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); } - (void)testDescendantsWithIdentifier { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" + shouldReturnAfterFirstMatch:NO]; int snapshotsCount = 1; if (@available(iOS 13.0, *)) { snapshotsCount = 2; @@ -81,7 +84,8 @@ - (void)testDescendantsWithIdentifier - (void)testSingleDescendantWithIdentifier { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" shouldReturnAfterFirstMatch:YES]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" + shouldReturnAfterFirstMatch:YES]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); @@ -89,7 +93,8 @@ - (void)testSingleDescendantWithIdentifier - (void)testStableInstance { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" shouldReturnAfterFirstMatch:YES]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingIdentifier:@"Alerts" + shouldReturnAfterFirstMatch:YES]; XCTAssertEqual(matchingSnapshots.count, 1); for (XCUIElement *el in @[matchingSnapshots.lastObject, matchingSnapshots.lastObject.fb_stableInstance]) { XCTAssertEqual(el.elementType, XCUIElementTypeButton); @@ -105,7 +110,8 @@ - (void)testSingleDescendantWithMissingIdentifier - (void)testDescendantsWithXPathQuery { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts']" shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts']" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); XCTAssertEqualObjects(matchingSnapshots.lastObject.label, @"Alerts"); @@ -113,14 +119,16 @@ - (void)testDescendantsWithXPathQuery - (void)testSelfWithXPathQuery { - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeApplication" shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeApplication" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeApplication); } - (void)testSingleDescendantWithXPathQuery { - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton" shouldReturnAfterFirstMatch:YES]; + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@hittable='true']" + shouldReturnAfterFirstMatch:YES]; XCTAssertEqual(matchingSnapshots.count, 1); XCUIElement *matchingSnapshot = [matchingSnapshots firstObject]; XCTAssertNotNil(matchingSnapshot); @@ -130,26 +138,30 @@ - (void)testSingleDescendantWithXPathQuery - (void)testSingleDescendantWithXPathQueryNoMatches { - XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButtonnn" shouldReturnAfterFirstMatch:YES] firstObject]; + XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButtonnn" + shouldReturnAfterFirstMatch:YES] firstObject]; XCTAssertNil(matchingSnapshot); } - (void)testSingleLastDescendantWithXPathQuery { - XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"(//XCUIElementTypeButton)[last()]" shouldReturnAfterFirstMatch:YES] firstObject]; + XCUIElement *matchingSnapshot = [[self.testedView fb_descendantsMatchingXPathQuery:@"(//XCUIElementTypeButton)[last()]" + shouldReturnAfterFirstMatch:YES] firstObject]; XCTAssertNotNil(matchingSnapshot); XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); } - (void)testDescendantsWithXPathQueryNoMatches { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts1']" shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeButton[@label='Alerts1']" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, 0); } - (void)testDescendantsWithComplexXPathQuery { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//*[@label='Scrolling']/preceding::*[boolean(string(@label))]" shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingXPathQuery:@"//*[@label='Scrolling']/preceding::*[boolean(string(@label))]" + shouldReturnAfterFirstMatch:NO]; int snapshotsCount = 3; if (@available(iOS 13.0, *)) { snapshotsCount = 6; @@ -159,13 +171,15 @@ - (void)testDescendantsWithComplexXPathQuery - (void)testDescendantsWithWrongXPathQuery { - XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" shouldReturnAfterFirstMatch:NO], + XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" + shouldReturnAfterFirstMatch:NO], NSException, FBInvalidXPathException); } - (void)testFirstDescendantWithWrongXPathQuery { - XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" shouldReturnAfterFirstMatch:YES], + XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingXPathQuery:@"//*[blabla(@label, Scrolling')]" + shouldReturnAfterFirstMatch:YES], NSException, FBInvalidXPathException); } @@ -200,7 +214,8 @@ - (void)testDescendantsWithPredicateString - (void)testSelfWithPredicateString { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type == 'XCUIElementTypeApplication'"]; - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingPredicate:predicate + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeApplication); } @@ -223,7 +238,9 @@ - (void)testSingleDescendantWithPredicateStringByIndex - (void)testDescendantsWithPropertyStrict { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" value:@"Alert" partialSearch:NO]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" + value:@"Alert" + partialSearch:NO]; XCTAssertEqual(matchingSnapshots.count, 0); matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" value:@"Alerts" partialSearch:NO]; int snapshotsCount = 1; @@ -237,7 +254,9 @@ - (void)testDescendantsWithPropertyStrict - (void)testGlobalWithPropertyStrict { - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingProperty:@"label" value:@"Alert" partialSearch:NO]; + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingProperty:@"label" + value:@"Alert" + partialSearch:NO]; XCTAssertEqual(matchingSnapshots.count, 0); matchingSnapshots = [self.testedApplication fb_descendantsMatchingProperty:@"label" value:@"Alerts" partialSearch:NO]; int snapshotsCount = 1; @@ -251,7 +270,9 @@ - (void)testGlobalWithPropertyStrict - (void)testDescendantsWithPropertyPartial { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" value:@"Alerts" partialSearch:NO]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingProperty:@"label" + value:@"Alerts" + partialSearch:NO]; int snapshotsCount = 1; if (@available(iOS 13.0, *)) { snapshotsCount = 2; @@ -265,7 +286,8 @@ - (void)testDescendantsWithClassChain { NSArray *matchingSnapshots; NSString *queryString =@"XCUIElementTypeWindow/XCUIElementTypeOther/**/XCUIElementTypeButton"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton for (XCUIElement *matchingSnapshot in matchingSnapshots) { XCTAssertEqual(matchingSnapshot.elementType, XCUIElementTypeButton); @@ -279,14 +301,17 @@ - (void)testDescendantsWithClassChainWithIndex if (@available(iOS 13.0, *)) { // iPhone queryString = @"XCUIElementTypeWindow/*/*/*/*[2]/*/*/XCUIElementTypeButton"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; if (matchingSnapshots.count == 0) { // iPad queryString = @"XCUIElementTypeWindow/*/*/*/*/*[2]/*/*/XCUIElementTypeButton"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; } } else { - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; } XCTAssertEqual(matchingSnapshots.count, 5); // /XCUIElementTypeButton for (XCUIElement *matchingSnapshot in matchingSnapshots) { @@ -298,7 +323,8 @@ - (void)testDescendantsWithClassChainAndPredicates { NSArray *matchingSnapshots; NSString *queryString = @"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, 2); XCTAssertEqualObjects([matchingSnapshots firstObject].label, @"Alerts"); XCTAssertEqualObjects([matchingSnapshots lastObject].label, @"Attributes"); @@ -307,8 +333,10 @@ - (void)testDescendantsWithClassChainAndPredicates - (void)testDescendantsWithIndirectClassChainAndPredicates { NSString *queryString = @"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]"; - NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; - NSArray *deepQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]" shouldReturnAfterFirstMatch:NO]; + NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; + NSArray *deepQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/**/XCUIElementTypeButton[`label BEGINSWITH 'A'`]" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(simpleQueryMatches.count, deepQueryMatches.count); XCTAssertEqualObjects([simpleQueryMatches firstObject].label, [deepQueryMatches firstObject].label); XCTAssertEqualObjects([simpleQueryMatches lastObject].label, [deepQueryMatches lastObject].label); @@ -316,8 +344,10 @@ - (void)testDescendantsWithIndirectClassChainAndPredicates - (void)testClassChainWithDescendantPredicate { - NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[1]" shouldReturnAfterFirstMatch:NO]; - NSArray *predicateQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[$type == 'XCUIElementTypeButton' AND label BEGINSWITH 'A'$]" shouldReturnAfterFirstMatch:NO]; + NSArray *simpleQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[1]" + shouldReturnAfterFirstMatch:NO]; + NSArray *predicateQueryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow/*/*[$type == 'XCUIElementTypeButton' AND label BEGINSWITH 'A'$]" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(simpleQueryMatches.count, predicateQueryMatches.count); XCTAssertEqual([simpleQueryMatches firstObject].elementType, [predicateQueryMatches firstObject].elementType); XCTAssertEqual([simpleQueryMatches lastObject].elementType, [predicateQueryMatches lastObject].elementType); @@ -325,7 +355,8 @@ - (void)testClassChainWithDescendantPredicate - (void)testSingleDescendantWithComplexIndirectClassChain { - NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeButton[2]" shouldReturnAfterFirstMatch:NO]; + NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeButton[2]" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(queryMatches.count, 1); XCTAssertEqual(queryMatches.lastObject.elementType, XCUIElementTypeButton); XCTAssertEqualObjects(queryMatches.lastObject.label, @"Deadlock app"); @@ -333,7 +364,8 @@ - (void)testSingleDescendantWithComplexIndirectClassChain - (void)testSingleDescendantWithComplexIndirectClassChainAndZeroMatches { - NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeWindow" shouldReturnAfterFirstMatch:NO]; + NSArray *queryMatches = [self.testedApplication fb_descendantsMatchingClassChain:@"**/*/XCUIElementTypeWindow" + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(queryMatches.count, 0); } @@ -341,14 +373,16 @@ - (void)testDescendantsWithClassChainAndPredicatesAndIndexes { NSArray *matchingSnapshots; NSString *queryString = @"XCUIElementTypeWindow[`name != 'bla'`]/**/XCUIElementTypeButton[`label BEGINSWITH \"A\"`][1]"; - matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; + matchingSnapshots = [self.testedApplication fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqualObjects([matchingSnapshots firstObject].label, @"Alerts"); } - (void)testSingleDescendantWithClassChain { - NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton" shouldReturnAfterFirstMatch:YES]; + NSArray *matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton" + shouldReturnAfterFirstMatch:YES]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); @@ -358,19 +392,22 @@ - (void)testSingleDescendantWithClassChain - (void)testSingleDescendantWithClassChainAndNegativeIndex { NSArray *matchingSnapshots; - matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-1]" shouldReturnAfterFirstMatch:YES]; + matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-1]" + shouldReturnAfterFirstMatch:YES]; XCTAssertEqual(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeButton); XCTAssertTrue([matchingSnapshots.lastObject.label isEqualToString:@"Touch"]); - matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-10]" shouldReturnAfterFirstMatch:YES]; + matchingSnapshots = [self.testedView fb_descendantsMatchingClassChain:@"XCUIElementTypeButton[-10]" + shouldReturnAfterFirstMatch:YES]; XCTAssertEqual(matchingSnapshots.count, 0); } - (void)testInvalidQueryWithClassChain { - XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingClassChain:@"NoXCUIElementTypePrefix" shouldReturnAfterFirstMatch:YES], + XCTAssertThrowsSpecificNamed([self.testedView fb_descendantsMatchingClassChain:@"NoXCUIElementTypePrefix" + shouldReturnAfterFirstMatch:YES], NSException, FBClassChainQueryParseException); } @@ -384,7 +421,8 @@ - (void)testHandleInvalidQueryWithClassChainAsNoElementWithoutError - (void)testClassChainWithInvalidPredicate { - XCTAssertThrowsSpecificNamed([self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow[`bla != 'bla'`]" shouldReturnAfterFirstMatch:NO], + XCTAssertThrowsSpecificNamed([self.testedApplication fb_descendantsMatchingClassChain:@"XCUIElementTypeWindow[`bla != 'bla'`]" + shouldReturnAfterFirstMatch:NO], NSException, FBUnknownAttributeException);; } @@ -411,8 +449,10 @@ - (void)testNestedQueryWithClassChain queryString = @"XCUIElementTypePicker"; } FBAssertWaitTillBecomesTrue(self.testedApplication.buttons[@"Button"].fb_isVisible); - XCUIElement *datePicker = [self.testedApplication descendantsMatchingType:XCUIElementTypeDatePicker].fb_firstMatch; - NSArray *matches = [datePicker fb_descendantsMatchingClassChain:queryString shouldReturnAfterFirstMatch:NO]; + XCUIElement *datePicker = [self.testedApplication + descendantsMatchingType:XCUIElementTypeDatePicker].allElementsBoundByIndex.firstObject; + NSArray *matches = [datePicker fb_descendantsMatchingClassChain:queryString + shouldReturnAfterFirstMatch:NO]; XCTAssertEqual(matches.count, 1); XCUIElementType expectedType = XCUIElementTypeOther; @@ -440,7 +480,8 @@ - (void)setUp - (void)testInvisibleDescendantWithXPathQuery { - NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeStaticText[@visible='false']" shouldReturnAfterFirstMatch:NO]; + NSArray *matchingSnapshots = [self.testedApplication fb_descendantsMatchingXPathQuery:@"//XCUIElementTypeStaticText[@visible='false']" + shouldReturnAfterFirstMatch:NO]; XCTAssertGreaterThan(matchingSnapshots.count, 1); XCTAssertEqual(matchingSnapshots.lastObject.elementType, XCUIElementTypeStaticText); XCTAssertFalse(matchingSnapshots.lastObject.fb_isVisible); diff --git a/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m b/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m index c145ea32b..f2d6d3a60 100644 --- a/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m +++ b/WebDriverAgentTests/IntegrationTests/XCUIElementHelperIntegrationTests.m @@ -32,9 +32,9 @@ - (void)setUp - (void)testDescendantsFiltering { - NSArray *buttons = self.testedApplication.buttons.allElementsBoundByAccessibilityElement; + NSArray *buttons = self.testedApplication.buttons.allElementsBoundByIndex; XCTAssertTrue(buttons.count > 0); - NSArray *windows = self.testedApplication.windows.allElementsBoundByAccessibilityElement; + NSArray *windows = self.testedApplication.windows.allElementsBoundByIndex; XCTAssertTrue(windows.count > 0); NSMutableArray *allElements = [NSMutableArray array]; diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h index 394760791..716ba7cbb 100644 --- a/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h +++ b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.h @@ -11,4 +11,5 @@ @interface XCElementSnapshotDouble : NSObject @property (readwrite, nullable) id value; +@property (readwrite, nullable, copy) NSString *label; @end diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m index cd25e3917..d52c2904c 100644 --- a/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m +++ b/WebDriverAgentTests/UnitTests/Doubles/XCElementSnapshotDouble.m @@ -11,6 +11,7 @@ #import "FBXCAccessibilityElement.h" #import "FBXCElementSnapshot.h" +#import "XCUIHitPointResult.h" @implementation XCElementSnapshotDouble @@ -18,6 +19,7 @@ - (id)init { self = [super init]; self->_value = @"magicValue"; + self->_label = @"testLabel"; return self; } @@ -36,11 +38,6 @@ - (NSString *)title return @"testTitle"; } -- (NSString *)label -{ - return @"testLabel"; -} - - (XCUIElementType)elementType { return XCUIElementTypeOther; @@ -91,6 +88,11 @@ - (NSDictionary *)additionalAttributes return nil; } +- (XCUIHitPointResult *)hitPoint:(NSError **)error +{ + return [[XCUIHitPointResult alloc] initWithHitPoint:CGPointZero hittable:YES]; +} + - (NSArray *)children { return @[]; diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h index 473a40121..d40945545 100644 --- a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.h @@ -32,6 +32,8 @@ @property (nonatomic, readwrite) NSUInteger wdIndex; @property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; @property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; +@property (nonatomic, readwrite, getter = isWDFocused) BOOL wdFocused; +@property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; @property (copy, nonnull) NSArray *children; @property (nonatomic, readwrite, assign) XCUIElementType elementType; @property (nonatomic, readwrite, getter=isWDAccessibilityContainer) BOOL wdAccessibilityContainer; diff --git a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m index b60e50c26..e9792f132 100644 --- a/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m +++ b/WebDriverAgentTests/UnitTests/Doubles/XCUIElementDouble.m @@ -27,6 +27,8 @@ - (id)init self.wdAccessible = YES; self.wdEnabled = YES; self.wdSelected = YES; + self.wdFocused = YES; + self.wdHittable = YES; self.wdIndex = 0; #if TARGET_OS_TV self.wdFocused = YES; diff --git a/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m b/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m index d586d8a59..14e71ebe1 100644 --- a/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m +++ b/WebDriverAgentTests/UnitTests/FBMathUtilsTests.m @@ -98,18 +98,6 @@ - (void)testFuzzyEqualRectsSymmetry XCTAssertFalse(FBRectFuzzyEqualToRect(referenceRect, CGRectMake(-1, -1, 1, 1), 1)); } - -- (void)testPointInvertion -{ - const CGPoint testPoint = CGPointMake(1, 2); - const CGSize screenSize = CGSizeMake(10, 15); - const CGFloat t = FBDefaultFrameFuzzyThreshold; - XCTAssertTrue(FBPointFuzzyEqualToPoint(CGPointMake(1, 2), FBInvertPointForApplication(testPoint, screenSize, UIInterfaceOrientationPortrait), t)); - XCTAssertTrue(FBPointFuzzyEqualToPoint(CGPointMake(9, 13), FBInvertPointForApplication(testPoint, screenSize, UIInterfaceOrientationPortraitUpsideDown), t)); - XCTAssertTrue(FBPointFuzzyEqualToPoint(CGPointMake(2, 14), FBInvertPointForApplication(testPoint, screenSize, UIInterfaceOrientationLandscapeLeft), t)); - XCTAssertTrue(FBPointFuzzyEqualToPoint(CGPointMake(8, 1), FBInvertPointForApplication(testPoint, screenSize, UIInterfaceOrientationLandscapeRight), t)); -} - - (void)testSizeInversion { const CGSize screenSizePortrait = CGSizeMake(10, 15); diff --git a/WebDriverAgentTests/UnitTests/FBXPathTests.m b/WebDriverAgentTests/UnitTests/FBXPathTests.m index a78fe72a7..ce9171107 100644 --- a/WebDriverAgentTests/UnitTests/FBXPathTests.m +++ b/WebDriverAgentTests/UnitTests/FBXPathTests.m @@ -9,6 +9,7 @@ #import +#import "FBMacros.h" #import "FBXPath.h" #import "FBXPath-Private.h" #import "XCUIElementDouble.h" @@ -29,7 +30,7 @@ - (NSString *)xmlStringWithElement:(id)element xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; int buffersize; - xmlChar *xmlbuff; + xmlChar *xmlbuff = NULL; int rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL); if (rc >= 0) { rc = [FBXPath xmlRepresentationWithRootElement:element @@ -63,7 +64,7 @@ - (void)testDefaultXPathPresentation xpathQuery:nil excludingAttributes:nil]; NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdType, element.wdValue, element.wdName, element.wdLabel, element.wdEnabled ? @"true" : @"false", element.wdVisible ? @"true" : @"false", element.wdAccessible ? @"true" : @"false", element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex]; + element.wdType, element.wdType, element.wdValue, element.wdName, element.wdLabel, FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex]; XCTAssertTrue([resultXml isEqualToString: expectedXml]); } @@ -75,7 +76,7 @@ - (void)testtXPathPresentationWithSomeAttributesExcluded xpathQuery:nil excludingAttributes:@[@"type", @"visible", @"value", @"index"]]; NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ name=\"%@\" label=\"%@\" enabled=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdName, element.wdLabel, element.wdEnabled ? @"true" : @"false", element.wdAccessible ? @"true" : @"false", element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"]]; + element.wdType, element.wdName, element.wdLabel, FBBoolToString(element.wdEnabled), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"]]; XCTAssertEqualObjects(resultXml, expectedXml); } @@ -83,13 +84,14 @@ - (void)testXPathPresentationBasedOnQueryMatchingAllAttributes { XCElementSnapshotDouble *snapshot = [XCElementSnapshotDouble new]; snapshot.value = @"йоло<>&\""; + snapshot.label = @"a\nb"; id element = (id)[FBXCElementSnapshotWrapper ensureWrapped:(id)snapshot]; NSString *resultXml = [self xmlStringWithElement:element xpathQuery:[NSString stringWithFormat:@"//%@[@*]", element.wdType] excludingAttributes:@[@"visible"]]; - NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" private_indexPath=\"top\"/>\n", - element.wdType, element.wdType, @"йоло<>&"", element.wdName, element.wdLabel, element.wdEnabled ? @"true" : @"false", element.wdVisible ? @"true" : @"false", element.wdAccessible ? @"true" : @"false", element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex]; - XCTAssertTrue([resultXml isEqualToString:expectedXml]); + NSString *expectedXml = [NSString stringWithFormat:@"\n<%@ type=\"%@\" value=\"%@\" name=\"%@\" label=\"%@\" enabled=\"%@\" visible=\"%@\" accessible=\"%@\" x=\"%@\" y=\"%@\" width=\"%@\" height=\"%@\" index=\"%lu\" hittable=\"%@\" private_indexPath=\"top\"/>\n", + element.wdType, element.wdType, @"йоло<>&"", element.wdName, @"a b", FBBoolToString(element.wdEnabled), FBBoolToString(element.wdVisible), FBBoolToString(element.wdAccessible), element.wdRect[@"x"], element.wdRect[@"y"], element.wdRect[@"width"], element.wdRect[@"height"], element.wdIndex, FBBoolToString(element.wdHittable)]; + XCTAssertEqualObjects(expectedXml, resultXml); } - (void)testXPathPresentationBasedOnQueryMatchingSomeAttributes diff --git a/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m b/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m new file mode 100644 index 000000000..3f6f465a3 --- /dev/null +++ b/WebDriverAgentTests/UnitTests/NSDictionaryFBUtf8SafeTests.m @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "NSDictionary+FBUtf8SafeDictionary.h" + +@interface NSDictionaryFBUtf8SafeTests : XCTestCase +@end + +@implementation NSDictionaryFBUtf8SafeTests + +- (void)testEmptySafeDictConversion +{ + NSDictionary *d = @{}; + XCTAssertEqualObjects(d, d.fb_utf8SafeDictionary); +} + +- (void)testNonEmptySafeDictConversion +{ + NSDictionary *d = @{ + @"1": @[@3, @4], + @"5": @{@"6": @7, @"8": @9}, + @"10": @"11" + }; + XCTAssertEqualObjects(d, d.fb_utf8SafeDictionary); +} + +@end diff --git a/WebDriverAgentTests/UnitTests/XCUICoordinateFix.m b/WebDriverAgentTests/UnitTests/XCUICoordinateFix.m deleted file mode 100644 index 562d1105b..000000000 --- a/WebDriverAgentTests/UnitTests/XCUICoordinateFix.m +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -#import "FBMathUtils.h" -#import "XCUICoordinate.h" -#import "XCUICoordinate+FBFix.h" -#import "XCUIElementDouble.h" - -@interface XCUICoordinateFix : XCTestCase -@property (nonatomic, strong, nullable) XCUIElement *mockElement; -@property (nonatomic, strong, nullable) XCUICoordinate *defaultCoordinate; -@end - -@implementation XCUICoordinateFix - -- (void)setUp -{ - [super setUp]; - XCUIElementDouble *element = [XCUIElementDouble new]; - element.frame = CGRectMake(1, 2, 9, 10); - self.mockElement = (XCUIElement *)element; - self.defaultCoordinate = [[XCUICoordinate alloc] initWithElement:self.mockElement normalizedOffset:CGVectorMake(0.0, 0.0)]; -} - -- (void)testCoordinateWithElement -{ - XCUICoordinate *coordinate = [[XCUICoordinate alloc] initWithElement:self.mockElement normalizedOffset:CGVectorMake(0.0, 0.0)]; - XCTAssertTrue(FBPointFuzzyEqualToPoint(coordinate.fb_screenPoint, CGPointMake(1, 2), 0.1)); -} - -- (void)testCoordinateWithElementWithOffset -{ - XCUICoordinate *coordinate = [[XCUICoordinate alloc] initWithElement:self.mockElement normalizedOffset:CGVectorMake(0.5, 0.5)]; - XCTAssertTrue(FBPointFuzzyEqualToPoint(coordinate.fb_screenPoint, CGPointMake(5.5, 7), 0.1)); -} - -- (void)testCoordinateWithCoordinate -{ - XCUICoordinate *coordinate = [[XCUICoordinate alloc] initWithCoordinate:self.defaultCoordinate pointsOffset:CGVectorMake(0, 0)]; - XCTAssertTrue(FBPointFuzzyEqualToPoint(coordinate.fb_screenPoint, CGPointMake(1, 2), 0.1)); -} - -- (void)testCoordinateWithCoordinateWithOffset -{ - XCUICoordinate *coordinate = [[XCUICoordinate alloc] initWithCoordinate:self.defaultCoordinate pointsOffset:CGVectorMake(1, 2)]; - XCTAssertTrue(FBPointFuzzyEqualToPoint(coordinate.fb_screenPoint, CGPointMake(2, 4), 0.1)); -} - -- (void)testCoordinateWithCoordinateWithOffsetOffBounds -{ - XCUICoordinate *coordinate = [[XCUICoordinate alloc] initWithCoordinate:self.defaultCoordinate pointsOffset:CGVectorMake(200, 200)]; - XCTAssertTrue(FBPointFuzzyEqualToPoint(coordinate.fb_screenPoint, CGPointMake(10, 12), 0.1)); -} - -@end diff --git a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h index d8f272d31..cb43c334d 100644 --- a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h +++ b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.h @@ -32,6 +32,7 @@ @property (nonatomic, readwrite, getter=isWDVisible) BOOL wdVisible; @property (nonatomic, readwrite, getter=isWDAccessible) BOOL wdAccessible; @property (nonatomic, readwrite, getter=isWDFocused) BOOL wdFocused; +@property (nonatomic, readwrite, getter = isWDHittable) BOOL wdHittable; @property (copy, nonnull) NSArray *children; @property (nonatomic, readwrite, assign) XCUIElementType elementType; @property (nonatomic, readwrite, getter=isWDAccessibilityContainer) BOOL wdAccessibilityContainer; diff --git a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m index f8b39940a..2ed546319 100644 --- a/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m +++ b/WebDriverAgentTests/UnitTests_tvOS/Doubles/XCUIElementDouble.m @@ -27,6 +27,7 @@ - (id)init self.wdAccessible = YES; self.wdEnabled = YES; self.wdSelected = YES; + self.wdHittable = YES; self.wdIndex = 0; #if TARGET_OS_TV self.wdFocused = YES; diff --git a/azure-templates/bootstrap_steps.yml b/azure-templates/bootstrap_steps.yml index 83847eb74..6c96e9ead 100644 --- a/azure-templates/bootstrap_steps.yml +++ b/azure-templates/bootstrap_steps.yml @@ -3,5 +3,5 @@ steps: - script: mkdir -p ./Resources/WebDriverAgent.bundle - task: UseRubyVersion@0 inputs: - versionSpec: '<3.0' + versionSpec: '3' addToPath: true diff --git a/ci-jobs/build.yml b/ci-jobs/build.yml deleted file mode 100644 index ac0532250..000000000 --- a/ci-jobs/build.yml +++ /dev/null @@ -1,14 +0,0 @@ -jobs: - - job: create_github_release - steps: - - task: GithubRelease@0 - inputs: - action: create - githubConnection: appiumbot - repositoryName: appium/WebDriverAgent - addChangeLog: false - - template: ./templates/build.yml - parameters: - vmImage: 'macOS-10.15' - name: 'macOS_10_15' - excludeXcode: '10.3.0,10.3,11.3,11.4,12' diff --git a/ci-jobs/scripts/build-webdriveragents.js b/ci-jobs/scripts/build-webdriveragents.js index f7485e25d..be7a70496 100644 --- a/ci-jobs/scripts/build-webdriveragents.js +++ b/ci-jobs/scripts/build-webdriveragents.js @@ -1,6 +1,6 @@ const buildWebDriverAgent = require('./build-webdriveragent'); const { asyncify } = require('asyncbox'); -const { fs, logger } = require('appium/support'); +const { fs, logger } = require('@appium/support'); const { exec } = require('teen_process'); const path = require('path'); diff --git a/ci-jobs/templates/build.yml b/ci-jobs/templates/build.yml index 4d0d0ef1b..d32d34a8f 100644 --- a/ci-jobs/templates/build.yml +++ b/ci-jobs/templates/build.yml @@ -1,6 +1,6 @@ parameters: - vmImage: 'macOS-10.15' - name: macOS_10_15 + vmImage: 'macOS-11' + name: macOS_11 excludeXcode: $(excludeXcode) jobs: - job: ${{ parameters.name }} @@ -14,12 +14,11 @@ jobs: displayName: Print Tag Name - script: ls /Applications/ displayName: List Installed Applications - - task: NodeTool@0 - inputs: - versionSpec: '16.x' - - script: | - npm install -g appium@next - npm install + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: lts/* + - script: npm install displayName: Install Node Modules - script: mkdir -p Resources/WebDriverAgent.bundle displayName: Make Resources Folder diff --git a/docs/CREATING_BUNDLES.md b/docs/CREATING_BUNDLES.md deleted file mode 100644 index 404b4e505..000000000 --- a/docs/CREATING_BUNDLES.md +++ /dev/null @@ -1,15 +0,0 @@ -# Creating WebDriverAgent bundles using Azure Pipelines - -The [bundle](https://dev.azure.com/AppiumCI/Appium%20CI/_build?definitionId=36&_a=summary) pipeline uses macOS agents on Azure and iterates through every installed version of Xcode and then builds WebDriverAgent using ./scripts/build-webdriveragents.js - -The bundle pipeline is run every time there is a tag and builds to a folder called `prebuilt-agents` and those agents get uploaded to [GitHub Releases](https://github.com/appium/WebDriverAgent/releases). - -The bundle can be run manually as well, but the GitHub release will not be created. This is useful if you wish to test that it bundles correctly but don't wish to publish it. - -# Creating WebDriverAgent bundle locally - -Building a WebDriverAgent bundle locally is useful if Azure pipelines doesn't build one of the Xcode bundles that we want. - -* Run `node ./Scripts/build-webdriveragent.js` -* This will build a single bundle to `prebuilt-agents` -* The bundle will be built using the Xcode version installed locally diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 740ea352f..000000000 --- a/gulpfile.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const gulp = require('gulp'); -const boilerplate = require('@appium/gulp-plugins').boilerplate.use(gulp); -const DEFAULTS = require('@appium/gulp-plugins').boilerplate.DEFAULTS; - - -boilerplate({ - build: 'appium-webdriveragent', - files: DEFAULTS.files.concat('index.js'), - projectRoot: __dirname, -}); - -gulp.task('install:dependencies', gulp.series('transpile', function installDependencies () { - // we cannot require `fetchDependencies` at the top level because it has not - // necessarily been transpiled at that point - const { checkForDependencies } = require('./build'); - return checkForDependencies(); -})); diff --git a/index.js b/index.js index 68fe37538..3513d683b 100644 --- a/index.js +++ b/index.js @@ -3,20 +3,13 @@ import * as proxies from './lib/no-session-proxy'; import * as driver from './lib/webdriveragent'; import * as constants from './lib/constants'; import * as utils from './lib/utils'; -import { asyncify } from 'asyncbox'; const { checkForDependencies, bundleWDASim } = dependencies; const { NoSessionProxy } = proxies; const { WebDriverAgent } = driver; -const { BOOTSTRAP_PATH, WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE } = constants; -const { resetTestProcesses } = utils; - - -// When run as a command line utility, this should check for the dependencies -if (require.main === module) { - asyncify(checkForDependencies); -} +const { WDA_BASE_URL, WDA_RUNNER_BUNDLE_ID, PROJECT_FILE } = constants; +const { resetTestProcesses, BOOTSTRAP_PATH } = utils; export { WebDriverAgent, diff --git a/lib/check-dependencies.js b/lib/check-dependencies.js index d8232dc7d..ce145f5a1 100644 --- a/lib/check-dependencies.js +++ b/lib/check-dependencies.js @@ -1,16 +1,17 @@ -import { fs } from 'appium/support'; +import { fs } from '@appium/support'; import _ from 'lodash'; import { exec } from 'teen_process'; import path from 'path'; import XcodeBuild from './xcodebuild'; import { - WDA_PROJECT, WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP + WDA_SCHEME, SDK_SIMULATOR, WDA_RUNNER_APP } from './constants'; +import { BOOTSTRAP_PATH } from './utils'; import log from './logger'; async function buildWDASim () { const args = [ - '-project', WDA_PROJECT, + '-project', path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'), '-scheme', WDA_SCHEME, '-sdk', SDK_SIMULATOR, 'CODE_SIGN_IDENTITY=""', @@ -26,10 +27,9 @@ async function checkForDependencies () { return false; } -async function bundleWDASim (xcodebuild, opts = {}) { +async function bundleWDASim (xcodebuild) { if (xcodebuild && !_.isFunction(xcodebuild.retrieveDerivedDataPath)) { - xcodebuild = new XcodeBuild(); - opts = xcodebuild; + xcodebuild = new XcodeBuild('', {}); } const derivedDataPath = await xcodebuild.retrieveDerivedDataPath(); @@ -37,8 +37,7 @@ async function bundleWDASim (xcodebuild, opts = {}) { if (await fs.exists(wdaBundlePath)) { return wdaBundlePath; } - await checkForDependencies(opts); - await buildWDASim(xcodebuild, opts); + await buildWDASim(); return wdaBundlePath; } diff --git a/lib/constants.js b/lib/constants.js index ad9c2535e..82f005d1c 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,12 +1,7 @@ import path from 'path'; - -const BOOTSTRAP_PATH = __dirname.endsWith('build') - ? path.resolve(__dirname, '..', '..', '..') - : path.resolve(__dirname, '..', '..'); -const WDA_PROJECT = path.join(BOOTSTRAP_PATH, 'WebDriverAgent.xcodeproj'); -const WDA_SCRIPTS_ROOT = path.join(BOOTSTRAP_PATH, 'Scripts'); const WDA_RUNNER_BUNDLE_ID = 'com.facebook.WebDriverAgentRunner'; +const WDA_RUNNER_BUNDLE_ID_FOR_XCTEST = `${WDA_RUNNER_BUNDLE_ID}.xctrunner`; const WDA_RUNNER_APP = 'WebDriverAgentRunner-Runner.app'; const WDA_SCHEME = 'WebDriverAgentRunner'; const PROJECT_FILE = 'project.pbxproj'; @@ -21,10 +16,8 @@ const SDK_DEVICE = 'iphoneos'; const WDA_UPGRADE_TIMESTAMP_PATH = path.join('.appium', 'webdriveragent', 'upgrade.time'); export { - BOOTSTRAP_PATH, WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_APP, PROJECT_FILE, - WDA_PROJECT, WDA_SCHEME, - PLATFORM_NAME_TVOS, PLATFORM_NAME_IOS, - SDK_SIMULATOR, SDK_DEVICE, - WDA_BASE_URL, WDA_SCRIPTS_ROOT, WDA_UPGRADE_TIMESTAMP_PATH + WDA_SCHEME, PLATFORM_NAME_TVOS, PLATFORM_NAME_IOS, + SDK_SIMULATOR, SDK_DEVICE, WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, + WDA_RUNNER_BUNDLE_ID_FOR_XCTEST }; diff --git a/lib/logger.js b/lib/logger.js index 3f822d8ee..4727be0ff 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,4 +1,4 @@ -import { logger } from 'appium/support'; +import { logger } from '@appium/support'; const log = logger.getLogger('WebDriverAgent'); diff --git a/lib/no-session-proxy.js b/lib/no-session-proxy.js index f73c6eeb9..f0760c701 100644 --- a/lib/no-session-proxy.js +++ b/lib/no-session-proxy.js @@ -1,4 +1,4 @@ -import { JWProxy } from 'appium/driver'; +import { JWProxy } from '@appium/base-driver'; class NoSessionProxy extends JWProxy { diff --git a/lib/utils.js b/lib/utils.js index 00d8d5502..f52ec6fe0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,17 +1,40 @@ -import { fs, tempDir, plist } from 'appium/support'; +import { fs, plist } from '@appium/support'; import { exec } from 'teen_process'; import path from 'path'; import log from './logger'; import _ from 'lodash'; import { WDA_RUNNER_BUNDLE_ID, PLATFORM_NAME_TVOS } from './constants'; import B from 'bluebird'; +import _fs from 'fs'; import { waitForCondition } from 'asyncbox'; -const ROOT_DIR = path.basename(__dirname) === 'lib' - ? path.resolve(__dirname, process.env.NO_PRECOMPILE ? '..' : '../..') - : __dirname; const PROJECT_FILE = 'project.pbxproj'; +/** + * Calculates the path to the current module's root folder + * + * @returns {string} The full path to module root + * @throws {Error} If the current module root folder cannot be determined + */ +const getModuleRoot = _.memoize(function getModuleRoot () { + let currentDir = path.dirname(path.resolve(__filename)); + let isAtFsRoot = false; + while (!isAtFsRoot) { + const manifestPath = path.join(currentDir, 'package.json'); + try { + if (_fs.existsSync(manifestPath) && + JSON.parse(_fs.readFileSync(manifestPath, 'utf8')).name === 'appium-webdriveragent') { + return currentDir; + } + } catch (ign) {} + currentDir = path.dirname(currentDir); + isAtFsRoot = currentDir.length <= path.dirname(currentDir).length; + } + throw new Error('Cannot find the root folder of the appium-webdriveragent Node.js module'); +}); + +export const BOOTSTRAP_PATH = getModuleRoot(); + async function getPIDsUsingPattern (pattern) { const args = [ '-if', // case insensitive, full cmdline match @@ -134,18 +157,6 @@ async function setRealDeviceSecurity (keychainPath, keychainPassword) { await exec('security', ['set-keychain-settings', '-t', '3600', '-l', keychainPath]); } -async function generateXcodeConfigFile (orgId, signingId) { - log.debug(`Generating xcode config file for orgId '${orgId}' and signingId ` + - `'${signingId}'`); - const contents = `DEVELOPMENT_TEAM = ${orgId} -CODE_SIGN_IDENTITY = ${signingId} -`; - const xcconfigPath = await tempDir.path('appium-temp.xcconfig'); - log.debug(`Writing xcode config file to ${xcconfigPath}`); - await fs.writeFile(xcconfigPath, contents, 'utf8'); - return xcconfigPath; -} - /** * Information of the device under test * @typedef {Object} DeviceInfo @@ -165,8 +176,8 @@ CODE_SIGN_IDENTITY = ${signingId} * @param {DeviceInfo} deviceInfo * @param {string} sdkVersion - The Xcode SDK version of OS. * @param {string} bootstrapPath - The folder path containing xctestrun file. - * @param {string} wdaRemotePort - The remote port WDA is listening on. - * @return {string} returns xctestrunFilePath for given device + * @param {number|string} wdaRemotePort - The remote port WDA is listening on. + * @return {Promise} returns xctestrunFilePath for given device * @throws if WebDriverAgentRunner_iphoneos${sdkVersion|platformVersion}-arm64.xctestrun for real device * or WebDriverAgentRunner_iphonesimulator${sdkVersion|platformVersion}-x86_64.xctestrun for simulator is not found @bootstrapPath, * then it will throw file not found exception @@ -184,7 +195,7 @@ async function setXctestrunFile (deviceInfo, sdkVersion, bootstrapPath, wdaRemot /** * Return the WDA object which appends existing xctest runner content * @param {string} platformName - The name of the platform - * @param {string} version - The Xcode SDK version of OS. + * @param {number|string} wdaRemotePort - The remote port number * @return {object} returns a runner object which has USE_PORT */ function getAdditionalRunContent (platformName, wdaRemotePort) { @@ -205,6 +216,7 @@ function getAdditionalRunContent (platformName, wdaRemotePort) { * @param {DeviceInfo} deviceInfo * @param {string} sdkVersion - The Xcode SDK version of OS. * @param {string} bootstrapPath - The folder path containing xctestrun file. + * @returns {Promise} */ async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) { // First try the SDK path, for Xcode 10 (at least) @@ -233,9 +245,11 @@ async function getXctestrunFilePath (deviceInfo, sdkVersion, bootstrapPath) { } } - log.errorAndThrow(`If you are using 'useXctestrunFile' capability then you ` + + throw new Error( + `If you are using 'useXctestrunFile' capability then you ` + `need to have a xctestrun file (expected: ` + - `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')`); + `'${path.resolve(bootstrapPath, getXctestrunFileName(deviceInfo, sdkVersion))}')` + ); } @@ -251,12 +265,19 @@ function getXctestrunFileName (deviceInfo, version) { : `WebDriverAgentRunner_iphone${deviceInfo.isRealDevice ? `os${version}-arm64` : `simulator${version}-x86_64`}.xctestrun`; } +/** + * Ensures the process is killed after the timeout + * + * @param {string} name + * @param {import('teen_process').SubProcess} proc + * @returns {Promise} + */ async function killProcess (name, proc) { if (!proc || !proc.isRunning) { return; } - log.info(`Shutting down '${name}' process (pid '${proc.proc.pid}')`); + log.info(`Shutting down '${name}' process (pid '${proc.proc?.pid}')`); log.info(`Sending 'SIGTERM'...`); try { @@ -293,11 +314,11 @@ function randomInt (low, high) { /** * Retrieves WDA upgrade timestamp * - * @return {?number} The UNIX timestamp of the package manifest. The manifest only gets modified on + * @return {Promise} The UNIX timestamp of the package manifest. The manifest only gets modified on * package upgrade. */ async function getWDAUpgradeTimestamp () { - const packageManifest = path.resolve(ROOT_DIR, 'package.json'); + const packageManifest = path.resolve(getModuleRoot(), 'package.json'); if (!await fs.exists(packageManifest)) { return null; } @@ -333,7 +354,7 @@ async function resetTestProcesses (udid, isSimulator) { * listening on given port, and is expected to return * either true or false to include/exclude the corresponding PID * from the resulting array. - * @returns {Array} - the list of matched process ids. + * @returns {Promise} - the list of matched process ids. */ async function getPIDsListeningOnPort (port, filteringFunc = null) { const result = []; @@ -368,8 +389,8 @@ async function getPIDsListeningOnPort (port, filteringFunc = null) { } export { updateProjectFile, resetProjectFile, setRealDeviceSecurity, - getAdditionalRunContent, getXctestrunFileName, generateXcodeConfigFile, + getAdditionalRunContent, getXctestrunFileName, setXctestrunFile, getXctestrunFilePath, killProcess, randomInt, getWDAUpgradeTimestamp, resetTestProcesses, - getPIDsListeningOnPort, killAppUsingPattern, isTvOS, + getPIDsListeningOnPort, killAppUsingPattern, isTvOS }; diff --git a/lib/webdriveragent.js b/lib/webdriveragent.js index 61e65d8ea..fdae2bff6 100644 --- a/lib/webdriveragent.js +++ b/lib/webdriveragent.js @@ -2,26 +2,29 @@ import _ from 'lodash'; import path from 'path'; import url from 'url'; import B from 'bluebird'; -import { JWProxy } from 'appium/driver'; -import { fs, util, plist, mkdirp } from 'appium/support'; +import { JWProxy } from '@appium/base-driver'; +import { fs, util, plist } from '@appium/support'; import defaultLogger from './logger'; import { NoSessionProxy } from './no-session-proxy'; import { - getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort + getWDAUpgradeTimestamp, resetTestProcesses, getPIDsListeningOnPort, BOOTSTRAP_PATH } from './utils'; import XcodeBuild from './xcodebuild'; import AsyncLock from 'async-lock'; import { exec } from 'teen_process'; import { bundleWDASim } from './check-dependencies'; import { - BOOTSTRAP_PATH, WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_APP, - WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH, + WDA_RUNNER_BUNDLE_ID, WDA_RUNNER_BUNDLE_ID_FOR_XCTEST, WDA_RUNNER_APP, + WDA_BASE_URL, WDA_UPGRADE_TIMESTAMP_PATH } from './constants'; +import {Xctest} from 'appium-ios-device'; +import {strongbox} from '@appium/strongbox'; const WDA_LAUNCH_TIMEOUT = 60 * 1000; const WDA_AGENT_PORT = 8100; const WDA_CF_BUNDLE_NAME = 'WebDriverAgentRunner-Runner'; const SHARED_RESOURCES_GUARD = new AsyncLock(); +const RECENT_MODULE_VERSION_ITEM_NAME = 'recentWdaModuleVersion'; class WebDriverAgent { constructor (xcodeVersion, args = {}, log = null) { @@ -47,6 +50,9 @@ class WebDriverAgent { this.prebuildWDA = args.prebuildWDA; + // this.args.webDriverAgentUrl guiarantees the capabilities acually + // gave 'appium:webDriverAgentUrl' but 'this.webDriverAgentUrl' + // could be used for caching WDA with xcodebuild. this.webDriverAgentUrl = args.webDriverAgentUrl; this.started = false; @@ -60,31 +66,54 @@ class WebDriverAgent { this.updatedWDABundleId = args.updatedWDABundleId; - this.xcodebuild = new XcodeBuild(this.xcodeVersion, this.device, { - platformVersion: this.platformVersion, - platformName: this.platformName, - iosSdkVersion: this.iosSdkVersion, - agentPath: this.agentPath, - bootstrapPath: this.bootstrapPath, - realDevice: this.isRealDevice, - showXcodeLog: args.showXcodeLog, - xcodeConfigFile: args.xcodeConfigFile, - xcodeOrgId: args.xcodeOrgId, - xcodeSigningId: args.xcodeSigningId, - keychainPath: args.keychainPath, - keychainPassword: args.keychainPassword, - useSimpleBuildTest: args.useSimpleBuildTest, - usePrebuiltWDA: args.usePrebuiltWDA, - updatedWDABundleId: this.updatedWDABundleId, - launchTimeout: args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT, - wdaRemotePort: this.wdaRemotePort, - useXctestrunFile: this.useXctestrunFile, - derivedDataPath: args.derivedDataPath, - mjpegServerPort: this.mjpegServerPort, - allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration, - resultBundlePath: args.resultBundlePath, - resultBundleVersion: args.resultBundleVersion, - }, this.log); + this.usePreinstalledWDA = args.usePreinstalledWDA; + this.xctestApiClient = null; + + this.xcodebuild = this.canSkipXcodebuild + ? null + : new XcodeBuild(this.xcodeVersion, this.device, { + platformVersion: this.platformVersion, + platformName: this.platformName, + iosSdkVersion: this.iosSdkVersion, + agentPath: this.agentPath, + bootstrapPath: this.bootstrapPath, + realDevice: this.isRealDevice, + showXcodeLog: args.showXcodeLog, + xcodeConfigFile: args.xcodeConfigFile, + xcodeOrgId: args.xcodeOrgId, + xcodeSigningId: args.xcodeSigningId, + keychainPath: args.keychainPath, + keychainPassword: args.keychainPassword, + useSimpleBuildTest: args.useSimpleBuildTest, + usePrebuiltWDA: args.usePrebuiltWDA, + updatedWDABundleId: this.updatedWDABundleId, + launchTimeout: args.wdaLaunchTimeout || WDA_LAUNCH_TIMEOUT, + wdaRemotePort: this.wdaRemotePort, + useXctestrunFile: this.useXctestrunFile, + derivedDataPath: args.derivedDataPath, + mjpegServerPort: this.mjpegServerPort, + allowProvisioningDeviceRegistration: args.allowProvisioningDeviceRegistration, + resultBundlePath: args.resultBundlePath, + resultBundleVersion: args.resultBundleVersion, + }, this.log); + } + + /** + * Return true if the session does not need xcodebuild. + * @returns {boolean} Whether the session needs/has xcodebuild. + */ + get canSkipXcodebuild () { + // Use this.args.webDriverAgentUrl to guarantee + // the capabilities set gave the `appium:webDriverAgentUrl`. + return this.usePreinstalledWDA || this.args.webDriverAgentUrl; + } + + /** + * + * @returns {string} Bundle ID for Xctest. + */ + get bundleIdForXctest () { + return this.updatedWDABundleId ? `${this.updatedWDABundleId}.xctrunner` : WDA_RUNNER_BUNDLE_ID_FOR_XCTEST; } setWDAPaths (bootstrapPath, agentPath) { @@ -121,7 +150,7 @@ class WebDriverAgent { /** * Return boolean if WDA is running or not - * @return {boolean} True if WDA is running + * @return {Promise} True if WDA is running * @throws {Error} If there was invalid response code or body */ async isRunning () { @@ -154,7 +183,7 @@ class WebDriverAgent { * } * } * - * @return {?object} State Object + * @return {Promise} State Object * @throws {Error} If there was invalid response code or body */ async getStatus () { @@ -197,65 +226,91 @@ class WebDriverAgent { } async _cleanupProjectIfFresh () { - const homeFolder = process.env.HOME; - if (!homeFolder) { - this.log.info('The HOME folder path cannot be determined'); - return; - } - - const currentUpgradeTimestamp = await getWDAUpgradeTimestamp(); - if (!_.isInteger(currentUpgradeTimestamp)) { - this.log.info('It is impossible to determine the timestamp of the package'); + if (this.canSkipXcodebuild) { return; } - const timestampPath = path.resolve(homeFolder, WDA_UPGRADE_TIMESTAMP_PATH); - const didTimestampExist = await fs.exists(timestampPath); - if (didTimestampExist) { - try { - await fs.access(timestampPath, fs.W_OK); - } catch (ign) { - this.log.info(`WebDriverAgent upgrade timestamp at '${timestampPath}' is not writeable. ` + - `Skipping sources cleanup`); - return; - } - const recentUpgradeTimestamp = parseInt(await fs.readFile(timestampPath, 'utf8'), 10); - if (_.isInteger(recentUpgradeTimestamp)) { - if (recentUpgradeTimestamp >= currentUpgradeTimestamp) { - this.log.info(`WebDriverAgent does not need a cleanup. The sources are up to date ` + - `(${recentUpgradeTimestamp} >= ${currentUpgradeTimestamp})`); + const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8')); + const box = strongbox(packageInfo.name); + let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME); + if (!boxItem) { + const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH); + if (await fs.exists(timestampPath)) { + // TODO: It is probably a bit ugly to hardcode the recent version string, + // TODO: hovewer it should do the job as a temporary transition trick + // TODO: to switch from a hardcoded file path to the strongbox usage. + try { + boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0'); + } catch (e) { + this.log.warn(`The actual module version cannot be persisted: ${e.message}`); return; } - this.log.info(`WebDriverAgent sources have been upgraded ` + - `(${recentUpgradeTimestamp} < ${currentUpgradeTimestamp})`); } else { - this.log.warn(`The recent upgrade timestamp at '${timestampPath}' is corrupted. Trying to fix it`); + this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); + try { + await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version); + } catch (e) { + this.log.warn(`The actual module version cannot be persisted: ${e.message}`); + } + return; } } + let recentModuleVersion = await boxItem.read(); try { - await mkdirp(path.dirname(timestampPath)); - await fs.writeFile(timestampPath, `${currentUpgradeTimestamp}`, 'utf8'); - this.log.debug(`Stored the recent WebDriverAgent upgrade timestamp ${currentUpgradeTimestamp} ` + - `at '${timestampPath}'`); + recentModuleVersion = util.coerceVersion(recentModuleVersion, true); } catch (e) { - this.log.info(`Unable to create the recent WebDriverAgent upgrade timestamp at '${timestampPath}'. ` + - `Original error: ${e.message}`); + this.log.warn(`The persisted module version string has been damaged: ${e.message}`); + this.log.info(`Updating it to '${packageInfo.version}' assuming the project clenup is not needed`); + await boxItem.write(packageInfo.version); return; } - if (!didTimestampExist) { - this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); + if (util.compareVersions(recentModuleVersion, '>=', packageInfo.version)) { + this.log.info( + `WebDriverAgent does not need a cleanup. The project sources are up to date ` + + `(${recentModuleVersion} >= ${packageInfo.version})` + ); return; } + this.log.info( + `Cleaning up the WebDriverAgent project after the module upgrade has happened ` + + `(${recentModuleVersion} < ${packageInfo.version})` + ); try { + // @ts-ignore xcodebuild should be set await this.xcodebuild.cleanProject(); + await boxItem.write(packageInfo.version); } catch (e) { this.log.warn(`Cannot perform WebDriverAgent project cleanup. Original error: ${e.message}`); } } + /** + * Launch WDA with preinstalled package without xcodebuild. + * @param {string} sessionId Launch WDA and establish the session with this sessionId + * @return {Promise} State Object + */ + async launchWithPreinstalledWDA(sessionId) { + const xctestEnv = { + USE_PORT: this.wdaLocalPort || WDA_AGENT_PORT, + WDA_PRODUCT_BUNDLE_IDENTIFIER: this.bundleIdForXctest + }; + if (this.mjpegServerPort) { + xctestEnv.MJPEG_SERVER_PORT = this.mjpegServerPort; + } + this.log.info('Launching WebDriverAgent on the device without xcodebuild'); + this.xctestApiClient = new Xctest(this.device.udid, this.bundleIdForXctest, null, {env: xctestEnv}); + + await this.xctestApiClient.start(); + + this.setupProxies(sessionId); + const status = await this.getStatus(); + this.started = true; + return status; + } + /** * Return current running WDA's status like below after launching WDA * { @@ -276,7 +331,7 @@ class WebDriverAgent { * } * * @param {string} sessionId Launch WDA and establish the session with this sessionId - * @return {?object} State Object + * @return {Promise} State Object * @throws {Error} If there was invalid response code or body */ async launch (sessionId) { @@ -287,6 +342,13 @@ class WebDriverAgent { return await this.getStatus(); } + if (this.usePreinstalledWDA) { + if (this.isRealDevice) { + return await this.launchWithPreinstalledWDA(sessionId); + } + throw new Error('usePreinstalledWDA is available only for a real device.'); + } + this.log.info('Launching WebDriverAgent on the device'); this.setupProxies(sessionId); @@ -313,12 +375,15 @@ class WebDriverAgent { return await this.startWithIDB(); } + // @ts-ignore xcodebuild should be set await this.xcodebuild.init(this.noSessionProxy); // Start the xcodebuild process if (this.prebuildWDA) { + // @ts-ignore xcodebuild should be set await this.xcodebuild.prebuild(); } + // @ts-ignore xcodebuild should be set return await this.xcodebuild.start(); } @@ -394,10 +459,20 @@ class WebDriverAgent { } async quit () { - this.log.info('Shutting down sub-processes'); - - await this.xcodebuild.quit(); - await this.xcodebuild.reset(); + if (this.usePreinstalledWDA) { + if (this.xctestApiClient) { + this.log.info('Stopping the XCTest session'); + this.xctestApiClient.stop(); + this.xctestApiClient = null; + } + } else if (!this.args.webDriverAgentUrl) { + this.log.info('Shutting down sub-processes'); + await this.xcodebuild?.quit(); + await this.xcodebuild?.reset(); + } else { + this.log.debug('Do not stop xcodebuild nor XCTest session ' + + 'since the WDA session is managed by outside this driver.'); + } if (this.jwproxy) { this.jwproxy.sessionId = null; @@ -433,11 +508,15 @@ class WebDriverAgent { return this.started; } - set fullyStarted (started = false) { - this.started = started; + set fullyStarted (started) { + this.started = started ?? false; } async retrieveDerivedDataPath () { + if (this.canSkipXcodebuild) { + return; + } + // @ts-ignore xcodebuild should be set return await this.xcodebuild.retrieveDerivedDataPath(); } @@ -445,8 +524,6 @@ class WebDriverAgent { * Reuse running WDA if it has the same bundle id with updatedWDABundleId. * Or reuse it if it has the default id without updatedWDABundleId. * Uninstall it if the method faces an exception for the above situation. - * - * @param {string} updatedWDABundleId BundleId you'd like to use */ async setupCaching () { const status = await this.getStatus(); diff --git a/lib/xcodebuild.js b/lib/xcodebuild.js index 2bd35239b..bdd484db2 100644 --- a/lib/xcodebuild.js +++ b/lib/xcodebuild.js @@ -1,12 +1,13 @@ import { retryInterval } from 'asyncbox'; import { SubProcess, exec } from 'teen_process'; -import { fs, logger, timing } from 'appium/support'; +import { logger, timing } from '@appium/support'; import defaultLogger from './logger'; import B from 'bluebird'; import { - setRealDeviceSecurity, generateXcodeConfigFile, setXctestrunFile, + setRealDeviceSecurity, setXctestrunFile, updateProjectFile, resetProjectFile, killProcess, - getWDAUpgradeTimestamp, isTvOS } from './utils'; + getWDAUpgradeTimestamp, isTvOS +} from './utils'; import _ from 'lodash'; import path from 'path'; import { EOL } from 'os'; @@ -33,6 +34,15 @@ const xcodeLog = logger.getLogger('Xcode'); class XcodeBuild { + /** @type {SubProcess} */ + xcodebuild; + + /** + * @param {string} xcodeVersion + * @param {any} device + * @param {any} args + * @param {import('@appium/types').AppiumLogger?} log + */ constructor (xcodeVersion, device, args = {}, log = null) { this.xcodeVersion = xcodeVersion; @@ -77,6 +87,9 @@ class XcodeBuild { this.resultBundlePath = args.resultBundlePath; this.resultBundleVersion = args.resultBundleVersion; + + this._didBuildFail = false; + this._didProcessExit = false; } async init (noSessionProxy) { @@ -154,16 +167,15 @@ class XcodeBuild { this.usePrebuiltWDA = true; await this.start(true); - this.xcodebuild = null; - - // pause a moment - await B.delay(this.prebuildDelay); + if (this.prebuildDelay > 0) { + // pause a moment + await B.delay(this.prebuildDelay); + } } async cleanProject () { - const tmpIsTvOS = isTvOS(this.platformName); - const libScheme = tmpIsTvOS ? LIB_SCHEME_TV : LIB_SCHEME_IOS; - const runnerScheme = tmpIsTvOS ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; + const libScheme = isTvOS(this.platformName) ? LIB_SCHEME_TV : LIB_SCHEME_IOS; + const runnerScheme = isTvOS(this.platformName) ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; for (const scheme of [libScheme, runnerScheme]) { this.log.debug(`Cleaning the project scheme '${scheme}' to make sure there are no leftovers from previous installs`); @@ -202,7 +214,7 @@ class XcodeBuild { args.push('-resultBundleVersion', this.resultBundleVersion); } - if (this.useXctestrunFile) { + if (this.useXctestrunFile && this.xctestrunFilePath) { args.push('-xctestrun', this.xctestrunFilePath); } else { const runnerScheme = isTvOS(this.platformName) ? RUNNER_SCHEME_TV : RUNNER_SCHEME_IOS; @@ -215,15 +227,25 @@ class XcodeBuild { const versionMatch = new RegExp(/^(\d+)\.(\d+)/).exec(this.platformVersion); if (versionMatch) { - args.push(`IPHONEOS_DEPLOYMENT_TARGET=${versionMatch[1]}.${versionMatch[2]}`); + args.push( + `${isTvOS(this.platformName) ? 'TV' : 'IPHONE'}OS_DEPLOYMENT_TARGET=${versionMatch[1]}.${versionMatch[2]}` + ); } else { this.log.warn(`Cannot parse major and minor version numbers from platformVersion "${this.platformVersion}". ` + 'Will build for the default platform instead'); } - if (this.realDevice && this.xcodeConfigFile) { - this.log.debug(`Using Xcode configuration file: '${this.xcodeConfigFile}'`); - args.push('-xcconfig', this.xcodeConfigFile); + if (this.realDevice) { + if (this.xcodeConfigFile) { + this.log.debug(`Using Xcode configuration file: '${this.xcodeConfigFile}'`); + args.push('-xcconfig', this.xcodeConfigFile); + } + if (this.xcodeOrgId && this.xcodeSigningId) { + args.push( + `DEVELOPMENT_TEAM=${this.xcodeOrgId}`, + `CODE_SIGN_IDENTITY=${this.xcodeSigningId}`, + ); + } } if (!process.env.APPIUM_XCUITEST_TREAT_WARNINGS_AS_ERRORS) { @@ -243,14 +265,12 @@ class XcodeBuild { if (this.keychainPath && this.keychainPassword) { await setRealDeviceSecurity(this.keychainPath, this.keychainPassword); } - if (this.xcodeOrgId && this.xcodeSigningId && !this.xcodeConfigFile) { - this.xcodeConfigFile = await generateXcodeConfigFile(this.xcodeOrgId, this.xcodeSigningId); - } } const {cmd, args} = this.getCommand(buildOnly); this.log.debug(`Beginning ${buildOnly ? 'build' : 'test'} with command '${cmd} ${args.join(' ')}' ` + `in directory '${this.bootstrapPath}'`); + /** @type {Record} */ const env = Object.assign({}, process.env, { USE_PORT: this.wdaRemotePort, WDA_PRODUCT_BUNDLE_IDENTIFIER: this.updatedWDABundleId || WDA_RUNNER_BUNDLE_ID, @@ -259,10 +279,11 @@ class XcodeBuild { // https://github.com/appium/WebDriverAgent/pull/105 env.MJPEG_SERVER_PORT = this.mjpegServerPort; } - const upgradeTimestamp = await getWDAUpgradeTimestamp(this.bootstrapPath); + const upgradeTimestamp = await getWDAUpgradeTimestamp(); if (upgradeTimestamp) { env.UPGRADE_TIMESTAMP = upgradeTimestamp; } + this._didBuildFail = false; const xcodebuild = new SubProcess(cmd, args, { cwd: this.bootstrapPath, env, @@ -277,14 +298,6 @@ class XcodeBuild { this.log.debug(`${logMsg}. To change this, use 'showXcodeLog' desired capability`); xcodebuild.on('output', (stdout, stderr) => { let out = stdout || stderr; - // we want to pull out the log file that is created, and highlight it - // for diagnostic purposes - if (out.includes('Writing diagnostic log for test session to')) { - // pull out the first line that begins with the path separator - // which *should* be the line indicating the log file generated - xcodebuild.logLocation = _.first(_.remove(out.trim().split('\n'), (v) => v.startsWith(path.sep))); - xcodeLog.debug(`Log file for xcodebuild test: ${xcodebuild.logLocation}`); - } // if we have an error we want to output the logs // otherwise the failure is inscrutible @@ -293,17 +306,14 @@ class XcodeBuild { if (this.showXcodeLog !== false && out.includes('Error Domain=') && !ignoreError) { logXcodeOutput = true; - // terrible hack to handle case where xcode return 0 but is failing - xcodebuild._wda_error_occurred = true; + // handle case where xcode returns 0 but is failing + this._didBuildFail = true; } // do not log permission errors from trying to write to attachments folder if (logXcodeOutput && !ignoreError) { for (const line of out.split(EOL)) { xcodeLog.error(line); - if (line) { - xcodebuild._wda_error_message += `${EOL}${line}`; - } } } }); @@ -313,30 +323,27 @@ class XcodeBuild { async start (buildOnly = false) { this.xcodebuild = await this.createSubProcess(buildOnly); - // Store xcodebuild message - this.xcodebuild._wda_error_message = ''; // wrap the start procedure in a promise so that we can catch, and report, // any startup errors that are thrown as events return await new B((resolve, reject) => { - this.xcodebuild.on('exit', async (code, signal) => { + this.xcodebuild.once('exit', (code, signal) => { xcodeLog.error(`xcodebuild exited with code '${code}' and signal '${signal}'`); - // print out the xcodebuild file if users have asked for it - if (this.showXcodeLog && this.xcodebuild.logLocation) { - xcodeLog.error(`Contents of xcodebuild log file '${this.xcodebuild.logLocation}':`); - try { - let data = await fs.readFile(this.xcodebuild.logLocation, 'utf8'); - for (let line of data.split('\n')) { - xcodeLog.error(line); - } - } catch (err) { - xcodeLog.error(`Unable to access xcodebuild log file: '${err.message}'`); + this.xcodebuild.removeAllListeners(); + this.didProcessExit = true; + if (this._didBuildFail || (!signal && code !== 0)) { + let errorMessage = `xcodebuild failed with code ${code}.` + + ` This usually indicates an issue with the local Xcode setup or WebDriverAgent` + + ` project configuration or the driver-to-platform version mismatch.`; + if (!this.showXcodeLog) { + errorMessage += ` Consider setting 'showXcodeLog' capability to true in` + + ` order to check the Appium server log for build-related error messages.`; + } else if (this.realDevice) { + errorMessage += ` Consider checking the WebDriverAgent configuration guide` + + ` for real iOS devices at` + + ` https://github.com/appium/appium-xcuitest-driver/blob/master/docs/real-device-config.md.`; } - } - this.xcodebuild.processExited = true; - if (this.xcodebuild._wda_error_occurred || (!signal && code !== 0)) { - return reject(new Error(`xcodebuild failed with code ${code}${EOL}` + - `xcodebuild error message:${EOL}${this.xcodebuild._wda_error_message}`)); + return reject(new Error(errorMessage)); } // in the case of just building, the process will exit and that is our finish if (buildOnly) { @@ -366,12 +373,13 @@ class XcodeBuild { this.log.debug(`Waiting up to ${this.launchTimeout}ms for WebDriverAgent to start`); let currentStatus = null; try { - let retries = parseInt(this.launchTimeout / 500, 10); + const retries = Math.trunc(this.launchTimeout / 500); await retryInterval(retries, 1000, async () => { - if (this.xcodebuild.processExited) { + if (this._didProcessExit) { // there has been an error elsewhere and we need to short-circuit - return; + return currentStatus; } + const proxyTimeout = this.noSessionProxy.timeout; this.noSessionProxy.timeout = 1000; try { @@ -388,17 +396,18 @@ class XcodeBuild { } }); - if (this.xcodebuild.processExited) { + if (this._didProcessExit) { // there has been an error elsewhere and we need to short-circuit return currentStatus; } this.log.debug(`WebDriverAgent successfully started after ${timer.getDuration().asMilliSeconds.toFixed(0)}ms`); } catch (err) { - // at this point, if we have not had any errors from xcode itself (reported - // elsewhere), we can let this go through and try to create the session - this.log.debug(err.message); - this.log.warn(`Getting status of WebDriverAgent on device timed out. Continuing`); + this.log.debug(err.stack); + throw new Error( + `We were not able to retrieve the /status response from the WebDriverAgent server after ${this.launchTimeout}ms timeout.` + + `Try to increase the value of 'appium:wdaLaunchTimeout' capability as a possible workaround.` + ); } return currentStatus; } diff --git a/package.json b/package.json index 0efc1500b..e5927c1fb 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,41 @@ { "name": "appium-webdriveragent", - "version": "4.8.1", + "version": "5.15.5", "description": "Package bundling WebDriverAgent", - "main": "build/index.js", + "main": "./build/index.js", "scripts": { - "test": "gulp once", - "e2e-test": "npm run build && _FORCE_LOGS=1 npx mocha -t 0 -R spec build/test/functional --exit", - "clean": "rm -rf node_modules && rm -f package-lock.json && npm install", - "install:dependencies": "gulp install:dependencies", - "build": "gulp transpile", - "prepare": "gulp prepublish", - "prepublishOnly": "npm run bundle", - "lint": "gulp lint", - "lint:fix": "gulp eslint --fix", + "build": "tsc -b", + "dev": "npm run build -- --watch", + "clean": "npm run build -- --clean", + "lint": "eslint .", + "lint:fix": "npm run lint -- --fix", "precommit-msg": "echo 'Pre-commit checks...' && exit 0", - "precommit-test": "gulp lint", - "bundle": "node ./Scripts/build-webdriveragent.js", - "fetch-prebuilt-wda": "node ./Scripts/fetch-prebuilt-wda" + "precommit-lint": "lint-staged", + "prepare": "npm run build", + "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.js\"", + "e2e-test": "mocha --exit --timeout 10m \"./test/functional/**/*-specs.js\"", + "bundle": "npm run bundle:ios && npm run bundle:tv", + "bundle:ios": "TARGET=runner SDK=sim node ./Scripts/build-webdriveragent.js", + "bundle:tv": "TARGET=tv_runner SDK=tv_sim node ./Scripts/build-webdriveragent.js", + "fetch-prebuilt-wda": "node ./Scripts/fetch-prebuilt-wda.js" }, - "bin": { - "appium-wda-bootstrap": "./build/index.js" + "engines": { + "node": ">=14", + "npm": ">=8" + }, + "lint-staged": { + "*.js": [ + "eslint --fix" + ] + }, + "prettier": { + "bracketSpacing": false, + "printWidth": 100, + "singleQuote": true }, "pre-commit": [ "precommit-msg", - "precommit-test" + "precommit-lint" ], "repository": { "type": "git", @@ -36,43 +48,64 @@ "Selenium", "WebDriverAgent" ], - "author": "appium", + "author": "Appium Contributors", "license": "Apache-2.0", "bugs": { "url": "https://github.com/appium/WebDriverAgent/issues" }, "homepage": "https://github.com/appium/WebDriverAgent#readme", "devDependencies": { - "@appium/gulp-plugins": "^7.0.0", - "@appium/eslint-config-appium": "^6.0.0", - "@appium/test-support": "^1.0.0", + "@appium/eslint-config-appium": "^8.0.4", + "@appium/eslint-config-appium-ts": "^0.x", + "@appium/test-support": "^3.0.0", + "@appium/tsconfig": "^0.x", + "@appium/types": "^0.x", + "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "appium-xcode": "^4.0.0", + "@types/bluebird": "^3.5.38", + "@types/chai": "^4.3.5", + "@types/chai-as-promised": "^7.1.5", + "@types/lodash": "^4.14.196", + "@types/mocha": "^10.0.1", + "@types/node": "^20.4.7", + "@types/sinon": "^17.0.0", + "@types/sinon-chai": "^3.2.9", + "@types/teen_process": "^2.0.1", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", + "appium-xcode": "^5.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "eslint-config-prettier": "^8.5.0", - "glob": "^8.0.1", - "gulp": "^4.0.2", - "ios-uicatalog": "^3.5.0", + "conventional-changelog-conventionalcommits": "^7.0.1", + "eslint": "^8.46.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.28.0", + "eslint-plugin-mocha": "^10.1.0", + "eslint-plugin-promise": "^6.1.1", + "lint-staged": "^15.0.2", "mocha": "^10.0.0", "pre-commit": "^1.2.2", - "semantic-release": "^19.0.2", - "sinon": "^14.0.0" - }, - "peerDependencies": { - "appium": "^2.0.0-beta.40" + "prettier": "^3.0.0", + "semantic-release": "^22.0.5", + "sinon": "^17.0.0", + "ts-node": "^10.9.1", + "typescript": "~5.2" }, "dependencies": { - "@babel/runtime": "^7.0.0", - "appium-ios-simulator": "^4.0.0", + "@appium/base-driver": "^9.0.0", + "@appium/strongbox": "^0.x", + "@appium/support": "^4.0.0", + "appium-ios-device": "^2.5.0", + "appium-ios-simulator": "^5.0.1", "async-lock": "^1.0.0", - "asyncbox": "^2.5.3", - "axios": "^0.x", + "asyncbox": "^3.0.0", + "axios": "^1.4.0", "bluebird": "^3.5.5", "lodash": "^4.17.11", "node-simctl": "^7.0.1", "source-map-support": "^0.x", - "teen_process": "^1.14.1" + "teen_process": "^2.0.0" }, "files": [ "index.js", @@ -82,8 +115,6 @@ "Scripts/build.sh", "Scripts/fetch-prebuilt-wda.js", "Scripts/build-webdriveragent.js", - "Cartfile", - "Cartfile.resolved", "Configurations", "PrivateHeaders", "WebDriverAgent.xcodeproj", @@ -91,6 +122,6 @@ "WebDriverAgentRunner", "WebDriverAgentTests", "XCTWebDriverAgentLib", - "WebDriverAgentRunner-Runner.app.zip" + "CHANGELOG.md" ] } diff --git a/test/functional/desired.js b/test/functional/desired.js index 98304f1ff..52a3d1932 100644 --- a/test/functional/desired.js +++ b/test/functional/desired.js @@ -1,147 +1,5 @@ -import _ from 'lodash'; -import path from 'path'; -import glob from 'glob'; -import fs from 'fs'; -import { system, util } from 'appium/support'; +import { util } from '@appium/support'; - -// translate integer environment variable to a boolean 0=false, !0=true -function checkFeatureInEnv (envArg) { - let feature = parseInt(process.env[envArg], 10); - if (isNaN(feature)) { - feature = process.env[envArg]; - } - return !!feature; -} - -const PLATFORM_VERSION = process.env.PLATFORM_VERSION ? process.env.PLATFORM_VERSION : '11.3'; -const LAUNCH_WITH_IDB = process.env.LAUNCH_WITH_IDB; - -// If it's real device cloud, don't set a device name. Use dynamic device allocation. -const DEVICE_NAME = process.env.DEVICE_NAME - ? process.env.DEVICE_NAME - : process.env.SAUCE_RDC - ? undefined - : util.compareVersions(PLATFORM_VERSION, '>=', '13.0') ? 'iPhone 8' : 'iPhone 6'; - -const SHOW_XCODE_LOG = checkFeatureInEnv('SHOW_XCODE_LOG'); -const REAL_DEVICE = checkFeatureInEnv('REAL_DEVICE'); -let XCCONFIG_FILE = process.env.XCCONFIG_FILE; -if (REAL_DEVICE && !XCCONFIG_FILE) { - // no xcconfig file specified, so try to find in the root directory of the package - // this happens once, at the start of a test run, so using sync method is ok - let cwd = path.resolve(__dirname, '..', '..', '..'); - let files = glob.sync('*.xcconfig', { cwd }); - if (files.length) { - XCCONFIG_FILE = path.resolve(cwd, _.first(files)); - } -} - -// Had to make these two optional dependencies so the tests -// can still run in linux -let uiCatalogPath; -if (system.isMac() && !process.env.CLOUD) { - // iOS 13+ need a slightly different app to be able to get the correct automation - uiCatalogPath = parseInt(PLATFORM_VERSION, 10) >= 13 - ? require('ios-uicatalog').uiKitCatalog.absolute - : require('ios-uicatalog').uiCatalog.absolute; -} - -const apps = {}; - -const CLOUD = process.env.CLOUD; - -if (REAL_DEVICE) { - if (CLOUD) { - apps.testAppId = 1; - } else { - apps.uiCatalogApp = uiCatalogPath.iphoneos; - } -} else { - if (CLOUD) { - apps.uiCatalogApp = 'http://appium.github.io/appium/assets/UICatalog9.4.app.zip'; - apps.touchIdApp = null; // TODO: Upload this to appium.io - } else { - apps.uiCatalogApp = uiCatalogPath.iphonesimulator; - apps.touchIdApp = path.resolve('.', 'test', 'assets', 'TouchIDExample.app'); - } -} - -const REAL_DEVICE_CAPS = REAL_DEVICE ? { - udid: 'auto', - xcodeConfigFile: XCCONFIG_FILE, - webkitResponseTimeout: 30000, - testobject_app_id: apps.testAppId, - testobject_api_key: process.env.SAUCE_RDC_ACCESS_KEY, - testobject_remote_appium_url: process.env.APPIUM_STAGING_URL, // TODO: Once RDC starts supporting this again, re-insert this -} : {}; - -let GENERIC_CAPS = { - platformName: 'iOS', - platformVersion: PLATFORM_VERSION, - deviceName: DEVICE_NAME, - automationName: 'XCUITest', - launchWithIDB: !!LAUNCH_WITH_IDB, - noReset: true, - maxTypingFrequency: 30, - clearSystemFiles: true, - showXcodeLog: SHOW_XCODE_LOG, - wdaLaunchTimeout: (60 * 1000 * 4), - wdaConnectionTimeout: (60 * 1000 * 8), - useNewWDA: true, - simulatorStartupTimeout: 240000, -}; - -if (process.env.CLOUD) { - GENERIC_CAPS.platformVersion = process.env.CLOUD_PLATFORM_VERSION; - GENERIC_CAPS.build = process.env.SAUCE_BUILD; - GENERIC_CAPS.showIOSLog = false; - GENERIC_CAPS[process.env.APPIUM_BUNDLE_CAP || 'appium-version'] = {'appium-url': 'sauce-storage:appium.zip'}; - // TODO: If it's SAUCE_RDC add the appium staging URL - - // `name` will be set during session initialization -} - -// on Travis, when load is high, the app often fails to build, -// and tests fail, so use static one in assets if necessary, -// but prefer to have one build locally -// only do this for sim, since real device one needs to be built with dev creds -if (!REAL_DEVICE && !process.env.CLOUD) { - // this happens a single time, at load-time for the test suite, - // so sync method is not overly problematic - if (!fs.existsSync(apps.uiCatalogApp)) { - apps.uiCatalogApp = path.resolve('.', 'test', 'assets', - `${parseInt(PLATFORM_VERSION, 10) >= 13 ? 'UIKitCatalog' : 'UICatalog'}-iphonesimulator.app`); - } - if (!fs.existsSync(apps.iosTestApp)) { - apps.iosTestApp = path.resolve('.', 'test', 'assets', 'TestApp-iphonesimulator.app'); - } -} - -const UICATALOG_CAPS = _.defaults({ - app: apps.uiCatalogApp, -}, GENERIC_CAPS, REAL_DEVICE_CAPS); - -const UICATALOG_SIM_CAPS = _.defaults({ - app: apps.uiCatalogApp, -}, GENERIC_CAPS); -delete UICATALOG_SIM_CAPS.noReset; // do not want to have no reset on the tests that use this - -const W3C_CAPS = { - capabilities: { - alwaysMatch: UICATALOG_CAPS, - firstMatch: [{}], - } -}; - -let TVOS_CAPS = _.defaults({ - platformName: 'tvOS', - bundleId: 'com.apple.TVSettings', - deviceName: 'Apple TV' -}, GENERIC_CAPS); - -export { - UICATALOG_CAPS, UICATALOG_SIM_CAPS, - PLATFORM_VERSION, DEVICE_NAME, W3C_CAPS, - TVOS_CAPS -}; +export const PLATFORM_VERSION = process.env.PLATFORM_VERSION ? process.env.PLATFORM_VERSION : '11.3'; +export const DEVICE_NAME = process.env.DEVICE_NAME + || (util.compareVersions(PLATFORM_VERSION, '>=', '13.0') ? 'iPhone X' : 'iPhone 6'); diff --git a/test/functional/webdriveragent-e2e-specs.js b/test/functional/webdriveragent-e2e-specs.js index ae63a799d..e715fb6d4 100644 --- a/test/functional/webdriveragent-e2e-specs.js +++ b/test/functional/webdriveragent-e2e-specs.js @@ -10,10 +10,11 @@ import { retryInterval } from 'asyncbox'; import { WebDriverAgent } from '../../lib/webdriveragent'; import axios from 'axios'; +const MOCHA_TIMEOUT_MS = 60 * 1000 * 4; const SIM_DEVICE_NAME = 'webDriverAgentTest'; +const SIM_STARTUP_TIMEOUT_MS = MOCHA_TIMEOUT_MS; -const MOCHA_TIMEOUT = 60 * 1000 * 4; chai.should(); chai.use(chaiAsPromised); @@ -29,13 +30,12 @@ function getStartOpts (device) { realDevice: false, showXcodeLog: true, wdaLaunchTimeout: 60 * 3 * 1000, - simulatorStartupTimeout: 60 * 4 * 1000, }; } describe('WebDriverAgent', function () { - this.timeout(MOCHA_TIMEOUT); + this.timeout(MOCHA_TIMEOUT_MS); let xcodeVersion; before(async function () { @@ -61,7 +61,7 @@ describe('WebDriverAgent', function () { }); after(async function () { - this.timeout(MOCHA_TIMEOUT); + this.timeout(MOCHA_TIMEOUT_MS); await shutdownSimulator(device); @@ -72,7 +72,7 @@ describe('WebDriverAgent', function () { this.timeout(6 * 60 * 1000); beforeEach(async function () { await killAllSimulators(); - await device.run(); + await device.run({startupTimeout: SIM_STARTUP_TIMEOUT_MS}); }); afterEach(async function () { try { diff --git a/test/unit/utils-specs.js b/test/unit/utils-specs.js index 7ce35763c..1d61b4854 100644 --- a/test/unit/utils-specs.js +++ b/test/unit/utils-specs.js @@ -3,7 +3,7 @@ import { PLATFORM_NAME_IOS, PLATFORM_NAME_TVOS } from '../../lib/constants'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import { withMocks } from '@appium/test-support'; -import { fs } from 'appium/support'; +import { fs } from '@appium/support'; import path from 'path'; import { fail } from 'assert'; diff --git a/test/unit/webdriveragent-specs.js b/test/unit/webdriveragent-specs.js index bfdeab9f9..6eac94c5b 100644 --- a/test/unit/webdriveragent-specs.js +++ b/test/unit/webdriveragent-specs.js @@ -1,4 +1,5 @@ -import { WebDriverAgent, BOOTSTRAP_PATH } from '../..'; +import { BOOTSTRAP_PATH } from '../../lib/utils'; +import { WebDriverAgent } from '../../lib/webdriveragent'; import * as utils from '../../lib/utils'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..33d83ef45 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@appium/tsconfig/tsconfig.json", + "compilerOptions": { + "strict": false, // TODO: make this flag true + "outDir": "build", + "types": ["node"], + "checkJs": true + }, + "include": [ + "index.js", + "lib" + ] +}