diff --git a/.config-pep8 b/.config-pep8
deleted file mode 100644
index 68859ad03..000000000
--- a/.config-pep8
+++ /dev/null
@@ -1,2 +0,0 @@
-[pep8]
-max-line-length = 120
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..13a08e874
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+- package-ecosystem: pip
+  directory: "/"
+  schedule:
+    interval: daily
+    time: "11:00"
+  open-pull-requests-limit: 10
diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml
new file mode 100644
index 000000000..45bf02b8a
--- /dev/null
+++ b/.github/workflows/functional-test.yml
@@ -0,0 +1,292 @@
+name: Functional Tests
+
+on:
+  # Run by manual at this time
+  workflow_dispatch:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  ios_test:
+    strategy:
+      fail-fast: false
+      matrix:
+        test_targets:
+          - target: test/functional/ios/safari_tests.py
+            name: func_test_ios1
+
+    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@v4
+
+    - name: Install Node.js
+      uses: actions/setup-node@v4
+      with:
+        node-version: 'lts/*'
+
+    - name: Select Xcode
+      uses: maxim-lobanov/setup-xcode@v1
+      with:
+        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: ${{ env.IPHONE_MODEL }}
+        os_version: ${{ env.IOS_VERSION }}
+
+    # needed?
+    - run: brew install ffmpeg
+
+    # Start Appium
+    - 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 &
+
+    - run: |
+        appium driver run xcuitest download-wda-sim  --platform=ios --outdir=${{ github.workspace }}/wda
+      name: Downloading prebuilt WDA
+
+    - name: Set up Python 3.12
+      uses: actions/setup-python@v5
+      with:
+        python-version: 3.12
+
+    - name: Install uv
+      run: make install-uv
+
+    - name: Run Tests
+      run: |
+        uv run pytest ${{ matrix.test_targets.target}} --doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html
+      env:
+        LOCAL_PREBUILT_WDA: ${{ github.workspace }}/wda/WebDriverAgentRunner-Runner.app
+
+    - name: Save server output
+      if: ${{ always() }}
+      uses: actions/upload-artifact@master
+      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/appium_service_tests.py test/functional/android/chrome_tests.py test/functional/android/bidi_tests.py
+            name: func_test_android1
+
+    runs-on: ubuntu-latest
+
+    env:
+      API_LEVEL: 29
+      ARCH: x86
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - uses: actions/setup-java@v4
+      with:
+        distribution: 'temurin'
+        java-version: '17'
+
+    - name: Install Node.js
+      uses: actions/setup-node@v4
+      with:
+        node-version: 'lts/*'
+
+    # Start Appium
+    - run: npm install -g appium
+    - run: |
+        appium driver install uiautomator2
+        appium driver install espresso
+        appium plugin install execute-driver
+        nohup appium --use-plugins=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.12
+      uses: actions/setup-python@v5
+      with:
+        python-version: 3.12
+
+    - name: run tests
+      uses: reactivecircus/android-emulator-runner@v2
+      with:
+        api-level: ${{ env.API_LEVEL }}
+        arch: ${{ env.ARCH }}
+        script: |
+          make install-uv
+          uv run 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
+
+  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@v5
+      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: |
+          make install-uv
+          uv run 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: |
+        make install-uv
+        export PLATFORM=ios
+        uv run 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/.github/workflows/lock-update.yml b/.github/workflows/lock-update.yml
new file mode 100644
index 000000000..9d50ac2a4
--- /dev/null
+++ b/.github/workflows/lock-update.yml
@@ -0,0 +1,31 @@
+name: Update uv.lock
+
+on:
+  pull_request:
+    types: [ opened, synchronize ]
+    paths:
+      - 'pyproject.toml'
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  uv-lock-update:
+    if: github.actor == 'dependabot[bot]'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+          ref: ${{ github.head_ref }}
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.12'
+      - name: Install uv
+        run: make install-uv
+      - run: uv lock
+      - uses: stefanzweifel/git-auto-commit-action@v5
+        with:
+          commit_message: "chore: Refresh uv.lock"
diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml
new file mode 100644
index 000000000..51c3a5065
--- /dev/null
+++ b/.github/workflows/pr-title.yml
@@ -0,0 +1,16 @@
+name: Conventional Commits
+on:
+  pull_request:
+    types: [opened, edited, synchronize, reopened]
+
+
+jobs:
+  lint:
+    name: https://www.conventionalcommits.org
+    runs-on: ubuntu-latest
+    steps:
+      - uses: beemojs/conventional-pr-action@v3
+        with:
+          config-preset: angular
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
new file mode 100644
index 000000000..de0fc07bd
--- /dev/null
+++ b/.github/workflows/unit-test.yml
@@ -0,0 +1,27 @@
+name: Unit tests
+
+on:
+  workflow_dispatch:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+jobs:
+  test-ubuntu:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v5
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install uv
+      run: make install-uv
+    - name: Run Checks
+      run: make check
+    - name: Run Unit Tests
+      run: make unittest
diff --git a/.gitignore b/.gitignore
index 710b3114c..9ad0aa1d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
 *._*
 *.log
 *.log.*
+_*/
 
 *.pyc
 *.egg-info
@@ -14,15 +15,16 @@ MANIFEST
 build
 dist
 
+# Cache
 .cache
 __pycache__
 .idea
 .pytest_cache
+.mypy_cache
 
 # Virtual Environments
 venv*
+.venv
 .tox
 
-Pipfile.lock
-
 .coverage
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 85106e63f..000000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,4 +0,0 @@
-[settings]
-multi_line_output = 3
-known_third_party = dateutil,httpretty,pytest,selenium,setuptools,urllib3,mock,sauceclient
-known_first_party = test,appium
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c612326a0..17b86e2a6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,10 @@
--   repo: https://github.com/pre-commit/mirrors-autopep8
-    rev: v1.4
-    hooks:
-    -   id: autopep8
-        args: ["-a", "--global-config", ".config-pep8", "-i"]
--   repo: https://github.com/pre-commit/mirrors-isort
-    rev: v4.3.20
-    hooks:
-    -   id: isort
-        args: ["-rc", "."]
+repos:
+- repo: https://github.com/astral-sh/ruff-pre-commit
+  # Ruff version.
+  rev: v0.9.0
+  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 1d5306afa..000000000
--- a/.pylintrc
+++ /dev/null
@@ -1,553 +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*(# )?<?https?://\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
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/.travis.yml b/.travis.yml
deleted file mode 100644
index 4e51e993b..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: python
-dist: xenial
-
-python:
-  - "2.7"
-  - "3.4"
-  - "3.5"
-  - "3.6"
-  - "3.7"
-#  - "3.8-dev"  # TODO Remove comment-out when pylint and astroid upgraded to the latest and py2 support dropped
-
-install:
-  - pip install tox-travis
-
-script:
-  - tox
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5b756414e..8273d5b3b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,9 +2,4419 @@ Changelog
 =========
 
 
-(unreleased)
-------------
-- Chore: tweak changelog filter. [Kazuaki Matsuo]
+v5.2.0 (2025-08-07)
+-------------------
+
+New
+~~~
+- Feat: Switch package management from pipenv to uv (#1155) [Mykola
+  Mokhnach]
+
+  * feat: Switch package management from pipenv to uv
+
+  * line break
+
+  * Get rid of tox
+
+  * Update readme
+
+  * setup
+
+Fix
+~~~
+- Restore mypy linting (#1156) [Mykola Mokhnach]
+
+  * fix: Restore mypy linting
+
+  * update ci
+
+  * delete extra
+
+  * Update readme
+
+Test
+~~~~
+- Ci: Add a script to automatically update uv.lock upon dependabot
+  updates (#1158) [Mykola Mokhnach]
+
+Other
+~~~~~
+- Bump 5.2.0. [Kazuaki Matsuo]
+- Chore: use uv for version (#1160) [Kazuaki Matsuo]
+
+  * chore: use version via uv
+
+  * chore: update uv version
+
+  * ruff
+
+  * remove unused line
+
+  * fix ruff
+
+  * fix ruff
+
+  * fix naming
+
+  * rename
+- Chore: use uv build to build the package (#1159) [Kazuaki Matsuo]
+
+  * use uv build
+
+  * add comment
+
+  * add comment
+
+  * remove a new line
+
+  * reformat
+
+  * update review, metadatas
+
+  * fix format
+- Update changelog for 5.1.3. [Kazuaki Matsuo]
+
+
+v5.1.3 (2025-08-07)
+-------------------
+
+Test
+~~~~
+- Test: add bidi example in test (#1154) [Kazuaki Matsuo]
+
+  * test: add bidi log example test
+
+  * add ci
+
+  * add ubsubscribe
+
+  * run it first
+
+  * remove redundant log
+
+  * run at the end
+
+  * add comment
+
+Other
+~~~~~
+- Bump 5.1.3. [Kazuaki Matsuo]
+- Chore: fix some mypy errors (#1153) [Kazuaki Matsuo]
+
+  * chore: fix some mypy
+- Chore: wrong usage of CanFindElements #1148 (#1152) [Kazuaki Matsuo]
+
+  * chore: revert protocol
+
+  * add doc
+- Chore(deps-dev): update ruff requirement from ~=0.12.5 to ~=0.12.7
+  (#1151) [dependabot[bot]]
+- Chore(deps-dev): update ruff requirement from ~=0.12.4 to ~=0.12.5
+  (#1149) [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.12.4...0.12.5)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.12.5
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pytest-cov requirement from ~=5.0 to ~=6.2
+  (#1136) [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/v5.0.0...v6.2.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest-cov
+    dependency-version: 6.2.1
+    dependency-type: direct:development
+  ...
+- Update changelog for 5.1.2. [Kazuaki Matsuo]
+
+
+v5.1.2 (2025-07-24)
+-------------------
+
+Test
+~~~~
+- Ci: apply prebuilt wda (#1141) [Kazuaki Matsuo]
+
+Other
+~~~~~
+- Bump 5.1.2. [Kazuaki Matsuo]
+- Chore: inherit CanFindElements (#1148) [Kazuaki Matsuo]
+- Chore(deps-dev): update ruff requirement from ~=0.12.3 to ~=0.12.4
+  (#1146) [dependabot[bot], dependabot[bot]]
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.12.4
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.12.2 to ~=0.12.3
+  (#1144) [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.12.2...0.12.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.12.3
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.12.1 to ~=0.12.2
+  (#1142) [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.12.1...0.12.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.12.2
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.11.13 to ~=0.12.1
+  (#1139) [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.11.13...0.12.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.12.1
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.26 to ~=4.27 (#1138)
+  [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.26.0...4.27.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-version: 4.27.0
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.11.12 to ~=0.11.13
+  (#1135) [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.11.12...0.11.13)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.13
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pytest requirement from ~=8.3 to ~=8.4 (#1134)
+  [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.3.0...8.4.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest
+    dependency-version: 8.4.0
+    dependency-type: direct:development
+  ...
+- Chore: fix typos (#1133) [Noritaka Kobayashi]
+- Chore: remove unused commands (#1132) [Kazuaki Matsuo]
+- Chore(deps-dev): update ruff requirement from ~=0.11.11 to ~=0.11.12
+  (#1131) [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.11.11...0.11.12)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.12
+    dependency-type: direct:development
+  ...
+- Chore(deps): bump selenium from 4.32.0 to 4.33.0 (#1130)
+  [dependabot[bot], dependabot[bot]]
+
+  Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.32.0 to 4.33.0.
+  - [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
+  - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.32.0...selenium-4.33.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-version: 4.33.0
+    dependency-type: direct:production
+    update-type: version-update:semver-minor
+  ...
+- Docs: re-run the gen code (#1129) [Kazuaki Matsuo]
+
+  * docs: re-run the gen code
+
+  * update
+
+  * fix syntax
+- Chore(deps-dev): update ruff requirement from ~=0.11.10 to ~=0.11.11
+  (#1128) [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.11.10...0.11.11)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.11
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.25 to ~=4.26 (#1126)
+  [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.25.0...4.26.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-version: 4.26.0
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.11.8 to ~=0.11.10
+  (#1127) [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.11.8...0.11.10)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.10
+    dependency-type: direct:development
+  ...
+- Update changelog for 5.1.1. [Kazuaki Matsuo]
+
+
+v5.1.1 (2025-05-07)
+-------------------
+- Bump 5.1.1. [Kazuaki Matsuo]
+- Docs: update README.md. [Kazuaki Matsuo]
+- Chore(deps): bump selenium from 4.31.0 to 4.32.0 (#1124) [Kazuaki
+  Matsuo, dependabot[bot], dependabot[bot]]
+
+  * chore(deps): bump selenium from 4.31.0 to 4.32.0
+
+  Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.31.0 to 4.32.0.
+  - [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
+  - [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.31.0...selenium-4.32.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-version: 4.32.0
+    dependency-type: direct:production
+    update-type: version-update:semver-minor
+  ...
+- Docs: update compatibility matrix. [Kazuaki Matsuo]
+- Update changelog for 5.1.0. [Kazuaki Matsuo]
+
+
+v5.1.0 (2025-05-05)
+-------------------
+
+New
+~~~
+- Feat: add method for interacting with the Flutter integration driver
+  (#1123) [Oleg Matskiv]
+
+Test
+~~~~
+- Test: use timeout in client_config instead of the global var (#1120)
+  [Kazuaki Matsuo]
+- Ci: Tune CC title script. [Mykola Mokhnach]
+
+Other
+~~~~~
+- Bump 5.1.0. [Kazuaki Matsuo]
+- Chore(deps-dev): update ruff requirement from ~=0.11.7 to ~=0.11.8
+  (#1122) [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.11.7...0.11.8)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.8
+    dependency-type: direct:development
+  ...
+- Chore(deps): update typing-extensions requirement (#1115)
+  [dependabot[bot], dependabot[bot]]
+
+  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
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.11.4 to ~=0.11.7
+  (#1118) [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.11.4...0.11.7)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.7
+    dependency-type: direct:development
+  ...
+- Chore(deps): bump selenium from 4.30.0 to 4.31.0 (#1113)
+  [dependabot[bot], dependabot[bot]]
+
+  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
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.11.3 to ~=0.11.4
+  (#1114) [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.11.3...0.11.4)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.4
+    dependency-type: direct:development
+  ...
+- Chore(deps): update typing-extensions requirement (#1112)
+  [dependabot[bot], dependabot[bot]]
+
+  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
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.11.2 to ~=0.11.3
+  (#1111) [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.11.2...0.11.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-version: 0.11.3
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update ruff requirement from ~=0.10.0 to ~=0.11.2
+  (#1107) [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.10.0...0.11.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: ruff
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.24 to ~=4.25 (#1109)
+  [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.24.0...4.25.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps): bump selenium from 4.29.0 to 4.30.0 (#1108)
+  [dependabot[bot], dependabot[bot]]
+
+  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
+  ...
+- Update changelog for 5.0.0. [Kazuaki Matsuo]
+
+
+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)
+-------------------
+
+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]
+- 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)
+-------------------
+
+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)
+-------------------
+
+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
+~~~
+- 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)
+-------------------
+
+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)
+-------------------
+
+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)
+-------------------
+
+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)
+-------------------
+
+New
+~~~
+- Feat: remove MultiAction and TouchAction (#960) [Kazuaki Matsuo]
+
+  BREAKING CHANGE: Remove MultiAction and TouchAction as non-w3c WebDriver-defined methods. Please use w3c actions instead.
+
+Other
+~~~~~
+- Bump 4.0.0. [Kazuaki Matsuo]
+- Chore(deps-dev): update tox requirement from ~=4.13 to ~=4.14 (#972)
+  [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.13.0...4.14.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.12 to ~=4.13 (#957)
+  [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.12.0...4.13.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pytest requirement from ~=8.0 to ~=8.1 (#969)
+  [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.0.0...8.1.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest
+    dependency-type: direct:development
+  ...
+- Docs: update readme. [Kazuaki Matsuo]
+- Chore: remove deprecated AppiumBy.WINDOWS_UI_AUTOMATION (#968)
+  [Kazuaki Matsuo]
+- Chore(deps-dev): update python-dateutil requirement from ~=2.8 to
+  ~=2.9 (#967) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [python-dateutil](https://github.com/dateutil/dateutil) to permit the latest version.
+  - [Release notes](https://github.com/dateutil/dateutil/releases)
+  - [Changelog](https://github.com/dateutil/dateutil/blob/master/NEWS)
+  - [Commits](https://github.com/dateutil/dateutil/compare/2.8.0...2.9.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: python-dateutil
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=3.0.3 to ~=3.1.0
+  (#966) [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.0.3...v3.1.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Update changelog for 3.2.1. [Kazuaki Matsuo]
+
+
+v3.2.1 (2024-02-26)
+-------------------
+
+Fix
+~~~
+- Unclosed file <_io.BufferedReader name error by proper cleanup of
+  subprocess.Popen process (#965) [WrrngnCode]
+
+  https://github.com/appium/python-client/issues/962
+  add a Popen.communicate call after terminate to ensure that file stdout and sdterr file descriptors are closed properly.
+
+Other
+~~~~~
+- Bump 3.2.1. [Kazuaki Matsuo]
+- Update changelog for 3.2.0. [Kazuaki Matsuo]
+
+
+v3.2.0 (2024-02-24)
+-------------------
+
+New
+~~~
+- Feat: add pause in drag_and_drop (#961) [Kazuaki Matsuo]
+
+  * feat: add pause in drag_and_drop
+
+  * docs: tweak docstring
+
+  * Update action_helpers.py
+
+  * apply pause only when the value is not negative
+
+Fix
+~~~
+- Add return self in MultiAction#add (#964) [Kazuaki Matsuo]
+
+Other
+~~~~~
+- Bump 3.2.0. [Kazuaki Matsuo]
+- Chore(deps): update selenium requirement from ~=4.17 to ~=4.18 (#958)
+  [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.17.0...selenium-4.18.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pytest requirement from ~=7.4 to ~=8.0 (#953)
+  [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/7.4.0...8.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest
+    dependency-type: direct:development
+  ...
+- Chore(deps): update selenium requirement from ~=4.15 to ~=4.17 (#948)
+  [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.15.0...selenium-4.17.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Docs: update W3C actions example in readme (#946) [Dor Blayzer]
+- Chore(deps-dev): update tox requirement from ~=4.11 to ~=4.12 (#947)
+  [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.11.0...4.12.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Docs: update example in readme (#945) [Kazuaki Matsuo]
+- Docs: update links (#944) [Kazuaki Matsuo]
+- Update changelog for 3.1.1. [Kazuaki Matsuo]
+
+
+v3.1.1 (2023-12-15)
+-------------------
+
+Fix
+~~~
+- Typo in ActionHelpers (#937) [kkb912002]
+
+  touc -> touch
+- Self.command_executor instance in _update_command_executor (#940)
+  [Kazuaki Matsuo]
+
+Other
+~~~~~
+- Bump 3.1.1. [Kazuaki Matsuo]
+- Chore(deps-dev): update pylint requirement from ~=3.0.1 to ~=3.0.3
+  (#939) [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.0.1...v3.0.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps): update selenium requirement from ~=4.14 to ~=4.15 (#933)
+  [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.14.0...selenium-4.15.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Docs: address options in the migration guide (#929) [Kazuaki Matsuo]
+
+  * docs: adress options in the migration guide
+
+  * Update README.md
+
+  * Update README.md
+- Docs: update changelog. [Kazuaki Matsuo]
+- Update changelog for 3.1.0. [Kazuaki Matsuo]
+
+
+v3.1.0 (2023-10-13)
+-------------------
+
+New
+~~~
+- Feat: Add  missing platformVersion and browserName options (#925)
+  [Mykola Mokhnach]
+
+Test
+~~~~
+- Ci: Use appium from the release branch. [Mykola Mokhnach]
+
+Other
+~~~~~
+- Bump 3.1.0. [Kazuaki Matsuo]
+- Chore(deps): update selenium requirement from ~=4.13 to ~=4.14 (#923)
+  [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.13.0...selenium-4.14.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.17.5 to ~=3.0.1
+  (#922) [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/v2.17.5...v3.0.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps): update selenium requirement from ~=4.12 to ~=4.13 (#915)
+  [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.12.0...selenium-4.13.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Docs: update README.md for v3 (#912) [Kazuaki Matsuo]
+
+  * docs: update README.md
+
+  * Update README.md
+- Update changelog for 3.0.0. [Kazuaki Matsuo]
+
+
+v3.0.0 (2023-09-08)
+-------------------
+
+New
+~~~
+- Feat!: Update selenium dependency to 4.12 (#908) [Mykola Mokhnach]
+
+  BREAKING CHANGE: The minimum supported Python version set to 3.8
+  BREAKING CHANGE: The minimum supported selenium version set to 4.12
+
+Fix
+~~~
+- Handle the situation where payload is already a dictionary (#892)
+  [Mykola Mokhnach]
+- Add missing dependencies for types-python-dateutil (#891) [Dor
+  Blayzer]
+
+  * fix: Add missing dependencies for types-python-dateutil
+
+  * test: replace usage of selendroid app from 'test_install_app' in applications_tests.py
+
+  * Revert "test: replace usage of selendroid app from 'test_install_app' in applications_tests.py"
+
+  This reverts commit dcdf08b67d24257700923c89ae2643f26af2892f.
+
+Test
+~~~~
+- Test: selendroid cleanup (#895) [Dor Blayzer]
+
+  * test: replace usage of selendroid app from 'test_install_app' in applications_tests.py
+
+  * test: remove unused import pytest from applications_tests.py
+
+  * test: fix broken TestContextSwitching by replacing selendroid with ApiDemos
+
+  * test: remove selendroid-test-app.apk from apps folder
+- Ci: add pylint_quotes for pylint to use single quote as primary method
+  (#886) [Kazuaki Matsuo]
+
+Other
+~~~~~
+- Bump 3.0.0. [Kazuaki Matsuo]
+- Chore(deps-dev): update tox requirement from ~=4.8 to ~=4.11 (#906)
+  [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.8.0...4.11.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Refactor!: remove several previously deprecated APIs (#909) [Mykola
+  Mokhnach]
+
+  BREAKING CHANGE: Removed obsolete all_sessions and session properties
+  BREAKING CHANGE: Removed the obsolete start_activity method
+  BREAKING CHANGE: Removed the obsolete end_test_coverage method
+  BREAKING CHANGE: Removed the following obsolete arguments from the driver constructor: desired_capabilities, browser_profile, proxy
+  BREAKING CHANGE: Removed obsolete set_value and set_text methods
+  BREAKING CHANGE: Removed the obsolete MobileBy class
+  BREAKING CHANGE: Removed obsolete application management methods: launch_app, close_app, reset
+  BREAKING CHANGE: Removed obsolete IME methods: available_ime_engines, is_ime_active, activate_ime_engine, deactivate_ime_engine, active_ime_engine
+- Chore(deps-dev): update tox requirement from ~=4.6 to ~=4.8 (#902)
+  [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.6.0...4.8.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps): update selenium requirement from ~=4.10 to ~=4.11 (#899)
+  [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/commits)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.17.3 to ~=2.17.5
+  (#897) [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/v2.17.3...v2.17.5)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Docs: Update README.md (#898) [Dor Blayzer]
+
+  Some typos and grammar fixes
+- Chore(deps-dev): update mock requirement from ~=5.0 to ~=5.1 (#893)
+  [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.0.0...5.1.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mock
+    dependency-type: direct:development
+  ...
+- Chore: run pre-commit autoupdate (#890) [Kazuaki Matsuo]
+- Chore: update isort revision to 5.12.0 (#889) [Dor Blayzer]
+- Chore(deps-dev): update pytest requirement from ~=7.2 to ~=7.4 (#884)
+  [dependabot[bot]]
+- Chore(deps-dev): update typing-extensions requirement (#885)
+  [dependabot[bot], dependabot[bot]]
+
+  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.6.0...4.7.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: typing-extensions
+    dependency-type: direct:development
+  ...
+- Docs: update changelogs and version. [Kazuaki Matsuo]
+
+
+v2.11.1 (2023-06-13)
+--------------------
+- Revert "chore: remove duplicated clean command" (#881) [Kazuaki
+  Matsuo]
+
+  * Revert "chore: remove duplicated clean command (#809)"
+
+  This reverts commit 2f45ef935c12dec2ab8de044ce6a1c1e0b9aa46f.
+
+  * chore: left a comment
+
+  * trim spaces
+- Update changelog. [Kazuaki Matsuo]
+
+
+v2.11.0 (2023-06-09)
+--------------------
+
+New
+~~~
+- Feat: make the UA format with same as other clients (#793) [Kazuaki
+  Matsuo]
+
+  * chore: set version with /
+
+  * chore: update comment
+
+  * update test
+
+  * update tests
+
+Other
+~~~~~
+- Update changelog for 2.10.2. [Kazuaki Matsuo]
+
+
+v2.10.2 (2023-06-08)
+--------------------
+
+Fix
+~~~
+- Update the constructor for compatibility with python client 4.10
+  (#879) [Mykola Mokhnach]
+
+Test
+~~~~
+- Ci: add py11 for the unit test (#875) [Kazuaki Matsuo]
+
+Other
+~~~~~
+- Bump 2.10.2. [Kazuaki Matsuo]
+- Chore: remove duplicated clean command (#809) [Kazuaki Matsuo]
+
+  * remove clear
+
+  * remove clear more
+- Chore(deps-dev): update tox requirement from ~=4.5 to ~=4.6 (#877)
+  [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.5.0...4.6.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Docs: address version management recommendation in the readme (#874)
+  [Kazuaki Matsuo]
+
+  * chore: set the max selenium deps version
+
+  * ci: add python 11
+
+  * Update unit-test.yml
+
+  * add note in the readme
+
+  * Update README.md
+
+  * docs: merge the matrix pr into README.md
+- Docs: Improve usage examples (#873) [Mykola Mokhnach]
+- Chore(deps-dev): update pytest-cov requirement from ~=4.0 to ~=4.1
+  (#872) [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.0.0...v4.1.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest-cov
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update typing-extensions requirement (#871)
+  [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [typing-extensions](https://github.com/python/typing_extensions) to permit the latest version.
+  - [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md)
+  - [Commits](https://github.com/python/typing_extensions/compare/4.5.0...4.6.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: typing-extensions
+    dependency-type: direct:development
+  ...
+- Chore: nump the version. [Kazuaki Matsuo]
+- Update changelog for 2.10.1. [Kazuaki Matsuo]
+
+
+v2.10.1 (2023-05-20)
+--------------------
+
+Fix
+~~~
+- W3C errors to exception classes mapping (#869) [Mykola Mokhnach]
+
+  * fix: W3C errors to exception classes mapping
+
+  * Imports
+
+  * Tune
+
+Other
+~~~~~
+- Chrom: bump the version. [Kazuaki Matsuo]
+- Update changelog for 2.10.0. [Kazuaki Matsuo]
+
+
+v2.10.0 (2023-05-11)
+--------------------
+
+Fix
+~~~
+- Update connection manager creation (#864) [Mykola Mokhnach]
+
+Other
+~~~~~
+- Refactor: Move driver-specific commands to use extensions (part2)
+  (#859) [Mykola Mokhnach]
+- Chore(deps): update selenium requirement from ~=4.7 to ~=4.9 (#852)
+  [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.7.0...selenium-4.9.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Refactor: Move driver-specific commands to use extensions (part1)
+  (#856) [Mykola Mokhnach]
+- Chore(deps-dev): update pylint requirement from ~=2.17.2 to ~=2.17.3
+  (#853) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.17.2...v2.17.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.4 to ~=4.5 (#854)
+  [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.4.0...4.5.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update mypy requirement from ~=1.1 to ~=1.2 (#848)
+  [dependabot[bot]]
+- Chore(deps-dev): update pylint requirement from ~=2.17.1 to ~=2.17.2
+  (#847) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.17.1...v2.17.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore: bump and correct version. [Kazuaki Matsuo]
+- Update changelog for 2.9.0. [Kazuaki Matsuo]
+
+
+v2.9.0 (2023-04-02)
+-------------------
+
+New
+~~~
+- Feat: respect the given executor (#844) [Kazuaki Matsuo]
+
+  * feat: can provide a custom connection
+
+  * add tests
+
+  * tweak tests
+
+  * lint
+
+  * tweak
+
+  * add comment
+
+  * fix lint
+
+  * tweak
+
+  * add test
+
+  * tweak review
+
+Fix
+~~~
+- Set_value and set_text sent incorrect data (#831) [eyJhb]
+
+Other
+~~~~~
+- Update changelog for 2.8.0. [Kazuaki Matsuo]
+- Chore(deps-dev): update pylint requirement from ~=2.17.0 to ~=2.17.1
+  (#843) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.17.0...v2.17.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.16.3 to ~=2.17.0
+  (#838) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.16.3...v2.17.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update mypy requirement from ~=1.0 to ~=1.1 (#836)
+  [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v1.0.0...v1.1.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.16.2 to ~=2.16.3
+  (#834) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.16.2...v2.16.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update typing-extensions requirement (#830)
+  [dependabot[bot], dependabot[bot]]
+
+  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.4.0...4.5.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: typing-extensions
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.16.1 to ~=2.16.2
+  (#829) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.16.1...v2.16.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update mypy requirement from ~=0.991 to ~=1.0 (#828)
+  [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.991...v1.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.16.0 to ~=2.16.1
+  (#827) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.16.0...v2.16.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.10 to ~=2.16.0
+  (#826) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.10...v2.16.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.3 to ~=4.4 (#823)
+  [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.3.0...4.4.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Update changelog for 2.8.1. [Kazuaki Matsuo]
+
+
+v2.8.1 (2023-01-20)
+-------------------
+
+New
+~~~
+- Feat: add status tentatively (#820) [Kazuaki Matsuo]
+
+  * feat: add status tentatively
+
+  * update test
+
+  * fix docstring
+
+  * fix typo
+
+  * fix lint
+
+
+v2.8.0 (2023-01-20)
+-------------------
+
+New
+~~~
+- Feat: add status tentatively. [Kazuaki Matsuo]
+
+Fix
+~~~
+- Fix lint. [Kazuaki Matsuo]
+- Fix typo. [Kazuaki Matsuo]
+- Fix docstring. [Kazuaki Matsuo]
+
+Other
+~~~~~
+- Update test. [Kazuaki Matsuo]
+- Chore(deps-dev): update tox requirement from ~=4.2 to ~=4.3 (#817)
+  [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.2.0...4.3.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps): update sphinx requirement from <6.0,>=4.0 to >=4.0,<7.0
+  (#814) [dependabot[bot]]
+- Chore(deps-dev): update pylint requirement from ~=2.15.9 to ~=2.15.10
+  (#816) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.9...v2.15.10)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.1 to ~=4.2 (#815)
+  [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.1.0...4.2.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=4.0 to ~=4.1 (#813)
+  [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.0.0...4.1.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update mock requirement from ~=4.0 to ~=5.0 (#812)
+  [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [mock](https://github.com/testing-cabal/mock) to permit the latest version.
+  - [Release notes](https://github.com/testing-cabal/mock/releases)
+  - [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst)
+  - [Commits](https://github.com/testing-cabal/mock/compare/4.0.0...5.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mock
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pre-commit requirement from ~=2.20 to ~=2.21
+  (#811) [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/v2.20.0...v2.21.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pre-commit
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.8 to ~=2.15.9
+  (#810) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.8...v2.15.9)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update isort requirement from ~=5.10 to ~=5.11 (#808)
+  [Kazuaki Matsuo, dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [isort](https://github.com/pycqa/isort) to permit the latest version.
+  - [Release notes](https://github.com/pycqa/isort/releases)
+  - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
+  - [Commits](https://github.com/pycqa/isort/compare/5.10.0...5.11.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: isort
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update black requirement from ~=22.10.0 to ~=22.12.0
+  (#807) [Kazuaki Matsuo, dependabot[bot], dependabot[bot]]
+
+  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/22.10.0...22.12.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: black
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=3.27 to ~=4.0 (#806)
+  [Kazuaki Matsuo, dependabot[bot], dependabot[bot]]
+
+  * chore(deps-dev): update tox requirement from ~=3.27 to ~=4.0
+
+  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/3.27.0...4.0.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.7 to ~=2.15.8
+  (#804) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.7...v2.15.8)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps): update selenium requirement from ~=4.5 to ~=4.7 (#801)
+  [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.5.0...selenium-4.7.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.6 to ~=2.15.7
+  (#800) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.6...v2.15.7)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.5 to ~=2.15.6
+  (#799) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.5...v2.15.6)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore: update docstring in touch_action.py (#797) [Kazuaki Matsuo,
+  wojciodataist]
+
+  add detailed information to long_press duration param
+- Chore(deps-dev): update mypy requirement from ~=0.982 to ~=0.991
+  (#798) [Kazuaki Matsuo, dependabot[bot], dependabot[bot]]
+
+  * chore(deps-dev): update mypy requirement from ~=0.982 to ~=0.991
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.982...v0.991)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update tox requirement from ~=3.26 to ~=3.27 (#792)
+  [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/master/docs/changelog.rst)
+  - [Commits](https://github.com/tox-dev/tox/compare/3.26.0...3.27.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pytest requirement from ~=7.1 to ~=7.2 (#791)
+  [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/7.1.0...7.2.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.4 to ~=2.15.5
+  (#790) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.4...v2.15.5)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.3 to ~=2.15.4
+  (#788) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.3...v2.15.4)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Chore: update precommit (#787) [Kazuaki Matsuo]
+- Update changelog for 2.7.1. [Kazuaki Matsuo]
+- Bump 2.7.1. [Kazuaki Matsuo]
+
+
+v2.7.1 (2022-10-11)
+-------------------
+
+Test
+~~~~
+- Ci: run unit tests on actions (#773) [Kazuaki Matsuo]
+
+  * ci: run unit tests on actions
+
+  * ci: remove unit test section
+
+  * ci: comment out win for now
+
+  * ci: tweak trigger
+
+Other
+~~~~~
+- Refactor: Make service startup failures more helpful (#786) [Mykola
+  Mokhnach]
+- Chore(deps-dev): update typing-extensions requirement (#783) [Kazuaki
+  Matsuo, dependabot[bot], dependabot[bot]]
+
+  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.3.0...4.4.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: typing-extensions
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update black requirement from ~=22.8.0 to ~=22.10.0
+  (#784) [dependabot[bot], dependabot[bot]]
+
+  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/22.8.0...22.10.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: black
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update mypy requirement from ~=0.981 to ~=0.982
+  (#782) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.981...v0.982)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pytest-cov requirement from ~=3.0 to ~=4.0
+  (#779) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version.
+  - [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
+  - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
+  - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v3.0.0...v4.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest-cov
+    dependency-type: direct:development
+  ...
+- Chore(deps): update selenium requirement from ~=4.4 to ~=4.5 (#780)
+  [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.4.0...selenium-4.5.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update mypy requirement from ~=0.971 to ~=0.981
+  (#777) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.971...v0.981)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:development
+  ...
+- Chore(deps-dev): update pylint requirement from ~=2.15.2 to ~=2.15.3
+  (#774) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.2...v2.15.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:development
+  ...
+- Bump 2.7.0. [Kazuaki Matsuo]
+- Update changelog for 2.6.3. [Kazuaki Matsuo]
+
+
+v2.7.0 (2022-09-22)
+-------------------
+
+New
+~~~
+- Feat: Add appArguments option to WindowsOptions (#768) [Mykola
+  Mokhnach]
+
+Fix
+~~~
+- Move dev-only dependencies to [dev-packages] section (#772) [Mykola
+  Mokhnach]
+
+Test
+~~~~
+- Ci: Fix runner name. [Mykola Mokhnach]
+
+Other
+~~~~~
+- Chore(deps): update pylint requirement from ~=2.15.2 to ~=2.15.3
+  (#770) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.2...v2.15.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Docs: Update changelog for 2.6.2. [Kazuaki Matsuo]
+
+
+v2.6.2 (2022-09-16)
+-------------------
+
+Fix
+~~~
+- Use total_seconds property of timedelta (#767) [Mykola Mokhnach]
+
+Test
+~~~~
+- Ci: Update Conventional Commits config preset. [Mykola Mokhnach]
+- Ci: Add Conventional commit format validation (#764) [Mykola Mokhnach]
+
+  * ci: Add Conventional commit format validation
+
+  * Rename
+
+Other
+~~~~~
+- Chore(deps): update tox requirement from ~=3.25 to ~=3.26 (#766)
+  [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/master/docs/changelog.rst)
+  - [Commits](https://github.com/tox-dev/tox/compare/3.25.0...3.26.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pylint requirement from ~=2.15.0 to ~=2.15.2
+  (#765) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.0...v2.15.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Chore(deps): update astroid requirement from ~=2.9 to ~=2.12 (#762)
+  [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [astroid](https://github.com/PyCQA/astroid) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/astroid/releases)
+  - [Changelog](https://github.com/PyCQA/astroid/blob/main/ChangeLog)
+  - [Commits](https://github.com/PyCQA/astroid/compare/v2.9.0...v2.12.5)
+
+  ---
+  updated-dependencies:
+  - dependency-name: astroid
+    dependency-type: direct:production
+  ...
+- Chore(deps): bump black from 22.6.0 to 22.8.0 (#763) [dependabot[bot],
+  dependabot[bot]]
+
+  Bumps [black](https://github.com/psf/black) from 22.6.0 to 22.8.0.
+  - [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/22.6.0...22.8.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: black
+    dependency-type: direct:production
+    update-type: version-update:semver-minor
+  ...
+- Chore(deps): update pylint requirement from ~=2.14.5 to ~=2.15.0
+  (#761) [dependabot[bot], dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.14.5...v2.15.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Docs: Update changelog for 2.6.1. [Kazuaki Matsuo]
+
+
+v2.6.1 (2022-08-11)
+-------------------
+
+Fix
+~~~
+- Fix options in mac2 (#759) [Kazuaki Matsuo]
+- Backwards compatible behaviour of swipe and scroll in action_helpers
+  (#744) [jatalahd]
+
+  * Backwards compatible behaviour of swipe and scroll in action_helpers
+
+  - Fixed handling the duration argument in swipe() and scroll() helpers
+  - Functionality is now the same as in older versions using TouchActions
+
+  Fixes #743
+
+  * Backwards compatible behaviour of swipe and scroll in action_helpers
+
+  - Fixed handling the duration argument in swipe() and scroll() helpers
+  - Functionality is now the same as in older versions using TouchActions
+
+  Fixes #743
+
+  * Backwards compatible behaviour of swipe and scroll in action_helpers
+
+  - Fixed handling the duration argument in swipe() and scroll() helpers
+  - Functionality is now the same as in older versions using TouchActions
+
+  Fixes #743
+
+  * Backwards compatible behaviour of swipe and scroll in action_helpers
+
+  - Fixed handling the duration argument in swipe() and scroll() helpers
+  - Functionality is now the same as in older versions using TouchActions
+
+  Fixes #743
+
+  * Backwards compatible behaviour of swipe and scroll in action_helpers
+
+  - Fixed handling the duration argument in swipe() and scroll() helpers
+  - Functionality is now the same as in older versions using TouchActions
+
+  Fixes #743
+- Move py.typed to the hierarchy root (#751) [Mykola Mokhnach]
+- Typos/copypaste in various options (#750) [Mykola Mokhnach]
+
+Other
+~~~~~
+- Chore(deps): update selenium requirement from ~=4.3 to ~=4.4 (#757)
+  [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.3.0...selenium-4.4.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps): update mypy requirement from ~=0.961 to ~=0.971 (#749)
+  [dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.961...v0.971)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pylint requirement from ~=2.14.4 to ~=2.14.5
+  (#747) [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.14.4...v2.14.5)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pre-commit requirement from ~=2.19 to ~=2.20
+  (#746) [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/v2.19.0...v2.20.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pre-commit
+    dependency-type: direct:development
+  ...
+- Chore(deps): update typing-extensions requirement from ~=4.2 to ~=4.3
+  (#745) [dependabot[bot]]
+
+  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.2.0...4.3.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: typing-extensions
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pylint requirement from ~=2.14.3 to ~=2.14.4
+  (#742) [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.14.3...v2.14.4)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Docs: Update changelog for 2.6.0. [Kazuaki Matsuo]
+
+
+v2.6.0 (2022-06-28)
+-------------------
+
+New
+~~~
+- Feat: Add Android drivers options (#740) [Mykola Mokhnach]
+
+Other
+~~~~~
+- Chore(deps): bump black from 22.3.0 to 22.6.0 (#741) [dependabot[bot]]
+
+  Bumps [black](https://github.com/psf/black) from 22.3.0 to 22.6.0.
+  - [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/22.3.0...22.6.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: black
+    dependency-type: direct:production
+    update-type: version-update:semver-minor
+  ...
+- Chore: Improve autocompletion for methods returning self instance
+  (#739) [Mykola Mokhnach]
+- Refactor: Remove previously deprecated methods and mark
+  reset/close/launch APIs as deprecated (#738) [Mykola Mokhnach]
+- Docs: Update changelog for 2.5.0. [Kazuaki Matsuo]
+
+
+v2.5.0 (2022-06-25)
+-------------------
+
+New
+~~~
+- Feat: Add xcuitest driver options (#737) [Mykola Mokhnach]
+- Feat: Add Gecko driver options (#735) [Mykola Mokhnach]
+- Feat: Add Windows driver options (#732) [Mykola Mokhnach]
+- Feat: Add Safari driver options (#731) [Mykola Mokhnach]
+- Feat: Add Mac2Driver options (#730) [Mykola Mokhnach]
+
+Other
+~~~~~
+- Chore(deps): update selenium requirement from ~=4.2 to ~=4.3 (#736)
+  [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.2.0...selenium-4.3.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pylint requirement from ~=2.14.2 to ~=2.14.3
+  (#733) [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.14.2...v2.14.3)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Refactor: Make system_port and system_host options common (#734)
+  [Mykola Mokhnach]
+- Chore(deps): update pylint requirement from ~=2.14.1 to ~=2.14.2
+  (#725) [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.14.1...v2.14.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Chore: bump version to 2.4.0. [Kazuaki Matsuo]
+- Chore(deps): update pylint requirement from ~=2.14.1 to ~=2.14.2
+  (#725) [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.14.1...v2.14.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+
+
+v2.4.0 (2022-06-17)
+-------------------
+
+New
+~~~
+- Feat: Add common options (#728) [Mykola Mokhnach]
+
+Other
+~~~~~
+- Chore: Add better error handling for session creation responses (#727)
+  [Mykola Mokhnach]
+- Docs: Update changelog for 2.3.0. [Kazuaki Matsuo]
+- Bump 2.3.0. [Kazuaki Matsuo]
+- Chore: Update comments to locator patches (#724) [VladimirPodolyan]
+
+  * Update webelement.py
+
+  * update comment section
+
+  * CR fixes
+
+
+v2.3.0 (2022-06-13)
+-------------------
+
+New
+~~~
+- Feat: Add base options for all supported automation names (#721)
+  [Mykola Mokhnach]
+- Feat: Add support for w3c options (#720) [Mykola Mokhnach]
+
+Test
+~~~~
+- Test: Use Appium2 to run functional tests (#723) [Mykola Mokhnach]
+
+Other
+~~~~~
+- Docs: Update README with the new options format (#722) [Mykola
+  Mokhnach]
+- Chore(deps): update mypy requirement from ~=0.960 to ~=0.961 (#718)
+  [dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.960...v0.961)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:production
+  ...
+- Chore: Disable pylint checks fail CI (#719) [Mykola Mokhnach]
+- Chore(deps): update selenium requirement from ~=4.1 to ~=4.2 (#715)
+  [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.1.0...selenium-4.2.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: selenium
+    dependency-type: direct:production
+  ...
+- Chore(deps): update sphinx requirement from <5.0,>=4.0 to >=4.0,<6.0
+  (#716) [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/5.x/CHANGES)
+  - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.0.0...v5.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: sphinx
+    dependency-type: direct:production
+  ...
+- Chore(deps): update mypy requirement from ~=0.950 to ~=0.960 (#714)
+  [dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.950...v0.960)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pre-commit requirement from ~=2.18 to ~=2.19
+  (#713) [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/v2.18.0...v2.19.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pre-commit
+    dependency-type: direct:development
+  ...
+- Chore(deps): update mypy requirement from ~=0.942 to ~=0.950 (#712)
+  [dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.942...v0.950)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:production
+  ...
+- Chore(deps): update typing-extensions requirement from ~=4.1 to ~=4.2
+  (#711) [dependabot[bot]]
+
+  Updates the requirements on [typing-extensions](https://github.com/python/typing) to permit the latest version.
+  - [Release notes](https://github.com/python/typing/releases)
+  - [Changelog](https://github.com/python/typing/blob/master/typing_extensions/CHANGELOG)
+  - [Commits](https://github.com/python/typing/compare/4.1.0...4.2.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: typing-extensions
+    dependency-type: direct:production
+  ...
+- Chore(deps): update tox requirement from ~=3.24 to ~=3.25 (#709)
+  [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/master/docs/changelog.rst)
+  - [Commits](https://github.com/tox-dev/tox/compare/3.24.0...3.25.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pre-commit requirement from ~=2.17 to ~=2.18
+  (#708) [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/v2.17.0...v2.18.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pre-commit
+    dependency-type: direct:development
+  ...
+- Update changelog for 2.2.0. [Kazuaki Matsuo]
+
+
+v2.2.0 (2022-03-30)
+-------------------
+
+New
+~~~
+- Feat: add non-w3c but still need commands (#701) [Kazuaki Matsuo]
+
+  * add non-w3c but still need commands
+
+  * fix id as $
+
+Other
+~~~~~
+- Bump 2.2.0. [Kazuaki Matsuo]
+- Chore(deps): bump black from 22.1.0 to 22.3.0 (#705) [dependabot[bot]]
+
+  Bumps [black](https://github.com/psf/black) from 22.1.0 to 22.3.0.
+  - [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/22.1.0...22.3.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: black
+    dependency-type: direct:production
+    update-type: version-update:semver-minor
+  ...
+- Revert: pylint (#706) [Kazuaki Matsuo]
+- Chore: relax selenium version as same as before. [Kazuaki Matsuo]
+- Chore(deps): update mypy requirement from ~=0.941 to ~=0.942 (#703)
+  [dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.941...v0.942)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pylint requirement from ~=2.12 to ~=2.13 (#702)
+  [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.12.0...v2.13.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Chore(deps): update mypy requirement from ~=0.930 to ~=0.941 (#696)
+  [dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.930...v0.941)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:production
+  ...
+- Chore(deps): update typing-extensions requirement from ~=4.0 to ~=4.1
+  (#684) [dependabot[bot]]
+
+  Updates the requirements on [typing-extensions](https://github.com/python/typing) to permit the latest version.
+  - [Release notes](https://github.com/python/typing/releases)
+  - [Changelog](https://github.com/python/typing/blob/master/typing_extensions/CHANGELOG)
+  - [Commits](https://github.com/python/typing/compare/4.0.0...4.1.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: typing-extensions
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pytest requirement from ~=7.0 to ~=7.1 (#694)
+  [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/7.0.0...7.1.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest
+    dependency-type: direct:production
+  ...
+- Docs: update missing changelog. [Kazuaki Matsuo]
+
+
+v2.1.4 (2022-02-28)
+-------------------
+- Bump 2.1.4. [Kazuaki Matsuo]
+- Update changelog for 2.1.3. [Kazuaki Matsuo]
+
+
+v2.1.3 (2022-02-26)
+-------------------
+
+Test
+~~~~
+- Test: update tests to use find_element(by...) (#674) [Kazuaki Matsuo]
+
+  * test: update find element/s methods
+
+  * fix arguments
+
+  * fix default value
+
+Other
+~~~~~
+- Bump 2.1.3. [Kazuaki Matsuo]
+- Chore: restrict selenium client version (#686) [Kazuaki Matsuo]
+- Chore(deps): bump black from 21.12b0 to 22.1.0 (#681)
+  [dependabot[bot]]
+
+  Bumps [black](https://github.com/psf/black) from 21.12b0 to 22.1.0.
+  - [Release notes](https://github.com/psf/black/releases)
+  - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
+  - [Commits](https://github.com/psf/black/commits/22.1.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: black
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pytest requirement from ~=6.2 to ~=7.0 (#682)
+  [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/6.2.0...7.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pre-commit requirement from ~=2.16 to ~=2.17
+  (#678) [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/master/CHANGELOG.md)
+  - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.16.0...v2.17.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pre-commit
+    dependency-type: direct:development
+  ...
+- Refactor: Update types descriptions for mixin classes (#677) [Mykola
+  Mokhnach]
+- Chore: bump mypy (#675) [Kazuaki Matsuo]
+- Update changelog for 2.1.2. [Kazuaki Matsuo]
+
+
+v2.1.2 (2021-12-30)
+-------------------
+
+Fix
+~~~
+- Default duration in tap (#673) [Kazuaki Matsuo]
+
+Other
+~~~~~
+- Bump 2.1.2. [Kazuaki Matsuo]
+- Update changelog for 2.1.1. [Kazuaki Matsuo]
+
+
+v2.1.1 (2021-12-24)
+-------------------
+
+New
+~~~
+- Feat: use 'touch' pointer action (#670) [Kazuaki Matsuo]
+
+  * chore: specify touch
+
+  * comment out touch in drag_and_drop
+
+  * fix mypy
+
+  * add desctiption of touch action
+
+Test
+~~~~
+- Ci: remove ==2021.5.29 (#653) [Kazuaki Matsuo]
+
+  * ci: remove ==2021.5.29
+
+  * bump black
+
+Other
+~~~~~
+- Bump 2.1.1. [Kazuaki Matsuo]
+- Chore(deps): bump black from 21.11b1 to 21.12b0 (#664)
+  [dependabot[bot]]
+
+  Bumps [black](https://github.com/psf/black) from 21.11b1 to 21.12b0.
+  - [Release notes](https://github.com/psf/black/releases)
+  - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
+  - [Commits](https://github.com/psf/black/commits)
+
+  ---
+  updated-dependencies:
+  - dependency-name: black
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pre-commit requirement from ~=2.15 to ~=2.16
+  (#663) [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/master/CHANGELOG.md)
+  - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.15.0...v2.16.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pre-commit
+    dependency-type: direct:development
+  ...
+- Chore(deps): update pylint requirement from ~=2.11 to ~=2.12 (#662)
+  [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.11.0...v2.12.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Chore(deps): update astroid requirement from ~=2.8 to ~=2.9 (#661)
+  [dependabot[bot]]
+
+  Updates the requirements on [astroid](https://github.com/PyCQA/astroid) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/astroid/releases)
+  - [Changelog](https://github.com/PyCQA/astroid/blob/main/ChangeLog)
+  - [Commits](https://github.com/PyCQA/astroid/compare/v2.8.0...v2.9.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: astroid
+    dependency-type: direct:production
+  ...
+- Update changelog for 2.1.0. [Kazuaki Matsuo]
+
+
+v2.1.0 (2021-11-27)
+-------------------
+
+New
+~~~
+- Feat: add AppiumBy instead of MobileBy (#659) [Kazuaki Matsuo]
+
+  * feat: add AppiumBy instead of MobileBy
+
+  * add class description
+
+  * use deprecated::
+
+Other
+~~~~~
+- Bump 2.1.0. [Kazuaki Matsuo]
+- Chore: add deprecated mark for find_element_by* (#657) [Kazuaki
+  Matsuo]
+- Chore: relax selenium version control (#656) [Kazuaki Matsuo]
+- Chore: tweak keyword in metadata. [Kazuaki Matsuo]
+- Update changelog for 2.0.0. [Kazuaki Matsuo]
+
+
+v2.0.0 (2021-11-09)
+-------------------
+
+New
+~~~
+- Feat: Change base selenium client version to selenium 4 (#636)
+  [Kazuaki Matsuo]
+
+  - Changed base selenium client version to v4
+  - No longer forceMjsonwp works
+  - Add strict_ssl option in webdriver.Remote
+
+Test
+~~~~
+- Ci: set pipenv==2021.5.29 to prevent dependencies error (#651)
+  [Kazuaki Matsuo]
+
+  * ci: add --pre
+
+  * specify pipenv as same as the previous ok case
+
+  * set 2021.5.29 in tox as well
+
+Other
+~~~~~
+- Bump 2.0.0. [Kazuaki Matsuo]
+- Docs: update readme. [Kazuaki Matsuo]
+- Chore: add Python 3.9 as metadata. [Kazuaki Matsuo]
+- Chore(deps): update isort requirement from ~=5.9 to ~=5.10 (#650)
+  [dependabot[bot]]
+
+  Updates the requirements on [isort](https://github.com/pycqa/isort) to permit the latest version.
+  - [Release notes](https://github.com/pycqa/isort/releases)
+  - [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
+  - [Commits](https://github.com/pycqa/isort/compare/5.9.0...5.10.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: isort
+    dependency-type: direct:production
+  ...
+- Update changelog for 2.0.0.rc6. [Kazuaki Matsuo]
+- Bump 2.0.0.rc6. [Kazuaki Matsuo]
+- Docs: update readme. [Kazuaki Matsuo]
+- Chore: adding deprecation mark in touch actions and multi touch (#648)
+  [Kazuaki Matsuo]
+
+  * chore: add deprecation mark in touch actions and multi touch
+
+  * chore: add deprecated mark in MultiAction class
+
+  * docs: update readme
+- Chore: deprecate -windows uiautomation (#649) [Kazuaki Matsuo]
+
+  * chore: add Deprecated for -windows uiautomation
+
+  * chore: add logger
+- Update changelog for 2.0.0.rc5. [Kazuaki Matsuo]
+- Bump 2.0.0.rc5. [Kazuaki Matsuo]
+- Chore(deps): update sphinx requirement from <4.0,>=3.0 to >=3.0,<5.0
+  (#603) [Kazuaki Matsuo, 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/4.x/CHANGES)
+  - [Commits](https://github.com/sphinx-doc/sphinx/compare/v3.0.0...v4.0.0)
+- Update gitchangelog once. [Kazuaki Matsuo]
+- Chore(deps): update sphinx-rtd-theme requirement from <1.0 to <2.0
+  (#637) [Kazuaki Matsuo, dependabot[bot]]
+
+  Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version.
+  - [Release notes](https://github.com/readthedocs/sphinx_rtd_theme/releases)
+  - [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...1.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: sphinx-rtd-theme
+    dependency-type: direct:production
+  ...
+- Chore: cleanup no longer needed code in w3c, bump dev Pipfile (#646)
+  [Kazuaki Matsuo]
+
+  chore: cleanup no longer needed code in w3c, bump dev Pipfile
+- Chore(deps): update pylint requirement from ~=2.10 to ~=2.11 (#638)
+  [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
+  - [Commits](https://github.com/PyCQA/pylint/compare/v2.10.0...v2.11.1)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pytest-cov requirement from ~=2.12 to ~=3.0 (#641)
+  [dependabot[bot]]
+
+  Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version.
+  - [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
+  - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
+  - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.12.0...v3.0.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pytest-cov
+    dependency-type: direct:production
+  ...
+- Update changelog for 1.3.0. [Kazuaki Matsuo]
+
+
+v1.3.0 (2021-09-27)
+-------------------
+
+New
+~~~
+- Feat: do not raise an error in case method is already defined (#632)
+  [Kazuaki Matsuo]
+- Feat: add satellites in set_location (#620) [Kazuaki Matsuo]
+
+  * feat: add satellites in set_location
+
+  * fix review
+- Feat: Add command with `setattr` (#615) [Kazuaki Matsuo]
+
+  * chore: add placeholder
+
+  * move to extention way
+
+  * revert pytest
+
+  * add todo
+
+  * call method_name instead of wrapper
+
+  * remove types
+
+  * rename a method
+
+  * add examples
+
+  * add types-python-dateutil as error message
+
+  * add example more
+
+  * tweak naming
+
+  * Explicit Dict
+
+Other
+~~~~~
+- Bump 1.3.0. [Kazuaki Matsuo]
+- Chore(deps): update types-python-dateutil requirement (#633)
+  [dependabot[bot]]
+
+  Updates the requirements on [types-python-dateutil](https://github.com/python/typeshed) to permit the latest version.
+  - [Release notes](https://github.com/python/typeshed/releases)
+  - [Commits](https://github.com/python/typeshed/commits)
+
+  ---
+  updated-dependencies:
+  - dependency-name: types-python-dateutil
+    dependency-type: direct:production
+  ...
+- Chore(deps-dev): update pre-commit requirement from ~=2.13 to ~=2.15
+  (#634) [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/master/CHANGELOG.md)
+  - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.13.0...v2.15.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pre-commit
+    dependency-type: direct:development
+  ...
+- Chore(deps): update mypy requirement from ~=0.812 to ~=0.910 (#616)
+  [dependabot[bot]]
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.812...v0.910)
+
+  ---
+  updated-dependencies:
+  - dependency-name: mypy
+    dependency-type: direct:production
+  ...
+- Chore(deps): update astroid requirement from ~=2.5 to ~=2.7 (#629)
+  [dependabot[bot]]
+
+  Updates the requirements on [astroid](https://github.com/PyCQA/astroid) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/astroid/releases)
+  - [Changelog](https://github.com/PyCQA/astroid/blob/main/ChangeLog)
+  - [Commits](https://github.com/PyCQA/astroid/compare/astroid-2.5...v2.7.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: astroid
+    dependency-type: direct:production
+  ...
+- Chore(deps): update pylint requirement from ~=2.8 to ~=2.10 (#628)
+  [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
+  - [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.8.0...v2.10.2)
+
+  ---
+  updated-dependencies:
+  - dependency-name: pylint
+    dependency-type: direct:production
+  ...
+- Chore(deps): update tox requirement from ~=3.23 to ~=3.24 (#619)
+  [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/master/docs/changelog.rst)
+  - [Commits](https://github.com/tox-dev/tox/compare/3.23.0...3.24.0)
+
+  ---
+  updated-dependencies:
+  - dependency-name: tox
+    dependency-type: direct:production
+  ...
+- Update changelog for 1.2.0. [Kazuaki Matsuo]
+
+
+v1.2.0 (2021-06-07)
+-------------------
+
+New
+~~~
+- Feat: allow to add a command dynamically (#608) [Kazuaki Matsuo]
+
+  * add add_commmand in python
+
+  * add test
+
+  * add exceptions, tweak method
+
+  * append docstring
+
+  * add $id example
+
+  * use pytest.raises
+
+  * add examples as docstring
+
+Other
+~~~~~
+- Bump 1.2.0. [Kazuaki Matsuo]
+- Chore(deps-dev): update pre-commit requirement from ~=2.12 to ~=2.13
+  (#607) [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/master/CHANGELOG.md)
+  - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.12.0...v2.13.0)
+- Chore(deps): update pytest-cov requirement from ~=2.11 to ~=2.12
+  (#606) [Kazuaki Matsuo, dependabot[bot]]
+
+  * chore(deps): update pytest-cov requirement from ~=2.11 to ~=2.12
+
+  Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version.
+  - [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
+  - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
+  - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.11.0...v2.12.0)
+- Chore(deps): update pylint requirement from ~=2.7 to ~=2.8 (#600)
+  [dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
+  - [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.7.0...pylint-2.8.1)
+- Chore(deps-dev): update pre-commit requirement from ~=2.11 to ~=2.12
+  (#599) [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/master/CHANGELOG.md)
+  - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.11.0...v2.12.0)
+- Chore(deps): update isort requirement from ~=5.7 to ~=5.8 (#596)
+  [dependabot[bot]]
+
+  Updates the requirements on [isort](https://github.com/pycqa/isort) to permit the latest version.
+  - [Release notes](https://github.com/pycqa/isort/releases)
+  - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md)
+  - [Commits](https://github.com/pycqa/isort/compare/5.7.0...5.8.0)
+
+
+v1.1.0 (2021-03-10)
+-------------------
+
+New
+~~~
+- Feat: Add optional location speed attribute for android devices (#594)
+  [salabogdan]
+- Feat: Added docstring for macOS screenrecord option (#580) [Mori
+  Atsushi]
+
+  * Added docstring for macOS screenrecord option
+
+  * tweak
+
+  * review comment
+- Feat: add warning to drop forceMjsonwp for W3C (#567) [Kazuaki Matsuo]
+
+  * tweak
+
+  * fix test
+
+  * print warning
+
+  * revert test
+
+  * Update webdriver.py
+
+  * fix autopep8
+- Feat: Added descriptions for newly added screenrecord opts (#540)
+  [Mori Atsushi]
+
+  * Add description for newly added opts for screen record
+
+  * Updates
+
+Test
+~~~~
+- Ci: Use node v12 (#585) [Mori Atsushi]
+
+  * Use node 12 on ci
+
+  * Update copyright
+
+  * Update README for doc
+
+  * tweak
+
+  * fix copyright
+
+  * try py310
+
+  * remove py310
+- Ci: remove travis (#581) [Mori Atsushi]
+
+  * Removed travis and run unit test on azure
+
+  * review comment
+
+  * run tox on azure pipelines
+
+  * removed tox-travis from pipfile
+- Ci: move azure project to Appium CI, update readme (#564) [Kazuaki
+  Matsuo]
+- Ci: Added py39-dev for travis (#557) [Mori Atsushi]
+
+  * ci: Added py39-dev
+
+  * Add xv option for debug
+
+  * [debug] pip list
+
+  * Avoid error in py39
+
+  * Updated modules in pre-commit
+- Ci: upgrade xcode and macos (#556) [Mori Atsushi]
+
+  * ci: upgrade xcode ver and macos
+
+  * Upgrade iOS ver for functional tests
+
+  * Changed xcode to 11.6
+
+Other
+~~~~~
+- Chore(deps-dev): update pre-commit requirement from ~=2.10 to ~=2.11
+  (#595) [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/master/CHANGELOG.md)
+  - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.10.0...v2.11.0)
+- Chore(deps): update tox requirement from ~=3.22 to ~=3.23 (#593)
+  [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/3.23.0/docs/changelog.rst)
+  - [Commits](https://github.com/tox-dev/tox/compare/3.22.0...3.23.0)
+- Chore(deps): update pylint requirement from ~=2.6 to ~=2.7 (#588)
+  [Mori Atsushi, dependabot[bot]]
+
+  Updates the requirements on [pylint](https://github.com/PyCQA/pylint) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/pylint/releases)
+  - [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
+  - [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.6.0...pylint-2.7.0)
+- Chore(deps): update astroid requirement from ~=2.4 to ~=2.5 (#587)
+  [dependabot[bot]]
+
+  Updates the requirements on [astroid](https://github.com/PyCQA/astroid) to permit the latest version.
+  - [Release notes](https://github.com/PyCQA/astroid/releases)
+  - [Changelog](https://github.com/PyCQA/astroid/blob/master/ChangeLog)
+  - [Commits](https://github.com/PyCQA/astroid/compare/astroid-2.4.0...astroid-2.5)
+- Chore(deps): update mypy requirement from ~=0.800 to ~=0.812 (#589)
+  [Mori Atsushi, dependabot[bot]]
+
+  * chore(deps): update mypy requirement from ~=0.800 to ~=0.812
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.800...v0.812)
+
+  Signed-off-by: dependabot[bot] <support@github.com>
+
+  * Fix mypy error with mypy v0.812 (#590)
+
+  * chore(deps): update mypy requirement from ~=0.800 to ~=0.812
+
+  Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.
+  - [Release notes](https://github.com/python/mypy/releases)
+  - [Commits](https://github.com/python/mypy/compare/v0.800...v0.812)
+- Chore(deps): update tox requirement from ~=3.21 to ~=3.22 (#586)
+  [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/master/docs/changelog.rst)
+  - [Commits](https://github.com/tox-dev/tox/compare/3.21.0...3.22.0)
+- Chore: Add table for screen_record kwarg (#582) [Mori Atsushi]
+
+  * Add table for kwarg
+
+  * update
+
+  * Add missing doc to stop_recording
+
+  * Push auto-generated changes by sphinx
+
+  * delete duplicated entry [skip ci]
+- Chore(deps): update isort requirement from ~=5.0 to ~=5.7 (#578)
+  [dependabot-preview[bot]]
+
+  Updates the requirements on [isort](https://github.com/pycqa/isort) to permit the latest version.
+  - [Release notes](https://github.com/pycqa/isort/releases)
+  - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md)
+  - [Commits](https://github.com/pycqa/isort/compare/5.0.0...5.7.0)
+- Create Dependabot config file (#579) [dependabot-preview[bot],
+  dependabot-preview[bot]]
+- Chore: Update pipfile to respect isort v5 (#577) [Mori Atsushi]
+- Chore: Fix iOS app management functional tests (#575) [Mori Atsushi]
+
+  * Added sleep to wait the app has gone
+
+  * Upgrade AndroidSDK to 30 from 27
+
+  * Added sleep to ios tc
+
+  * Fix android activities test
+
+  * Revert android sdk ver
+
+  * Used timer instead of fixed wait time
+
+  * Created wait_for
+
+  * Update test/functional/test_helper.py
+
+  * review comments
+
+  * review comments
+
+  * Extend callable type
+
+  * fix
+
+  * review comment
+
+  * review comment
+
+  * review comment
+
+  * fix comment
+- Chore: Fix functional keyboard tests with appium v1.21.0-beta.0 (#574)
+  [Mori Atsushi]
+
+  * Fix function keyboard tests
+
+  * Updated class name for keyboard
+- Chore: Apply Black code formatter (#571) [Mori Atsushi]
+
+  * Applied black (length: 120, String skipped)
+
+  * Updated related to ci
+
+  * Update README
+- Chore: address selenium-4 branch in readme (#566) [Kazuaki Matsuo]
+- Docs: fix wrong code example in README.md (#555) [sanlengjingvv]
+- Update changelog for 1.0.2. [Kazuaki Matsuo]
+
+
+v1.0.2 (2020-07-15)
+-------------------
+- Bump 1.0.2. [Kazuaki Matsuo]
+- Chore: Add the workaround to avoid service freezes on Windows (#552)
+  [Mykola Mokhnach]
+- Chore: add checking package file count comparison in release script
+  (#547) [Kazuaki Matsuo]
+
+  * chore: Add file count in release script
+
+  * use f string for Python 3 :P
+
+  * handle exit in method
+- Update changelog for 1.0.1. [Kazuaki Matsuo]
+
+
+v1.0.1 (2020-05-18)
+-------------------
+
+Fix
+~~~
+- Broken package (#545) [Kazuaki Matsuo]
+
+  * add appium/webdriver/py.typed in find_packages
+
+  * fix
+
+Other
+~~~~~
+- Bump 1.0.1. [Kazuaki Matsuo]
+- Update changelog for 1.0.0. [Kazuaki Matsuo]
+
+
+v1.0.0 (2020-05-16)
+-------------------
+
+New
+~~~
+- Feat: Added Makefile (#530) [Mori Atsushi]
+
+  * Created setup.cfg
+
+  * Updated lib ver for pre-commit
+
+  * Fix ci.sh to set failure even when one command failed
+
+  * Fix pylint error
+
+  * Add help to Makefile
+
+  * Update README
+
+  * Add check-all command
+- Feat: Merge python3 branch to master (#526) [Hannes Hauer, Hannes
+  Hauer <hanneshauer@beeware.at>    * chore: Update readme and
+  gitchangelog section role (#524) (#525)    * chore: tweak changelog
+  filter    * address stoping Python 2 support    * 2 instead of 2.0...
+  * tweak readme    * Revert some unexpected changes    * review
+  comments    * Changed bound for TypeVar    * Fix crashing ci    *
+  Remove beta    Co-authored-by: dependabot-preview[bot]
+  <27856297+dependabot-preview[bot]@users.noreply.github.com>, Kazuaki
+  Matsuo, Kazuaki Matsuo, Mori Atsushi, Mykola Mokhnach, Mykola
+  Mokhnach, Nrupesh Patel, Nrupesh Patel, Venkatesh, Venkatesh]
+
+  * Drop py2 support (#478)
+
+  * Drop py2 support
+
+  * Support 3.7+
+
+  * Add explicit type declarations (#482)
+
+  * Fixed mypy warning: touch_action.py
+
+  * Fixed mypy warning: multi_action.py
+
+  * Fixed mypy warning: extensions/android
+
+  * Fixed mypy warning: extensions/search_context
+
+  * Updated
+
+  * Revert some changes to run unit test
+
+  * Review comments
+
+  * Updates
+
+  * Updates
+
+  * Add mypy check to ci.sh
+
+  * Add mypy to Pipfile
+
+  * Updates
+
+  * Update README
+
+  * Revert unexpected changes
+
+  * Updates Dict
+
+  * Revert unexpected changes
+
+  * Updates
+
+  * Review comments
+
+  * Review comments
+
+  * tweak
+
+  * Restore and modify changes
+
+  * Fix wrong return type
+
+  * Add comments
+
+  * Revert unexpected changes
+
+  * Fix mypy error
+
+  * updates
+
+  * Add mypy to pre-commit (#485)
+
+  * chore: Applied some py3 formats (#486)
+
+  * Removed unused import
+
+  * Removed unnecessary codes
+
+  * Applied f'' format instead ''.format()
+
+  * Fixes
+
+  * tweak
+
+  * chore: Fix mypy errors under test folder (#487)
+
+  * Fix mypy errors under test folder
+
+  * Add mypy check for test folder to pre-commit
+
+  * Add mypy check to ci
+
+  * chore: Remove unittest dependency (#488)
+
+  * Removed unnecessary codes from calling super
+
+  * Removed unittest dependency
+
+  * Upgrade the dependencies to the latest
+
+  * Removed unused args
+
+  * Review comments
+
+  * Update mock requirement from ~=3.0 to ~=4.0 (#502)
+
+  Updates the requirements on [mock](https://github.com/testing-cabal/mock) to permit the latest version.
+  - [Release notes](https://github.com/testing-cabal/mock/releases)
+  - [Changelog](https://github.com/testing-cabal/mock/blob/master/CHANGELOG.rst)
+  - [Commits](https://github.com/testing-cabal/mock/compare/3.0.0...4.0.0)
+
+  Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
+
+  * Add 'from' to except (#503)
+
+  * Update pre-commit requirement from ~=1.21 to ~=2.1 (#506)
+
+  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/master/CHANGELOG.md)
+  - [Commits](https://github.com/pre-commit/pre-commit/compare/v1.21.0...v2.1.0)
+
+  Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
+
+  * doc: Add script to generate sphinx doc  (#508)
+
+  * Add quickstart template files
+
+  * Update conf file
+
+  * Update
+
+  * Update settings
+
+  * Change project name
+
+  * Add script to generate docs
+
+  * Changed header title
+
+  * Add new line to usage section
+
+  * Add py.typed file(PEP561)
+
+  * Replace \n with new line
+
+  * tweak
+
+  * Use sphinx format for tables
+
+  * Rebase python3 branch with master (#522)
+
+  * Update pytest-cov requirement from ~=2.6 to ~=2.8 (#489)
+
+  Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version.
+  - [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
+  - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
+  - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.6.0...v2.8.1)
+
+  Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
+
+  * Update autopep8 requirement from ~=1.4 to ~=1.5 (#490)
+
+  Updates the requirements on [autopep8](https://github.com/hhatto/autopep8) to permit the latest version.
+  - [Release notes](https://github.com/hhatto/autopep8/releases)
+  - [Commits](https://github.com/hhatto/autopep8/compare/v1.4...v1.5)
+
+  Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
+
+  * Update tox-travis requirement from ~=0.11 to ~=0.12 (#491)
+
+  Updates the requirements on [tox-travis](https://github.com/tox-dev/tox-travis) to permit the latest version.
+  - [Release notes](https://github.com/tox-dev/tox-travis/releases)
+  - [Changelog](https://github.com/tox-dev/tox-travis/blob/master/HISTORY.rst)
+  - [Commits](https://github.com/tox-dev/tox-travis/compare/0.11...0.12)
+
+  Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
+
+  * Update tox requirement from ~=3.6 to ~=3.14 (#494)
+
+  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/master/docs/changelog.rst)
+  - [Commits](https://github.com/tox-dev/tox/compare/3.6.0...3.14.3)
+
+  Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
+
+  * chore: Fix find_by_images_tests.py (#495)
+
+  * chore: Fix find_by_images_tests.py
+
+  * Add installation opencv4nodejs
+
+  * Fix typo
+
+  * Add taking screen record to find_by_image_test
+
+  * Fix errors on the emulator
+
+  * Remove unused imports
+
+  * feat: Add viewmatcher (#480)
+
+  * Add android view matcher as strategy locator
+
+  * Add docstring
+
+  * Add functional test
+
+  * Remove find_elements_by_android_data_matcher
+
+  * Fix docstring
+
+  * tweak docstring
+
+  * Bump 0.50
+
+  * Update changelog for 0.50
+
+  * Fix flaky functional tests (#473)
+
+  * Run all tests
+
+  * Fix apk file path
+
+  * Skip find_element_by_image test cases
+
+  * Skip context switching test
+
+  * Skip multi tap test on CI
+
+  * Change strategy for waiting element
+
+  * Add functions for same steps
+
+  * Restore unexpected changes
+
+  * Fix touch_action_tests
+
+  * Fix
+
+  * Fix
+  Fix test_driver_swipe
+
+  * fix
+
+  * Create _move_to_[target_view]
+
+  * [test_driver_swipe] Add wait
+
+  * feat: Add idempotency key header to create session requests (#514)
+
+  * feat: Override send_keys without file upload function (#515)
+
+  * add send_keys_direct
+
+  * override send_keys
+
+  * tune
+
+  * add unittest instead of functional test
+
+  * tweak syntax
+
+  * Bump 0.51
+
+  * Update changelog for 0.51
+
+  * test: Fix test_clear flaky functional test (#519)
+
+  * test: Add unit test for set_value (setImmediateValue) (#518)
+
+  * chore: Fix int - str comparison error in ios desired capabilities (#517)
+
+  if number >= PytestXdistWorker.COUNT:
+
+Fix
+~~~
+- Tune mixin types, so linters could recognize them better (#536)
+  [Mykola Mokhnach]
+
+Test
+~~~~
+- Test: Add appium_service functional test (#531) [Mori Atsushi]
+
+  * Add appium_service functional test
+
+  * Fix expressions
+
+Other
+~~~~~
+- Bump 1.0.0. [Kazuaki Matsuo]
+- Chore: Updates docstring (#533) [Mori Atsushi]
+
+  * Updates docstring
+
+  * Add description to Returns field
+
+  * Remove type from docstring
+
+  Since type hint already added to args
+
+  * Set default lang to en
+
+  * Change usage style in docstring
+
+  * Updates
+
+  * Remove rtype
+
+  unnecessary anymore since type hint works for auto completion
+
+  * tweak
+
+  * Update return type
+
+  * Restore types for keyword args
+
+  * Remove types from Return field
+
+  Except for property and TypeVar
+- Chore: Remove  saucetestcase from the client (#539) [Mykola Mokhnach]
+- Chore: add py.typed in package, add maintainers (#538) [Kazuaki
+  Matsuo]
+- Docs: Update documentation (#527) [Kazuaki Matsuo]
+
+  * Chore: correct license, update readme
+
+  * cleanup
+
+  * docs: update the url of documentation
+- Chore: Update readme and gitchangelog section role (#524) [Kazuaki
+  Matsuo]
+
+  * chore: tweak changelog filter
+
+  * address stoping Python 2 support
+
+  * 2 instead of 2.0...
+
+  * tweak readme
 - Update changelog for 0.52. [Kazuaki Matsuo]
 
 
@@ -1067,6 +5477,11 @@ Other
 ~~~~~
 - Update changelog for 0.38. [Kazuaki Matsuo]
 - Bump 0.38. [Kazuaki Matsuo]
+
+
+v0.38 (2019-02-11)
+------------------
+- Bump 0.38. [Kazuaki Matsuo]
 - Remove io.open from getting version code (#334) [Kazuaki Matsuo]
 
   * remove io.open
diff --git a/LICENSE b/LICENSE
index 25caf4058..540e41dcb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,192 @@
-Copyright 2012-2014 Appium Committers
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright JS Foundation and other contributors, https://js.foundation
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..ad2c96828
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,51 @@
+.PHONY: Commands for developers
+
+.PHONY: check-all
+check-all: check unittest
+
+.PHONY: check
+check: check-lint check-format
+
+.PHONY: check-lint
+check-lint:
+	uv run ruff check .
+	uv run mypy appium
+
+.PHONY: check-format
+check-format:
+	uv run ruff format --check .
+
+.PHONY: fix
+fix: fix-lint fix-format
+
+.PHONY: fix-lint
+fix-lint:
+	uv run ruff check --fix .
+
+.PHONY: fix-format
+fix-format:
+	uv run ruff format .
+
+.PHONY: install-uv
+install-uv:
+	@command -v uv >/dev/null 2>&1 || { \
+		echo "Installing uv"; \
+		curl -LsSf https://astral.sh/uv/install.sh | sh; \
+		if [ -n "$$GITHUB_PATH" ]; then \
+			echo "PATH=$$HOME/.local/bin:$$PATH" >> $$GITHUB_PATH; \
+		else \
+			echo "Please restart your shell or run 'exec $$SHELL'"; \
+		fi; \
+	}
+
+.PHONY: sync-dev
+sync-dev:
+	uv sync
+
+.PHONY: unittest
+unittest: ## Run unittest
+	uv run pytest $(ARGS) test/unit/
+
+.PHONY: help
+help: ## Display this help screen
+	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 7446b7b3d..000000000
--- a/Pipfile
+++ /dev/null
@@ -1,27 +0,0 @@
-[[source]]
-name = "pypi"
-url = "https://pypi.org/simple"
-verify_ssl = true
-
-[dev-packages]
-pre-commit = "~=1.13"
-
-[packages]
-selenium = "~=3.141"
-
-autopep8 = "~=1.5"
-
-pytest = "~=4.0"
-pytest-cov = "~=2.8"
-
-tox = "~=3.14"
-tox-travis = "~=0.12"
-
-httpretty = "~=0.9"
-python-dateutil = "~=2.8"
-mock = "~=3.0"
-
-# TODO Update to the latest ver when py2 support dropped
-pylint = "~=1.9"
-astroid = "~=1.6"
-isort = "~=4.3"
diff --git a/README.md b/README.md
index 347088374..927580471 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,13 @@
-Appium Python Client
-====================
+# Appium Python Client
 
 [![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://travis-ci.org/appium/python-client.svg?branch=master)](https://travis-ci.org/appium/python-client)
-[![Build Status](https://dev.azure.com/ki4070ma/python-client/_apis/build/status/appium.python-client?branchName=master)](https://dev.azure.com/ki4070ma/python-client/_build/latest?definitionId=2&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)
 
-An extension library for adding [Selenium 3.0 draft](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html) and [Mobile JSON Wire Protocol Specification draft](https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md)
-functionality to the Python language bindings, for use with the mobile testing
-framework [Appium](https://appium.io).
+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).
 
-# Notice
-
-**Since v1.0.0 only Python 3 is supported**
-
-# Getting the Appium Python client
+## Getting the Appium Python client
 
 There are three ways to install and use the Appium Python client.
 
@@ -45,92 +37,125 @@ download and unarchive the source tarball (Appium-Python-Client-X.X.tar.gz).
     python setup.py install
     ```
 
-# Development
-
-- Style Guide: https://www.python.org/dev/peps/pep-0008/
-    - `autopep8` helps to format code automatically
-        ```
-        $ python -m autopep8 -r --global-config .config-pep8 -i .
+## Compatibility Matrix
+
+|Appium Python Client| Selenium binding| Python version |
+|----|----|----|
+|`5.1.1`+|`4.26.0`+ | 3.9+ |
+|`4.5.0` - `5.1.0`|`4.26.0` - `4.31.0` | 3.9+ |
+|`4.3.0` -  `4.4.0`|`4.26.0` - `4.31.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+ |
+|`1.0.0` - `1.3.0` |`3.x`| 3.7+ |
+|`0.52` and below|`3.x`| 2.7, 3.4 - 3.7 |
+
+The Appium Python Client depends on [Selenium Python binding](https://pypi.org/project/selenium/), thus
+the Selenium Python binding update might affect the Appium Python Client behavior.
+For example, some changes in the Selenium binding could break the Appium client.
+
+> **Note**
+> We strongly recommend you manage dependencies with version management tools such as
+> [uv](https://docs.astral.sh/uv/) 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
+        )
         ```
-    - `isort` helps to order imports automatically
-        ```
-        $ python -m isort -rc .
-        ```
-        - When you use newly 3rd party modules, add it to [.isort.cfg](.isort.cfg) to keep import order correct
-- Docstring style: Google Style
-    - Refer [link](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)
-- You can customise `CHANGELOG.rst` with commit messages following [.gitchangelog.rc](.gitchangelog.rc)
-    - It generates readable changelog
-- Setup
-    - `pip install --user pipenv`
-    - `python -m pipenv lock --clear`
-       - If you experience the below error, then refer [pypa/pipenv#187](https://github.com/pypa/pipenv/issues/187) to solve it.
-          ```
-          Locking Failed! unknown locale: UTF-8
-          ```
-    - `python -m pipenv install --dev --system`
-    - `pre-commit install`
-
-## Run tests
-
-You can run all of tests running on CI via `tox` in your local.
-
-```
-$ tox
-```
-
-You also can run particular tests like below.
-
-### Unit
-
-```
-$ pytest test/unit
-```
-
-Run with `pytest-xdist`
-
-```
-$ pytest -n 2 test/unit
-```
-
-### Functional
-
-```
-$ pytest test/functional/ios/search_context/find_by_ios_class_chain_tests.py
-```
-
-### In parallel for iOS
-1. Create simulators named 'iPhone 8 - 8100' and 'iPhone 8 - 8101'
-2. Install test libraries via pip
-    ```
-    $ pip install pytest pytest-xdist
-    ```
-3. Run tests
-    ```
-    $ pytest -n 2 test/functional/ios/search_context/find_by_ios_class_chain_tests.py
-    ```
-
-# Release
-
-Follow below steps.
-
-```bash
-$ pip install twine
-$ pip install git+git://github.com/vaab/gitchangelog.git # Getting via GitHub repository is necessary for Python 3.7
-# Type the new version number and 'yes' if you can publish it
-# You can test the command with DRY_RUN
-$ DRY_RUN=1 ./release.sh
-$ ./release.sh # release
-```
-
-# Usage
-
-The Appium Python Client is fully compliant with the Selenium 3.0 specification
-draft, with some helpers to make mobile testing in Python easier. The majority of
-the usage remains as it has been for Selenium 2 (WebDriver), and as the [official
-Selenium Python bindings](https://pypi.org/project/selenium/) begins to
-implement the new specification that implementation will be used underneath, so
-test code can be written that is utilizable with both bindings.
+        - 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
+        - [appium/webdriver/extensions/action_helpers.py](appium/webdriver/extensions/action_helpers.py)
+        - https://www.selenium.dev/documentation/webdriver/actions_api/
+        - https://www.youtube.com/watch?v=oAJ7jwMNFVU
+        - https://appiumpro.com/editions/30-ios-specific-touch-action-methods
+        - https://appiumpro.com/editions/29-automating-complex-gestures-with-the-w3c-actions-api
+    - Deprecated `AppiumBy.WINDOWS_UI_AUTOMATION`, which has no usage right now.
+
+### Quick migration guide from v2 to v3
+- `options` keyword argument in the `webdriver.Remote` constructor such as `XCUITestOptions` instead of `desired_capabilities`
+    - Available options are https://github.com/appium/python-client/tree/master/appium/options
+        - Please check the [Usage](#usage) below as an example.
+    - Not a "new" change, but the `desired_capabilities` argument has been removed since v3.
+- Replacement
+    - `start_activity` method: Please use [`mobile: startActivity`](https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#mobile-startactivity)
+    - `launch_app`, `close_app` and `reset` methods: Please refer to https://github.com/appium/appium/issues/15807
+    - `available_ime_engines`, `is_ime_active`, `activate_ime_engine`, `deactivate_ime_engine` and `active_ime_engine` methods: Please use [`mobile: shell`](https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#mobile-shell)
+    - `set_value` and `set_text` methods: Please use `element.send_keys` or `send_keys` by W3C Actions
+- Removal
+    - `end_test_coverage` method is no longer available
+    - `session` property is no longer available
+    - `all_sessions` property is no longer available
+
+### Quick migration guide from v1 to v2
+- Enhancement
+    - Updated base Selenium Python binding version to v4
+        - Removed `forceMjsonwp` since Selenium v4 and Appium Python client v2 expect only W3C WebDriver protocol
+    - Methods `ActionHelpers#scroll`, `ActionHelpers#drag_and_drop`, `ActionHelpers#tap`, `ActionHelpers#swipe` and `ActionHelpers#flick` now call W3C actions as its backend
+        - Please check each behavior. Their behaviors could slightly differ.
+    - Added `strict_ssl` to relax SSL errors such as self-signed ones
+- Deprecated
+    - `MultiAction` and `TouchAction` are deprecated. Please use W3C WebDriver actions or `mobile:` extensions
+    - `launch_app`, `close_app`, and `reset` are deprecated. Please read [issues#15807](https://github.com/appium/appium/issues/15807) for more details
+
+#### MultiAction/TouchAction to W3C actions
+
+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 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
+
+actions = ActionChains(driver)
+# override as 'touch' pointer action
+actions.w3c_actions = ActionBuilder(driver, 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.pause(2)
+actions.w3c_actions.pointer_action.move_to_location(end_x, end_y)
+actions.w3c_actions.pointer_action.release()
+actions.perform()
+```
+
+- [appium/webdriver/extensions/action_helpers.py](appium/webdriver/extensions/action_helpers.py)
+- https://www.selenium.dev/documentation/webdriver/actions_api/
+
+## Usage
+
+The Appium Python Client is fully compliant with the WebDriver Protocol
+including several helpers to make mobile testing in Python easier.
 
 To use the new functionality now, and to use the superset of functions, instead of
 including the Selenium `webdriver` module in your test code, use that from
@@ -142,44 +167,129 @@ from appium import webdriver
 
 From there much of your test code will work with no change.
 
-As a base for the following code examples, the following sets up the [UnitTest](https://docs.python.org/2/library/unittest.html)
+As a base for the following code examples, the following set up the [UnitTest](https://docs.python.org/3/library/unittest.html)
 environment:
 
 ```python
-# Android environment
-import unittest
-from appium import webdriver
-
-desired_caps = {}
-desired_caps['platformName'] = 'Android'
-desired_caps['platformVersion'] = '8.1'
-desired_caps['automationName'] = 'uiautomator2'
-desired_caps['deviceName'] = 'Android Emulator'
-desired_caps['app'] = PATH('../../../apps/selendroid-test-app.apk')
-
-self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
-```
+# Python/Pytest
+import pytest
 
-```python
-# iOS environment
-import unittest
 from appium import webdriver
-
-desired_caps = {}
-desired_caps['platformName'] = 'iOS'
-desired_caps['platformVersion'] = '11.4'
-desired_caps['automationName'] = 'xcuitest'
-desired_caps['deviceName'] = 'iPhone Simulator'
-desired_caps['app'] = PATH('../../apps/UICatalog.app.zip')
-
-self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
-```
-
-## Changed or added functionality
-
-The methods that do change are...
-
-### Direct Connect URLs
+# Options are only available since client version 2.3.0
+# If you use an older client then switch to desired_capabilities
+# instead: https://github.com/appium/python-client/pull/720
+from appium.options.android import UiAutomator2Options
+from appium.options.ios import XCUITestOptions
+from appium.webdriver.appium_service import AppiumService
+from appium.webdriver.common.appiumby import AppiumBy
+
+APPIUM_PORT = 4723
+APPIUM_HOST = '127.0.0.1'
+
+
+# HINT: fixtures below could be extracted into conftest.py
+# HINT: and shared across all tests in the suite
+@pytest.fixture(scope='session')
+def appium_service():
+    service = AppiumService()
+    service.start(
+        # Check the output of `appium server --help` for the complete list of
+        # server command line arguments
+        args=['--address', APPIUM_HOST, '-p', str(APPIUM_PORT)],
+        timeout_ms=20000,
+    )
+    yield service
+    service.stop()
+
+
+def create_ios_driver(custom_opts = None):
+    options = XCUITestOptions()
+    options.platformVersion = '13.4'
+    options.udid = '123456789ABC'
+    if custom_opts is not None:
+        options.load_capabilities(custom_opts)
+    # Appium1 points to http://127.0.0.1:4723/wd/hub by default
+    return webdriver.Remote(f'http://{APPIUM_HOST}:{APPIUM_PORT}', options=options)
+
+
+def create_android_driver(custom_opts = None):
+    options = UiAutomator2Options()
+    options.platformVersion = '10'
+    options.udid = '123456789ABC'
+    if custom_opts is not None:
+        options.load_capabilities(custom_opts)
+    # Appium1 points to http://127.0.0.1:4723/wd/hub by default
+    return webdriver.Remote(f'http://{APPIUM_HOST}:{APPIUM_PORT}', options=options)
+
+
+@pytest.fixture
+def ios_driver_factory():
+    return create_ios_driver
+
+
+@pytest.fixture
+def ios_driver():
+    # prefer this fixture if there is no need to customize driver options in tests
+    driver = create_ios_driver()
+    yield driver
+    driver.quit()
+
+
+@pytest.fixture
+def android_driver_factory():
+    return create_android_driver
+
+
+@pytest.fixture
+def android_driver():
+    # prefer this fixture if there is no need to customize driver options in tests
+    driver = create_android_driver()
+    yield driver
+    driver.quit()
+
+
+def test_ios_click(appium_service, ios_driver_factory):
+    # Usage of the context manager ensures the driver session is closed properly
+    # after the test completes. Otherwise, make sure to call `driver.quit()` on teardown.
+    with ios_driver_factory({
+        'appium:app': '/path/to/app/UICatalog.app.zip'
+    }) as driver:
+        el = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='item')
+        el.click()
+
+
+def test_android_click(appium_service, android_driver_factory):
+    # Usage of the context manager ensures the driver session is closed properly
+    # after the test completes. Otherwise, make sure to call `driver.quit()` on teardown.
+    with android_driver_factory({
+        'appium:app': '/path/to/app/test-app.apk',
+        'appium:udid': '567890',
+    }) as driver:
+        el = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='item')
+        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:
 
@@ -191,533 +301,247 @@ If your Selenium/Appium server decorates the new session capabilities response w
 Then python client will switch its endpoint to the one specified by the values of those keys.
 
 ```python
-import unittest
 from appium import webdriver
+# Options are only available since client version 2.3.0
+# 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
 
-desired_caps = {}
-desired_caps['platformName'] = 'iOS'
-desired_caps['platformVersion'] = '11.4'
-desired_caps['automationName'] = 'xcuitest'
-desired_caps['deviceName'] = 'iPhone Simulator'
-desired_caps['app'] = PATH('../../apps/UICatalog.app.zip')
-
-self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps, direct_connection=True)
-```
-
+# load_capabilities API could be used to
+# load options mapping stored in a dictionary
+options = XCUITestOptions().load_capabilities({
+    'platformVersion': '13.4',
+    'deviceName': 'iPhone Simulator',
+    'app': '/full/path/to/app/UICatalog.app.zip',
+})
 
-### Switching between 'Native' and 'Webview'
+client_config = AppiumClientConfig(
+    remote_server_addr='http://127.0.0.1:4723',
+    direct_connection=True
+)
 
-For mobile testing the Selenium methods for switching between windows was previously
-commandeered for switching between native applications and webview contexts. Methods
-explicitly for this have been added to the Selenium 3 specification, so moving
-forward these 'context' methods are to be used.
-
-To get the current context, rather than calling `driver.current_window_handle` you
-use
-
-```python
-current = driver.current_context
+driver = webdriver.Remote(
+    # Appium1 points to http://127.0.0.1:4723/wd/hub by default
+    'http://127.0.0.1:4723',
+    options=options,
+    client_config=client_config
+)
 ```
 
-The available contexts are not retrieved using `driver.window_handles` but with
+## Relax SSL validation
 
-```python
-driver.contexts
-```
-
-Finally, to switch to a new context, rather than `driver.switch_to.window(name)`,
-use the comparable context method
+`strict_ssl` option allows you to send commands to an invalid certificate host like a self-signed one.
 
 ```python
-context_name = "WEBVIEW_1"
-driver.switch_to.context(context_name)
-```
-
-
-### Finding elements by iOS UIAutomation search
-
-This allows elements in iOS applications to be found using recursive element
-search using the UIAutomation library. This method is supported on iOS devices
-that still support UIAutomation, that is, versions which predate XCUITEST.
-
-Adds the methods `driver.find_element_by_ios_uiautomation`
-and `driver.find_elements_by_ios_uiautomation`.
-
-```python
-el = self.driver.find_element_by_ios_uiautomation('.elements()[0]')
-self.assertEqual('UICatalog', el.get_attribute('name'))
-```
-
-```python
-els = self.driver.find_elements_by_ios_uiautomation('.elements()')
-self.assertIsInstance(els, list)
-```
-
-
-### Finding elements by Android UIAutomator search
-
-This allows elements in an Android application to be found using recursive element
-search using the UIAutomator library. Adds the methods `driver.find_element_by_android_uiautomator`
-and `driver.find_elements_by_android_uiautomator`.
-
-```python
-el = self.driver.find_element_by_android_uiautomator('new UiSelector().description("Animation")')
-self.assertIsNotNone(el)
-```
-
-```python
-els = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)')
-self.assertIsInstance(els, list)
-```
-
-### Finding elements by Android viewtag search
-
-This method allows finding elements using [View#tags](https://developer.android.com/reference/android/view/View#tags).
-This method works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
-
-Adds the methods `driver.find_element_by_android_viewtag` and `driver.find_elements_by_android_viewtag`.
-
-```python
-el = self.driver.find_element_by_android_viewtag('a tag name')
-self.assertIsNotNone(el)
-```
-
-```python
-els = self.driver.find_elements_by_android_viewtag('a tag name')
-self.assertIsInstance(els, list)
-```
-
-### Finding elements by iOS predicates
-
-This method allows finding elements using iOS predicates. The methods take a
-string in the format of a predicate, including element type and the value of
-fields.
-
-Adds the methods
-`driver.find_element_by_ios_predicate` and `find_elements_by_ios_predicate`.
-
-```python
-el = self.driver.find_element_by_ios_predicate('wdName == "Buttons"')
-self.assertIsNotNone(el)
-```
-
-```python
-els = self.driver.find_elements_by_ios_predicate('wdValue == "SearchBar" AND isWDDivisible == 1')
-self.assertIsInstance(els, list)
-```
-
-
-### Finding elements by iOS class chain
-
-**This method is only for [XCUITest driver](https://github.com/appium/appium-xcuitest-driver)**
-
-This method allows finding elements using iOS class chain. The methods take
-a string in the format of a class chain, including element type.
-
-Adds the methods
-`driver.find_element_by_ios_class_chain` and `find_elements_by_ios_class_chain`.
+from appium import webdriver
+# Options are only available since client version 2.3.0
+# If you use an older client then switch to desired_capabilities
+# instead: https://github.com/appium/python-client/pull/720
+from appium.options.common import AppiumOptions
 
-```python
-el = self.driver.find_element_by_ios_class_chain('XCUIElementTypeWindow/XCUIElementTypeButton[3]')
-self.assertIsNotNone(el)
-```
+options = AppiumOptions()
+options.platform_name = 'mac'
+options.automation_name = 'safari'
+# set_capability API allows to provide any custom option
+# calls to it could be chained
+options.set_capability('browser_name', 'safari')
 
-```python
-els = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/XCUIElementTypeButton')
-self.assertIsInstance(els, list)
+# Appium1 points to http://127.0.0.1:4723/wd/hub by default
+driver = webdriver.Remote('http://127.0.0.1:4723', options=options, strict_ssl=False)
 ```
 
-### Finding elements by Accessibility ID
-
-Allows for elements to be found using the "Accessibility ID". The methods take a
-string representing the accessibility id or label attached to a given element, e.g., for iOS the accessibility identifier and for Android the content-description. Adds the methods
-`driver.find_element_by_accessibility_id` and `find_elements_by_accessibility_id`.
-
-```python
-el = self.driver.find_element_by_accessibility_id('Animation')
-self.assertIsNotNone(el)
-```
+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
-els = self.driver.find_elements_by_accessibility_id('Animation')
-self.assertIsInstance(els, list)
-```
-
-
-### Touch actions
-
-In order to accommodate mobile touch actions, and touch actions involving
-multiple pointers, the Selenium 3.0 draft specifies ["touch gestures"](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#touch-gestures) and ["multi actions"](https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#multiactions-1), which build upon the touch actions.
-
-move_to: note that use keyword arguments if no element
-
-The API is built around `TouchAction` objects, which are chains of one or more actions to be performed in a sequence. The actions are:
-
-#### `perform`
-
-The `perform` method sends the chain to the server in order to be enacted. It also empties the action chain, so the object can be reused. It will be at the end of all single action chains, but is unused when writing multi-action chains.
-
-#### `tap`
-
-The `tap` method stands alone, being unable to be chained with other methods. If you need a `tap`-like action that starts a longer chain, use `press`.
+from appium import webdriver
 
-It can take either an element with an optional x-y offset, or absolute x-y coordinates for the tap, and an optional count.
+from selenium.webdriver.remote.client_config import ClientConfig
 
-```python
-el = self.driver.find_element_by_accessibility_id('Animation')
-action = TouchAction(self.driver)
-action.tap(el).perform()
-el = self.driver.find_element_by_accessibility_id('Bouncing Balls')
-self.assertIsNotNone(el)
+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)
 ```
 
-#### `press`
-
-#### `long_press`
+## Set custom `AppiumConnection`
 
-#### `release`
+The first argument of `webdriver.Remote` can set an arbitrary command executor for you.
 
-#### `move_to`
-
-#### `wait`
-
-#### `cancel`
-
-
-### Multi-touch actions
-
-In addition to chains of actions performed within a single gesture, it is also possible to perform multiple chains at the same time, to simulate multi-finger actions. This is done through building a `MultiAction` object that comprises a number of individual `TouchAction` objects, one for each "finger".
-
-Given two lists next to each other, we can scroll them independently but simultaneously:
+1. Set init arguments for the pool manager Appium Python client uses to manage HTTP requests.
 
 ```python
-els = self.driver.find_elements_by_class_name('listView')
-a1 = TouchAction()
-a1.press(els[0]) \
-    .move_to(x=10, y=0).move_to(x=10, y=-75).move_to(x=10, y=-600).release()
-
-a2 = TouchAction()
-a2.press(els[1]) \
-    .move_to(x=10, y=10).move_to(x=10, y=-300).move_to(x=10, y=-600).release()
-
-ma = MultiAction(self.driver, els[0])
-ma.add(a1, a2)
-ma.perform();
-```
-
-### Appium-Specific touch actions
-
-There are a small number of operations that mobile testers need to do quite a bit that can be relatively complicated to build using the Touch and Multi-touch Action API.  For these we provide some convenience methods in the Appium client.
-
-#### `driver.tap`
-
-This method, on the WebDriver object, allows for tapping with multiple fingers, simply by passing in an array of x-y coordinates to tap.
+from appium import webdriver
+from appium.options.ios import XCUITestOptions
 
-```python
-el = self.driver.find_element_by_name('Touch Paint')
-action.tap(el).perform()
+import urllib3
+from appium.webdriver.appium_connection import AppiumConnection
 
-# set up array of two coordinates
-positions = []
-positions.append((100, 200))
-positions.append((100, 400))
+# Retry connection error up to 3 times.
+init_args_for_pool_manage = {
+    'retries': urllib3.util.retry.Retry(total=3, connect=3, read=False)
+}
+appium_executor = AppiumConnection(
+    remote_server_addr='http://127.0.0.1:4723',
+    init_args_for_pool_manage=init_args_for_pool_manage
+)
 
-self.driver.tap(positions)
+options = XCUITestOptions()
+options.platformVersion = '13.4'
+options.udid = '123456789ABC'
+options.app = '/full/path/to/app/UICatalog.app.zip'
+driver = webdriver.Remote(appium_executor, options=options)
 ```
 
-#### `driver.swipe`
-
-Swipe from one point to another point.
 
-
-### Application management methods
-
-There are times when you want, in your tests, to manage the running application,
-such as installing or removing an application, etc.
-
-
-#### Backgrounding an application
-
-The method `driver.background_app` sends the running application to the background
-for the specified amount of time, in seconds. After that time, the application is
-brought back to the foreground.
+2. Define a subclass of `AppiumConnection`
 
 ```python
-driver.background_app(1)
-sleep(2)
-el = driver.find_element_by_name('Animation')
-assertIsNotNone(el)
-```
-
-
-#### Checking if an application is installed
+from appium import webdriver
+from appium.options.ios import XCUITestOptions
 
-To check if an application is currently installed on the device, use the `device.is_app_installed`
-method. This method takes the bundle id of the application and return `True` or
-`False`.
+from appium.webdriver.appium_connection import AppiumConnection
 
-```python
-assertFalse(self.driver.is_app_installed('sdfsdf'))
-assertTrue(self.driver.is_app_installed('com.example.android.apis'))
-```
+class CustomAppiumConnection(AppiumConnection):
+    # Can add your own methods for the custom class
+    pass
 
+custom_executor = CustomAppiumConnection(remote_server_addr='http://127.0.0.1:4723')
 
-#### Installing an application
+options = XCUITestOptions().load_capabilities({
+    'platformVersion': '13.4',
+    'deviceName': 'iPhone Simulator',
+    'app': '/full/path/to/app/UICatalog.app.zip',
+})
+driver = webdriver.Remote(custom_executor, options=options)
 
-To install an uninstalled application on the device, use `device.install_app`,
-sending in the path to the application file or archive.
-
-```python
-assertFalse(driver.is_app_installed('io.selendroid.testapp'))
-driver.install_app('/Users/isaac/code/python-client/test/apps/selendroid-test-app.apk')
-assertTrue(driver.is_app_installed('io.selendroid.testapp'))
 ```
 
+The `AppiumConnection` can set `selenium.webdriver.remote.client_config.ClientConfig` as well.
 
-#### Removing an application
-
-If you need to remove an application from the device, use `device.remove_app`,
-passing in the application id.
+## Relaxing HTTP request read timeout
 
-```python
-assertTrue(driver.is_app_installed('com.example.android.apis'))
-driver.remove_app('com.example.android.apis')
-assertFalse(driver.is_app_installed('com.example.android.apis'))
-```
+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. **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))
 
-#### Closing and Launching an application
+## Documentation
 
-To launch the application specified in the desired capabilities, call `driver.launch_app`.
-Closing that application is initiated by `driver.close_app`
+- https://appium.github.io/python-client-sphinx/ is detailed documentation
+- [functional tests](test/functional) also may help to see concrete examples.
 
-```python
-el = driver.find_element_by_name('Animation')
-assertIsNotNone(el)
-driver.close_app();
-
-try:
-    driver.find_element_by_name('Animation')
-except Exception as e:
-    pass # should not exist
-
-driver.launch_app()
-el = driver.find_element_by_name('Animation')
-assertIsNotNone(el)
-```
+## Development
 
-#### Resetting an application
-
-To reset the running application, use `driver.reset`.
-
-```python
-el = driver.find_element_by_name('App')
-el.click()
+- Code Style: [PEP-0008](https://www.python.org/dev/peps/pep-0008/)
+  - 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`
 
-driver.reset()
-sleep(5)
+### Setup
 
-el = driver.find_element_by_name('App')
-assertIsNotNone(el)
+```bash
+make install-uv
+exec $SHELL
+make sync-dev
 ```
 
+Running above commands should automatically setup the virtual environment for the project
+using the default system Python version and put it into the `.venv` folder under the project root.
+If you'd like to customize the Python version then run the following command before `make sync-dev`:
 
-### Other methods
-
-
-#### Start an arbitrary activity
-
-The `driver.start_activity` method opens arbitrary activities on a device.
-If the activity is not part of the application under test, it will also
-launch the activity's application.
-
-```python
-driver.start_activity('com.foo.app', '.MyActivity')
+```bash
+uv venv --python <V>
 ```
 
-You can also pass additional arguments to start an activity with intent as below,
+where `<V>` is the actual Python version, for example `3.12`.
 
-```python
-driver.start_activity('com.foo.app', '.MainActivity', app_wait_package='your package name')
-```
+If you want to customize the folder where uv stores the virtual environment by default
+(e.g. `.venv`) then add an argument containing the destination folder path to the above command:
 
-and the list of additional arguments that can be passed are,
-
-```python
-             'app_wait_package'
-             'app_wait_activity'
-             'intent_action'
-             'intent_category'
-             'intent_flags'
-             'optional_intent_arguments'
-             'dont_stop_app_on_reset'
+```bash
+uv venv /venv/root/folder
 ```
 
-#### Retrieving application strings
-
-The property method `driver.app_strings` returns the application strings from
-the application on the device.
+In order to activate the newly created virtual environment you may either source it:
 
-```python
-strings = driver.app_strings
+```bash
+source /venv/root/folder/bin/activate
 ```
 
+or add it to PATH:
 
-#### Sending a key event to an Android device
-
-The `driver.keyevent` method sends a keycode to the device. The keycodes can be
-found in `AndroidKey` class.
-Android only.
-
-```python
-from appium.webdriver.extensions.android.nativekey import AndroidKey
-# sending 'Home' key event
-driver.press_keycode(AndroidKey.HOME)
+```bash
+export "PATH=/venv/root/folder/bin:$PATH"
 ```
 
+### Linting And Formatting
 
-#### Hiding the keyboard in iOS
-
-To hide the keyboard from view in iOS, use `driver.hide_keyboard`. If a key name
-is sent, the keyboard key with that name will be pressed. If no arguments are
-passed in, the keyboard will be hidden by tapping on the screen outside the text
-field, thus removing focus from it.
-
-```python
-# get focus on text field, so keyboard comes up
-el = driver.find_element_by_class_name('android.widget.TextView')
-el.set_value('Testing')
-
-el = driver.find_element_by_class_name('keyboard')
-assertTrue(el.is_displayed())
+Run linter and format checks
 
-driver.hide_keyboard('Done')
-
-assertFalse(el.is_displayed())
+```bash
+make check
 ```
 
-```python
-# get focus on text field, so keyboard comes up
-el = driver.find_element_by_class_name('android.widget.TextView')
-el.set_value('Testing')
-
-el = driver.find_element_by__name('keyboard')
-assertTrue(el.is_displayed())
-
-driver.hide_keyboard()
+Address autofixable linter and formatting issues
 
-assertFalse(el.is_displayed())
+```bash
+make fix
 ```
 
+### Testing
 
-#### Retrieving the current running package and activity
-
-The property method `driver.current_package` returns the name of the current
-package running on the device.
+#### Unit
 
-```python
-package = driver.current_package
-assertEquals('com.example.android.apis', package)
-```
-
-The property method `driver.current_activity` returns the name of the current
-activity running on the device.
-
-```python
-activity = driver.current_activity
-assertEquals('.ApiDemos', activity)
+```bash
+make unittest
 ```
 
+Run in parallel (2 threads)
 
-#### Set a value directly on an element
-
-Sometimes one needs to directly set the value of an element on the device. To do
-this, the method `driver.set_value` or `element.set_value` is invoked.
-
-```python
-el = driver.find_element_by_class_name('android.widget.EditText')
-driver.set_value(el, 'Testing')
-
-text = el.get_attribute('text')
-assertEqual('Testing', text)
-
-el.set_value('More testing')
-text = el.get_attribute('text')
-assertEqual('More testing', text)
+```bash
+make unittest ARGS="-n 2"
 ```
 
+#### Functional
 
-#### Retrieve a file from the device
-
-To retrieve the contents of a file from the device, use `driver.pull_file`, which
-returns the contents of the specified file encoded in [Base64](https://docs.python.org/2/library/base64.html).
-
-```python
-# pulling the strings file for our application
-data = driver.pull_file('data/local/tmp/strings.json')
-strings = json.loads(data.decode('base64', 'strict'))
-assertEqual('You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data'])
+```bash
+uv run pytest test/functional/ios/search_context/find_by_ios_class_chain_tests.py
 ```
 
+#### In parallel for iOS
 
-#### Place a file on the device
+1. Create simulators named 'iPhone X - 8100' and 'iPhone X - 8101'
+1. Run tests
 
-To put a file onto the device at a particular place, use the `driver.push_file`
-method, which takes the path and the data, encoded as [Base64](https://docs.python.org/2/library/base64.html), to be written to the file.
-
-```python
-path = 'data/local/tmp/test_push_file.txt'
-data = 'This is the contents of the file to push to the device.'
-driver.push_file(path, data.encode('base64'))
-data_ret = driver.pull_file('data/local/tmp/test_push_file.txt').decode('base64')
-self.assertEqual(data, data_ret)
+```bash
+uv run pytest -n 2 test/functional/ios/search_context/find_by_ios_class_chain_tests.py
 ```
 
+## Release
 
-#### End test coverage
+Follow the below steps.
 
-There is functionality in the Android emulator to instrument certain activities.
-For information on this, see the [Appium docs](https://github.com/appium/appium/blob/master/docs/en/android_coverage.md). To end this coverage
-and retrieve the data, use `driver.end_test_coverage`, passing in the `intent`
-that is being instrumentalized, and the path to the `coverage.ec` file on the
-device.
-
-```python
-coverage_ec_file = driver.end_test_coverage(intent='android.intent.action.MAIN', path='')
+```bash
+uv pip install setuptools
+uv pip install twine
+uv pip install gitchangelog
+# Type the new version number and 'yes' if you can publish it
+# You can test the command with DRY_RUN
+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 <name> -p <pass> dist/Appium-Python-Client-4.1.0.tar.gz`.
 
-#### Lock the device
-
-To lock the device for a certain amount of time, on iOS, use `driver.lock`. The
-argument is the number of seconds to wait before unlocking.
-
+## License
 
-#### Shake the device
-
-To shake the device, use `driver.shake`.
-
-
-#### Appium Settings
-
-Settings are a new concept introduced by appium. They are currently not a part of the Mobile JSON Wire Protocol, or the Webdriver spec.
-
-Settings are a way to specify the behavior of the appium server.
-
-Settings are:
-
-Mutable, they can be changed during a session
-Only relevant during the session they are applied. They are reset for each new session.
-Control the way the appium server behaves during test automation. They do not apply to controlling the app or device under test.
-
-See [the docs](https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md) for more information.
-
-To get settings:
-```python
-settings = driver.get_settings()
-```
-
-To set settings:
-```python
-driver.update_settings({"some setting": "the value"})
-```
+Apache License v2
diff --git a/appium/__init__.py b/appium/__init__.py
index 2e890a900..4f33bddc3 100644
--- a/appium/__init__.py
+++ b/appium/__init__.py
@@ -15,9 +15,3 @@
 """
 Appium Python Client
 """
-try:
-    from .saucetestcase import SauceTestCase
-    from .saucetestcase import on_platforms
-except ImportError:
-    # SauceClient not found
-    pass
diff --git a/appium/common/exceptions.py b/appium/common/exceptions.py
index 674059bac..3fe1712e8 100644
--- a/appium/common/exceptions.py
+++ b/appium/common/exceptions.py
@@ -21,7 +21,6 @@ class NoSuchContextException(InvalidSwitchToTargetException):
     To find the current set of active contexts, you can get a list
     of the active contexts in the following way:
 
-        print driver.contexts
+        print(driver.contexts)
 
     """
-    pass
diff --git a/appium/common/helper.py b/appium/common/helper.py
index b694b9fae..1565b96e9 100644
--- a/appium/common/helper.py
+++ b/appium/common/helper.py
@@ -12,45 +12,31 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from collections import OrderedDict
+import base64
+from typing import Any, Dict
 
 from appium import version as appium_version
 
 
-def appium_bytes(value, encoding):
-    """Return a bytes-like object
-
-    Has _appium_ prefix to avoid overriding built-in bytes.
-
-    Args:
-        value (str): A value to convert
-        encoding (str): A encoding which will convert to
-
-    Returns:
-        str: A bytes-like object
-    """
-
-    try:
-        return bytes(value, encoding)  # Python 3
-    except TypeError:
-        return value  # Python 2
-
-
-def extract_const_attributes(cls):
+def extract_const_attributes(cls: type) -> Dict[str, Any]:
     """Return dict with constants attributes and values in the class(e.g. {'VAL1': 1, 'VAL2': 2})
 
     Args:
-        cls (type): Class to be extracted constants
+        cls: Class to be extracted constants
 
     Returns:
-        OrderedDict: dict with constants attributes and values in the class
+        dict with constants attributes and values in the class
     """
-    return OrderedDict(
-        [(attr, value) for attr, value in vars(cls).items() if not callable(getattr(cls, attr)) and attr.isupper()])
+    return {attr: value for attr, value in vars(cls).items() if not callable(getattr(cls, attr)) and attr.isupper()}
 
 
-def library_version():
-    """Return a version of this python library
-    """
+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/common/logger.py b/appium/common/logger.py
index 372d7fa80..014b69e41 100644
--- a/appium/common/logger.py
+++ b/appium/common/logger.py
@@ -16,7 +16,7 @@
 import sys
 
 
-def setup_logger(level=logging.NOTSET):
+def setup_logger(level: int = logging.NOTSET) -> None:
     logger.propagate = False
     logger.setLevel(level)
     handler = logging.StreamHandler(stream=sys.stderr)
diff --git a/test/functional/android/search_context/__init__.py b/appium/options/__init__.py
similarity index 100%
rename from test/functional/android/search_context/__init__.py
rename to appium/options/__init__.py
diff --git a/appium/options/android/__init__.py b/appium/options/android/__init__.py
new file mode 100644
index 000000000..8926c5a87
--- /dev/null
+++ b/appium/options/android/__init__.py
@@ -0,0 +1,2 @@
+from .espresso.base import EspressoOptions
+from .uiautomator2.base import UiAutomator2Options
diff --git a/test/functional/ios/search_context/__init__.py b/appium/options/android/common/__init__.py
similarity index 100%
rename from test/functional/ios/search_context/__init__.py
rename to appium/options/android/common/__init__.py
diff --git a/appium/options/android/common/adb/__init__.py b/appium/options/android/common/adb/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/adb/adb_exec_timeout_option.py b/appium/options/android/common/adb/adb_exec_timeout_option.py
new file mode 100644
index 000000000..f0ea5b802
--- /dev/null
+++ b/appium/options/android/common/adb/adb_exec_timeout_option.py
@@ -0,0 +1,41 @@
+# 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
+
+ADB_EXEC_TIMEOUT = 'adbExecTimeout'
+
+
+class AdbExecTimeoutOption(SupportsCapabilities):
+    @property
+    def adb_exec_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum time to wait until single ADB command is executed.
+        """
+        value = self.get_capability(ADB_EXEC_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @adb_exec_timeout.setter
+    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)
diff --git a/appium/options/android/common/adb/adb_port_option.py b/appium/options/android/common/adb/adb_port_option.py
new file mode 100644
index 000000000..4685c7a0f
--- /dev/null
+++ b/appium/options/android/common/adb/adb_port_option.py
@@ -0,0 +1,38 @@
+# 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
+
+ADB_PORT = 'adbPort'
+
+
+class AdbPortOption(SupportsCapabilities):
+    @property
+    def adb_port(self) -> Optional[int]:
+        """
+        Number of the port where ADB is running.
+        """
+        return self.get_capability(ADB_PORT)
+
+    @adb_port.setter
+    def adb_port(self, value: int) -> None:
+        """
+        Set number of the port where ADB is running. 5037 by default
+        """
+        self.set_capability(ADB_PORT, value)
diff --git a/appium/options/android/common/adb/allow_delay_adb_option.py b/appium/options/android/common/adb/allow_delay_adb_option.py
new file mode 100644
index 000000000..cafd6ff3c
--- /dev/null
+++ b/appium/options/android/common/adb/allow_delay_adb_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ALLOW_DELAY_ADB = 'allowDelayAdb'
+
+
+class AllowDelayAdbOption(SupportsCapabilities):
+    @property
+    def allow_delay_adb(self) -> Optional[bool]:
+        """
+        Whether to prevent the emulator to use -delay-adb feature.
+        """
+        return self.get_capability(ALLOW_DELAY_ADB)
+
+    @allow_delay_adb.setter
+    def allow_delay_adb(self, value: bool) -> None:
+        """
+        Being set to false prevents emulator to use -delay-adb feature to detect its startup.
+        See https://github.com/appium/appium/issues/14773 for more details.
+        """
+        self.set_capability(ALLOW_DELAY_ADB, value)
diff --git a/appium/options/android/common/adb/build_tools_version_option.py b/appium/options/android/common/adb/build_tools_version_option.py
new file mode 100644
index 000000000..a5cd96c71
--- /dev/null
+++ b/appium/options/android/common/adb/build_tools_version_option.py
@@ -0,0 +1,42 @@
+# 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
+
+BUILD_TOOLS_VERSION = 'buildToolsVersion'
+
+
+class BuildToolsVersionOption(SupportsCapabilities):
+    @property
+    def build_tools_version(self) -> Optional[str]:
+        """
+        Version of Android build tools to use.
+        """
+        return self.get_capability(BUILD_TOOLS_VERSION)
+
+    @build_tools_version.setter
+    def build_tools_version(self, value: str) -> None:
+        """
+        The version of Android build tools to use. By default, UiAutomator2
+        driver uses the most recent version of build tools installed on
+        the machine, but sometimes it might be necessary to give it a hint
+        (let say if there is a known bug in the most recent tools version).
+        Example: 28.0.3
+        """
+        self.set_capability(BUILD_TOOLS_VERSION, value)
diff --git a/appium/options/android/common/adb/clear_device_logs_on_start_option.py b/appium/options/android/common/adb/clear_device_logs_on_start_option.py
new file mode 100644
index 000000000..050cc6a7d
--- /dev/null
+++ b/appium/options/android/common/adb/clear_device_logs_on_start_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CLEAR_DEVICE_LOGS_ON_START = 'clearDeviceLogsOnStart'
+
+
+class ClearDeviceLogsOnStartOption(SupportsCapabilities):
+    @property
+    def clear_device_logs_on_start(self) -> Optional[bool]:
+        """
+        Makes the driver to delete all the existing logs in the
+        device buffer before starting a new test.
+        """
+        return self.get_capability(CLEAR_DEVICE_LOGS_ON_START)
+
+    @clear_device_logs_on_start.setter
+    def clear_device_logs_on_start(self, value: bool) -> None:
+        """
+        If set to true then the driver deletes all the existing logs in the
+        device buffer before starting a new test.
+        """
+        self.set_capability(CLEAR_DEVICE_LOGS_ON_START, value)
diff --git a/appium/options/android/common/adb/ignore_hidden_api_policy_error_option.py b/appium/options/android/common/adb/ignore_hidden_api_policy_error_option.py
new file mode 100644
index 000000000..4813ed246
--- /dev/null
+++ b/appium/options/android/common/adb/ignore_hidden_api_policy_error_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+IGNORE_HIDDEN_API_POLICY_ERROR = 'ignoreHiddenApiPolicyError'
+
+
+class IgnoreHiddenApiPolicyErrorOption(SupportsCapabilities):
+    @property
+    def ignore_hidden_api_policy_error(self) -> Optional[bool]:
+        """
+        Whether to ignore a failure while changing hidden API access policies.
+        """
+        return self.get_capability(IGNORE_HIDDEN_API_POLICY_ERROR)
+
+    @ignore_hidden_api_policy_error.setter
+    def ignore_hidden_api_policy_error(self, value: bool) -> None:
+        """
+        Being set to true ignores a failure while changing hidden API access policies.
+        Could be useful on some devices, where access to these policies has been locked by its vendor.
+        false by default.
+        """
+        self.set_capability(IGNORE_HIDDEN_API_POLICY_ERROR, value)
diff --git a/appium/options/android/common/adb/logcat_filter_specs_option.py b/appium/options/android/common/adb/logcat_filter_specs_option.py
new file mode 100644
index 000000000..8a464d350
--- /dev/null
+++ b/appium/options/android/common/adb/logcat_filter_specs_option.py
@@ -0,0 +1,42 @@
+# 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
+
+LOGCAT_FILTER_SPECS = 'logcatFilterSpecs'
+
+
+class LogcatFilterSpecsOption(SupportsCapabilities):
+    @property
+    def logcat_filter_specs(self) -> Optional[str]:
+        """
+        Logcat filter format.
+        """
+        return self.get_capability(LOGCAT_FILTER_SPECS)
+
+    @logcat_filter_specs.setter
+    def logcat_filter_specs(self, value: str) -> None:
+        """
+        Series of tag[:priority] where tag is a log component tag (or * for all)
+        and priority is: V Verbose, D Debug, I Info, W Warn, E Error, F Fatal,
+        S Silent (suppress all output). '' means ':d' and tag by itself means tag:v.
+        If not specified on the commandline, filterspec is set from ANDROID_LOG_TAGS.
+        If no filterspec is found, filter defaults to '*:I'.
+        """
+        self.set_capability(LOGCAT_FILTER_SPECS, value)
diff --git a/appium/options/android/common/adb/logcat_format_option.py b/appium/options/android/common/adb/logcat_format_option.py
new file mode 100644
index 000000000..363a6f5a7
--- /dev/null
+++ b/appium/options/android/common/adb/logcat_format_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+LOGCAT_FORMAT = 'logcatFormat'
+
+
+class LogcatFormatOption(SupportsCapabilities):
+    @property
+    def logcat_format(self) -> Optional[str]:
+        """
+        Log print format.
+        """
+        return self.get_capability(LOGCAT_FORMAT)
+
+    @logcat_format.setter
+    def logcat_format(self, value: str) -> None:
+        """
+        The log print format, where format is one of: brief process tag thread raw time
+        threadtime long. threadtime is the default value.
+        """
+        self.set_capability(LOGCAT_FORMAT, value)
diff --git a/appium/options/android/common/adb/mock_location_app_option.py b/appium/options/android/common/adb/mock_location_app_option.py
new file mode 100644
index 000000000..d263ea0e6
--- /dev/null
+++ b/appium/options/android/common/adb/mock_location_app_option.py
@@ -0,0 +1,42 @@
+# 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
+
+MOCK_LOCATION_APP = 'mockLocationApp'
+
+
+class MockLocationAppOption(SupportsCapabilities):
+    @property
+    def mock_location_app(self) -> Optional[str]:
+        """
+        Identifier of the app, which is used as a system mock location provider.
+        """
+        return self.get_capability(MOCK_LOCATION_APP)
+
+    @mock_location_app.setter
+    def mock_location_app(self, value: str) -> None:
+        """
+        Sets the package identifier of the app, which is used as a system mock location
+        provider since Appium 1.18.0+. This capability has no effect on emulators.
+        If the value is set to null or an empty string, then Appium will skip the mocked
+        location provider setup procedure. Defaults to Appium Setting package
+        identifier (io.appium.settings).
+        """
+        self.set_capability(MOCK_LOCATION_APP, value)
diff --git a/appium/options/android/common/adb/remote_adb_host_option.py b/appium/options/android/common/adb/remote_adb_host_option.py
new file mode 100644
index 000000000..1656e9bd5
--- /dev/null
+++ b/appium/options/android/common/adb/remote_adb_host_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+REMOTE_ADB_HOST = 'remoteAdbHost'
+
+
+class RemoteAdbHostOption(SupportsCapabilities):
+    @property
+    def remote_adb_host(self) -> Optional[str]:
+        """
+        Address of the host where ADB is running.
+        """
+        return self.get_capability(REMOTE_ADB_HOST)
+
+    @remote_adb_host.setter
+    def remote_adb_host(self, value: str) -> None:
+        """
+        Address of the host where ADB is running (the value of -H ADB command line option).
+        Localhost by default.
+        """
+        self.set_capability(REMOTE_ADB_HOST, value)
diff --git a/appium/options/android/common/adb/skip_logcat_capture_option.py b/appium/options/android/common/adb/skip_logcat_capture_option.py
new file mode 100644
index 000000000..2f9f9074f
--- /dev/null
+++ b/appium/options/android/common/adb/skip_logcat_capture_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SKIP_LOGCAT_CAPTURE = 'skipLogcatCapture'
+
+
+class SkipLogcatCaptureOption(SupportsCapabilities):
+    @property
+    def skip_logcat_capture(self) -> Optional[bool]:
+        """
+        Whether to delete all the existing logs in the
+        device buffer before starting a new test.
+        """
+        return self.get_capability(SKIP_LOGCAT_CAPTURE)
+
+    @skip_logcat_capture.setter
+    def skip_logcat_capture(self, value: bool) -> None:
+        """
+        Being set to true disables automatic logcat output collection during the test run.
+        false by default
+        """
+        self.set_capability(SKIP_LOGCAT_CAPTURE, value)
diff --git a/appium/options/android/common/adb/suppress_kill_server_option.py b/appium/options/android/common/adb/suppress_kill_server_option.py
new file mode 100644
index 000000000..fb7738994
--- /dev/null
+++ b/appium/options/android/common/adb/suppress_kill_server_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SUPPRESS_KILL_SERVER = 'suppressKillServer'
+
+
+class SuppressKillServerOption(SupportsCapabilities):
+    @property
+    def suppress_kill_server(self) -> Optional[bool]:
+        """
+        Prevents the driver from ever killing the ADB server explicitly.
+        """
+        return self.get_capability(SUPPRESS_KILL_SERVER)
+
+    @suppress_kill_server.setter
+    def suppress_kill_server(self, value: bool) -> None:
+        """
+        Being set to true prevents the driver from ever killing the ADB server explicitly.
+        Could be useful if ADB is connected wirelessly. false by default.
+        """
+        self.set_capability(SUPPRESS_KILL_SERVER, value)
diff --git a/appium/options/android/common/app/__init__.py b/appium/options/android/common/app/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/app/allow_test_packages_option.py b/appium/options/android/common/app/allow_test_packages_option.py
new file mode 100644
index 000000000..b09136932
--- /dev/null
+++ b/appium/options/android/common/app/allow_test_packages_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ALLOW_TEST_PACKAGES = 'allowTestPackages'
+
+
+class AllowTestPackagesOption(SupportsCapabilities):
+    @property
+    def allow_test_packages(self) -> Optional[bool]:
+        """
+        Whether it is possible to use packages built with the test flag for
+        the automated testing (literally adds -t flag to the adb install command).
+        """
+        return self.get_capability(ALLOW_TEST_PACKAGES)
+
+    @allow_test_packages.setter
+    def allow_test_packages(self, value: bool) -> None:
+        """
+        If set to true then it would be possible to use packages built with the test flag for
+        the automated testing (literally adds -t flag to the adb install command). false by default.
+        """
+        self.set_capability(ALLOW_TEST_PACKAGES, value)
diff --git a/appium/options/android/common/app/android_install_timeout_option.py b/appium/options/android/common/app/android_install_timeout_option.py
new file mode 100644
index 000000000..38a997648
--- /dev/null
+++ b/appium/options/android/common/app/android_install_timeout_option.py
@@ -0,0 +1,43 @@
+# 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
+
+ANDROID_INSTALL_TIMEOUT = 'androidInstallTimeout'
+
+
+class AndroidInstallTimeoutOption(SupportsCapabilities):
+    @property
+    def android_install_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum amount of time to wait until the application under test is installed.
+        """
+        value = self.get_capability(ANDROID_INSTALL_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @android_install_timeout.setter
+    def android_install_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Maximum amount of time to wait until the application under test is installed.
+        90000 ms by default
+        """
+        self.set_capability(
+            ANDROID_INSTALL_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/android/common/app/app_activity_option.py b/appium/options/android/common/app/app_activity_option.py
new file mode 100644
index 000000000..41f66eed8
--- /dev/null
+++ b/appium/options/android/common/app/app_activity_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_ACTIVITY = 'appActivity'
+
+
+class AppActivityOption(SupportsCapabilities):
+    @property
+    def app_activity(self) -> Optional[str]:
+        """
+        Name of the main app activity.
+        """
+        return self.get_capability(APP_ACTIVITY)
+
+    @app_activity.setter
+    def app_activity(self, value: str) -> None:
+        """
+        Main application activity identifier. If not provided then the driver
+        will try to detect it automatically from the package provided by the app capability.
+        """
+        self.set_capability(APP_ACTIVITY, value)
diff --git a/appium/options/android/common/app/app_package_option.py b/appium/options/android/common/app/app_package_option.py
new file mode 100644
index 000000000..a8c7b065c
--- /dev/null
+++ b/appium/options/android/common/app/app_package_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_PACKAGE = 'appPackage'
+
+
+class AppPackageOption(SupportsCapabilities):
+    @property
+    def app_package(self) -> Optional[str]:
+        """
+        App package identifier.
+        """
+        return self.get_capability(APP_PACKAGE)
+
+    @app_package.setter
+    def app_package(self, value: str) -> None:
+        """
+        Application package identifier to be started. If not provided then the driver will
+        try to detect it automatically from the package provided by the app capability.
+        """
+        self.set_capability(APP_PACKAGE, value)
diff --git a/appium/options/android/common/app/app_wait_activity_option.py b/appium/options/android/common/app/app_wait_activity_option.py
new file mode 100644
index 000000000..5d72f311d
--- /dev/null
+++ b/appium/options/android/common/app/app_wait_activity_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_WAIT_ACTIVITY = 'appWaitActivity'
+
+
+class AppWaitActivityOption(SupportsCapabilities):
+    @property
+    def app_wait_activity(self) -> Optional[str]:
+        """
+        Name of the app activity to wait for.
+        """
+        return self.get_capability(APP_WAIT_ACTIVITY)
+
+    @app_wait_activity.setter
+    def app_wait_activity(self, value: str) -> None:
+        """
+        Identifier of the activity that the driver should wait for
+        (not necessarily the main one).
+        If not provided then defaults to appium:appActivity.
+        """
+        self.set_capability(APP_WAIT_ACTIVITY, 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
new file mode 100644
index 000000000..362cb3725
--- /dev/null
+++ b/appium/options/android/common/app/app_wait_duration_option.py
@@ -0,0 +1,41 @@
+# 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
+
+APP_WAIT_DURATION = 'appWaitDuration'
+
+
+class AppWaitDurationOption(SupportsCapabilities):
+    @property
+    def app_wait_duration(self) -> Optional[timedelta]:
+        """
+        Identifier of the app package to wait for.
+        """
+        value = self.get_capability(APP_WAIT_DURATION)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @app_wait_duration.setter
+    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)
diff --git a/appium/options/android/common/app/app_wait_for_launch_option.py b/appium/options/android/common/app/app_wait_for_launch_option.py
new file mode 100644
index 000000000..5f5b48c51
--- /dev/null
+++ b/appium/options/android/common/app/app_wait_for_launch_option.py
@@ -0,0 +1,41 @@
+# 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
+
+APP_WAIT_FOR_LAUNCH = 'appWaitForLaunch'
+
+
+class AppWaitForLaunchOption(SupportsCapabilities):
+    @property
+    def app_wait_for_launch(self) -> Optional[bool]:
+        """
+        Whether to block until the app under test returns the control to the
+        caller after its activity has been started by Activity Manager.
+        """
+        return self.get_capability(APP_WAIT_FOR_LAUNCH)
+
+    @app_wait_for_launch.setter
+    def app_wait_for_launch(self, value: bool) -> None:
+        """
+        Whether to block until the app under test returns the control to the
+        caller after its activity has been started by Activity Manager
+        (true, the default value) or to continue the test without waiting for that (false).
+        """
+        self.set_capability(APP_WAIT_FOR_LAUNCH, value)
diff --git a/appium/options/android/common/app/app_wait_package_option.py b/appium/options/android/common/app/app_wait_package_option.py
new file mode 100644
index 000000000..9df4fa0d0
--- /dev/null
+++ b/appium/options/android/common/app/app_wait_package_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_WAIT_PACKAGE = 'appWaitPackage'
+
+
+class AppWaitPackageOption(SupportsCapabilities):
+    @property
+    def app_wait_package(self) -> Optional[str]:
+        """
+        Identifier of the app package to wait for.
+        """
+        return self.get_capability(APP_WAIT_PACKAGE)
+
+    @app_wait_package.setter
+    def app_wait_package(self, value: str) -> None:
+        """
+        Identifier of the package that the driver should wait for
+        (not necessarily the main one).
+        If not provided then defaults to appium:appPackage.
+        """
+        self.set_capability(APP_WAIT_PACKAGE, value)
diff --git a/appium/options/android/common/app/auto_grant_premissions_option.py b/appium/options/android/common/app/auto_grant_premissions_option.py
new file mode 100644
index 000000000..e276e7da4
--- /dev/null
+++ b/appium/options/android/common/app/auto_grant_premissions_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+AUTO_GRANT_PERMISSIONS = 'autoGrantPermissions'
+
+
+class AutoGrantPermissionsOption(SupportsCapabilities):
+    @property
+    def auto_grant_permissions(self) -> Optional[bool]:
+        """
+        Whether to grant all the requested application permissions
+        automatically when a test starts.
+        """
+        return self.get_capability(AUTO_GRANT_PERMISSIONS)
+
+    @auto_grant_permissions.setter
+    def auto_grant_permissions(self, value: bool) -> None:
+        """
+        Whether to grant all the requested application permissions
+        automatically when a test starts(true). false by default.
+        """
+        self.set_capability(AUTO_GRANT_PERMISSIONS, value)
diff --git a/appium/options/android/common/app/enforce_app_install_option.py b/appium/options/android/common/app/enforce_app_install_option.py
new file mode 100644
index 000000000..85d128ed7
--- /dev/null
+++ b/appium/options/android/common/app/enforce_app_install_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ENFORCE_APP_INSTALL = 'enforceAppInstall'
+
+
+class EnforceAppInstallOption(SupportsCapabilities):
+    @property
+    def enforce_app_install(self) -> Optional[bool]:
+        """
+        Whether the application under test is always reinstalled even
+        if a newer version of it already exists on the device under test.
+        """
+        return self.get_capability(ENFORCE_APP_INSTALL)
+
+    @enforce_app_install.setter
+    def enforce_app_install(self, value: bool) -> None:
+        """
+        Allows setting whether the application under test is always reinstalled even
+        if a newer version of it already exists on the device under test. false by default.
+        """
+        self.set_capability(ENFORCE_APP_INSTALL, value)
diff --git a/appium/options/android/common/app/intent_action_option.py b/appium/options/android/common/app/intent_action_option.py
new file mode 100644
index 000000000..8019bcc54
--- /dev/null
+++ b/appium/options/android/common/app/intent_action_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+INTENT_ACTION = 'intentAction'
+
+
+class IntentActionOption(SupportsCapabilities):
+    @property
+    def intent_action(self) -> Optional[str]:
+        """
+        Intent action to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        return self.get_capability(INTENT_ACTION)
+
+    @intent_action.setter
+    def intent_action(self, value: str) -> None:
+        """
+        Set an optional intent action to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        self.set_capability(INTENT_ACTION, value)
diff --git a/appium/options/android/common/app/intent_category_option.py b/appium/options/android/common/app/intent_category_option.py
new file mode 100644
index 000000000..f6f568c20
--- /dev/null
+++ b/appium/options/android/common/app/intent_category_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+INTENT_CATEGORY = 'intentCategory'
+
+
+class IntentCategoryOption(SupportsCapabilities):
+    @property
+    def intent_category(self) -> Optional[str]:
+        """
+        Intent category to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        return self.get_capability(INTENT_CATEGORY)
+
+    @intent_category.setter
+    def intent_category(self, value: str) -> None:
+        """
+        Set an optional intent category to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        self.set_capability(INTENT_CATEGORY, value)
diff --git a/appium/options/android/common/app/intent_flags_option.py b/appium/options/android/common/app/intent_flags_option.py
new file mode 100644
index 000000000..141e51c30
--- /dev/null
+++ b/appium/options/android/common/app/intent_flags_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+INTENT_FLAGS = 'intentFlags'
+
+
+class IntentFlagsOption(SupportsCapabilities):
+    @property
+    def intent_flags(self) -> Optional[str]:
+        """
+        Intent flags to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        return self.get_capability(INTENT_FLAGS)
+
+    @intent_flags.setter
+    def intent_flags(self, value: str) -> None:
+        """
+        Set optional intent flags to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        self.set_capability(INTENT_FLAGS, value)
diff --git a/appium/options/android/common/app/optional_intent_arguments_option.py b/appium/options/android/common/app/optional_intent_arguments_option.py
new file mode 100644
index 000000000..3411fe96c
--- /dev/null
+++ b/appium/options/android/common/app/optional_intent_arguments_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+OPTIONAL_INTENT_ARGUMENTS = 'optionalIntentArguments'
+
+
+class OptionalIntentArgumentsOption(SupportsCapabilities):
+    @property
+    def optional_intent_arguments(self) -> Optional[str]:
+        """
+        Intent arguments to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        return self.get_capability(OPTIONAL_INTENT_ARGUMENTS)
+
+    @optional_intent_arguments.setter
+    def optional_intent_arguments(self, value: str) -> None:
+        """
+        Set optional intent arguments to be applied when
+        starting the given appActivity by Activity Manager.
+        """
+        self.set_capability(OPTIONAL_INTENT_ARGUMENTS, value)
diff --git a/appium/options/android/common/app/remote_apps_cache_limit_option.py b/appium/options/android/common/app/remote_apps_cache_limit_option.py
new file mode 100644
index 000000000..0f21ec782
--- /dev/null
+++ b/appium/options/android/common/app/remote_apps_cache_limit_option.py
@@ -0,0 +1,42 @@
+# 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
+
+REMOTE_APPS_CACHE_LIMIT = 'remoteAppsCacheLimit'
+
+
+class RemoteAppsCacheLimitOption(SupportsCapabilities):
+    @property
+    def remote_apps_cache_limit(self) -> Optional[int]:
+        """
+        Maximum amount of apps that could be cached on the remote device.
+        """
+        return self.get_capability(REMOTE_APPS_CACHE_LIMIT)
+
+    @remote_apps_cache_limit.setter
+    def remote_apps_cache_limit(self, value: int) -> None:
+        """
+        Set the maximum amount of application packages to be cached on the device under test.
+        This is needed for devices that don't support streamed installs (Android 7 and below),
+        because ADB must push app packages to the device first in order to install them,
+        which takes some time. Setting this capability to zero disables apps caching.
+        10 by default.
+        """
+        self.set_capability(REMOTE_APPS_CACHE_LIMIT, value)
diff --git a/appium/options/android/common/app/uninstall_other_packages_option.py b/appium/options/android/common/app/uninstall_other_packages_option.py
new file mode 100644
index 000000000..2cd05cdf5
--- /dev/null
+++ b/appium/options/android/common/app/uninstall_other_packages_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+UNINSTALL_OTHER_PACKAGES = 'uninstallOtherPackages'
+
+
+class UninstallOtherPackagesOption(SupportsCapabilities):
+    @property
+    def uninstall_other_packages(self) -> Optional[str]:
+        """
+        Identifiers of packages to be uninstalled from the device before a test starts.
+        """
+        return self.get_capability(UNINSTALL_OTHER_PACKAGES)
+
+    @uninstall_other_packages.setter
+    def uninstall_other_packages(self, value: str) -> None:
+        """
+        Allows to set one or more comma-separated package
+        identifiers to be uninstalled from the device before a test starts.
+        """
+        self.set_capability(UNINSTALL_OTHER_PACKAGES, value)
diff --git a/appium/options/android/common/avd/__init__.py b/appium/options/android/common/avd/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/avd/avd_args_option.py b/appium/options/android/common/avd/avd_args_option.py
new file mode 100644
index 000000000..a8b7b7506
--- /dev/null
+++ b/appium/options/android/common/avd/avd_args_option.py
@@ -0,0 +1,38 @@
+# 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
+
+AVD_ARGS = 'avdArgs'
+
+
+class AvdArgsOption(SupportsCapabilities):
+    @property
+    def avd_args(self) -> Optional[str]:
+        """
+        Emulator command line arguments.
+        """
+        return self.get_capability(AVD_ARGS)
+
+    @avd_args.setter
+    def avd_args(self, value: str) -> None:
+        """
+        Set emulator command line arguments.
+        """
+        self.set_capability(AVD_ARGS, value)
diff --git a/appium/options/android/common/avd/avd_env_option.py b/appium/options/android/common/avd/avd_env_option.py
new file mode 100644
index 000000000..a673a95fd
--- /dev/null
+++ b/appium/options/android/common/avd/avd_env_option.py
@@ -0,0 +1,38 @@
+# 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, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+AVD_ENV = 'avdEnv'
+
+
+class AvdEnvOption(SupportsCapabilities):
+    @property
+    def avd_env(self) -> Optional[Dict[str, str]]:
+        """
+        Mapping of emulator environment variables.
+        """
+        return self.get_capability(AVD_ENV)
+
+    @avd_env.setter
+    def avd_env(self, value: Dict[str, str]) -> None:
+        """
+        Set the mapping of emulator environment variables.
+        """
+        self.set_capability(AVD_ENV, 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
new file mode 100644
index 000000000..017396ef0
--- /dev/null
+++ b/appium/options/android/common/avd/avd_launch_timeout_option.py
@@ -0,0 +1,41 @@
+# 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
+
+AVD_LAUNCH_TIMEOUT = 'avdLaunchTimeout'
+
+
+class AvdLaunchTimeoutOption(SupportsCapabilities):
+    @property
+    def avd_launch_timeout(self) -> Optional[timedelta]:
+        """
+        Timeout to wait until Android Emulator is started.
+        """
+        value = self.get_capability(AVD_LAUNCH_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @avd_launch_timeout.setter
+    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)
diff --git a/appium/options/android/common/avd/avd_option.py b/appium/options/android/common/avd/avd_option.py
new file mode 100644
index 000000000..42c543fc8
--- /dev/null
+++ b/appium/options/android/common/avd/avd_option.py
@@ -0,0 +1,41 @@
+# 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
+
+AVD = 'avd'
+
+
+class AvdOption(SupportsCapabilities):
+    @property
+    def avd(self) -> Optional[str]:
+        """
+        Name of Android emulator to run the test on.
+        """
+        return self.get_capability(AVD)
+
+    @avd.setter
+    def avd(self, value: str) -> None:
+        """
+        The name of Android emulator to run the test on.
+        Names of currently installed emulators could be listed using
+        avdmanager list avd command. If the emulator with the given name
+        is not running then it is going to be started before a test.
+        """
+        self.set_capability(AVD, 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
new file mode 100644
index 000000000..c074b3190
--- /dev/null
+++ b/appium/options/android/common/avd/avd_ready_timeout_option.py
@@ -0,0 +1,41 @@
+# 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
+
+AVD_READY_TIMEOUT = 'avdReadyTimeout'
+
+
+class AvdReadyTimeoutOption(SupportsCapabilities):
+    @property
+    def avd_ready_timeout(self) -> Optional[timedelta]:
+        """
+        Timeout to wait until Android Emulator is fully booted and is ready for usage.
+        """
+        value = self.get_capability(AVD_READY_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @avd_ready_timeout.setter
+    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)
diff --git a/appium/options/android/common/avd/gps_enabled_option.py b/appium/options/android/common/avd/gps_enabled_option.py
new file mode 100644
index 000000000..c9b3c60b1
--- /dev/null
+++ b/appium/options/android/common/avd/gps_enabled_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+GPS_ENABLED = 'gpsEnabled'
+
+
+class GpsEnabledOption(SupportsCapabilities):
+    @property
+    def gps_enabled(self) -> Optional[bool]:
+        """
+        State of the GPS service on emulator.
+        """
+        return self.get_capability(GPS_ENABLED)
+
+    @gps_enabled.setter
+    def gps_enabled(self, value: bool) -> None:
+        """
+        Set whether to enable (true) or disable (false) GPS service in the Emulator.
+        Unset by default, which means to not change the current value.
+        """
+        self.set_capability(GPS_ENABLED, value)
diff --git a/appium/options/android/common/avd/network_speed_option.py b/appium/options/android/common/avd/network_speed_option.py
new file mode 100644
index 000000000..1ab6c1cba
--- /dev/null
+++ b/appium/options/android/common/avd/network_speed_option.py
@@ -0,0 +1,41 @@
+# 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
+
+NETWORK_SPEED = 'networkSpeed'
+
+
+class NetworkSpeedOption(SupportsCapabilities):
+    @property
+    def network_speed(self) -> Optional[str]:
+        """
+        Desired network speed limit for the emulator.
+        """
+        return self.get_capability(NETWORK_SPEED)
+
+    @network_speed.setter
+    def network_speed(self, value: str) -> None:
+        """
+        Sets the desired network speed limit for the emulator.
+        It is only applied if the emulator is not running before
+        the test starts. See emulator command line arguments description
+        for more details.
+        """
+        self.set_capability(NETWORK_SPEED, value)
diff --git a/appium/options/android/common/context/__init__.py b/appium/options/android/common/context/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/context/auto_webview_timeout_option.py b/appium/options/android/common/context/auto_webview_timeout_option.py
new file mode 100644
index 000000000..774574313
--- /dev/null
+++ b/appium/options/android/common/context/auto_webview_timeout_option.py
@@ -0,0 +1,41 @@
+# 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
+
+AUTO_WEBVIEW_TIMEOUT = 'autoWebviewTimeout'
+
+
+class AutoWebviewTimeoutOption(SupportsCapabilities):
+    @property
+    def auto_webview_timeout(self) -> Optional[timedelta]:
+        """
+        Set the maximum timeout to wait until a web view is
+        available if autoWebview capability is set to true. 2000 ms by default.
+        """
+        value = self.get_capability(AUTO_WEBVIEW_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @auto_webview_timeout.setter
+    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)
diff --git a/appium/options/android/common/context/chrome_logging_prefs_option.py b/appium/options/android/common/context/chrome_logging_prefs_option.py
new file mode 100644
index 000000000..0c2b69c4d
--- /dev/null
+++ b/appium/options/android/common/context/chrome_logging_prefs_option.py
@@ -0,0 +1,41 @@
+# 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 Any, Dict, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CHROME_LOGGING_PREFS = 'chromeLoggingPrefs'
+
+
+class ChromeLoggingPrefsOption(SupportsCapabilities):
+    @property
+    def chrome_logging_prefs(self) -> Optional[Dict[str, Any]]:
+        """
+        Chrome logging preferences.
+        """
+        return self.get_capability(CHROME_LOGGING_PREFS)
+
+    @chrome_logging_prefs.setter
+    def chrome_logging_prefs(self, value: Dict[str, Any]) -> None:
+        """
+        Chrome logging preferences mapping. Basically the same as
+        [goog:loggingPrefs](https://newbedev.com/
+        getting-console-log-output-from-chrome-with-selenium-python-api-bindings).
+        It is set to {"browser": "ALL"} by default.
+        """
+        self.set_capability(CHROME_LOGGING_PREFS, value)
diff --git a/appium/options/android/common/context/chrome_options_option.py b/appium/options/android/common/context/chrome_options_option.py
new file mode 100644
index 000000000..3adb9cebb
--- /dev/null
+++ b/appium/options/android/common/context/chrome_options_option.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 Any, Dict, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CHROME_OPTIONS = 'chromeOptions'
+
+
+class ChromeOptionsOption(SupportsCapabilities):
+    @property
+    def chrome_options(self) -> Optional[Dict[str, Any]]:
+        """
+        Chrome options.
+        """
+        return self.get_capability(CHROME_OPTIONS)
+
+    @chrome_options.setter
+    def chrome_options(self, value: Dict[str, Any]) -> None:
+        """
+        A mapping, that allows to customize chromedriver options.
+        See https://chromedriver.chromium.org/capabilities for the list
+        of available entries.
+        """
+        self.set_capability(CHROME_OPTIONS, value)
diff --git a/appium/options/android/common/context/chromedriver_args_option.py b/appium/options/android/common/context/chromedriver_args_option.py
new file mode 100644
index 000000000..b3633b105
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_args_option.py
@@ -0,0 +1,41 @@
+# 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 List, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CHROMEDRIVER_ARGS = 'chromedriverArgs'
+
+
+class ChromedriverArgsOption(SupportsCapabilities):
+    @property
+    def chromedriver_args(self) -> Optional[List[str]]:
+        """
+        Array of chromedriver CLI arguments.
+        """
+        return self.get_capability(CHROMEDRIVER_ARGS)
+
+    @chromedriver_args.setter
+    def chromedriver_args(self, value: List[str]) -> None:
+        """
+        Array of chromedriver [command line
+        arguments](http://www.assertselenium.com/java/list-of-chrome-driver-command-line-arguments/).
+        Note, that not all command line arguments that are available for the desktop
+        browser are also available for the mobile one.
+        """
+        self.set_capability(CHROMEDRIVER_ARGS, value)
diff --git a/appium/options/android/common/context/chromedriver_chrome_mapping_file_option.py b/appium/options/android/common/context/chromedriver_chrome_mapping_file_option.py
new file mode 100644
index 000000000..cc84bed1b
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_chrome_mapping_file_option.py
@@ -0,0 +1,43 @@
+# 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
+
+CHROMEDRIVER_CHROME_MAPPING_FILE = 'chromedriverChromeMappingFile'
+
+
+class ChromedriverChromeMappingFileOption(SupportsCapabilities):
+    @property
+    def chromedriver_chrome_mapping_file(self) -> Optional[str]:
+        """
+        Full path to the chromedrivers mapping file is located.
+        """
+        return self.get_capability(CHROMEDRIVER_CHROME_MAPPING_FILE)
+
+    @chromedriver_chrome_mapping_file.setter
+    def chromedriver_chrome_mapping_file(self, value: str) -> None:
+        """
+        Full path to the chromedrivers mapping file. This file is used to statically
+        map webview/browser versions to the chromedriver versions that are capable
+        of automating them. Read [Automatic Chromedriver Discovery](https://github.com/
+        appium/appium/blob/master/docs/en/writing-running-appium/web/
+        chromedriver.md#automatic-discovery-of-compatible-chromedriver)
+        article for more details.
+        """
+        self.set_capability(CHROMEDRIVER_CHROME_MAPPING_FILE, value)
diff --git a/appium/options/android/common/context/chromedriver_disable_build_check_option.py b/appium/options/android/common/context/chromedriver_disable_build_check_option.py
new file mode 100644
index 000000000..16886ab1c
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_disable_build_check_option.py
@@ -0,0 +1,41 @@
+# 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
+
+CHROMEDRIVER_DISABLE_BUILD_CHECK = 'chromedriverDisableBuildCheck'
+
+
+class ChromedriverDisableBuildCheckOption(SupportsCapabilities):
+    @property
+    def chromedriver_disable_build_check(self) -> Optional[bool]:
+        """
+        Whether to disable the compatibility validation between the current
+        chromedriver and the destination browser/web view.
+        """
+        return self.get_capability(CHROMEDRIVER_DISABLE_BUILD_CHECK)
+
+    @chromedriver_disable_build_check.setter
+    def chromedriver_disable_build_check(self, value: bool) -> None:
+        """
+        Being set to true disables the compatibility validation between the current
+        chromedriver and the destination browser/web view. Use it with care.
+        false by default.
+        """
+        self.set_capability(CHROMEDRIVER_DISABLE_BUILD_CHECK, value)
diff --git a/appium/options/android/common/context/chromedriver_executable_dir_option.py b/appium/options/android/common/context/chromedriver_executable_dir_option.py
new file mode 100644
index 000000000..64e96c98e
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_executable_dir_option.py
@@ -0,0 +1,43 @@
+# 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
+
+CHROMEDRIVER_EXECUTABLE_DIR = 'chromedriverExecutableDir'
+
+
+class ChromedriverExecutableDirOption(SupportsCapabilities):
+    @property
+    def chromedriver_executable_dir(self) -> Optional[str]:
+        """
+        Full path to the folder where chromedriver executables are located.
+        """
+        return self.get_capability(CHROMEDRIVER_EXECUTABLE_DIR)
+
+    @chromedriver_executable_dir.setter
+    def chromedriver_executable_dir(self, value: str) -> None:
+        """
+        Full path to the folder where chromedriver executables are located.
+        This folder is used then to store the downloaded chromedriver executables
+        if automatic download is enabled. Read [Automatic Chromedriver
+        Discovery](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/
+        web/chromedriver.md#automatic-discovery-of-compatible-chromedriver)
+        article for more details.
+        """
+        self.set_capability(CHROMEDRIVER_EXECUTABLE_DIR, value)
diff --git a/appium/options/android/common/context/chromedriver_executable_option.py b/appium/options/android/common/context/chromedriver_executable_option.py
new file mode 100644
index 000000000..79f30ca14
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_executable_option.py
@@ -0,0 +1,38 @@
+# 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
+
+CHROMEDRIVER_EXECUTABLE = 'chromedriverExecutable'
+
+
+class ChromedriverExecutableOption(SupportsCapabilities):
+    @property
+    def chromedriver_executable(self) -> Optional[str]:
+        """
+        Path to the chromedriver executable on the server file system.
+        """
+        return self.get_capability(CHROMEDRIVER_EXECUTABLE)
+
+    @chromedriver_executable.setter
+    def chromedriver_executable(self, value: str) -> None:
+        """
+        Full path to the chromedriver executable on the server file system.
+        """
+        self.set_capability(CHROMEDRIVER_EXECUTABLE, value)
diff --git a/appium/options/android/common/context/chromedriver_port_option.py b/appium/options/android/common/context/chromedriver_port_option.py
new file mode 100644
index 000000000..8a7ef8ab7
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_port_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CHROMEDRIVER_PORT = 'chromedriverPort'
+
+
+class ChromedriverPortOption(SupportsCapabilities):
+    @property
+    def chromedriver_port(self) -> Optional[int]:
+        """
+        Local port number to use for Chromedriver communication.
+        """
+        return self.get_capability(CHROMEDRIVER_PORT)
+
+    @chromedriver_port.setter
+    def chromedriver_port(self, value: int) -> None:
+        """
+        The port number to use for Chromedriver communication.
+        Any free port number is selected by default if unset.
+        """
+        self.set_capability(CHROMEDRIVER_PORT, value)
diff --git a/appium/options/android/common/context/chromedriver_ports_option.py b/appium/options/android/common/context/chromedriver_ports_option.py
new file mode 100644
index 000000000..5e13c987b
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_ports_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 typing import List, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CHROMEDRIVER_PORTS = 'chromedriverPorts'
+
+
+class ChromedriverPortsOption(SupportsCapabilities):
+    @property
+    def chromedriver_ports(self) -> Optional[List[int]]:
+        """
+        Local port numbers to use for Chromedriver communication.
+        """
+        return self.get_capability(CHROMEDRIVER_PORTS)
+
+    @chromedriver_ports.setter
+    def chromedriver_ports(self, value: List[int]) -> None:
+        """
+        Array of possible port numbers to assign for Chromedriver communication.
+        If none of the port in this array is free then a server error is thrown.
+        """
+        self.set_capability(CHROMEDRIVER_PORTS, value)
diff --git a/appium/options/android/common/context/chromedriver_use_system_executable_option.py b/appium/options/android/common/context/chromedriver_use_system_executable_option.py
new file mode 100644
index 000000000..b164ed13f
--- /dev/null
+++ b/appium/options/android/common/context/chromedriver_use_system_executable_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CHROMEDRIVER_USE_SYSTEM_EXECUTABLE = 'chromedriverUseSystemExecutable'
+
+
+class ChromedriverUseSystemExecutableOption(SupportsCapabilities):
+    @property
+    def chromedriver_use_system_executable(self) -> Optional[bool]:
+        """
+        Whether to use the system chromedriver.
+        """
+        return self.get_capability(CHROMEDRIVER_USE_SYSTEM_EXECUTABLE)
+
+    @chromedriver_use_system_executable.setter
+    def chromedriver_use_system_executable(self, value: bool) -> None:
+        """
+        Set it to true in order to enforce the usage of chromedriver, which gets
+        downloaded by Appium automatically upon installation. This driver might not
+        be compatible with the destination browser or a web view. false by default.
+        """
+        self.set_capability(CHROMEDRIVER_USE_SYSTEM_EXECUTABLE, value)
diff --git a/appium/options/android/common/context/ensure_webviews_have_pages_option.py b/appium/options/android/common/context/ensure_webviews_have_pages_option.py
new file mode 100644
index 000000000..d1f549a9f
--- /dev/null
+++ b/appium/options/android/common/context/ensure_webviews_have_pages_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ENSURE_WEBVIEWS_HAVE_PAGES = 'ensureWebviewsHavePages'
+
+
+class EnsureWebviewsHavePagesOption(SupportsCapabilities):
+    @property
+    def ensure_webviews_have_pages(self) -> Optional[bool]:
+        """
+        Whether to ensure if web views have pages.
+        """
+        return self.get_capability(ENSURE_WEBVIEWS_HAVE_PAGES)
+
+    @ensure_webviews_have_pages.setter
+    def ensure_webviews_have_pages(self, value: bool) -> None:
+        """
+        Whether to skip web views that have no pages from being shown in getContexts
+        output. The driver uses devtools connection to retrieve the information about
+        existing pages. true by default since Appium 1.19.0, false if lower than 1.19.0.
+        """
+        self.set_capability(ENSURE_WEBVIEWS_HAVE_PAGES, value)
diff --git a/appium/options/android/common/context/extract_chrome_android_package_from_context_name_option.py b/appium/options/android/common/context/extract_chrome_android_package_from_context_name_option.py
new file mode 100644
index 000000000..95559072f
--- /dev/null
+++ b/appium/options/android/common/context/extract_chrome_android_package_from_context_name_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME = 'extractChromeAndroidPackageFromContextName'
+
+
+class ExtractChromeAndroidPackageFromContextNameOption(SupportsCapabilities):
+    @property
+    def extract_chrome_android_package_from_context_name(self) -> Optional[bool]:
+        """
+        Whether to use the android package identifier associated with the context name.
+        """
+        return self.get_capability(EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME)
+
+    @extract_chrome_android_package_from_context_name.setter
+    def extract_chrome_android_package_from_context_name(self, value: bool) -> None:
+        """
+        If set to true, tell chromedriver to attach to the android package we have associated
+        with the context name, rather than the package of the application under test.
+        false by default.
+        """
+        self.set_capability(EXTRACT_CHROME_ANDROID_PACKAGE_FROM_CONTEXT_NAME, value)
diff --git a/appium/options/android/common/context/native_web_screenshot_option.py b/appium/options/android/common/context/native_web_screenshot_option.py
new file mode 100644
index 000000000..1c59a60b5
--- /dev/null
+++ b/appium/options/android/common/context/native_web_screenshot_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+NATIVE_WEB_SCREENSHOT = 'nativeWebScreenshot'
+
+
+class NativeWebScreenshotOption(SupportsCapabilities):
+    @property
+    def native_web_screenshot(self) -> Optional[bool]:
+        """
+        Whether to use native screenshots in web view context.
+        """
+        return self.get_capability(NATIVE_WEB_SCREENSHOT)
+
+    @native_web_screenshot.setter
+    def native_web_screenshot(self, value: bool) -> None:
+        """
+        Whether to use screenshoting endpoint provided by UiAutomator framework (true)
+        rather than the one provided by chromedriver (false, the default value).
+        Use it when you experience issues with the latter.
+        """
+        self.set_capability(NATIVE_WEB_SCREENSHOT, value)
diff --git a/appium/options/android/common/context/recreate_chrome_driver_sessions_option.py b/appium/options/android/common/context/recreate_chrome_driver_sessions_option.py
new file mode 100644
index 000000000..4ef8d1096
--- /dev/null
+++ b/appium/options/android/common/context/recreate_chrome_driver_sessions_option.py
@@ -0,0 +1,41 @@
+# 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
+
+RECREATE_CHROME_DRIVER_SESSIONS = 'recreateChromeDriverSessions'
+
+
+class RecreateChromeDriverSessionsOption(SupportsCapabilities):
+    @property
+    def recreate_chrome_driver_sessions(self) -> Optional[bool]:
+        """
+        Whether chromedriver sessions should be killed and then recreated instead
+        of just suspending it on context switch.
+        """
+        return self.get_capability(RECREATE_CHROME_DRIVER_SESSIONS)
+
+    @recreate_chrome_driver_sessions.setter
+    def recreate_chrome_driver_sessions(self, value: bool) -> None:
+        """
+        If this capability is set to true then chromedriver session is always going
+        to be killed and then recreated instead of just suspending it on context
+        switching. false by default.
+        """
+        self.set_capability(RECREATE_CHROME_DRIVER_SESSIONS, value)
diff --git a/appium/options/android/common/context/show_chromedriver_log_option.py b/appium/options/android/common/context/show_chromedriver_log_option.py
new file mode 100644
index 000000000..da4f283a4
--- /dev/null
+++ b/appium/options/android/common/context/show_chromedriver_log_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SHOW_CHROMEDRIVER_LOG = 'showChromedriverLog'
+
+
+class ShowChromedriverLogOption(SupportsCapabilities):
+    @property
+    def show_chromedriver_log(self) -> Optional[bool]:
+        """
+        Whether to forward chromedriver output to the Appium server log.
+        """
+        return self.get_capability(SHOW_CHROMEDRIVER_LOG)
+
+    @show_chromedriver_log.setter
+    def show_chromedriver_log(self, value: bool) -> None:
+        """
+        If set to true then all the output from chromedriver binary will be
+        forwarded to the Appium server log. false by default.
+        """
+        self.set_capability(SHOW_CHROMEDRIVER_LOG, value)
diff --git a/appium/options/android/common/context/webview_devtools_port_option.py b/appium/options/android/common/context/webview_devtools_port_option.py
new file mode 100644
index 000000000..2f0e6d7a7
--- /dev/null
+++ b/appium/options/android/common/context/webview_devtools_port_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+WEBVIEW_DEVTOOLS_PORT = 'webviewDevtoolsPort'
+
+
+class WebviewDevtoolsPortOption(SupportsCapabilities):
+    @property
+    def webview_devtools_port(self) -> Optional[int]:
+        """
+        Local port number to use for devtools communication.
+        """
+        return self.get_capability(WEBVIEW_DEVTOOLS_PORT)
+
+    @webview_devtools_port.setter
+    def webview_devtools_port(self, value: int) -> None:
+        """
+        The local port number to use for devtools communication. By default, the first
+        free port from 10900..11000 range is selected. Consider setting the custom
+        value if you are running parallel tests.
+        """
+        self.set_capability(WEBVIEW_DEVTOOLS_PORT, value)
diff --git a/appium/options/android/common/localization/__init__.py b/appium/options/android/common/localization/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/localization/locale_script_option.py b/appium/options/android/common/localization/locale_script_option.py
new file mode 100644
index 000000000..abcd08529
--- /dev/null
+++ b/appium/options/android/common/localization/locale_script_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+LOCALE_SCRIPT = 'localeScript'
+
+
+class LocaleScriptOption(SupportsCapabilities):
+    @property
+    def locale_script(self) -> Optional[str]:
+        """
+        Canonical name of the locale to be set for the app under test.
+        """
+        return self.get_capability(LOCALE_SCRIPT)
+
+    @locale_script.setter
+    def locale_script(self, value: str) -> None:
+        """
+        Set canonical name of the locale to be set for the app under test,
+        for example zh-Hans-CN.
+        See https://developer.android.com/reference/java/util/Locale.html for more details.
+        """
+        self.set_capability(LOCALE_SCRIPT, value)
diff --git a/appium/options/android/common/locking/__init__.py b/appium/options/android/common/locking/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/locking/skip_unlock_option.py b/appium/options/android/common/locking/skip_unlock_option.py
new file mode 100644
index 000000000..c80a6efa7
--- /dev/null
+++ b/appium/options/android/common/locking/skip_unlock_option.py
@@ -0,0 +1,42 @@
+# 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
+
+SKIP_UNLOCK = 'skipUnlock'
+
+
+class SkipUnlockOption(SupportsCapabilities):
+    @property
+    def skip_unlock(self) -> Optional[bool]:
+        """
+        Whether to skip the check for lock screen presence.
+        """
+        return self.get_capability(SKIP_UNLOCK)
+
+    @skip_unlock.setter
+    def skip_unlock(self, value: bool) -> None:
+        """
+        Whether to skip the check for lock screen presence (true). By default,
+        the driver tries to detect if the device's screen is locked
+        before starting the test and to unlock that (which sometimes might be unstable).
+        Note, that this operation takes some time, so it is highly recommended to set
+        this capability to true and disable screen locking on devices under test.
+        """
+        self.set_capability(SKIP_UNLOCK, value)
diff --git a/appium/options/android/common/locking/unlock_key_option.py b/appium/options/android/common/locking/unlock_key_option.py
new file mode 100644
index 000000000..ffee5a1a5
--- /dev/null
+++ b/appium/options/android/common/locking/unlock_key_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+UNLOCK_KEY = 'unlockKey'
+
+
+class UnlockKeyOption(SupportsCapabilities):
+    @property
+    def unlock_key(self) -> Optional[str]:
+        """
+        Unlock key.
+        """
+        return self.get_capability(UNLOCK_KEY)
+
+    @unlock_key.setter
+    def unlock_key(self, value: str) -> None:
+        """
+        Allows to set an unlock key.
+        Read [Unlock tutorial](https://github.com/appium/appium-android-driver/blob/master/docs/UNLOCK.md)
+        for more details.
+        """
+        self.set_capability(UNLOCK_KEY, value)
diff --git a/appium/options/android/common/locking/unlock_strategy_option.py b/appium/options/android/common/locking/unlock_strategy_option.py
new file mode 100644
index 000000000..fb884518d
--- /dev/null
+++ b/appium/options/android/common/locking/unlock_strategy_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+UNLOCK_STRATEGY = 'unlockStrategy'
+
+
+class UnlockStrategyOption(SupportsCapabilities):
+    @property
+    def unlock_strategy(self) -> Optional[str]:
+        """
+        Unlock strategy name.
+        """
+        return self.get_capability(UNLOCK_STRATEGY)
+
+    @unlock_strategy.setter
+    def unlock_strategy(self, value: str) -> None:
+        """
+        Either 'locksettings' (default) or 'uiautomator'.
+        Setting it to 'uiautomator' will enforce the driver to avoid using special
+        ADB shortcuts in order to speed up the unlock procedure.
+        """
+        self.set_capability(UNLOCK_STRATEGY, value)
diff --git a/appium/options/android/common/locking/unlock_success_timeout_option.py b/appium/options/android/common/locking/unlock_success_timeout_option.py
new file mode 100644
index 000000000..488ae5f9e
--- /dev/null
+++ b/appium/options/android/common/locking/unlock_success_timeout_option.py
@@ -0,0 +1,43 @@
+# 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
+
+UNLOCK_SUCCESS_TIMEOUT = 'unlockSuccessTimeout'
+
+
+class UnlockSuccessTimeoutOption(SupportsCapabilities):
+    @property
+    def unlock_success_timeout(self) -> Optional[timedelta]:
+        """
+        Timeout to wait until the device is unlocked.
+        """
+        value = self.get_capability(UNLOCK_SUCCESS_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @unlock_success_timeout.setter
+    def unlock_success_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Maximum timeout to wait until the device is unlocked.
+        2000 ms by default.
+        """
+        self.set_capability(
+            UNLOCK_SUCCESS_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/android/common/locking/unlock_type_option.py b/appium/options/android/common/locking/unlock_type_option.py
new file mode 100644
index 000000000..7026cf343
--- /dev/null
+++ b/appium/options/android/common/locking/unlock_type_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+UNLOCK_TYPE = 'unlockType'
+
+
+class UnlockTypeOption(SupportsCapabilities):
+    @property
+    def unlock_type(self) -> Optional[str]:
+        """
+        Unlock type.
+        """
+        return self.get_capability(UNLOCK_TYPE)
+
+    @unlock_type.setter
+    def unlock_type(self, value: str) -> None:
+        """
+        Set one of the possible types of Android lock screens to unlock.
+        Read the [Unlock tutorial](https://github.com/appium/appium-android-driver/blob/master/docs/UNLOCK.md)
+        for more details.
+        """
+        self.set_capability(UNLOCK_TYPE, value)
diff --git a/appium/options/android/common/mjpeg/__init__.py b/appium/options/android/common/mjpeg/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/mjpeg/mjpeg_screenshot_url_option.py b/appium/options/android/common/mjpeg/mjpeg_screenshot_url_option.py
new file mode 100644
index 000000000..70bad0177
--- /dev/null
+++ b/appium/options/android/common/mjpeg/mjpeg_screenshot_url_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+MJPEG_SCREENSHOT_URL = 'mjpegScreenshotUrl'
+
+
+class MjpegScreenshotUrlOption(SupportsCapabilities):
+    @property
+    def mjpeg_screenshot_url(self) -> Optional[str]:
+        """
+        URL of a service that provides realtime device screenshots in MJPEG format.
+        """
+        return self.get_capability(MJPEG_SCREENSHOT_URL)
+
+    @mjpeg_screenshot_url.setter
+    def mjpeg_screenshot_url(self, value: str) -> None:
+        """
+        The URL of a service that provides realtime device screenshots in MJPEG format.
+        If provided then the actual command to retrieve a screenshot will be
+        requesting pictures from this service rather than directly from the server.
+        """
+        self.set_capability(MJPEG_SCREENSHOT_URL, value)
diff --git a/appium/options/android/common/other/__init__.py b/appium/options/android/common/other/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/other/disable_suppress_accessibility_service_option.py b/appium/options/android/common/other/disable_suppress_accessibility_service_option.py
new file mode 100644
index 000000000..980da567c
--- /dev/null
+++ b/appium/options/android/common/other/disable_suppress_accessibility_service_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE = 'disableSuppressAccessibilityService'
+
+
+class DisableSuppressAccessibilityServiceOption(SupportsCapabilities):
+    @property
+    def disable_suppress_accessibility_service(self) -> Optional[bool]:
+        """
+        Whether to suppress accessibility services.
+        """
+        return self.get_capability(DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE)
+
+    @disable_suppress_accessibility_service.setter
+    def disable_suppress_accessibility_service(self, value: bool) -> None:
+        """
+        Being set to true tells the instrumentation process to not suppress
+        accessibility services during the automated test. This might be useful
+        if your automated test needs these services. false by default.
+        """
+        self.set_capability(DISABLE_SUPPRESS_ACCESSIBILITY_SERVICE, value)
diff --git a/appium/options/android/common/other/user_profile_option.py b/appium/options/android/common/other/user_profile_option.py
new file mode 100644
index 000000000..76871396a
--- /dev/null
+++ b/appium/options/android/common/other/user_profile_option.py
@@ -0,0 +1,42 @@
+# 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
+
+USER_PROFILE = 'userProfile'
+
+
+class UserProfileOption(SupportsCapabilities):
+    @property
+    def user_profile(self) -> Optional[int]:
+        """
+        Integer identifier of a user profile.
+        """
+        return self.get_capability(USER_PROFILE)
+
+    @user_profile.setter
+    def user_profile(self, value: int) -> None:
+        """
+        Integer identifier of a user profile. By default, the app under test is
+        installed for the currently active user, but in case it is necessary to
+        test how the app performs while being installed for a user profile,
+        which is different from the current one, this capability might
+        come in handy.
+        """
+        self.set_capability(USER_PROFILE, value)
diff --git a/appium/options/android/common/signing/__init__.py b/appium/options/android/common/signing/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/common/signing/key_alias_option.py b/appium/options/android/common/signing/key_alias_option.py
new file mode 100644
index 000000000..471b28ee8
--- /dev/null
+++ b/appium/options/android/common/signing/key_alias_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+KEY_ALIAS = 'keyAlias'
+
+
+class KeyAliasOption(SupportsCapabilities):
+    @property
+    def key_alias(self) -> Optional[str]:
+        """
+        Keystore key alias.
+        """
+        return self.get_capability(KEY_ALIAS)
+
+    @key_alias.setter
+    def key_alias(self, value: str) -> None:
+        """
+        The alias of the key in the keystore file provided in keystorePath capability.
+        This option is used in combination with useKeystore, keystorePath,
+        keystorePassword, keyAlias and keyPassword options. Unset by default
+        """
+        self.set_capability(KEY_ALIAS, value)
diff --git a/appium/options/android/common/signing/key_password_option.py b/appium/options/android/common/signing/key_password_option.py
new file mode 100644
index 000000000..5ed91d1c1
--- /dev/null
+++ b/appium/options/android/common/signing/key_password_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+KEY_PASSWORD = 'keyPassword'
+
+
+class KeyPasswordOption(SupportsCapabilities):
+    @property
+    def key_password(self) -> Optional[str]:
+        """
+        Keystore key password.
+        """
+        return self.get_capability(KEY_PASSWORD)
+
+    @key_password.setter
+    def key_password(self, value: str) -> None:
+        """
+        The password of the key in the keystore file provided in keystorePath capability.
+        This option is used in combination with useKeystore, keystorePath,
+        keystorePassword, keyAlias and keyPassword options. Unset by default
+        """
+        self.set_capability(KEY_PASSWORD, value)
diff --git a/appium/options/android/common/signing/keystore_password_option.py b/appium/options/android/common/signing/keystore_password_option.py
new file mode 100644
index 000000000..8b7667117
--- /dev/null
+++ b/appium/options/android/common/signing/keystore_password_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+KEYSTORE_PASSWORD = 'keystorePassword'
+
+
+class KeystorePasswordOption(SupportsCapabilities):
+    @property
+    def keystore_password(self) -> Optional[str]:
+        """
+        Keystore password.
+        """
+        return self.get_capability(KEYSTORE_PASSWORD)
+
+    @keystore_password.setter
+    def keystore_password(self, value: str) -> None:
+        """
+        The password to the keystore file provided in keystorePath capability.
+        This option is used in combination with useKeystore, keystorePath,
+        keystorePassword, keyAlias and keyPassword options. Unset by default
+        """
+        self.set_capability(KEYSTORE_PASSWORD, value)
diff --git a/appium/options/android/common/signing/keystore_path_option.py b/appium/options/android/common/signing/keystore_path_option.py
new file mode 100644
index 000000000..3489efe5c
--- /dev/null
+++ b/appium/options/android/common/signing/keystore_path_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+KEYSTORE_PATH = 'keystorePath'
+
+
+class KeystorePathOption(SupportsCapabilities):
+    @property
+    def keystore_path(self) -> Optional[str]:
+        """
+        The path to keystore.
+        """
+        return self.get_capability(KEYSTORE_PATH)
+
+    @keystore_path.setter
+    def keystore_path(self, value: str) -> None:
+        """
+        The full path to the keystore file on the server filesystem.
+        This option is used in combination with useKeystore, keystorePath,
+        keystorePassword, keyAlias and keyPassword options. Unset by default
+        """
+        self.set_capability(KEYSTORE_PATH, value)
diff --git a/appium/options/android/common/signing/no_sign_option.py b/appium/options/android/common/signing/no_sign_option.py
new file mode 100644
index 000000000..a5a52841b
--- /dev/null
+++ b/appium/options/android/common/signing/no_sign_option.py
@@ -0,0 +1,42 @@
+# 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
+
+NO_SIGN = 'noSign'
+
+
+class NoSignOption(SupportsCapabilities):
+    @property
+    def no_sign(self) -> Optional[bool]:
+        """
+        Whether to skip application signing.
+        """
+        return self.get_capability(NO_SIGN)
+
+    @no_sign.setter
+    def no_sign(self, value: bool) -> None:
+        """
+        Set it to true in order to skip application signing. By default
+        all apps are always signed with the default Appium debug signature
+        if they don't have any. This capability cancels all the signing checks
+        and makes the driver to use the application package as is. This option
+        does not affect .apks packages as these are expected to be already signed.
+        """
+        self.set_capability(NO_SIGN, value)
diff --git a/appium/options/android/common/signing/use_keystore_option.py b/appium/options/android/common/signing/use_keystore_option.py
new file mode 100644
index 000000000..f8a947a5c
--- /dev/null
+++ b/appium/options/android/common/signing/use_keystore_option.py
@@ -0,0 +1,42 @@
+# 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
+
+USE_KEYSTORE = 'useKeystore'
+
+
+class UseKeystoreOption(SupportsCapabilities):
+    @property
+    def use_keystore(self) -> Optional[bool]:
+        """
+        Whether to use custom keystore.
+        """
+        return self.get_capability(USE_KEYSTORE)
+
+    @use_keystore.setter
+    def use_keystore(self, value: bool) -> None:
+        """
+        Whether to use a custom keystore to sign the app under test.
+        false by default, which means apps are always signed with the default A
+        ppium debug certificate (unless canceled by noSign capability).
+        This option is used in combination with keystorePath, keystorePassword,
+        keyAlias and keyPassword options.
+        """
+        self.set_capability(USE_KEYSTORE, value)
diff --git a/appium/options/android/espresso/__init__.py b/appium/options/android/espresso/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/espresso/activity_options_option.py b/appium/options/android/espresso/activity_options_option.py
new file mode 100644
index 000000000..a15935feb
--- /dev/null
+++ b/appium/options/android/espresso/activity_options_option.py
@@ -0,0 +1,41 @@
+# 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, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ACTIVITY_OPTIONS = 'activityOptions'
+
+
+class ActivityOptionsOption(SupportsCapabilities):
+    @property
+    def activity_options(self) -> Optional[Dict]:
+        """
+        Activity options.
+        """
+        return self.get_capability(ACTIVITY_OPTIONS)
+
+    @activity_options.setter
+    def activity_options(self, value: Dict) -> None:
+        """
+        The mapping of custom options for the main app activity that is going to
+        be started. Check
+        https://github.com/appium/appium-espresso-driver#activity-options
+        for more details.
+        """
+        self.set_capability(ACTIVITY_OPTIONS, value)
diff --git a/appium/options/android/espresso/app_locale_option.py b/appium/options/android/espresso/app_locale_option.py
new file mode 100644
index 000000000..01ae27545
--- /dev/null
+++ b/appium/options/android/espresso/app_locale_option.py
@@ -0,0 +1,44 @@
+# 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, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_LOCALE = 'appLocale'
+
+
+class AppLocaleOption(SupportsCapabilities):
+    @property
+    def app_locale(self) -> Optional[Dict[str, str]]:
+        """
+        Locale for the app under test.
+        """
+        return self.get_capability(APP_LOCALE)
+
+    @app_locale.setter
+    def app_locale(self, value: Dict[str, str]) -> None:
+        """
+        Sets the locale for the app under test. The main difference between this option
+        and the above ones is that this option only changes the locale for the application
+        under test and does not affect other parts of the system. Also, it only uses
+        public APIs for its purpose. See
+        https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers to get the
+        list of available language abbreviations.
+        Example: {"language": "zh", "country": "CN", "variant": "Hans"}.
+        """
+        self.set_capability(APP_LOCALE, value)
diff --git a/appium/options/android/espresso/base.py b/appium/options/android/espresso/base.py
new file mode 100644
index 000000000..c725b63a7
--- /dev/null
+++ b/appium/options/android/espresso/base.py
@@ -0,0 +1,221 @@
+# 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.android.common.adb.adb_exec_timeout_option import AdbExecTimeoutOption
+from appium.options.android.common.adb.adb_port_option import AdbPortOption
+from appium.options.android.common.adb.allow_delay_adb_option import AllowDelayAdbOption
+from appium.options.android.common.adb.build_tools_version_option import BuildToolsVersionOption
+from appium.options.android.common.adb.clear_device_logs_on_start_option import ClearDeviceLogsOnStartOption
+from appium.options.android.common.adb.ignore_hidden_api_policy_error_option import IgnoreHiddenApiPolicyErrorOption
+from appium.options.android.common.adb.logcat_filter_specs_option import LogcatFilterSpecsOption
+from appium.options.android.common.adb.logcat_format_option import LogcatFormatOption
+from appium.options.android.common.adb.mock_location_app_option import MockLocationAppOption
+from appium.options.android.common.adb.remote_adb_host_option import RemoteAdbHostOption
+from appium.options.android.common.adb.skip_logcat_capture_option import SkipLogcatCaptureOption
+from appium.options.android.common.adb.suppress_kill_server_option import SuppressKillServerOption
+from appium.options.android.common.app.allow_test_packages_option import AllowTestPackagesOption
+from appium.options.android.common.app.android_install_timeout_option import AndroidInstallTimeoutOption
+from appium.options.android.common.app.app_activity_option import AppActivityOption
+from appium.options.android.common.app.app_package_option import AppPackageOption
+from appium.options.android.common.app.app_wait_activity_option import AppWaitActivityOption
+from appium.options.android.common.app.app_wait_duration_option import AppWaitDurationOption
+from appium.options.android.common.app.app_wait_for_launch_option import AppWaitForLaunchOption
+from appium.options.android.common.app.app_wait_package_option import AppWaitPackageOption
+from appium.options.android.common.app.auto_grant_premissions_option import AutoGrantPermissionsOption
+from appium.options.android.common.app.enforce_app_install_option import EnforceAppInstallOption
+from appium.options.android.common.app.intent_action_option import IntentActionOption
+from appium.options.android.common.app.intent_category_option import IntentCategoryOption
+from appium.options.android.common.app.intent_flags_option import IntentFlagsOption
+from appium.options.android.common.app.optional_intent_arguments_option import OptionalIntentArgumentsOption
+from appium.options.android.common.app.remote_apps_cache_limit_option import RemoteAppsCacheLimitOption
+from appium.options.android.common.app.uninstall_other_packages_option import UninstallOtherPackagesOption
+from appium.options.android.common.avd.avd_args_option import AvdArgsOption
+from appium.options.android.common.avd.avd_env_option import AvdEnvOption
+from appium.options.android.common.avd.avd_launch_timeout_option import AvdLaunchTimeoutOption
+from appium.options.android.common.avd.avd_option import AvdOption
+from appium.options.android.common.avd.avd_ready_timeout_option import AvdReadyTimeoutOption
+from appium.options.android.common.avd.gps_enabled_option import GpsEnabledOption
+from appium.options.android.common.avd.network_speed_option import NetworkSpeedOption
+from appium.options.android.common.context.auto_webview_timeout_option import AutoWebviewTimeoutOption
+from appium.options.android.common.context.chrome_logging_prefs_option import ChromeLoggingPrefsOption
+from appium.options.android.common.context.chrome_options_option import ChromeOptionsOption
+from appium.options.android.common.context.chromedriver_args_option import ChromedriverArgsOption
+from appium.options.android.common.context.chromedriver_chrome_mapping_file_option import (
+    ChromedriverChromeMappingFileOption,
+)
+from appium.options.android.common.context.chromedriver_disable_build_check_option import (
+    ChromedriverDisableBuildCheckOption,
+)
+from appium.options.android.common.context.chromedriver_executable_dir_option import ChromedriverExecutableDirOption
+from appium.options.android.common.context.chromedriver_executable_option import ChromedriverExecutableOption
+from appium.options.android.common.context.chromedriver_port_option import ChromedriverPortOption
+from appium.options.android.common.context.chromedriver_ports_option import ChromedriverPortsOption
+from appium.options.android.common.context.chromedriver_use_system_executable_option import (
+    ChromedriverUseSystemExecutableOption,
+)
+from appium.options.android.common.context.ensure_webviews_have_pages_option import EnsureWebviewsHavePagesOption
+from appium.options.android.common.context.extract_chrome_android_package_from_context_name_option import (
+    ExtractChromeAndroidPackageFromContextNameOption,
+)
+from appium.options.android.common.context.native_web_screenshot_option import NativeWebScreenshotOption
+from appium.options.android.common.context.recreate_chrome_driver_sessions_option import (
+    RecreateChromeDriverSessionsOption,
+)
+from appium.options.android.common.context.show_chromedriver_log_option import ShowChromedriverLogOption
+from appium.options.android.common.context.webview_devtools_port_option import WebviewDevtoolsPortOption
+from appium.options.android.common.localization.locale_script_option import LocaleScriptOption
+from appium.options.android.common.locking.skip_unlock_option import SkipUnlockOption
+from appium.options.android.common.locking.unlock_key_option import UnlockKeyOption
+from appium.options.android.common.locking.unlock_strategy_option import UnlockStrategyOption
+from appium.options.android.common.locking.unlock_success_timeout_option import UnlockSuccessTimeoutOption
+from appium.options.android.common.locking.unlock_type_option import UnlockTypeOption
+from appium.options.android.common.mjpeg.mjpeg_screenshot_url_option import MjpegScreenshotUrlOption
+from appium.options.android.common.other.disable_suppress_accessibility_service_option import (
+    DisableSuppressAccessibilityServiceOption,
+)
+from appium.options.android.common.other.user_profile_option import UserProfileOption
+from appium.options.android.common.signing.key_alias_option import KeyAliasOption
+from appium.options.android.common.signing.key_password_option import KeyPasswordOption
+from appium.options.android.common.signing.keystore_password_option import KeystorePasswordOption
+from appium.options.android.common.signing.keystore_path_option import KeystorePathOption
+from appium.options.android.common.signing.no_sign_option import NoSignOption
+from appium.options.android.common.signing.use_keystore_option import UseKeystoreOption
+from appium.options.common.app_option import AppOption
+from appium.options.common.auto_web_view_option import AutoWebViewOption
+from appium.options.common.automation_name_option import AUTOMATION_NAME
+from appium.options.common.base import PLATFORM_NAME, AppiumOptions
+from appium.options.common.clear_system_files_option import ClearSystemFilesOption
+from appium.options.common.device_name_option import DeviceNameOption
+from appium.options.common.enable_performance_logging_option import EnablePerformanceLoggingOption
+from appium.options.common.is_headless_option import IsHeadlessOption
+from appium.options.common.language_option import LanguageOption
+from appium.options.common.locale_option import LocaleOption
+from appium.options.common.orientation_option import OrientationOption
+from appium.options.common.other_apps_option import OtherAppsOption
+from appium.options.common.platform_version_option import PlatformVersionOption
+from appium.options.common.skip_log_capture_option import SkipLogCaptureOption
+from appium.options.common.system_port_option import SystemPortOption
+from appium.options.common.udid_option import UdidOption
+
+from .activity_options_option import ActivityOptionsOption
+from .app_locale_option import AppLocaleOption
+from .espresso_build_config_option import EspressoBuildConfigOption
+from .espresso_server_launch_timeout_option import EspressoServerLaunchTimeoutOption
+from .force_espresso_rebuild_option import ForceEspressoRebuildOption
+from .intent_options_option import IntentOptionsOption
+from .show_gradle_log_option import ShowGradleLogOption
+
+
+class EspressoOptions(
+    AppiumOptions,
+    AppOption,
+    OrientationOption,
+    UdidOption,
+    LanguageOption,
+    LocaleOption,
+    IsHeadlessOption,
+    SkipLogCaptureOption,
+    AutoWebViewOption,
+    DeviceNameOption,
+    ClearSystemFilesOption,
+    EnablePerformanceLoggingOption,
+    OtherAppsOption,
+    SystemPortOption,
+    AppPackageOption,
+    AppActivityOption,
+    AppWaitActivityOption,
+    AppWaitPackageOption,
+    AppWaitDurationOption,
+    AndroidInstallTimeoutOption,
+    AppWaitForLaunchOption,
+    IntentCategoryOption,
+    IntentActionOption,
+    IntentFlagsOption,
+    OptionalIntentArgumentsOption,
+    AutoGrantPermissionsOption,
+    UninstallOtherPackagesOption,
+    AllowTestPackagesOption,
+    RemoteAppsCacheLimitOption,
+    EnforceAppInstallOption,
+    LocaleScriptOption,
+    AdbPortOption,
+    PlatformVersionOption,
+    RemoteAdbHostOption,
+    AdbExecTimeoutOption,
+    ClearDeviceLogsOnStartOption,
+    BuildToolsVersionOption,
+    SkipLogcatCaptureOption,
+    SuppressKillServerOption,
+    IgnoreHiddenApiPolicyErrorOption,
+    MockLocationAppOption,
+    LogcatFormatOption,
+    LogcatFilterSpecsOption,
+    AllowDelayAdbOption,
+    AvdOption,
+    AvdLaunchTimeoutOption,
+    AvdReadyTimeoutOption,
+    AvdArgsOption,
+    AvdEnvOption,
+    NetworkSpeedOption,
+    GpsEnabledOption,
+    UseKeystoreOption,
+    KeystorePathOption,
+    KeystorePasswordOption,
+    KeyAliasOption,
+    KeyPasswordOption,
+    NoSignOption,
+    SkipUnlockOption,
+    UnlockKeyOption,
+    UnlockStrategyOption,
+    UnlockSuccessTimeoutOption,
+    UnlockTypeOption,
+    MjpegScreenshotUrlOption,
+    AutoWebviewTimeoutOption,
+    ChromeLoggingPrefsOption,
+    ChromeOptionsOption,
+    ChromedriverArgsOption,
+    ChromedriverChromeMappingFileOption,
+    ChromedriverDisableBuildCheckOption,
+    ChromedriverExecutableDirOption,
+    ChromedriverExecutableOption,
+    ChromedriverPortOption,
+    ChromedriverPortsOption,
+    ChromedriverUseSystemExecutableOption,
+    EnsureWebviewsHavePagesOption,
+    ExtractChromeAndroidPackageFromContextNameOption,
+    NativeWebScreenshotOption,
+    RecreateChromeDriverSessionsOption,
+    ShowChromedriverLogOption,
+    WebviewDevtoolsPortOption,
+    DisableSuppressAccessibilityServiceOption,
+    UserProfileOption,
+    EspressoBuildConfigOption,
+    EspressoServerLaunchTimeoutOption,
+    ForceEspressoRebuildOption,
+    ShowGradleLogOption,
+    IntentOptionsOption,
+    ActivityOptionsOption,
+    AppLocaleOption,
+):
+    @property
+    def default_capabilities(self) -> Dict:
+        return {
+            AUTOMATION_NAME: 'Espresso',
+            PLATFORM_NAME: 'Android',
+        }
diff --git a/appium/options/android/espresso/espresso_build_config_option.py b/appium/options/android/espresso/espresso_build_config_option.py
new file mode 100644
index 000000000..7a8694f31
--- /dev/null
+++ b/appium/options/android/espresso/espresso_build_config_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.
+
+import json
+from typing import Any, Dict, Optional, Union
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ESPRESSO_BUILD_CONFIG = 'espressoBuildConfig'
+
+
+class EspressoBuildConfigOption(SupportsCapabilities):
+    @property
+    def espresso_build_config(self) -> Optional[Union[Dict[str, Any], str]]:
+        """
+        Espresso build config.
+        """
+        value = self.get_capability(ESPRESSO_BUILD_CONFIG)
+        try:
+            return json.loads(value)
+        except Exception:
+            return value
+
+    @espresso_build_config.setter
+    def espresso_build_config(self, value: Union[Dict[str, Any], str]) -> None:
+        """
+        This config allows to customize several important properties of
+        Espresso server. Refer to
+        https://github.com/appium/appium-espresso-driver#espresso-build-config
+        for more information on how to properly construct such config.
+        """
+        self.set_capability(ESPRESSO_BUILD_CONFIG, value if isinstance(value, str) else json.dumps(value, ensure_ascii=False))
diff --git a/appium/options/android/espresso/espresso_server_launch_timeout_option.py b/appium/options/android/espresso/espresso_server_launch_timeout_option.py
new file mode 100644
index 000000000..9335358de
--- /dev/null
+++ b/appium/options/android/espresso/espresso_server_launch_timeout_option.py
@@ -0,0 +1,43 @@
+# 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
+
+ESPRESSO_SERVER_LAUNCH_TIMEOUT = 'espressoServerLaunchTimeout'
+
+
+class EspressoServerLaunchTimeoutOption(SupportsCapabilities):
+    @property
+    def espresso_server_launch_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout to wait until Espresso server is listening on the device.
+        """
+        value = self.get_capability(ESPRESSO_SERVER_LAUNCH_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @espresso_server_launch_timeout.setter
+    def espresso_server_launch_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Set the maximum timeout to wait util Espresso  is listening on the device.
+        45000 ms by default
+        """
+        self.set_capability(
+            ESPRESSO_SERVER_LAUNCH_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/android/espresso/force_espresso_rebuild_option.py b/appium/options/android/espresso/force_espresso_rebuild_option.py
new file mode 100644
index 000000000..8971db795
--- /dev/null
+++ b/appium/options/android/espresso/force_espresso_rebuild_option.py
@@ -0,0 +1,41 @@
+# 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
+
+FORCE_ESPRESSO_REBUILD = 'forceEspressoRebuild'
+
+
+class ForceEspressoRebuildOption(SupportsCapabilities):
+    @property
+    def force_espresso_rebuild(self) -> Optional[bool]:
+        """
+        Whether to force Espresso server rebuild on a new session startup.
+        """
+        return self.get_capability(FORCE_ESPRESSO_REBUILD)
+
+    @force_espresso_rebuild.setter
+    def force_espresso_rebuild(self, value: bool) -> None:
+        """
+        Whether to always enforce Espresso server rebuild (true).
+        By default, Espresso caches the already built server apk and only rebuilds
+        it when it is necessary, because rebuilding process needs extra time.
+        false by default.
+        """
+        self.set_capability(FORCE_ESPRESSO_REBUILD, value)
diff --git a/appium/options/android/espresso/intent_options_option.py b/appium/options/android/espresso/intent_options_option.py
new file mode 100644
index 000000000..59625bd91
--- /dev/null
+++ b/appium/options/android/espresso/intent_options_option.py
@@ -0,0 +1,41 @@
+# 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 Any, Dict, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+INTENT_OPTIONS = 'intentOptions'
+
+
+class IntentOptionsOption(SupportsCapabilities):
+    @property
+    def intent_options(self) -> Optional[Dict[str, Any]]:
+        """
+        Intent options.
+        """
+        return self.get_capability(INTENT_OPTIONS)
+
+    @intent_options.setter
+    def intent_options(self, value: Dict[str, Any]) -> None:
+        """
+        The mapping of custom options for the intent that is going to be passed
+        to the main app activity. Check
+        https://github.com/appium/appium-espresso-driver#intent-options
+        for more details.
+        """
+        self.set_capability(INTENT_OPTIONS, value)
diff --git a/appium/options/android/espresso/show_gradle_log_option.py b/appium/options/android/espresso/show_gradle_log_option.py
new file mode 100644
index 000000000..48d233dbf
--- /dev/null
+++ b/appium/options/android/espresso/show_gradle_log_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SHOW_GRADLE_LOG = 'showGradleLog'
+
+
+class ShowGradleLogOption(SupportsCapabilities):
+    @property
+    def show_gradle_log(self) -> Optional[bool]:
+        """
+        Whether to include Gradle log to the regular server log.
+        """
+        return self.get_capability(SHOW_GRADLE_LOG)
+
+    @show_gradle_log.setter
+    def show_gradle_log(self, value: bool) -> None:
+        """
+        Whether to include Gradle log to the regular server logs while
+        building Espresso server. false by default.
+        """
+        self.set_capability(SHOW_GRADLE_LOG, value)
diff --git a/appium/options/android/uiautomator2/__init__.py b/appium/options/android/uiautomator2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/android/uiautomator2/base.py b/appium/options/android/uiautomator2/base.py
new file mode 100644
index 000000000..7efcb9036
--- /dev/null
+++ b/appium/options/android/uiautomator2/base.py
@@ -0,0 +1,221 @@
+# 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.android.common.adb.adb_exec_timeout_option import AdbExecTimeoutOption
+from appium.options.android.common.adb.adb_port_option import AdbPortOption
+from appium.options.android.common.adb.allow_delay_adb_option import AllowDelayAdbOption
+from appium.options.android.common.adb.build_tools_version_option import BuildToolsVersionOption
+from appium.options.android.common.adb.clear_device_logs_on_start_option import ClearDeviceLogsOnStartOption
+from appium.options.android.common.adb.ignore_hidden_api_policy_error_option import IgnoreHiddenApiPolicyErrorOption
+from appium.options.android.common.adb.logcat_filter_specs_option import LogcatFilterSpecsOption
+from appium.options.android.common.adb.logcat_format_option import LogcatFormatOption
+from appium.options.android.common.adb.mock_location_app_option import MockLocationAppOption
+from appium.options.android.common.adb.remote_adb_host_option import RemoteAdbHostOption
+from appium.options.android.common.adb.skip_logcat_capture_option import SkipLogcatCaptureOption
+from appium.options.android.common.adb.suppress_kill_server_option import SuppressKillServerOption
+from appium.options.android.common.app.allow_test_packages_option import AllowTestPackagesOption
+from appium.options.android.common.app.android_install_timeout_option import AndroidInstallTimeoutOption
+from appium.options.android.common.app.app_activity_option import AppActivityOption
+from appium.options.android.common.app.app_package_option import AppPackageOption
+from appium.options.android.common.app.app_wait_activity_option import AppWaitActivityOption
+from appium.options.android.common.app.app_wait_duration_option import AppWaitDurationOption
+from appium.options.android.common.app.app_wait_for_launch_option import AppWaitForLaunchOption
+from appium.options.android.common.app.app_wait_package_option import AppWaitPackageOption
+from appium.options.android.common.app.auto_grant_premissions_option import AutoGrantPermissionsOption
+from appium.options.android.common.app.enforce_app_install_option import EnforceAppInstallOption
+from appium.options.android.common.app.intent_action_option import IntentActionOption
+from appium.options.android.common.app.intent_category_option import IntentCategoryOption
+from appium.options.android.common.app.intent_flags_option import IntentFlagsOption
+from appium.options.android.common.app.optional_intent_arguments_option import OptionalIntentArgumentsOption
+from appium.options.android.common.app.remote_apps_cache_limit_option import RemoteAppsCacheLimitOption
+from appium.options.android.common.app.uninstall_other_packages_option import UninstallOtherPackagesOption
+from appium.options.android.common.avd.avd_args_option import AvdArgsOption
+from appium.options.android.common.avd.avd_env_option import AvdEnvOption
+from appium.options.android.common.avd.avd_launch_timeout_option import AvdLaunchTimeoutOption
+from appium.options.android.common.avd.avd_option import AvdOption
+from appium.options.android.common.avd.avd_ready_timeout_option import AvdReadyTimeoutOption
+from appium.options.android.common.avd.gps_enabled_option import GpsEnabledOption
+from appium.options.android.common.avd.network_speed_option import NetworkSpeedOption
+from appium.options.android.common.context.auto_webview_timeout_option import AutoWebviewTimeoutOption
+from appium.options.android.common.context.chrome_logging_prefs_option import ChromeLoggingPrefsOption
+from appium.options.android.common.context.chrome_options_option import ChromeOptionsOption
+from appium.options.android.common.context.chromedriver_args_option import ChromedriverArgsOption
+from appium.options.android.common.context.chromedriver_chrome_mapping_file_option import (
+    ChromedriverChromeMappingFileOption,
+)
+from appium.options.android.common.context.chromedriver_disable_build_check_option import (
+    ChromedriverDisableBuildCheckOption,
+)
+from appium.options.android.common.context.chromedriver_executable_dir_option import ChromedriverExecutableDirOption
+from appium.options.android.common.context.chromedriver_executable_option import ChromedriverExecutableOption
+from appium.options.android.common.context.chromedriver_port_option import ChromedriverPortOption
+from appium.options.android.common.context.chromedriver_ports_option import ChromedriverPortsOption
+from appium.options.android.common.context.chromedriver_use_system_executable_option import (
+    ChromedriverUseSystemExecutableOption,
+)
+from appium.options.android.common.context.ensure_webviews_have_pages_option import EnsureWebviewsHavePagesOption
+from appium.options.android.common.context.extract_chrome_android_package_from_context_name_option import (
+    ExtractChromeAndroidPackageFromContextNameOption,
+)
+from appium.options.android.common.context.native_web_screenshot_option import NativeWebScreenshotOption
+from appium.options.android.common.context.recreate_chrome_driver_sessions_option import (
+    RecreateChromeDriverSessionsOption,
+)
+from appium.options.android.common.context.show_chromedriver_log_option import ShowChromedriverLogOption
+from appium.options.android.common.context.webview_devtools_port_option import WebviewDevtoolsPortOption
+from appium.options.android.common.localization.locale_script_option import LocaleScriptOption
+from appium.options.android.common.locking.skip_unlock_option import SkipUnlockOption
+from appium.options.android.common.locking.unlock_key_option import UnlockKeyOption
+from appium.options.android.common.locking.unlock_strategy_option import UnlockStrategyOption
+from appium.options.android.common.locking.unlock_success_timeout_option import UnlockSuccessTimeoutOption
+from appium.options.android.common.locking.unlock_type_option import UnlockTypeOption
+from appium.options.android.common.mjpeg.mjpeg_screenshot_url_option import MjpegScreenshotUrlOption
+from appium.options.android.common.other.disable_suppress_accessibility_service_option import (
+    DisableSuppressAccessibilityServiceOption,
+)
+from appium.options.android.common.other.user_profile_option import UserProfileOption
+from appium.options.android.common.signing.key_alias_option import KeyAliasOption
+from appium.options.android.common.signing.key_password_option import KeyPasswordOption
+from appium.options.android.common.signing.keystore_password_option import KeystorePasswordOption
+from appium.options.android.common.signing.keystore_path_option import KeystorePathOption
+from appium.options.android.common.signing.no_sign_option import NoSignOption
+from appium.options.android.common.signing.use_keystore_option import UseKeystoreOption
+from appium.options.common.app_option import AppOption
+from appium.options.common.auto_web_view_option import AutoWebViewOption
+from appium.options.common.automation_name_option import AUTOMATION_NAME
+from appium.options.common.base import PLATFORM_NAME, AppiumOptions
+from appium.options.common.clear_system_files_option import ClearSystemFilesOption
+from appium.options.common.device_name_option import DeviceNameOption
+from appium.options.common.enable_performance_logging_option import EnablePerformanceLoggingOption
+from appium.options.common.is_headless_option import IsHeadlessOption
+from appium.options.common.language_option import LanguageOption
+from appium.options.common.locale_option import LocaleOption
+from appium.options.common.orientation_option import OrientationOption
+from appium.options.common.other_apps_option import OtherAppsOption
+from appium.options.common.platform_version_option import PlatformVersionOption
+from appium.options.common.skip_log_capture_option import SkipLogCaptureOption
+from appium.options.common.system_port_option import SystemPortOption
+from appium.options.common.udid_option import UdidOption
+
+from .disable_window_animation_option import DisableWindowAnimationOption
+from .mjpeg_server_port_option import MjpegServerPortOption
+from .skip_device_initialization_option import SkipDeviceInitializationOption
+from .skip_server_installation_option import SkipServerInstallationOption
+from .uiautomator2_server_install_timeout_option import Uiautomator2ServerInstallTimeoutOption
+from .uiautomator2_server_launch_timeout_option import Uiautomator2ServerLaunchTimeoutOption
+from .uiautomator2_server_read_timeout_option import Uiautomator2ServerReadTimeoutOption
+
+
+class UiAutomator2Options(
+    AppiumOptions,
+    AppOption,
+    ClearSystemFilesOption,
+    OrientationOption,
+    UdidOption,
+    LanguageOption,
+    LocaleOption,
+    IsHeadlessOption,
+    SkipLogCaptureOption,
+    AutoWebViewOption,
+    EnablePerformanceLoggingOption,
+    OtherAppsOption,
+    DeviceNameOption,
+    SystemPortOption,
+    SkipServerInstallationOption,
+    Uiautomator2ServerInstallTimeoutOption,
+    Uiautomator2ServerLaunchTimeoutOption,
+    Uiautomator2ServerReadTimeoutOption,
+    DisableWindowAnimationOption,
+    SkipDeviceInitializationOption,
+    AppPackageOption,
+    AppActivityOption,
+    PlatformVersionOption,
+    AppWaitActivityOption,
+    AppWaitPackageOption,
+    AppWaitDurationOption,
+    AndroidInstallTimeoutOption,
+    AppWaitForLaunchOption,
+    IntentCategoryOption,
+    IntentActionOption,
+    IntentFlagsOption,
+    OptionalIntentArgumentsOption,
+    AutoGrantPermissionsOption,
+    UninstallOtherPackagesOption,
+    AllowTestPackagesOption,
+    RemoteAppsCacheLimitOption,
+    EnforceAppInstallOption,
+    LocaleScriptOption,
+    AdbPortOption,
+    RemoteAdbHostOption,
+    AdbExecTimeoutOption,
+    ClearDeviceLogsOnStartOption,
+    SkipLogcatCaptureOption,
+    BuildToolsVersionOption,
+    SuppressKillServerOption,
+    IgnoreHiddenApiPolicyErrorOption,
+    MockLocationAppOption,
+    LogcatFormatOption,
+    LogcatFilterSpecsOption,
+    AllowDelayAdbOption,
+    AvdOption,
+    AvdLaunchTimeoutOption,
+    AvdReadyTimeoutOption,
+    AvdArgsOption,
+    AvdEnvOption,
+    NetworkSpeedOption,
+    GpsEnabledOption,
+    UseKeystoreOption,
+    KeystorePathOption,
+    KeystorePasswordOption,
+    KeyAliasOption,
+    KeyPasswordOption,
+    NoSignOption,
+    SkipUnlockOption,
+    UnlockKeyOption,
+    UnlockStrategyOption,
+    UnlockSuccessTimeoutOption,
+    UnlockTypeOption,
+    MjpegServerPortOption,
+    MjpegScreenshotUrlOption,
+    AutoWebviewTimeoutOption,
+    ChromeLoggingPrefsOption,
+    ChromeOptionsOption,
+    ChromedriverArgsOption,
+    ChromedriverChromeMappingFileOption,
+    ChromedriverDisableBuildCheckOption,
+    ChromedriverExecutableDirOption,
+    ChromedriverExecutableOption,
+    ChromedriverPortOption,
+    ChromedriverPortsOption,
+    ChromedriverUseSystemExecutableOption,
+    EnsureWebviewsHavePagesOption,
+    ExtractChromeAndroidPackageFromContextNameOption,
+    NativeWebScreenshotOption,
+    RecreateChromeDriverSessionsOption,
+    ShowChromedriverLogOption,
+    WebviewDevtoolsPortOption,
+    DisableSuppressAccessibilityServiceOption,
+    UserProfileOption,
+):
+    @property
+    def default_capabilities(self) -> Dict:
+        return {
+            AUTOMATION_NAME: 'UIAutomator2',
+            PLATFORM_NAME: 'Android',
+        }
diff --git a/appium/options/android/uiautomator2/disable_window_animation_option.py b/appium/options/android/uiautomator2/disable_window_animation_option.py
new file mode 100644
index 000000000..5ff614b49
--- /dev/null
+++ b/appium/options/android/uiautomator2/disable_window_animation_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+DISABLE_WINDOWS_ANIMATION = 'disableWindowAnimation'
+
+
+class DisableWindowAnimationOption(SupportsCapabilities):
+    @property
+    def disable_window_animation(self) -> Optional[bool]:
+        """
+        Whether window animations when starting the instrumentation process
+        are disabled.
+        """
+        return self.get_capability(DISABLE_WINDOWS_ANIMATION)
+
+    @disable_window_animation.setter
+    def disable_window_animation(self, value: bool) -> None:
+        """
+        Set whether to disable window animations when starting the instrumentation process.
+        false by default
+        """
+        self.set_capability(DISABLE_WINDOWS_ANIMATION, value)
diff --git a/appium/options/android/uiautomator2/mjpeg_server_port_option.py b/appium/options/android/uiautomator2/mjpeg_server_port_option.py
new file mode 100644
index 000000000..6c702646f
--- /dev/null
+++ b/appium/options/android/uiautomator2/mjpeg_server_port_option.py
@@ -0,0 +1,41 @@
+# 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
+
+MJPEG_SERVER_PORT = 'mjpegServerPort'
+
+
+class MjpegServerPortOption(SupportsCapabilities):
+    @property
+    def mjpeg_server_port(self) -> Optional[int]:
+        """
+        Number of the port UiAutomator2 server starts the MJPEG server on.
+        """
+        return self.get_capability(MJPEG_SERVER_PORT)
+
+    @mjpeg_server_port.setter
+    def mjpeg_server_port(self, value: int) -> None:
+        """
+        The number of the port UiAutomator2 server starts the MJPEG server on.
+        If not provided then the screenshots broadcasting service on the remote
+        device does not get exposed to a local port (e.g. no adb port forwarding
+        is happening).
+        """
+        self.set_capability(MJPEG_SERVER_PORT, value)
diff --git a/appium/options/android/uiautomator2/skip_device_initialization_option.py b/appium/options/android/uiautomator2/skip_device_initialization_option.py
new file mode 100644
index 000000000..eda37be30
--- /dev/null
+++ b/appium/options/android/uiautomator2/skip_device_initialization_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SKIP_DEVICE_INITIALIZATION = 'skipDeviceInitialization'
+
+
+class SkipDeviceInitializationOption(SupportsCapabilities):
+    @property
+    def skip_device_initialization(self) -> Optional[bool]:
+        """
+        Whether initial device startup checks by the server are disabled.
+        """
+        return self.get_capability(SKIP_DEVICE_INITIALIZATION)
+
+    @skip_device_initialization.setter
+    def skip_device_initialization(self, value: bool) -> None:
+        """
+        If set to true then device startup checks (whether it is ready and whether
+        Settings app is installed) will be canceled on session creation.
+        Could speed up the session creation if you know what you are doing. false by default
+        """
+        self.set_capability(SKIP_DEVICE_INITIALIZATION, value)
diff --git a/appium/options/android/uiautomator2/skip_server_installation_option.py b/appium/options/android/uiautomator2/skip_server_installation_option.py
new file mode 100644
index 000000000..c343f78f1
--- /dev/null
+++ b/appium/options/android/uiautomator2/skip_server_installation_option.py
@@ -0,0 +1,44 @@
+# 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
+
+SKIP_SERVER_INSTALLATION = 'skipServerInstallation'
+
+
+class SkipServerInstallationOption(SupportsCapabilities):
+    @property
+    def skip_server_installation(self) -> Optional[bool]:
+        """
+        Whether to skip the server components installation
+        on the device under test and all the related checks.
+        """
+        return self.get_capability(SKIP_SERVER_INSTALLATION)
+
+    @skip_server_installation.setter
+    def skip_server_installation(self, value: bool) -> None:
+        """
+        Set whether to skip the server components installation
+        on the device under test and all the related checks.
+        This could help to speed up the session startup if you know for sure the
+        correct server version is installed on the device.
+        In case the server is not installed or an incorrect version of it is installed
+        then you may get an unexpected error later.
+        """
+        self.set_capability(SKIP_SERVER_INSTALLATION, value)
diff --git a/appium/options/android/uiautomator2/uiautomator2_server_install_timeout_option.py b/appium/options/android/uiautomator2/uiautomator2_server_install_timeout_option.py
new file mode 100644
index 000000000..4b25638f8
--- /dev/null
+++ b/appium/options/android/uiautomator2/uiautomator2_server_install_timeout_option.py
@@ -0,0 +1,44 @@
+# 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
+
+UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT = 'uiautomator2ServerInstallTimeout'
+
+
+class Uiautomator2ServerInstallTimeoutOption(SupportsCapabilities):
+    @property
+    def uiautomator2_server_install_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout to wait until UiAutomator2 server is installed on the device.
+        """
+        value = self.get_capability(UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @uiautomator2_server_install_timeout.setter
+    def uiautomator2_server_install_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Set the maximum timeout to wait util UiAutomator2 server is installed on the device.
+        20000 ms by default
+        """
+        self.set_capability(
+            UIAUTOMATOR2_SERVER_INSTALL_TIMEOUT,
+            int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value,
+        )
diff --git a/appium/options/android/uiautomator2/uiautomator2_server_launch_timeout_option.py b/appium/options/android/uiautomator2/uiautomator2_server_launch_timeout_option.py
new file mode 100644
index 000000000..ce74c4473
--- /dev/null
+++ b/appium/options/android/uiautomator2/uiautomator2_server_launch_timeout_option.py
@@ -0,0 +1,44 @@
+# 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
+
+UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT = 'uiautomator2ServerLaunchTimeout'
+
+
+class Uiautomator2ServerLaunchTimeoutOption(SupportsCapabilities):
+    @property
+    def uiautomator2_server_launch_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout to wait until UiAutomator2 server is listening on the device.
+        """
+        value = self.get_capability(UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @uiautomator2_server_launch_timeout.setter
+    def uiautomator2_server_launch_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Set the maximum timeout to wait util UiAutomator2Server is listening on
+        the device. 30000 ms by default
+        """
+        self.set_capability(
+            UIAUTOMATOR2_SERVER_LAUNCH_TIMEOUT,
+            int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value,
+        )
diff --git a/appium/options/android/uiautomator2/uiautomator2_server_read_timeout_option.py b/appium/options/android/uiautomator2/uiautomator2_server_read_timeout_option.py
new file mode 100644
index 000000000..cb8a39c25
--- /dev/null
+++ b/appium/options/android/uiautomator2/uiautomator2_server_read_timeout_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 datetime import timedelta
+from typing import Optional, Union
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+UIAUTOMATOR2_SERVER_READ_TIMEOUT = 'uiautomator2ServerReadTimeout'
+
+
+class Uiautomator2ServerReadTimeoutOption(SupportsCapabilities):
+    @property
+    def uiautomator2_server_read_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout to wait for an HTTP response from UiAutomator2Server.
+        """
+        value = self.get_capability(UIAUTOMATOR2_SERVER_READ_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @uiautomator2_server_read_timeout.setter
+    def uiautomator2_server_read_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Set the maximum timeout to wait for a HTTP response from UiAutomator2Server.
+        Only values greater than zero are accepted. If the given value is too low
+        then expect driver commands to fail with timeout of Xms exceeded error.
+        240000 ms by default
+        """
+        self.set_capability(
+            UIAUTOMATOR2_SERVER_READ_TIMEOUT,
+            int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value,
+        )
diff --git a/appium/options/common/__init__.py b/appium/options/common/__init__.py
new file mode 100644
index 000000000..a7a37ea6f
--- /dev/null
+++ b/appium/options/common/__init__.py
@@ -0,0 +1 @@
+from .base import AppiumOptions
diff --git a/appium/options/common/app_option.py b/appium/options/common/app_option.py
new file mode 100644
index 000000000..4a8fff1b1
--- /dev/null
+++ b/appium/options/common/app_option.py
@@ -0,0 +1,41 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+APP = 'app'
+
+
+class AppOption(SupportsCapabilities):
+    @property
+    def app(self) -> Optional[str]:
+        """
+        String representing app location.
+        """
+        return self.get_capability(APP)
+
+    @app.setter
+    def app(self, value: str) -> None:
+        """
+        Set the absolute local path for the location of the App.
+        The app must be located on the same machine where Appium
+        server is running.
+        Could also be a valid URL.
+        """
+        self.set_capability(APP, value)
diff --git a/appium/options/common/auto_web_view_option.py b/appium/options/common/auto_web_view_option.py
new file mode 100644
index 000000000..6983aef47
--- /dev/null
+++ b/appium/options/common/auto_web_view_option.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 Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+AUTO_WEB_VIEW = 'autoWebView'
+
+
+class AutoWebViewOption(SupportsCapabilities):
+    @property
+    def auto_web_view(self) -> Optional[bool]:
+        """
+        Whether the driver should try to automatically switch
+        to a web view context after the session is started.
+        """
+        return self.get_capability(AUTO_WEB_VIEW)
+
+    @auto_web_view.setter
+    def auto_web_view(self, value: bool) -> None:
+        """
+        Set whether the driver should try to automatically switch
+        a web view context after the session is started.
+        """
+        self.set_capability(AUTO_WEB_VIEW, value)
diff --git a/appium/options/common/automation_name_option.py b/appium/options/common/automation_name_option.py
new file mode 100644
index 000000000..c18e0a15b
--- /dev/null
+++ b/appium/options/common/automation_name_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+AUTOMATION_NAME = 'automationName'
+
+
+class AutomationNameOption(SupportsCapabilities):
+    @property
+    def automation_name(self) -> Optional[str]:
+        """
+        String representing the name of the automation engine name.
+        """
+        return self.get_capability(AUTOMATION_NAME)
+
+    @automation_name.setter
+    def automation_name(self, value: str) -> None:
+        """
+        Set the automation driver name to use for the given platform.
+        """
+        self.set_capability(AUTOMATION_NAME, value)
diff --git a/appium/options/common/base.py b/appium/options/common/base.py
new file mode 100644
index 000000000..17bccb61a
--- /dev/null
+++ b/appium/options/common/base.py
@@ -0,0 +1,125 @@
+# 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.
+
+import copy
+from typing import Any, Dict, TypeVar
+
+from selenium.webdriver.common.options import BaseOptions
+
+from .automation_name_option import AutomationNameOption
+from .browser_name_option import BROWSER_NAME, BrowserNameOption
+from .event_timings_option import EventTimingsOption
+from .full_reset_option import FullResetOption
+from .new_command_timeout_option import NewCommandTimeoutOption
+from .no_reset_option import NoResetOption
+from .print_page_source_on_find_failure_option import PrintPageSourceOnFindFailureOption
+
+APPIUM_PREFIX = 'appium:'
+T = TypeVar('T', bound='AppiumOptions')
+PLATFORM_NAME = 'platformName'
+
+
+class AppiumOptions(
+    BaseOptions,
+    BrowserNameOption,
+    AutomationNameOption,
+    EventTimingsOption,
+    PrintPageSourceOnFindFailureOption,
+    NoResetOption,
+    FullResetOption,
+    NewCommandTimeoutOption,
+):
+    _caps: Dict
+    W3C_CAPABILITY_NAMES = frozenset(
+        [
+            'acceptInsecureCerts',
+            BROWSER_NAME,
+            'browserVersion',
+            PLATFORM_NAME,
+            'pageLoadStrategy',
+            'proxy',
+            'setWindowRect',
+            'timeouts',
+            'unhandledPromptBehavior',
+        ]
+    )
+    _OSS_W3C_CONVERSION = {
+        'acceptSslCerts': 'acceptInsecureCerts',
+        'version': 'browserVersion',
+        'platform': PLATFORM_NAME,
+    }
+
+    # noinspection PyMissingConstructor
+    def __init__(self) -> None:
+        self._caps = self.default_capabilities
+        # FIXME: https://github.com/SeleniumHQ/selenium/issues/10755
+        self._ignore_local_proxy = False
+
+    def set_capability(self: T, name: str, value: Any) -> T:  # type: ignore[override]
+        w3c_name = name if name in self.W3C_CAPABILITY_NAMES or ':' in name else f'{APPIUM_PREFIX}{name}'
+        if value is None:
+            if w3c_name in self._caps:
+                del self._caps[w3c_name]
+        else:
+            self._caps[w3c_name] = value
+        return self
+
+    def get_capability(self, name: str) -> Any:
+        """Fetches capability value or None if the capability is not set"""
+        return self._caps[name] if name in self._caps else self._caps.get(f'{APPIUM_PREFIX}{name}')
+
+    def load_capabilities(self: T, caps: Dict[str, Any]) -> T:
+        """Sets multiple capabilities"""
+        for name, value in caps.items():
+            self.set_capability(name, value)
+        return self
+
+    @staticmethod
+    def as_w3c(capabilities: Dict) -> Dict:
+        """
+        Formats given capabilities to a valid W3C session request object
+
+        Args:
+            capabilities: Capabilities mapping
+
+        Returns:
+            W3C session request object
+        """
+
+        def process_key(k: str) -> str:
+            key = AppiumOptions._OSS_W3C_CONVERSION.get(k, k)
+            if key in AppiumOptions.W3C_CAPABILITY_NAMES:
+                return key
+            return key if ':' in key else f'{APPIUM_PREFIX}{key}'
+
+        processed_caps = {process_key(k): v for k, v in copy.deepcopy(capabilities).items()}
+        return {'capabilities': {'firstMatch': [{}], 'alwaysMatch': processed_caps}}
+
+    def to_w3c(self) -> Dict:
+        """
+        Formats the instance to a valid W3C session request object
+
+        :return: W3C session request object
+        """
+        return self.as_w3c(self.to_capabilities())
+
+    def to_capabilities(self) -> Dict:
+        return copy.copy(self._caps)
+
+    @property
+    def default_capabilities(self) -> Dict:
+        return {}
diff --git a/appium/options/common/browser_name_option.py b/appium/options/common/browser_name_option.py
new file mode 100644
index 000000000..2b107972a
--- /dev/null
+++ b/appium/options/common/browser_name_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+BROWSER_NAME = 'browserName'
+
+
+class BrowserNameOption(SupportsCapabilities):
+    @property
+    def browser_name(self) -> Optional[str]:
+        """
+        The name of the browser to run the test on.
+        """
+        return self.get_capability(BROWSER_NAME)
+
+    @browser_name.setter
+    def browser_name(self, value: str) -> None:
+        """
+        Set the name of the browser to run the test on.
+        """
+        self.set_capability(BROWSER_NAME, value)
diff --git a/appium/options/common/bundle_id_option.py b/appium/options/common/bundle_id_option.py
new file mode 100644
index 000000000..0546571a5
--- /dev/null
+++ b/appium/options/common/bundle_id_option.py
@@ -0,0 +1,38 @@
+# 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
+
+BUNDLE_ID = 'bundleId'
+
+
+class BundleIdOption(SupportsCapabilities):
+    @property
+    def bundle_id(self) -> Optional[str]:
+        """
+        The bundle identifier of the application to automate.
+        """
+        return self.get_capability(BUNDLE_ID)
+
+    @bundle_id.setter
+    def bundle_id(self, value: str) -> None:
+        """
+        Set the bundle identifier of the application to automate.
+        """
+        self.set_capability(BUNDLE_ID, value)
diff --git a/appium/options/common/clear_system_files_option.py b/appium/options/common/clear_system_files_option.py
new file mode 100644
index 000000000..208c4bb96
--- /dev/null
+++ b/appium/options/common/clear_system_files_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+CLEAR_SYSTEM_FILES = 'clearSystemFiles'
+
+
+class ClearSystemFilesOption(SupportsCapabilities):
+    @property
+    def clear_system_files(self) -> Optional[bool]:
+        """
+        Whether the driver should delete generated files at the end of a session.
+        """
+        return self.get_capability(CLEAR_SYSTEM_FILES)
+
+    @clear_system_files.setter
+    def clear_system_files(self, value: bool) -> None:
+        """
+        Set whether the driver should delete generated files at the end of a session.
+        """
+        self.set_capability(CLEAR_SYSTEM_FILES, value)
diff --git a/appium/options/common/device_name_option.py b/appium/options/common/device_name_option.py
new file mode 100644
index 000000000..8e33cce39
--- /dev/null
+++ b/appium/options/common/device_name_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+DEVICE_NAME = 'deviceName'
+
+
+class DeviceNameOption(SupportsCapabilities):
+    @property
+    def device_name(self) -> Optional[str]:
+        """
+        The name of the device.
+        """
+        return self.get_capability(DEVICE_NAME)
+
+    @device_name.setter
+    def device_name(self, value: str) -> None:
+        """
+        Set the name of the device to be used in the test.
+        """
+        self.set_capability(DEVICE_NAME, value)
diff --git a/appium/options/common/enable_performance_logging_option.py b/appium/options/common/enable_performance_logging_option.py
new file mode 100644
index 000000000..22a4bdb30
--- /dev/null
+++ b/appium/options/common/enable_performance_logging_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+ENABLE_PERFORMANCE_LOGGING = 'enablePerformanceLogging'
+
+
+class EnablePerformanceLoggingOption(SupportsCapabilities):
+    @property
+    def enable_performance_logging(self) -> Optional[bool]:
+        """
+        Whether to enable additional performance logging.
+        """
+        return self.get_capability(ENABLE_PERFORMANCE_LOGGING)
+
+    @enable_performance_logging.setter
+    def enable_performance_logging(self, value: bool) -> None:
+        """
+        Set whether to enable additional performance logging.
+        """
+        self.set_capability(ENABLE_PERFORMANCE_LOGGING, value)
diff --git a/appium/options/common/event_timings_option.py b/appium/options/common/event_timings_option.py
new file mode 100644
index 000000000..b9c0a1245
--- /dev/null
+++ b/appium/options/common/event_timings_option.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 Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+EVENT_TIMINGS = 'eventTimings'
+
+
+class EventTimingsOption(SupportsCapabilities):
+    @property
+    def event_timings(self) -> Optional[bool]:
+        """
+        Whether the driver should to report the timings
+        for various Appium-internal events.
+        """
+        return self.get_capability(EVENT_TIMINGS)
+
+    @event_timings.setter
+    def event_timings(self, value: bool) -> None:
+        """
+        Set whether the driver should to report the timings
+        for various Appium-internal events.
+        """
+        self.set_capability(EVENT_TIMINGS, value)
diff --git a/appium/options/common/full_reset_option.py b/appium/options/common/full_reset_option.py
new file mode 100644
index 000000000..aee86d7db
--- /dev/null
+++ b/appium/options/common/full_reset_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+FULL_RESET = 'fullReset'
+
+
+class FullResetOption(SupportsCapabilities):
+    @property
+    def full_reset(self) -> Optional[bool]:
+        """
+        Whether the driver should perform a full reset.
+        """
+        return self.get_capability(FULL_RESET)
+
+    @full_reset.setter
+    def full_reset(self, value: bool) -> None:
+        """
+        Set whether the driver should perform a full reset.
+        """
+        self.set_capability(FULL_RESET, value)
diff --git a/appium/options/common/is_headless_option.py b/appium/options/common/is_headless_option.py
new file mode 100644
index 000000000..bc0c2b110
--- /dev/null
+++ b/appium/options/common/is_headless_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 typing import Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+IS_HEADLESS = 'isHeadless'
+
+
+class IsHeadlessOption(SupportsCapabilities):
+    @property
+    def is_headless(self) -> Optional[bool]:
+        """
+        Whether the driver should start emulator/simulator in headless mode.
+        """
+        return self.get_capability(IS_HEADLESS)
+
+    @is_headless.setter
+    def is_headless(self, value: bool) -> None:
+        """
+        Set emulator/simulator to start in headless mode (e.g. no UI is shown).
+        It is only applied if the emulator is not running before the test starts.
+        """
+        self.set_capability(IS_HEADLESS, value)
diff --git a/appium/options/common/language_option.py b/appium/options/common/language_option.py
new file mode 100644
index 000000000..f82de63d0
--- /dev/null
+++ b/appium/options/common/language_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+LANGUAGE = 'language'
+
+
+class LanguageOption(SupportsCapabilities):
+    @property
+    def language(self) -> Optional[str]:
+        """
+        Language abbreviation to use in a test session.
+        """
+        return self.get_capability(LANGUAGE)
+
+    @language.setter
+    def language(self, value: str) -> None:
+        """
+        Set language abbreviation to use in a test session.
+        """
+        self.set_capability(LANGUAGE, value)
diff --git a/appium/options/common/locale_option.py b/appium/options/common/locale_option.py
new file mode 100644
index 000000000..58503a890
--- /dev/null
+++ b/appium/options/common/locale_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+LOCALE = 'locale'
+
+
+class LocaleOption(SupportsCapabilities):
+    @property
+    def locale(self) -> Optional[str]:
+        """
+        Locale abbreviation to use in a test session.
+        """
+        return self.get_capability(LOCALE)
+
+    @locale.setter
+    def locale(self, value: str) -> None:
+        """
+        Set locale abbreviation to use in a test session.
+        """
+        self.set_capability(LOCALE, value)
diff --git a/appium/options/common/new_command_timeout_option.py b/appium/options/common/new_command_timeout_option.py
new file mode 100644
index 000000000..377ef7193
--- /dev/null
+++ b/appium/options/common/new_command_timeout_option.py
@@ -0,0 +1,41 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+NEW_COMMAND_TIMEOUT = 'newCommandTimeout'
+
+
+class NewCommandTimeoutOption(SupportsCapabilities):
+    @property
+    def new_command_timeout(self) -> Optional[timedelta]:
+        """
+        The allowed time before seeing a new server command.
+        """
+        value = self.get_capability(NEW_COMMAND_TIMEOUT)
+        return None if value is None else timedelta(seconds=value)
+
+    @new_command_timeout.setter
+    def new_command_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Set the allowed time before seeing a new server command.
+        The value could either be provided as timedelta instance or an integer number of seconds.
+        """
+        self.set_capability(NEW_COMMAND_TIMEOUT, value.total_seconds() if isinstance(value, timedelta) else value)
diff --git a/appium/options/common/no_reset_option.py b/appium/options/common/no_reset_option.py
new file mode 100644
index 000000000..ee2dd9988
--- /dev/null
+++ b/appium/options/common/no_reset_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+NO_RESET = 'noReset'
+
+
+class NoResetOption(SupportsCapabilities):
+    @property
+    def no_reset(self) -> Optional[bool]:
+        """
+        Whether the driver should not perform a reset.
+        """
+        return self.get_capability(NO_RESET)
+
+    @no_reset.setter
+    def no_reset(self, value: bool) -> None:
+        """
+        Set whether the driver should not perform a reset.
+        """
+        self.set_capability(NO_RESET, value)
diff --git a/appium/options/common/orientation_option.py b/appium/options/common/orientation_option.py
new file mode 100644
index 000000000..4141a4d90
--- /dev/null
+++ b/appium/options/common/orientation_option.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 Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+ORIENTATION = 'orientation'
+
+
+class OrientationOption(SupportsCapabilities):
+    @property
+    def orientation(self) -> Optional[str]:
+        """
+        The orientation of the device's screen.
+        Usually this is either 'PORTRAIT' or 'LANDSCAPE'.
+        """
+        return self.get_capability(ORIENTATION)
+
+    @orientation.setter
+    def orientation(self, value: str) -> None:
+        """
+        Set the orientation of the device's screen.
+        Usually this is either 'PORTRAIT' or 'LANDSCAPE'.
+        """
+        self.set_capability(ORIENTATION, value)
diff --git a/appium/options/common/other_apps_option.py b/appium/options/common/other_apps_option.py
new file mode 100644
index 000000000..6efbbbc4e
--- /dev/null
+++ b/appium/options/common/other_apps_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 typing import Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+OTHER_APPS = 'otherApps'
+
+
+class OtherAppsOption(SupportsCapabilities):
+    @property
+    def other_apps(self) -> Optional[str]:
+        """
+        Locations of apps to install before running a test.
+        """
+        return self.get_capability(OTHER_APPS)
+
+    @other_apps.setter
+    def other_apps(self, value: str) -> None:
+        """
+        Set locations of apps to install before running a test.
+        Each item could be separated with a single comma.
+        """
+        self.set_capability(OTHER_APPS, value)
diff --git a/appium/options/common/platform_version_option.py b/appium/options/common/platform_version_option.py
new file mode 100644
index 000000000..d237cc280
--- /dev/null
+++ b/appium/options/common/platform_version_option.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 Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+PLATFORM_VERSION = 'platformVersion'
+
+
+class PlatformVersionOption(SupportsCapabilities):
+    @property
+    def platform_version(self) -> Optional[str]:
+        """
+        The platform version of an emulator or a real device.
+        This capability is used for device autodetection if udid is not provided.
+        """
+        return self.get_capability(PLATFORM_VERSION)
+
+    @platform_version.setter
+    def platform_version(self, value: str) -> None:
+        """
+        Set the platform version of an emulator or a real device.
+        This capability is used for device autodetection if udid is not provided.
+        """
+        self.set_capability(PLATFORM_VERSION, value)
diff --git a/appium/options/common/postrun_option.py b/appium/options/common/postrun_option.py
new file mode 100644
index 000000000..f940cd6b6
--- /dev/null
+++ b/appium/options/common/postrun_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 typing import Dict, Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+POSTRUN = 'postrun'
+
+
+class PostrunOption(SupportsCapabilities):
+    @property
+    def postrun(self) -> Optional[Dict[str, str]]:
+        """
+        System script which is supposed to be executed upon
+        driver session quit.
+        """
+        return self.get_capability(POSTRUN)
+
+    @postrun.setter
+    def postrun(self, value: Dict[str, str]) -> None:
+        """
+        Set a system script to execute upon driver session quit.
+        """
+        self.set_capability(POSTRUN, value)
diff --git a/appium/options/common/prerun_option.py b/appium/options/common/prerun_option.py
new file mode 100644
index 000000000..139874c76
--- /dev/null
+++ b/appium/options/common/prerun_option.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, Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+PRERUN = 'prerun'
+
+
+class PrerunOption(SupportsCapabilities):
+    @property
+    def prerun(self) -> Optional[Dict[str, str]]:
+        """
+        System script which is supposed to be executed before
+        a driver session is initialised.
+        """
+        return self.get_capability(PRERUN)
+
+    @prerun.setter
+    def prerun(self, value: Dict[str, str]) -> None:
+        """
+        Set a system script which is supposed to be executed before
+        a driver session is initialised.
+        """
+        self.set_capability(PRERUN, value)
diff --git a/appium/options/common/print_page_source_on_find_failure_option.py b/appium/options/common/print_page_source_on_find_failure_option.py
new file mode 100644
index 000000000..b93f07cf5
--- /dev/null
+++ b/appium/options/common/print_page_source_on_find_failure_option.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 Optional
+
+from .supports_capabilities import SupportsCapabilities
+
+PRINT_PAGE_SOURCE_ON_FIND_FAILURE = 'printPageSourceOnFindFailure'
+
+
+class PrintPageSourceOnFindFailureOption(SupportsCapabilities):
+    @property
+    def print_page_source_on_find_failure(self) -> Optional[bool]:
+        """
+        Whether the driver should print the page source to the log
+        if a find failure occurs.
+        """
+        return self.get_capability(PRINT_PAGE_SOURCE_ON_FIND_FAILURE)
+
+    @print_page_source_on_find_failure.setter
+    def print_page_source_on_find_failure(self, value: bool) -> None:
+        """
+        Set whether the driver should print the page source to the log
+        if a find failure occurs.
+        """
+        self.set_capability(PRINT_PAGE_SOURCE_ON_FIND_FAILURE, value)
diff --git a/appium/options/common/skip_log_capture_option.py b/appium/options/common/skip_log_capture_option.py
new file mode 100644
index 000000000..668704e13
--- /dev/null
+++ b/appium/options/common/skip_log_capture_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+SKIP_LOG_CAPTURE = 'skipLogCapture'
+
+
+class SkipLogCaptureOption(SupportsCapabilities):
+    @property
+    def skip_log_capture(self) -> Optional[bool]:
+        """
+        Whether the driver should not record device logs.
+        """
+        return self.get_capability(SKIP_LOG_CAPTURE)
+
+    @skip_log_capture.setter
+    def skip_log_capture(self, value: bool) -> None:
+        """
+        Set whether the driver should not record device logs.
+        """
+        self.set_capability(SKIP_LOG_CAPTURE, value)
diff --git a/appium/options/common/supports_capabilities.py b/appium/options/common/supports_capabilities.py
new file mode 100644
index 000000000..a8e7fb29c
--- /dev/null
+++ b/appium/options/common/supports_capabilities.py
@@ -0,0 +1,26 @@
+# 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 Any, Protocol, TypeVar
+
+T = TypeVar('T')
+
+
+class SupportsCapabilities(Protocol):
+    def set_capability(self: T, name: str, value: Any) -> T: ...
+
+    def get_capability(self: T, name: str) -> Any: ...
diff --git a/appium/options/common/system_host_option.py b/appium/options/common/system_host_option.py
new file mode 100644
index 000000000..0544de5ca
--- /dev/null
+++ b/appium/options/common/system_host_option.py
@@ -0,0 +1,38 @@
+# 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
+
+SYSTEM_HOST = 'systemHost'
+
+
+class SystemHostOption(SupportsCapabilities):
+    @property
+    def system_host(self) -> Optional[str]:
+        """
+        The name of the host for the internal server to listen on.
+        """
+        return self.get_capability(SYSTEM_HOST)
+
+    @system_host.setter
+    def system_host(self, value: str) -> None:
+        """
+        Set the name of the host for the internal server to listen on.
+        """
+        self.set_capability(SYSTEM_HOST, value)
diff --git a/appium/options/common/system_port_option.py b/appium/options/common/system_port_option.py
new file mode 100644
index 000000000..e11f3b59d
--- /dev/null
+++ b/appium/options/common/system_port_option.py
@@ -0,0 +1,38 @@
+# 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
+
+SYSTEM_PORT = 'systemPort'
+
+
+class SystemPortOption(SupportsCapabilities):
+    @property
+    def system_port(self) -> Optional[int]:
+        """
+        The number of the port for the internal server to listen on.
+        """
+        return self.get_capability(SYSTEM_PORT)
+
+    @system_port.setter
+    def system_port(self, value: int) -> None:
+        """
+        Set the number of the port for the internal server to listen on.
+        """
+        self.set_capability(SYSTEM_PORT, value)
diff --git a/appium/options/common/udid_option.py b/appium/options/common/udid_option.py
new file mode 100644
index 000000000..f760a1d21
--- /dev/null
+++ b/appium/options/common/udid_option.py
@@ -0,0 +1,38 @@
+# 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 .supports_capabilities import SupportsCapabilities
+
+UDID = 'udid'
+
+
+class UdidOption(SupportsCapabilities):
+    @property
+    def udid(self) -> Optional[str]:
+        """
+        The unique identifier of the device under test.
+        """
+        return self.get_capability(UDID)
+
+    @udid.setter
+    def udid(self, value: str) -> None:
+        """
+        Set the unique identifier of the device under test.
+        """
+        self.set_capability(UDID, value)
diff --git a/appium/options/flutter_integration/__init__.py b/appium/options/flutter_integration/__init__.py
new file mode 100644
index 000000000..ba044a8aa
--- /dev/null
+++ b/appium/options/flutter_integration/__init__.py
@@ -0,0 +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/options/flutter_integration/base.py b/appium/options/flutter_integration/base.py
new file mode 100644
index 000000000..f67b9a93c
--- /dev/null
+++ b/appium/options/flutter_integration/base.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 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..bd4279774
--- /dev/null
+++ b/appium/options/flutter_integration/flutter_element_wait_timeout_option.py
@@ -0,0 +1,50 @@
+# 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..b90cb0011
--- /dev/null
+++ b/appium/options/flutter_integration/flutter_enable_mock_camera_option.py
@@ -0,0 +1,44 @@
+# 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 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..32cea2f94
--- /dev/null
+++ b/appium/options/flutter_integration/flutter_server_launch_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_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..db7ab7f54
--- /dev/null
+++ b/appium/options/flutter_integration/flutter_system_port_option.py
@@ -0,0 +1,45 @@
+# 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/options/gecko/__init__.py b/appium/options/gecko/__init__.py
new file mode 100644
index 000000000..1dfd32877
--- /dev/null
+++ b/appium/options/gecko/__init__.py
@@ -0,0 +1 @@
+from .base import GeckoOptions
diff --git a/appium/options/gecko/android_storage_option.py b/appium/options/gecko/android_storage_option.py
new file mode 100644
index 000000000..1209ac501
--- /dev/null
+++ b/appium/options/gecko/android_storage_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ANDROID_STORAGE = 'androidStorage'
+
+
+class AndroidStorageOption(SupportsCapabilities):
+    @property
+    def android_storage(self) -> Optional[str]:
+        """
+        The currently set storage type.
+        """
+        return self.get_capability(ANDROID_STORAGE)
+
+    @android_storage.setter
+    def android_storage(self, value: str) -> None:
+        """
+        See https://firefox-source-docs.mozilla.org/testing/geckodriver
+        /Flags.html#code-android-storage-var-android-storage-var-code
+        """
+        self.set_capability(ANDROID_STORAGE, value)
diff --git a/appium/options/gecko/base.py b/appium/options/gecko/base.py
new file mode 100644
index 000000000..873e5646a
--- /dev/null
+++ b/appium/options/gecko/base.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 typing import Dict
+
+from appium.options.common.automation_name_option import AUTOMATION_NAME
+from appium.options.common.base import AppiumOptions
+from appium.options.common.system_port_option import SystemPortOption
+
+from .android_storage_option import AndroidStorageOption
+from .firefox_options_option import FirefoxOptionsOption
+from .marionette_port_option import MarionettePortOption
+from .verbosity_option import VerbosityOption
+
+
+class GeckoOptions(
+    AppiumOptions,
+    AndroidStorageOption,
+    FirefoxOptionsOption,
+    MarionettePortOption,
+    SystemPortOption,
+    VerbosityOption,
+):
+    @SystemPortOption.system_port.setter  # type: ignore
+    def system_port(self, value: int) -> None:
+        """
+        The number of the port for the driver to listen on. Must be unique
+        for each session. If not provided then the driver will try to detect
+        it automatically.
+        """
+        SystemPortOption.system_port.fset(self, value)  # type: ignore
+
+    @property
+    def default_capabilities(self) -> Dict:
+        return {
+            AUTOMATION_NAME: 'Gecko',
+        }
diff --git a/appium/options/gecko/firefox_options_option.py b/appium/options/gecko/firefox_options_option.py
new file mode 100644
index 000000000..87906b4bb
--- /dev/null
+++ b/appium/options/gecko/firefox_options_option.py
@@ -0,0 +1,38 @@
+# 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 Any, Dict, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+FIREFOX_OPTIONS = 'moz:firefoxOptions'
+
+
+class FirefoxOptionsOption(SupportsCapabilities):
+    @property
+    def firefox_options(self) -> Optional[Dict[str, Any]]:
+        """
+        Firefox options mapping.
+        """
+        return self.get_capability(FIREFOX_OPTIONS)
+
+    @firefox_options.setter
+    def firefox_options(self, value: Dict[str, Any]) -> None:
+        """
+        See https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
+        """
+        self.set_capability(FIREFOX_OPTIONS, value)
diff --git a/appium/options/gecko/marionette_port_option.py b/appium/options/gecko/marionette_port_option.py
new file mode 100644
index 000000000..24846fd76
--- /dev/null
+++ b/appium/options/gecko/marionette_port_option.py
@@ -0,0 +1,43 @@
+# 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
+
+MARIONETTE_PORT = 'marionettePort'
+
+
+class MarionettePortOption(SupportsCapabilities):
+    @property
+    def marionette_port(self) -> Optional[int]:
+        """
+        The number of the port for the Marionette server to listen on.
+        """
+        return self.get_capability(MARIONETTE_PORT)
+
+    @marionette_port.setter
+    def marionette_port(self, value: int) -> None:
+        """
+        Selects the port for Geckodriver’s connection to the Marionette
+        remote protocol. The existing Firefox instance must have Marionette
+        enabled. To enable the remote protocol in Firefox, you can pass the
+        -marionette flag. Unless the marionette.port preference has been
+        user-set, Marionette will listen on port 2828, which is the default
+        value for this capability.
+        """
+        self.set_capability(MARIONETTE_PORT, value)
diff --git a/appium/options/gecko/verbosity_option.py b/appium/options/gecko/verbosity_option.py
new file mode 100644
index 000000000..9888ddf2d
--- /dev/null
+++ b/appium/options/gecko/verbosity_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+VERBOSITY = 'verbosity'
+
+
+class VerbosityOption(SupportsCapabilities):
+    @property
+    def verbosity(self) -> Optional[str]:
+        """
+        The verbosity level of driver logging.
+        """
+        return self.get_capability(VERBOSITY)
+
+    @verbosity.setter
+    def verbosity(self, value: str) -> None:
+        """
+        The verbosity level of driver logging.
+        By default, minimum verbosity is applied.
+        Either 'debug' or 'trace'.
+        """
+        self.set_capability(VERBOSITY, value)
diff --git a/appium/options/ios/__init__.py b/appium/options/ios/__init__.py
new file mode 100644
index 000000000..21e82f751
--- /dev/null
+++ b/appium/options/ios/__init__.py
@@ -0,0 +1,2 @@
+from .safari.base import SafariOptions
+from .xcuitest.base import XCUITestOptions
diff --git a/appium/options/ios/safari/__init__.py b/appium/options/ios/safari/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/safari/automatic_inspection_option.py b/appium/options/ios/safari/automatic_inspection_option.py
new file mode 100644
index 000000000..3ab38e82d
--- /dev/null
+++ b/appium/options/ios/safari/automatic_inspection_option.py
@@ -0,0 +1,41 @@
+# 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
+
+AUTOMATIC_INSPECTION = 'safari:automaticInspection'
+
+
+class AutomaticInspectionOption(SupportsCapabilities):
+    @property
+    def automatic_inspection(self) -> Optional[bool]:
+        """
+        Whether to use automatic inspection.
+        """
+        return self.get_capability(AUTOMATIC_INSPECTION)
+
+    @automatic_inspection.setter
+    def automatic_inspection(self, value: bool) -> None:
+        """
+        This capability instructs Safari to preload the Web Inspector and JavaScript
+        debugger in the background prior to returning a newly-created window.
+        To pause the test's execution in JavaScript and bring up Web Inspector's
+        Debugger tab, you can simply evaluate a debugger statement in the test page.
+        """
+        self.set_capability(AUTOMATIC_INSPECTION, value)
diff --git a/appium/options/ios/safari/automatic_profiling_option.py b/appium/options/ios/safari/automatic_profiling_option.py
new file mode 100644
index 000000000..76eae6206
--- /dev/null
+++ b/appium/options/ios/safari/automatic_profiling_option.py
@@ -0,0 +1,41 @@
+# 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
+
+AUTOMATIC_PROFILING = 'safari:automaticProfiling'
+
+
+class AutomaticProfilingOption(SupportsCapabilities):
+    @property
+    def automatic_profiling(self) -> Optional[bool]:
+        """
+        Whether to use automatic profiling.
+        """
+        return self.get_capability(AUTOMATIC_PROFILING)
+
+    @automatic_profiling.setter
+    def automatic_profiling(self, value: bool) -> None:
+        """
+        This capability instructs Safari to preload the Web Inspector and start
+        a Timeline recording in the background prior to returning a newly-created
+        window. To view the recording, open the Web Inspector through Safari's
+        Develop menu.
+        """
+        self.set_capability(AUTOMATIC_PROFILING, value)
diff --git a/appium/options/ios/safari/base.py b/appium/options/ios/safari/base.py
new file mode 100644
index 000000000..8c6cfa685
--- /dev/null
+++ b/appium/options/ios/safari/base.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 typing import Dict
+
+from appium.options.common.automation_name_option import AUTOMATION_NAME
+from appium.options.common.base import PLATFORM_NAME, AppiumOptions
+
+from .automatic_inspection_option import AutomaticInspectionOption
+from .automatic_profiling_option import AutomaticProfilingOption
+from .device_name_option import DeviceNameOption
+from .device_type_option import DeviceTypeOption
+from .device_udid_option import DeviceUdidOption
+from .platform_build_version_option import PlatformBuildVersionOption
+from .platform_version_option import PlatformVersionOption
+from .use_simulator_option import UseSimulatorOption
+from .webkit_webrtc_option import WebkitWebrtcOption
+
+
+class SafariOptions(
+    AppiumOptions,
+    AutomaticInspectionOption,
+    AutomaticProfilingOption,
+    DeviceNameOption,
+    DeviceTypeOption,
+    DeviceUdidOption,
+    PlatformBuildVersionOption,
+    PlatformVersionOption,
+    UseSimulatorOption,
+    WebkitWebrtcOption,
+):
+    @property
+    def default_capabilities(self) -> Dict:
+        return {
+            PLATFORM_NAME: 'iOS',
+            AUTOMATION_NAME: 'Safari',
+        }
diff --git a/appium/options/ios/safari/device_name_option.py b/appium/options/ios/safari/device_name_option.py
new file mode 100644
index 000000000..ebc949ad8
--- /dev/null
+++ b/appium/options/ios/safari/device_name_option.py
@@ -0,0 +1,43 @@
+# 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
+
+DEVICE_NAME = 'safari:deviceName'
+
+
+class DeviceNameOption(SupportsCapabilities):
+    @property
+    def device_name(self) -> Optional[str]:
+        """
+        String representing the name of the device.
+        """
+        return self.get_capability(DEVICE_NAME)
+
+    @device_name.setter
+    def device_name(self, value: str) -> None:
+        """
+        safaridriver will only create a session using hosts whose device name
+        matches the value of safari:deviceName. Device names are compared
+        case-insensitively. NOTE: Device names for connected devices are shown in
+        iTunes. If Xcode is installed, device names for connected devices are available
+        via the output of instruments(1) and in the Devices and Simulators window
+        (accessed in Xcode via "Window -&gt; Devices and Simulators").
+        """
+        self.set_capability(DEVICE_NAME, value)
diff --git a/appium/options/ios/safari/device_type_option.py b/appium/options/ios/safari/device_type_option.py
new file mode 100644
index 000000000..329974e34
--- /dev/null
+++ b/appium/options/ios/safari/device_type_option.py
@@ -0,0 +1,41 @@
+# 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
+
+DEVICE_TYPE = 'safari:deviceType'
+
+
+class DeviceTypeOption(SupportsCapabilities):
+    @property
+    def device_type(self) -> Optional[str]:
+        """
+        String representing the type of the device.
+        """
+        return self.get_capability(DEVICE_TYPE)
+
+    @device_type.setter
+    def device_type(self, value: str) -> None:
+        """
+        If the value of safari:deviceType is 'iPhone', safaridriver will only create a session
+        using an iPhone device or iPhone simulator. If the value of safari:deviceType is 'iPad',
+        safaridriver will only create a session using an iPad device or iPad simulator.
+        Values of safari:deviceType are compared case-insensitively.
+        """
+        self.set_capability(DEVICE_TYPE, value)
diff --git a/appium/options/ios/safari/device_udid_option.py b/appium/options/ios/safari/device_udid_option.py
new file mode 100644
index 000000000..12850acfc
--- /dev/null
+++ b/appium/options/ios/safari/device_udid_option.py
@@ -0,0 +1,43 @@
+# 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
+
+DEVICE_UDID = 'safari:deviceUDID'
+
+
+class DeviceUdidOption(SupportsCapabilities):
+    @property
+    def device_udid(self) -> Optional[str]:
+        """
+        String representing the UDID of the device.
+        """
+        return self.get_capability(DEVICE_UDID)
+
+    @device_udid.setter
+    def device_udid(self, value: str) -> None:
+        """
+        safaridriver will only create a session using hosts whose device UDID
+        matches the value of safari:deviceUDID. Device UDIDs are compared
+        case-insensitively. NOTE: If Xcode is installed, UDIDs for connected
+        devices are available via the output of instruments(1) and in the
+        Devices and Simulators window (accessed in Xcode via
+        "Window -&gt; Devices and Simulators").
+        """
+        self.set_capability(DEVICE_UDID, value)
diff --git a/appium/options/ios/safari/platform_build_version_option.py b/appium/options/ios/safari/platform_build_version_option.py
new file mode 100644
index 000000000..f411ec22e
--- /dev/null
+++ b/appium/options/ios/safari/platform_build_version_option.py
@@ -0,0 +1,41 @@
+# 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
+
+PLATFORM_BUILD_VERSION = 'safari:platformBuildVersion'
+
+
+class PlatformBuildVersionOption(SupportsCapabilities):
+    @property
+    def platform_build_version(self) -> Optional[str]:
+        """
+        String representing the platform build version.
+        """
+        return self.get_capability(PLATFORM_BUILD_VERSION)
+
+    @platform_build_version.setter
+    def platform_build_version(self, value: str) -> None:
+        """
+        safaridriver will only create a session using hosts whose OS build
+        version matches the value of safari:platformBuildVersion. Example
+        of a macOS build version is '18E193'. On macOS, the OS build version
+        can be determined by running the sw_vers(1) utility.
+        """
+        self.set_capability(PLATFORM_BUILD_VERSION, value)
diff --git a/appium/options/ios/safari/platform_version_option.py b/appium/options/ios/safari/platform_version_option.py
new file mode 100644
index 000000000..fd3d4b60b
--- /dev/null
+++ b/appium/options/ios/safari/platform_version_option.py
@@ -0,0 +1,41 @@
+# 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
+
+PLATFORM_VERSION = 'safari:platformVersion'
+
+
+class PlatformVersionOption(SupportsCapabilities):
+    @property
+    def platform_version(self) -> Optional[str]:
+        """
+        String representing the platform version.
+        """
+        return self.get_capability(PLATFORM_VERSION)
+
+    @platform_version.setter
+    def platform_version(self, value: str) -> None:
+        """
+        safaridriver will only create a session using hosts whose OS
+        version matches the value of safari:platformVersion. OS version
+        numbers are prefix-matched. For example, if the value of safari:platformVersion
+        is '12', this will allow hosts with an OS version of '12.0' or '12.1' but not '10.12'.
+        """
+        self.set_capability(PLATFORM_VERSION, value)
diff --git a/appium/options/ios/safari/use_simulator_option.py b/appium/options/ios/safari/use_simulator_option.py
new file mode 100644
index 000000000..25a09a6b0
--- /dev/null
+++ b/appium/options/ios/safari/use_simulator_option.py
@@ -0,0 +1,41 @@
+# 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
+
+USE_SIMULATOR = 'safari:useSimulator'
+
+
+class UseSimulatorOption(SupportsCapabilities):
+    @property
+    def use_simulator(self) -> Optional[bool]:
+        """
+        Whether to use iOS Simulator.
+        """
+        return self.get_capability(USE_SIMULATOR)
+
+    @use_simulator.setter
+    def use_simulator(self, value: bool) -> None:
+        """
+        If the value of safari:useSimulator is true, safaridriver will only use
+        iOS Simulator hosts. If the value of safari:useSimulator is false, safaridriver
+        will not use iOS Simulator hosts. NOTE: An Xcode installation is required
+        in order to run WebDriver tests on iOS Simulator hosts.
+        """
+        self.set_capability(USE_SIMULATOR, value)
diff --git a/appium/options/ios/safari/webkit_webrtc_option.py b/appium/options/ios/safari/webkit_webrtc_option.py
new file mode 100644
index 000000000..7980df7ab
--- /dev/null
+++ b/appium/options/ios/safari/webkit_webrtc_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 typing import Any, Dict, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+WEBKIT_WEBRTC = 'webkit:WebRTC'
+
+
+class WebkitWebrtcOption(SupportsCapabilities):
+    @property
+    def webkit_webrtc(self) -> Optional[Dict[str, Any]]:
+        """
+        WebRTC policies.
+        """
+        return self.get_capability(WEBKIT_WEBRTC)
+
+    @webkit_webrtc.setter
+    def webkit_webrtc(self, value: Dict[str, Any]) -> None:
+        """
+        This option allows a test to temporarily change Safari's policies
+        for WebRTC and Media Capture.
+        The following dictionary values are supported:
+        - DisableInsecureMediaCapture: Boolean value.
+        Normally, Safari refuses to allow media capture over insecure connections.
+        This restriction is relaxed by default for WebDriver sessions for testing
+        purposes (for example, a test web server not configured for HTTPS). When
+        this capability is specified, Safari will revert to the normal behavior of
+        preventing media capture over insecure connections.
+        - DisableICECandidateFiltering: Boolean value.
+        To protect a user's privacy, Safari normally filters out WebRTC
+        ICE candidates that correspond to internal network addresses when
+        capture devices are not in use. This capability suppresses ICE candidate
+        filtering so that both internal and external network addresses are
+        always sent as ICE candidates.
+        """
+        self.set_capability(WEBKIT_WEBRTC, value)
diff --git a/appium/options/ios/xcuitest/__init__.py b/appium/options/ios/xcuitest/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/xcuitest/app/__init__.py b/appium/options/ios/xcuitest/app/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/xcuitest/app/app_install_strategy_option.py b/appium/options/ios/xcuitest/app/app_install_strategy_option.py
new file mode 100644
index 000000000..ff812fe86
--- /dev/null
+++ b/appium/options/ios/xcuitest/app/app_install_strategy_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
+
+APP_INSTALL_STRATEGY = 'appInstallStrategy'
+
+
+class AppInstallStrategyOption(SupportsCapabilities):
+    @property
+    def app_install_strategy(self) -> Optional[str]:
+        """
+        App install strategy.
+        """
+        return self.get_capability(APP_INSTALL_STRATEGY)
+
+    @app_install_strategy.setter
+    def app_install_strategy(self, value: str) -> None:
+        """
+        Select application installation strategy for real devices. The following
+        strategies are supported:
+        * serial (default) - pushes app files to the device in a sequential order;
+        this is the least performant strategy, although the most reliable;
+        * parallel - pushes app files simultaneously; this is usually the
+        most performant strategy, but sometimes could not be very stable;
+        * ios-deploy - tells the driver to use a third-party tool ios-deploy to
+        install the app; obviously the tool must be installed separately
+        first and must be present in PATH before it could be used.
+        """
+        self.set_capability(APP_INSTALL_STRATEGY, value)
diff --git a/appium/options/ios/xcuitest/app/app_push_timeout_option.py b/appium/options/ios/xcuitest/app/app_push_timeout_option.py
new file mode 100644
index 000000000..3f49bde96
--- /dev/null
+++ b/appium/options/ios/xcuitest/app/app_push_timeout_option.py
@@ -0,0 +1,42 @@
+# 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
+
+APP_PUSH_TIMEOUT = 'appPushTimeout'
+
+
+class AppPushTimeoutOption(SupportsCapabilities):
+    @property
+    def app_push_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout for application upload.
+        """
+        value = self.get_capability(APP_PUSH_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @app_push_timeout.setter
+    def app_push_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        The timeout for application upload.
+        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)
diff --git a/appium/options/ios/xcuitest/app/localizable_strings_dir_option.py b/appium/options/ios/xcuitest/app/localizable_strings_dir_option.py
new file mode 100644
index 000000000..e78155a6c
--- /dev/null
+++ b/appium/options/ios/xcuitest/app/localizable_strings_dir_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+LOCALIZABLE_STRINGS_DIR = 'localizableStringsDir'
+
+
+class LocalizableStringsDirOption(SupportsCapabilities):
+    @property
+    def localizable_strings_dir(self) -> Optional[str]:
+        """
+        Resource folder name where the main locale strings are stored.
+        """
+        return self.get_capability(LOCALIZABLE_STRINGS_DIR)
+
+    @localizable_strings_dir.setter
+    def localizable_strings_dir(self, value: str) -> None:
+        """
+        Where to look for localizable strings in the application bundle.
+        Defaults to en.lproj.
+        """
+        self.set_capability(LOCALIZABLE_STRINGS_DIR, value)
diff --git a/appium/options/ios/xcuitest/base.py b/appium/options/ios/xcuitest/base.py
new file mode 100644
index 000000000..02143a095
--- /dev/null
+++ b/appium/options/ios/xcuitest/base.py
@@ -0,0 +1,223 @@
+# 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.app_option import AppOption
+from appium.options.common.auto_web_view_option import AutoWebViewOption
+from appium.options.common.automation_name_option import AUTOMATION_NAME
+from appium.options.common.base import PLATFORM_NAME, AppiumOptions
+from appium.options.common.bundle_id_option import BundleIdOption
+from appium.options.common.clear_system_files_option import ClearSystemFilesOption
+from appium.options.common.device_name_option import DeviceNameOption
+from appium.options.common.enable_performance_logging_option import EnablePerformanceLoggingOption
+from appium.options.common.is_headless_option import IsHeadlessOption
+from appium.options.common.language_option import LanguageOption
+from appium.options.common.locale_option import LocaleOption
+from appium.options.common.orientation_option import OrientationOption
+from appium.options.common.other_apps_option import OtherAppsOption
+from appium.options.common.platform_version_option import PlatformVersionOption
+from appium.options.common.skip_log_capture_option import SkipLogCaptureOption
+from appium.options.common.udid_option import UdidOption
+
+from .app.app_install_strategy_option import AppInstallStrategyOption
+from .app.app_push_timeout_option import AppPushTimeoutOption
+from .app.localizable_strings_dir_option import LocalizableStringsDirOption
+from .general.include_device_caps_to_session_info_option import IncludeDeviceCapsToSessionInfoOption
+from .general.reset_location_service_option import ResetLocationServiceOption
+from .other.command_timeouts_option import CommandTimeoutsOption
+from .other.launch_with_idb_option import LaunchWithIdbOption
+from .other.show_ios_log_option import ShowIosLogOption
+from .other.use_json_source_option import UseJsonSourceOption
+from .simulator.calendar_access_authorized_option import CalendarAccessAuthorizedOption
+from .simulator.calendar_format_option import CalendarFormatOption
+from .simulator.connect_hardware_keyboard_option import ConnectHardwareKeyboardOption
+from .simulator.custom_ssl_cert_option import CustomSslCertOption
+from .simulator.enforce_fresh_simulator_creation_option import EnforceFreshSimulatorCreationOption
+from .simulator.force_simulator_software_keyboard_presence_option import ForceSimulatorSoftwareKeyboardPresenceOption
+from .simulator.ios_simulator_logs_predicate_option import IosSimulatorLogsPredicateOption
+from .simulator.keep_key_chains_option import KeepKeyChainsOption
+from .simulator.keychains_exclude_patterns_option import KeychainsExcludePatternsOption
+from .simulator.permissions_option import PermissionsOption
+from .simulator.reduce_motion_option import ReduceMotionOption
+from .simulator.reset_on_session_start_only_option import ResetOnSessionStartOnlyOption
+from .simulator.scale_factor_option import ScaleFactorOption
+from .simulator.shutdown_other_simulators_option import ShutdownOtherSimulatorsOption
+from .simulator.simulator_devices_set_path_option import SimulatorDevicesSetPathOption
+from .simulator.simulator_pasteboard_automatic_sync_option import SimulatorPasteboardAutomaticSyncOption
+from .simulator.simulator_startup_timeout_option import SimulatorStartupTimeoutOption
+from .simulator.simulator_trace_pointer_option import SimulatorTracePointerOption
+from .simulator.simulator_window_center_option import SimulatorWindowCenterOption
+from .wda.allow_provisioning_device_regitration_option import AllowProvisioningDeviceRegistrationOption
+from .wda.auto_accept_alerts_option import AutoAcceptAlertsOption
+from .wda.auto_disimiss_alerts_option import AutoDismissAlertsOption
+from .wda.derived_data_path_option import DerivedDataPathOption
+from .wda.disable_automatic_screenshots_option import DisableAutomaticScreenshotsOption
+from .wda.force_app_launch_option import ForceAppLaunchOption
+from .wda.keychain_password_option import KeychainPasswordOption
+from .wda.keychain_path_option import KeychainPathOption
+from .wda.max_typing_frequency_option import MaxTypingFrequencyOption
+from .wda.mjpeg_server_port_option import MjpegServerPortOption
+from .wda.process_arguments_option import ProcessArgumentsOption
+from .wda.result_bundle_path_option import ResultBundlePathOption
+from .wda.screenshot_quality_option import ScreenshotQualityOption
+from .wda.should_terminate_app_option import ShouldTerminateAppOption
+from .wda.should_use_singleton_test_manager_option import ShouldUseSingletonTestManagerOption
+from .wda.show_xcode_log_option import ShowXcodeLogOption
+from .wda.simple_is_visible_check_option import SimpleIsVisibleCheckOption
+from .wda.updated_wda_bundle_id_option import UpdatedWdaBundleIdOption
+from .wda.use_native_caching_strategy_option import UseNativeCachingStrategyOption
+from .wda.use_new_wda_option import UseNewWdaOption
+from .wda.use_prebuilt_wda_option import UsePrebuiltWdaOption
+from .wda.use_simple_build_test_option import UseSimpleBuildTestOption
+from .wda.use_xctestrun_file_option import UseXctestrunFileOption
+from .wda.wait_for_idle_timeout_option import WaitForIdleTimeoutOption
+from .wda.wait_for_quiescence_option import WaitForQuiescenceOption
+from .wda.wda_base_url_option import WdaBaseUrlOption
+from .wda.wda_connection_timeout_option import WdaConnectionTimeoutOption
+from .wda.wda_eventloop_idle_delay_option import WdaEventloopIdleDelayOption
+from .wda.wda_launch_timeout_option import WdaLaunchTimeoutOption
+from .wda.wda_local_port_option import WdaLocalPortOption
+from .wda.wda_startup_retries_option import WdaStartupRetriesOption
+from .wda.wda_startup_retry_interval_option import WdaStartupRetryIntervalOption
+from .wda.web_driver_agent_url_option import WebDriverAgentUrlOption
+from .wda.xcode_org_id_option import XcodeOrgIdOption
+from .wda.xcode_signing_id_option import XcodeSigningIdOption
+from .webview.absolute_web_locations_option import AbsoluteWebLocationsOption
+from .webview.additional_webview_bundle_ids_option import AdditionalWebviewBundleIdsOption
+from .webview.enable_async_execute_from_https_option import EnableAsyncExecuteFromHttpsOption
+from .webview.full_context_list_option import FullContextListOption
+from .webview.include_safari_in_webviews_option import IncludeSafariInWebviewsOption
+from .webview.native_web_tap_option import NativeWebTapOption
+from .webview.safari_garbage_collect_option import SafariGarbageCollectOption
+from .webview.safari_ignore_fraud_warning_option import SafariIgnoreFraudWarningOption
+from .webview.safari_ignore_web_hostnames_option import SafariIgnoreWebHostnamesOption
+from .webview.safari_initial_url_option import SafariInitialUrlOption
+from .webview.safari_log_all_communication_hex_dump_option import SafariLogAllCommunicationHexDumpOption
+from .webview.safari_log_all_communication_option import SafariLogAllCommunicationOption
+from .webview.safari_open_links_in_background_option import SafariOpenLinksInBackgroundOption
+from .webview.safari_socket_chunk_size_option import SafariSocketChunkSizeOption
+from .webview.safari_web_inspector_max_frame_length_option import SafariWebInspectorMaxFrameLengthOption
+from .webview.webkit_response_timeout_option import WebkitResponseTimeoutOption
+from .webview.webview_connect_retries_option import WebviewConnectRetriesOption
+from .webview.webview_connect_timeout_option import WebviewConnectTimeoutOption
+
+
+class XCUITestOptions(
+    AppiumOptions,
+    AppOption,
+    BundleIdOption,
+    PlatformVersionOption,
+    ClearSystemFilesOption,
+    OrientationOption,
+    UdidOption,
+    LanguageOption,
+    LocaleOption,
+    IsHeadlessOption,
+    SkipLogCaptureOption,
+    AutoWebViewOption,
+    EnablePerformanceLoggingOption,
+    OtherAppsOption,
+    DeviceNameOption,
+    IncludeDeviceCapsToSessionInfoOption,
+    ResetLocationServiceOption,
+    AppInstallStrategyOption,
+    AppPushTimeoutOption,
+    LocalizableStringsDirOption,
+    CommandTimeoutsOption,
+    LaunchWithIdbOption,
+    ShowIosLogOption,
+    UseJsonSourceOption,
+    CalendarAccessAuthorizedOption,
+    CalendarFormatOption,
+    ConnectHardwareKeyboardOption,
+    CustomSslCertOption,
+    EnforceFreshSimulatorCreationOption,
+    ForceSimulatorSoftwareKeyboardPresenceOption,
+    IosSimulatorLogsPredicateOption,
+    KeepKeyChainsOption,
+    KeychainsExcludePatternsOption,
+    PermissionsOption,
+    ReduceMotionOption,
+    ResetOnSessionStartOnlyOption,
+    ScaleFactorOption,
+    ShutdownOtherSimulatorsOption,
+    SimulatorDevicesSetPathOption,
+    SimulatorPasteboardAutomaticSyncOption,
+    SimulatorStartupTimeoutOption,
+    SimulatorTracePointerOption,
+    SimulatorWindowCenterOption,
+    AllowProvisioningDeviceRegistrationOption,
+    AutoAcceptAlertsOption,
+    AutoDismissAlertsOption,
+    DerivedDataPathOption,
+    DisableAutomaticScreenshotsOption,
+    ForceAppLaunchOption,
+    KeychainPasswordOption,
+    KeychainPathOption,
+    MaxTypingFrequencyOption,
+    MjpegServerPortOption,
+    ProcessArgumentsOption,
+    ResultBundlePathOption,
+    ScreenshotQualityOption,
+    ShouldTerminateAppOption,
+    ShouldUseSingletonTestManagerOption,
+    ShowXcodeLogOption,
+    SimpleIsVisibleCheckOption,
+    UpdatedWdaBundleIdOption,
+    UseNativeCachingStrategyOption,
+    UseNewWdaOption,
+    UsePrebuiltWdaOption,
+    UseSimpleBuildTestOption,
+    UseXctestrunFileOption,
+    WaitForIdleTimeoutOption,
+    WaitForQuiescenceOption,
+    WdaBaseUrlOption,
+    WdaConnectionTimeoutOption,
+    WdaEventloopIdleDelayOption,
+    WdaLaunchTimeoutOption,
+    WdaLocalPortOption,
+    WdaStartupRetriesOption,
+    WdaStartupRetryIntervalOption,
+    WebDriverAgentUrlOption,
+    XcodeOrgIdOption,
+    XcodeSigningIdOption,
+    AbsoluteWebLocationsOption,
+    AdditionalWebviewBundleIdsOption,
+    EnableAsyncExecuteFromHttpsOption,
+    FullContextListOption,
+    IncludeSafariInWebviewsOption,
+    NativeWebTapOption,
+    SafariGarbageCollectOption,
+    SafariIgnoreFraudWarningOption,
+    SafariIgnoreWebHostnamesOption,
+    SafariInitialUrlOption,
+    SafariLogAllCommunicationHexDumpOption,
+    SafariLogAllCommunicationOption,
+    SafariOpenLinksInBackgroundOption,
+    SafariSocketChunkSizeOption,
+    SafariWebInspectorMaxFrameLengthOption,
+    WebkitResponseTimeoutOption,
+    WebviewConnectRetriesOption,
+    WebviewConnectTimeoutOption,
+):
+    @property
+    def default_capabilities(self) -> Dict:
+        return {
+            AUTOMATION_NAME: 'XCUITest',
+            PLATFORM_NAME: 'iOS',
+        }
diff --git a/appium/options/ios/xcuitest/general/__init__.py b/appium/options/ios/xcuitest/general/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/xcuitest/general/include_device_caps_to_session_info_option.py b/appium/options/ios/xcuitest/general/include_device_caps_to_session_info_option.py
new file mode 100644
index 000000000..b5df6d286
--- /dev/null
+++ b/appium/options/ios/xcuitest/general/include_device_caps_to_session_info_option.py
@@ -0,0 +1,41 @@
+# 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
+
+INCLUDE_DEVICE_CAPS_TO_SESSION_INFO = 'includeDeviceCapsToSessionInfo'
+
+
+class IncludeDeviceCapsToSessionInfoOption(SupportsCapabilities):
+    @property
+    def include_device_caps_to_session_info(self) -> Optional[bool]:
+        """
+        Whether to include screen information as the result of Get Session Capabilities.
+        """
+        return self.get_capability(INCLUDE_DEVICE_CAPS_TO_SESSION_INFO)
+
+    @include_device_caps_to_session_info.setter
+    def include_device_caps_to_session_info(self, value: bool) -> None:
+        """
+        Whether to include screen information as the result of Get Session Capabilities.
+        It includes pixelRatio, statBarHeight and viewportRect, but
+        it causes an extra API call to WDA which may increase the response time.
+        Defaults to true.
+        """
+        self.set_capability(INCLUDE_DEVICE_CAPS_TO_SESSION_INFO, value)
diff --git a/appium/options/ios/xcuitest/general/reset_location_service_option.py b/appium/options/ios/xcuitest/general/reset_location_service_option.py
new file mode 100644
index 000000000..c9e242cc5
--- /dev/null
+++ b/appium/options/ios/xcuitest/general/reset_location_service_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+RESET_LOCATION_SERVICE = 'resetLocationService'
+
+
+class ResetLocationServiceOption(SupportsCapabilities):
+    @property
+    def reset_location_service(self) -> Optional[bool]:
+        """
+        Whether to reset the location service in the session deletion on real devices.
+        """
+        return self.get_capability(RESET_LOCATION_SERVICE)
+
+    @reset_location_service.setter
+    def reset_location_service(self, value: bool) -> None:
+        """
+        Whether reset the location service in the session deletion on real devices.
+        Defaults to false.
+        """
+        self.set_capability(RESET_LOCATION_SERVICE, value)
diff --git a/appium/options/ios/xcuitest/other/__init__.py b/appium/options/ios/xcuitest/other/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/xcuitest/other/command_timeouts_option.py b/appium/options/ios/xcuitest/other/command_timeouts_option.py
new file mode 100644
index 000000000..420b35039
--- /dev/null
+++ b/appium/options/ios/xcuitest/other/command_timeouts_option.py
@@ -0,0 +1,57 @@
+# 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 Dict, Optional, Union
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+COMMAND_TIMEOUTS = 'commandTimeouts'
+
+
+class CommandTimeoutsOption(SupportsCapabilities):
+    @property
+    def command_timeouts(self) -> Optional[Union[Dict[str, timedelta], timedelta]]:
+        """
+        Custom timeout(s) for WDA backend commands execution.
+        """
+        value = self.get_capability(COMMAND_TIMEOUTS)
+        if value is None:
+            return None
+        if isinstance(value, dict):
+            return {k: timedelta(milliseconds=v) for k, v in value.items()}
+        return timedelta(milliseconds=int(value))
+
+    @command_timeouts.setter
+    def command_timeouts(self, value: Union[Dict[str, timedelta], timedelta, int]) -> None:
+        """
+        Custom timeout for all WDA backend commands execution.
+        This might be useful if WDA backend freezes unexpectedly or requires too
+        much time to fail and blocks automated test execution.
+        Dictionary keys are command names which you can find in server logs. Look for
+        "Executing command 'command_name'" records.
+        Timeout value is expected to contain max duration to wait for
+        the given WDA command to be executed before terminating the session forcefully.
+        The magic 'default' key allows to provide the timeout for all other commands that
+        were not explicitly mentioned as dictionary keys
+        """
+        if isinstance(value, dict):
+            self.set_capability(COMMAND_TIMEOUTS, {k: int(v.total_seconds() * 1000) for k, v in value.items()})
+        elif isinstance(value, timedelta):
+            self.set_capability(COMMAND_TIMEOUTS, f'{int(value.total_seconds() * 1000)}')
+        else:
+            self.set_capability(COMMAND_TIMEOUTS, value)
diff --git a/appium/options/ios/xcuitest/other/launch_with_idb_option.py b/appium/options/ios/xcuitest/other/launch_with_idb_option.py
new file mode 100644
index 000000000..c8701c620
--- /dev/null
+++ b/appium/options/ios/xcuitest/other/launch_with_idb_option.py
@@ -0,0 +1,42 @@
+# 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
+
+LAUNCH_WITH_IDB = 'launchWithIDB'
+
+
+class LaunchWithIdbOption(SupportsCapabilities):
+    @property
+    def launch_with_idb(self) -> Optional[bool]:
+        """
+        Whether to launch WebDriverAgentRunner with idb instead of xcodebuild.
+        """
+        return self.get_capability(LAUNCH_WITH_IDB)
+
+    @launch_with_idb.setter
+    def launch_with_idb(self, value: bool) -> None:
+        """
+        Launch WebDriverAgentRunner with idb instead of xcodebuild. This could save
+        a significant amount of time by skipping the xcodebuild process, although the
+        idb might not be very reliable, especially with fresh Xcode SDKs. Check
+        the idb repository for more details on possible compatibility issues.
+        Defaults to false.
+        """
+        self.set_capability(LAUNCH_WITH_IDB, value)
diff --git a/appium/options/ios/xcuitest/other/show_ios_log_option.py b/appium/options/ios/xcuitest/other/show_ios_log_option.py
new file mode 100644
index 000000000..b4bf75d3a
--- /dev/null
+++ b/appium/options/ios/xcuitest/other/show_ios_log_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SHOW_IOS_LOG = 'showIOSLog'
+
+
+class ShowIosLogOption(SupportsCapabilities):
+    @property
+    def show_ios_log(self) -> Optional[bool]:
+        """
+        Whether to show any logs captured from a device in the appium logs.
+        """
+        return self.get_capability(SHOW_IOS_LOG)
+
+    @show_ios_log.setter
+    def show_ios_log(self, value: bool) -> None:
+        """
+        Whether to show any logs captured from a device in the appium logs.
+        Default false.
+        """
+        self.set_capability(SHOW_IOS_LOG, value)
diff --git a/appium/options/ios/xcuitest/other/use_json_source_option.py b/appium/options/ios/xcuitest/other/use_json_source_option.py
new file mode 100644
index 000000000..1eac50bb8
--- /dev/null
+++ b/appium/options/ios/xcuitest/other/use_json_source_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+USE_JSON_SOURCE = 'useJSONSource'
+
+
+class UseJsonSourceOption(SupportsCapabilities):
+    @property
+    def use_json_source(self) -> Optional[bool]:
+        """
+        Whether to get JSON source from WDA and transform it to XML on the driver side.
+        """
+        return self.get_capability(USE_JSON_SOURCE)
+
+    @use_json_source.setter
+    def use_json_source(self, value: bool) -> None:
+        """
+        Whether to get JSON source from WDA and transform it to XML on the driver side.
+        Defaults to false.
+        """
+        self.set_capability(USE_JSON_SOURCE, value)
diff --git a/appium/options/ios/xcuitest/simulator/__init__.py b/appium/options/ios/xcuitest/simulator/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/xcuitest/simulator/calendar_access_authorized_option.py b/appium/options/ios/xcuitest/simulator/calendar_access_authorized_option.py
new file mode 100644
index 000000000..a0e1afe9b
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/calendar_access_authorized_option.py
@@ -0,0 +1,41 @@
+# 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
+
+CALENDAR_ACCESS_AUTHORIZED = 'calendarAccessAuthorized'
+
+
+class CalendarAccessAuthorizedOption(SupportsCapabilities):
+    @property
+    def calendar_access_authorized(self) -> Optional[bool]:
+        """
+        Whether to enable calendar access on IOS Simulator.
+        """
+        return self.get_capability(CALENDAR_ACCESS_AUTHORIZED)
+
+    @calendar_access_authorized.setter
+    def calendar_access_authorized(self, value: bool) -> None:
+        """
+        Set this to true if you want to enable calendar access on IOS Simulator
+        with given bundleId. Set to false, if you want to disable calendar access
+        on IOS Simulator with given bundleId. If not set, the calendar
+        authorization status will not be set.
+        """
+        self.set_capability(CALENDAR_ACCESS_AUTHORIZED, value)
diff --git a/appium/options/ios/xcuitest/simulator/calendar_format_option.py b/appium/options/ios/xcuitest/simulator/calendar_format_option.py
new file mode 100644
index 000000000..4e7565987
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/calendar_format_option.py
@@ -0,0 +1,38 @@
+# 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
+
+CALENDAR_FORMAT = 'calendarFormat'
+
+
+class CalendarFormatOption(SupportsCapabilities):
+    @property
+    def calendar_format(self) -> Optional[str]:
+        """
+        Calendar format for the iOS Simulator.
+        """
+        return self.get_capability(CALENDAR_FORMAT)
+
+    @calendar_format.setter
+    def calendar_format(self, value: str) -> None:
+        """
+        Set calendar format for the iOS Simulator.
+        """
+        self.set_capability(CALENDAR_FORMAT, value)
diff --git a/appium/options/ios/xcuitest/simulator/connect_hardware_keyboard_option.py b/appium/options/ios/xcuitest/simulator/connect_hardware_keyboard_option.py
new file mode 100644
index 000000000..f7118d6de
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/connect_hardware_keyboard_option.py
@@ -0,0 +1,43 @@
+# 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
+
+CONNECT_HARDWARE_KEYBOARD = 'connectHardwareKeyboard'
+
+
+class ConnectHardwareKeyboardOption(SupportsCapabilities):
+    @property
+    def connect_hardware_keyboard(self) -> Optional[bool]:
+        """
+        Whether to connect hardware keyboard to Simulator.
+        """
+        return self.get_capability(CONNECT_HARDWARE_KEYBOARD)
+
+    @connect_hardware_keyboard.setter
+    def connect_hardware_keyboard(self, value: bool) -> None:
+        """
+        Set this option to true in order to enable hardware keyboard in Simulator.
+        The preference works only when Appium launches a simulator instance with
+        this value. It is set to false by default, because this helps to workaround
+        some XCTest bugs. connectHardwareKeyboard: true makes
+        forceSimulatorSoftwareKeyboardPresence: false if no explicit value is set
+        for forceSimulatorSoftwareKeyboardPresence capability since Appium 1.22.0.
+        """
+        self.set_capability(CONNECT_HARDWARE_KEYBOARD, value)
diff --git a/appium/options/ios/xcuitest/simulator/custom_ssl_cert_option.py b/appium/options/ios/xcuitest/simulator/custom_ssl_cert_option.py
new file mode 100644
index 000000000..a3914b939
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/custom_ssl_cert_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+CUSTOM_SSL_CERT = 'customSSLCert'
+
+
+class CustomSslCertOption(SupportsCapabilities):
+    @property
+    def custom_ssl_cert(self) -> Optional[str]:
+        """
+        SSL certificate content.
+        """
+        return self.get_capability(CUSTOM_SSL_CERT)
+
+    @custom_ssl_cert.setter
+    def custom_ssl_cert(self, value: str) -> None:
+        """
+        Adds a root SSL certificate to IOS Simulator.
+        The certificate content must be provided in PEM format.
+        """
+        self.set_capability(CUSTOM_SSL_CERT, value)
diff --git a/appium/options/ios/xcuitest/simulator/enforce_fresh_simulator_creation_option.py b/appium/options/ios/xcuitest/simulator/enforce_fresh_simulator_creation_option.py
new file mode 100644
index 000000000..cea87eb05
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/enforce_fresh_simulator_creation_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ENFORCE_FRESH_SIMULATOR_CREATION = 'enforceFreshSimulatorCreation'
+
+
+class EnforceFreshSimulatorCreationOption(SupportsCapabilities):
+    @property
+    def enforce_fresh_simulator_creation(self) -> Optional[bool]:
+        """
+        Whether to create a new simulator for each new test session.
+        """
+        return self.get_capability(ENFORCE_FRESH_SIMULATOR_CREATION)
+
+    @enforce_fresh_simulator_creation.setter
+    def enforce_fresh_simulator_creation(self, value: bool) -> None:
+        """
+        Creates a new simulator in session creation and deletes it in session deletion.
+        Defaults to false.
+        """
+        self.set_capability(ENFORCE_FRESH_SIMULATOR_CREATION, value)
diff --git a/appium/options/ios/xcuitest/simulator/force_simulator_software_keyboard_presence_option.py b/appium/options/ios/xcuitest/simulator/force_simulator_software_keyboard_presence_option.py
new file mode 100644
index 000000000..1e1a697aa
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/force_simulator_software_keyboard_presence_option.py
@@ -0,0 +1,45 @@
+# 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
+
+FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE = 'forceSimulatorSoftwareKeyboardPresence'
+
+
+class ForceSimulatorSoftwareKeyboardPresenceOption(SupportsCapabilities):
+    @property
+    def force_simulator_software_keyboard_presence(self) -> Optional[bool]:
+        """
+        Whether to enforce software keyboard presence.
+        """
+        return self.get_capability(FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE)
+
+    @force_simulator_software_keyboard_presence.setter
+    def force_simulator_software_keyboard_presence(self, value: bool) -> None:
+        """
+        Set this option to true in order to turn software keyboard on and turn
+        hardware keyboard off in Simulator since Appium 1.22.0. This option helps
+        to avoid Keyboard is not present error. It is set to true by default.
+        Appium respects preset simulator software/hardware keyboard preference
+        when this value is false, so connectHardwareKeyboard: false and
+        forceSimulatorSoftwareKeyboardPresence: false means for Appium to keep
+        the current Simulator keyboard preferences. This option has priority
+        over connectHardwareKeyboard.
+        """
+        self.set_capability(FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE, value)
diff --git a/appium/options/ios/xcuitest/simulator/ios_simulator_logs_predicate_option.py b/appium/options/ios/xcuitest/simulator/ios_simulator_logs_predicate_option.py
new file mode 100644
index 000000000..c7d486477
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/ios_simulator_logs_predicate_option.py
@@ -0,0 +1,38 @@
+# 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
+
+IOS_SIMULATOR_LOGS_PREDICATE = 'iosSimulatorLogsPredicate'
+
+
+class IosSimulatorLogsPredicateOption(SupportsCapabilities):
+    @property
+    def ios_simulator_logs_predicate(self) -> Optional[bool]:
+        """
+        Get Simulator log filtering predicate.
+        """
+        return self.get_capability(IOS_SIMULATOR_LOGS_PREDICATE)
+
+    @ios_simulator_logs_predicate.setter
+    def ios_simulator_logs_predicate(self, value: bool) -> None:
+        """
+        Set the --predicate flag in the ios simulator logs.
+        """
+        self.set_capability(IOS_SIMULATOR_LOGS_PREDICATE, value)
diff --git a/appium/options/ios/xcuitest/simulator/keep_key_chains_option.py b/appium/options/ios/xcuitest/simulator/keep_key_chains_option.py
new file mode 100644
index 000000000..a304c935e
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/keep_key_chains_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+KEEP_KEY_CHAINS = 'keepKeyChains'
+
+
+class KeepKeyChainsOption(SupportsCapabilities):
+    @property
+    def keep_key_chains(self) -> Optional[bool]:
+        """
+        Whether to preserve Simulator keychains after full reset.
+        """
+        return self.get_capability(KEEP_KEY_CHAINS)
+
+    @keep_key_chains.setter
+    def keep_key_chains(self, value: bool) -> None:
+        """
+        Set the capability to true in order to preserve Simulator keychains folder after
+        full reset. This feature has no effect on real devices. Defaults to false.
+        """
+        self.set_capability(KEEP_KEY_CHAINS, value)
diff --git a/appium/options/ios/xcuitest/simulator/keychains_exclude_patterns_option.py b/appium/options/ios/xcuitest/simulator/keychains_exclude_patterns_option.py
new file mode 100644
index 000000000..d971f5c5c
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/keychains_exclude_patterns_option.py
@@ -0,0 +1,44 @@
+# 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
+
+KEYCHAINS_EXCLUDE_PATTERNS = 'keychainsExcludePatterns'
+
+
+class KeychainsExcludePatternsOption(SupportsCapabilities):
+    @property
+    def keychains_exclude_patterns(self) -> Optional[str]:
+        """
+        Keychains exclude patterns.
+        """
+        return self.get_capability(KEYCHAINS_EXCLUDE_PATTERNS)
+
+    @keychains_exclude_patterns.setter
+    def keychains_exclude_patterns(self, value: str) -> None:
+        """
+        This capability accepts comma-separated path patterns,
+        which are going to be excluded from keychains restore while
+        full reset is being performed on Simulator. It might be
+        useful if you want to exclude only particular keychain types
+        from being restored, like the applications keychain. This
+        feature has no effect on real devices. E.g. "*keychain*.db*"
+        to exclude applications keychain from being restored
+        """
+        self.set_capability(KEYCHAINS_EXCLUDE_PATTERNS, value)
diff --git a/appium/options/ios/xcuitest/simulator/permissions_option.py b/appium/options/ios/xcuitest/simulator/permissions_option.py
new file mode 100644
index 000000000..6146e382b
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/permissions_option.py
@@ -0,0 +1,50 @@
+# 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.
+
+import json
+from typing import Dict, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+PERMISSIONS = 'permissions'
+
+
+class PermissionsOption(SupportsCapabilities):
+    @property
+    def permissions(self) -> Optional[Dict[str, Dict[str, str]]]:
+        """
+        Get Simulator permissions.
+        """
+        value = self.get_capability(PERMISSIONS)
+        return None if value is None else json.loads(value)
+
+    @permissions.setter
+    def permissions(self, value: Dict[str, Dict[str, str]]) -> None:
+        """
+        Allows setting of permissions for the specified application bundle on
+        Simulator only.
+
+        Since Xcode SDK 11.4 Apple provides native APIs to interact with
+        application settings. Check the output of `xcrun simctl privacy booted`
+        command to get the list of available permission names. Use yes, no
+        and unset as values in order to grant, revoke or reset the corresponding
+        permission. Below Xcode SDK 11.4 it is required that applesimutils package
+        is installed and available in PATH. The list of available service names
+        and statuses can be found at https://github.com/wix/AppleSimulatorUtils.
+        For example: {"com.apple.mobilecal": {"calendar": "YES"}}
+        """
+        self.set_capability(PERMISSIONS, json.dumps(value, ensure_ascii=False))
diff --git a/appium/options/ios/xcuitest/simulator/reduce_motion_option.py b/appium/options/ios/xcuitest/simulator/reduce_motion_option.py
new file mode 100644
index 000000000..89dbe8779
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/reduce_motion_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+REDUCE_MOTION = 'reduceMotion'
+
+
+class ReduceMotionOption(SupportsCapabilities):
+    @property
+    def reduce_motion(self) -> Optional[bool]:
+        """
+        Whether to reduce motion accessibility preference.
+        """
+        return self.get_capability(REDUCE_MOTION)
+
+    @reduce_motion.setter
+    def reduce_motion(self, value: bool) -> None:
+        """
+        Allows to turn on/off reduce motion accessibility preference.
+        Setting reduceMotion on helps to reduce flakiness during tests.
+        Only on simulators.
+        """
+        self.set_capability(REDUCE_MOTION, value)
diff --git a/appium/options/ios/xcuitest/simulator/reset_on_session_start_only_option.py b/appium/options/ios/xcuitest/simulator/reset_on_session_start_only_option.py
new file mode 100644
index 000000000..294a72117
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/reset_on_session_start_only_option.py
@@ -0,0 +1,41 @@
+# 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
+
+RESET_ON_SESSION_START_ONLY = 'resetOnSessionStartOnly'
+
+
+class ResetOnSessionStartOnlyOption(SupportsCapabilities):
+    @property
+    def reset_on_session_start_only(self) -> Optional[bool]:
+        """
+        Whether to perform Simulator reset on test session finish (false) or not (true).
+        """
+        return self.get_capability(RESET_ON_SESSION_START_ONLY)
+
+    @reset_on_session_start_only.setter
+    def reset_on_session_start_only(self, value: bool) -> None:
+        """
+        Whether to perform reset on test session finish (false) or not (true).
+        Keeping this variable set to true and Simulator running (the default
+        behaviour since version 1.6.4) may significantly shorten the duration of
+        test session initialization.
+        """
+        self.set_capability(RESET_ON_SESSION_START_ONLY, value)
diff --git a/appium/options/ios/xcuitest/simulator/scale_factor_option.py b/appium/options/ios/xcuitest/simulator/scale_factor_option.py
new file mode 100644
index 000000000..8b7e6e037
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/scale_factor_option.py
@@ -0,0 +1,44 @@
+# 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
+
+SCALE_FACTOR = 'scaleFactor'
+
+
+class ScaleFactorOption(SupportsCapabilities):
+    @property
+    def scale_factor(self) -> Optional[str]:
+        """
+        Simulator scale factor.
+        """
+        return self.get_capability(SCALE_FACTOR)
+
+    @scale_factor.setter
+    def scale_factor(self, value: str) -> None:
+        """
+        Simulator scale factor. This is useful to have if the default resolution
+        of simulated device is greater than the actual display resolution.
+        So you can scale the simulator to see the whole device screen without scrolling.
+        Acceptable values for simulators running Xcode SDK 8 and older are: '1.0',
+        '0.75', '0.5', '0.33' and '0.25', where '1.0' means 100% scale.
+        For simulators running Xcode SDK 9 and above the value could be any valid
+        positive float number.
+        """
+        self.set_capability(SCALE_FACTOR, value)
diff --git a/appium/options/ios/xcuitest/simulator/shutdown_other_simulators_option.py b/appium/options/ios/xcuitest/simulator/shutdown_other_simulators_option.py
new file mode 100644
index 000000000..a7c5edc51
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/shutdown_other_simulators_option.py
@@ -0,0 +1,44 @@
+# 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
+
+SHUTDOWN_OTHER_SIMULATORS = 'shutdownOtherSimulators'
+
+
+class ShutdownOtherSimulatorsOption(SupportsCapabilities):
+    @property
+    def shutdown_other_simulators(self) -> Optional[bool]:
+        """
+        Whether to shut down of other booted simulators except of the current one.
+        """
+        return self.get_capability(SHUTDOWN_OTHER_SIMULATORS)
+
+    @shutdown_other_simulators.setter
+    def shutdown_other_simulators(self, value: bool) -> None:
+        """
+        If this capability set to true and the current device under test is an iOS
+        Simulator then Appium will try to shut down all the other running Simulators
+        before to start a new session. This might be useful while executing webview
+        tests on different devices, since only one device can be debugged remotely
+        at once due to an Apple bug. The capability only has an effect if
+        --relaxed-security command line argument is provided to the server.
+        Defaults to false.
+        """
+        self.set_capability(SHUTDOWN_OTHER_SIMULATORS, value)
diff --git a/appium/options/ios/xcuitest/simulator/simulator_devices_set_path_option.py b/appium/options/ios/xcuitest/simulator/simulator_devices_set_path_option.py
new file mode 100644
index 000000000..845575355
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/simulator_devices_set_path_option.py
@@ -0,0 +1,41 @@
+# 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
+
+SIMULATOR_DEVICES_SET_PATH = 'simulatorDevicesSetPath'
+
+
+class SimulatorDevicesSetPathOption(SupportsCapabilities):
+    @property
+    def simulator_devices_set_path(self) -> Optional[str]:
+        """
+        Alternative path to the simulator devices set.
+        """
+        return self.get_capability(SIMULATOR_DEVICES_SET_PATH)
+
+    @simulator_devices_set_path.setter
+    def simulator_devices_set_path(self, value: str) -> None:
+        """
+        This capability allows to set an alternative path to the simulator devices
+        set in case you have multiple sets deployed on your local system. Such
+        feature could be useful if you, for example, would like to save disk space
+        on the main system volume.
+        """
+        self.set_capability(SIMULATOR_DEVICES_SET_PATH, value)
diff --git a/appium/options/ios/xcuitest/simulator/simulator_pasteboard_automatic_sync_option.py b/appium/options/ios/xcuitest/simulator/simulator_pasteboard_automatic_sync_option.py
new file mode 100644
index 000000000..a1258df4e
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/simulator_pasteboard_automatic_sync_option.py
@@ -0,0 +1,42 @@
+# 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
+
+SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC = 'simulatorPasteboardAutomaticSync'
+
+
+class SimulatorPasteboardAutomaticSyncOption(SupportsCapabilities):
+    @property
+    def simulator_pasteboard_automatic_sync(self) -> Optional[bool]:
+        """
+        Pasteboard automation sync state.
+        """
+        return self.get_capability(SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC)
+
+    @simulator_pasteboard_automatic_sync.setter
+    def simulator_pasteboard_automatic_sync(self, value: bool) -> None:
+        """
+        Handle the -PasteboardAutomaticSync flag when simulator process launches.
+        It could improve launching simulator performance not to sync pasteboard with
+        the system when this value is off. on forces the flag enabled. system does
+        not provide the flag to the launching command. on, off, or system is available.
+        They are case-insensitive. Defaults to off.
+        """
+        self.set_capability(SIMULATOR_PASTEBOARD_AUTOMATIC_SYNC, value)
diff --git a/appium/options/ios/xcuitest/simulator/simulator_startup_timeout_option.py b/appium/options/ios/xcuitest/simulator/simulator_startup_timeout_option.py
new file mode 100644
index 000000000..29353498d
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/simulator_startup_timeout_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 datetime import timedelta
+from typing import Optional, Union
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SIMULATOR_STARTUP_TIMEOUT = 'simulatorStartupTimeout'
+
+
+class SimulatorStartupTimeoutOption(SupportsCapabilities):
+    @property
+    def simulator_startup_timeout(self) -> Optional[timedelta]:
+        """
+        Simulator startup timeout.
+        """
+        value = self.get_capability(SIMULATOR_STARTUP_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @simulator_startup_timeout.setter
+    def simulator_startup_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Allows to change the default timeout for Simulator startup.
+        By default, this value is set to 120000ms (2 minutes),
+        although the startup could take longer on a weak hardware
+        or if other concurrent processes use much system resources
+        during the boot up procedure.
+        """
+        self.set_capability(
+            SIMULATOR_STARTUP_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/ios/xcuitest/simulator/simulator_trace_pointer_option.py b/appium/options/ios/xcuitest/simulator/simulator_trace_pointer_option.py
new file mode 100644
index 000000000..d5f5d1fd8
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/simulator_trace_pointer_option.py
@@ -0,0 +1,41 @@
+# 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
+
+SIMULATOR_TRACE_POINTER = 'simulatorTracePointer'
+
+
+class SimulatorTracePointerOption(SupportsCapabilities):
+    @property
+    def simulator_trace_pointer(self) -> Optional[bool]:
+        """
+        Whether to highlight pointer moves in the Simulator window.
+        """
+        return self.get_capability(SIMULATOR_TRACE_POINTER)
+
+    @simulator_trace_pointer.setter
+    def simulator_trace_pointer(self, value: bool) -> None:
+        """
+        Set whether to highlight pointer moves in the Simulator window.
+        The Simulator UI client must be shut down before the session
+        startup in order for this capability to be applied properly.
+        false by default.
+        """
+        self.set_capability(SIMULATOR_TRACE_POINTER, value)
diff --git a/appium/options/ios/xcuitest/simulator/simulator_window_center_option.py b/appium/options/ios/xcuitest/simulator/simulator_window_center_option.py
new file mode 100644
index 000000000..1f8b2d8b8
--- /dev/null
+++ b/appium/options/ios/xcuitest/simulator/simulator_window_center_option.py
@@ -0,0 +1,41 @@
+# 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
+
+SIMULATOR_WINDOW_CENTER = 'simulatorWindowCenter'
+
+
+class SimulatorWindowCenterOption(SupportsCapabilities):
+    @property
+    def simulator_window_center(self) -> Optional[str]:
+        """
+        Simulator window center coordinates.
+        """
+        return self.get_capability(SIMULATOR_WINDOW_CENTER)
+
+    @simulator_window_center.setter
+    def simulator_window_center(self, value: str) -> None:
+        """
+        Allows to explicitly set the coordinates of Simulator window center
+        for Xcode9+ SDK. This capability only has an effect if Simulator
+        window has not been opened yet for the current session before it started.
+        e.g. "{-100.0,100.0}" or "{500,500}", spaces are not allowed
+        """
+        self.set_capability(SIMULATOR_WINDOW_CENTER, value)
diff --git a/appium/options/ios/xcuitest/wda/__init__.py b/appium/options/ios/xcuitest/wda/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/xcuitest/wda/allow_provisioning_device_regitration_option.py b/appium/options/ios/xcuitest/wda/allow_provisioning_device_regitration_option.py
new file mode 100644
index 000000000..c88124b4c
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/allow_provisioning_device_regitration_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ALLOW_PROVISIONING_DEVICE_REGISTRATION = 'allowProvisioningDeviceRegistration'
+
+
+class AllowProvisioningDeviceRegistrationOption(SupportsCapabilities):
+    @property
+    def allow_provisioning_device_registration(self) -> Optional[bool]:
+        """
+        Whether to allow xcodebuild to register your destination device on the developer portal.
+        """
+        return self.get_capability(ALLOW_PROVISIONING_DEVICE_REGISTRATION)
+
+    @allow_provisioning_device_registration.setter
+    def allow_provisioning_device_registration(self, value: bool) -> None:
+        """
+        Allow xcodebuild to register your destination device on the developer portal
+        if necessary. Requires a developer account to have been added in Xcode's Accounts
+        preference pane. Defaults to false.
+        """
+        self.set_capability(ALLOW_PROVISIONING_DEVICE_REGISTRATION, value)
diff --git a/appium/options/ios/xcuitest/wda/auto_accept_alerts_option.py b/appium/options/ios/xcuitest/wda/auto_accept_alerts_option.py
new file mode 100644
index 000000000..05c55943e
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/auto_accept_alerts_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+AUTO_ACCEPT_ALERTS = 'autoAcceptAlerts'
+
+
+class AutoAcceptAlertsOption(SupportsCapabilities):
+    @property
+    def auto_accept_alerts(self) -> Optional[bool]:
+        """
+        Whether to accept all alerts automatically.
+        """
+        return self.get_capability(AUTO_ACCEPT_ALERTS)
+
+    @auto_accept_alerts.setter
+    def auto_accept_alerts(self, value: bool) -> None:
+        """
+        Accept all iOS alerts automatically if they pop up. This includes privacy
+        access permission alerts (e.g., location, contacts, photos). Default is false.
+        """
+        self.set_capability(AUTO_ACCEPT_ALERTS, value)
diff --git a/appium/options/ios/xcuitest/wda/auto_disimiss_alerts_option.py b/appium/options/ios/xcuitest/wda/auto_disimiss_alerts_option.py
new file mode 100644
index 000000000..61a28414c
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/auto_disimiss_alerts_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+AUTO_DISMISS_ALERTS = 'autoDismissAlerts'
+
+
+class AutoDismissAlertsOption(SupportsCapabilities):
+    @property
+    def auto_dismiss_alerts(self) -> Optional[bool]:
+        """
+        Whether to dismiss all alerts automatically.
+        """
+        return self.get_capability(AUTO_DISMISS_ALERTS)
+
+    @auto_dismiss_alerts.setter
+    def auto_dismiss_alerts(self, value: bool) -> None:
+        """
+        Dismiss all iOS alerts automatically if they pop up. This includes privacy
+        access permission alerts (e.g., location, contacts, photos). Default is false.
+        """
+        self.set_capability(AUTO_DISMISS_ALERTS, value)
diff --git a/appium/options/ios/xcuitest/wda/derived_data_path_option.py b/appium/options/ios/xcuitest/wda/derived_data_path_option.py
new file mode 100644
index 000000000..ffa40a682
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/derived_data_path_option.py
@@ -0,0 +1,41 @@
+# 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
+
+DERIVED_DATA_PATH = 'derivedDataPath'
+
+
+class DerivedDataPathOption(SupportsCapabilities):
+    @property
+    def derived_data_path(self) -> Optional[str]:
+        """
+        Path to the derived data WDA folder.
+        """
+        return self.get_capability(DERIVED_DATA_PATH)
+
+    @derived_data_path.setter
+    def derived_data_path(self, value: str) -> None:
+        """
+        Use along with usePrebuiltWDA capability and choose where to search for the existing WDA app.
+        If the capability is not set then Xcode will store the derived data in the default root
+        taken from preferences.
+        It also makes sense to choose different folders for parallel WDA sessions.
+        """
+        self.set_capability(DERIVED_DATA_PATH, value)
diff --git a/appium/options/ios/xcuitest/wda/disable_automatic_screenshots_option.py b/appium/options/ios/xcuitest/wda/disable_automatic_screenshots_option.py
new file mode 100644
index 000000000..ac76af330
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/disable_automatic_screenshots_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+DISABLE_AUTOMATIC_SCREENSHOTS = 'disableAutomaticScreenshots'
+
+
+class DisableAutomaticScreenshotsOption(SupportsCapabilities):
+    @property
+    def disable_automatic_screenshots(self) -> Optional[bool]:
+        """
+        Whether to disable automatic XCTest screenshots.
+        """
+        return self.get_capability(DISABLE_AUTOMATIC_SCREENSHOTS)
+
+    @disable_automatic_screenshots.setter
+    def disable_automatic_screenshots(self, value: bool) -> None:
+        """
+        Disable automatic screenshots taken by XCTest at every interaction.
+        Default is up to WebDriverAgent's config to decide, which currently
+        defaults to true.
+        """
+        self.set_capability(DISABLE_AUTOMATIC_SCREENSHOTS, value)
diff --git a/appium/options/ios/xcuitest/wda/force_app_launch_option.py b/appium/options/ios/xcuitest/wda/force_app_launch_option.py
new file mode 100644
index 000000000..364133d03
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/force_app_launch_option.py
@@ -0,0 +1,42 @@
+# 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
+
+FORCE_APP_LAUNCH = 'forceAppLaunch'
+
+
+class ForceAppLaunchOption(SupportsCapabilities):
+    @property
+    def force_app_launch(self) -> Optional[bool]:
+        """
+        Whether to enforce app restart on session startup.
+        """
+        return self.get_capability(FORCE_APP_LAUNCH)
+
+    @force_app_launch.setter
+    def force_app_launch(self, value: bool) -> None:
+        """
+        Specify if the app should be forcefully restarted if it is already
+        running on session startup. This capability only has an effect if an
+        application identifier has been passed to the test session (either
+        explicitly, by setting bundleId, or implicitly, by providing app).
+        Default is true unless noReset capability is set to true.
+        """
+        self.set_capability(FORCE_APP_LAUNCH, value)
diff --git a/appium/options/ios/xcuitest/wda/keychain_password_option.py b/appium/options/ios/xcuitest/wda/keychain_password_option.py
new file mode 100644
index 000000000..5514519f0
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/keychain_password_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+KEYCHAIN_PASSWORD = 'keychainPassword'
+
+
+class KeychainPasswordOption(SupportsCapabilities):
+    @property
+    def keychain_password(self) -> Optional[str]:
+        """
+        Custom keychain password.
+        """
+        return self.get_capability(KEYCHAIN_PASSWORD)
+
+    @keychain_password.setter
+    def keychain_password(self, value: str) -> None:
+        """
+        Custom keychain password. The keychain is expected to
+        contain the private development key.
+        """
+        self.set_capability(KEYCHAIN_PASSWORD, value)
diff --git a/appium/options/ios/xcuitest/wda/keychain_path_option.py b/appium/options/ios/xcuitest/wda/keychain_path_option.py
new file mode 100644
index 000000000..5cf397426
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/keychain_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+KEYCHAIN_PATH = 'keychainPath'
+
+
+class KeychainPathOption(SupportsCapabilities):
+    @property
+    def keychain_path(self) -> Optional[str]:
+        """
+        Path to a custom keychain.
+        """
+        return self.get_capability(KEYCHAIN_PATH)
+
+    @keychain_path.setter
+    def keychain_path(self, value: str) -> None:
+        """
+        Path to a custom keychain, which
+        contains the private development key.
+        """
+        self.set_capability(KEYCHAIN_PATH, value)
diff --git a/appium/options/ios/xcuitest/wda/max_typing_frequency_option.py b/appium/options/ios/xcuitest/wda/max_typing_frequency_option.py
new file mode 100644
index 000000000..d5ea8dea1
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/max_typing_frequency_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+MAX_TYPING_FREQUENCY = 'maxTypingFrequency'
+
+
+class MaxTypingFrequencyOption(SupportsCapabilities):
+    @property
+    def max_typing_frequency(self) -> Optional[int]:
+        """
+        The number of keystrokes per minute.
+        """
+        return self.get_capability(MAX_TYPING_FREQUENCY)
+
+    @max_typing_frequency.setter
+    def max_typing_frequency(self, value: int) -> None:
+        """
+        Maximum frequency of keystrokes for typing and clear. If your tests
+        are failing because of typing errors, you may want to adjust this.
+        Defaults to 60 keystrokes per minute.
+        """
+        self.set_capability(MAX_TYPING_FREQUENCY, value)
diff --git a/appium/options/ios/xcuitest/wda/mjpeg_server_port_option.py b/appium/options/ios/xcuitest/wda/mjpeg_server_port_option.py
new file mode 100644
index 000000000..679c1af4a
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/mjpeg_server_port_option.py
@@ -0,0 +1,42 @@
+# 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
+
+MJPEG_SERVER_PORT = 'mjpegServerPort'
+
+
+class MjpegServerPortOption(SupportsCapabilities):
+    @property
+    def mjpeg_server_port(self) -> Optional[int]:
+        """
+        Port number on which WDA broadcasts screenshots stream encoded into MJPEG
+        format from the device under test.
+        """
+        return self.get_capability(MJPEG_SERVER_PORT)
+
+    @mjpeg_server_port.setter
+    def mjpeg_server_port(self, value: int) -> None:
+        """
+        Port number on which WDA broadcasts screenshots stream encoded into MJPEG
+        format from the device under test. It might be necessary to change this value
+        if the default port is busy because of other tests running in parallel.
+        Default value: 9100.
+        """
+        self.set_capability(MJPEG_SERVER_PORT, value)
diff --git a/appium/options/ios/xcuitest/wda/process_arguments_option.py b/appium/options/ios/xcuitest/wda/process_arguments_option.py
new file mode 100644
index 000000000..5b169a945
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/process_arguments_option.py
@@ -0,0 +1,42 @@
+# 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, List, Optional, Union
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+PROCESS_ARGUMENTS = 'processArguments'
+
+
+class ProcessArgumentsOption(SupportsCapabilities):
+    @property
+    def process_arguments(self) -> Optional[Dict[str, Union[List[str], Dict[str, str]]]]:
+        """
+        Command line arguments and/or environment variables of the application under test.
+        """
+        return self.get_capability(PROCESS_ARGUMENTS)
+
+    @process_arguments.setter
+    def process_arguments(self, value: Dict[str, Union[List[str], Dict[str, str]]]) -> None:
+        """
+        Provides process arguments and environment which will be sent
+        to the WebDriverAgent server. Acceptable dictionary keys are 'env'
+        and 'args'. The value of 'args' should be a list of app command line
+        arguments represented as strings and the value of 'env' is expected to
+        be a dictionary of environment variable names and their values (also strings).
+        """
+        self.set_capability(PROCESS_ARGUMENTS, value)
diff --git a/appium/options/ios/xcuitest/wda/result_bundle_path_option.py b/appium/options/ios/xcuitest/wda/result_bundle_path_option.py
new file mode 100644
index 000000000..5cf7ec098
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/result_bundle_path_option.py
@@ -0,0 +1,42 @@
+# 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
+
+RESULT_BUNDLE_PATH = 'resultBundlePath'
+
+
+class ResultBundlePathOption(SupportsCapabilities):
+    @property
+    def result_bundle_path(self) -> Optional[str]:
+        """
+        Path where the resulting XCTest bundle should be stored.
+        """
+        return self.get_capability(RESULT_BUNDLE_PATH)
+
+    @result_bundle_path.setter
+    def result_bundle_path(self, value: str) -> None:
+        """
+        Specify the path to the result bundle path as xcodebuild argument for
+        WebDriverAgent build under a security flag. WebDriverAgent process must
+        start/stop every time to pick up changed value of this property.
+        Specifying useNewWDA to true may help there. Please read 'man xcodebuild'
+        for more details.
+        """
+        self.set_capability(RESULT_BUNDLE_PATH, value)
diff --git a/appium/options/ios/xcuitest/wda/screenshot_quality_option.py b/appium/options/ios/xcuitest/wda/screenshot_quality_option.py
new file mode 100644
index 000000000..d808db9d6
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/screenshot_quality_option.py
@@ -0,0 +1,42 @@
+# 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
+
+SCREENSHOT_QUALITY = 'screenshotQuality'
+
+
+class ScreenshotQualityOption(SupportsCapabilities):
+    @property
+    def screenshot_quality(self) -> Optional[int]:
+        """
+        Screenshot quality value.
+        """
+        return self.get_capability(SCREENSHOT_QUALITY)
+
+    @screenshot_quality.setter
+    def screenshot_quality(self, value: int) -> None:
+        """
+        Changes the quality of phone display screenshots following
+        xctest/xctimagequality Default value is 1. 0 is the highest and
+        2 is the lowest quality. You can also change it via settings
+        command. 0 might cause OutOfMemory crash on high-resolution
+        devices like iPad Pro.
+        """
+        self.set_capability(SCREENSHOT_QUALITY, value)
diff --git a/appium/options/ios/xcuitest/wda/should_terminate_app_option.py b/appium/options/ios/xcuitest/wda/should_terminate_app_option.py
new file mode 100644
index 000000000..8550627cf
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/should_terminate_app_option.py
@@ -0,0 +1,42 @@
+# 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
+
+SHOULD_TERMINATE_APP = 'shouldTerminateApp'
+
+
+class ShouldTerminateAppOption(SupportsCapabilities):
+    @property
+    def should_terminate_app(self) -> Optional[bool]:
+        """
+        Whether to enforce app termination on session quit.
+        """
+        return self.get_capability(SHOULD_TERMINATE_APP)
+
+    @should_terminate_app.setter
+    def should_terminate_app(self, value: bool) -> None:
+        """
+        Specify if the app should be terminated on session end.
+        This capability only has an effect if an application identifier
+        has been passed to the test session (either explicitly,
+        by setting bundleId, or implicitly, by providing app).
+        Default is true unless noReset capability is set to true.
+        """
+        self.set_capability(SHOULD_TERMINATE_APP, value)
diff --git a/appium/options/ios/xcuitest/wda/should_use_singleton_test_manager_option.py b/appium/options/ios/xcuitest/wda/should_use_singleton_test_manager_option.py
new file mode 100644
index 000000000..569236810
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/should_use_singleton_test_manager_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SHOULD_USE_SINGLETON_TEST_MANAGER = 'shouldUseSingletonTestManager'
+
+
+class ShouldUseSingletonTestManagerOption(SupportsCapabilities):
+    @property
+    def should_use_singleton_test_manager(self) -> Optional[bool]:
+        """
+        Whether to use the default proxy for test management within WebDriverAgent.
+        """
+        return self.get_capability(SHOULD_USE_SINGLETON_TEST_MANAGER)
+
+    @should_use_singleton_test_manager.setter
+    def should_use_singleton_test_manager(self, value: bool) -> None:
+        """
+        Use default proxy for test management within WebDriverAgent. Setting this to false
+        sometimes helps with socket hangup problems. Defaults to true.
+        """
+        self.set_capability(SHOULD_USE_SINGLETON_TEST_MANAGER, value)
diff --git a/appium/options/ios/xcuitest/wda/show_xcode_log_option.py b/appium/options/ios/xcuitest/wda/show_xcode_log_option.py
new file mode 100644
index 000000000..0e435b71d
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/show_xcode_log_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SHOW_XCODE_LOG = 'showXcodeLog'
+
+
+class ShowXcodeLogOption(SupportsCapabilities):
+    @property
+    def show_xcode_log(self) -> Optional[bool]:
+        """
+        Whether to display the output of the Xcode command used to run the tests.
+        """
+        return self.get_capability(SHOW_XCODE_LOG)
+
+    @show_xcode_log.setter
+    def show_xcode_log(self, value: bool) -> None:
+        """
+        Whether to display the output of the Xcode command used to run the tests in
+        server logs. If this is true, there will be lots of extra logging at startup.
+        Defaults to false.
+        """
+        self.set_capability(SHOW_XCODE_LOG, value)
diff --git a/appium/options/ios/xcuitest/wda/simple_is_visible_check_option.py b/appium/options/ios/xcuitest/wda/simple_is_visible_check_option.py
new file mode 100644
index 000000000..694ed5743
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/simple_is_visible_check_option.py
@@ -0,0 +1,42 @@
+# 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
+
+SIMPLE_IS_VISIBLE_CHECK = 'simpleIsVisibleCheck'
+
+
+class SimpleIsVisibleCheckOption(SupportsCapabilities):
+    @property
+    def simple_is_visible_check(self) -> Optional[bool]:
+        """
+        Whether to use native methods for determining visibility of elements.
+        """
+        return self.get_capability(SIMPLE_IS_VISIBLE_CHECK)
+
+    @simple_is_visible_check.setter
+    def simple_is_visible_check(self, value: bool) -> None:
+        """
+        Use native methods for determining visibility of elements.
+        In some cases this takes a long time. Setting this capability to false will
+        cause the system to use the position and size of elements to make sure they
+        are visible on the screen. This can, however, lead to false results in some
+        situations. Defaults to false, except iOS 9.3, where it defaults to true.
+        """
+        self.set_capability(SIMPLE_IS_VISIBLE_CHECK, value)
diff --git a/appium/options/ios/xcuitest/wda/updated_wda_bundle_id_option.py b/appium/options/ios/xcuitest/wda/updated_wda_bundle_id_option.py
new file mode 100644
index 000000000..b799c949b
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/updated_wda_bundle_id_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+UPDATED_WDA_BUNDLE_ID = 'updatedWDABundleId'
+
+
+class UpdatedWdaBundleIdOption(SupportsCapabilities):
+    @property
+    def updated_wda_bundle_id(self) -> Optional[str]:
+        """
+        WDA bundle identifier.
+        """
+        return self.get_capability(UPDATED_WDA_BUNDLE_ID)
+
+    @updated_wda_bundle_id.setter
+    def updated_wda_bundle_id(self, value: str) -> None:
+        """
+        Bundle id to update WDA to before building and launching on real devices.
+        This bundle id must be associated with a valid provisioning profile.
+        """
+        self.set_capability(UPDATED_WDA_BUNDLE_ID, value)
diff --git a/appium/options/ios/xcuitest/wda/use_native_caching_strategy_option.py b/appium/options/ios/xcuitest/wda/use_native_caching_strategy_option.py
new file mode 100644
index 000000000..a3e30de17
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/use_native_caching_strategy_option.py
@@ -0,0 +1,41 @@
+# 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
+
+USE_NATIVE_CACHING_STRATEGY = 'useNativeCachingStrategy'
+
+
+class UseNativeCachingStrategyOption(SupportsCapabilities):
+    @property
+    def use_native_caching_strategy(self) -> Optional[bool]:
+        """
+        Whether to use the native caching strategy.
+        """
+        return self.get_capability(USE_NATIVE_CACHING_STRATEGY)
+
+    @use_native_caching_strategy.setter
+    def use_native_caching_strategy(self, value: bool) -> None:
+        """
+        Set this capability to false in order to use the custom elements caching
+        strategy. This might help to avoid stale element exception on property
+        change. By default, the native XCTest cache resolution is used (true)
+        for all native locators (e.g. all, but xpath).
+        """
+        self.set_capability(USE_NATIVE_CACHING_STRATEGY, value)
diff --git a/appium/options/ios/xcuitest/wda/use_new_wda_option.py b/appium/options/ios/xcuitest/wda/use_new_wda_option.py
new file mode 100644
index 000000000..f52684a65
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/use_new_wda_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+USE_NEW_WDA = 'useNewWDA'
+
+
+class UseNewWdaOption(SupportsCapabilities):
+    @property
+    def use_new_wda(self) -> Optional[bool]:
+        """
+        Whether whether to uninstall of any existing WebDriverAgent app
+        on the device under test.
+        """
+        return self.get_capability(USE_NEW_WDA)
+
+    @use_new_wda.setter
+    def use_new_wda(self, value: bool) -> None:
+        """
+        If true, forces uninstall of any existing WebDriverAgent app on device.
+        Set it to true if you want to apply different startup options for WebDriverAgent
+        for each session. Although, it is only guaranteed to work stable on Simulator.
+        Real devices require WebDriverAgent client to run for as long as possible without
+        reinstall/restart to avoid issues like
+        https://github.com/facebook/WebDriverAgent/issues/507. The false value
+        (the default behaviour since driver version 2.35.0) will try to
+        detect currently running WDA listener executed by previous testing session(s)
+        and reuse it if possible, which is highly recommended for real device testing
+        and to speed up suites of multiple tests in general. A new WDA session will be
+        triggered at the default URL (http://localhost:8100) if WDA is not listening and
+        webDriverAgentUrl capability is not set. The negative/unset value of useNewWDA
+        capability has no effect prior to xcuitest driver version 2.35.0.
+        """
+        self.set_capability(USE_NEW_WDA, value)
diff --git a/appium/options/ios/xcuitest/wda/use_prebuilt_wda_option.py b/appium/options/ios/xcuitest/wda/use_prebuilt_wda_option.py
new file mode 100644
index 000000000..76fd5163e
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/use_prebuilt_wda_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+USE_PREBUILT_WDA = 'usePrebuiltWDA'
+
+
+class UsePrebuiltWdaOption(SupportsCapabilities):
+    @property
+    def use_prebuilt_wda(self) -> Optional[bool]:
+        """
+        Whether to skip the build phase of running the WDA app.
+        """
+        return self.get_capability(USE_PREBUILT_WDA)
+
+    @use_prebuilt_wda.setter
+    def use_prebuilt_wda(self, value: bool) -> None:
+        """
+        Skips the build phase of running the WDA app. Building is then the responsibility
+        of the user. Only works for Xcode 8+. Defaults to false.
+        """
+        self.set_capability(USE_PREBUILT_WDA, value)
diff --git a/appium/options/ios/xcuitest/wda/use_simple_build_test_option.py b/appium/options/ios/xcuitest/wda/use_simple_build_test_option.py
new file mode 100644
index 000000000..516e08cbe
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/use_simple_build_test_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+USE_SIMPLE_BUILD_TEST = 'useSimpleBuildTest'
+
+
+class UseSimpleBuildTestOption(SupportsCapabilities):
+    @property
+    def use_simple_build_test(self) -> Optional[bool]:
+        """
+        Whether to enforce app termination on session quit.
+        """
+        return self.get_capability(USE_SIMPLE_BUILD_TEST)
+
+    @use_simple_build_test.setter
+    def use_simple_build_test(self, value: bool) -> None:
+        """
+        Build with 'build' and run test with 'test' in xcodebuild for all Xcode versions if
+        this is true, or build with 'build-for-testing' and run tests with
+        'test-without-building' for over Xcode 8 if this is false. Defaults to false.
+        """
+        self.set_capability(USE_SIMPLE_BUILD_TEST, value)
diff --git a/appium/options/ios/xcuitest/wda/use_xctestrun_file_option.py b/appium/options/ios/xcuitest/wda/use_xctestrun_file_option.py
new file mode 100644
index 000000000..0d1a3c64b
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/use_xctestrun_file_option.py
@@ -0,0 +1,49 @@
+# 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
+
+USE_XCTESTRUN_FILE = 'useXctestrunFile'
+
+
+class UseXctestrunFileOption(SupportsCapabilities):
+    @property
+    def use_xctestrun_file(self) -> Optional[bool]:
+        """
+        Whether to use of .xctestrun file to launch WDA.
+        """
+        return self.get_capability(USE_XCTESTRUN_FILE)
+
+    @use_xctestrun_file.setter
+    def use_xctestrun_file(self, value: bool) -> None:
+        """
+        Use Xctestrun file to launch WDA. It will search for such file in bootstrapPath.
+        Expected name of file is WebDriverAgentRunner_iphoneos&lt;sdkVersion&gt;-arm64.xctestrun for
+        real device and WebDriverAgentRunner_iphonesimulator&lt;sdkVersion&gt;-x86_64.xctestrun for
+        simulator. One can do build-for-testing for WebDriverAgent project for simulator and
+        real device and then you will see Product Folder like this and you need to copy content
+        of this folder at bootstrapPath location. Since this capability expects that you have
+        already built WDA project, it neither checks whether you have necessary dependencies to
+        build WDA nor will it try to build project. Defaults to false. Tips: Xcodebuild builds for the
+        target platform version. We'd recommend you to build with minimal OS version which you'd
+        like to run as the original WDA module. e.g. If you build WDA for 12.2, the module cannot
+        run on iOS 11.4 because of loading some module error on simulator. A module built with 11.4
+        can work on iOS 12.2. (This is xcodebuild's expected behaviour.)
+        """
+        self.set_capability(USE_XCTESTRUN_FILE, value)
diff --git a/appium/options/ios/xcuitest/wda/wait_for_idle_timeout_option.py b/appium/options/ios/xcuitest/wda/wait_for_idle_timeout_option.py
new file mode 100644
index 000000000..ff994d43e
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wait_for_idle_timeout_option.py
@@ -0,0 +1,45 @@
+# 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
+
+WAIT_FOR_IDLE_TIMEOUT = 'waitForIdleTimeout'
+
+
+class WaitForIdleTimeoutOption(SupportsCapabilities):
+    @property
+    def wait_for_idle_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout to wait until WDA responds to HTTP requests.
+        """
+        value = self.get_capability(WAIT_FOR_IDLE_TIMEOUT)
+        return None if value is None else timedelta(seconds=value)
+
+    @wait_for_idle_timeout.setter
+    def wait_for_idle_timeout(self, value: Union[timedelta, float]) -> None:
+        """
+        The time to wait until the application under test is idling.
+        XCTest requires the app's main thread to be idling in order to execute any action on it,
+        so WDA might not even start/freeze if the app under test is constantly hogging the main
+        thread. The default value is 10 (seconds). Setting it to zero disables idling checks completely
+        (not recommended) and has the same effect as setting waitForQuiescence to false.
+        Available since Appium 1.20.0.
+        """
+        self.set_capability(WAIT_FOR_IDLE_TIMEOUT, value.total_seconds() if isinstance(value, timedelta) else value)
diff --git a/appium/options/ios/xcuitest/wda/wait_for_quiescence_option.py b/appium/options/ios/xcuitest/wda/wait_for_quiescence_option.py
new file mode 100644
index 000000000..b8be7f322
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wait_for_quiescence_option.py
@@ -0,0 +1,42 @@
+# 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
+
+WAIT_FOR_QUIESCENCE = 'waitForQuiescence'
+
+
+class WaitForQuiescenceOption(SupportsCapabilities):
+    @property
+    def wait_for_quiescence(self) -> Optional[bool]:
+        """
+        Whether to wait for application quiescence.
+        """
+        return self.get_capability(WAIT_FOR_QUIESCENCE)
+
+    @wait_for_quiescence.setter
+    def wait_for_quiescence(self, value: Union[timedelta, float]) -> None:
+        """
+        It allows to turn on/off waiting for application quiescence in WebDriverAgent,
+        while performing queries. The default value is true. You can avoid this kind
+        of issues if you turn it off. Consider using waitForIdleTimeout capability
+        instead for this purpose since Appium 1.20.0.
+        """
+        self.set_capability(WAIT_FOR_QUIESCENCE, value)
diff --git a/appium/options/ios/xcuitest/wda/wda_base_url_option.py b/appium/options/ios/xcuitest/wda/wda_base_url_option.py
new file mode 100644
index 000000000..c2ca5cd81
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wda_base_url_option.py
@@ -0,0 +1,41 @@
+# 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
+
+WDA_BASE_URL = 'wdaBaseUrl'
+
+
+class WdaBaseUrlOption(SupportsCapabilities):
+    @property
+    def wda_base_url(self) -> Optional[str]:
+        """
+        Prefix to build a custom WebDriverAgent URL.
+        """
+        return self.get_capability(WDA_BASE_URL)
+
+    @wda_base_url.setter
+    def wda_base_url(self, value: str) -> None:
+        """
+        This value, if specified, will be used as a prefix to build a custom
+        WebDriverAgent url. It is different from webDriverAgentUrl, because
+        if the latter is set then it expects WebDriverAgent to be already
+        listening and skips the building phase. Defaults to http://localhost.
+        """
+        self.set_capability(WDA_BASE_URL, value)
diff --git a/appium/options/ios/xcuitest/wda/wda_connection_timeout_option.py b/appium/options/ios/xcuitest/wda/wda_connection_timeout_option.py
new file mode 100644
index 000000000..e21bb1834
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wda_connection_timeout_option.py
@@ -0,0 +1,43 @@
+# 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
+
+WDA_CONNECTION_TIMEOUT = 'wdaConnectionTimeout'
+
+
+class WdaConnectionTimeoutOption(SupportsCapabilities):
+    @property
+    def wda_connection_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout to wait until WDA responds to HTTP requests.
+        """
+        value = self.get_capability(WDA_CONNECTION_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @wda_connection_timeout.setter
+    def wda_connection_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Connection timeout to wait for a response from WebDriverAgent.
+        Defaults to 240000ms.
+        """
+        self.set_capability(
+            WDA_CONNECTION_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/ios/xcuitest/wda/wda_eventloop_idle_delay_option.py b/appium/options/ios/xcuitest/wda/wda_eventloop_idle_delay_option.py
new file mode 100644
index 000000000..e957f2690
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wda_eventloop_idle_delay_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 datetime import timedelta
+from typing import Optional, Union
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+WDA_EVENTLOOP_IDLE_DELAY = 'wdaEventloopIdleDelay'
+
+
+class WdaEventloopIdleDelayOption(SupportsCapabilities):
+    @property
+    def wda_eventloop_idle_delay(self) -> Optional[timedelta]:
+        """
+        Event loop idle delay.
+        """
+        value = self.get_capability(WDA_EVENTLOOP_IDLE_DELAY)
+        return None if value is None else timedelta(seconds=value)
+
+    @wda_eventloop_idle_delay.setter
+    def wda_eventloop_idle_delay(self, value: Union[timedelta, float]) -> None:
+        """
+        Delays the invocation of -[XCUIApplicationProcess setEventLoopHasIdled:] by the
+        duration specified with this capability. This can help quiescence apps
+        that fail to do so for no obvious reason (and creating a session fails for
+        that reason). This increases the time for session creation
+        because -[XCUIApplicationProcess setEventLoopHasIdled:] is called multiple times.
+        If you enable this capability start with at least 3 seconds and try increasing it,
+        if creating the session still fails. Defaults to 0.
+        """
+        self.set_capability(WDA_EVENTLOOP_IDLE_DELAY, value.total_seconds() 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
new file mode 100644
index 000000000..325b7e40e
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wda_launch_timeout_option.py
@@ -0,0 +1,41 @@
+# 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
+
+WDA_LAUNCH_TIMEOUT = 'wdaLaunchTimeout'
+
+
+class WdaLaunchTimeoutOption(SupportsCapabilities):
+    @property
+    def wda_launch_timeout(self) -> Optional[timedelta]:
+        """
+        Maximum timeout to wait until WDA is listening.
+        """
+        value = self.get_capability(WDA_LAUNCH_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @wda_launch_timeout.setter
+    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)
diff --git a/appium/options/ios/xcuitest/wda/wda_local_port_option.py b/appium/options/ios/xcuitest/wda/wda_local_port_option.py
new file mode 100644
index 000000000..1af241a35
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wda_local_port_option.py
@@ -0,0 +1,41 @@
+# 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
+
+WDA_LOCAL_PORT = 'wdaLocalPort'
+
+
+class WdaLocalPortOption(SupportsCapabilities):
+    @property
+    def wda_local_port(self) -> Optional[int]:
+        """
+        Local port number where the WDA traffic is being forwarded.
+        """
+        return self.get_capability(WDA_LOCAL_PORT)
+
+    @wda_local_port.setter
+    def wda_local_port(self, value: int) -> None:
+        """
+        This value, if specified, will be used to forward traffic from
+        Mac host to real ios devices over USB.
+        Default value is the same as the port number used by WDA on
+        the device under test (8100).
+        """
+        self.set_capability(WDA_LOCAL_PORT, value)
diff --git a/appium/options/ios/xcuitest/wda/wda_startup_retries_option.py b/appium/options/ios/xcuitest/wda/wda_startup_retries_option.py
new file mode 100644
index 000000000..a70999621
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wda_startup_retries_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+WDA_STARTUP_RETRIES = 'wdaStartupRetries'
+
+
+class WdaStartupRetriesOption(SupportsCapabilities):
+    @property
+    def wda_startup_retries(self) -> Optional[int]:
+        """
+        Number of retries before to fail WDA deployment.
+        """
+        return self.get_capability(WDA_STARTUP_RETRIES)
+
+    @wda_startup_retries.setter
+    def wda_startup_retries(self, value: int) -> None:
+        """
+        Number of times to try to build and launch WebDriverAgent onto the device.
+        Defaults to 2.
+        """
+        self.set_capability(WDA_STARTUP_RETRIES, value)
diff --git a/appium/options/ios/xcuitest/wda/wda_startup_retry_interval_option.py b/appium/options/ios/xcuitest/wda/wda_startup_retry_interval_option.py
new file mode 100644
index 000000000..99d89daa6
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/wda_startup_retry_interval_option.py
@@ -0,0 +1,43 @@
+# 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
+
+WDA_STARTUP_RETRY_INTERVAL = 'wdaStartupRetryInterval'
+
+
+class WdaStartupRetryIntervalOption(SupportsCapabilities):
+    @property
+    def wda_startup_retry_interval(self) -> Optional[timedelta]:
+        """
+        Interval to wait between tries to build and launch WebDriverAgent.
+        """
+        value = self.get_capability(WDA_STARTUP_RETRY_INTERVAL)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @wda_startup_retry_interval.setter
+    def wda_startup_retry_interval(self, value: Union[timedelta, int]) -> None:
+        """
+        Time interval to wait between tries to build and launch WebDriverAgent.
+        Defaults to 10000ms.
+        """
+        self.set_capability(
+            WDA_STARTUP_RETRY_INTERVAL, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/ios/xcuitest/wda/web_driver_agent_url_option.py b/appium/options/ios/xcuitest/wda/web_driver_agent_url_option.py
new file mode 100644
index 000000000..5dd89f2c6
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/web_driver_agent_url_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+WEB_DRIVER_AGENT_URL = 'webDriverAgentUrl'
+
+
+class WebDriverAgentUrlOption(SupportsCapabilities):
+    @property
+    def web_driver_agent_url(self) -> Optional[str]:
+        """
+        WedDriverAgent URL.
+        """
+        return self.get_capability(WEB_DRIVER_AGENT_URL)
+
+    @web_driver_agent_url.setter
+    def web_driver_agent_url(self, value: str) -> None:
+        """
+        If provided, Appium will connect to an existing WebDriverAgent
+        instance at this URL instead of starting a new one.
+        """
+        self.set_capability(WEB_DRIVER_AGENT_URL, value)
diff --git a/appium/options/ios/xcuitest/wda/xcode_org_id_option.py b/appium/options/ios/xcuitest/wda/xcode_org_id_option.py
new file mode 100644
index 000000000..d5c517935
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/xcode_org_id_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+XCODE_ORG_ID = 'xcodeOrgId'
+
+
+class XcodeOrgIdOption(SupportsCapabilities):
+    @property
+    def xcode_org_id(self) -> Optional[str]:
+        """
+        Signing certificate organization id for WebDriverAgent compilation.
+        """
+        return self.get_capability(XCODE_ORG_ID)
+
+    @xcode_org_id.setter
+    def xcode_org_id(self, value: str) -> None:
+        """
+        Provides a signing certificate organization id for WebDriverAgent compilation.
+        If signing id is not provided then it defaults to "iPhone Developer"
+        """
+        self.set_capability(XCODE_ORG_ID, value)
diff --git a/appium/options/ios/xcuitest/wda/xcode_signing_id_option.py b/appium/options/ios/xcuitest/wda/xcode_signing_id_option.py
new file mode 100644
index 000000000..08ba1855e
--- /dev/null
+++ b/appium/options/ios/xcuitest/wda/xcode_signing_id_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+XCODE_SIGNING_ID = 'xcodeSigningId'
+
+
+class XcodeSigningIdOption(SupportsCapabilities):
+    @property
+    def xcode_signing_id(self) -> Optional[str]:
+        """
+        Signing certificate for WebDriverAgent compilation.
+        """
+        return self.get_capability(XCODE_SIGNING_ID)
+
+    @xcode_signing_id.setter
+    def xcode_signing_id(self, value: str) -> None:
+        """
+        Provides a signing certificate for WebDriverAgent compilation.
+        If signing id is not provided then it defaults to "iPhone Developer"
+        """
+        self.set_capability(XCODE_SIGNING_ID, value)
diff --git a/appium/options/ios/xcuitest/webview/__init__.py b/appium/options/ios/xcuitest/webview/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/ios/xcuitest/webview/absolute_web_locations_option.py b/appium/options/ios/xcuitest/webview/absolute_web_locations_option.py
new file mode 100644
index 000000000..114c737dc
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/absolute_web_locations_option.py
@@ -0,0 +1,42 @@
+# 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
+
+ABSOLUTE_WEB_LOCATIONS = 'absoluteWebLocations'
+
+
+class AbsoluteWebLocationsOption(SupportsCapabilities):
+    @property
+    def absolute_web_locations(self) -> Optional[bool]:
+        """
+        Whether Get Element Location returns coordinates
+        relative to the page origin for web view elements.
+        """
+        return self.get_capability(ABSOLUTE_WEB_LOCATIONS)
+
+    @absolute_web_locations.setter
+    def absolute_web_locations(self, value: bool) -> None:
+        """
+        This capability will direct the Get Element Location command, when used
+        within webviews, to return coordinates which are relative to the origin of
+        the page, rather than relative to the current scroll offset. This capability
+        has no effect outside of webviews. Defaults to  false.
+        """
+        self.set_capability(ABSOLUTE_WEB_LOCATIONS, value)
diff --git a/appium/options/ios/xcuitest/webview/additional_webview_bundle_ids_option.py b/appium/options/ios/xcuitest/webview/additional_webview_bundle_ids_option.py
new file mode 100644
index 000000000..6546c548f
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/additional_webview_bundle_ids_option.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 List, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ADDITIONAL_WEBVIEW_BUNDLE_IDS = 'additionalWebviewBundleIds'
+
+
+class AdditionalWebviewBundleIdsOption(SupportsCapabilities):
+    @property
+    def additional_webview_bundle_ids(self) -> Optional[List[str]]:
+        """
+        Array of possible bundle identifiers for webviews.
+        """
+        return self.get_capability(ADDITIONAL_WEBVIEW_BUNDLE_IDS)
+
+    @additional_webview_bundle_ids.setter
+    def additional_webview_bundle_ids(self, value: List[str]) -> None:
+        """
+        Array of possible bundle identifiers for webviews. This is sometimes
+        necessary if the Web Inspector is found to be returning a modified
+        bundle identifier for the app. Defaults to [].
+        """
+        self.set_capability(ADDITIONAL_WEBVIEW_BUNDLE_IDS, value)
diff --git a/appium/options/ios/xcuitest/webview/enable_async_execute_from_https_option.py b/appium/options/ios/xcuitest/webview/enable_async_execute_from_https_option.py
new file mode 100644
index 000000000..ae2a3dcc1
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/enable_async_execute_from_https_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ENABLE_ASYNC_EXECUTE_FROM_HTTPS = 'enableAsyncExecuteFromHttps'
+
+
+class EnableAsyncExecuteFromHttpsOption(SupportsCapabilities):
+    @property
+    def enable_async_execute_from_https(self) -> Optional[bool]:
+        """
+        Whether to allow simulators to execute async JavaScript on pages using HTTPS.
+        """
+        return self.get_capability(ENABLE_ASYNC_EXECUTE_FROM_HTTPS)
+
+    @enable_async_execute_from_https.setter
+    def enable_async_execute_from_https(self, value: bool) -> None:
+        """
+        Capability to allow simulators to execute asynchronous JavaScript
+        on pages using HTTPS. Defaults to false.
+        """
+        self.set_capability(ENABLE_ASYNC_EXECUTE_FROM_HTTPS, value)
diff --git a/appium/options/ios/xcuitest/webview/full_context_list_option.py b/appium/options/ios/xcuitest/webview/full_context_list_option.py
new file mode 100644
index 000000000..7bdd3102d
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/full_context_list_option.py
@@ -0,0 +1,42 @@
+# 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
+
+FULL_CONTEXT_LIST = 'fullContextList'
+
+
+class FullContextListOption(SupportsCapabilities):
+    @property
+    def full_context_list(self) -> Optional[bool]:
+        """
+        Whether to return the detailed information on contexts for the get available
+        context command.
+        """
+        return self.get_capability(FULL_CONTEXT_LIST)
+
+    @full_context_list.setter
+    def full_context_list(self, value: bool) -> None:
+        """
+        Sets to return the detailed information on contexts for the get available
+        context command. If this capability is enabled, then each item in the returned
+        contexts list would additionally include WebView title, full URL and the bundle
+        identifier. Defaults to false.
+        """
+        self.set_capability(FULL_CONTEXT_LIST, value)
diff --git a/appium/options/ios/xcuitest/webview/include_safari_in_webviews_option.py b/appium/options/ios/xcuitest/webview/include_safari_in_webviews_option.py
new file mode 100644
index 000000000..c21cb141c
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/include_safari_in_webviews_option.py
@@ -0,0 +1,41 @@
+# 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
+
+INCLUDE_SAFARI_IN_WEBVIEWS = 'includeSafariInWebviews'
+
+
+class IncludeSafariInWebviewsOption(SupportsCapabilities):
+    @property
+    def include_safari_in_webviews(self) -> Optional[bool]:
+        """
+        Whether to add Safari web views to the list of contexts available
+        during a native/webview app test.
+        """
+        return self.get_capability(INCLUDE_SAFARI_IN_WEBVIEWS)
+
+    @include_safari_in_webviews.setter
+    def include_safari_in_webviews(self, value: bool) -> None:
+        """
+        Add Safari web contexts to the list of contexts available during a
+        native/webview app test. This is useful if the test opens Safari and
+        needs to be able to interact with it. Defaults to false.
+        """
+        self.set_capability(INCLUDE_SAFARI_IN_WEBVIEWS, value)
diff --git a/appium/options/ios/xcuitest/webview/native_web_tap_option.py b/appium/options/ios/xcuitest/webview/native_web_tap_option.py
new file mode 100644
index 000000000..02a40b98e
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/native_web_tap_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+NATIVE_WEB_TAP = 'nativeWebTap'
+
+
+class NativeWebTapOption(SupportsCapabilities):
+    @property
+    def native_web_tap(self) -> Optional[bool]:
+        """
+        Whether to enable native taps in web view mode.
+        """
+        return self.get_capability(NATIVE_WEB_TAP)
+
+    @native_web_tap.setter
+    def native_web_tap(self, value: bool) -> None:
+        """
+        Enable native, non-javascript-based taps being in web context mode. Defaults
+        to false. Warning: sometimes the preciseness of native taps could be broken,
+        because there is no reliable way to map web element coordinates to native ones.
+        """
+        self.set_capability(NATIVE_WEB_TAP, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_garbage_collect_option.py b/appium/options/ios/xcuitest/webview/safari_garbage_collect_option.py
new file mode 100644
index 000000000..63aefc4c4
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_garbage_collect_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SAFARI_GARBAGE_COLLECT = 'safariGarbageCollect'
+
+
+class SafariGarbageCollectOption(SupportsCapabilities):
+    @property
+    def safari_garbage_collect(self) -> Optional[bool]:
+        """
+        Whether to turn on garbage collection when executing scripts on Safari.
+        """
+        return self.get_capability(SAFARI_GARBAGE_COLLECT)
+
+    @safari_garbage_collect.setter
+    def safari_garbage_collect(self, value: bool) -> None:
+        """
+        Turns on/off Web Inspector garbage collection when executing scripts on Safari.
+        Turning on may improve performance. Defaults to false.
+        """
+        self.set_capability(SAFARI_GARBAGE_COLLECT, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_ignore_fraud_warning_option.py b/appium/options/ios/xcuitest/webview/safari_ignore_fraud_warning_option.py
new file mode 100644
index 000000000..c600285d0
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_ignore_fraud_warning_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SAFARI_IGNORE_FRAUD_WARNING = 'safariIgnoreFraudWarning'
+
+
+class SafariIgnoreFraudWarningOption(SupportsCapabilities):
+    @property
+    def safari_ignore_fraud_warning(self) -> Optional[bool]:
+        """
+        Whether to prevent Safari from showing a fraudulent website warning.
+        """
+        return self.get_capability(SAFARI_IGNORE_FRAUD_WARNING)
+
+    @safari_ignore_fraud_warning.setter
+    def safari_ignore_fraud_warning(self, value: bool) -> None:
+        """
+        Prevent Safari from showing a fraudulent website warning.
+        Default keeps current sim setting..
+        """
+        self.set_capability(SAFARI_IGNORE_FRAUD_WARNING, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_ignore_web_hostnames_option.py b/appium/options/ios/xcuitest/webview/safari_ignore_web_hostnames_option.py
new file mode 100644
index 000000000..9f48f8cf3
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_ignore_web_hostnames_option.py
@@ -0,0 +1,42 @@
+# 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
+
+SAFARI_IGNORE_WEB_HOSTNAMES = 'safariIgnoreWebHostnames'
+
+
+class SafariIgnoreWebHostnamesOption(SupportsCapabilities):
+    @property
+    def safari_ignore_web_hostnames(self) -> Optional[str]:
+        """
+        Comma-separated list of host names to be ignored.
+        """
+        return self.get_capability(SAFARI_IGNORE_WEB_HOSTNAMES)
+
+    @safari_ignore_web_hostnames.setter
+    def safari_ignore_web_hostnames(self, value: str) -> None:
+        """
+        Provide a list of hostnames (comma-separated) that the Safari automation
+        tools should ignore. This is to provide a workaround to prevent a webkit
+        bug where the web context is unintentionally changed to a 3rd party website
+        and the test gets stuck. The common culprits are search engines (yahoo, bing,
+        google) and about:blank.
+        """
+        self.set_capability(SAFARI_IGNORE_WEB_HOSTNAMES, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_initial_url_option.py b/appium/options/ios/xcuitest/webview/safari_initial_url_option.py
new file mode 100644
index 000000000..79dc14fe3
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_initial_url_option.py
@@ -0,0 +1,38 @@
+# 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
+
+SAFARI_INITIAL_URL = 'safariInitialUrl'
+
+
+class SafariInitialUrlOption(SupportsCapabilities):
+    @property
+    def safari_initial_url(self) -> Optional[str]:
+        """
+        The initial safari url.
+        """
+        return self.get_capability(SAFARI_INITIAL_URL)
+
+    @safari_initial_url.setter
+    def safari_initial_url(self, value: str) -> None:
+        """
+        Set initial safari url, default is a local welcome page.
+        """
+        self.set_capability(SAFARI_INITIAL_URL, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_log_all_communication_hex_dump_option.py b/appium/options/ios/xcuitest/webview/safari_log_all_communication_hex_dump_option.py
new file mode 100644
index 000000000..03b2e28cc
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_log_all_communication_hex_dump_option.py
@@ -0,0 +1,43 @@
+# 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
+
+SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP = 'safariLogAllCommunicationHexDump'
+
+
+class SafariLogAllCommunicationHexDumpOption(SupportsCapabilities):
+    @property
+    def safari_log_all_communication_hex_dump(self) -> Optional[bool]:
+        """
+        Whether to log of plists sent to and received from the Web Inspector
+        in hex dump format.
+        """
+        return self.get_capability(SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP)
+
+    @safari_log_all_communication_hex_dump.setter
+    def safari_log_all_communication_hex_dump(self, value: bool) -> None:
+        """
+        Log all communication sent to and received from the Web Inspector, as raw
+        hex dump and printable characters. This logging is done before any data
+        manipulation, and so can elucidate some communication issues. Like
+        appium:safariLogAllCommunication, this can produce a lot of data in some cases,
+        so it is recommended to be used only when necessary. Defaults to false.
+        """
+        self.set_capability(SAFARI_LOG_ALL_COMMUNICATION_HEX_DUMP, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_log_all_communication_option.py b/appium/options/ios/xcuitest/webview/safari_log_all_communication_option.py
new file mode 100644
index 000000000..b022dba3d
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_log_all_communication_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SAFARI_LOG_ALL_COMMUNICATION = 'safariLogAllCommunication'
+
+
+class SafariLogAllCommunicationOption(SupportsCapabilities):
+    @property
+    def safari_log_all_communication(self) -> Optional[bool]:
+        """
+        Whether to log of plists sent to and received from the Web Inspector.
+        """
+        return self.get_capability(SAFARI_LOG_ALL_COMMUNICATION)
+
+    @safari_log_all_communication.setter
+    def safari_log_all_communication(self, value: bool) -> None:
+        """
+        Log all plists sent to and received from the Web Inspector, as plain text.
+        For some operations this can be a lot of data, so it is recommended to
+        be used only when necessary. Defaults to false.
+        """
+        self.set_capability(SAFARI_LOG_ALL_COMMUNICATION, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_open_links_in_background_option.py b/appium/options/ios/xcuitest/webview/safari_open_links_in_background_option.py
new file mode 100644
index 000000000..2ebb48011
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_open_links_in_background_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SAFARI_OPEN_LINKS_IN_BACKGROUND = 'safariOpenLinksInBackground'
+
+
+class SafariOpenLinksInBackgroundOption(SupportsCapabilities):
+    @property
+    def safari_open_links_in_background(self) -> Optional[bool]:
+        """
+        Whether Safari should allow links to open in new windows.
+        """
+        return self.get_capability(SAFARI_OPEN_LINKS_IN_BACKGROUND)
+
+    @safari_open_links_in_background.setter
+    def safari_open_links_in_background(self, value: bool) -> None:
+        """
+        Whether Safari should allow links to open in new windows.
+        Default keeps current sim setting.
+        """
+        self.set_capability(SAFARI_OPEN_LINKS_IN_BACKGROUND, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_socket_chunk_size_option.py b/appium/options/ios/xcuitest/webview/safari_socket_chunk_size_option.py
new file mode 100644
index 000000000..c33ea61d2
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_socket_chunk_size_option.py
@@ -0,0 +1,41 @@
+# 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
+
+SAFARI_SOCKET_CHUNK_SIZE = 'safariSocketChunkSize'
+
+
+class SafariSocketChunkSizeOption(SupportsCapabilities):
+    @property
+    def safari_socket_chunk_size(self) -> Optional[int]:
+        """
+        Get the size of a single remote debugger socket chunk.
+        """
+        return self.get_capability(SAFARI_SOCKET_CHUNK_SIZE)
+
+    @safari_socket_chunk_size.setter
+    def safari_socket_chunk_size(self, value: int) -> None:
+        """
+        The size, in bytes, of the data to be sent to the Web Inspector on
+        iOS 11+ real devices. Some devices hang when sending large amounts of
+        data to the Web Inspector, and breaking them into smaller parts can be
+        helpful in those cases. Defaults to 16384 (also the maximum possible).
+        """
+        self.set_capability(SAFARI_SOCKET_CHUNK_SIZE, value)
diff --git a/appium/options/ios/xcuitest/webview/safari_web_inspector_max_frame_length_option.py b/appium/options/ios/xcuitest/webview/safari_web_inspector_max_frame_length_option.py
new file mode 100644
index 000000000..d92fb2d4a
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/safari_web_inspector_max_frame_length_option.py
@@ -0,0 +1,41 @@
+# 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
+
+SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH = 'safariWebInspectorMaxFrameLength'
+
+
+class SafariWebInspectorMaxFrameLengthOption(SupportsCapabilities):
+    @property
+    def safari_web_inspector_max_frame_length(self) -> Optional[int]:
+        """
+        Maximum size in bytes of a single data frame for the Web Inspector.
+        """
+        return self.get_capability(SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH)
+
+    @safari_web_inspector_max_frame_length.setter
+    def safari_web_inspector_max_frame_length(self, value: int) -> None:
+        """
+        The maximum size in bytes of a single data frame for the Web Inspector.
+        Too high values could introduce slowness and/or memory leaks.
+        Too low values could introduce possible buffer overflow exceptions.
+        Defaults to 20MiB (20*1024*1024).
+        """
+        self.set_capability(SAFARI_WEB_INSPECTOR_MAX_FRAME_LENGTH, value)
diff --git a/appium/options/ios/xcuitest/webview/webkit_response_timeout_option.py b/appium/options/ios/xcuitest/webview/webkit_response_timeout_option.py
new file mode 100644
index 000000000..1e274bf01
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/webkit_response_timeout_option.py
@@ -0,0 +1,43 @@
+# 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
+
+WEBKIT_RESPONSE_TIMEOUT = 'webkitResponseTimeout'
+
+
+class WebkitResponseTimeoutOption(SupportsCapabilities):
+    @property
+    def webkit_response_timeout(self) -> Optional[timedelta]:
+        """
+        Time to wait for a response from WebKit in a Safari session.
+        """
+        value = self.get_capability(WEBKIT_RESPONSE_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @webkit_response_timeout.setter
+    def webkit_response_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        (Real device only) Set the time to wait for a response from
+        WebKit in a Safari session. Defaults to 5000ms.
+        """
+        self.set_capability(
+            WEBKIT_RESPONSE_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/ios/xcuitest/webview/webview_connect_retries_option.py b/appium/options/ios/xcuitest/webview/webview_connect_retries_option.py
new file mode 100644
index 000000000..7792f15b0
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/webview_connect_retries_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+WEBVIEW_CONNECT_RETRIES = 'webviewConnectRetries'
+
+
+class WebviewConnectRetriesOption(SupportsCapabilities):
+    @property
+    def webview_connect_retries(self) -> Optional[int]:
+        """
+        Number of retries to send connection message to remote debugger,
+        to get a webview.
+        """
+        return self.get_capability(WEBVIEW_CONNECT_RETRIES)
+
+    @webview_connect_retries.setter
+    def webview_connect_retries(self, value: int) -> None:
+        """
+        Number of times to send connection message to remote debugger,
+        to get a webview. Default: 8.
+        """
+        self.set_capability(WEBVIEW_CONNECT_RETRIES, value)
diff --git a/appium/options/ios/xcuitest/webview/webview_connect_timeout_option.py b/appium/options/ios/xcuitest/webview/webview_connect_timeout_option.py
new file mode 100644
index 000000000..9fa742020
--- /dev/null
+++ b/appium/options/ios/xcuitest/webview/webview_connect_timeout_option.py
@@ -0,0 +1,43 @@
+# 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
+
+WEBVIEW_CONNECT_TIMEOUT = 'webviewConnectTimeout'
+
+
+class WebviewConnectTimeoutOption(SupportsCapabilities):
+    @property
+    def webview_connect_timeout(self) -> Optional[timedelta]:
+        """
+        Timeout to wait for the initial presence of webviews.
+        """
+        value = self.get_capability(WEBVIEW_CONNECT_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @webview_connect_timeout.setter
+    def webview_connect_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        The time to wait for the initial presence of webviews in
+        MobileSafari or hybrid apps. Defaults to 0ms.
+        """
+        self.set_capability(
+            WEBVIEW_CONNECT_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/mac/__init__.py b/appium/options/mac/__init__.py
new file mode 100644
index 000000000..41a091df5
--- /dev/null
+++ b/appium/options/mac/__init__.py
@@ -0,0 +1 @@
+from .mac2.base import Mac2Options
diff --git a/appium/options/mac/mac2/__init__.py b/appium/options/mac/mac2/__init__.py
new file mode 100644
index 000000000..e69de29bb
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/arguments_option.py b/appium/options/mac/mac2/arguments_option.py
new file mode 100644
index 000000000..3c9a5a70e
--- /dev/null
+++ b/appium/options/mac/mac2/arguments_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 typing import List, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ARGUMENTS = 'arguments'
+
+
+class ArgumentsOption(SupportsCapabilities):
+    @property
+    def arguments(self) -> Optional[List[str]]:
+        """
+        Array of application command line arguments.
+        """
+        return self.get_capability(ARGUMENTS)
+
+    @arguments.setter
+    def arguments(self, value: List[str]) -> None:
+        """
+        Set the array of application command line arguments. This capability is
+        only going to be applied if the application is not running on session startup.
+        """
+        self.set_capability(ARGUMENTS, value)
diff --git a/appium/options/mac/mac2/base.py b/appium/options/mac/mac2/base.py
new file mode 100644
index 000000000..d635f2b38
--- /dev/null
+++ b/appium/options/mac/mac2/base.py
@@ -0,0 +1,113 @@
+# 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 PLATFORM_NAME, AppiumOptions
+from appium.options.common.bundle_id_option import BundleIdOption
+from appium.options.common.postrun_option import PostrunOption
+from appium.options.common.prerun_option import PrerunOption
+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
+from .server_startup_timeout_option import ServerStartupTimeoutOption
+from .show_server_logs_option import ShowServerLogsOption
+from .skip_app_kill_option import SkipAppKillOption
+from .web_driver_agent_mac_url_option import WebDriverAgentMacUrlOption
+
+
+class Mac2Options(
+    AppiumOptions,
+    AppPathOption,
+    PrerunOption,
+    PostrunOption,
+    ArgumentsOption,
+    BootstrapRootOption,
+    BundleIdOption,
+    EnvironmentOption,
+    ServerStartupTimeoutOption,
+    ShowServerLogsOption,
+    SkipAppKillOption,
+    SystemHostOption,
+    SystemPortOption,
+    WebDriverAgentMacUrlOption,
+):
+    @PrerunOption.prerun.setter  # type: ignore
+    def prerun(self, value: Dict[str, str]) -> None:
+        """
+        A mapping containing either 'script' or 'command' key. The value of
+        each key must be a valid AppleScript script or command to be
+        executed after before Mac2Driver session is started. See
+        https://github.com/appium/appium-mac2-driver#applescript-commands-execution
+        for more details.
+        """
+        PrerunOption.prerun.fset(self, value)  # type: ignore
+
+    @PostrunOption.postrun.setter  # type: ignore
+    def postrun(self, value: Dict[str, str]) -> None:
+        """
+        A mapping containing either 'script' or 'command' key. The value of
+        each key must be a valid AppleScript script or command to be
+        executed after Mac2Driver session is stopped. See
+        https://github.com/appium/appium-mac2-driver#applescript-commands-execution
+        for more details.
+        """
+        PostrunOption.postrun.fset(self, value)  # type: ignore
+
+    @SystemPortOption.system_port.setter  # type: ignore
+    def system_port(self, value: int) -> None:
+        """
+        Set the number of the port for the internal server to listen on.
+        If not provided then Mac2Driver will use the default port 10100.
+        """
+        SystemPortOption.system_port.fset(self, value)  # type: ignore
+
+    @SystemHostOption.system_host.setter  # type: ignore
+    def system_host(self, value: str) -> None:
+        """
+        Set the number of the port for the internal server to listen on.
+        If not provided then Mac2Driver will use the default host
+        address 127.0.0.1. You could set it to 0.0.0.0 to make the
+        server listening on all available network interfaces.
+        It is also possible to set the particular interface name, for example en1.
+        """
+        SystemHostOption.system_host.fset(self, value)  # type: ignore
+
+    @BundleIdOption.bundle_id.setter  # type: ignore
+    def bundle_id(self, value: str) -> None:
+        """
+        Set the bundle identifier of the application to automate, for example
+        com.apple.TextEdit. This is an optional capability. If it is not provided
+        then the session will be started without an application under test
+        (actually, it will be Finder). If the application with the given
+        identifier is not installed then an error will be thrown on session
+        startup. If the application is already running then it will be moved to
+        the foreground.
+        """
+        BundleIdOption.bundle_id.fset(self, value)  # type: ignore
+
+    @property
+    def default_capabilities(self) -> Dict:
+        return {
+            AUTOMATION_NAME: 'Mac2',
+            PLATFORM_NAME: 'Mac',
+        }
diff --git a/appium/options/mac/mac2/bootstrap_root_option.py b/appium/options/mac/mac2/bootstrap_root_option.py
new file mode 100644
index 000000000..25c53d1ba
--- /dev/null
+++ b/appium/options/mac/mac2/bootstrap_root_option.py
@@ -0,0 +1,41 @@
+# 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
+
+BOOTSTRAP_ROOT = 'bootstrapRoot'
+
+
+class BootstrapRootOption(SupportsCapabilities):
+    @property
+    def bootstrap_root(self) -> Optional[str]:
+        """
+        The full path to WebDriverAgentMac root folder where Xcode project
+        of the server sources lives.
+        """
+        return self.get_capability(BOOTSTRAP_ROOT)
+
+    @bootstrap_root.setter
+    def bootstrap_root(self, value: str) -> None:
+        """
+        Set the full path to WebDriverAgentMac root folder where Xcode project
+        of the server sources lives. By default, this project is located in
+        the same folder where the corresponding driver Node.js module lives.
+        """
+        self.set_capability(BOOTSTRAP_ROOT, value)
diff --git a/appium/options/mac/mac2/environment_option.py b/appium/options/mac/mac2/environment_option.py
new file mode 100644
index 000000000..b5d72a9fc
--- /dev/null
+++ b/appium/options/mac/mac2/environment_option.py
@@ -0,0 +1,41 @@
+# 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, Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+ENVIRONMENT = 'environment'
+
+
+class EnvironmentOption(SupportsCapabilities):
+    @property
+    def environment(self) -> Optional[Dict[str, str]]:
+        """
+        Application environment variables mapping.
+        """
+        return self.get_capability(ENVIRONMENT)
+
+    @environment.setter
+    def environment(self, value: Dict[str, str]) -> None:
+        """
+        Set the dictionary of environment variables (name-&gt;value) that are going to be passed
+        to the application under test on top of environment variables inherited from
+        the parent process. This option is only going to be applied if the application
+        is not running on session startup.
+        """
+        self.set_capability(ENVIRONMENT, value)
diff --git a/appium/options/mac/mac2/server_startup_timeout_option.py b/appium/options/mac/mac2/server_startup_timeout_option.py
new file mode 100644
index 000000000..32e13f79a
--- /dev/null
+++ b/appium/options/mac/mac2/server_startup_timeout_option.py
@@ -0,0 +1,44 @@
+# 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
+
+SERVER_STARTUP_TIMEOUT = 'serverStartupTimeout'
+
+
+class ServerStartupTimeoutOption(SupportsCapabilities):
+    @property
+    def server_startup_timeout(self) -> Optional[timedelta]:
+        """
+        Get the timeout to wait util the WebDriverAgentMac
+        project is built and started.
+        """
+        value_ms = self.get_capability(SERVER_STARTUP_TIMEOUT)
+        return None if value_ms is None else timedelta(milliseconds=value_ms)
+
+    @server_startup_timeout.setter
+    def server_startup_timeout(self, value: Union[int, timedelta]) -> None:
+        """
+        Set the timeout to wait util the WebDriverAgentMac
+        project is built and started.
+        """
+        self.set_capability(
+            SERVER_STARTUP_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/mac/mac2/show_server_logs_option.py b/appium/options/mac/mac2/show_server_logs_option.py
new file mode 100644
index 000000000..635304925
--- /dev/null
+++ b/appium/options/mac/mac2/show_server_logs_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SHOW_SERVER_LOGS = 'showServerLogs'
+
+
+class ShowServerLogsOption(SupportsCapabilities):
+    @property
+    def show_server_logs(self) -> Optional[bool]:
+        """
+        Whether to show WDA server logs in the Appium log.
+        """
+        return self.get_capability(SHOW_SERVER_LOGS)
+
+    @show_server_logs.setter
+    def show_server_logs(self, value: bool) -> None:
+        """
+        Set it to true in order to include xcodebuild output to the Appium
+        server log. false by default.
+        """
+        self.set_capability(SHOW_SERVER_LOGS, value)
diff --git a/appium/options/mac/mac2/skip_app_kill_option.py b/appium/options/mac/mac2/skip_app_kill_option.py
new file mode 100644
index 000000000..adfcd19a3
--- /dev/null
+++ b/appium/options/mac/mac2/skip_app_kill_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+SKIP_APP_KILL = 'skipAppKill'
+
+
+class SkipAppKillOption(SupportsCapabilities):
+    @property
+    def skip_app_kill(self) -> Optional[bool]:
+        """
+        Whether to skip the termination of the application under test.
+        """
+        return self.get_capability(SKIP_APP_KILL)
+
+    @skip_app_kill.setter
+    def skip_app_kill(self, value: bool) -> None:
+        """
+        Set whether to skip the termination of the application under test
+        when the testing session quits. false by default. This capability
+        is only going to be applied if bundleId is set.
+        """
+        self.set_capability(SKIP_APP_KILL, value)
diff --git a/appium/options/mac/mac2/web_driver_agent_mac_url_option.py b/appium/options/mac/mac2/web_driver_agent_mac_url_option.py
new file mode 100644
index 000000000..8ffab4391
--- /dev/null
+++ b/appium/options/mac/mac2/web_driver_agent_mac_url_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+WEB_DRIVER_ARGENT_MAC_URL = 'webDriverAgentMacUrl'
+
+
+class WebDriverAgentMacUrlOption(SupportsCapabilities):
+    @property
+    def web_driver_agent_mac_url(self) -> Optional[str]:
+        """
+        The URL Appium will connect to an existing WebDriverAgentMac instance.
+        """
+        return self.get_capability(WEB_DRIVER_ARGENT_MAC_URL)
+
+    @web_driver_agent_mac_url.setter
+    def web_driver_agent_mac_url(self, value: str) -> None:
+        """
+        Set the URL Appium will connect to an existing WebDriverAgentMac
+        instance at this URL instead of starting a new one.
+        """
+        self.set_capability(WEB_DRIVER_ARGENT_MAC_URL, value)
diff --git a/appium/options/windows/__init__.py b/appium/options/windows/__init__.py
new file mode 100644
index 000000000..12d31fe34
--- /dev/null
+++ b/appium/options/windows/__init__.py
@@ -0,0 +1 @@
+from .windows.base import WindowsOptions
diff --git a/appium/options/windows/windows/__init__.py b/appium/options/windows/windows/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/options/windows/windows/app_arguments_option.py b/appium/options/windows/windows/app_arguments_option.py
new file mode 100644
index 000000000..665977d37
--- /dev/null
+++ b/appium/options/windows/windows/app_arguments_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_ARGUMENTS = 'appArguments'
+
+
+class AppArgumentsOption(SupportsCapabilities):
+    @property
+    def app_arguments(self) -> Optional[str]:
+        """
+        Application arguments string, for example `/?`.
+        """
+        return self.get_capability(APP_ARGUMENTS)
+
+    @app_arguments.setter
+    def app_arguments(self, value: str) -> None:
+        """
+        Set application arguments string, for example `/argone "/arg two"`.
+        Make sure arguments are quoted/escaped properly if necessary:
+        https://ss64.com/nt/syntax-esc.html
+        """
+        self.set_capability(APP_ARGUMENTS, value)
diff --git a/appium/options/windows/windows/app_top_level_window_option.py b/appium/options/windows/windows/app_top_level_window_option.py
new file mode 100644
index 000000000..f573fb48e
--- /dev/null
+++ b/appium/options/windows/windows/app_top_level_window_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_TOP_LEVEL_WINDOW = 'appTopLevelWindow'
+
+
+class AppTopLevelWindowOption(SupportsCapabilities):
+    @property
+    def app_top_level_window(self) -> Optional[str]:
+        """
+        Hexadecimal handle of an existing application top level window to attach to.
+        """
+        return self.get_capability(APP_TOP_LEVEL_WINDOW)
+
+    @app_top_level_window.setter
+    def app_top_level_window(self, value: str) -> None:
+        """
+        Set the hexadecimal handle of an existing application top level
+        window to attach to, for example 0x12345 (should be of string type).
+        Either this capability or app one must be provided on session startup.
+        """
+        self.set_capability(APP_TOP_LEVEL_WINDOW, value)
diff --git a/appium/options/windows/windows/app_working_dir_option.py b/appium/options/windows/windows/app_working_dir_option.py
new file mode 100644
index 000000000..0160f89aa
--- /dev/null
+++ b/appium/options/windows/windows/app_working_dir_option.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 Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+APP_WORKING_DIR = 'appWorkingDir'
+
+
+class AppWorkingDirOption(SupportsCapabilities):
+    @property
+    def app_working_dir(self) -> Optional[str]:
+        """
+        Full path to the folder, which is going to be set as the working
+        dir for the application under test.
+        """
+        return self.get_capability(APP_WORKING_DIR)
+
+    @app_working_dir.setter
+    def app_working_dir(self, value: str) -> None:
+        """
+        Set the full path to the folder, which is going to be set as the working
+        dir for the application under test. This is only applicable for classic apps.
+        """
+        self.set_capability(APP_WORKING_DIR, value)
diff --git a/appium/options/windows/windows/base.py b/appium/options/windows/windows/base.py
new file mode 100644
index 000000000..52de731e6
--- /dev/null
+++ b/appium/options/windows/windows/base.py
@@ -0,0 +1,97 @@
+# 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.app_option import AppOption
+from appium.options.common.automation_name_option import AUTOMATION_NAME
+from appium.options.common.base import PLATFORM_NAME, AppiumOptions
+from appium.options.common.postrun_option import PostrunOption
+from appium.options.common.prerun_option import PrerunOption
+from appium.options.common.system_port_option import SystemPortOption
+
+from .app_arguments_option import AppArgumentsOption
+from .app_top_level_window_option import AppTopLevelWindowOption
+from .app_working_dir_option import AppWorkingDirOption
+from .create_session_timeout_option import CreateSessionTimeoutOption
+from .expreimental_web_driver_option import ExperimentalWebDriverOption
+from .wait_for_app_launch_option import WaitForAppLaunchOption
+
+
+class WindowsOptions(
+    AppiumOptions,
+    PrerunOption,
+    PostrunOption,
+    AppOption,
+    AppTopLevelWindowOption,
+    AppWorkingDirOption,
+    CreateSessionTimeoutOption,
+    ExperimentalWebDriverOption,
+    SystemPortOption,
+    WaitForAppLaunchOption,
+    AppArgumentsOption,
+):
+    @AppOption.app.setter  # type: ignore
+    def app(self, value: str) -> None:
+        """
+        The name of the UWP application to test or full path to a classic app,
+        for example Microsoft.WindowsCalculator_8wekyb3d8bbwe!App or
+        C:\\Windows\\System32\\notepad.exe. It is also possible to set app to Root.
+        In such case the session will be invoked without any explicit target application
+        (actually, it will be Explorer). Either this capability or appTopLevelWindow must
+        be provided on session startup.
+        """
+        AppOption.app.fset(self, value)  # type: ignore
+
+    @PrerunOption.prerun.setter  # type: ignore
+    def prerun(self, value: Dict[str, str]) -> None:
+        """
+        A mapping containing either 'script' or 'command' key. The value of
+        each key must be a valid PowerShell script or command to be
+        executed prior to the WinAppDriver session startup.
+        See https://github.com/appium/appium-windows-driver#power-shell-commands-execution
+        for more details.
+        """
+        PrerunOption.prerun.fset(self, value)  # type: ignore
+
+    @PostrunOption.postrun.setter  # type: ignore
+    def postrun(self, value: Dict[str, str]) -> None:
+        """
+        A mapping containing either 'script' or 'command' key. The value of
+        each key must be a valid PowerShell script or command to be
+        executed after a WinAppDriver session is finished.
+        See https://github.com/appium/appium-windows-driver#power-shell-commands-execution
+        for more details.
+        """
+        PostrunOption.postrun.fset(self, value)  # type: ignore
+
+    @SystemPortOption.system_port.setter  # type: ignore
+    def system_port(self, value: int) -> None:
+        """
+        The port number to execute Appium Windows Driver server listener on,
+        for example 5556. The port must not be occupied. The default starting port
+        number for a new Appium Windows Driver session is 4724. If this port is
+        already busy then the next free port will be automatically selected.
+        """
+        SystemPortOption.system_port.fset(self, value)  # type: ignore
+
+    @property
+    def default_capabilities(self) -> Dict:
+        return {
+            AUTOMATION_NAME: 'Windows',
+            PLATFORM_NAME: 'Windows',
+        }
diff --git a/appium/options/windows/windows/create_session_timeout_option.py b/appium/options/windows/windows/create_session_timeout_option.py
new file mode 100644
index 000000000..acf1289a8
--- /dev/null
+++ b/appium/options/windows/windows/create_session_timeout_option.py
@@ -0,0 +1,45 @@
+# 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
+
+CREATE_SESSION_TIMEOUT = 'createSessionTimeout'
+
+
+class CreateSessionTimeoutOption(SupportsCapabilities):
+    @property
+    def create_session_timeout(self) -> Optional[timedelta]:
+        """
+        Timeout used to retry Appium Windows Driver session startup.
+        """
+        value = self.get_capability(CREATE_SESSION_TIMEOUT)
+        return None if value is None else timedelta(milliseconds=value)
+
+    @create_session_timeout.setter
+    def create_session_timeout(self, value: Union[timedelta, int]) -> None:
+        """
+        Set the timeout used to retry Appium Windows Driver session startup.
+        This capability could be used as a workaround for the long startup times
+        of UWP applications (aka Failed to locate opened application window
+        with appId: TestCompany.my_app4!App, and processId: 8480).
+        """
+        self.set_capability(
+            CREATE_SESSION_TIMEOUT, int(value.total_seconds() * 1000) if isinstance(value, timedelta) else value
+        )
diff --git a/appium/options/windows/windows/expreimental_web_driver_option.py b/appium/options/windows/windows/expreimental_web_driver_option.py
new file mode 100644
index 000000000..ae065d717
--- /dev/null
+++ b/appium/options/windows/windows/expreimental_web_driver_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 typing import Optional
+
+from appium.options.common.supports_capabilities import SupportsCapabilities
+
+EXPERIMENTAL_WEB_DRIVER = 'ms:experimental-webdriver'
+
+
+class ExperimentalWebDriverOption(SupportsCapabilities):
+    @property
+    def experimental_webdriver(self) -> Optional[bool]:
+        """
+        Whether to enable experimental features and optimizations.
+        """
+        return self.get_capability(EXPERIMENTAL_WEB_DRIVER)
+
+    @experimental_webdriver.setter
+    def experimental_webdriver(self, value: bool) -> None:
+        """
+        Enables experimental features and optimizations. See Appium Windows
+        Driver release notes for more details on this capability.
+        """
+        self.set_capability(EXPERIMENTAL_WEB_DRIVER, value)
diff --git a/appium/options/windows/windows/wait_for_app_launch_option.py b/appium/options/windows/windows/wait_for_app_launch_option.py
new file mode 100644
index 000000000..9631b9231
--- /dev/null
+++ b/appium/options/windows/windows/wait_for_app_launch_option.py
@@ -0,0 +1,43 @@
+# 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
+
+WAIT_FOR_APP_LAUNCH = 'ms:waitForAppLaunch'
+
+
+class WaitForAppLaunchOption(SupportsCapabilities):
+    @property
+    def wait_for_app_launch(self) -> Optional[timedelta]:
+        """
+        Timeout used to retry Appium Windows Driver session startup.
+        """
+        value = self.get_capability(WAIT_FOR_APP_LAUNCH)
+        return None if value is None else timedelta(seconds=value)
+
+    @wait_for_app_launch.setter
+    def wait_for_app_launch(self, value: Union[timedelta, int]) -> None:
+        """
+        Similar to createSessionTimeout, but is
+        applied on the server side. Enables Appium Windows Driver to wait for
+        a defined amount of time after an app launch is initiated prior to
+        attaching to the application session. The limit for this is 50 seconds.
+        """
+        self.set_capability(WAIT_FOR_APP_LAUNCH, value.total_seconds() if isinstance(value, timedelta) else value)
diff --git a/appium/protocols/__init__.py b/appium/protocols/__init__.py
new file mode 100644
index 000000000..cc173e9d7
--- /dev/null
+++ b/appium/protocols/__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.
diff --git a/appium/protocols/webdriver/__init__.py b/appium/protocols/webdriver/__init__.py
new file mode 100644
index 000000000..cc173e9d7
--- /dev/null
+++ b/appium/protocols/webdriver/__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.
diff --git a/appium/webdriver/extensions/search_context/base_search_context.py b/appium/protocols/webdriver/can_execute_commands.py
similarity index 63%
rename from appium/webdriver/extensions/search_context/base_search_context.py
rename to appium/protocols/webdriver/can_execute_commands.py
index 500a6e3a4..de4f1b4ad 100644
--- a/appium/webdriver/extensions/search_context/base_search_context.py
+++ b/appium/protocols/webdriver/can_execute_commands.py
@@ -12,14 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# pylint: disable=abstract-method
+from typing import Dict, Protocol, Union
 
+from selenium.webdriver.remote.remote_connection import RemoteConnection
 
-class BaseSearchContext(object):
-    """Used by each search context. Dummy find_element/s are for preventing pylint error"""
 
-    def find_element(self, by=None, value=None):
-        raise NotImplementedError
+class CanExecuteCommands(Protocol):
+    command_executor: RemoteConnection
 
-    def find_elements(self, by=None, value=None):
-        raise NotImplementedError
+    def execute(self, driver_command: str, params: Union[Dict, None] = None) -> Dict: ...
diff --git a/test/functional/android/device_time_tests.py b/appium/protocols/webdriver/can_execute_scripts.py
similarity index 56%
rename from test/functional/android/device_time_tests.py
rename to appium/protocols/webdriver/can_execute_scripts.py
index deefdd36c..1d04f6cae 100644
--- a/test/functional/android/device_time_tests.py
+++ b/appium/protocols/webdriver/can_execute_scripts.py
@@ -1,5 +1,4 @@
 #!/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.
@@ -13,20 +12,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
+from typing import Any, List, Optional, Protocol
 
-from dateutil.parser import parse
 
-from .helper.test_helper import BaseTestCase
+class CanExecuteScripts(Protocol):
+    def pin_script(self, script: str, script_key: Optional[Any] = None) -> Any: ...
 
+    def unpin(self, script_key: Any) -> None: ...
 
-class DeviceTimeTests(BaseTestCase):
-    def test_device_time(self):
-        date_time = self.driver.device_time
-        # convert to date ought to work
-        parse(date_time)
+    def get_pinned_scripts(self) -> List[str]: ...
 
+    def execute_script(self, script: str, *args: Any) -> Any: ...
 
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(DeviceTimeTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
+    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
new file mode 100644
index 000000000..07b0b8279
--- /dev/null
+++ b/appium/protocols/webdriver/can_find_elements.py
@@ -0,0 +1,32 @@
+#!/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, Dict, List, Protocol, Union, runtime_checkable
+
+if TYPE_CHECKING:
+    from appium.webdriver.webelement import WebElement
+
+
+@runtime_checkable
+class CanFindElements(Protocol):
+    """Protocol for objects that can find web elements.
+
+    Any class implementing this protocol must provide:
+    - find_element(by, value): Find a single element
+    - find_elements(by, value): Find multiple elements
+    """
+
+    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']: ...
diff --git a/test/functional/android/location_tests.py b/appium/protocols/webdriver/can_remember_extension_presence.py
similarity index 61%
rename from test/functional/android/location_tests.py
rename to appium/protocols/webdriver/can_remember_extension_presence.py
index 1584fd5ac..1684788b0 100644
--- a/test/functional/android/location_tests.py
+++ b/appium/protocols/webdriver/can_remember_extension_presence.py
@@ -1,5 +1,4 @@
 #!/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.
@@ -13,16 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
+from typing import Protocol, TypeVar
 
-from .helper.test_helper import BaseTestCase
+T = TypeVar('T')
 
 
-class LocationTests(BaseTestCase):
-    def test_toggle_location_services(self):
-        self.driver.toggle_location_services()
+class CanRememberExtensionPresence(Protocol):
+    def assert_extension_exists(self: T, ext_name: str) -> T: ...
 
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(LocationTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
+    def mark_extension_absence(self: T, ext_name: str) -> T: ...
diff --git a/appium/py.typed b/appium/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/appium/saucetestcase.py b/appium/saucetestcase.py
deleted file mode 100644
index 8192f5048..000000000
--- a/appium/saucetestcase.py
+++ /dev/null
@@ -1,60 +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.
-
-# pylint: disable=import-error,no-member
-
-from __future__ import print_function
-
-import os
-import sys
-import unittest
-
-from sauceclient import SauceClient
-
-from appium import webdriver
-
-SAUCE_USERNAME = os.environ.get('SAUCE_USERNAME')
-SAUCE_ACCESS_KEY = os.environ.get('SAUCE_ACCESS_KEY')
-sauce = SauceClient(SAUCE_USERNAME, SAUCE_ACCESS_KEY)
-
-
-def on_platforms(platforms):
-    def decorator(base_class):
-        module = sys.modules[base_class.__module__].__dict__
-        for i, platform in enumerate(platforms):
-            name = "%s_%s" % (base_class.__name__, i + 1)
-            d_caps = {'desired_capabilities': platform}
-            module[name] = type(name, (base_class,), d_caps)
-    return decorator
-
-
-class SauceTestCase(unittest.TestCase):
-    def setUp(self):
-        self.desired_capabilities['name'] = self.id()
-        sauce_url = "http://%s:%s@ondemand.saucelabs.com:80/wd/hub"
-        self.driver = webdriver.Remote(
-            desired_capabilities=self.desired_capabilities,
-            command_executor=sauce_url % (SAUCE_USERNAME, SAUCE_ACCESS_KEY)
-        )
-        self.driver.implicitly_wait(30)
-
-    def tearDown(self):
-        print("Link to your job: https://saucelabs.com/jobs/%s" % self.driver.session_id)
-        try:
-            if sys.exc_info() == (None, None, None):
-                sauce.jobs.update_job(self.driver.session_id, passed=True)
-            else:
-                sauce.jobs.update_job(self.driver.session_id, passed=False)
-        finally:
-            self.driver.quit()
diff --git a/appium/version.py b/appium/version.py
index 04e6d68c0..0616a7c5e 100644
--- a/appium/version.py
+++ b/appium/version.py
@@ -1 +1,22 @@
-version = '0.52'
+#!/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 importlib import metadata
+
+
+def _get_version():
+    return metadata.version('Appium-Python-Client')
+
+
+version = _get_version()
diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py
index 9b7218dde..9ca311824 100644
--- a/appium/webdriver/appium_connection.py
+++ b/appium/webdriver/appium_connection.py
@@ -13,21 +13,53 @@
 # limitations under the License.
 
 import uuid
+from typing import TYPE_CHECKING, Any, Dict
 
 from selenium.webdriver.remote.remote_connection import RemoteConnection
 
 from appium.common.helper import library_version
 
+if TYPE_CHECKING:
+    from urllib.parse import ParseResult
+
+
+PREFIX_HEADER = 'appium/'
+
+HEADER_IDEMOTENCY_KEY = 'X-Idempotency-Key'
+
+
+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}
+
 
 class AppiumConnection(RemoteConnection):
+    """
+    A subclass of selenium.webdriver.remote.remote_connection.Remoteconnection.
+
+    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
+    """
+
+    user_agent = f'{PREFIX_HEADER}{library_version()} ({RemoteConnection.user_agent})'
+    extra_headers = {}
 
     @classmethod
-    def get_remote_connection_headers(cls, parsed_url, keep_alive=True):
-        """Override get_remote_connection_headers in RemoteConnection"""
-        headers = RemoteConnection.get_remote_connection_headers(parsed_url, keep_alive=keep_alive)
-        headers['User-Agent'] = 'appium/python {} ({})'.format(library_version(), headers['User-Agent'])
+    def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bool = True) -> Dict[str, Any]:
+        """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/appium_service.py b/appium/webdriver/appium_service.py
index 42527c242..93c926c65 100644
--- a/appium/webdriver/appium_service.py
+++ b/appium/webdriver/appium_service.py
@@ -12,223 +12,319 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
 import os
-import subprocess
+import re
+import subprocess as sp
 import sys
 import time
+from typing import Any, Callable, List, Optional, Set
 
-import urllib3
+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 = '/wd/hub/status'
-
-
-def find_executable(executable):
-    path = os.environ['PATH']
-    paths = path.split(os.pathsep)
-    base, 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
+STATUS_URL = '/status'
+DEFAULT_BASE_PATH = '/'
+HTTP_STATUS_ERROR = 400
 
 
-def poll_url(host, port, path, timeout_ms):
-    time_started_sec = time.time()
-    while time.time() < time_started_sec + timeout_ms / 1000.0:
-        try:
-            conn = urllib3.PoolManager(timeout=1.0)
-            resp = conn.request('HEAD', 'http://{host}:{port}{path}'.format(
-                host=host, port=port, path=path))
-            if resp.status < 400:
-                return True
-        except Exception:
-            pass
-        time.sleep(1.0)
-    return False
+class AppiumServiceError(RuntimeError):
+    pass
 
 
-class AppiumServiceError(RuntimeError):
+class AppiumStartupError(RuntimeError):
     pass
 
 
-class AppiumService(object):
-    def __init__(self):
-        self._process = None
-        self._cmd = None
+class AppiumService:
+    def __init__(self) -> None:
+        self._process: Optional[sp.Popen] = None
+        self._cmd: Optional[List[str]] = None
 
-    def _get_node(self):
-        if not hasattr(self, '_node_executable'):
-            self._node_executable = find_executable('node')
-        if self._node_executable is None:
-            raise AppiumServiceError('NodeJS main executable cannot be found. ' +
-                                     'Make sure it is installed and present in PATH')
-        return self._node_executable
-
-    def _get_npm(self):
-        if not hasattr(self, '_npm_executable'):
-            self._npm_executable = find_executable('npm.cmd' if sys.platform == 'win32' else 'npm')
-        if self._npm_executable is None:
-            raise AppiumServiceError('Node Package Manager executable cannot be found. ' +
-                                     'Make sure it is installed and present in PATH')
-        return self._npm_executable
-
-    def _get_main_script(self):
-        if not hasattr(self, '_main_script'):
-            for args in [['root', '-g'], ['root']]:
-                try:
-                    modules_root = subprocess.check_output([self._get_npm()] + args).strip().decode('utf-8')
-                    if os.path.exists(os.path.join(modules_root, MAIN_SCRIPT_PATH)):
-                        self._main_script = os.path.join(modules_root, MAIN_SCRIPT_PATH)
-                        break
-                except subprocess.CalledProcessError:
-                    continue
-            if not hasattr(self, '_main_script'):
-                try:
-                    self._main_script = subprocess.check_output(
-                        [self._get_node(),
-                         '-e',
-                         'console.log(require.resolve("{}"))'.format(MAIN_SCRIPT_PATH)]).strip()
-                except subprocess.CalledProcessError as e:
-                    raise AppiumServiceError(e.output)
-        return self._main_script
-
-    @staticmethod
-    def _parse_port(args):
-        for idx, arg in enumerate(args or []):
-            if arg in ('--port', '-p') and idx < len(args) - 1:
-                return int(args[idx + 1])
-        return DEFAULT_PORT
-
-    @staticmethod
-    def _parse_host(args):
-        for idx, arg in enumerate(args or []):
-            if arg in ('--address', '-a') and idx < len(args) - 1:
-                return args[idx + 1]
-        return DEFAULT_HOST
-
-    def start(self, **kwargs):
+    def start(self, **kwargs: Any) -> sp.Popen:
         """Starts Appium service with given arguments.
 
+        If you use the service to start Appium 1.x
+        then consider providing ['--base-path', '/wd/hub'] arguments. By default,
+        the service assumes Appium server listens on '/' path, which is the default path
+        for Appium 2.
+
         The service will be forcefully restarted if it is already running.
 
-        Keyword Args:
+        Args:
             env (dict): Environment variables mapping. The default system environment,
-                which is inherited from the parent process is assigned by default.
+                which is inherited from the parent process, is assigned by default.
             node (str): The full path to the main NodeJS executable. The service will try
-                to retrieve it automatically by default.
-            stdout: Check on the documentation for subprocess.Popen for more details.
-                The default value is subprocess.PIPE.
-            stderr: Check on the documentation for subprocess.Popen for more details.
-                The default value is subprocess.PIPE.
+                to retrieve it automatically if not provided.
+            npm (str): The full path to the Node Package Manager (npm) script. The service will try
+                to retrieve it automatically if not provided.
+            stdout (int): Check the documentation for subprocess.Popen for more details.
+                The default value is subprocess.DEVNULL on Windows and subprocess.PIPE on other platforms.
+            stderr (int): Check the documentation for subprocess.Popen for more details.
+                The default value is subprocess.DEVNULL on Windows and subprocess.PIPE on other platforms.
             timeout_ms (int): The maximum time to wait until Appium process starts listening
                 for HTTP connections. If set to zero or a negative number then no wait will be applied.
-                60000 ms by default
+                60000 ms by default.
             main_script (str): The full path to the main Appium executable
-                (usually located this is build/lib/main.js). If this is not set
+                (usually located at build/lib/main.js). If not set
                 then the service tries to detect the path automatically.
-            args (str): List of Appium arguments (all must be strings). Check on
+            args (str): List of Appium arguments (all must be strings). Check
                 https://appium.io/docs/en/writing-running-appium/server-args/ for more details
                 about possible arguments and their values.
 
-        Returns:
-            subprocess.Popen instance: 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.
+        Returns: 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.
+
         """
         self.stop()
 
         env = kwargs['env'] if 'env' in kwargs else None
-        node = kwargs['node'] if 'node' in kwargs else self._get_node()
-        stdout = kwargs['stdout'] if 'stdout' in kwargs else subprocess.PIPE
-        stderr = kwargs['stderr'] if 'stderr' in kwargs else subprocess.PIPE
+        node: str = kwargs.get('node') or get_node()
+        npm: str = kwargs.get('npm') or get_npm()
+        main_script: str = kwargs.get('main_script') or get_main_script(node, npm)
+        # A workaround for https://github.com/appium/python-client/issues/534
+        default_std = sp.DEVNULL if sys.platform == 'win32' else sp.PIPE
+        stdout = kwargs['stdout'] if 'stdout' in kwargs else default_std
+        stderr = kwargs['stderr'] if 'stderr' in kwargs else default_std
         timeout_ms = int(kwargs['timeout_ms']) if 'timeout_ms' in kwargs else STARTUP_TIMEOUT_MS
-        main_script = kwargs['main_script'] if 'main_script' in kwargs else self._get_main_script()
-        args = [node, main_script]
+        args: List[str] = [node, main_script]
         if 'args' in kwargs:
             args.extend(kwargs['args'])
         self._cmd = args
-        self._process = subprocess.Popen(args=args, stdout=stdout, stderr=stderr, env=env)
-        host = self._parse_host(args)
-        port = self._parse_port(args)
-        error_msg = None
-        if not self.is_running or (timeout_ms > 0 and not poll_url(host, port, STATUS_URL, timeout_ms)):
-            error_msg = 'Appium has failed to start on {}:{} within {}ms timeout'\
-                        .format(host, port, timeout_ms)
+        self._process = sp.Popen(args=args, stdout=stdout, stderr=stderr, env=env)
+        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'method arguments.'
+        )
+        if timeout_ms > 0:
+            server_url = _make_server_url(args)
+            try:
+                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 {server_url} '
+                        f'within {timeout_ms}ms timeout. Make sure proper values have been provided '
+                        f'to --base-path, --address and --port process arguments.'
+                    )
+            except AppiumStartupError:
+                error_msg = startup_failure_msg
+        elif not self.is_running:
+            error_msg = startup_failure_msg
         if error_msg is not None:
-            if stderr == subprocess.PIPE:
+            if stderr == sp.PIPE and self._process.stderr is not None:
+                # noinspection PyUnresolvedReferences
                 err_output = self._process.stderr.read()
                 if err_output:
-                    error_msg += '\nOriginal error: {}'.format(err_output)
+                    error_msg += f'\nOriginal error: {str(err_output)}'
             self.stop()
             raise AppiumServiceError(error_msg)
         return self._process
 
-    def stop(self):
+    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.
 
+        Args:
+            timeout: The maximum time in float seconds to wait for the server process to terminate
+
         Returns:
-            bool: `True` if the service was running before being stopped
+            `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()
-            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):
+    def is_running(self) -> bool:
         """Check if the service is running.
 
-        Returns:
-            bool: `True` or `False`
+        :return: `True` if the service is running
         """
-        return self._process is not None and self._process.poll() is None
+        return self._process is not None and self._cmd is not None and self._process.poll() is None
 
     @property
-    def is_listening(self):
+    def is_listening(self) -> bool:
         """Check if the service is listening on the given/default host/port.
 
         The fact, that the service is running, does not always mean it is listening.
-        the default host/port values can be customized by providing --address/--port
-        command line arguments while starting the service.
+        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
+            `True` if the service is running and listening on the given/default host/port
         """
-        if not self.is_running or self._cmd is None:
+        if not self.is_running:
             return False
-        host = self._parse_host(self._cmd)
-        port = self._parse_port(self._cmd)
-        return self.is_running and poll_url(host, port, STATUS_URL, 1000)
+
+        assert self._cmd
+        try:
+            return is_service_listening(
+                _make_server_url(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
+
+    Args:
+        url: Full server url
+        timeout: Timeout in float seconds
+        custom_validator: Custom callable method to be executed upon each validation loop before the timeout happens
+
+    Returns:
+        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 < HTTP_STATUS_ERROR:
+                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(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)
-    assert(find_executable('npm') is not None)
+    assert find_executable('node') is not None
+    assert find_executable('npm') is not None
     service = AppiumService()
     service.start(args=['--address', '127.0.0.1', '-p', str(DEFAULT_PORT)])
     # service.start(args=['--address', '127.0.0.1', '-p', '80'], timeout_ms=2000)
-    assert(service.is_running)
-    assert(service.is_listening)
+    assert service.is_running
+    assert service.is_listening
     service.stop()
-    assert(not service.is_running)
-    assert(not service.is_listening)
+    assert not service.is_running
+    assert not service.is_listening
diff --git a/appium/webdriver/applicationstate.py b/appium/webdriver/applicationstate.py
index e3c255ae1..0658b264e 100644
--- a/appium/webdriver/applicationstate.py
+++ b/appium/webdriver/applicationstate.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 
-class ApplicationState(object):
+class ApplicationState:
     NOT_INSTALLED = 0
     NOT_RUNNING = 1
     RUNNING_IN_BACKGROUND_SUSPENDED = 2
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/clipboard_content_type.py b/appium/webdriver/clipboard_content_type.py
index 67aac3510..7c1211b31 100644
--- a/appium/webdriver/clipboard_content_type.py
+++ b/appium/webdriver/clipboard_content_type.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 
-class ClipboardContentType(object):
+class ClipboardContentType:
     PLAINTEXT = 'plaintext'
     IMAGE = 'image'
     URL = 'url'
diff --git a/appium/webdriver/command_method.py b/appium/webdriver/command_method.py
new file mode 100644
index 000000000..dde45cb44
--- /dev/null
+++ b/appium/webdriver/command_method.py
@@ -0,0 +1,27 @@
+#!/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 enum
+
+
+class CommandMethod(enum.Enum):
+    GET = 'GET'
+    HEAD = 'HEAD'
+    POST = 'POST'
+    PUT = 'PUT'
+    DELETE = 'DELETE'
+    CONNECT = 'CONNECT'
+    OPTIONS = 'OPTIONS'
+    TRACE = 'TRACE'
+    PATCH = 'PATCH'
diff --git a/appium/webdriver/common/mobileby.py b/appium/webdriver/common/appiumby.py
similarity index 54%
rename from appium/webdriver/common/mobileby.py
rename to appium/webdriver/common/appiumby.py
index 2034275fc..371573ab0 100644
--- a/appium/webdriver/common/mobileby.py
+++ b/appium/webdriver/common/appiumby.py
@@ -12,18 +12,43 @@
 # 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
 
 
-class MobileBy(By):
+class AppiumBy(By):
     IOS_PREDICATE = '-ios predicate string'
-    IOS_UIAUTOMATION = '-ios uiautomation'
     IOS_CLASS_CHAIN = '-ios class chain'
     ANDROID_UIAUTOMATOR = '-android uiautomator'
     ANDROID_VIEWTAG = '-android viewtag'
     ANDROID_DATA_MATCHER = '-android datamatcher'
     ANDROID_VIEW_MATCHER = '-android viewmatcher'
-    WINDOWS_UI_AUTOMATION = '-windows uiautomation'
     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'
+
+
+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',
+    '-flutter text containing',
+]
diff --git a/appium/webdriver/common/multi_action.py b/appium/webdriver/common/multi_action.py
deleted file mode 100644
index f80a111fc..000000000
--- a/appium/webdriver/common/multi_action.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.
-
-
-# The Selenium team implemented something like the Multi Action API in the form of
-# "action chains" (https://code.google.com/p/selenium/source/browse/py/selenium/webdriver/common/action_chains.py).
-# These do not quite work for this situation, and do not allow for ad hoc action
-# chaining as the spec requires.
-
-import copy
-
-from appium.webdriver.mobilecommand import MobileCommand as Command
-
-
-class MultiAction(object):
-    def __init__(self, driver, element=None):
-        self._driver = driver
-        self._element = element
-        self._touch_actions = []
-
-    def add(self, *touch_actions):
-        """Add TouchAction objects to the MultiAction, to be performed later.
-
-        Args:
-            touch_actions (`TouchAction`): one or more TouchAction objects describing a chain of actions to be performed by one finger
-
-        Usage:
-            a1 = TouchAction(driver)
-            a1.press(el1).move_to(el2).release()
-            a2 = TouchAction(driver)
-            a2.press(el2).move_to(el1).release()
-
-            MultiAction(driver).add(a1, a2)
-        """
-        for touch_action in touch_actions:
-            if self._touch_actions is None:
-                self._touch_actions = []
-
-            self._touch_actions.append(copy.copy(touch_action))
-
-    def perform(self):
-        """Perform the actions stored in the object.
-
-        Usage:
-            a1 = TouchAction(driver)
-            a1.press(el1).move_to(el2).release()
-            a2 = TouchAction(driver)
-            a2.press(el2).move_to(el1).release()
-
-            MultiAction(driver).add(a1, a2).perform()
-        """
-        self._driver.execute(Command.MULTI_ACTION, self.json_wire_gestures)
-
-        # clean up and be ready for the next batch
-        self._touch_actions = []
-
-        return self
-
-    @property
-    def json_wire_gestures(self):
-        actions = []
-        for action in self._touch_actions:
-            actions.append(action.json_wire_gestures)
-        if self._element is not None:
-            return {'actions': actions, 'elementId': self._element.id}
-        return {'actions': actions}
diff --git a/appium/webdriver/common/touch_action.py b/appium/webdriver/common/touch_action.py
deleted file mode 100644
index 84668e170..000000000
--- a/appium/webdriver/common/touch_action.py
+++ /dev/null
@@ -1,173 +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.
-
-
-# The Selenium team implemented a version of the Touch Action API in their code
-# (https://code.google.com/p/selenium/source/browse/py/selenium/webdriver/common/touch_actions.py)
-# but it is deficient in many ways, and does not work in such a way as to be
-# amenable to Appium's use of iOS UIAutomation and Android UIAutomator
-# So it is reimplemented here.
-#
-# Theirs is `TouchActions`. Appium's is `TouchAction`.
-
-# pylint: disable=no-self-use
-
-import copy
-
-from appium.webdriver.mobilecommand import MobileCommand as Command
-
-
-class TouchAction(object):
-    def __init__(self, driver=None):
-        self._driver = driver
-        self._actions = []
-
-    def tap(self, element=None, x=None, y=None, count=1):
-        """Perform a tap action on the element
-
-        Args:
-            element (`appium.webdriver.webelement.WebElement`): the element to tap
-            x (:obj:`int`, optional): x coordinate to tap, relative to the top left corner of the element.
-            y (:obj:`int`, optional): y coordinate. If y is used, x must also be set, and vice versa
-
-        Returns:
-            `TouchAction`: self instance
-        """
-        opts = self._get_opts(element, x, y)
-        opts['count'] = count
-        self._add_action('tap', opts)
-
-        return self
-
-    def press(self, el=None, x=None, y=None, pressure=None):
-        """Begin a chain with a press down action at a particular element or point
-
-        Args:
-            el (:obj:`appium.webdriver.webelement.WebElement`, optional): the element to press
-            x (:obj:`int`, optional): x coordiate to press. If y is used, x must also be set
-            y (:obj:`int`, optional): y coordiate to press. If x is used, y must also be set
-            pressure (:obj:`float`, optional): [iOS Only] press as force touch. Read the description of `force` property on Apple's UITouch class
-                                (https://developer.apple.com/documentation/uikit/uitouch?language=objc) for more details on possible value ranges.
-
-        Returns:
-            `TouchAction`: self instance
-        """
-        self._add_action('press', self._get_opts(el, x, y, pressure=pressure))
-
-        return self
-
-    def long_press(self, el=None, x=None, y=None, duration=1000):
-        """Begin a chain with a press down that lasts `duration` milliseconds
-
-        Args:
-            el (:obj:`appium.webdriver.webelement.WebElement`, optional): the element to press
-            x (:obj:`int`, optional): x coordiate to press. If y is used, x must also be set
-            y (:obj:`int`, optional): y coordiate to press. If x is used, y must also be set
-            duration (:obj:`int`, optional): Duration to press
-
-        Returns:
-            `TouchAction`: self instance
-        """
-        self._add_action('longPress', self._get_opts(el, x, y, duration))
-
-        return self
-
-    def wait(self, ms=0):
-        """Pause for `ms` milliseconds.
-
-        Args:
-            ms (int): The time to pause
-
-        Returns:
-            `TouchAction`: self instance
-        """
-        if ms is None:
-            ms = 0
-
-        opts = {'ms': ms}
-
-        self._add_action('wait', opts)
-
-        return self
-
-    def move_to(self, el=None, x=None, y=None):
-        """Move the pointer from the previous point to the element or point specified
-
-        Args:
-            el (:obj:`appium.webdriver.webelement.WebElement`, optional): the element to be moved to
-            x (:obj:`int`, optional): x coordiate to be moved to. If y is used, x must also be set
-            y (:obj:`int`, optional): y coordiate to be moved to. If x is used, y must also be set
-
-        Returns:
-            `TouchAction`: self instance
-        """
-        self._add_action('moveTo', self._get_opts(el, x, y))
-
-        return self
-
-    def release(self):
-        """End the action by lifting the pointer off the screen
-
-        Returns:
-            `TouchAction`: self instance
-        """
-        self._add_action('release', {})
-
-        return self
-
-    def perform(self):
-        """Perform the action by sending the commands to the server to be operated upon
-
-        Returns:
-            `TouchAction`: self instance
-        """
-        params = {'actions': self._actions}
-        self._driver.execute(Command.TOUCH_ACTION, params)
-
-        # get rid of actions so the object can be reused
-        self._actions = []
-
-        return self
-
-    @property
-    def json_wire_gestures(self):
-        gestures = []
-        for action in self._actions:
-            gestures.append(copy.deepcopy(action))
-        return gestures
-
-    def _add_action(self, action, options):
-        gesture = {
-            'action': action,
-            'options': options,
-        }
-        self._actions.append(gesture)
-
-    def _get_opts(self, element, x, y, duration=None, pressure=None):
-        opts = {}
-        if element is not None:
-            opts['element'] = element.id
-
-        # it makes no sense to have x but no y, or vice versa.
-        if x is not None and y is not None:
-            opts['x'] = x
-            opts['y'] = y
-
-        if duration is not None:
-            opts['duration'] = duration
-
-        if pressure is not None:
-            opts['pressure'] = pressure
-
-        return opts
diff --git a/appium/webdriver/connectiontype.py b/appium/webdriver/connectiontype.py
index 5a408ba57..7db69ba36 100644
--- a/appium/webdriver/connectiontype.py
+++ b/appium/webdriver/connectiontype.py
@@ -16,17 +16,25 @@
 """
 Connection types are specified here:
     https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile#120
-    Value (Alias)      | Data | Wifi | Airplane Mode
-    -------------------------------------------------
-    0 (None)           | 0    | 0    | 0
-    1 (Airplane Mode)  | 0    | 0    | 1
-    2 (Wifi only)      | 0    | 1    | 0
-    4 (Data only)      | 1    | 0    | 0
-    6 (All network on) | 1    | 1    | 0
+
+    +--------------------+------+------+---------------+
+    | Value (Alias)      | Data | Wifi | Airplane Mode |
+    +====================+======+======+===============+
+    | 0 (None)           | 0    | 0    | 0             |
+    +--------------------+------+------+---------------+
+    | 1 (Airplane Mode)  | 0    | 0    | 1             |
+    +--------------------+------+------+---------------+
+    | 2 (Wifi only)      | 0    | 1    | 0             |
+    +--------------------+------+------+---------------+
+    | 4 (Data only)      | 1    | 0    | 0             |
+    +--------------------+------+------+---------------+
+    | 6 (All network on) | 1    | 1    | 0             |
+    +--------------------+------+------+---------------+
+
 """
 
 
-class ConnectionType(object):
+class ConnectionType:
     NO_CONNECTION = 0
     AIRPLANE_MODE = 1
     WIFI_ONLY = 2
diff --git a/appium/webdriver/errorhandler.py b/appium/webdriver/errorhandler.py
index 1fa793bac..b2a5cab91 100644
--- a/appium/webdriver/errorhandler.py
+++ b/appium/webdriver/errorhandler.py
@@ -12,18 +12,114 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium.common.exceptions import WebDriverException
+import json
+from typing import Any, Dict, List, Sequence, Type, Union
+
+import selenium.common.exceptions as sel_exceptions
 from selenium.webdriver.remote import errorhandler
 
-from appium.common.exceptions import NoSuchContextException
+import appium.common.exceptions as appium_exceptions
+
+ERROR_TO_EXC_MAPPING: Dict[str, Type[sel_exceptions.WebDriverException]] = {
+    'element click intercepted': sel_exceptions.ElementClickInterceptedException,
+    'element not interactable': sel_exceptions.ElementNotInteractableException,
+    'insecure certificate': sel_exceptions.InsecureCertificateException,
+    'invalid argument': sel_exceptions.InvalidArgumentException,
+    'invalid cookie domain': sel_exceptions.InvalidCookieDomainException,
+    'invalid element state': sel_exceptions.InvalidElementStateException,
+    'invalid selector': sel_exceptions.InvalidSelectorException,
+    'invalid session id': sel_exceptions.InvalidSessionIdException,
+    'javascript error': sel_exceptions.JavascriptException,
+    'move target out of bounds': sel_exceptions.MoveTargetOutOfBoundsException,
+    'no such alert': sel_exceptions.NoAlertPresentException,
+    'no such cookie': sel_exceptions.NoSuchCookieException,
+    'no such element': sel_exceptions.NoSuchElementException,
+    'no such frame': sel_exceptions.NoSuchFrameException,
+    'no such window': sel_exceptions.NoSuchWindowException,
+    'no such shadow root': sel_exceptions.NoSuchShadowRootException,
+    'script timeout': sel_exceptions.TimeoutException,
+    'session not created': sel_exceptions.SessionNotCreatedException,
+    'stale element reference': sel_exceptions.StaleElementReferenceException,
+    'detached shadow root': sel_exceptions.NoSuchShadowRootException,
+    'timeout': sel_exceptions.TimeoutException,
+    'unable to set cookie': sel_exceptions.UnableToSetCookieException,
+    'unable to capture screen': sel_exceptions.ScreenshotException,
+    'unexpected alert open': sel_exceptions.UnexpectedAlertPresentException,
+    'unknown command': sel_exceptions.UnknownMethodException,
+    'unknown error': sel_exceptions.WebDriverException,
+    'unknown method': sel_exceptions.UnknownMethodException,
+    'unsupported operation': sel_exceptions.UnknownMethodException,
+    'element not visible': sel_exceptions.ElementNotVisibleException,
+    'element not selectable': sel_exceptions.ElementNotSelectableException,
+    'invalid coordinates': sel_exceptions.InvalidCoordinatesException,
+}
+
+
+def format_stacktrace(original: Union[None, str, Sequence]) -> List[str]:
+    if not original:
+        return []
+    if isinstance(original, str):
+        return original.split('\n')
+
+    result: List[str] = []
+    try:
+        for frame in original:
+            if not isinstance(frame, dict):
+                continue
+
+            line = frame.get('lineNumber', '')
+            file = frame.get('fileName', '<anonymous>')
+            if line:
+                file = f'{file}:{line}'
+            meth = frame.get('methodName', '<anonymous>')
+            if 'className' in frame:
+                meth = f'{frame["className"]}.{meth}'
+            result.append(f'    at {meth} ({file})')
+    except TypeError:
+        pass
+    return result
 
 
 class MobileErrorHandler(errorhandler.ErrorHandler):
-    def check_response(self, response):
-        try:
-            super(MobileErrorHandler, self).check_response(response)
-        except WebDriverException as wde:
-            if wde.msg == 'No such context found.':
-                raise NoSuchContextException(wde.msg, wde.screen, wde.stacktrace)
-            else:
-                raise wde
+    def check_response(self, response: Dict[str, Any]) -> None:
+        """
+        https://www.w3.org/TR/webdriver/#errors
+        """
+        payload = response.get('value', '')
+        if isinstance(payload, dict):
+            payload_dict = payload
+        else:
+            try:
+                payload_dict = json.loads(payload)
+            except (json.JSONDecodeError, TypeError):
+                return
+            if not isinstance(payload_dict, dict):
+                return
+        value = payload_dict.get('value')
+        if not isinstance(value, dict):
+            return
+        error = value.get('error')
+        if not error:
+            return
+
+        message = value.get('message', error)
+        stacktrace = value.get('stacktrace', '')
+        # In theory, we should also be checking HTTP status codes.
+        # Java client, for example, prints a warning if the actual `error`
+        # value does not match to the response's HTTP status code.
+        exception_class: Type[sel_exceptions.WebDriverException] = ERROR_TO_EXC_MAPPING.get(
+            error, sel_exceptions.WebDriverException
+        )
+        if exception_class is sel_exceptions.WebDriverException and message:
+            if message == 'No such context found.':
+                exception_class = appium_exceptions.NoSuchContextException
+            elif message == 'That command could not be executed in the current context.':
+                exception_class = appium_exceptions.InvalidSwitchToTargetException
+
+        if exception_class is sel_exceptions.UnexpectedAlertPresentException:
+            raise sel_exceptions.UnexpectedAlertPresentException(
+                msg=message,
+                stacktrace=format_stacktrace(stacktrace),
+                alert_text=value.get('data'),
+            )
+        raise exception_class(msg=message, stacktrace=format_stacktrace(stacktrace))
diff --git a/appium/webdriver/extensions/action_helpers.py b/appium/webdriver/extensions/action_helpers.py
index 108fcfd5c..9c3709223 100644
--- a/appium/webdriver/extensions/action_helpers.py
+++ b/appium/webdriver/extensions/action_helpers.py
@@ -12,140 +12,177 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import TYPE_CHECKING, List, Optional, Tuple, cast
 
-from appium.webdriver.common.multi_action import MultiAction
-from appium.webdriver.common.touch_action import TouchAction
+from selenium.webdriver.common.action_chains import ActionChains
+from selenium.webdriver.common.actions import interaction
+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
 
-class ActionHelpers(webdriver.Remote):
+if TYPE_CHECKING:
+    # noinspection PyUnresolvedReferences
+    from appium.webdriver.webdriver import WebDriver
 
-    def scroll(self, origin_el, destination_el, duration=None):
+
+class ActionHelpers:
+    def scroll(self, origin_el: WebElement, destination_el: WebElement, duration: Optional[int] = None) -> Self:
         """Scrolls from one element to another
 
         Args:
-            originalEl (`appium.webdriver.webelement.WebElement`): the element from which to being scrolling
-            destinationEl (`appium.webdriver.webelement.WebElement`): the element to scroll to
-            duration (int): a duration after pressing originalEl and move the element to destinationEl.
-                Default is 600 ms for W3C spec. Zero for MJSONWP.
+            origin_el: the element from which to begin scrolling (center of element)
+            destination_el: the element to scroll to (center of element)
+            duration: defines speed of scroll action when moving from originalEl to destinationEl.
+                Default is 600 ms for W3C spec.
 
         Usage:
             driver.scroll(el1, el2)
 
         Returns:
-            `appium.webdriver.webelement.WebElement`
+            Union['WebDriver', 'ActionHelpers']: Self instance
         """
-
         # XCUITest x W3C spec has no duration by default in server side
-        if self.w3c and duration is None:
+        if duration is None:
             duration = 600
 
-        action = TouchAction(self)
-        if duration is None:
-            action.press(origin_el).move_to(destination_el).release().perform()
-        else:
-            action.press(origin_el).wait(duration).move_to(destination_el).release().perform()
+        touch_input = PointerInput(interaction.POINTER_TOUCH, 'touch')
+
+        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
+        actions.w3c_actions.pointer_action.move_to(origin_el)
+        actions.w3c_actions.pointer_action.pointer_down()
+        # setup duration for second move only, assuming duration always has atleast default value
+        actions.w3c_actions = ActionBuilder(self, mouse=touch_input, duration=duration)
+        actions.w3c_actions.pointer_action.move_to(destination_el)
+        actions.w3c_actions.pointer_action.release()
+        actions.perform()
         return self
 
-    def drag_and_drop(self, origin_el, destination_el):
+    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:
-            originEl (`appium.webdriver.webelement.WebElement`): the element to drag
-            destinationEl (`appium.webdriver.webelement.WebElement`): the element to drag to
+            origin_el: the element to drag
+            destination_el: the element to drag to
+            pause: how long the action pauses before moving after the tap and hold, in float seconds.
 
         Returns:
-            `appium.webdriver.webelement.WebElement`
+            Union['WebDriver', 'ActionHelpers']: Self instance
         """
-        action = TouchAction(self)
-        action.long_press(origin_el).move_to(destination_el).release().perform()
+        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:
+            actions.w3c_actions.pointer_action.pause(pause)
+        actions.w3c_actions.pointer_action.move_to(destination_el)
+        actions.w3c_actions.pointer_action.release()
+        actions.perform()
         return self
 
-    def tap(self, positions, duration=None):
+    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
 
         Args:
-            positions (:obj:`list` of :obj:`tuple`): an array of tuples representing the x/y coordinates of
+            positions: an array of tuples representing the x/y coordinates of
                 the fingers to tap. Length can be up to five.
-            duration (:obj:`int`, optional): length of time to tap, in ms
+            duration: length of time to tap, in ms
 
         Usage:
             driver.tap([(100, 20), (100, 60), (100, 100)], 500)
 
         Returns:
-            `appium.webdriver.webelement.WebElement`
+            Union['WebDriver', 'ActionHelpers']: Self instance
         """
         if len(positions) == 1:
-            action = TouchAction(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]
+            actions.w3c_actions.pointer_action.move_to_location(x, y)
+            actions.w3c_actions.pointer_action.pointer_down()
             if duration:
-                action.long_press(x=x, y=y, duration=duration).release()
+                actions.w3c_actions.pointer_action.pause(duration / 1000)
             else:
-                action.tap(x=x, y=y)
-            action.perform()
+                actions.w3c_actions.pointer_action.pause(0.1)
+            actions.w3c_actions.pointer_action.release()
+            actions.perform()
         else:
-            ma = MultiAction(self)
+            finger = 0
+            actions = ActionChains(cast('WebDriver', self))
+            actions.w3c_actions.devices = []
+
             for position in positions:
+                finger += 1
                 x = position[0]
                 y = position[1]
-                action = TouchAction(self)
+
+                # https://github.com/SeleniumHQ/selenium/blob/trunk/py/selenium/webdriver/common/actions/pointer_input.py
+                new_input = actions.w3c_actions.add_pointer_input('touch', f'finger{finger}')
+                new_input.create_pointer_move(x=x, y=y)
+                new_input.create_pointer_down(button=MouseButton.LEFT)
                 if duration:
-                    action.long_press(x=x, y=y, duration=duration).release()
+                    new_input.create_pause(duration / 1000)
                 else:
-                    action.press(x=x, y=y).release()
-                ma.add(action)
-
-            ma.perform()
+                    new_input.create_pause(0.1)
+                new_input.create_pointer_up(MouseButton.LEFT)
+            actions.perform()
         return self
 
-    def swipe(self, start_x, start_y, end_x, end_y, duration=None):
+    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:
-            start_x (int): x-coordinate at which to start
-            start_y (int): y-coordinate at which to start
-            end_x (int): x-coordinate at which to stop
-            end_y (int): y-coordinate at which to stop
-            duration (:obj:`int`, optional): time to take the swipe, in ms.
+            start_x: x-coordinate at which to start
+            start_y: y-coordinate at which to start
+            end_x: x-coordinate at which to stop
+            end_y: y-coordinate at which to stop
+            duration: defines the swipe speed as time taken to swipe from point a to point b, in ms.
 
         Usage:
             driver.swipe(100, 100, 100, 400)
 
         Returns:
-            `appium.webdriver.webelement.WebElement`
+            Union['WebDriver', 'ActionHelpers']: Self instance
         """
-        # `swipe` is something like press-wait-move_to-release, which the server
-        # will translate into the correct action
-        action = TouchAction(self)
-        action \
-            .press(x=start_x, y=start_y) \
-            .wait(ms=duration) \
-            .move_to(x=end_x, y=end_y) \
-            .release()
-        action.perform()
+        touch_input = PointerInput(interaction.POINTER_TOUCH, 'touch')
+
+        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()
+        if duration > 0:
+            actions.w3c_actions = ActionBuilder(self, mouse=touch_input, duration=duration)
+        actions.w3c_actions.pointer_action.move_to_location(end_x, end_y)
+        actions.w3c_actions.pointer_action.release()
+        actions.perform()
         return self
 
-    def flick(self, start_x, start_y, end_x, end_y):
+    def flick(self, start_x: int, start_y: int, end_x: int, end_y: int) -> Self:
         """Flick from one point to another point.
 
         Args:
-            start_x (int): x-coordinate at which to start
-            start_y (int): y-coordinate at which to start
-            end_x (int): x-coordinate at which to stop
-            end_y (int): y-coordinate at which to stop
+            start_x: x-coordinate at which to start
+            start_y: y-coordinate at which to start
+            end_x: x-coordinate at which to stop
+            end_y: y-coordinate at which to stop
 
         Usage:
             driver.flick(100, 100, 100, 400)
 
         Returns:
-            `appium.webdriver.webelement.WebElement`
+            Union['WebDriver', 'ActionHelpers']: Self instance
         """
-        action = TouchAction(self)
-        action \
-            .press(x=start_x, y=start_y) \
-            .move_to(x=end_x, y=end_y) \
-            .release()
-        action.perform()
+        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 self
diff --git a/appium/webdriver/extensions/android/activities.py b/appium/webdriver/extensions/android/activities.py
index 236252c37..76da521b0 100644
--- a/appium/webdriver/extensions/android/activities.py
+++ b/appium/webdriver/extensions/android/activities.py
@@ -12,82 +12,54 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
-from selenium.common.exceptions import TimeoutException
+from selenium.common.exceptions import TimeoutException, UnknownMethodException
 from selenium.webdriver.support.ui import WebDriverWait
 
+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
 
 
-class Activities(webdriver.Remote):
-    def start_activity(self, app_package, app_activity, **opts):
-        """Opens an arbitrary activity during a test. If the activity belongs to
-        another application, that application is started and the activity is opened.
-
-        This is an Android-only method.
-
-        Args:
-            app_package (str): The package containing the activity to start.
-            app_activity (str): The activity to start.
-
-        Keyword Args:
-            app_wait_package (str): Begin automation after this package starts.
-            app_wait_activity (str): Begin automation after this activity starts.
-            intent_action (str): Intent to start.
-            intent_category (str): Intent category to start.
-            intent_flags (str): Flags to send to the intent.
-            optional_intent_arguments (str): Optional arguments to the intent.
-            dont_stop_app_on_reset (str): Should the app be stopped on reset?
-        """
-        data = {
-            'appPackage': app_package,
-            'appActivity': app_activity
-        }
-        arguments = {
-            'app_wait_package': 'appWaitPackage',
-            'app_wait_activity': 'appWaitActivity',
-            'intent_action': 'intentAction',
-            'intent_category': 'intentCategory',
-            'intent_flags': 'intentFlags',
-            'optional_intent_arguments': 'optionalIntentArguments',
-            'dont_stop_app_on_reset': 'dontStopAppOnReset'
-        }
-        for key, value in arguments.items():
-            if key in opts:
-                data[value] = opts[key]
-        self.execute(Command.START_ACTIVITY, data)
-        return self
-
+class Activities(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
     @property
-    def current_activity(self):
+    def current_activity(self) -> str:
         """Retrieves the current activity running on the device.
 
         Returns:
             str: The current activity name running on the device
         """
-        return self.execute(Command.GET_CURRENT_ACTIVITY)['value']
+        ext_name = 'mobile: getCurrentActivity'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_ACTIVITY)['value']
 
-    def wait_activity(self, activity, timeout, interval=1):
+    def wait_activity(self, activity: str, timeout: int, interval: int = 1) -> bool:
         """Wait for an activity: block until target activity presents or time out.
 
         This is an Android-only method.
 
         Args:
-            activity (str): target activity
-            timeout (int): max wait time, in seconds
-            interval (int): sleep interval between retries, in seconds
+            activity: target activity
+            timeout: max wait time, in seconds
+            interval: sleep interval between retries, in seconds
+
+        Returns:
+            `True` if the target activity is shown
         """
         try:
-            WebDriverWait(self, timeout, interval).until(
-                lambda d: d.current_activity == activity)
+            WebDriverWait(self, timeout, interval).until(  # type: ignore[type-var]
+                lambda d: d.current_activity == activity
+            )
             return True
         except TimeoutException:
             return False
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_CURRENT_ACTIVITY] = \
-            ('GET', '/session/$sessionId/appium/device/current_activity')
-        self.command_executor._commands[Command.START_ACTIVITY] = \
-            ('POST', '/session/$sessionId/appium/device/start_activity')
+    def _add_commands(self) -> None:
+        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 4fa3a00fe..fe75260b5 100644
--- a/appium/webdriver/extensions/android/common.py
+++ b/appium/webdriver/extensions/android/common.py
@@ -12,51 +12,48 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+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
 
 
-class Common(webdriver.Remote):
-
-    def end_test_coverage(self, intent, path):
-        """Ends the coverage collection and pull the coverage.ec file from the device.
-
-        Android only.
-        See https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android/android-coverage.md
-
-        Args:
-            intent (str): description of operation to be performed
-            path (str): path to coverage.ec file to be pulled from the device
-
-        Returns:
-            TODO
-        """
-        data = {
-            'intent': intent,
-            'path': path,
-        }
-        return self.execute(Command.END_TEST_COVERAGE, data)['value']
-
-    def open_notifications(self):
+class Common(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def open_notifications(self) -> Self:
         """Open notification shade in Android (API Level 18 and above)
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Common']: Self instance
         """
-        self.execute(Command.OPEN_NOTIFICATIONS, {})
+        ext_name = 'mobile: openNotifications'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.OPEN_NOTIFICATIONS, {})
         return self
 
     @property
-    def current_package(self):
-        """Retrieves the current package running on the device.
-        """
-        return self.execute(Command.GET_CURRENT_PACKAGE)['value']
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_CURRENT_PACKAGE] = \
-            ('GET', '/session/$sessionId/appium/device/current_package')
-        self.command_executor._commands[Command.END_TEST_COVERAGE] = \
-            ('POST', '/session/$sessionId/appium/app/end_test_coverage')
-        self.command_executor._commands[Command.OPEN_NOTIFICATIONS] = \
-            ('POST', '/session/$sessionId/appium/device/open_notifications')
+    def current_package(self) -> str:
+        """Retrieves the current package running on the device."""
+        ext_name = 'mobile: getCurrentPackage'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_CURRENT_PACKAGE)['value']
+
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(
+            Command.GET_CURRENT_PACKAGE,
+            'GET',
+            '/session/$sessionId/appium/device/current_package',
+        )
+        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 c05e1a5d9..28abdcbbf 100644
--- a/appium/webdriver/extensions/android/display.py
+++ b/appium/webdriver/extensions/android/display.py
@@ -12,26 +12,37 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from selenium.common.exceptions import UnknownMethodException
 
+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
 
 
-class Display(webdriver.Remote):
-
-    def get_display_density(self):
+class Display(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def get_display_density(self) -> int:
         """Get the display density, Android only
 
         Returns:
-            int: The display density of the Android device(dpi)
+            The display density of the Android device(dpi)
 
         Usage:
             self.driver.get_display_density()
-        """
-        return self.execute(Command.GET_DISPLAY_DENSITY)['value']
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_DISPLAY_DENSITY] = \
-            ('GET', '/session/$sessionId/appium/device/display_density')
+        Return:
+            int: The display density
+        """
+        ext_name = 'mobile: getDisplayDensity'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_DISPLAY_DENSITY)['value']
+
+    def _add_commands(self) -> None:
+        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 4aaf4fc8d..ed43d3c66 100644
--- a/appium/webdriver/extensions/android/gsm.py
+++ b/appium/webdriver/extensions/android/gsm.py
@@ -12,21 +12,25 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+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
+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
 
 
-class GsmCallActions(object):
+class GsmCallActions:
     CALL = 'call'
     ACCEPT = 'accept'
     CANCEL = 'cancel'
     HOLD = 'hold'
 
 
-class GsmSignalStrength(object):
+class GsmSignalStrength:
     NONE_OR_UNKNOWN = 0
     POOR = 1
     MODERATE = 2
@@ -34,7 +38,7 @@ class GsmSignalStrength(object):
     GREAT = 4
 
 
-class GsmVoiceState(object):
+class GsmVoiceState:
     UNREGISTERED = 'unregistered'
     HOME = 'home'
     ROAMING = 'roaming'
@@ -44,72 +48,100 @@ class GsmVoiceState(object):
     ON = 'on'
 
 
-class Gsm(webdriver.Remote):
-
-    def make_gsm_call(self, phone_number, action):
+class Gsm(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def make_gsm_call(self, phone_number: str, action: str) -> Self:
         """Make GSM call (Emulator only)
 
         Android only.
 
         Args:
-            phone_number (str): The phone number to call to.
-            action (str): The call action.
+            phone_number: The phone number to call to.
+            action: The call action.
                 A member of the const `appium.webdriver.extensions.android.gsm.GsmCallActions`
 
-        :Usage:
+        Usage:
             self.driver.make_gsm_call('5551234567', GsmCallActions.CALL)
+
+        Returns:
+            Union['WebDriver', 'Gsm']: Self instance
         """
+        ext_name = 'mobile: gsmCall'
         constants = extract_const_attributes(GsmCallActions)
         if action not in constants.values():
-            logger.warning('{} is unknown. Consider using one of {} constants. (e.g. {}.CALL)'.format(
-                action, list(constants.keys()), GsmCallActions.__name__))
-        self.execute(Command.MAKE_GSM_CALL, {'phoneNumber': phone_number, 'action': action})
+            logger.warning(
+                f'{action} is unknown. Consider using one of {list(constants.keys())} constants. '
+                f'(e.g. {GsmCallActions.__name__}.CALL)'
+            )
+        args = {'phoneNumber': phone_number, 'action': action}
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.MAKE_GSM_CALL, args)
         return self
 
-    def set_gsm_signal(self, strength):
+    def set_gsm_signal(self, strength: int) -> Self:
         """Set GSM signal strength (Emulator only)
 
         Android only.
 
         Args:
-            strength (int): Signal strength.
-                A member of the enum `appium.webdriver.extensions.android.gsm.GsmSignalStrength`
+            strength: Signal strength.
+                A member of the enum :obj:`appium.webdriver.extensions.android.gsm.GsmSignalStrength`
 
         Usage:
             self.driver.set_gsm_signal(GsmSignalStrength.GOOD)
+
+        Returns:
+            Union['WebDriver', 'Gsm']: Self instance
         """
+        ext_name = 'mobile: gsmSignal'
         constants = extract_const_attributes(GsmSignalStrength)
         if strength not in constants.values():
-            logger.warning('{} is out of range. Consider using one of {} constants. (e.g. {}.GOOD)'.format(
-                strength, list(constants.keys()), GsmSignalStrength.__name__))
-        self.execute(Command.SET_GSM_SIGNAL, {'signalStrength': strength, 'signalStrengh': strength})
+            logger.warning(
+                f'{strength} is out of range. Consider using one of {list(constants.keys())} constants. '
+                f'(e.g. {GsmSignalStrength.__name__}.GOOD)'
+            )
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, {'strength': strength})
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(
+                Command.SET_GSM_SIGNAL, {'signalStrength': strength, 'signalStrengh': strength}
+            )
         return self
 
-    def set_gsm_voice(self, state):
+    def set_gsm_voice(self, state: str) -> Self:
         """Set GSM voice state (Emulator only)
 
         Android only.
 
         Args:
-            state (str): State of GSM voice.
+            state: State of GSM voice.
                 A member of the const `appium.webdriver.extensions.android.gsm.GsmVoiceState`
 
         Usage:
             self.driver.set_gsm_voice(GsmVoiceState.HOME)
+
+        Returns:
+            Union['WebDriver', 'Gsm']: Self instance
         """
+        ext_name = 'mobile: gmsVoice'
         constants = extract_const_attributes(GsmVoiceState)
         if state not in constants.values():
-            logger.warning('{} is unknown. Consider using one of {} constants. (e.g. {}.HOME)'.format(
-                state, list(constants.keys()), GsmVoiceState.__name__))
-        self.execute(Command.SET_GSM_VOICE, {'state': state})
+            logger.warning(
+                f'{state} is unknown. Consider using one of {list(constants.keys())} constants. '
+                f'(e.g. {GsmVoiceState.__name__}.HOME)'
+            )
+        args = {'state': state}
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.SET_GSM_VOICE, args)
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.MAKE_GSM_CALL] = \
-            ('POST', '/session/$sessionId/appium/device/gsm_call')
-        self.command_executor._commands[Command.SET_GSM_SIGNAL] = \
-            ('POST', '/session/$sessionId/appium/device/gsm_signal')
-        self.command_executor._commands[Command.SET_GSM_VOICE] = \
-            ('POST', '/session/$sessionId/appium/device/gsm_voice')
+    def _add_commands(self) -> None:
+        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/nativekey.py b/appium/webdriver/extensions/android/nativekey.py
index aca374eb0..05b558790 100644
--- a/appium/webdriver/extensions/android/nativekey.py
+++ b/appium/webdriver/extensions/android/nativekey.py
@@ -1,3 +1,18 @@
+#!/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.
+
+
 class AndroidKey:
     # Key code constant: Unknown key code.
     UNKNOWN = 0
@@ -852,7 +867,7 @@ class AndroidKey:
     TV_SATELLITE_SERVICE = 240
 
     # Key code constant: Toggle Network key.
-    # Toggles selecting broacast services.
+    # Toggles selecting broadcast services.
     TV_NETWORK = 241
 
     # Key code constant: Antenna/Cable key.
@@ -995,53 +1010,110 @@ class AndroidKey:
     # Key code constant: Copy key.
     COPY = 278
 
-    gamepad_buttons = [BUTTON_A, BUTTON_B, BUTTON_C, BUTTON_X,
-                       BUTTON_Y, BUTTON_Z, BUTTON_L1, BUTTON_R1,
-                       BUTTON_L2, BUTTON_R2, BUTTON_THUMBL, BUTTON_THUMBR,
-                       BUTTON_START, BUTTON_SELECT, BUTTON_MODE, BUTTON_1,
-                       BUTTON_2, BUTTON_3, BUTTON_4, BUTTON_5, BUTTON_6,
-                       BUTTON_7, BUTTON_8, BUTTON_9, BUTTON_10,
-                       BUTTON_11, BUTTON_12, BUTTON_13, BUTTON_14,
-                       BUTTON_15, BUTTON_16]
+    gamepad_buttons = [
+        BUTTON_A,
+        BUTTON_B,
+        BUTTON_C,
+        BUTTON_X,
+        BUTTON_Y,
+        BUTTON_Z,
+        BUTTON_L1,
+        BUTTON_R1,
+        BUTTON_L2,
+        BUTTON_R2,
+        BUTTON_THUMBL,
+        BUTTON_THUMBR,
+        BUTTON_START,
+        BUTTON_SELECT,
+        BUTTON_MODE,
+        BUTTON_1,
+        BUTTON_2,
+        BUTTON_3,
+        BUTTON_4,
+        BUTTON_5,
+        BUTTON_6,
+        BUTTON_7,
+        BUTTON_8,
+        BUTTON_9,
+        BUTTON_10,
+        BUTTON_11,
+        BUTTON_12,
+        BUTTON_13,
+        BUTTON_14,
+        BUTTON_15,
+        BUTTON_16,
+    ]
 
     @staticmethod
-    def is_gamepad_button(code):
+    def is_gamepad_button(code: int) -> bool:
         """Returns true if the specified nativekey is a gamepad button."""
         return code in AndroidKey.gamepad_buttons
 
     confirm_buttons = [DPAD_CENTER, ENTER, SPACE, NUMPAD_ENTER]
 
     @staticmethod
-    def is_confirm_key(code):
+    def is_confirm_key(code: int) -> bool:
         """Returns true if the key will, by default, trigger a click on the focused view."""
         return code in AndroidKey.confirm_buttons
 
-    media_buttons = [MEDIA_PLAY, MEDIA_PAUSE, MEDIA_PLAY_PAUSE,
-                     MUTE, HEADSETHOOK, MEDIA_STOP, MEDIA_NEXT, MEDIA_PREVIOUS,
-                     MEDIA_REWIND, MEDIA_RECORD, MEDIA_FAST_FORWARD]
+    media_buttons = [
+        MEDIA_PLAY,
+        MEDIA_PAUSE,
+        MEDIA_PLAY_PAUSE,
+        MUTE,
+        HEADSETHOOK,
+        MEDIA_STOP,
+        MEDIA_NEXT,
+        MEDIA_PREVIOUS,
+        MEDIA_REWIND,
+        MEDIA_RECORD,
+        MEDIA_FAST_FORWARD,
+    ]
 
     @staticmethod
-    def is_media_key(code):
+    def is_media_key(code: int) -> bool:
         """Returns true if this key is a media key, which can be send to apps that are
         interested in media key events."""
         return code in AndroidKey.media_buttons
 
-    system_buttons = [MENU, SOFT_RIGHT, HOME, BACK, CALL,
-                      ENDCALL, VOLUME_UP, VOLUME_DOWN, VOLUME_MUTE, MUTE,
-                      POWER, HEADSETHOOK, MEDIA_PLAY, MEDIA_PAUSE,
-                      MEDIA_PLAY_PAUSE, MEDIA_STOP, MEDIA_NEXT,
-                      MEDIA_PREVIOUS, MEDIA_REWIND, MEDIA_RECORD,
-                      MEDIA_FAST_FORWARD, CAMERA, FOCUS, SEARCH,
-                      BRIGHTNESS_DOWN, BRIGHTNESS_UP, MEDIA_AUDIO_TRACK]
+    system_buttons = [
+        MENU,
+        SOFT_RIGHT,
+        HOME,
+        BACK,
+        CALL,
+        ENDCALL,
+        VOLUME_UP,
+        VOLUME_DOWN,
+        VOLUME_MUTE,
+        MUTE,
+        POWER,
+        HEADSETHOOK,
+        MEDIA_PLAY,
+        MEDIA_PAUSE,
+        MEDIA_PLAY_PAUSE,
+        MEDIA_STOP,
+        MEDIA_NEXT,
+        MEDIA_PREVIOUS,
+        MEDIA_REWIND,
+        MEDIA_RECORD,
+        MEDIA_FAST_FORWARD,
+        CAMERA,
+        FOCUS,
+        SEARCH,
+        BRIGHTNESS_DOWN,
+        BRIGHTNESS_UP,
+        MEDIA_AUDIO_TRACK,
+    ]
 
     @staticmethod
-    def is_system_key(code):
+    def is_system_key(code: int) -> bool:
         """Returns true if the key is a system key, System keys can not be used for menu shortcuts."""
         return code in AndroidKey.system_buttons
 
     wake_buttons = [BACK, MENU, WAKEUP, PAIRING, STEM_1, STEM_2, STEM_3]
 
     @staticmethod
-    def is_wake_key(code):
+    def is_wake_key(code: int) -> bool:
         """Returns true if the key is a wake key."""
         return code in AndroidKey.wake_buttons
diff --git a/appium/webdriver/extensions/android/network.py b/appium/webdriver/extensions/android/network.py
index 3a648154a..6054e29d9 100644
--- a/appium/webdriver/extensions/android/network.py
+++ b/appium/webdriver/extensions/android/network.py
@@ -12,14 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+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
+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
 
 
-class NetSpeed(object):
+class NetSpeed:
     GSM = 'gsm'  # GSM/CSD (up: 14.4(kbps), down: 14.4(kbps))
     SCSD = 'scsd'  # HSCSD (up: 14.4, down: 57.6)
     GPRS = 'gprs'  # GPRS (up: 28.8, down: 57.6)
@@ -31,83 +35,141 @@ class NetSpeed(object):
     FULL = 'full'  # No limit, the default (up: 0.0, down: 0.0)
 
 
-class Network(webdriver.Remote):
+class NetworkMask:
+    WIFI = 0b010
+    DATA = 0b100
+    AIRPLANE_MODE = 0b001
 
+
+class Network(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
     @property
-    def network_connection(self):
+    def network_connection(self) -> int:
         """Returns an integer bitmask specifying the network connection type.
 
         Android only.
         Possible values are available through the enumeration `appium.webdriver.ConnectionType`
-        """
-        return self.execute(Command.GET_NETWORK_CONNECTION, {})['value']
 
-    def set_network_connection(self, connection_type):
+        This API only works reliably on emulators (any version) and real devices
+        since API level 31.
+        """
+        ext_name = 'mobile: getConnectivity'
+        try:
+            result_map = self.assert_extension_exists(ext_name).execute_script(ext_name)
+            return (
+                (NetworkMask.WIFI if result_map['wifi'] else 0)
+                | (NetworkMask.DATA if result_map['data'] else 0)
+                | (NetworkMask.AIRPLANE_MODE if result_map['airplaneMode'] else 0)
+            )
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_NETWORK_CONNECTION, {})['value']
+
+    def set_network_connection(self, connection_type: int) -> int:
         """Sets the network connection type. Android only.
 
         Possible values:
-            Value (Alias)      | Data | Wifi | Airplane Mode
-            -------------------------------------------------
-            0 (None)           | 0    | 0    | 0
-            1 (Airplane Mode)  | 0    | 0    | 1
-            2 (Wifi only)      | 0    | 1    | 0
-            4 (Data only)      | 1    | 0    | 0
-            6 (All network on) | 1    | 1    | 0
+
+            +--------------------+------+------+---------------+
+            | Value (Alias)      | Data | Wifi | Airplane Mode |
+            +====================+======+======+===============+
+            | 0 (None)           | 0    | 0    | 0             |
+            +--------------------+------+------+---------------+
+            | 1 (Airplane Mode)  | 0    | 0    | 1             |
+            +--------------------+------+------+---------------+
+            | 2 (Wifi only)      | 0    | 1    | 0             |
+            +--------------------+------+------+---------------+
+            | 4 (Data only)      | 1    | 0    | 0             |
+            +--------------------+------+------+---------------+
+            | 6 (All network on) | 1    | 1    | 0             |
+            +--------------------+------+------+---------------+
+
         These are available through the enumeration `appium.webdriver.ConnectionType`
 
+        This API only works reliably on emulators (any version) and real devices
+        since API level 31.
+
         Args:
-            connection_type (int): a member of the enum appium.webdriver.ConnectionType
+            connection_type: a member of the enum `appium.webdriver.ConnectionType`
 
-        Returns:
-            `appium.webdriver.webdriver.WebDriver`
+        Return:
+            int: Set network connection type
         """
-        data = {
-            'parameters': {
-                'type': connection_type
-            }
-        }
-        return self.execute(Command.SET_NETWORK_CONNECTION, data)['value']
-
-    def toggle_wifi(self):
+        ext_name = 'mobile: setConnectivity'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    'wifi': bool(connection_type & NetworkMask.WIFI),
+                    'data': bool(connection_type & NetworkMask.DATA),
+                    'airplaneMode': bool(connection_type & NetworkMask.AIRPLANE_MODE),
+                },
+            )
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(
+                Command.SET_NETWORK_CONNECTION, {'parameters': {'type': connection_type}}
+            )['value']
+
+    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.
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Network']: Self instance
         """
-        self.execute(Command.TOGGLE_WIFI, {})
+        ext_name = 'mobile: setConnectivity'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(
+                ext_name, {'wifi': not (self.network_connection & NetworkMask.WIFI)}
+            )
+        except UnknownMethodException:
+            self.mark_extension_absence(ext_name).execute(Command.TOGGLE_WIFI, {})
         return self
 
-    def set_network_speed(self, speed_type):
+    def set_network_speed(self, speed_type: str) -> Self:
         """Set the network speed emulation.
 
         Android Emulator only.
 
         Args:
-            speed_type (str): The network speed type.
+            speed_type: The network speed type.
                 A member of the const appium.webdriver.extensions.android.network.NetSpeed.
 
         Usage:
             self.driver.set_network_speed(NetSpeed.LTE)
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Network']: Self instance
         """
         constants = extract_const_attributes(NetSpeed)
         if speed_type not in constants.values():
-            logger.warning('{} is unknown. Consider using one of {} constants. (e.g. {}.LTE)'.format(
-                speed_type, list(constants.keys()), NetSpeed.__name__))
-
-        self.execute(Command.SET_NETWORK_SPEED, {'netspeed': speed_type})
+            logger.warning(
+                f'{speed_type} is unknown. Consider using one of {list(constants.keys())} constants. '
+                f'(e.g. {NetSpeed.__name__}.LTE)'
+            )
+        ext_name = 'mobile: networkSpeed'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, {'speed': speed_type})
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.SET_NETWORK_SPEED, {'netspeed': speed_type})
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.TOGGLE_WIFI] = \
-            ('POST', '/session/$sessionId/appium/device/toggle_wifi')
-        self.command_executor._commands[Command.GET_NETWORK_CONNECTION] = \
-            ('GET', '/session/$sessionId/network_connection')
-        self.command_executor._commands[Command.SET_NETWORK_CONNECTION] = \
-            ('POST', '/session/$sessionId/network_connection')
-        self.command_executor._commands[Command.SET_NETWORK_SPEED] = \
-            ('POST', '/session/$sessionId/appium/device/network_speed')
+    def _add_commands(self) -> None:
+        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',
+        )
+        self.command_executor.add_command(
+            Command.SET_NETWORK_CONNECTION,
+            'POST',
+            '/session/$sessionId/network_connection',
+        )
+        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 7b77b74e9..f5781cf3f 100644
--- a/appium/webdriver/extensions/android/performance.py
+++ b/appium/webdriver/extensions/android/performance.py
@@ -12,38 +12,49 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Dict, List, Union
 
-from appium.webdriver.mobilecommand import MobileCommand as Command
+from selenium.common.exceptions import UnknownMethodException
 
+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
 
-class Performance(webdriver.Remote):
 
-    def get_performance_data(self, package_name, data_type, data_read_timeout=None):
+class Performance(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def get_performance_data(
+        self, package_name: str, data_type: str, data_read_timeout: Union[int, None] = None
+    ) -> List[List[str]]:
         """Returns the information of the system state
         which is supported to read as like cpu, memory, network traffic, and battery.
 
         Android only.
 
         Args:
-            package_name (str): The package name of the application
-            data_type (str): The type of system state which wants to read.
+            package_name: The package name of the application
+            data_type: The type of system state which wants to read.
                 It should be one of the supported performance data types.
                 Check :func:`.get_performance_data_types` for supported types
-            data_read_timeout (:obj:`int`, optional): The number of attempts to read
+            data_read_timeout: The number of attempts to read
 
         Usage:
             self.driver.get_performance_data('my.app.package', 'cpuinfo', 5)
 
         Returns:
-            dict: The data along to `data_type`
+            The data along to `data_type`
         """
-        data = {'packageName': package_name, 'dataType': data_type}
-        if data_read_timeout is not None:
-            data['dataReadTimeout'] = data_read_timeout
-        return self.execute(Command.GET_PERFORMANCE_DATA, data)['value']
+        ext_name = 'mobile: getPerformanceData'
+        args: Dict[str, Union[str, int]] = {'packageName': package_name, 'dataType': data_type}
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            if data_read_timeout is not None:
+                args['dataReadTimeout'] = data_read_timeout
+            return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA, args)['value']
 
-    def get_performance_data_types(self):
+    def get_performance_data_types(self) -> List[str]:
         """Returns the information types of the system state
         which is supported to read as like cpu, memory, network traffic, and battery.
         Android only.
@@ -52,14 +63,23 @@ def get_performance_data_types(self):
             self.driver.get_performance_data_types()
 
         Returns:
-            list: Available data types
+            Available data types
         """
-        return self.execute(Command.GET_PERFORMANCE_DATA_TYPES)['value']
-
-    # pylint: disable=protected-access
+        ext_name = 'mobile: getPerformanceDataTypes'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_PERFORMANCE_DATA_TYPES)['value']
 
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_PERFORMANCE_DATA] = \
-            ('POST', '/session/$sessionId/appium/getPerformanceData')
-        self.command_executor._commands[Command.GET_PERFORMANCE_DATA_TYPES] = \
-            ('POST', '/session/$sessionId/appium/performanceData/types')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(
+            Command.GET_PERFORMANCE_DATA,
+            'POST',
+            '/session/$sessionId/appium/getPerformanceData',
+        )
+        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 bfa6a8248..537ab6803 100644
--- a/appium/webdriver/extensions/android/power.py
+++ b/appium/webdriver/extensions/android/power.py
@@ -12,54 +12,69 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+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
 
 
-class Power(webdriver.Remote):
-
+class Power(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
     AC_OFF, AC_ON = 'off', 'on'
 
-    def set_power_capacity(self, percent):
+    def set_power_capacity(self, percent: int) -> Self:
         """Emulate power capacity change on the connected emulator.
 
         Android only.
 
         Args:
-            percent (int): The power capacity to be set. Can be set from 0 to 100
+            percent: The power capacity to be set. Can be set from 0 to 100
 
         Usage:
             self.driver.set_power_capacity(50)
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Power']: Self instance
         """
-        self.execute(Command.SET_POWER_CAPACITY, {'percent': percent})
+        ext_name = 'mobile: powerCapacity'
+        args = {'percent': percent}
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.SET_POWER_CAPACITY, args)
         return self
 
-    def set_power_ac(self, ac_state):
+    def set_power_ac(self, ac_state: str) -> Self:
         """Emulate power state change on the connected emulator.
 
         Android only.
 
         Args:
-            ac_state (str): The power ac state to be set. Use `.AC_OFF`, `.AC_ON`
+            ac_state: The power ac state to be set. Use `Power.AC_OFF`, `Power.AC_ON`
 
         Usage:
-            self.driver.set_power_ac(Power.AC_OFF)
-            self.driver.set_power_ac(Power.AC_ON)
+            | self.driver.set_power_ac(Power.AC_OFF)
+            | self.driver.set_power_ac(Power.AC_ON)
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Power']: Self instance
         """
-        self.execute(Command.SET_POWER_AC, {'state': ac_state})
+        ext_name = 'mobile: powerAC'
+        args = {'state': ac_state}
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.SET_POWER_AC, args)
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.SET_POWER_CAPACITY] = \
-            ('POST', '/session/$sessionId/appium/device/power_capacity')
-        self.command_executor._commands[Command.SET_POWER_AC] = \
-            ('POST', '/session/$sessionId/appium/device/power_ac')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(
+            Command.SET_POWER_CAPACITY,
+            'POST',
+            '/session/$sessionId/appium/device/power_capacity',
+        )
+        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 4c0675a08..f5769c561 100644
--- a/appium/webdriver/extensions/android/sms.py
+++ b/appium/webdriver/extensions/android/sms.py
@@ -12,33 +12,39 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+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
 
 
-class Sms(webdriver.Remote):
-
-    def send_sms(self, phone_number, message):
+class Sms(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def send_sms(self, phone_number: str, message: str) -> Self:
         """Emulate send SMS event on the connected emulator.
 
         Android only.
 
         Args:
-            phone_number (str): The phone number of message sender
-            message (str): The message to send
+            phone_number: The phone number of message sender
+            message: The message to send
 
         Usage:
             self.driver.send_sms('555-123-4567', 'Hey lol')
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Sms']: Self instance
         """
-        self.execute(Command.SEND_SMS, {'phoneNumber': phone_number, 'message': message})
+        ext_name = 'mobile: sendSms'
+        args = {'phoneNumber': phone_number, 'message': message}
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.SEND_SMS, args)
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.SEND_SMS] = \
-            ('POST', '/session/$sessionId/appium/device/send_sms')
+    def _add_commands(self) -> None:
+        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 edc98a5e1..a02c21f8f 100644
--- a/appium/webdriver/extensions/android/system_bars.py
+++ b/appium/webdriver/extensions/android/system_bars.py
@@ -12,37 +12,47 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Dict, Union
 
-from appium.webdriver.mobilecommand import MobileCommand as Command
+from selenium.common.exceptions import UnknownMethodException
 
+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
 
-class SystemBars(webdriver.Remote):
 
-    def get_system_bars(self):
+class SystemBars(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def get_system_bars(self) -> Dict[str, Dict[str, Union[int, bool]]]:
         """Retrieve visibility and bounds information of the status and navigation bars.
 
         Android only.
 
         Returns:
             A dictionary whose keys are
-             - statusBar
-               - visible
-               - x
-               - y
-               - width
-               - height
-             - navigationBar
-               - visible
-               - x
-               - y
-               - width
-               - height
+               - statusBar
+                   - visible
+                   - x
+                   - y
+                   - width
+                   - height
+               - navigationBar
+                   - visible
+                   - x
+                   - y
+                   - width
+                   - height
         """
-        return self.execute(Command.GET_SYSTEM_BARS)['value']
-
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_SYSTEM_BARS] = \
-            ('GET', '/session/$sessionId/appium/device/system_bars')
+        ext_name = 'mobile: getSystemBars'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_SYSTEM_BARS)['value']
+
+    def _add_commands(self) -> None:
+        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 e89b097da..b259422ec 100644
--- a/appium/webdriver/extensions/applications.py
+++ b/appium/webdriver/extensions/applications.py
@@ -11,47 +11,71 @@
 # WITHOUT 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 Any, Dict, Union
 
-from selenium import webdriver
+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
+from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence
 
 from ..mobilecommand import MobileCommand as Command
 
 
-class Applications(webdriver.Remote):
-    def background_app(self, seconds):
+class Applications(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def background_app(self, seconds: int) -> Self:
         """Puts the application in the background on the device for a certain duration.
 
         Args:
-            seconds (int): the duration for the application to remain in the background
+            seconds: the duration for the application to remain in the background.
+            Providing a negative value will continue immediately after putting the app
+            under test to the background.
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Applications']: Self instance
         """
-        data = {
-            'seconds': seconds,
-        }
-        self.execute(Command.BACKGROUND, data)
+        ext_name = 'mobile: backgroundApp'
+        args = {'seconds': seconds}
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.BACKGROUND, args)
         return self
 
-    def is_app_installed(self, bundle_id):
+    def is_app_installed(self, bundle_id: str) -> bool:
         """Checks whether the application specified by `bundle_id` is installed on the device.
 
         Args:
-            bundle_id (str): the id of the application to query
+            bundle_id: the id of the application to query
 
         Returns:
-            bool: `True` if app is installed
+            `True` if app is installed
         """
-        data = {
-            'bundleId': bundle_id,
-        }
-        return self.execute(Command.IS_APP_INSTALLED, data)['value']
-
-    def install_app(self, app_path, **options):
+        ext_name = 'mobile: isAppInstalled'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    'bundleId': bundle_id,
+                    'appId': bundle_id,
+                },
+            )
+        except (UnknownMethodException, InvalidArgumentException):
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(
+                Command.IS_APP_INSTALLED,
+                {
+                    'bundleId': bundle_id,
+                },
+            )['value']
+
+    def install_app(self, app_path: str, **options: Any) -> Self:
         """Install the application found at `app_path` on the device.
 
         Args:
-            app_path (str): the local or remote path to the application to install
+            app_path: the local or remote path to the application to install
 
         Keyword Args:
             replace (bool): [Android only] whether to reinstall/upgrade the package if it is
@@ -65,21 +89,31 @@ def install_app(self, app_path, **options):
                 on Android 6+ after the installation completes. False by default
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Applications']: Self instance
         """
-        data = {
-            'appPath': app_path,
-        }
-        if options:
-            data.update({'options': options})
-        self.execute(Command.INSTALL_APP, data)
+        ext_name = 'mobile: installApp'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(
+                'mobile: installApp',
+                {
+                    'app': app_path,
+                    'appPath': app_path,
+                    **(options or {}),
+                },
+            )
+        except (UnknownMethodException, InvalidArgumentException):
+            # TODO: Remove the fallback
+            data: Dict[str, Any] = {'appPath': app_path}
+            if options:
+                data.update({'options': options})
+            self.mark_extension_absence(ext_name).execute(Command.INSTALL_APP, data)
         return self
 
-    def remove_app(self, app_id, **options):
+    def remove_app(self, app_id: str, **options: Any) -> Self:
         """Remove the specified application from the device.
 
         Args:
-            app_id (str): the application id to be removed
+            app_id: the application id to be removed
 
         Keyword Args:
             keepData (bool): [Android only] whether to keep application data and caches after it is uninstalled.
@@ -88,129 +122,153 @@ def remove_app(self, app_id, **options):
                 20000ms by default.
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
-        """
-        data = {
-            'appId': app_id,
-        }
-        if options:
-            data.update({'options': options})
-        self.execute(Command.REMOVE_APP, data)
-        return self
-
-    def launch_app(self):
-        """Start on the device the application specified in the desired capabilities.
-
-        Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Applications']: Self instance
         """
-        self.execute(Command.LAUNCH_APP)
+        ext_name = 'mobile: removeApp'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    'appId': app_id,
+                    'bundleId': app_id,
+                    **(options or {}),
+                },
+            )
+        except (UnknownMethodException, InvalidArgumentException):
+            # TODO: Remove the fallback
+            data: Dict[str, Any] = {'appId': app_id}
+            if options:
+                data.update({'options': options})
+            self.mark_extension_absence(ext_name).execute(Command.REMOVE_APP, data)
         return self
 
-    def close_app(self):
-        """Stop the running application, specified in the desired capabilities, on
-        the device.
-
-        Returns:
-            `appium.webdriver.webdriver.WebDriver`
-        """
-        self.execute(Command.CLOSE_APP)
-        return self
-
-    def terminate_app(self, app_id, **options):
+    def terminate_app(self, app_id: str, **options: Any) -> bool:
         """Terminates the application if it is running.
 
         Args:
-            app_id (str): the application id to be terminates
+            app_id: the application id to be terminates
 
         Keyword Args:
             `timeout` (int): [Android only] how much time to wait for the uninstall to complete.
                 500ms by default.
 
         Returns:
-            bool: True if the app has been successfully terminated
+            True if the app has been successfully terminated
         """
-        data = {
-            'appId': app_id,
-        }
-        if options:
-            data.update({'options': options})
-        return self.execute(Command.TERMINATE_APP, data)['value']
-
-    def activate_app(self, app_id):
+        ext_name = 'mobile: terminateApp'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    'appId': app_id,
+                    'bundleId': app_id,
+                    **(options or {}),
+                },
+            )
+        except (UnknownMethodException, InvalidArgumentException):
+            # TODO: Remove the fallback
+            data: Dict[str, Any] = {'appId': app_id}
+            if options:
+                data.update({'options': options})
+            return self.mark_extension_absence(ext_name).execute(Command.TERMINATE_APP, data)['value']
+
+    def activate_app(self, app_id: str) -> Self:
         """Activates the application if it is not running
         or is running in the background.
 
         Args:
-            app_id (str): the application id to be activated
+            app_id: the application id to be activated
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Applications']: Self instance
         """
-        data = {
-            'appId': app_id,
-        }
-        self.execute(Command.ACTIVATE_APP, data)
+        ext_name = 'mobile: activateApp'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    'appId': app_id,
+                    'bundleId': app_id,
+                },
+            )
+        except (UnknownMethodException, InvalidArgumentException):
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.ACTIVATE_APP, {'appId': app_id})
         return self
 
-    def query_app_state(self, app_id):
+    def query_app_state(self, app_id: str) -> int:
         """Queries the state of the application.
 
         Args:
-            app_id (str): the application id to be queried
+            app_id: the application id to be queried
 
         Returns:
             One of possible application state constants. See ApplicationState
             class for more details.
         """
-        data = {
-            'appId': app_id,
-        }
-        return self.execute(Command.QUERY_APP_STATE, data)['value']
-
-    def app_strings(self, language=None, string_file=None):
+        ext_name = 'mobile: queryAppState'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    'appId': app_id,
+                    'bundleId': app_id,
+                },
+            )
+        except (UnknownMethodException, InvalidArgumentException):
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(
+                Command.QUERY_APP_STATE,
+                {
+                    'appId': app_id,
+                },
+            )['value']
+
+    def app_strings(self, language: Union[str, None] = None, string_file: Union[str, None] = None) -> Dict[str, str]:
         """Returns the application strings from the device for the specified
         language.
 
         Args:
-            language (str): strings language code
-            string_file (str): the name of the string file to query
+            language: strings language code
+            string_file: the name of the string file to query. Only relevant for XCUITest driver
+
+        Returns:
+            The key is string id and the value is the content.
         """
+        ext_name = 'mobile: getAppStrings'
         data = {}
         if language is not None:
             data['language'] = language
         if string_file is not None:
             data['stringFile'] = string_file
-        return self.execute(Command.GET_APP_STRINGS, data)['value']
-
-    def reset(self):
-        """Resets the current application on the device.
-        """
-        self.execute(Command.RESET)
-        return self
-
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.BACKGROUND] = \
-            ('POST', '/session/$sessionId/appium/app/background')
-        self.command_executor._commands[Command.IS_APP_INSTALLED] = \
-            ('POST', '/session/$sessionId/appium/device/app_installed')
-        self.command_executor._commands[Command.INSTALL_APP] = \
-            ('POST', '/session/$sessionId/appium/device/install_app')
-        self.command_executor._commands[Command.REMOVE_APP] = \
-            ('POST', '/session/$sessionId/appium/device/remove_app')
-        self.command_executor._commands[Command.TERMINATE_APP] = \
-            ('POST', '/session/$sessionId/appium/device/terminate_app')
-        self.command_executor._commands[Command.ACTIVATE_APP] = \
-            ('POST', '/session/$sessionId/appium/device/activate_app')
-        self.command_executor._commands[Command.QUERY_APP_STATE] = \
-            ('POST', '/session/$sessionId/appium/device/app_state')
-        self.command_executor._commands[Command.GET_APP_STRINGS] = \
-            ('POST', '/session/$sessionId/appium/app/strings')
-        self.command_executor._commands[Command.RESET] = \
-            ('POST', '/session/$sessionId/appium/app/reset')
-        self.command_executor._commands[Command.LAUNCH_APP] = \
-            ('POST', '/session/$sessionId/appium/app/launch')
-        self.command_executor._commands[Command.CLOSE_APP] = \
-            ('POST', '/session/$sessionId/appium/app/close')
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name, data)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_APP_STRINGS, data)['value']
+
+    def _add_commands(self) -> None:
+        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',
+        )
+        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',
+        )
+        self.command_executor.add_command(
+            Command.ACTIVATE_APP,
+            'POST',
+            '/session/$sessionId/appium/device/activate_app',
+        )
+        self.command_executor.add_command(
+            Command.QUERY_APP_STATE,
+            'POST',
+            '/session/$sessionId/appium/device/app_state',
+        )
+        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 e05e6b86c..f5354f2e7 100644
--- a/appium/webdriver/extensions/clipboard.py
+++ b/appium/webdriver/extensions/clipboard.py
@@ -13,71 +13,95 @@
 # limitations under the License.
 
 import base64
+from typing import Optional
 
-from selenium import webdriver
+from selenium.common.exceptions import UnknownMethodException
+from typing_extensions import Self
 
-from appium.common.helper import appium_bytes
+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.clipboard_content_type import ClipboardContentType
 
 from ..mobilecommand import MobileCommand as Command
 
 
-class Clipboard(webdriver.Remote):
-
-    def set_clipboard(self, content, content_type=ClipboardContentType.PLAINTEXT, label=None):
+class Clipboard(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def set_clipboard(
+        self, content: bytes, content_type: str = ClipboardContentType.PLAINTEXT, label: Optional[str] = None
+    ) -> Self:
         """Set the content of the system clipboard
 
         Args:
-            content (str): The content to be set as bytearray string
-            content_type (str): One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT
+            content: The content to be set as bytearray string
+            content_type: One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT
                 is supported on Android
-            label (:obj:`str`, optional): label argument, which only works for Android
+            label: label argument, which only works for Android
+
+        Returns:
+            Union['WebDriver', 'Clipboard']: Self instance
         """
+        ext_name = 'mobile: setClipboard'
         options = {
             'content': base64.b64encode(content).decode('UTF-8'),
             'contentType': content_type,
         }
         if label:
             options['label'] = label
-        self.execute(Command.SET_CLIPBOARD, options)
-
-    def set_clipboard_text(self, text, label=None):
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, options)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.SET_CLIPBOARD, options)
+        return self
+
+    def set_clipboard_text(self, text: str, label: Optional[str] = None) -> Self:
         """Copies the given text to the system clipboard
 
         Args:
-            text (str): The text to be set
-            label (:obj:`int`, optional):label argument, which only works for Android
-        """
+            text: The text to be set
+            label:label argument, which only works for Android
 
-        self.set_clipboard(appium_bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label)
+        Returns:
+            Union['WebDriver', 'Clipboard']: Self instance
+        """
+        return self.set_clipboard(bytes(str(text), 'UTF-8'), ClipboardContentType.PLAINTEXT, label)
 
-    def get_clipboard(self, content_type=ClipboardContentType.PLAINTEXT):
+    def get_clipboard(self, content_type: str = ClipboardContentType.PLAINTEXT) -> bytes:
         """Receives the content of the system clipboard
 
         Args:
-            content_type (str): One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT
+            content_type: One of ClipboardContentType items. Only ClipboardContentType.PLAINTEXT
                 is supported on Android
 
         Returns:
-            base64-encoded string: Clipboard content. Or return an empty string if the clipboard is empty
+            Clipboard content as bytearray. Or empty bytes if the clipboard is empty
         """
-        base64_str = self.execute(Command.GET_CLIPBOARD, {
-            'contentType': content_type
-        })['value']
+        ext_name = 'mobile: getClipboard'
+        options = {'contentType': content_type}
+        try:
+            base64_str = self.assert_extension_exists(ext_name).execute_script(ext_name, options)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            base64_str = self.mark_extension_absence(ext_name).execute(Command.GET_CLIPBOARD, options)['value']
         return base64.b64decode(base64_str)
 
-    def get_clipboard_text(self):
+    def get_clipboard_text(self) -> str:
         """Receives the text of the system clipboard
 
-        Return:
-            str: The actual clipboard text or an empty string if the clipboard is empty
+        Returns:
+            The actual clipboard text or an empty string if the clipboard is empty
         """
         return self.get_clipboard(ClipboardContentType.PLAINTEXT).decode('UTF-8')
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.SET_CLIPBOARD] = \
-            ('POST', '/session/$sessionId/appium/device/set_clipboard')
-        self.command_executor._commands[Command.GET_CLIPBOARD] = \
-            ('POST', '/session/$sessionId/appium/device/get_clipboard')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(
+            Command.SET_CLIPBOARD,
+            'POST',
+            '/session/$sessionId/appium/device/set_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 8581cc3f4..628432e11 100644
--- a/appium/webdriver/extensions/context.py
+++ b/appium/webdriver/extensions/context.py
@@ -12,14 +12,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import List
+
+from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands
 
 from ..mobilecommand import MobileCommand as Command
 
 
-class Context(webdriver.Remote):
+class Context(CanExecuteCommands):
     @property
-    def contexts(self):
+    def contexts(self) -> List[str]:
         """Returns the contexts within the current session.
 
         Usage:
@@ -32,7 +34,7 @@ def contexts(self):
         return self.execute(Command.CONTEXTS)['value']
 
     @property
-    def current_context(self):
+    def current_context(self) -> str:
         """Returns the current context of the current session.
 
         Usage:
@@ -44,7 +46,7 @@ def current_context(self):
         return self.execute(Command.GET_CURRENT_CONTEXT)['value']
 
     @property
-    def context(self):
+    def context(self) -> str:
         """Returns the current context of the current session.
 
         Usage:
@@ -55,12 +57,7 @@ def context(self):
         """
         return self.current_context
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.CONTEXTS] = \
-            ('GET', '/session/$sessionId/contexts')
-        self.command_executor._commands[Command.GET_CURRENT_CONTEXT] = \
-            ('GET', '/session/$sessionId/context')
-        self.command_executor._commands[Command.SWITCH_TO_CONTEXT] = \
-            ('POST', '/session/$sessionId/context')
+    def _add_commands(self) -> None:
+        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 e40b36d21..22ac3c25a 100644
--- a/appium/webdriver/extensions/device_time.py
+++ b/appium/webdriver/extensions/device_time.py
@@ -12,46 +12,64 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Optional
 
-from ..mobilecommand import MobileCommand as Command
+from selenium.common.exceptions import UnknownMethodException
+
+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 ..mobilecommand import MobileCommand as Command
 
-class DeviceTime(webdriver.Remote):
 
+class DeviceTime(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
     @property
-    def device_time(self):
+    def device_time(self) -> str:
         """Returns the date and time from the device.
 
         Return:
             str: The date and time
         """
-        return self.execute(Command.GET_DEVICE_TIME_GET, {})['value']
+        ext_name = 'mobile: getDeviceTime'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.GET_DEVICE_TIME_GET, {})['value']
 
-    def get_device_time(self, format=None):
+    def get_device_time(self, format: Optional[str] = None) -> str:
         """Returns the date and time from the device.
 
         Args:
-            format (optional):  The set of format specifiers. Read https://momentjs.com/docs/
+            format:  The set of format specifiers. Read https://momentjs.com/docs/
                 to get the full list of supported datetime format specifiers.
                 If unset, return :func:`.device_time` as default format is `YYYY-MM-DDTHH:mm:ssZ`,
                 which complies to ISO-8601
 
         Usage:
-            self.driver.get_device_time()
-            self.driver.get_device_time("YYYY-MM-DD")
+            | self.driver.get_device_time()
+            | self.driver.get_device_time("YYYY-MM-DD")
 
         Return:
             str: The date and time
         """
+        ext_name = 'mobile: getDeviceTime'
         if format is None:
             return self.device_time
-        return self.execute(Command.GET_DEVICE_TIME_POST, {'format': format})['value']
-
-    # pylint: disable=protected-access
+        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']
 
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_DEVICE_TIME_GET] = \
-            ('GET', '/session/$sessionId/appium/device/system_time')
-        self.command_executor._commands[Command.GET_DEVICE_TIME_POST] = \
-            ('POST', '/session/$sessionId/appium/device/system_time')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(
+            Command.GET_DEVICE_TIME_GET,
+            'GET',
+            '/session/$sessionId/appium/device/system_time',
+        )
+        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 5c866a398..698c0218b 100644
--- a/appium/webdriver/extensions/execute_driver.py
+++ b/appium/webdriver/extensions/execute_driver.py
@@ -12,50 +12,49 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Any, Dict, Optional, Union
 
-from ..mobilecommand import MobileCommand as Command
+from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands
 
+from ..mobilecommand import MobileCommand as Command
 
-class ExecuteDriver(webdriver.Remote):
 
-    def execute_driver(self, script, script_type='webdriverio', timeout_ms=None):
+class ExecuteDriver(CanExecuteCommands):
+    # TODO Inner class case
+    def execute_driver(self, script: str, script_type: str = 'webdriverio', timeout_ms: Optional[int] = None) -> Any:
         """Run a set of script against the current session, allowing execution of many commands in one Appium request.
         Please read http://appium.io/docs/en/commands/session/execute-driver for more details about the acceptable
         scripts and the output format.
 
         Args:
-            script (string): The string consisting of the script itself
-            script_type (string): The name of the script type. Defaults to 'webdriverio'.
-            timeout_ms (optional): The number of `ms` Appium should wait for the script to finish before killing it due to timeout_ms.
+            script: The string consisting of the script itself
+            script_type: The name of the script type. Defaults to 'webdriverio'.
+            timeout_ms: The number of `ms` Appium should wait for the script to finish before
+             killing it due to timeout_ms.
 
         Usage:
-            self.driver.execute_driver(script='return [];')
-            self.driver.execute_driver(script='return [];', script_type='webdriverio')
-            self.driver.execute_driver(script='return [];', script_type='webdriverio', timeout_ms=10000)
+            | self.driver.execute_driver(script='return [];')
+            | self.driver.execute_driver(script='return [];', script_type='webdriverio')
+            | self.driver.execute_driver(script='return [];', script_type='webdriverio', timeout_ms=10000)
 
         Returns:
             ExecuteDriver.Result: The result of the script. It has 'result' and 'logs' keys.
 
         Raises:
-            WebDriverException: If something error happenes in the script. The message has the original error message.
+            WebDriverException: If something error happens in the script. The message has the original error message.
         """
 
-        class Result(object):
+        class Result:
+            def __init__(self, res: Dict):
+                self.result = res['result']
+                self.logs = res['logs']
 
-            def __init__(self, response):
-                self.result = response['result']
-                self.logs = response['logs']
-
-        option = {'script': script, 'type': script_type}
+        option: Dict[str, Union[str, int]] = {'script': script, 'type': script_type}
         if timeout_ms is not None:
             option['timeout'] = timeout_ms
 
         response = self.execute(Command.EXECUTE_DRIVER, option)['value']
         return Result(response)
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.EXECUTE_DRIVER] = \
-            ('POST', '/session/$sessionId/appium/execute_driver')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(Command.EXECUTE_DRIVER, 'POST', '/session/$sessionId/appium/execute_driver')
diff --git a/appium/webdriver/extensions/execute_mobile_command.py b/appium/webdriver/extensions/execute_mobile_command.py
index 789dcfef7..8d78d418b 100644
--- a/appium/webdriver/extensions/execute_mobile_command.py
+++ b/appium/webdriver/extensions/execute_mobile_command.py
@@ -12,12 +12,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Any, Dict
 
+from typing_extensions import Self
 
-class ExecuteMobileCommand(webdriver.Remote):
+from appium.protocols.webdriver.can_execute_scripts import CanExecuteScripts
 
-    def press_button(self, button_name):
+
+class ExecuteMobileCommand(CanExecuteScripts):
+    def press_button(self, button_name: str) -> Self:
         """Sends a physical button name to the device to simulate the user pressing.
 
         iOS only.
@@ -25,20 +28,18 @@ def press_button(self, button_name):
         https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Categories/XCUIDevice%2BFBHelpers.h
 
         Args:
-            button_name (str): the button name to be sent to the device
+            button_name: the button name to be sent to the device
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'ExecuteMobileCommand']: Self instance
 
         """
-        data = {
-            'name': button_name
-        }
+        data = {'name': button_name}
         self.execute_script('mobile: pressButton', data)
         return self
 
     @property
-    def battery_info(self):
+    def battery_info(self) -> Dict[str, Any]:
         """Retrieves battery information for the device under test.
 
         Returns:
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.
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..5e469cae1
--- /dev/null
+++ b/appium/webdriver/extensions/flutter_integration/flutter_commands.py
@@ -0,0 +1,296 @@
+#!/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, List, 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 get_render_tree(
+        self,
+        widget_type: Optional[str] = None,
+        key: Optional[str] = None,
+        text: Optional[str] = None,
+    ) -> List[Optional[Dict]]:
+        """
+        Returns the render tree of the root widget.
+
+        Args:
+            widget_type (Optional[str]): The type of the widget to primary filter by.
+            key (Optional[str]): The key of the widget to filter by.
+            text (Optional[str]): The text of the widget to filter by.
+
+        Returns:
+            List[Optional[Dict]]: A list of dictionaries or None values representing the render tree.
+
+            The result is a nested list of dictionaries representing each widget and its properties,
+            such as type, key, size, attribute, state, visual information, and hierarchy.
+
+            The example widget includes the following code, which is rendered as part of the widget tree:
+
+            .. code-block:: dart
+
+                Semantics(
+                    key: const Key('add_activity_semantics'),
+                    label: 'add_activity_button',
+                    button: true,
+                    child: FloatingActionButton.small(
+                        key: const Key('add_activity_button'),
+                        tooltip: 'add_activity_button',
+                        heroTag: 'add',
+                        backgroundColor: const Color(0xFF2E2E3A),
+                        onPressed: null,
+                        child: Icon(
+                        Icons.add,
+                        size: 16,
+                        color: Colors.amber.shade200.withOpacity(0.5),
+                        semanticLabel: 'Add icon',
+                        ),
+                    ),
+                )
+
+        Example execute command:
+
+            .. code-block:: python
+
+                >>> flutter_command = FlutterCommand(driver)  # noqa
+                >>> flutter_command.get_render_tree(widget_type='Semantics', key='add_activity_semantics')
+                output >> [
+                            {
+                                "type": "Semantics",
+                                "elementType": "SingleChildRenderObjectElement",
+                                "description": "Semantics-[<'add_activity_semantics'>]",
+                                "depth": 0,
+                                "key": "[<'add_activity_semantics'>]",
+                                "attributes": {
+                                    "semanticsLabel": "add_activity_button"
+                                },
+                                "visual": {},
+                                "state": {},
+                                "rect": {
+                                    "x": 0,
+                                    "y": 0,
+                                    "width": 48,
+                                    "height": 48
+                                },
+                                "children": [
+                                    {
+                                        "type": "FloatingActionButton",
+                                        "elementType": "StatelessElement",
+                                        "description": "FloatingActionButton-[<'add_activity_button'>]",
+                                        "depth": 1,
+                                        "key": "[<'add_activity_button'>]",
+                                        "attributes": {},
+                                        "visual": {},
+                                        "state": {},
+                                        "rect": {
+                                            "x": 0,
+                                            "y": 0,
+                                            "width": 48,
+                                            "height": 48
+                                        },
+                                        "children": [
+                                            {...},
+                                                "children": [...]
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        ]
+        """
+        opts = {}
+        if widget_type is not None:
+            opts['widgetType'] = widget_type
+        if key is not None:
+            opts['key'] = key
+        if text is not None:
+            opts['text'] = text
+
+        return self.execute_flutter_command('renderTree', opts)
+
+    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, Union[dict, WebElement]]:
+        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..0aed46a9a
--- /dev/null
+++ b/appium/webdriver/extensions/flutter_integration/flutter_finder.py
@@ -0,0 +1,55 @@
+# 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, Union, cast
+
+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: Union[SeleniumByType, AppiumByType], value: str) -> None:
+        self.using = using
+        self.value = value
+
+    @staticmethod
+    def by_key(value: str) -> 'FlutterFinder':
+        return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_KEY), value)
+
+    @staticmethod
+    def by_text(value: str) -> 'FlutterFinder':
+        return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TEXT), value)
+
+    @staticmethod
+    def by_semantics_label(value: str) -> 'FlutterFinder':
+        return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_SEMANTICS_LABEL), value)
+
+    @staticmethod
+    def by_type(value: str) -> 'FlutterFinder':
+        return FlutterFinder(cast(AppiumByType, AppiumBy.FLUTTER_INTEGRATION_TYPE), value)
+
+    @staticmethod
+    def by_text_containing(value: str) -> 'FlutterFinder':
+        return FlutterFinder(cast(AppiumByType, 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/appium/webdriver/extensions/hw_actions.py b/appium/webdriver/extensions/hw_actions.py
index a3d6babb7..b6bbb468b 100644
--- a/appium/webdriver/extensions/hw_actions.py
+++ b/appium/webdriver/extensions/hw_actions.py
@@ -12,107 +12,138 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Optional
 
-from ..mobilecommand import MobileCommand as Command
+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 ..mobilecommand import MobileCommand as Command
 
-class HardwareActions(webdriver.Remote):
 
-    def lock(self, seconds=None):
+class HardwareActions(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def lock(self, seconds: Optional[int] = None) -> Self:
         """Lock the device. No changes are made if the device is already unlocked.
 
         Args:
-            seconds (optional): The duration to lock the device, in seconds.
+            seconds: The duration to lock the device, in seconds.
                 The device is going to be locked forever until `unlock` is called
                 if it equals or is less than zero, otherwise this call blocks until
                 the timeout expires and unlocks the screen automatically.
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'HardwareActions']: Self instance
         """
-        if seconds is None:
-            self.execute(Command.LOCK)
-        else:
-            self.execute(Command.LOCK, {'seconds': seconds})
-
+        ext_name = 'mobile: lock'
+        args = {'seconds': seconds or 0}
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.LOCK, args)
         return self
 
-    def unlock(self):
+    def unlock(self) -> Self:
         """Unlock the device. No changes are made if the device is already locked.
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'HardwareActions']: Self instance
         """
-        self.execute(Command.UNLOCK)
+        ext_name = 'mobile: unlock'
+        try:
+            if not self.assert_extension_exists(ext_name).execute_script('mobile: isLocked'):
+                return self
+            self.execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.UNLOCK)
         return self
 
-    def is_locked(self):
+    def is_locked(self) -> bool:
         """Checks whether the device is locked.
 
         Returns:
-            bool: `True` if the device is locked
+            `True` if the device is locked
         """
-        return self.execute(Command.IS_LOCKED)['value']
-
-    def shake(self):
+        ext_name = 'mobile: isLocked'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script('mobile: isLocked')
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.IS_LOCKED)['value']
+
+    def shake(self) -> Self:
         """Shake the device.
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'HardwareActions']: Self instance
         """
-        self.execute(Command.SHAKE)
+        ext_name = 'mobile: shake'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.SHAKE)
         return self
 
-    def touch_id(self, match):
+    def touch_id(self, match: bool) -> Self:
         """Simulate touchId on iOS Simulator
 
         Args:
-            match (bool): Simulates a successful touch (`True`) or a failed touch (`False`)
+            match: Simulates a successful touch (`True`) or a failed touch (`False`)
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'HardwareActions']: Self instance
         """
-        data = {
-            'match': match
-        }
-        self.execute(Command.TOUCH_ID, data)
+        self.execute_script(
+            'mobile: sendBiometricMatch',
+            {
+                'type': 'touchId',
+                'match': match,
+            },
+        )
         return self
 
-    def toggle_touch_id_enrollment(self):
+    def toggle_touch_id_enrollment(self) -> Self:
         """Toggle enroll touchId on iOS Simulator
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'HardwareActions']: Self instance
         """
-        self.execute(Command.TOGGLE_TOUCH_ID_ENROLLMENT)
+        is_enrolled = self.execute_script('mobile: isBiometricEnrolled')
+        self.execute_script('mobile: enrollBiometric', {'isEnabled': not is_enrolled})
         return self
 
-    def finger_print(self, finger_id):
+    def finger_print(self, finger_id: int) -> Self:
         """Authenticate users by using their finger print scans on supported Android emulators.
 
         Args:
-            finger_id (int): Finger prints stored in Android Keystore system (from 1 to 10)
-
-        Returns:
-            TODO
+            finger_id: Finger prints stored in Android Keystore system (from 1 to 10)
         """
-        return self.execute(Command.FINGER_PRINT, {'fingerprintId': finger_id})['value']
-
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.LOCK] = \
-            ('POST', '/session/$sessionId/appium/device/lock')
-        self.command_executor._commands[Command.UNLOCK] = \
-            ('POST', '/session/$sessionId/appium/device/unlock')
-        self.command_executor._commands[Command.IS_LOCKED] = \
-            ('POST', '/session/$sessionId/appium/device/is_locked')
-        self.command_executor._commands[Command.SHAKE] = \
-            ('POST', '/session/$sessionId/appium/device/shake')
-        self.command_executor._commands[Command.TOUCH_ID] = \
-            ('POST', '/session/$sessionId/appium/simulator/touch_id')
-        self.command_executor._commands[Command.TOGGLE_TOUCH_ID_ENROLLMENT] = \
-            ('POST', '/session/$sessionId/appium/simulator/toggle_touch_id_enrollment')
-        self.command_executor._commands[Command.FINGER_PRINT] = \
-            ('POST', '/session/$sessionId/appium/device/finger_print')
+        ext_name = 'mobile: fingerprint'
+        args = {'fingerprintId': finger_id}
+        try:
+            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 self
+
+    def _add_commands(self) -> None:
+        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',
+        )
+        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 c0f16a903..6f7308157 100644
--- a/appium/webdriver/extensions/images_comparison.py
+++ b/appium/webdriver/extensions/images_comparison.py
@@ -12,14 +12,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Any, Dict, Union
 
-from ..mobilecommand import MobileCommand as Command
+from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands
 
+from ..mobilecommand import MobileCommand as Command
 
-class ImagesComparison(webdriver.Remote):
 
-    def match_images_features(self, base64_image1, base64_image2, **opts):
+class ImagesComparison(CanExecuteCommands):
+    def match_images_features(self, base64_image1: bytes, base64_image2: bytes, **opts: Any) -> Dict[str, Any]:
         """Performs images matching by features.
 
         Read
@@ -28,8 +29,8 @@ def match_images_features(self, base64_image1, base64_image2, **opts):
         The method supports all image formats, which are supported by OpenCV itself.
 
         Args:
-            base64_image1 (bytes): base64-encoded content of the first image
-            base64_image2 (bytes): base64-encoded content of the second image
+            base64_image1: base64-encoded content of the first image
+            base64_image2: base64-encoded content of the second image
 
         Keyword Args:
             visualize (bool): Set it to True in order to return the visualization of the matching operation.
@@ -56,7 +57,7 @@ def match_images_features(self, base64_image1, base64_image2, **opts):
                 It is equal to `count` if `goodMatchesFactor` does not limit the matches,
                 otherwise it contains the total count of matches before `goodMatchesFactor` is
                 applied.
-            points1 (dict)): The array of matching points on the first image. Each point is a dictionary
+            points1 (dict): The array of matching points on the first image. Each point is a dictionary
                 with 'x' and 'y' keys
             rect1 (dict): The bounding rect for the `points1` array or a zero rect if not enough matching points
                 were found. The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys
@@ -65,15 +66,12 @@ def match_images_features(self, base64_image1, base64_image2, **opts):
             rect2 (dict): The bounding rect for the `points2` array or a zero rect if not enough matching points
                 were found. The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys
         """
-        options = {
-            'mode': 'matchFeatures',
-            'firstImage': base64_image1,
-            'secondImage': base64_image2,
-            'options': opts
-        }
+        options = {'mode': 'matchFeatures', 'firstImage': base64_image1, 'secondImage': base64_image2, 'options': opts}
         return self.execute(Command.COMPARE_IMAGES, options)['value']
 
-    def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts):
+    def find_image_occurrence(
+        self, base64_full_image: bytes, base64_partial_image: bytes, **opts: Any
+    ) -> Dict[str, Union[bytes, Dict]]:
         """Performs images matching by template to find possible occurrence of the partial image
         in the full image.
 
@@ -83,28 +81,29 @@ def find_image_occurrence(self, base64_full_image, base64_partial_image, **opts)
         The method supports all image formats, which are supported by OpenCV itself.
 
         Args:
-            base64_full_image (bytes): base64-encoded content of the full image
-            base64_partial_image (bytes): base64-encoded content of the partial image
+            base64_full_image: base64-encoded content of the full image
+            base64_partial_image: base64-encoded content of the partial image
 
         Keyword Args:
             visualize (bool): Set it to True in order to return the visualization of the matching operation.
                 False by default
 
         Returns:
-            visualization (bytes): base64-encoded content of PNG visualization of the current comparison
-                operation. This entry is only present if `visualize` option is enabled
-            rect (dict): The region of the partial image occurrence on the full image.
-                The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys
+            The dictionary containing the following entries:
+                visualization (bytes): base64-encoded content of PNG visualization of the current comparison
+                    operation. This entry is only present if `visualize` option is enabled
+                rect (dict): The region of the partial image occurrence on the full image.
+                    The rect is represented by a dictionary with 'x', 'y', 'width' and 'height' keys
         """
         options = {
             'mode': 'matchTemplate',
             'firstImage': base64_full_image,
             'secondImage': base64_partial_image,
-            'options': opts
+            'options': opts,
         }
         return self.execute(Command.COMPARE_IMAGES, options)['value']
 
-    def get_images_similarity(self, base64_image1, base64_image2, **opts):
+    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
@@ -112,29 +111,22 @@ def get_images_similarity(self, base64_image1, base64_image2, **opts):
         The method supports all image formats, which are supported by OpenCV itself.
 
         Args:
-            base64_image1 (bytes): base64-encoded content of the first image
-            base64_image2 (bytes): base64-encoded content of the second image
+            base64_image1: base64-encoded content of the first image
+            base64_image2: base64-encoded content of the second image
 
         Keyword Args:
-            visualize (bool): Set it to True in order to return the visualization of the matching operation.
+            visualize (boo): Set it to True in order to return the visualization of the matching operation.
                 False by default
 
         Returns:
-            visualization (bytes): base64-encoded content of PNG visualization of the current comparison
-                operation. This entry is only present if `visualize` option is enabled
-            score (float): The similarity score as a float number in range [0.0, 1.0].
-                1.0 is the highest score (means both images are totally equal).
+            The dictionary containing the following entries:
+                visualization (bytes): base64-encoded content of PNG visualization of the current comparison
+                    operation. This entry is only present if `visualize` option is enabled
+                score (float): The similarity score as a float number in range [0.0, 1.0].
+                    1.0 is the highest score (means both images are totally equal).
         """
-        options = {
-            'mode': 'getSimilarity',
-            'firstImage': base64_image1,
-            'secondImage': base64_image2,
-            'options': opts
-        }
+        options = {'mode': 'getSimilarity', 'firstImage': base64_image1, 'secondImage': base64_image2, 'options': opts}
         return self.execute(Command.COMPARE_IMAGES, options)['value']
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.COMPARE_IMAGES] = \
-            ('POST', '/session/$sessionId/appium/compare_images')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(Command.COMPARE_IMAGES, 'POST', '/session/$sessionId/appium/compare_images')
diff --git a/appium/webdriver/extensions/ime.py b/appium/webdriver/extensions/ime.py
deleted file mode 100644
index 9c3230e32..000000000
--- a/appium/webdriver/extensions/ime.py
+++ /dev/null
@@ -1,95 +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 selenium import webdriver
-
-from ..mobilecommand import MobileCommand as Command
-
-
-class IME(webdriver.Remote):
-
-    @property
-    def available_ime_engines(self):
-        """Get the available input methods for an Android device.
-
-        Package and activity are returned (e.g., ['com.android.inputmethod.latin/.LatinIME'])
-        Android only.
-
-        Returns:
-            :obj:`list` of :obj:`str`: The available input methods for an Android device
-        """
-        return self.execute(Command.GET_AVAILABLE_IME_ENGINES, {})['value']
-
-    def is_ime_active(self):
-        """Checks whether the device has IME service active.
-        Android only.
-
-        Returns:
-            bool: `True` if IME service is active
-        """
-        return self.execute(Command.IS_IME_ACTIVE, {})['value']
-
-    def activate_ime_engine(self, engine):
-        """Activates the given IME engine on the device.
-
-        Android only.
-
-        Args:
-           engine (str): the package and activity of the IME engine to activate
-               (e.g., 'com.android.inputmethod.latin/.LatinIME')
-
-        Returns:
-            `appium.webdriver.webdriver.WebDriver`
-        """
-        data = {
-            'engine': engine
-        }
-        self.execute(Command.ACTIVATE_IME_ENGINE, data)
-        return self
-
-    def deactivate_ime_engine(self):
-        """Deactivates the currently active IME engine on the device.
-
-        Android only.
-
-        Returns:
-            `appium.webdriver.webdriver.WebDriver`
-        """
-        self.execute(Command.DEACTIVATE_IME_ENGINE, {})
-        return self
-
-    @property
-    def active_ime_engine(self):
-        """Returns the activity and package of the currently active IME engine(e.g., 'com.android.inputmethod.latin/.LatinIME').
-
-        Android only.
-
-        Returns:
-            str: The activity and package of the currently active IME engine
-        """
-        return self.execute(Command.GET_ACTIVE_IME_ENGINE, {})['value']
-
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_AVAILABLE_IME_ENGINES] = \
-            ('GET', '/session/$sessionId/ime/available_engines')
-        self.command_executor._commands[Command.IS_IME_ACTIVE] = \
-            ('GET', '/session/$sessionId/ime/activated')
-        self.command_executor._commands[Command.ACTIVATE_IME_ENGINE] = \
-            ('POST', '/session/$sessionId/ime/activate')
-        self.command_executor._commands[Command.DEACTIVATE_IME_ENGINE] = \
-            ('POST', '/session/$sessionId/ime/deactivate')
-        self.command_executor._commands[Command.GET_ACTIVE_IME_ENGINE] = \
-            ('GET', '/session/$sessionId/ime/active_engine')
diff --git a/appium/webdriver/extensions/keyboard.py b/appium/webdriver/extensions/keyboard.py
index 612a99b64..4640b1163 100644
--- a/appium/webdriver/extensions/keyboard.py
+++ b/appium/webdriver/extensions/keyboard.py
@@ -12,120 +12,157 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Dict, Optional
 
-from ..mobilecommand import MobileCommand as Command
+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 ..mobilecommand import MobileCommand as Command
 
-class Keyboard(webdriver.Remote):
 
-    def hide_keyboard(self, key_name=None, key=None, strategy=None):
+class Keyboard(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    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
         a particular key, or `strategy`. In Android, no parameters are used.
 
         Args:
-            key_name (:obj:`str`, optional): key to press
-            key (:obj:`str`, optional):
-            strategy (:obj:`str`, optional): strategy for closing the keyboard (e.g., `tapOutside`)
+            key_name: key to press
+            key:
+            strategy: strategy for closing the keyboard (e.g., `tapOutside`)
+
+        Returns:
+            Union['WebDriver', 'Keyboard']: Self instance
         """
-        data = {}
-        if key_name is not None:
-            data['keyName'] = key_name
-        elif key is not None:
-            data['key'] = key
-        elif strategy is None:
-            strategy = 'tapOutside'
-        data['strategy'] = strategy
-        self.execute(Command.HIDE_KEYBOARD, data)
+        ext_name = 'mobile: hideKeyboard'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(
+                ext_name, {**({'keys': [key or key_name]} if key or key_name else {})}
+            )
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            data: Dict[str, Optional[str]] = {}
+            if key_name is not None:
+                data['keyName'] = key_name
+            elif key is not None:
+                data['key'] = key
+            elif strategy is None:
+                strategy = 'tapOutside'
+            data['strategy'] = strategy
+            self.mark_extension_absence(ext_name).execute(Command.HIDE_KEYBOARD, data)
         return self
 
-    def is_keyboard_shown(self):
+    def is_keyboard_shown(self) -> bool:
         """Attempts to detect whether a software keyboard is present
 
         Returns:
-            bool: `True` if keyboard is shown
+            `True` if keyboard is shown
         """
-        return self.execute(Command.IS_KEYBOARD_SHOWN)['value']
+        ext_name = 'mobile: isKeyboardShown'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name)
+        except UnknownMethodException:
+            return self.mark_extension_absence(ext_name).execute(Command.IS_KEYBOARD_SHOWN)['value']
 
-    def keyevent(self, keycode, metastate=None):
+    def keyevent(self, keycode: int, metastate: Optional[int] = None) -> Self:
         """Sends a keycode to the device.
 
         Android only.
         Possible keycodes can be found in http://developer.android.com/reference/android/view/KeyEvent.html.
 
         Args:
-            keycode (int): the keycode to be sent to the device
-            metastate (:obj:`int`, optional): meta information about the keycode being sent
+            keycode: the keycode to be sent to the device
+            metastate: meta information about the keycode being sent
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Keyboard']: Self instance
         """
-        data = {
-            'keycode': keycode,
-        }
-        if metastate is not None:
-            data['metastate'] = metastate
-        self.execute(Command.KEY_EVENT, data)
-        return self
+        return self.press_keycode(keycode=keycode, metastate=metastate)
 
-    def press_keycode(self, keycode, metastate=None, flags=None):
+    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 in http://developer.android.com/reference/android/view/KeyEvent.html.
+        Android only. Possible keycodes can be found
+        in http://developer.android.com/reference/android/view/KeyEvent.html.
 
         Args:
-            keycode (int): the keycode to be sent to the device
-            metastate (:obj:`int`, optional): meta information about the keycode being sent
-            flags (:obj:`int`, optional): the set of key event flags
+            keycode: the keycode to be sent to the device
+            metastate: meta information about the keycode being sent
+            flags: the set of key event flags
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Keyboard']: Self instance
         """
-        data = {
-            'keycode': keycode,
-        }
+        ext_name = 'mobile: pressKey'
+        args = {'keycode': keycode}
         if metastate is not None:
-            data['metastate'] = metastate
+            args['metastate'] = metastate
         if flags is not None:
-            data['flags'] = flags
-        self.execute(Command.PRESS_KEYCODE, data)
+            args['flags'] = flags
+        try:
+            self.assert_extension_exists(ext_name).execute_script(ext_name, args)
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.PRESS_KEYCODE, args)
         return self
 
-    def long_press_keycode(self, keycode, metastate=None, flags=None):
+    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 http://developer.android.com/reference/android/view/KeyEvent.html.
+        Android only. Possible keycodes can be found in
+        http://developer.android.com/reference/android/view/KeyEvent.html.
 
         Args:
-            keycode (int): the keycode to be sent to the device
-            metastate (:obj:`int`, optional): meta information about the keycode being sent
-            flags (:obj:`int`, optional): the set of key event flags
+            keycode: the keycode to be sent to the device
+            metastate: meta information about the keycode being sent
+            flags: the set of key event flags
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Keyboard']: Self instance
         """
-        data = {
-            'keycode': keycode
-        }
+        ext_name = 'mobile: pressKey'
+        args = {'keycode': keycode}
         if metastate is not None:
-            data['metastate'] = metastate
+            args['metastate'] = metastate
         if flags is not None:
-            data['flags'] = flags
-        self.execute(Command.LONG_PRESS_KEYCODE, data)
+            args['flags'] = flags
+        try:
+            self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    **args,
+                    'isLongPress': True,
+                },
+            )
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(Command.LONG_PRESS_KEYCODE, args)
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.HIDE_KEYBOARD] = \
-            ('POST', '/session/$sessionId/appium/device/hide_keyboard')
-        self.command_executor._commands[Command.IS_KEYBOARD_SHOWN] = \
-            ('GET', '/session/$sessionId/appium/device/is_keyboard_shown')
-        self.command_executor._commands[Command.KEY_EVENT] = \
-            ('POST', '/session/$sessionId/appium/device/keyevent')
-        self.command_executor._commands[Command.PRESS_KEYCODE] = \
-            ('POST', '/session/$sessionId/appium/device/press_keycode')
-        self.command_executor._commands[Command.LONG_PRESS_KEYCODE] = \
-            ('POST', '/session/$sessionId/appium/device/long_press_keycode')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(
+            Command.HIDE_KEYBOARD,
+            'POST',
+            '/session/$sessionId/appium/device/hide_keyboard',
+        )
+        self.command_executor.add_command(
+            Command.IS_KEYBOARD_SHOWN,
+            'GET',
+            '/session/$sessionId/appium/device/is_keyboard_shown',
+        )
+        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',
+        )
+        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 7dffcb0e1..3141050ae 100644
--- a/appium/webdriver/extensions/location.py
+++ b/appium/webdriver/extensions/location.py
@@ -12,63 +12,87 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+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
 
 
-class Location(webdriver.Remote):
-    def toggle_location_services(self):
+class Location(CanExecuteCommands, CanExecuteScripts):
+    def toggle_location_services(self) -> Self:
         """Toggle the location services on the device.
+        This API only reliably since Android 12 (API level 31)
 
         Android only.
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Location']: Self instance
         """
-        self.execute(Command.TOGGLE_LOCATION_SERVICES, {})
+        try:
+            self.execute_script('mobile: toggleGps')
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.execute(Command.TOGGLE_LOCATION_SERVICES)
         return self
 
-    def set_location(self, latitude, longitude, altitude=None):
+    def set_location(
+        self,
+        latitude: Union[float, str],
+        longitude: Union[float, str],
+        altitude: Union[float, str, None] = None,
+        speed: Union[float, str, None] = None,
+        satellites: Union[float, str, None] = None,
+    ) -> Self:
         """Set the location of the device
 
         Args:
-            latitude (Union[float, str]): String or numeric value between -90.0 and 90.00
-            longitude (Union[float, str]): String or numeric value between -180.0 and 180.0
-            altitude (Union[float, str], optional): String or numeric value (Android real device only)
+            latitude: String or numeric value between -90.0 and 90.00
+            longitude: String or numeric value between -180.0 and 180.0
+            altitude: String or numeric value (Android real device only)
+            speed: String or numeric value larger than 0.0 (Android real devices only)
+            satellites: String or numeric value of active GPS satellites in range 1..12. (Android emulators only)
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'Location']: Self instance
         """
         data = {
-            "location": {
-                "latitude": latitude,
-                "longitude": longitude,
+            'location': {
+                'latitude': latitude,
+                'longitude': longitude,
             }
         }
         if altitude is not None:
             data['location']['altitude'] = altitude
+        if speed is not None:
+            data['location']['speed'] = speed
+        if satellites is not None:
+            data['location']['satellites'] = satellites
         self.execute(Command.SET_LOCATION, data)
         return self
 
     @property
-    def location(self):
+    def location(self) -> Dict[str, float]:
         """Retrieves the current location
 
         Returns:
             A dictionary whose keys are
-             - latitude (float)
-             - longitude (float)
-             - altitude (float)
+                - latitude (float)
+                - longitude (float)
+                - altitude (float)
         """
         return self.execute(Command.GET_LOCATION)['value']
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.TOGGLE_LOCATION_SERVICES] = \
-            ('POST', '/session/$sessionId/appium/device/toggle_location_services')
-        self.command_executor._commands[Command.GET_LOCATION] = \
-            ('GET', '/session/$sessionId/location')
-        self.command_executor._commands[Command.SET_LOCATION] = \
-            ('POST', '/session/$sessionId/location')
+    def _add_commands(self) -> None:
+        """Add location endpoints. They are not int w3c spec."""
+        self.command_executor.add_command(
+            Command.TOGGLE_LOCATION_SERVICES,
+            'POST',
+            '/session/$sessionId/appium/device/toggle_location_services',
+        )
+        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 383664d62..4ca53b3fe 100644
--- a/appium/webdriver/extensions/log_event.py
+++ b/appium/webdriver/extensions/log_event.py
@@ -12,61 +12,57 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Dict, List, Union
 
-from ..mobilecommand import MobileCommand as Command
+from typing_extensions import Self
+
+from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands
 
+from ..mobilecommand import MobileCommand as Command
 
-class LogEvent(webdriver.Remote):
 
-    def get_events(self, type=None):
-        """ Retrieves events information from the current session
+class LogEvent(CanExecuteCommands):
+    def get_events(self, type: Union[List[str], None] = None) -> Dict[str, Union[str, int]]:
+        """Retrieves events information from the current session
         (Since Appium 1.16.0)
 
         Args:
-            type (:obj:`list` of :obj:`str`): The event type to filter with
+            type: The event type to filter with
 
         Usage:
-            events = driver.get_events()
-            events = driver.get_events(['appium:funEvent'])
+            | events = driver.get_events()
+            | events = driver.get_events(['appium:funEvent'])
 
         Returns:
             `dict`: A dictionary of events timing information containing the following entries
-                commands: (`list` of `dict`) List of dictionaries containing the following entries
-                    cmd: (str) The command name that has been sent to the appium server
-                    startTime: (int) Received time
-                    endTime: (init) Response time
+                | commands: (`list` of `dict`) List of dictionaries containing the following entries
+                | cmd: The command name that has been sent to the appium server
+                | startTime: Received time
+                | endTime: Response time
         """
         data = {}
         if type is not None:
             data['type'] = type
         return self.execute(Command.GET_EVENTS, data)['value']
 
-    def log_event(self, vendor, event):
+    def log_event(self, vendor: str, event: str) -> Self:
         """Log a custom event on the Appium server.
         (Since Appium 1.16.0)
 
         Args:
-            vendor (str): The vendor to log
-            event (str): The event to log
+            vendor: The vendor to log
+            event: The event to log
 
         Usage:
             driver.log_event('appium', 'funEvent')
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'LogEvent']: Self instance
         """
-        data = {
-            'vendor': vendor,
-            'event': event
-        }
+        data = {'vendor': vendor, 'event': event}
         self.execute(Command.LOG_EVENT, data)
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_EVENTS] = \
-            ('POST', '/session/$sessionId/appium/events')
-        self.command_executor._commands[Command.LOG_EVENT] = \
-            ('POST', '/session/$sessionId/appium/log_event')
+    def _add_commands(self) -> None:
+        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/logs.py b/appium/webdriver/extensions/logs.py
new file mode 100644
index 000000000..53e165944
--- /dev/null
+++ b/appium/webdriver/extensions/logs.py
@@ -0,0 +1,53 @@
+#!/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 Any, Dict, List
+
+from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands
+
+from ..mobilecommand import MobileCommand as Command
+
+
+class Logs(CanExecuteCommands):
+    @property
+    def log_types(self) -> List[str]:
+        """Gets a list of the available log types. This only works with w3c
+        compliant browsers.
+
+        Example:
+            .. code-block:: python
+
+                driver.log_types
+        """
+        return self.execute(Command.GET_AVAILABLE_LOG_TYPES)['value']
+
+    def get_log(self, log_type: str) -> List[Dict[str, Any]]:
+        """Gets the log for a given log type.
+
+        Args:
+            log_type: Type of log that which will be returned
+
+        Example:
+            .. code-block:: python
+
+                driver.get_log('browser')
+                driver.get_log('driver')
+                driver.get_log('client')
+                driver.get_log('server')
+        """
+        return self.execute(Command.GET_LOG, {'type': log_type})['value']
+
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(Command.GET_LOG, 'POST', '/session/$sessionId/se/log')
+        self.command_executor.add_command(Command.GET_AVAILABLE_LOG_TYPES, 'GET', '/session/$sessionId/se/log/types')
diff --git a/appium/webdriver/extensions/remote_fs.py b/appium/webdriver/extensions/remote_fs.py
index 10fbff234..5ccebd37d 100644
--- a/appium/webdriver/extensions/remote_fs.py
+++ b/appium/webdriver/extensions/remote_fs.py
@@ -13,54 +13,64 @@
 # limitations under the License.
 
 import base64
+from typing import Optional
 
-from selenium import webdriver
-from selenium.common.exceptions import InvalidArgumentException
+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
+from appium.protocols.webdriver.can_remember_extension_presence import CanRememberExtensionPresence
 
 from ..mobilecommand import MobileCommand as Command
 
 
-class RemoteFS(webdriver.Remote):
-    def pull_file(self, path):
+class RemoteFS(CanExecuteCommands, CanExecuteScripts, CanRememberExtensionPresence):
+    def pull_file(self, path: str) -> str:
         """Retrieves the file at `path`.
 
         Args:
-            path (str): the path to the file on the device
+            path: the path to the file on the device
 
         Returns:
-            bytes: The file's contents as base64.
+            The file's contents encoded as Base64.
         """
-        data = {
-            'path': path,
-        }
-        return self.execute(Command.PULL_FILE, data)['value']
-
-    def pull_folder(self, path):
+        ext_name = 'mobile: pullFile'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name, {'remotePath': path})
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            return self.mark_extension_absence(ext_name).execute(Command.PULL_FILE, {'path': path})['value']
+
+    def pull_folder(self, path: str) -> str:
         """Retrieves a folder at `path`.
 
         Args:
-            path (str): the path to the folder on the device
+            path: the path to the folder on the device
 
         Returns:
-            bytes: The folder's contents zipped and encoded as Base64.
+            The folder's contents zipped and encoded as Base64.
         """
-        data = {
-            'path': path,
-        }
-        return self.execute(Command.PULL_FOLDER, data)['value']
-
-    def push_file(self, destination_path, base64data=None, source_path=None):
+        ext_name = 'mobile: pullFolder'
+        try:
+            return self.assert_extension_exists(ext_name).execute_script(ext_name, {'remotePath': path})
+        except UnknownMethodException:
+            # 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:
         """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`
 
         Args:
-            destination_path (str): the location on the device/simulator where the local file contents should be saved
-            base64data (:obj:`bytes`, optional): file contents, encoded as Base64, to be written to the file on the device/simulator
-            source_path (:obj:`str`, optional): local file path for the file to be loaded on device
+            destination_path: the location on the device/simulator where the local file contents should be saved
+            base64data: file contents, encoded as Base64, to be written
+            to the file on the device/simulator
+            source_path: local file path for the file to be loaded on device
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            Union['WebDriver', 'RemoteFS']: Self instance
         """
         if source_path is None and base64data is None:
             raise InvalidArgumentException('Must either pass base64 data or a local file path')
@@ -68,25 +78,33 @@ def push_file(self, destination_path, base64data=None, source_path=None):
         if source_path is not None:
             try:
                 with open(source_path, 'rb') as f:
-                    data = f.read()
-            except IOError:
-                message = 'source_path {} could not be found. Are you sure the file exists?'.format(source_path)
-                raise InvalidArgumentException(message)
-            base64data = base64.b64encode(data).decode('utf-8')
-
-        data = {
-            'path': destination_path,
-            'data': base64data,
-        }
-        self.execute(Command.PUSH_FILE, data)
+                    file_data = f.read()
+            except IOError as e:
+                message = f'source_path "{source_path}" could not be found. Are you sure the file exists?'
+                raise InvalidArgumentException(message) from e
+            base64data = base64.b64encode(file_data).decode('utf-8')
+
+        ext_name = 'mobile: pushFile'
+        try:
+            self.assert_extension_exists(ext_name).execute_script(
+                ext_name,
+                {
+                    'remotePath': destination_path,
+                    'payload': base64data,
+                },
+            )
+        except UnknownMethodException:
+            # TODO: Remove the fallback
+            self.mark_extension_absence(ext_name).execute(
+                Command.PUSH_FILE,
+                {
+                    'path': destination_path,
+                    'data': base64data,
+                },
+            )
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.PULL_FILE] = \
-            ('POST', '/session/$sessionId/appium/device/pull_file')
-        self.command_executor._commands[Command.PULL_FOLDER] = \
-            ('POST', '/session/$sessionId/appium/device/pull_folder')
-        self.command_executor._commands[Command.PUSH_FILE] = \
-            ('POST', '/session/$sessionId/appium/device/push_file')
+    def _add_commands(self) -> None:
+        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 0b7f1bc74..11e26c414 100644
--- a/appium/webdriver/extensions/screen_record.py
+++ b/appium/webdriver/extensions/screen_record.py
@@ -12,16 +12,69 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Any, Union
 
-from ..mobilecommand import MobileCommand as Command
+from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands
 
+from ..mobilecommand import MobileCommand as Command
 
-class ScreenRecord(webdriver.Remote):
 
-    def start_recording_screen(self, **options):
+class ScreenRecord(CanExecuteCommands):
+    def start_recording_screen(self, **options: Any) -> Union[bytes, str]:
         """Start asynchronous screen recording process.
 
+        +--------------+-----+---------+-----+-------+
+        | Keyword Args | iOS | Android | Win | macOS |
+        +==============+=====+=========+=====+=======+
+        | remotePath   | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | user         | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | password     | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | method       | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | timeLimit    | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | forceRestart | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | fileFieldName| O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | formFields   | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | headers      | O   | O       | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | videoQuality | O   |         |     |       |
+        +--------------+-----+---------+-----+-------+
+        | videoType    | O   |         |     |       |
+        +--------------+-----+---------+-----+-------+
+        | videoFps     | O   |         |     |       |
+        +--------------+-----+---------+-----+-------+
+        | videoFilter  | O   |         | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | videoScale   | O   |         |     |       |
+        +--------------+-----+---------+-----+-------+
+        | pixelFormat  | O   |         |     |       |
+        +--------------+-----+---------+-----+-------+
+        | videoSize    |     | O       |     |       |
+        +--------------+-----+---------+-----+-------+
+        | bitRate      |     | O       |     |       |
+        +--------------+-----+---------+-----+-------+
+        | bugReport    |     | O       |     |       |
+        +--------------+-----+---------+-----+-------+
+        | fps          |     |         | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | captureCursor|     |         | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | captureClicks|     |         | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | deviceId     |     |         |     | O     |
+        +--------------+-----+---------+-----+-------+
+        | preset       |     |         | O   | O     |
+        +--------------+-----+---------+-----+-------+
+        | audioInput   |     |         | O   |       |
+        +--------------+-----+---------+-----+-------+
+
         Keyword Args:
             remotePath (str): The remotePath upload option is the path to the remote location,
                 where the resulting video from the previous screen recording should be uploaded.
@@ -40,37 +93,63 @@ def start_recording_screen(self, **options):
                 Only has an effect if `remotePath` is set.
             timeLimit (int): The actual time limit of the recorded video in seconds.
                 The default value for both iOS and Android is 180 seconds (3 minutes).
+                The default value for macOS is 600 seconds (10 minutes).
                 The maximum value for Android is 3 minutes.
                 The maximum value for iOS is 10 minutes.
+                The maximum value for macOS is 10000 seconds (166 minutes).
             forcedRestart (bool): Whether to ignore the result of previous capture and start a new recording
-                immediately (`True` value). By default  (`False`) the endpoint will try to catch and return the result of
-                the previous capture if it's still available.
-            bugReport (str): Makes the recorder to display an additional information on the video overlay,
-                such as a timestamp, that is helpful in videos captured to illustrate bugs.
-                This option is only supported since API level 27 (Android P).
+                immediately (`True` value). By default  (`False`) the endpoint will try to catch and
+                return the result of the previous capture if it's still available.
+            fileFieldName (str): [multipart/form-data requests] The name of the form field
+                containing the binary payload. "file" by default. (Since Appium 1.18.0)
+            formFields (dict): [multipart/form-data requests] Additional form fields mapping. If any entry has
+                the same key as `fileFieldName` then it is going to be ignored. (Since Appium 1.18.0)
+            headers (dict): [multipart/form-data requests] Headers mapping (Since Appium 1.18.0)
 
-            videoQuality (str): [iOS only] The video encoding quality: 'low', 'medium', 'high', 'photo'. Defaults to 'medium'.
-            videoType (str): [iOS only] The format of the screen capture to be recorded.
+            videoQuality (str): [iOS] The video encoding quality: 'low', 'medium', 'high', 'photo'. Defaults
+                to 'medium'.
+            videoType (str): [iOS] The format of the screen capture to be recorded.
                 Available formats: Execute `ffmpeg -codecs` in the terminal to see the list of supported video codecs.
                 'mjpeg' by default. (Since Appium 1.10.0)
-            videoFps (int): [iOS only] The Frames Per Second rate of the recorded video. Change this value if the resulting video
-                is too slow or too fast. Defaults to 10. This can decrease the resulting file size.
-            videoFilters (str): [iOS only] The FFMPEG video filters to apply. These filters allow to scale,
-                flip, rotate and do many other useful transformations on the source video stream. The format of the property
-                must comply with https://ffmpeg.org/ffmpeg-filters.html. (Since Appium 1.15)
-            videoScale (str): [iOS only] The scaling value to apply. Read https://trac.ffmpeg.org/wiki/Scaling for possible values.
-                No scale is applied by default. If videoFilters are set then the scale setting is effectively ignored.
-                (Since Appium 1.10.0)
-            pixelFormat (str): [iOS only] Output pixel format. Run `ffmpeg -pix_fmts` to list possible values.
+            videoFps (int): [iOS] The Frames Per Second rate of the recorded video. Change this value if the
+                resulting video is too slow or too fast. Defaults to 10. This can decrease the resulting file size.
+            videoFilters (str): [iOS, Win, macOS] The FFMPEG video filters to apply. These filters allow to scale,
+                flip, rotate and do many other useful transformations on the source video stream. The format of the
+                property must comply with https://ffmpeg.org/ffmpeg-filters.html. (Since Appium 1.15)
+            videoScale (str): [iOS] The scaling value to apply. Read https://trac.ffmpeg.org/wiki/Scaling for
+                possible values. No scale is applied by default. If videoFilters are set then the scale setting is
+                effectively ignored. (Since Appium 1.10.0)
+            pixelFormat (str): [iOS] Output pixel format. Run `ffmpeg -pix_fmts` to list possible values.
                 For Quicktime compatibility, set to "yuv420p" along with videoType: "libx264". (Since Appium 1.12.0)
 
-            videoSize (str): [Android only] The video size of the generated media file. The format is WIDTHxHEIGHT.
+            videoSize (str): [Android] The video size of the generated media file. The format is WIDTHxHEIGHT.
                 The default value is the device's native display resolution (if supported),
                 1280x720 if not. For best results, use a size supported by your device's
                 Advanced Video Coding (AVC) encoder.
-            bitRate (int): [Android only] The video bit rate for the video, in megabits per second.
+            bitRate (int): [Android] The video bit rate for the video, in megabits per second.
                 The default value is 4. You can increase the bit rate to improve video quality,
                 but doing so results in larger movie files.
+            bugReport (str): [Android] Makes the recorder to display an additional information on the video overlay,
+                such as a timestamp, that is helpful in videos captured to illustrate bugs.
+                This option is only supported since API level 27 (Android P).
+
+            fps (int): [Win, macOS] The count of frames per second in the resulting video.
+                Increasing fps value also increases the size of the resulting video file and the CPU usage.
+            captureCursor (bool): [Win, macOS] Whether to capture the mouse cursor while recording the screen.
+                Disabled by default.
+            captureClick (bool): [Win, macOS] Whether to capture the click gestures while recording the screen.
+                Disabled by default.
+            deviceId (int): [macOS] Screen device index to use for the recording.
+                The list of available devices could be retrieved using
+                `ffmpeg -f avfoundation -list_devices true -i` command.
+                This option is mandatory and must be always provided.
+            preset (str): [Win, macOS] A preset is a collection of options that will provide a certain encoding
+                speed to compression ratio. A slower preset will provide better compression
+                (compression is quality per filesize). This means that, for example, if you target a certain file size
+                or constant bit rate, you will achieve better quality with a slower preset.
+                Read https://trac.ffmpeg.org/wiki/Encode/H.264 for more details.
+                Possible values are 'ultrafast', 'superfast', 'veryfast'(default), 'faster', 'fast', 'medium', 'slow',
+                'slower', 'veryslow'
 
         Returns:
             bytes: Base-64 encoded content of the recorded media
@@ -82,7 +161,7 @@ def start_recording_screen(self, **options):
             del options['password']
         return self.execute(Command.START_RECORDING_SCREEN, {'options': options})['value']
 
-    def stop_recording_screen(self, **options):
+    def stop_recording_screen(self, **options: Any) -> bytes:
         """Gather the output from the previously started screen recording to a media file.
 
         Keyword Args:
@@ -99,6 +178,11 @@ def stop_recording_screen(self, **options):
                 Only has an effect if both `remotePath` and `user` are set.
             method (str): The HTTP method name ('PUT'/'POST'). PUT method is used by default.
                 Only has an effect if `remotePath` is set.
+            fileFieldName (str): [multipart/form-data requests] The name of the form field
+                containing the binary payload. "file" by default. (Since Appium 1.18.0)
+            formFields (dict): [multipart/form-data requests] Additional form fields mapping. If any entry has
+                the same key as `fileFieldName` then it is going to be ignored. (Since Appium 1.18.0)
+            headers (dict): [multipart/form-data requests] Headers mapping (Since Appium 1.18.0)
 
         Returns:
             bytes: Base-64 encoded content of the recorded media file or an empty string
@@ -110,10 +194,14 @@ def stop_recording_screen(self, **options):
             del options['password']
         return self.execute(Command.STOP_RECORDING_SCREEN, {'options': options})['value']
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.START_RECORDING_SCREEN] = \
-            ('POST', '/session/$sessionId/appium/start_recording_screen')
-        self.command_executor._commands[Command.STOP_RECORDING_SCREEN] = \
-            ('POST', '/session/$sessionId/appium/stop_recording_screen')
+    def _add_commands(self) -> None:
+        self.command_executor.add_command(
+            Command.START_RECORDING_SCREEN,
+            'POST',
+            '/session/$sessionId/appium/start_recording_screen',
+        )
+        self.command_executor.add_command(
+            Command.STOP_RECORDING_SCREEN,
+            'POST',
+            '/session/$sessionId/appium/stop_recording_screen',
+        )
diff --git a/appium/webdriver/extensions/search_context/__init__.py b/appium/webdriver/extensions/search_context/__init__.py
deleted file mode 100644
index 40f38aac0..000000000
--- a/appium/webdriver/extensions/search_context/__init__.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.
-
-# pylint: disable=abstract-method
-
-from selenium import webdriver
-from selenium.webdriver.remote.webelement import \
-    WebElement as SeleniumWebElement
-
-from .android import AndroidSearchContext
-from .custom import CustomSearchContext
-from .ios import iOSSearchContext
-from .mobile import MobileSearchContext
-from .windows import WindowsSearchContext
-
-
-class AppiumSearchContext(webdriver.Remote,
-                          AndroidSearchContext,
-                          CustomSearchContext,
-                          iOSSearchContext,
-                          MobileSearchContext,
-                          WindowsSearchContext):
-    """Returns appium driver search conext"""
-
-
-class AppiumWebElementSearchContext(SeleniumWebElement,
-                                    AndroidSearchContext,
-                                    CustomSearchContext,
-                                    iOSSearchContext,
-                                    MobileSearchContext,
-                                    WindowsSearchContext):
-    """Returns appium web element search context"""
diff --git a/appium/webdriver/extensions/search_context/android.py b/appium/webdriver/extensions/search_context/android.py
deleted file mode 100644
index 942675c7c..000000000
--- a/appium/webdriver/extensions/search_context/android.py
+++ /dev/null
@@ -1,195 +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.
-
-# pylint: disable=abstract-method
-
-import json
-
-from appium.webdriver.common.mobileby import MobileBy
-
-from .base_search_context import BaseSearchContext
-
-
-class AndroidSearchContext(BaseSearchContext):
-    """Define search context for Android"""
-
-    def find_element_by_android_view_matcher(self, name=None, args=None, className=None):
-        """Finds element by [onView](https://developer.android.com/training/testing/espresso/basics) in Android
-
-        It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
-
-        Args:
-            name (:obj:`str`, optional): The name of a method to invoke.
-                The method must return a Hamcrest
-                [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html)
-            args (:obj:`str`, optional): The args provided to the method
-            className (:obj:`str`, optional): The class name that the method is part of (defaults to `org.hamcrest.Matchers`).
-                Can be fully qualified by having the androidx.test.espresso.matcher. prefix.
-                If the prefix is not provided then it is going to be added implicitly.
-                (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers`
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`: The found element
-
-        Raises:
-            TypeError - Raises a TypeError if the arguments are not validated for JSON format
-
-        Usage:
-            driver.find_element_by_android_view_matcher(name='withText', args=['Accessibility'], className='ViewMatchers')
-
-        # To enable auto completion in PyCharm(IDE)
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-
-        return self.find_element(
-            by=MobileBy.ANDROID_VIEW_MATCHER,
-            value=self._build_data_matcher(name=name, args=args, className=className)
-        )
-
-    def find_element_by_android_data_matcher(self, name=None, args=None, className=None):
-        """Finds element by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android
-
-        It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
-
-        Args:
-            name (:obj:`str`, optional): The name of a method to invoke.
-                The method must return a Hamcrest
-                [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html)
-            args (:obj:`str`, optional): The args provided to the method
-            className (:obj:`str`, optional): The class name that the method is part of (defaults to `org.hamcrest.Matchers`).
-                Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package
-                (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers`
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`: The found element
-
-        Raises:
-            TypeError - Raises a TypeError if the arguments are not validated for JSON format
-
-        Usage:
-            driver.find_element_by_android_data_matcher(name='hasEntry', args=['title', 'Animation'])
-
-        # To enable auto completion in PyCharm(IDE)
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-
-        return self.find_element(
-            by=MobileBy.ANDROID_DATA_MATCHER,
-            value=self._build_data_matcher(name=name, args=args, className=className)
-        )
-
-    def find_elements_by_android_data_matcher(self, name=None, args=None, className=None):
-        """Finds elements by [onData](https://medium.com/androiddevelopers/adapterviews-and-espresso-f4172aa853cf) in Android
-
-        It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
-
-        Args:
-            name (:obj:`str`, optional): The name of a method to invoke.
-                The method must return a Hamcrest
-                [Matcher](http://hamcrest.org/JavaHamcrest/javadoc/1.3/org/hamcrest/Matcher.html)
-            args (:obj:`str`, optional): The args provided to the method
-            className (:obj:`str`, optional): The class name that the method is part of (defaults to `org.hamcrest.Matchers`).
-                Can be fully qualified, or simple, and simple defaults to `androidx.test.espresso.matcher` package
-                (e.g.: `class=CursorMatchers` fully qualified is `class=androidx.test.espresso.matcher.CursorMatchers`
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`: The found elements
-
-        Usage:
-            driver.find_elements_by_android_data_matcher(name='hasEntry', args=['title', 'Animation'])
-
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-
-        return self.find_elements(
-            by=MobileBy.ANDROID_DATA_MATCHER,
-            value=self._build_data_matcher(name=name, args=args, className=className)
-        )
-
-    def _build_data_matcher(self, name=None, args=None, className=None):
-        result = {}
-
-        for key, value in {'name': name, 'args': args, 'class': className}.items():
-            if value is not None:
-                result[key] = value
-
-        return json.dumps(result)
-
-    def find_element_by_android_uiautomator(self, uia_string):
-        """Finds element by uiautomator in Android.
-
-        Args:
-            uia_string: The element name in the Android UIAutomator library
-
-        Usage:
-            driver.find_element_by_android_uiautomator('.elements()[1].cells()[2]')
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string)
-
-    def find_elements_by_android_uiautomator(self, uia_string):
-        """Finds elements by uiautomator in Android.
-
-        Args:
-            uia_string (str): The element name in the Android UIAutomator library
-
-        Usage:
-            driver.find_elements_by_android_uiautomator('.elements()[1].cells()[2]')
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.ANDROID_UIAUTOMATOR, value=uia_string)
-
-    def find_element_by_android_viewtag(self, tag):
-        """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android.
-
-        It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
-
-        Args:
-            tag (str): The tag name of the view to look for
-
-        Usage:
-            driver.find_element_by_android_viewtag('a tag name')
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.ANDROID_VIEWTAG, value=tag)
-
-    def find_elements_by_android_viewtag(self, tag):
-        """Finds element by [View#tags](https://developer.android.com/reference/android/view/View#tags) in Android.
-
-        It works with [Espresso Driver](https://github.com/appium/appium-espresso-driver).
-
-        Args:
-            tag (str): The tag name of the view to look for
-
-        Usage:
-            driver.find_elements_by_android_viewtag('a tag name')
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.ANDROID_VIEWTAG, value=tag)
diff --git a/appium/webdriver/extensions/search_context/custom.py b/appium/webdriver/extensions/search_context/custom.py
deleted file mode 100644
index dabcc6fdb..000000000
--- a/appium/webdriver/extensions/search_context/custom.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.
-
-# pylint: disable=abstract-method
-
-from appium.webdriver.common.mobileby import MobileBy
-
-from .base_search_context import BaseSearchContext
-
-
-class CustomSearchContext(BaseSearchContext):
-    """Define search context for custom plugin"""
-
-    def find_element_by_custom(self, selector):
-        """Finds an element in conjunction with a custom element finding plugin
-
-        Args:
-            selector (str): a string of the form "module:selector", where "module" is
-                the shortcut name given in the customFindModules capability, and
-                "selector" is the string that will be passed to the custom element
-                finding plugin itself
-
-        Usage:
-            driver.find_element_by_custom("foo:bar")
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        # To enable auto completion in PyCharm(IDE)
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.CUSTOM, value=selector)
-
-    def find_elements_by_custom(self, selector):
-        """Finds elements in conjunction with a custom element finding plugin
-
-        Args:
-            selector: a string of the form "module:selector", where "module" is
-                the shortcut name given in the customFindModules capability, and
-                "selector" is the string that will be passed to the custom element
-                finding plugin itself
-
-        Usage:
-            driver.find_elements_by_custom("foo:bar")
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.CUSTOM, value=selector)
diff --git a/appium/webdriver/extensions/search_context/ios.py b/appium/webdriver/extensions/search_context/ios.py
deleted file mode 100644
index ad42ce911..000000000
--- a/appium/webdriver/extensions/search_context/ios.py
+++ /dev/null
@@ -1,120 +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.
-
-# pylint: disable=abstract-method
-
-from appium.webdriver.common.mobileby import MobileBy
-
-from .base_search_context import BaseSearchContext
-
-
-class iOSSearchContext(BaseSearchContext):
-    """Define search context for iOS"""
-
-    def find_element_by_ios_uiautomation(self, uia_string):
-        """Finds an element by uiautomation in iOS.
-
-        Args:
-            uia_string (str): The element name in the iOS UIAutomation library
-
-        Usage:
-            driver.find_element_by_ios_uiautomation('.elements()[1].cells()[2]')
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        # To enable auto completion in PyCharm(IDE)
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.IOS_UIAUTOMATION, value=uia_string)
-
-    def find_elements_by_ios_uiautomation(self, uia_string):
-        """Finds elements by uiautomation in iOS.
-
-        Args:
-            uia_string (str): The element name in the iOS UIAutomation library
-
-        Usage:
-            driver.find_elements_by_ios_uiautomation('.elements()[1].cells()[2]')
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.IOS_UIAUTOMATION, value=uia_string)
-
-    def find_element_by_ios_predicate(self, predicate_string):
-        """Find an element by ios predicate string.
-
-        Args:
-            predicate_string (str): The predicate string
-
-        Usage:
-            driver.find_element_by_ios_predicate('label == "myLabel"')
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.IOS_PREDICATE, value=predicate_string)
-
-    def find_elements_by_ios_predicate(self, predicate_string):
-        """Finds elements by ios predicate string.
-
-        Args:
-            predicate_string (str): The predicate string
-
-        Usage:
-            driver.find_elements_by_ios_predicate('label == "myLabel"')
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.IOS_PREDICATE, value=predicate_string)
-
-    def find_element_by_ios_class_chain(self, class_chain_string):
-        """Find an element by ios class chain string.
-
-        Args:
-            class_chain_string (str): The class chain string
-
-        Usage:
-            driver.find_element_by_ios_class_chain('XCUIElementTypeWindow/XCUIElementTypeButton[3]')
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.IOS_CLASS_CHAIN, value=class_chain_string)
-
-    def find_elements_by_ios_class_chain(self, class_chain_string):
-        """Finds elements by ios class chain string.
-
-        Args:
-            class_chain_string (str): The class chain string
-
-        Usage:
-            driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow[2]/XCUIElementTypeAny[-2]')
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.IOS_CLASS_CHAIN, value=class_chain_string)
diff --git a/appium/webdriver/extensions/search_context/mobile.py b/appium/webdriver/extensions/search_context/mobile.py
deleted file mode 100644
index c9d686329..000000000
--- a/appium/webdriver/extensions/search_context/mobile.py
+++ /dev/null
@@ -1,97 +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.
-
-# pylint: disable=abstract-method
-
-import base64
-
-from appium.webdriver.common.mobileby import MobileBy
-
-from .base_search_context import BaseSearchContext
-
-
-class MobileSearchContext(BaseSearchContext):
-    """Define search context for Mobile(Android, iOS)"""
-
-    def find_element_by_accessibility_id(self, accessibility_id):
-        """Finds an element by accessibility id.
-
-        Args:
-            accessibility_id (str): A string corresponding to a recursive element search using the
-                Id/Name that the native Accessibility options utilize
-
-        Usage:
-            driver.find_element_by_accessibility_id()
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        # To enable auto completion in PyCharm(IDE)
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id)
-
-    def find_elements_by_accessibility_id(self, accessibility_id):
-        """Finds elements by accessibility id.
-
-        Args:
-            accessibility_id (str): a string corresponding to a recursive element search using the
-                Id/Name that the native Accessibility options utilize
-
-        Usage:
-            driver.find_elements_by_accessibility_id()
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.ACCESSIBILITY_ID, value=accessibility_id)
-
-    def find_element_by_image(self, img_path):
-        """Finds a portion of a screenshot by an image.
-
-        Uses driver.find_image_occurrence under the hood.
-
-        Args:
-            img_path (str): a string corresponding to the path of a image
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        with open(img_path, 'rb') as i_file:
-            b64_data = base64.b64encode(i_file.read()).decode('UTF-8')
-
-        return self.find_element(by=MobileBy.IMAGE, value=b64_data)
-
-    def find_elements_by_image(self, img_path):
-        """Finds a portion of a screenshot by an image.
-
-        Uses driver.find_image_occurrence under the hood. Note that this will
-        only ever return at most one element
-
-        Args:
-            img_path (str): a string corresponding to the path of a image
-
-        Return:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        with open(img_path, 'rb') as i_file:
-            b64_data = base64.b64encode(i_file.read()).decode('UTF-8')
-
-        return self.find_elements(by=MobileBy.IMAGE, value=b64_data)
diff --git a/appium/webdriver/extensions/search_context/windows.py b/appium/webdriver/extensions/search_context/windows.py
deleted file mode 100644
index 66ca23a29..000000000
--- a/appium/webdriver/extensions/search_context/windows.py
+++ /dev/null
@@ -1,56 +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.
-
-# pylint: disable=abstract-method
-
-from appium.webdriver.common.mobileby import MobileBy
-
-from .base_search_context import BaseSearchContext
-
-
-class WindowsSearchContext(BaseSearchContext):
-    """Define search context for Windows"""
-
-    def find_element_by_windows_uiautomation(self, win_uiautomation):
-        """Finds an element by windows uiautomation
-
-        Args:
-            win_uiautomation (str): The element name in the windows UIAutomation selector
-
-        Usage:
-            driver.find_element_by_windows_uiautomation()
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        # To enable auto completion in PyCharm(IDE)
-        :rtype: `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_element(by=MobileBy.WINDOWS_UI_AUTOMATION, value=win_uiautomation)
-
-    def find_elements_by_windows_uiautomation(self, win_uiautomation):
-        """Finds elements by windows uiautomation
-
-        Args:
-            win_uiautomation (str): The element name in the windows UIAutomation selector
-
-        Usage:
-            driver.find_elements_by_windows_uiautomation()
-
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-
-        :rtype: list of `appium.webdriver.webelement.WebElement`
-        """
-        return self.find_elements(by=MobileBy.WINDOWS_UI_AUTOMATION, value=win_uiautomation)
diff --git a/appium/webdriver/extensions/session.py b/appium/webdriver/extensions/session.py
index 64d493705..e0bcd8b60 100644
--- a/appium/webdriver/extensions/session.py
+++ b/appium/webdriver/extensions/session.py
@@ -12,41 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+from typing import Dict
 
 from appium.common.logger import logger
+from appium.protocols.webdriver.can_execute_commands import CanExecuteCommands
 
 from ..mobilecommand import MobileCommand as Command
 
 
-class Session(webdriver.Remote):
+class Session(CanExecuteCommands):
     @property
-    def session(self):
-        """ Retrieves session information from the current session
-
-        Usage:
-            session = driver.session
-
-        Returns:
-            `dict`: containing information from the current session
-        """
-        return self.execute(Command.GET_SESSION)['value']
-
-    @property
-    def all_sessions(self):
-        """ Retrieves all sessions that are open
-
-        Usage:
-            sessions = driver.all_sessions
-
-        Returns:
-            `dict`: containing all open sessions
-        """
-        return self.execute(Command.GET_ALL_SESSIONS)['value']
-
-    @property
-    def events(self):
-        """ Retrieves events information from the current session
+    def events(self) -> Dict:
+        """Retrieves events information from the current session
 
         Usage:
             events = driver.events
@@ -55,16 +32,10 @@ def events(self):
             `dict`:  containing events timing information from the current session
         """
         try:
-            session = self.session
-            return session['events']
+            return self.execute(Command.GET_SESSION)['value']['events']
         except Exception as e:
-            logger.warning('Could not find events information in the session. Error:', e)
+            logger.warning('Could not find events information in the session. Error: %s', e)
             return {}
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_SESSION] = \
-            ('GET', '/session/$sessionId')
-        self.command_executor._commands[Command.GET_ALL_SESSIONS] = \
-            ('GET', '/sessions')
+    def _add_commands(self) -> None:
+        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 2a0f42351..d9a55111e 100644
--- a/appium/webdriver/extensions/settings.py
+++ b/appium/webdriver/extensions/settings.py
@@ -12,40 +12,38 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium import webdriver
+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
 
 
-class Settings(webdriver.Remote):
-    def get_settings(self):
+class Settings(CanExecuteCommands):
+    def get_settings(self) -> Dict[str, Any]:
         """Returns the appium server Settings for the current session.
 
         Do not get Settings confused with Desired Capabilities, they are
         separate concepts. See https://github.com/appium/appium/blob/master/docs/en/advanced-concepts/settings.md
 
         Returns:
-            dict: Current settings
+            Current settings
         """
         return self.execute(Command.GET_SETTINGS, {})['value']
 
-    def update_settings(self, settings):
+    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
 
         Args:
-            settings (dict): dictionary of settings to apply to the current test session
+            settings: dictionary of settings to apply to the current test session
         """
-        data = {"settings": settings}
-
-        self.execute(Command.UPDATE_SETTINGS, data)
+        self.execute(Command.UPDATE_SETTINGS, {'settings': settings})
         return self
 
-    # pylint: disable=protected-access
-
-    def _addCommands(self):
-        self.command_executor._commands[Command.GET_SETTINGS] = \
-            ('GET', '/session/$sessionId/appium/settings')
-        self.command_executor._commands[Command.UPDATE_SETTINGS] = \
-            ('POST', '/session/$sessionId/appium/settings')
+    def _add_commands(self) -> None:
+        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/mobilecommand.py b/appium/webdriver/mobilecommand.py
index f6d90679e..79ac25e1b 100644
--- a/appium/webdriver/mobilecommand.py
+++ b/appium/webdriver/mobilecommand.py
@@ -13,20 +13,16 @@
 # limitations under the License.
 
 
-class MobileCommand(object):
+class MobileCommand:
     # Common
     GET_SESSION = 'getSession'
-    GET_ALL_SESSIONS = 'getAllSessions'
 
+    GET_STATUS = 'getStatus'
+
+    ## MJSONWP for Selenium v4
     GET_LOCATION = 'getLocation'
     SET_LOCATION = 'setLocation'
 
-    GET_AVAILABLE_IME_ENGINES = 'getAvailableIMEEngines'
-    IS_IME_ACTIVE = 'isIMEActive'
-    ACTIVATE_IME_ENGINE = 'activateIMEEngine'
-    DEACTIVATE_IME_ENGINE = 'deactivateIMEEngine'
-    GET_ACTIVE_IME_ENGINE = 'getActiveEngine'
-
     CLEAR = 'clear'
     LOCATION_IN_VIEW = 'locationInView'
 
@@ -34,15 +30,6 @@ class MobileCommand(object):
     GET_CURRENT_CONTEXT = 'getCurrentContext'
     SWITCH_TO_CONTEXT = 'switchToContext'
 
-    TOUCH_ACTION = 'touchAction'
-    MULTI_ACTION = 'multiAction'
-
-    SET_IMMEDIATE_VALUE = 'setImmediateValue'
-    REPLACE_KEYS = 'replaceKeys'
-
-    LAUNCH_APP = 'launchApp'
-    CLOSE_APP = 'closeApp'
-    RESET = 'reset'
     BACKGROUND = 'background'
     GET_APP_STRINGS = 'getAppStrings'
 
@@ -80,16 +67,24 @@ class MobileCommand(object):
     GET_EVENTS = 'getLogEvents'
     LOG_EVENT = 'logCustomEvent'
 
+    ## MJSONWP for Selenium v4
+    IS_ELEMENT_DISPLAYED = 'isElementDisplayed'
+    GET_CAPABILITIES = 'getCapabilities'
+    GET_SCREEN_ORIENTATION = 'getScreenOrientation'
+    SET_SCREEN_ORIENTATION = 'setScreenOrientation'
+
+    # To override selenium commands
+    GET_LOG = 'getLog'
+    GET_AVAILABLE_LOG_TYPES = 'getAvailableLogTypes'
+
     # Android
     OPEN_NOTIFICATIONS = 'openNotifications'
-    START_ACTIVITY = 'startActivity'
     GET_CURRENT_ACTIVITY = 'getCurrentActivity'
     GET_CURRENT_PACKAGE = 'getCurrentPackage'
     GET_SYSTEM_BARS = 'getSystemBars'
     GET_DISPLAY_DENSITY = 'getDisplayDensity'
     TOGGLE_WIFI = 'toggleWiFi'
     TOGGLE_LOCATION_SERVICES = 'toggleLocationServices'
-    END_TEST_COVERAGE = 'endTestCoverage'
     GET_PERFORMANCE_DATA_TYPES = 'getPerformanceDataTypes'
     GET_PERFORMANCE_DATA = 'getPerformanceData'
     GET_NETWORK_CONNECTION = 'getNetworkConnection'
diff --git a/appium/webdriver/switch_to.py b/appium/webdriver/switch_to.py
index d496b7a30..ed9db7c76 100644
--- a/appium/webdriver/switch_to.py
+++ b/appium/webdriver/switch_to.py
@@ -12,19 +12,24 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from typing import Optional
+
 from selenium.webdriver.remote.switch_to import SwitchTo
+from typing_extensions import Self
 
 from .mobilecommand import MobileCommand
 
 
 class MobileSwitchTo(SwitchTo):
-    def context(self, context_name):
+    def context(self, context_name: Optional[str]) -> Self:
         """Sets the context for the current session.
+        Passing `None` is equal to switching to native context.
 
         Args:
-            context_name (str): The name of the context to switch to.
+            context_name: The name of the context to switch to.
 
         Usage:
             driver.switch_to.context('WEBVIEW_1')
         """
         self._driver.execute(MobileCommand.SWITCH_TO_CONTEXT, {'name': context_name})
+        return self
diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py
index 03e7e3fdf..06c61f44d 100644
--- a/appium/webdriver/webdriver.py
+++ b/appium/webdriver/webdriver.py
@@ -12,19 +12,24 @@
 # 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
-
-import copy
-
-from selenium.common.exceptions import InvalidArgumentException
-from selenium.webdriver.common.by import By
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union
+
+from selenium import webdriver
+from selenium.common.exceptions import (
+    InvalidArgumentException,
+    SessionNotCreatedException,
+    UnknownMethodException,
+    WebDriverException,
+)
 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.webdriver.common.mobileby import MobileBy
+from appium.options.common.base import AppiumOptions
 
 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
@@ -44,75 +49,166 @@
 from .extensions.execute_mobile_command import ExecuteMobileCommand
 from .extensions.hw_actions import HardwareActions
 from .extensions.images_comparison import ImagesComparison
-from .extensions.ime import IME
 from .extensions.keyboard import Keyboard
 from .extensions.location import Location
 from .extensions.log_event import LogEvent
+from .extensions.logs import Logs
 from .extensions.remote_fs import RemoteFS
 from .extensions.screen_record import ScreenRecord
-from .extensions.search_context import AppiumSearchContext
 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
 
-# From remote/webdriver.py
-_W3C_CAPABILITY_NAMES = frozenset([
-    'acceptInsecureCerts',
-    'browserName',
-    'browserVersion',
-    'platformName',
-    'pageLoadStrategy',
-    'proxy',
-    'setWindowRect',
-    'timeouts',
-    'unhandledPromptBehavior',
-])
-
-# From remote/webdriver.py
-_OSS_W3C_CONVERSION = {
-    'acceptSslCerts': 'acceptInsecureCerts',
-    'version': 'browserVersion',
-    'platform': 'platformName'
-}
-
-_EXTENSION_CAPABILITY = ':'
-_FORCE_MJSONWP = 'forceMjsonwp'
-
-# override
-# Add appium prefix for the non-W3C capabilities
-
-
-def _make_w3c_caps(caps):
-    appium_prefix = 'appium:'
-
-    caps = copy.deepcopy(caps)
-    profile = caps.get('firefox_profile')
-    first_match = {}
-    if caps.get('proxy') and caps['proxy'].get('proxyType'):
-        caps['proxy']['proxyType'] = caps['proxy']['proxyType'].lower()
-    for k, v in caps.items():
-        if v and k in _OSS_W3C_CONVERSION:
-            first_match[_OSS_W3C_CONVERSION[k]] = v.lower() if k == 'platform' else v
-        if k in _W3C_CAPABILITY_NAMES or _EXTENSION_CAPABILITY in k:
-            first_match[k] = v
-        else:
-            if not k.startswith(appium_prefix):
-                first_match[appium_prefix + k] = v
-    if profile:
-        moz_opts = first_match.get('moz:firefoxOptions', {})
-        # If it's already present, assume the caller did that intentionally.
-        if 'profile' not in moz_opts:
-            # Don't mutate the original capabilities.
-            new_opts = copy.deepcopy(moz_opts)
-            new_opts['profile'] = profile
-            first_match['moz:firefoxOptions'] = new_opts
-    return {'firstMatch': [first_match]}
+
+class ExtensionBase:
+    """
+    Used to define an extension command as driver's methods.
+
+    Example:
+        When you want to add `example_command` which calls a get request to
+        `session/$sessionId/path/to/your/custom/url`.
+
+        #. Defines an extension as a subclass of `ExtensionBase`
+            .. code-block:: python
+
+                class YourCustomCommand(ExtensionBase):
+                    def method_name(self):
+                        return 'custom_method_name'
+
+                    # Define a method with the name of `method_name`
+                    def custom_method_name(self):
+                        # Generally the response of Appium follows `{ 'value': { data } }`
+                        # format.
+                        return self.execute()['value']
+
+                    # Used to register the command pair as "Appium command" in this driver.
+                    def add_command(self):
+                        return ('GET', 'session/$sessionId/path/to/your/custom/url')
+
+        #. Creates a session with the extension.
+            .. code-block:: python
+
+                # Appium capabilities
+                options = AppiumOptions()
+                driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options,
+                    extensions=[YourCustomCommand])
+
+        #. Calls the custom command
+            .. code-block:: python
+
+                # Then, the driver calls a get request against
+                # `session/$sessionId/path/to/your/custom/url`. `$sessionId` will be
+                # replaced properly in the driver. Then, the method returns
+                # the `value` part of the response.
+                driver.custom_method_name()
+
+        #. Remove added commands (if needed)
+            .. code-block:: python
+
+                # New commands are added by `setattr`. They remain in the module,
+                # so you should explicitly delete them to define the same name method
+                # with different arguments or process in the method.
+                driver.delete_extensions()
+
+
+        You can give arbitrary arguments for the command like the below.
+
+        .. code-block:: python
+
+            class YourCustomCommand(ExtensionBase):
+                def method_name(self):
+                    return 'custom_method_name'
+
+                def test_command(self, argument):
+                    return self.execute(argument)['value']
+
+                def add_command(self):
+                    return ('post', 'session/$sessionId/path/to/your/custom/url')
+
+            driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options,
+                extensions=[YourCustomCommand])
+
+            # Then, the driver sends a post request to `session/$sessionId/path/to/your/custom/url`
+            # with `{'dummy_arg': 'as a value'}` JSON body.
+            driver.custom_method_name({'dummy_arg': 'as a value'})
+
+
+        When you customize the URL dynamically with element id.
+
+        .. code-block:: python
+
+            class CustomURLCommand(ExtensionBase):
+                def method_name(self):
+                    return 'custom_method_name'
+
+                def custom_method_name(self, element_id):
+                    return self.execute({'id': element_id})['value']
+
+                def add_command(self):
+                    return ('GET', 'session/$sessionId/path/to/your/custom/$id/url')
+
+            driver = webdriver.Remote('http://localhost:4723/wd/hub', options=options,
+                extensions=[YourCustomCommand])
+            element = driver.find_element(by=AppiumBy.ACCESSIBILITY_ID, value='id')
+
+            # Then, the driver calls a get request to `session/$sessionId/path/to/your/custom/$id/url`
+            # with replacing the `$id` with the given `element.id`
+            driver.custom_method_name(element.id)
+
+    """
+
+    def __init__(self, execute: Callable[[str, Dict], Dict[str, Any]]):
+        self._execute = execute
+
+    def execute(self, parameters: Union[Dict[str, Any], None] = None) -> Any:
+        param = {}
+        if parameters:
+            param = parameters
+        return self._execute(self.method_name(), param)
+
+    def method_name(self) -> str:
+        """
+        Expected to return a method name.
+        This name will be available as a driver method.
+
+        Returns:
+            'str' The method name.
+        """
+        raise NotImplementedError()
+
+    def add_command(self) -> Tuple[str, str]:
+        """
+        Expected to define the pair of HTTP method and its URL.
+        """
+        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(
-    AppiumSearchContext,
+    webdriver.Remote,
     ActionHelpers,
     Activities,
     Applications,
@@ -126,10 +222,10 @@ class WebDriver(
     Gsm,
     HardwareActions,
     ImagesComparison,
-    IME,
     Keyboard,
     Location,
     LogEvent,
+    Logs,
     Network,
     Performance,
     Power,
@@ -138,238 +234,262 @@ class WebDriver(
     Session,
     Settings,
     Sms,
-    SystemBars
+    SystemBars,
 ):
-
-    def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
-                 desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=True, direct_connection=False):
-
-        super(WebDriver, self).__init__(
-            AppiumConnection(command_executor, keep_alive=keep_alive),
-            desired_capabilities,
-            browser_profile,
-            proxy
+    def __init__(
+        self,
+        command_executor: Union[str, AppiumConnection] = 'http://127.0.0.1:4723',
+        extensions: Optional[List[Type['ExtensionBase']]] = None,
+        options: Union[AppiumOptions, List[AppiumOptions], None] = None,
+        client_config: Optional[AppiumClientConfig] = None,
+    ):
+        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,  # type: ignore[arg-type]
+            locator_converter=AppiumLocatorConverter(),
+            web_element_cls=MobileWebElement,
+            client_config=client_config,
         )
 
-        if hasattr(self, 'command_executor'):
-            self._addCommands()
+        # to explicitly set type after the initialization
+        self.command_executor: RemoteConnection
+
+        self._add_commands()
 
         self.error_handler = MobileErrorHandler()
-        self._switch_to = MobileSwitchTo(self)
-
-        if direct_connection:
-            self._update_command_executor(keep_alive=keep_alive)
-
-        # add new method to the `find_by_*` pantheon
-        By.IOS_UIAUTOMATION = MobileBy.IOS_UIAUTOMATION
-        By.IOS_PREDICATE = MobileBy.IOS_PREDICATE
-        By.IOS_CLASS_CHAIN = MobileBy.IOS_CLASS_CHAIN
-        By.ANDROID_UIAUTOMATOR = MobileBy.ANDROID_UIAUTOMATOR
-        By.ANDROID_VIEWTAG = MobileBy.ANDROID_VIEWTAG
-        By.WINDOWS_UI_AUTOMATION = MobileBy.WINDOWS_UI_AUTOMATION
-        By.ACCESSIBILITY_ID = MobileBy.ACCESSIBILITY_ID
-        By.IMAGE = MobileBy.IMAGE
-        By.CUSTOM = MobileBy.CUSTOM
-
-    def _update_command_executor(self, keep_alive):
+
+        if client_config and client_config.direct_connection:
+            self._update_command_executor(keep_alive=client_config.keep_alive)
+
+        self._absent_extensions: Set[str] = set()
+
+        self._extensions = extensions or []
+        for extension in self._extensions:
+            instance = extension(self.execute)
+            method_name = instance.method_name()
+            if hasattr(WebDriver, 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))
+            method, url_cmd = instance.add_command()
+            self.command_executor.add_command(method_name, method.upper(), url_cmd)
+
+    if TYPE_CHECKING:
+
+        def find_element(self, by: str, value: Union[str, Dict, None] = None) -> 'MobileWebElement':  # type: ignore[override]
+            ...
+
+        def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List['MobileWebElement']:  # type: ignore[override]
+            ...
+
+    def delete_extensions(self) -> None:
+        """Delete extensions added in the class with 'setattr'"""
+        for extension in self._extensions:
+            instance = extension(self.execute)
+            method_name = instance.method_name()
+            if hasattr(WebDriver, method_name):
+                delattr(WebDriver, method_name)
+
+    def _update_command_executor(self, keep_alive: bool) -> None:
         """Update command executor following directConnect feature"""
         direct_protocol = 'directConnectProtocol'
         direct_host = 'directConnectHost'
         direct_port = 'directConnectPort'
         direct_path = 'directConnectPath'
 
-        if (not {direct_protocol, direct_host, direct_port, direct_path}.issubset(set(self.capabilities))):
+        assert self.caps, 'Driver capabilities must be defined'
+        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 += '{}: \'{}\'\n'.format(key, self.capabilities.get(key, ''))
-            logger.warning(message)
+                message += f"{key}: '{self.caps.get(key, '')}' "
+            logger.debug(message)
             return
 
-        protocol = self.capabilities[direct_protocol]
-        hostname = self.capabilities[direct_host]
-        port = self.capabilities[direct_port]
-        path = self.capabilities[direct_path]
-        executor = '{scheme}://{hostname}:{port}{path}'.format(
-            scheme=protocol,
-            hostname=hostname,
-            port=port,
-            path=path
-        )
+        protocol = self.caps[direct_protocol]
+        hostname = self.caps[direct_host]
+        port = self.caps[direct_port]
+        path = self.caps[direct_path]
+        executor = f'{protocol}://{hostname}:{port}{path}'
 
-        logger.info('Updated request endpoint to %s', executor)
-        # Override command executor
-        self.command_executor = RemoteConnection(executor, keep_alive=keep_alive)
-        self._addCommands()
+        logger.debug('Updated request endpoint to %s', executor)
+        # Override command executor.
+        if isinstance(self.command_executor, AppiumConnection):  # type: ignore
+            self.command_executor = AppiumConnection(executor, keep_alive=keep_alive)
+        else:
+            self.command_executor = RemoteConnection(executor, keep_alive=keep_alive)
+        self._add_commands()
 
-    def start_session(self, capabilities, browser_profile=None):
+    # https://github.com/SeleniumHQ/selenium/blob/06fdf2966df6bca47c0ae45e8201cd30db9b9a49/py/selenium/webdriver/remote/webdriver.py#L277
+    # noinspection PyAttributeOutsideInit
+    def start_session(self, capabilities: Union[Dict, AppiumOptions], browser_profile: Optional[str] = None) -> None:
         """Creates a new session with the desired capabilities.
 
         Override for Appium
 
         Args:
-            automation_name: The name of automation engine to use.
-            platform_name: The name of target platform.
-            platform_version: The kind of mobile device or emulator to use
-            app: The absolute local path or remote http URL to an .ipa or .apk file, or a .zip containing one of these.
-
-        Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md for more details.
+            capabilities: Read https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md
+             for more details.
+            browser_profile: Browser profile
         """
-        if not isinstance(capabilities, dict):
-            raise InvalidArgumentException('Capabilities must be a dictionary')
-        if browser_profile:
-            if 'moz:firefoxOptions' in capabilities:
-                capabilities['moz:firefoxOptions']['profile'] = browser_profile.encoded
-            else:
-                capabilities.update({'firefox_profile': browser_profile.encoded})
-
-        parameters = self._merge_capabilities(capabilities)
-
-        response = self.execute(RemoteCommand.NEW_SESSION, parameters)
-        if 'sessionId' not in response:
-            response = response['value']
-        self.session_id = response['sessionId']
-        self.capabilities = response.get('value')
-
-        # if capabilities is none we are probably speaking to
-        # a W3C endpoint
-        if self.capabilities is None:
-            self.capabilities = response.get('capabilities')
-
-        # Double check to see if we have a W3C Compliant browser
-        self.w3c = response.get('status') is None
-        self.command_executor.w3c = self.w3c
-
-    def _merge_capabilities(self, capabilities):
-        """Manage capabilities whether W3C format or MJSONWP format
+        if not isinstance(capabilities, (dict, AppiumOptions)):
+            raise InvalidArgumentException('Capabilities must be a dictionary or AppiumOptions instance')
+
+        w3c_caps = AppiumOptions.as_w3c(capabilities) if isinstance(capabilities, dict) else capabilities.to_w3c()
+        response = self.execute(RemoteCommand.NEW_SESSION, w3c_caps)
+        # https://w3c.github.io/webdriver/#new-session
+        if not isinstance(response, dict):
+            raise SessionNotCreatedException(
+                f'A valid W3C session creation response must be a dictionary. Got "{response}" instead'
+            )
+        # Due to a W3C spec parsing misconception some servers
+        # pack the createSession response stuff into 'value' dictionary and
+        # some other put it to the top level of the response JSON nesting hierarchy
+        get_response_value: Callable[[str], Optional[Any]] = lambda key: response.get(key) or (
+            response['value'].get(key) if isinstance(response.get('value'), dict) else None
+        )
+        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. Got "{response}" instead'
+            )
+        self.session_id = session_id
+        self.caps = get_response_value('capabilities') or {}
+
+    def get_status(self) -> Dict:
         """
-        if _FORCE_MJSONWP in capabilities:
-            force_mjsonwp = capabilities[_FORCE_MJSONWP]
-            del capabilities[_FORCE_MJSONWP]
+        Get the Appium server status
+
+        Usage:
+            driver.get_status()
 
-            if force_mjsonwp != False:
-                return {'desiredCapabilities': capabilities}
+        Returns:
+            dict: The status information
 
-        w3c_caps = _make_w3c_caps(capabilities)
-        return {'capabilities': w3c_caps, 'desiredCapabilities': capabilities}
+        """
+        return self.execute(Command.GET_STATUS)['value']
 
-    def find_element(self, by=By.ID, value=None):
-        """'Private' method used by the find_element_by_* methods.
+    def create_web_element(self, element_id: Union[int, str]) -> MobileWebElement:
+        """Creates a web element with the specified element_id.
 
-        Override for Appium
+        Overrides method in Selenium WebDriver in order to always give them
+        Appium WebElement
 
-        Usage:
-            Use the corresponding find_element_by_* instead of this.
+        Args:
+            element_id: The element id to create a web element
 
         Returns:
-            `appium.webdriver.webelement.WebElement`
-
-        :rtype: `MobileWebElement`
+            `MobileWebElement`
         """
-        # TODO: If we need, we should enable below converter for Web context
-        # if self.w3c:
-        #     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=By.ID, value=None):
-        """'Private' method used by the find_elements_by_* methods.
+        return MobileWebElement(self, element_id)
 
-        Override for Appium
+    @property
+    def switch_to(self) -> MobileSwitchTo:
+        """Returns an object containing all options to switch focus into
 
-        Usage:
-            Use the corresponding find_elements_by_* instead of this.
+        Override for appium
 
         Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
+            `appium.webdriver.switch_to.MobileSwitchTo`
 
-        :rtype: list of `MobileWebElement`
         """
-        # TODO: If we need, we should enable below converter for Web context
-        # if self.w3c:
-        #     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, w3c=False):
-        """Creates a web element with the specified element_id.
 
-        Overrides method in Selenium WebDriver in order to always give them
-        Appium WebElement
+        return MobileSwitchTo(self)
+
+    # MJSONWP for Selenium v4
+    @property
+    def orientation(self) -> str:
+        """
+        Gets the current orientation of the device
+
+        Example:
+
+            .. code-block:: python
+
+                orientation = driver.orientation
+        """
+        return self.execute(Command.GET_SCREEN_ORIENTATION)['value']
+
+    # MJSONWP for Selenium v4
+    @orientation.setter
+    def orientation(self, value: str) -> None:
+        """
+        Sets the current orientation of the device
 
         Args:
-            element_id (int): The element id to create a web element
-            w3c (bool): Whether the element is W3C or MJSONWP
+         - value: orientation to set it to.
 
-        Returns:
-            `MobileWebElement`
+        Example:
+            .. code-block:: python
+
+                driver.orientation = 'landscape'
         """
-        return MobileWebElement(self, element_id, w3c)
+        allowed_values = ['LANDSCAPE', 'PORTRAIT']
+        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'")
 
-    def set_value(self, element, value):
-        """Set the value on an element in the application.
+    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.
+        This API is designed for private usage.
 
         Args:
-            element (`appium.webdriver.webelement.WebElement`: the element whose value will be set
-            value (str): the value to set on the element
+            ext_name: extension name
 
         Returns:
-            `appium.webdriver.webdriver.WebDriver`
+            self instance for chaining
+
+        Raises:
+            UnknownMethodException: If the extension has been marked as absent once
         """
-        data = {
-            'id': element.id,
-            'value': [value],
-        }
-        self.execute(Command.SET_IMMEDIATE_VALUE, data)
+        if ext_name in self._absent_extensions:
+            raise UnknownMethodException()
         return self
 
-    # pylint: disable=protected-access
+    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.
 
-    def _addCommands(self):
+        Args:
+            ext_name: extension name
+
+        Returns:
+            self instance for chaining
+        """
+        logger.debug(f'Marking driver extension "{ext_name}" as absent for the current instance')
+        self._absent_extensions.add(ext_name)
+        return self
+
+    def _add_commands(self) -> None:
         # call the overridden command binders from all mixin classes except for
         # appium.webdriver.webdriver.WebDriver and its sub-classes
         # https://github.com/appium/python-client/issues/342
         for mixin_class in filter(lambda x: not issubclass(x, WebDriver), self.__class__.__mro__):
-            if hasattr(mixin_class, self._addCommands.__name__):
-                getattr(mixin_class, self._addCommands.__name__, None)(self)
+            if hasattr(mixin_class, self._add_commands.__name__):
+                get_atter = getattr(mixin_class, self._add_commands.__name__, None)
+                if get_atter:
+                    get_atter(self)
 
-        self.command_executor._commands[Command.TOUCH_ACTION] = \
-            ('POST', '/session/$sessionId/touch/perform')
-        self.command_executor._commands[Command.MULTI_ACTION] = \
-            ('POST', '/session/$sessionId/touch/multi/perform')
-        self.command_executor._commands[Command.SET_IMMEDIATE_VALUE] = \
-            ('POST', '/session/$sessionId/appium/element/$id/value')
+        self.command_executor.add_command(Command.GET_STATUS, 'GET', '/status')
 
         # TODO Move commands for element to webelement
-        self.command_executor._commands[Command.REPLACE_KEYS] = \
-            ('POST', '/session/$sessionId/appium/element/$id/replace_value')
-        self.command_executor._commands[Command.CLEAR] = \
-            ('POST', '/session/$sessionId/element/$id/clear')
-        self.command_executor._commands[Command.LOCATION_IN_VIEW] = \
-            ('GET', '/session/$sessionId/element/$id/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
+        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')
+
+        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')
diff --git a/appium/webdriver/webelement.py b/appium/webdriver/webelement.py
index a90080c49..61076ca37 100644
--- a/appium/webdriver/webelement.py
+++ b/appium/webdriver/webelement.py
@@ -12,22 +12,29 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from selenium.webdriver.common.by import By
+from typing import TYPE_CHECKING, Callable, Dict, List, 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 .extensions.search_context import AppiumWebElementSearchContext
 from .mobilecommand import MobileCommand as Command
 
-# Python 3 imports
-try:
-    str = basestring
-except NameError:
-    pass
 
+class WebElement(SeleniumWebElement):
+    _execute: Callable
+    _id: str
+
+    if TYPE_CHECKING:
+
+        def find_element(self, by: str, value: Union[str, Dict, None] = None) -> Self:  # type: ignore[override]
+            ...
 
-class WebElement(AppiumWebElementSearchContext):
-    def get_attribute(self, name):
+        def find_elements(self, by: str, value: Union[str, Dict, None] = None) -> List[Self]:  # type: ignore[override]
+            ...
+
+    def get_attribute(self, name: str) -> Optional[Union[str, Dict]]:  # type: ignore[override]
         """Gets the given attribute or property of the element.
 
         Override for Appium
@@ -43,112 +50,43 @@ def get_attribute(self, name):
         is returned.
 
         Args:
-            name (str): Name of the attribute/property to retrieve.
+            name: Name of the attribute/property to retrieve.
 
         Usage:
             # Check if the "active" CSS class is applied to an element.
+
             is_active = "active" in target_element.get_attribute("class")
 
         Returns:
-            str: The given attribute or property of the element
+            The given attribute or property of the element
         """
 
         resp = self._execute(RemoteCommand.GET_ELEMENT_ATTRIBUTE, {'name': name})
-        attributeValue = resp.get('value')
+        attribute_value = resp.get('value')
 
-        if attributeValue is None:
+        if attribute_value is None:
             return None
 
-        if isinstance(attributeValue, dict):
-            return attributeValue
+        if isinstance(attribute_value, dict):
+            return attribute_value
 
-        if not isinstance(attributeValue, str):
-            try:
-                attributeValue = unicode(attributeValue)
-            except NameError:
-                attributeValue = str(attributeValue)
+        # Convert to str along to the spec
+        if not isinstance(attribute_value, str):
+            attribute_value = str(attribute_value)
 
-        if name != 'value' and attributeValue.lower() in ('true', 'false'):
-            return attributeValue.lower()
+        if name != 'value' and attribute_value.lower() in ('true', 'false'):
+            return attribute_value.lower()
 
-        return attributeValue
+        return attribute_value
 
-    def is_displayed(self):
+    def is_displayed(self) -> bool:
         """Whether the element is visible to a user.
 
         Override for Appium
         """
-        return self._execute(RemoteCommand.IS_ELEMENT_DISPLAYED)['value']
-
-    def find_element(self, by=By.ID, value=None):
-        """Find an element given a By strategy and locator
-
-        Override for Appium
-
-        Prefer the find_element_by_* methods when possible.
-
-        Args:
-            by (:obj:`str`, optional): The strategy
-            value (:obj:`str`, optional): The locator
-
-        Usage:
-            element = element.find_element(By.ID, 'foo')
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-        """
-        # TODO: If we need, we should enable below converter for Web context
-        # if self._w3c:
-        #     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=By.ID, value=None):
-        """Find elements given a By strategy and locator
-
-        Override for Appium
-
-        Prefer the find_elements_by_* methods when possible.
-
-        Args:
-            by (:obj:`str`, optional): The strategy
-            value (:obj:`str`, optional): The locator
-
-        Usage:
-            element = element.find_elements(By.CLASS_NAME, 'foo')
+        return self._execute(Command.IS_ELEMENT_DISPLAYED)['value']
 
-        Returns:
-            :obj:`list` of :obj:`appium.webdriver.webelement.WebElement`
-        """
-        # TODO: If we need, we should enable below converter for Web context
-        # if self._w3c:
-        #     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):
+    def clear(self) -> Self:  # type: ignore[override]
         """Clears text.
 
         Override for Appium
@@ -156,73 +94,37 @@ def clear(self):
         Returns:
             `appium.webdriver.webelement.WebElement`
         """
+
+        # NOTE: this method is overridden because the selenium client returned None instead of self.
+        # Appium python client would like to allow users to chain methods.
         data = {'id': self.id}
         self._execute(Command.CLEAR, data)
         return self
 
-    def set_text(self, keys=''):
-        """Sends text to the element.
-
-        Previous text is removed.
-        Android only.
-
-        Args:
-            keys (str): the text to be sent to the element.
-
-        Usage:
-            element.set_text('some text')
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-        """
-        data = {
-            'id': self._id,
-            'value': [keys]
-        }
-        self._execute(Command.REPLACE_KEYS, data)
-        return self
-
     @property
-    def location_in_view(self):
+    def location_in_view(self) -> Dict[str, int]:
         """Gets the location of an element relative to the view.
 
         Usage:
-            location = element.location_in_view
-            x = location['x']
-            y = location['y']
+            | location = element.location_in_view
+            | x = location['x']
+            | y = location['y']
 
         Returns:
             dict: The location of an element relative to the view
         """
         return self._execute(Command.LOCATION_IN_VIEW)['value']
 
-    def set_value(self, value):
-        """Set the value on this element in the application
-
-        Args:
-            value (str): The value to be set
-
-        Returns:
-            `appium.webdriver.webelement.WebElement`
-        """
-        data = {
-            'id': self.id,
-            'value': [value],
-        }
-        self._execute(Command.SET_IMMEDIATE_VALUE, data)
-        return self
-
     # Override
-    def send_keys(self, *value):
+    def send_keys(self, *value: str) -> Self:  # type: ignore[override]
         """Simulates typing into the element.
 
         Args:
-            value (str): A string for typing.
+            value: A string for typing.
 
         Returns:
             `appium.webdriver.webelement.WebElement`
         """
         keys = keys_to_typing(value)
-        self._execute(RemoteCommand.SEND_KEYS_TO_ELEMENT,
-                      {'text': ''.join(keys), 'value': keys})
+        self._execute(RemoteCommand.SEND_KEYS_TO_ELEMENT, {'text': ''.join(keys), 'value': keys})
         return self
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 4eb2fbbb2..000000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,15 +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
-
-# Runs tests nightly to make sure they works against appium@beta
-schedules:
-  - cron: "0 0 * * *"
-    displayName: Daily Nightly build (UTC)
-    branches:
-      include:
-        - master
-    always: true
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 1786e8dc1..000000000
--- a/ci-jobs/functional/run_android_test.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-jobs:
-  - job: ${{ parameters.name }}
-    pool:
-      vmImage: ${{ parameters.vmImage }}
-    variables:
-      ANDROID_SDK_VERSION: ${{ parameters.sdkVer }}
-      CI: ${{ parameters.ci }}
-    steps:
-      - template: ./run_appium.yml
-        parameters:
-          OPENCV: ${{ parameters.opencv }}
-      - script: bash ci-jobs/functional/start-emulator.sh
-        displayName: Create and run Emulator
-      - script: |
-          cd test/functional/android
-          python -m pytest ${{ parameters.testFiles}} ${{ parameters.pytestOpt }}
-        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_appium.yml b/ci-jobs/functional/run_appium.yml
deleted file mode 100644
index dbc773b13..000000000
--- a/ci-jobs/functional/run_appium.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-steps:
-- task: NodeTool@0
-  inputs:
-    versionSpec: '11.x'
-  displayName: Install Node 11.x
-- script: npm install -g appium@beta --chromedriver_version='2.44'
-  displayName: Install appium
-- script: npm install -g opencv4nodejs
-  condition: and(succeeded(), eq('${{ parameters.opencv }}', true))
-  displayName: Install opencv4nodejs
-- task: UsePythonVersion@0
-  inputs:
-    versionSpec: '3.x'
-- script: brew install ffmpeg
-  displayName: Resolve dependencies (Appium server)
-- script: python setup.py install
-  displayName: Install python language bindings for Appium
-- script: |
-    pip install pipenv
-    pipenv lock --clear
-    pipenv install --system
-  displayName: Resolve dependencies (Python)
-- script: |
-    git --no-pager log -n1
-    python --version
-    ffmpeg -version
-    appium --version
-    node --version
-  displayName: Check versions
-- script: nohup appium --relaxed-security > appium_log.txt &
-  displayName: Run Appium in background
diff --git a/ci-jobs/functional/run_ios_test.yml b/ci-jobs/functional/run_ios_test.yml
deleted file mode 100644
index 5fa792d0e..000000000
--- a/ci-jobs/functional/run_ios_test.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-jobs:
-  - job: ${{ parameters.name }}
-    pool:
-      vmImage: ${{ parameters.vmImage }}
-    variables:
-      CI: ${{ parameters.ci }}
-    steps:
-      - template: ./run_appium.yml
-      - template: ./ios_setup.yml
-        parameters:
-          xcodeVersion: ${{ parameters.xcodeForIOS }}
-      - script: |
-          cd test/functional/ios
-          python -m pytest ${{ parameters.testFiles}} ${{ parameters.pytestOpt }}
-        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/start-emulator.sh b/ci-jobs/functional/start-emulator.sh
deleted file mode 100644
index 6228a9883..000000000
--- a/ci-jobs/functional/start-emulator.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env bash
-
-# This file comes from https://github.com/appium/ruby_lib_core
-
-# 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 testemulator -k "${emulator}" --force
-
-echo ${ANDROID_HOME}/emulator/emulator -list-avds
-
-echo "Starting emulator"
-
-# Start emulator in background
-nohup ${ANDROID_HOME}/emulator/emulator -avd testemulator -no-boot-anim -no-snapshot > /dev/null 2>&1 &
-${ANDROID_HOME}/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82'
-
-${ANDROID_HOME}/platform-tools/adb devices
-
-echo "Emulator started"
diff --git a/ci-jobs/functional_test.yml b/ci-jobs/functional_test.yml
deleted file mode 100644
index 200b7f204..000000000
--- a/ci-jobs/functional_test.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-parameters:
-  vmImage: 'macOS-10.14'
-  pytestOpt: '--doctest-modules --junitxml=junit/test-results.xml --cov=com --cov-report=xml --cov-report=html'
-  androidSdkVer: 27
-  xcodeForIOS: 11.3
-  CI: true
-
-jobs:
-  - template: ./functional/run_ios_test.yml
-    parameters:
-      name: 'func_test_ios1'
-      vmImage: ${{ parameters.vmImage }}
-      pytestOpt: ${{ parameters.pytestOpt }}
-      testFiles: 'search_context/find_by_*.py remote_fs_tests.py safari_tests.py execute_driver_tests.py'
-      CI: ${{ parameters.ci }}
-      xcodeForIOS: ${{ parameters.xcodeForIOS }}
-  - template: ./functional/run_ios_test.yml
-    parameters:
-      name: 'func_test_ios2'
-      vmImage: ${{ parameters.vmImage }}
-      pytestOpt: ${{ parameters.pytestOpt }}
-      testFiles: 'applications_tests.py hw_actions_tests.py keyboard_tests.py screen_record_tests.py webdriver_tests.py'
-      CI: ${{ parameters.ci }}
-      xcodeForIOS: ${{ parameters.xcodeForIOS }}
-  - 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 }}
-      OPENCV: true
-  - 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: 'chrome_tests.py'
-      sdkVer: ${{ parameters.androidSdkVer }}
-      CI: ${{ parameters.ci }}
-  - 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'
-      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_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'
-      vmImage: ${{ parameters.vmImage }}
-      pytestOpt: ${{ parameters.pytestOpt }}
-      testFiles: 'network_connection_tests.py log_event_tests.py activities_tests.py hw_actions_tests.py touch_action_tests.py'
-      sdkVer: ${{ parameters.androidSdkVer }}
-      CI: ${{ parameters.ci }}
diff --git a/ci.sh b/ci.sh
deleted file mode 100755
index 0fb538628..000000000
--- a/ci.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/bash
-
-set -o pipefail
-
-EXIT_STATUS=0
-if ! python -m autopep8 --exit-code -a -r --global-config .config-pep8 -i . ; then
-  echo "Please run command 'python -m autopep8 -a -r --global-config .config-pep8 -i .' on your local and commit the result"
-  EXIT_STATUS=1
-fi
-
-if ! python -m isort --check-only -rc . ; then
-  echo "Please run command 'python -m isort -rc .' on your local and commit the result"
-  EXIT_STATUS=1
-fi
-
-(
-  LINT_RESULT=$(python -m pylint --rcfile .pylintrc appium test --errors-only 2>&1 | tee /dev/tty)
-  if [[ $? -ne 0 ]] ; then
-    EXIT_STATUS=1
-  fi
-
-  # FIXME: pylint x Python 3.7 cause this runtime error.
-  # We must remove here when we drop Python 2 (and can update pylint) or
-  # install newer pylint for Python 3.7 environment on CI
-  if [[ $LINT_RESULT =~ "RuntimeError: generator raised StopIteration" ]] ; then
-    EXIT_STATUS=0
-  fi
-)
-
-(
-  python -m pytest test/unit/
-)
-if [[ $? -ne 0 ]] ; then
-  EXIT_STATUS=1
-fi
-
-exit $EXIT_STATUS
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 000000000..d4bb2cbb9
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..d788be7f1
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,15 @@
+### How to check generated doc
+
+```bash
+$cd python-client/docs
+$pip install -r requirements.txt
+$bash generate.sh
+$cd python-client/docs/_build/html
+$python -m http.server 1234
+```
+
+Access to http://localhost:1234 on web browser
+
+
+### How to deploy generated doc
+Handled at https://github.com/ki4070ma/python-client-sphinx
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 000000000..236b9e5d3
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,58 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath('../appium'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'Python client 1.1'
+copyright = '2020-2025, Appium'
+author = 'Appium'
+
+# The full version, including alpha/beta/rc tags
+release = '1.1'
+
+language = 'en'
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.githubpages']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+add_module_names = False
diff --git a/docs/generate.sh b/docs/generate.sh
new file mode 100644
index 000000000..219b7325d
--- /dev/null
+++ b/docs/generate.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+rm -rf *rst _build
+sphinx-apidoc -F -H 'Appium python client' -o . ../appium/webdriver
+make html
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 000000000..b98b63760
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,18 @@
+.. Appium python client documentation master file, created by
+   sphinx-quickstart on Sat May 24 20:49:49 2025.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Appium python client documentation
+==================================
+
+Add your content using ``reStructuredText`` syntax. See the
+`reStructuredText <https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html>`_
+documentation for details.
+
+
+.. toctree::
+   :maxdepth: 4
+   :caption: Contents:
+
+   webdriver
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 000000000..6247f7e23
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 000000000..21bd02829
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,3 @@
+sphinx >= 4.0, < 9.0
+sphinx_rtd_theme < 4.0
+sphinxcontrib-apidoc < 1.0
diff --git a/docs/roadmap.md b/docs/roadmap.md
deleted file mode 100644
index cf61486a7..000000000
--- a/docs/roadmap.md
+++ /dev/null
@@ -1,36 +0,0 @@
-Appium Python Client Plan
-=========================
-
-This library will be a simple extension of the official Python bindings, through
-subclassing, to add the new methods. I would like to maintain the same package
-structure, so that switching to the Appium library would be a matter of changing
-the import.
-
-The official client allows for three ways to interact with the server: with the
-`selenium` class, with the `webdriver.Remote` class, and with specific browser
-classes, which subclass `webdriver.Remote` in `webdriver.*` classes. It seems
-like we would not need to update the browser classes for our use case, and the
-first is for RC, which we don't support. Thus we can subclass the official
-`webdriver.Remote` classes and add the new methods. Otherwise we would need to
-use composition, since we have to change the base class and subclasses.
-
-Usage will remain as it currently is, using the first two methods from above,
-other than importing from Appium:
-
-```python
-from appium import webdriver
-
-desired_caps = {}
-# ...
-
-driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
-
-print driver.get_window_size()
-elem = driver.find_element_by_name('Graphics')
-elem.click()
-driver.quit()
-```
-
-As Selenium catches up, the methods can be seemlessly removed from the Appium
-client. Any methods outside of the spec can remain and be used without issue,
-should the user choose.
diff --git a/docs/webdriver.common.rst b/docs/webdriver.common.rst
new file mode 100644
index 000000000..bdc71e983
--- /dev/null
+++ b/docs/webdriver.common.rst
@@ -0,0 +1,21 @@
+webdriver.common package
+========================
+
+Submodules
+----------
+
+webdriver.common.appiumby module
+--------------------------------
+
+.. automodule:: webdriver.common.appiumby
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+Module contents
+---------------
+
+.. automodule:: webdriver.common
+   :members:
+   :show-inheritance:
+   :undoc-members:
diff --git a/docs/webdriver.extensions.android.rst b/docs/webdriver.extensions.android.rst
new file mode 100644
index 000000000..a1a54bfa8
--- /dev/null
+++ b/docs/webdriver.extensions.android.rst
@@ -0,0 +1,93 @@
+webdriver.extensions.android package
+====================================
+
+Submodules
+----------
+
+webdriver.extensions.android.activities module
+----------------------------------------------
+
+.. automodule:: webdriver.extensions.android.activities
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.common module
+------------------------------------------
+
+.. automodule:: webdriver.extensions.android.common
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.display module
+-------------------------------------------
+
+.. automodule:: webdriver.extensions.android.display
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.gsm module
+---------------------------------------
+
+.. automodule:: webdriver.extensions.android.gsm
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.nativekey module
+---------------------------------------------
+
+.. automodule:: webdriver.extensions.android.nativekey
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.network module
+-------------------------------------------
+
+.. automodule:: webdriver.extensions.android.network
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.performance module
+-----------------------------------------------
+
+.. automodule:: webdriver.extensions.android.performance
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.power module
+-----------------------------------------
+
+.. automodule:: webdriver.extensions.android.power
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.sms module
+---------------------------------------
+
+.. automodule:: webdriver.extensions.android.sms
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.android.system\_bars module
+------------------------------------------------
+
+.. automodule:: webdriver.extensions.android.system_bars
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+Module contents
+---------------
+
+.. automodule:: webdriver.extensions.android
+   :members:
+   :show-inheritance:
+   :undoc-members:
diff --git a/docs/webdriver.extensions.flutter_integration.rst b/docs/webdriver.extensions.flutter_integration.rst
new file mode 100644
index 000000000..a7bb41df3
--- /dev/null
+++ b/docs/webdriver.extensions.flutter_integration.rst
@@ -0,0 +1,37 @@
+webdriver.extensions.flutter\_integration package
+=================================================
+
+Submodules
+----------
+
+webdriver.extensions.flutter\_integration.flutter\_commands module
+------------------------------------------------------------------
+
+.. automodule:: webdriver.extensions.flutter_integration.flutter_commands
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.flutter\_integration.flutter\_finder module
+----------------------------------------------------------------
+
+.. automodule:: webdriver.extensions.flutter_integration.flutter_finder
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.flutter\_integration.scroll\_directions module
+-------------------------------------------------------------------
+
+.. automodule:: webdriver.extensions.flutter_integration.scroll_directions
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+Module contents
+---------------
+
+.. automodule:: webdriver.extensions.flutter_integration
+   :members:
+   :show-inheritance:
+   :undoc-members:
diff --git a/docs/webdriver.extensions.rst b/docs/webdriver.extensions.rst
new file mode 100644
index 000000000..631f09f15
--- /dev/null
+++ b/docs/webdriver.extensions.rst
@@ -0,0 +1,158 @@
+webdriver.extensions package
+============================
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   webdriver.extensions.android
+   webdriver.extensions.flutter_integration
+
+Submodules
+----------
+
+webdriver.extensions.action\_helpers module
+-------------------------------------------
+
+.. automodule:: webdriver.extensions.action_helpers
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.applications module
+----------------------------------------
+
+.. automodule:: webdriver.extensions.applications
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.clipboard module
+-------------------------------------
+
+.. automodule:: webdriver.extensions.clipboard
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.context module
+-----------------------------------
+
+.. automodule:: webdriver.extensions.context
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.device\_time module
+----------------------------------------
+
+.. automodule:: webdriver.extensions.device_time
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.execute\_driver module
+-------------------------------------------
+
+.. automodule:: webdriver.extensions.execute_driver
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.execute\_mobile\_command module
+----------------------------------------------------
+
+.. automodule:: webdriver.extensions.execute_mobile_command
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.hw\_actions module
+---------------------------------------
+
+.. automodule:: webdriver.extensions.hw_actions
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.images\_comparison module
+----------------------------------------------
+
+.. automodule:: webdriver.extensions.images_comparison
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.keyboard module
+------------------------------------
+
+.. automodule:: webdriver.extensions.keyboard
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.location module
+------------------------------------
+
+.. automodule:: webdriver.extensions.location
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.log\_event module
+--------------------------------------
+
+.. automodule:: webdriver.extensions.log_event
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.logs module
+--------------------------------
+
+.. automodule:: webdriver.extensions.logs
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.remote\_fs module
+--------------------------------------
+
+.. automodule:: webdriver.extensions.remote_fs
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.screen\_record module
+------------------------------------------
+
+.. automodule:: webdriver.extensions.screen_record
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.session module
+-----------------------------------
+
+.. automodule:: webdriver.extensions.session
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.extensions.settings module
+------------------------------------
+
+.. automodule:: webdriver.extensions.settings
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+Module contents
+---------------
+
+.. automodule:: webdriver.extensions
+   :members:
+   :show-inheritance:
+   :undoc-members:
diff --git a/docs/webdriver.rst b/docs/webdriver.rst
new file mode 100644
index 000000000..f74a7db81
--- /dev/null
+++ b/docs/webdriver.rst
@@ -0,0 +1,126 @@
+webdriver package
+=================
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   webdriver.common
+   webdriver.extensions
+
+Submodules
+----------
+
+webdriver.appium\_connection module
+-----------------------------------
+
+.. automodule:: webdriver.appium_connection
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.appium\_service module
+--------------------------------
+
+.. automodule:: webdriver.appium_service
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.applicationstate module
+---------------------------------
+
+.. automodule:: webdriver.applicationstate
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.client\_config module
+-------------------------------
+
+.. automodule:: webdriver.client_config
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.clipboard\_content\_type module
+-----------------------------------------
+
+.. automodule:: webdriver.clipboard_content_type
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.command\_method module
+--------------------------------
+
+.. automodule:: webdriver.command_method
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.connectiontype module
+-------------------------------
+
+.. automodule:: webdriver.connectiontype
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.errorhandler module
+-----------------------------
+
+.. automodule:: webdriver.errorhandler
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.locator\_converter module
+-----------------------------------
+
+.. automodule:: webdriver.locator_converter
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.mobilecommand module
+------------------------------
+
+.. automodule:: webdriver.mobilecommand
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.switch\_to module
+---------------------------
+
+.. automodule:: webdriver.switch_to
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.webdriver module
+--------------------------
+
+.. automodule:: webdriver.webdriver
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+webdriver.webelement module
+---------------------------
+
+.. automodule:: webdriver.webelement
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+Module contents
+---------------
+
+.. automodule:: webdriver
+   :members:
+   :show-inheritance:
+   :undoc-members:
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..86509bd56
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,70 @@
+[project]
+name = "Appium-Python-Client"
+description = "Python client for Appium"
+version = "5.2.0"
+readme = "README.md"
+license = "Apache-2.0"
+license-files = ["LICENSE"]
+authors = [
+    {name = "Isaac Murchie", email = "isaac@saucelabs.com"},
+]
+maintainers = [
+    {name = "Kazuaki Matsuo"},
+    {name = "Mykola Mokhnach"},
+    {name = "Mori Atsushi"},
+]
+keywords = ["appium", "selenium", "python client", "mobile automation"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "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",
+    "Topic :: Software Development :: Testing",
+]
+requires-python = ">=3.9"
+dependencies = [
+  "selenium>=4.26,<5.0",
+  "typing-extensions~=4.13",
+]
+
+[project.urls]
+Homepage = "http://appium.io/"
+Repository = "https://github.com/appium/python-client"
+Issues = "https://github.com/appium/python-client/issues"
+Changelog = "https://github.com/appium/python-client/blob/master/CHANGELOG.rst"
+
+[tool.uv]
+dev-dependencies = [
+  "httpretty~=1.1",
+  "mock~=5.2",
+  "mypy~=1.17",
+  "pre-commit~=4.2",
+  "pytest~=8.4",
+  "pytest-cov~=6.2",
+  "pytest-xdist~=3.8",
+  "python-dateutil~=2.9",
+  "ruff~=0.12",
+  "types-python-dateutil~=2.9",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.version]
+source = "regex"
+path = "appium/version.py"
+pattern = "(?P<version>\\d+\\.\\d+\\.\\d+)"
+
+[tool.hatch.build]
+exclude = [
+  "test/",
+  "script/",
+  "release.sh"
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["appium"]
diff --git a/release.sh b/release.sh
index 02c503cb0..744aae34e 100755
--- a/release.sh
+++ b/release.sh
@@ -3,11 +3,7 @@
 set -e
 set -o pipefail
 
-if [ -z "$PYTHON_BIN_PATH" ]; then
-  PYTHON_BIN_PATH=$(which python3 || which python || true)
-fi
-
 CONFIGURE_DIR=$(dirname "$0")
-"$PYTHON_BIN_PATH" "${CONFIGURE_DIR}/script/release.py" "$@"
+uv run python "${CONFIGURE_DIR}/script/release.py" "$@"
 
 echo "Finish release process"
diff --git a/script/release.py b/script/release.py
index 23cfcf697..f3045daf9 100644
--- a/script/release.py
+++ b/script/release.py
@@ -13,23 +13,25 @@
 # limitations under the License.
 """Release script to publish release module to pipy."""
 
-import io
+import glob
 import os
+import shutil
+import subprocess
 import sys
+from typing import List
 
-VERSION_FILE_PATH = os.path.join(os.path.dirname('__file__'), 'appium', 'version.py')
 CHANGELOG_PATH = os.path.join(os.path.dirname('__file__'), 'CHANGELOG.rst')
 
+APPIUM_DIR_PATH = os.path.join(os.path.dirname('__file__'), 'appium')
+BUILT_APPIUM_DIR_PATH = os.path.join(os.path.dirname('__file__'), 'build', 'lib', 'appium')
+
 MESSAGE_RED = '\033[1;31m{}\033[0m'
 MESSAGE_GREEN = '\033[1;32m{}\033[0m'
 MESSAGE_YELLOW = '\033[1;33m{}\033[0m'
 
 
-def get_current_version():
-    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
+def print_current_version():
+    os.system('uv version')
 
 
 def get_new_version():
@@ -42,9 +44,7 @@ def get_new_version():
 
 
 def update_version_file(version):
-    new_version = VERSION_FORMAT.format(version)
-    with open(VERSION_FILE_PATH, 'w') as f:
-        f.write(new_version)
+    call_bash_script(f'uv version {version}')
 
 
 def call_bash_script(cmd):
@@ -55,23 +55,26 @@ def call_bash_script(cmd):
 
 
 def commit_version_code(new_version_num):
-    call_bash_script('git commit {} -m "Bump {}"'.format(VERSION_FILE_PATH, new_version_num))
+    call_bash_script('git commit pyproject.toml uv.lock -m "Bump {}"'.format(new_version_num))
 
 
 def tag_and_generate_changelog(new_version_num):
     call_bash_script('git tag "v{}"'.format(new_version_num))
-    call_bash_script('gitchangelog > {}'.format(CHANGELOG_PATH))
+    call_bash_script('uv run gitchangelog > {}'.format(CHANGELOG_PATH))
     call_bash_script('git commit {} -m "Update changelog for {}"'.format(CHANGELOG_PATH, new_version_num))
 
 
 def upload_sdist(new_version_num):
-    push_file = 'dist/Appium-Python-Client-{}.tar.gz'.format(new_version_num)
+    wheel_file = 'dist/appium_python_client-{}-py3-none-any.whl'.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))
+        call_bash_script(f"uv run twine upload '{wheel_file}' '{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))
+        print(
+            'Failed to upload {} to pypi. Please fix the original error and push it again later. Original error: {}'.format(
+                push_file, e
+            )
+        )
 
 
 def push_changes_to_master(new_version_num):
@@ -87,28 +90,59 @@ 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():
-    call_bash_script('{} setup.py sdist'.format(sys.executable))
+    call_bash_script('uv build')
 
 
-def validate_release_env():
-    if os.system('which twine') != 0:
-        exit("Please get twine via 'pip install twine'")
-    if os.system('which gitchangelog') != 0:
-        exit("Please get twine via 'pip install gitchangelog' or 'pip install git+git://github.com/vaab/gitchangelog.git' for Python 3.7")
+def build() -> None:
+    shutil.rmtree(BUILT_APPIUM_DIR_PATH, ignore_errors=True)
+    status, output = subprocess.getstatusoutput('uv run python setup.py install')
+    if status != 0:
+        sys.exit(f'Failed to build the package:\n{output}')
 
 
-def main():
-    validate_release_env()
+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)
+    ]
+
+
+def assert_files_count_in_package() -> None:
+    original_files = get_py_files_in_dir(APPIUM_DIR_PATH)
+    built_files = get_py_files_in_dir(BUILT_APPIUM_DIR_PATH)
+
+    if len(original_files) != len(built_files):
+        print(f"The count of files in '{APPIUM_DIR_PATH}' and '{BUILT_APPIUM_DIR_PATH}' were different.")
+
+        original_files_set = set(original_files)
+        built_files_set = set(built_files)
 
-    get_current_version()
+        diff = original_files_set.difference(built_files_set)
+        if diff:
+            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}')
+
+        sys.exit(
+            f"Python files in '{BUILT_APPIUM_DIR_PATH}' may differ from '{APPIUM_DIR_PATH}'. "
+            'Please make sure setup.py is configured properly.'
+        )
+
+
+def main():
+    print_current_version()
     new_version = get_new_version()
 
     update_version_file(new_version)
 
+    build()
+    assert_files_count_in_package()
+
     ensure_publication(new_version)
 
     commit_version_code(new_version)
diff --git a/setup.py b/setup.py
index 9912e7eb5..f32eab925 100644
--- a/setup.py
+++ b/setup.py
@@ -11,48 +11,35 @@
 # WITHOUT 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 io
-import os
 
-from setuptools import find_packages, setup
+# FIXME: Remove this setup.py completely.
+# Then, we should bump the major version since the package will not include setup.py.
+
+try:
+    # Python 3.11+
+    import tomllib
+except Exception:
+    # for older versions
+    import tomli as tomllib
 
-from appium.common.helper import library_version
+with open('pyproject.toml', 'rb') as f:
+    pyproject = tomllib.load(f)
+    project = pyproject['project']
+
+from setuptools import find_packages, setup
 
 setup(
-    name='Appium-Python-Client',
-    version=library_version(),
-    description='Python client for Appium',
-    long_description=io.open(os.path.join(os.path.dirname('__file__'), 'README.md'), encoding='utf-8').read(),
-    long_description_content_type='text/markdown',
-    keywords=[
-        'appium',
-        'selenium',
-        'selenium 3',
-        'python client',
-        'mobile automation'
-    ],
-    author='Isaac Murchie',
-    author_email='isaac@saucelabs.com',
-    url='http://appium.io/',
+    name=project['name'],
+    version=project['version'],
+    description=project['description'],
+    keywords=project['keywords'],
+    author=project['authors'][0]['name'],
+    author_email=project['authors'][0]['email'],
+    maintainer=', '.join([maintainer['name'] for maintainer in project['maintainers']]),
+    url=project['urls']['Homepage'],
+    package_data={'appium': ['py.typed']},
     packages=find_packages(include=['appium*']),
-    license='Apache 2.0',
-    classifiers=[
-        'Development Status :: 4 - Beta',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 2.7',
-        'Programming Language :: Python :: 3.4',
-        'Programming Language :: Python :: 3.5',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
-        'Environment :: Console',
-        'Environment :: MacOS X',
-        'Environment :: Win32 (MS Windows)',
-        'Intended Audience :: Developers',
-        'Intended Audience :: Other Audience',
-        'License :: OSI Approved :: Apache Software License',
-        'Operating System :: OS Independent',
-        'Topic :: Software Development :: Quality Assurance',
-        'Topic :: Software Development :: Testing'
-    ],
-    install_requires=['selenium >= 3.14.1, < 4']
+    license=project['license'],
+    classifiers=project['classifiers'],
+    install_requires=project['dependencies'],
 )
diff --git a/test/apps/UICatalog.app.zip b/test/apps/UICatalog.app.zip
index 0b112d2eb..479d92e50 100644
Binary files a/test/apps/UICatalog.app.zip and b/test/apps/UICatalog.app.zip differ
diff --git a/test/apps/selendroid-test-app.apk b/test/apps/selendroid-test-app.apk
deleted file mode 100644
index 8c1124264..000000000
Binary files a/test/apps/selendroid-test-app.apk and /dev/null differ
diff --git a/test/functional/android/activities_tests.py b/test/functional/android/activities_tests.py
deleted file mode 100644
index 7b1894ff0..000000000
--- a/test/functional/android/activities_tests.py
+++ /dev/null
@@ -1,47 +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 unittest
-
-from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase
-
-
-class ActivitiesTests(BaseTestCase):
-    def test_current_activity(self):
-        activity = self.driver.current_activity
-        self.assertEqual('.ApiDemos', activity)
-
-    def test_start_activity_this_app(self):
-        self.driver.start_activity(APIDEMO_PKG_NAME, ".ApiDemos")
-        self._assert_activity_contains('Demos')
-
-        self.driver.start_activity(APIDEMO_PKG_NAME, ".accessibility.AccessibilityNodeProviderActivity")
-        self._assert_activity_contains('Node')
-
-    def test_start_activity_other_app(self):
-        self.driver.start_activity(APIDEMO_PKG_NAME, ".ApiDemos")
-        self._assert_activity_contains('Demos')
-
-        self.driver.start_activity("com.android.calculator2", ".Calculator")
-        self._assert_activity_contains('Calculator')
-
-    def _assert_activity_contains(self, activity):
-        current = self.driver.current_activity
-        self.assertTrue(activity in current)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(ActivitiesTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/appium_service_tests.py b/test/functional/android/appium_service_tests.py
new file mode 100644
index 000000000..6c346f734
--- /dev/null
+++ b/test/functional/android/appium_service_tests.py
@@ -0,0 +1,44 @@
+#!/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 typing import Generator
+
+import pytest
+
+from appium.webdriver.appium_service import AppiumService
+
+
+@pytest.fixture
+def appium_service() -> Generator[AppiumService, None, None]:
+    service = AppiumService()
+    service.start(
+        args=[
+            '--address',
+            '127.0.0.1',
+            '-p',
+            '4773',
+            '--base-path',
+            '/wd/hub',
+        ]
+    )
+    try:
+        yield service
+    finally:
+        service.stop()
+
+
+def test_appium_service(appium_service: AppiumService) -> None:
+    assert appium_service.is_running
+    assert appium_service.is_listening
diff --git a/test/functional/android/applications_tests.py b/test/functional/android/applications_tests.py
deleted file mode 100644
index 9703fbc7b..000000000
--- a/test/functional/android/applications_tests.py
+++ /dev/null
@@ -1,83 +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
-import unittest
-from time import sleep
-
-from appium.webdriver.applicationstate import ApplicationState
-
-from .helper.desired_capabilities import PATH
-from .helper.test_helper import APIDEMO_PKG_NAME, BaseTestCase
-
-
-class ApplicationsTests(BaseTestCase):
-
-    def test_background_app(self):
-        self.driver.background_app(1)
-        sleep(3)
-        self.driver.launch_app()
-
-    def test_is_app_installed(self):
-        self.assertFalse(self.driver.is_app_installed('sdfsdf'))
-        self.assertTrue(self.driver.is_app_installed(APIDEMO_PKG_NAME))
-
-    def test_install_app(self):
-        self.assertFalse(self.driver.is_app_installed('io.selendroid.testapp'))
-        self.driver.install_app(PATH(os.path.join('../..', 'apps', 'selendroid-test-app.apk')))
-        self.assertTrue(self.driver.is_app_installed('io.selendroid.testapp'))
-
-    def test_remove_app(self):
-        self.assertTrue(self.driver.is_app_installed(APIDEMO_PKG_NAME))
-        self.driver.remove_app(APIDEMO_PKG_NAME)
-        self.assertFalse(self.driver.is_app_installed(APIDEMO_PKG_NAME))
-
-    def test_close_and_launch_app(self):
-        self.driver.close_app()
-        self.driver.launch_app()
-        activity = self.driver.current_activity
-        self.assertEqual('.ApiDemos', activity)
-
-    def test_app_management(self):
-        app_id = self.driver.current_package
-        self.assertEqual(self.driver.query_app_state(app_id),
-                         ApplicationState.RUNNING_IN_FOREGROUND)
-        self.driver.background_app(-1)
-        self.assertTrue(self.driver.query_app_state(app_id) <
-                        ApplicationState.RUNNING_IN_FOREGROUND)
-        self.driver.activate_app(app_id)
-        self.assertEqual(self.driver.query_app_state(app_id),
-                         ApplicationState.RUNNING_IN_FOREGROUND)
-
-    def test_app_strings(self):
-        strings = self.driver.app_strings()
-        self.assertEqual(u'You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data'])
-
-    def test_app_strings_with_language(self):
-        strings = self.driver.app_strings('en')
-        self.assertEqual(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):
-        strings = self.driver.app_strings('en', 'some_file')
-        self.assertEqual(u'You can\'t wipe my data, you are a monkey!', strings[u'monkey_wipe_data'])
-
-    def test_reset(self):
-        self.driver.reset()
-        self.assertTrue(self.driver.is_app_installed(APIDEMO_PKG_NAME))
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(ApplicationsTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/bidi_tests.py b/test/functional/android/bidi_tests.py
new file mode 100644
index 000000000..f37a2d94e
--- /dev/null
+++ b/test/functional/android/bidi_tests.py
@@ -0,0 +1,81 @@
+#!/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 selenium.webdriver.common.bidi.common import command_builder
+
+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
+
+
+class AppiumLogEntry:
+    event_class = 'log.entryAdded'
+
+    def __init__(self, level, text, timestamp, source, type):
+        self.level = level
+        self.text = text
+        self.timestamp = timestamp
+        self.source = source
+        self.type = type
+
+    @property
+    def json(self):
+        return dict(type=self.type, level=self.level, text=self.text, timestamp=self.timestamp, source=self.source)
+
+    @classmethod
+    def from_json(cls, json: dict):
+        return cls(
+            level=json['level'],
+            text=json['text'],
+            timestamp=json['timestamp'],
+            source=json['source'],
+            type=json['type'],
+        )
+
+
+class TestChromeWithBiDi:
+    """This test requires selenium python client which supports 'command_builder'"""
+
+    def setup_method(self) -> None:
+        client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE)
+        client_config.timeout = 600
+        caps = get_desired_capabilities()
+        caps['webSocketUrl'] = True
+        self.driver = webdriver.Remote(
+            SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps), client_config=client_config
+        )
+
+    def teardown_method(self) -> None:
+        self.driver.quit()
+
+    def test_bidi_log(self) -> None:
+        log_entries = []
+        bidi_log_param = {'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']}
+
+        self.driver.script.conn.execute(command_builder('session.subscribe', bidi_log_param))
+
+        def _log(entry: AppiumLogEntry):
+            # e.g. {'type': 'syslog', 'level': 'info', 'source': {'realm': ''}, 'text': '08-05 13:30:32.617 29677 29709 I appium  : channel read: GET /session/d7c38859-8930-4eb0-960a-8f917c9e6a38/source', 'timestamp': 1754368241565}
+            log_entries.append(entry.json)
+
+        try:
+            callback_id = self.driver.script.conn.add_callback(AppiumLogEntry, _log)
+            self.driver.page_source
+            assert len(log_entries) != 0
+            self.driver.script.conn.remove_callback(AppiumLogEntry, callback_id)
+        finally:
+            self.driver.script.conn.execute(command_builder('session.unsubscribe', bidi_log_param))
diff --git a/test/functional/android/chrome_tests.py b/test/functional/android/chrome_tests.py
index 32f74216b..000bc5dc3 100644
--- a/test/functional/android/chrome_tests.py
+++ b/test/functional/android/chrome_tests.py
@@ -12,29 +12,31 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
-
 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
 
 from .helper.desired_capabilities import get_desired_capabilities
 
 
-class ChromeTests(unittest.TestCase):
-    def setUp(self):
+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('http://localhost:4723/wd/hub', caps)
+        self.driver = webdriver.Remote(
+            SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps), client_config=client_config
+        )
 
-    def tearDown(self):
+    def teardown_method(self) -> None:
         self.driver.quit()
 
-    def test_find_single_element(self):
-        self.driver.get('http://10.0.2.2:4723/test/guinea-pig')
-        self.driver.find_element_by_link_text('i am a link').click()
-
-        self.assertTrue('I am some other page content' in self.driver.page_source)
-
+    def test_find_single_element(self) -> None:
+        e = self.driver.find_element(by=AppiumBy.XPATH, value='//body')
+        assert e.text == ''
 
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(ChromeTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
+        # Chrome browser's default page
+        assert '<html><head></head><body></body></html>' in self.driver.page_source
diff --git a/test/functional/android/common_tests.py b/test/functional/android/common_tests.py
deleted file mode 100644
index 7c50150cf..000000000
--- a/test/functional/android/common_tests.py
+++ /dev/null
@@ -1,71 +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 unittest
-from time import sleep
-
-from selenium.common.exceptions import NoSuchElementException
-
-from appium.webdriver.common.mobileby import MobileBy
-
-from ..test_helper import is_ci
-from .helper.test_helper import (
-    APIDEMO_PKG_NAME,
-    BaseTestCase,
-    wait_for_element
-)
-
-
-class CommonTests(BaseTestCase):
-
-    def test_current_package(self):
-        self.assertEqual(APIDEMO_PKG_NAME, self.driver.current_package)
-
-    def test_end_test_coverage(self):
-        self.skipTest('Not sure how to set this up to run')
-        self.driver.end_test_coverage(intent='android.intent.action.MAIN', path='')
-        sleep(5)
-
-    def test_open_notifications(self):
-        for word in ['App', 'Notification', 'Status Bar', ':-|']:
-            wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                             'new UiSelector().text("{}")'.format(word)).click()
-
-        self.driver.open_notifications()
-        sleep(1)
-        self.assertRaises(NoSuchElementException,
-                          self.driver.find_element_by_android_uiautomator, 'new UiSelector().text(":-|")')
-
-        els = self.driver.find_elements_by_class_name('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
-        self.assertTrue(title)
-        self.assertTrue(body)
-
-        self.driver.keyevent(4)
-        sleep(1)
-        self.driver.find_element_by_android_uiautomator('new UiSelector().text(":-|")')
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(CommonTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/context_switching_tests.py b/test/functional/android/context_switching_tests.py
deleted file mode 100644
index 2bc6d0926..000000000
--- a/test/functional/android/context_switching_tests.py
+++ /dev/null
@@ -1,68 +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 unittest
-
-import pytest
-
-from appium import webdriver
-from appium.common.exceptions import NoSuchContextException
-
-from .helper import desired_capabilities
-
-
-class ContextSwitchingTests(unittest.TestCase):
-    def setUp(self):
-        desired_caps = desired_capabilities.get_desired_capabilities('selendroid-test-app.apk')
-        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
-
-    def test_contexts_list(self):
-        self.skipTest('Need to replace with apk which has WEBVIEW as context')
-        self._enter_webview()
-        contexts = self.driver.contexts
-        self.assertEqual(2, len(contexts))
-
-    def test_move_to_correct_context(self):
-        self.skipTest('Need to replace with apk which has WEBVIEW as context')
-        self._enter_webview()
-        self.assertEqual('WEBVIEW_io.selendroid.testapp', self.driver.current_context)
-
-    def test_actually_in_webview(self):
-        self.skipTest('Need to replace with apk which has WEBVIEW as context')
-        self._enter_webview()
-        self.driver.find_element_by_css_selector('input[type=submit]').click()
-        el = self.driver.find_element_by_xpath("//h1[contains(., 'This is my way')]")
-        self.assertIsNot(None, el)
-
-    def test_move_back_to_native_context(self):
-        self.skipTest('Need to replace with apk which has WEBVIEW as context')
-        self._enter_webview()
-        self.driver.switch_to.context(None)
-        self.assertEqual('NATIVE_APP', self.driver.current_context)
-
-    def test_set_invalid_context(self):
-        self.assertRaises(NoSuchContextException, self.driver.switch_to.context, 'invalid name')
-
-    def tearDown(self):
-        self.driver.quit()
-
-    def _enter_webview(self):
-        btn = self.driver.find_element_by_accessibility_id('buttonStartWebviewCD')
-        btn.click()
-        self.driver.switch_to.context('WEBVIEW')
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(ContextSwitchingTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 5ea155cc4..000000000
Binary files a/test/functional/android/file/find_by_image_failure.png and /dev/null differ
diff --git a/test/functional/android/file/find_by_image_success.png b/test/functional/android/file/find_by_image_success.png
deleted file mode 100644
index 39435fd3a..000000000
Binary files a/test/functional/android/file/find_by_image_success.png and /dev/null differ
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 6434234fb..000000000
Binary files a/test/functional/android/file/test_image.jpg and /dev/null differ
diff --git a/test/functional/android/helper/desired_capabilities.py b/test/functional/android/helper/desired_capabilities.py
index eccb091af..ef400ffff 100644
--- a/test/functional/android/helper/desired_capabilities.py
+++ b/test/functional/android/helper/desired_capabilities.py
@@ -13,23 +13,23 @@
 # limitations under the License.
 
 import os
-
+from typing import Any, Dict, Optional
 
 # Returns abs path relative to this file and not cwd
-def PATH(p):
-    return os.path.abspath(
-        os.path.join(os.path.dirname(__file__), '..', p)
-    )
 
 
-def get_desired_capabilities(app=None):
-    desired_caps = {
+def PATH(p: str) -> str:
+    return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', p))
+
+
+def get_desired_capabilities(app: Optional[str] = None) -> Dict[str, Any]:
+    desired_caps: Dict[str, Any] = {
         'platformName': 'Android',
         'deviceName': 'Android Emulator',
         'newCommandTimeout': 240,
         'automationName': 'UIAutomator2',
         'uiautomator2ServerInstallTimeout': 120000,
-        'adbExecTimeout': 120000
+        'adbExecTimeout': 120000,
     }
 
     if app is not None:
diff --git a/test/functional/android/helper/test_helper.py b/test/functional/android/helper/test_helper.py
index 5813abc6b..7f113f210 100644
--- a/test/functional/android/helper/test_helper.py
+++ b/test/functional/android/helper/test_helper.py
@@ -15,13 +15,12 @@
 
 import base64
 import os
-import unittest
-
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.support.ui import WebDriverWait
 
 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
 
 from . import desired_capabilities
 
@@ -32,39 +31,22 @@
 APIDEMO_PKG_NAME = 'io.appium.android.apis'
 
 
-def wait_for_element(driver, locator, value, timeout=SLEEPY_TIME):
-    """Wait until the element located
-
-    Args:
-        driver (`appium.webdriver.webdriver.WebDriver`): WebDriver instance
-        locator (str): Locator like WebDriver, Mobile JSON Wire Protocol
-            (e.g. `appium.webdriver.common.mobileby.MobileBy.ACCESSIBILITY_ID`)
-        value (str): Query value to locator
-        timeout (int): Maximum time to wait the element. If time is over, `TimeoutException` is thrown
-
-    Raises:
-        `selenium.common.exceptions.TimeoutException`
-
-    Returns:
-        `appium.webdriver.webelement.WebElement`: Found WebElement
-    """
-    return WebDriverWait(driver, timeout).until(
-        EC.presence_of_element_located((locator, value))
-    )
-
-
-class BaseTestCase(unittest.TestCase):
-
-    def setUp(self):
-        desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip')
-        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
+class BaseTestCase:
+    def setup_method(self, method) -> None:  # type: ignore
+        caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip')
+        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()
 
-    def tearDown(self):
+    def teardown_method(self, method) -> None:  # type: ignore
+        if not hasattr(self, 'driver'):
+            return
+
         if is_ci():
             payload = self.driver.stop_recording_screen()
-            video_path = os.path.join(os.getcwd(), self._testMethodName + '.mp4')
-            with open(video_path, "wb") as fd:
+            video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4')
+            with open(video_path, 'wb') as fd:
                 fd.write(base64.b64decode(payload))
         self.driver.quit()
diff --git a/test/functional/android/hw_actions_tests.py b/test/functional/android/hw_actions_tests.py
deleted file mode 100644
index 3bb022f47..000000000
--- a/test/functional/android/hw_actions_tests.py
+++ /dev/null
@@ -1,35 +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 unittest
-from time import sleep
-
-from .helper.test_helper import BaseTestCase
-
-
-class HwActionsTests(BaseTestCase):
-    def test_lock(self):
-        self.driver.lock(-1)
-        sleep(10)
-        try:
-            self.assertTrue(self.driver.is_locked())
-        finally:
-            self.driver.unlock()
-        self.assertFalse(self.driver.is_locked())
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(HwActionsTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/ime_tests.py b/test/functional/android/ime_tests.py
deleted file mode 100644
index 86bf7449a..000000000
--- a/test/functional/android/ime_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.
-
-import unittest
-from time import sleep
-
-from .helper.test_helper import BaseTestCase
-
-ANDROID_LATIN = 'com.android.inputmethod.latin/.LatinIME'  # Android L/M/N
-GOOGLE_LATIN = 'com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME'  # Android O/P
-
-
-class IMETests(BaseTestCase):
-    def test_available_ime_engines(self):
-        engines = self.driver.available_ime_engines
-        self.assertIsInstance(engines, list)
-        self.assertTrue(ANDROID_LATIN in engines or GOOGLE_LATIN in engines)
-
-    def test_is_ime_active(self):
-        self.assertTrue(self.driver.is_ime_active())
-
-    def test_active_ime_engine(self):
-        engines = self.driver.available_ime_engines
-        self.assertTrue(self.driver.active_ime_engine in engines)
-
-    def test_activate_ime_engine(self):
-        engines = self.driver.available_ime_engines
-        active_engine = self.driver.active_ime_engine
-
-        self.driver.activate_ime_engine(engines[-1])
-        self.assertEqual(self.driver.active_ime_engine, engines[-1])
-
-    def test_deactivate_ime_engine(self):
-        engines = self.driver.available_ime_engines
-        self.driver.activate_ime_engine(engines[-1])
-
-        self.assertEqual(self.driver.active_ime_engine, engines[-1])
-
-        self.driver.deactivate_ime_engine()
-        sleep(1)
-        self.assertNotEqual(self.driver.active_ime_engine, engines[-1])
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(IMETests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/log_event_tests.py b/test/functional/android/log_event_tests.py
deleted file mode 100644
index 8067e9f7b..000000000
--- a/test/functional/android/log_event_tests.py
+++ /dev/null
@@ -1,31 +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 unittest
-
-from .helper.test_helper import BaseTestCase
-
-
-class LogEventTests(BaseTestCase):
-    def test_log_event(self):
-        vendor = 'appium'
-        event = 'funEvent'
-        self.driver.log_event(vendor, event)
-        assert '{}:{}'.format(vendor, event) in self.driver.get_events().keys()
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(LogEventTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/multi_action_tests.py b/test/functional/android/multi_action_tests.py
deleted file mode 100644
index 7ed477087..000000000
--- a/test/functional/android/multi_action_tests.py
+++ /dev/null
@@ -1,114 +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 unittest
-from time import sleep
-
-from appium.webdriver.common.mobileby import MobileBy
-from appium.webdriver.common.multi_action import MultiAction
-from appium.webdriver.common.touch_action import TouchAction
-
-from .helper.test_helper import BaseTestCase, is_ci, wait_for_element
-
-
-class MultiActionTests(BaseTestCase):
-    def test_parallel_actions(self):
-        self._move_to_splitting_touches_accros_views()
-
-        els = self.driver.find_elements_by_class_name('android.widget.ListView')
-        a1 = TouchAction()
-        a1.press(els[0]) \
-            .move_to(x=10, y=0).move_to(x=10, y=-75).move_to(x=10, y=-600).release()
-
-        a2 = TouchAction()
-        a2.press(els[1]) \
-            .move_to(x=10, y=10).move_to(x=10, y=-300).move_to(x=10, y=-600).release()
-
-        ma = MultiAction(self.driver, els[0])
-        ma.add(a1, a2)
-        ma.perform()
-
-    def test_actions_with_waits(self):
-        self._move_to_splitting_touches_accros_views()
-
-        els = self.driver.find_elements_by_class_name('android.widget.ListView')
-        a1 = TouchAction()
-        a1.press(els[0]) \
-            .move_to(x=10, y=0) \
-            .move_to(x=10, y=-75) \
-            .wait(1000) \
-            .move_to(x=10, y=-600) \
-            .release()
-
-        a2 = TouchAction()
-        a2.press(els[1]) \
-            .move_to(x=10, y=10) \
-            .move_to(x=10, y=-300) \
-            .wait(500) \
-            .move_to(x=10, y=-600) \
-            .release()
-
-        ma = MultiAction(self.driver, els[0])
-        ma.add(a1, a2)
-        ma.perform()
-
-    def _move_to_splitting_touches_accros_views(self):
-        el1 = self.driver.find_element_by_accessibility_id('Content')
-        el2 = self.driver.find_element_by_accessibility_id('Animation')
-        self.driver.scroll(el1, el2)
-
-        el = self.driver.find_element_by_accessibility_id('Views')
-        action = TouchAction(self.driver)
-        action.tap(el).perform()
-
-        # simulate a swipe/scroll
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists')
-        action.press(el).move_to(x=100, y=-1000).release().perform()
-        el = self.driver.find_element_by_accessibility_id('Layouts')
-        action.press(el).move_to(x=100, y=-1000).release().perform()
-
-        el = self.driver.find_element_by_accessibility_id('Splitting Touches across Views')
-        action.tap(el).perform()
-
-        wait_for_element(self.driver, MobileBy.ID, 'io.appium.android.apis:id/list1')
-
-    def test_driver_multi_tap(self):
-        if is_ci():
-            self.skipTest('Skip since the test must be watched to check if it works')
-        el = self.driver.find_element_by_accessibility_id('Graphics')
-        action = TouchAction(self.driver)
-        action.tap(el).perform()
-
-        wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.TextView')
-        els = self.driver.find_elements_by_class_name('android.widget.TextView')
-        self.driver.scroll(els[len(els) - 1], els[0])
-
-        els = self.driver.find_elements_by_class_name('android.widget.TextView')
-        if els[len(els) - 1].get_attribute('name') != 'Xfermodes':
-            self.driver.scroll(els[len(els) - 1], els[0])
-
-        el = self.driver.find_element_by_accessibility_id('Touch Paint')
-        action.tap(el).perform()
-
-        positions = [(100, 200), (100, 400)]
-
-        # makes two dots in the paint program
-        # THE TEST MUST BE WATCHED TO CHECK IF IT WORKS
-        self.driver.tap(positions)
-        sleep(10)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(MultiActionTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/network_connection_tests.py b/test/functional/android/network_connection_tests.py
deleted file mode 100644
index 342673453..000000000
--- a/test/functional/android/network_connection_tests.py
+++ /dev/null
@@ -1,36 +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 unittest
-
-from appium.webdriver.connectiontype import ConnectionType
-
-from .helper.test_helper import BaseTestCase
-
-
-class NetworkConnectionTests(BaseTestCase):
-    def test_get_network_connection(self):
-        nc = self.driver.network_connection
-        self.assertIsInstance(nc, int)
-
-    def test_set_network_connection(self):
-        nc = self.driver.set_network_connection(ConnectionType.DATA_ONLY)
-        self.assertIsInstance(nc, int)
-        self.assertEqual(nc, ConnectionType.DATA_ONLY)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(NetworkConnectionTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/remote_fs_tests.py b/test/functional/android/remote_fs_tests.py
deleted file mode 100644
index fefe08392..000000000
--- a/test/functional/android/remote_fs_tests.py
+++ /dev/null
@@ -1,66 +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
-import unittest
-from io import BytesIO
-from zipfile import ZipFile
-
-from appium.common.helper import appium_bytes
-
-from .helper.test_helper import BaseTestCase
-
-
-class RemoteFsTests(BaseTestCase):
-    def test_push_pull_file(self):
-        dest_path = '/data/local/tmp/test_push_file.txt'
-        data = appium_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))
-
-        self.assertEqual(data, data_ret)
-
-    def test_pull_folder(self):
-        data = appium_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 ['1.txt', '2.txt']:
-                self.assertTrue(filename in fzip.namelist())
-
-    def test_push_file_with_src_path(self):
-        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))
-            self.assertEqual(original_data, new_data)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(RemoteFsTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/screen_record_tests.py b/test/functional/android/screen_record_tests.py
deleted file mode 100644
index 26f5f7646..000000000
--- a/test/functional/android/screen_record_tests.py
+++ /dev/null
@@ -1,32 +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 unittest
-from time import sleep
-
-from .helper.test_helper import BaseTestCase
-
-
-class ScreenRecordTests(BaseTestCase):
-    def test_screen_record(self):
-        self.driver.start_recording_screen(timeLimit=10, forcedRestart=True)
-        sleep(10)
-        result = self.driver.stop_recording_screen()
-        self.assertTrue(len(result) > 0)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(ScreenRecordTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 b5e8a8787..000000000
--- a/test/functional/android/search_context/find_by_accessibility_id_tests.py
+++ /dev/null
@@ -1,55 +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 unittest
-
-from appium.webdriver.common.mobileby import MobileBy
-from test.functional.android.helper.test_helper import (
-    BaseTestCase,
-    wait_for_element
-)
-
-
-class FindByAccessibilityIDTests(BaseTestCase):
-    def test_find_single_element(self):
-        wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click()
-        wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                         'new UiSelector().text("Accessibility Node Querying")').click()
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Task Take out Trash')
-        self.assertIsNotNone(el)
-
-    def test_find_multiple_elements(self):
-        els = self.driver.find_elements_by_accessibility_id('Accessibility')
-        self.assertIsInstance(els, list)
-
-    def test_element_find_single_element(self):
-        wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("Accessibility")').click()
-        wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                         'new UiSelector().text("Accessibility Node Querying")').click()
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Task Take out Trash')
-
-        sub_el = el.find_element_by_accessibility_id('Task Take out Trash')
-        self.assertIsNotNone(sub_el)
-
-    def test_element_find_multiple_elements(self):
-        wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.ListView')
-        el = self.driver.find_element_by_class_name('android.widget.ListView')
-
-        sub_els = el.find_elements_by_accessibility_id('Animation')
-        self.assertIsInstance(sub_els, list)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FindByAccessibilityIDTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 0ba6f8a96..000000000
--- a/test/functional/android/search_context/find_by_image_tests.py
+++ /dev/null
@@ -1,86 +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 unittest
-
-from selenium.common.exceptions import NoSuchElementException, TimeoutException
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.support.ui import WebDriverWait
-
-from appium import webdriver
-from appium.webdriver.common.mobileby import MobileBy
-from test.functional.android.helper import desired_capabilities
-
-from ..helper.test_helper import wait_for_element
-
-
-class FindByImageTests(unittest.TestCase):
-
-    def setUp(self):
-        desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip')
-        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
-
-        # relax template matching
-        self.driver.update_settings({"fixImageFindScreenshotDims": False,
-                                     "fixImageTemplateSize": True,
-                                     "autoUpdateImageElementPosition": True})
-
-    def tearDown(self):
-        self.driver.quit()
-
-    def test_find_based_on_image_template(self):
-        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, MobileBy.IMAGE, b64_data)
-        size = el.size
-        self.assertIsNotNone(size['width'])
-        self.assertIsNotNone(size['height'])
-        loc = el.location
-        self.assertIsNotNone(loc['x'])
-        self.assertIsNotNone(loc['y'])
-        rect = el.rect
-        self.assertIsNotNone(rect['width'])
-        self.assertIsNotNone(rect['height'])
-        self.assertIsNotNone(rect['x'])
-        self.assertIsNotNone(rect['y'])
-        self.assertTrue(el.is_displayed())
-        el.click()
-        wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm")
-
-    def test_find_multiple_elements_by_image_just_returns_one(self):
-        wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "App")
-        image_path = desired_capabilities.PATH('file/find_by_image_success.png')
-        els = self.driver.find_elements_by_image(image_path)
-        els[0].click()
-        wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, "Alarm")
-
-    def test_find_throws_no_such_element(self):
-        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 self.assertRaises(TimeoutException):
-            WebDriverWait(self.driver, 3).until(
-                EC.presence_of_element_located((MobileBy.IMAGE, b64_data))
-            )
-        with self.assertRaises(NoSuchElementException):
-            self.driver.find_element_by_image(image_path)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FindByImageTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 b602a5913..000000000
--- a/test/functional/android/search_context/find_by_uiautomator_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 unittest
-
-import pytest
-
-from test.functional.android.helper.test_helper import BaseTestCase
-
-
-class FindByUIAutomatorTests(BaseTestCase):
-    def test_find_single_element(self):
-        el = self.driver.find_element_by_android_uiautomator('new UiSelector().text("Animation")')
-        self.assertIsNotNone(el)
-
-    def test_find_multiple_elements(self):
-        els = self.driver.find_elements_by_android_uiautomator('new UiSelector().clickable(true)')
-        self.assertIsInstance(els, list)
-
-    def test_element_find_single_element(self):
-        el = self.driver.find_element_by_class_name('android.widget.ListView')
-
-        sub_el = el.find_element_by_android_uiautomator('new UiSelector().description("Animation")')
-        self.assertIsNotNone(sub_el)
-
-    def test_element_find_multiple_elements(self):
-        el = self.driver.find_element_by_class_name('android.widget.ListView')
-
-        sub_els = el.find_elements_by_android_uiautomator('new UiSelector().clickable(true)')
-        self.assertIsInstance(sub_els, list)
-
-    def test_scroll_into_view(self):
-        el = self.driver.find_element_by_android_uiautomator(
-            'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));')
-        el.click()
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FindByUIAutomatorTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 0f8ed0d00..000000000
--- a/test/functional/android/search_context/find_by_view_matcher_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 os
-import unittest
-
-import pytest
-from selenium.common.exceptions import WebDriverException
-
-from appium import webdriver
-from appium.webdriver.common.mobileby import MobileBy
-from appium.webdriver.extensions.search_context.android import (
-    AndroidSearchContext
-)
-from test.functional.android.helper.test_helper import (
-    desired_capabilities,
-    is_ci
-)
-
-
-class FindByViewMatcherTests(unittest.TestCase):
-
-    def setUp(self):
-        desired_caps = desired_capabilities.get_desired_capabilities('ApiDemos-debug.apk.zip')
-        desired_caps['automationName'] = 'Espresso'
-        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
-
-    def tearDown(self):
-        if is_ci():
-            # Take the screenshot to investigate when tests failed only on CI
-            img_path = os.path.join(os.getcwd(), self._testMethodName + '.png')
-            self.driver.get_screenshot_as_file(img_path)
-        self.driver.quit()
-
-    def test_find_single_element(self):
-        el = self.driver.find_element_by_android_view_matcher(
-            name='withText', args=['Accessibility'], className='ViewMatchers')
-        assert el.text == 'Accessibility'
-
-    def test_find_single_element_ful_class_name(self):
-        el = self.driver.find_element_by_android_view_matcher(
-            name='withText', args=['Accessibility'], className='androidx.test.espresso.matcher.ViewMatchers')
-        assert el.text == 'Accessibility'
-
-    def test_find_single_element_using_hamcrest_matcher(self):
-        el = self.driver.find_element_by_android_view_matcher(
-            name='withText',
-            args={
-                'name': 'containsString',
-                'args': 'Animati',
-                'class': 'org.hamcrest.Matchers'},
-            className='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):
-        value = AndroidSearchContext()._build_data_matcher(
-            name='withSubstring', args=['Access'], className='ViewMatchers')
-        with pytest.raises(WebDriverException):
-            self.driver.find_elements(by=MobileBy.ANDROID_VIEW_MATCHER, value=value)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FindByViewMatcherTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/settings_tests.py b/test/functional/android/settings_tests.py
deleted file mode 100644
index a1ffe10bb..000000000
--- a/test/functional/android/settings_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 unittest
-
-from .helper.test_helper import BaseTestCase
-
-
-class SettingsTests(BaseTestCase):
-    def test_get_settings(self):
-        settings = self.driver.get_settings()
-        self.assertIsNotNone(settings)
-
-    def test_update_settings(self):
-        self.driver.update_settings({"waitForIdleTimeout": 10001})
-        settings = self.driver.get_settings()
-        self.assertEqual(settings["waitForIdleTimeout"], 10001)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(SettingsTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/touch_action_tests.py b/test/functional/android/touch_action_tests.py
deleted file mode 100644
index 5fb5dca54..000000000
--- a/test/functional/android/touch_action_tests.py
+++ /dev/null
@@ -1,202 +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 unittest
-
-from selenium.common.exceptions import NoSuchElementException
-
-from appium.webdriver.common.mobileby import MobileBy
-from appium.webdriver.common.touch_action import TouchAction
-
-from .helper.test_helper import (
-    APIDEMO_PKG_NAME,
-    BaseTestCase,
-    is_ci,
-    wait_for_element
-)
-
-
-class TouchActionTests(BaseTestCase):
-    def test_tap(self):
-        el = self.driver.find_element_by_accessibility_id('Animation')
-        action = TouchAction(self.driver)
-        action.tap(el).perform()
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls')
-        self.assertIsNotNone(el)
-
-    def test_tap_x_y(self):
-        el = self.driver.find_element_by_accessibility_id('Animation')
-        action = TouchAction(self.driver)
-        action.tap(el, 100, 10).perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls')
-        self.assertIsNotNone(el)
-
-    def test_tap_twice(self):
-        el = self.driver.find_element_by_accessibility_id('Text')
-        action = TouchAction(self.driver)
-        action.tap(el).perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'LogTextBox')
-        action.tap(el).perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Add')
-        action.tap(el, count=2).perform()
-
-        els = self.driver.find_elements_by_class_name('android.widget.TextView')
-        self.assertEqual('This is a test\nThis is a test\n', els[1].get_attribute("text"))
-
-    def test_press_and_immediately_release(self):
-        el = self.driver.find_element_by_accessibility_id('Animation')
-        action = TouchAction(self.driver)
-        action.press(el).release().perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls')
-        self.assertIsNotNone(el)
-
-    def test_press_and_immediately_release_x_y(self):
-        el = self.driver.find_element_by_accessibility_id('Animation')
-        action = TouchAction(self.driver)
-        action.press(el, 100, 10).release().perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Bouncing Balls')
-        self.assertIsNotNone(el)
-
-    def test_press_and_wait(self):
-        self._move_to_custom_adapter()
-        action = TouchAction(self.driver)
-
-        el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                              'new UiSelector().text("People Names")')
-        action.press(el).wait(2000).perform()
-
-        # 'Sample menu' only comes up with a long press, not a press
-        el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                              'new UiSelector().text("Sample menu")')
-        self.assertIsNotNone(el)
-
-    def test_press_and_moveto(self):
-        el1 = self.driver.find_element_by_accessibility_id('Content')
-        el2 = self.driver.find_element_by_accessibility_id('Animation')
-
-        action = TouchAction(self.driver)
-        action.press(el1).move_to(el2).release().perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views')
-        self.assertIsNotNone(el)
-
-    def test_press_and_moveto_x_y(self):
-        el1 = self.driver.find_element_by_accessibility_id('Content')
-        el2 = self.driver.find_element_by_accessibility_id('App')
-
-        action = TouchAction(self.driver)
-        action.press(el1).move_to(el2, 100, 100).release().perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views')
-        self.assertIsNotNone(el)
-
-    def test_long_press(self):
-        self._move_to_custom_adapter()
-        action = TouchAction(self.driver)
-
-        el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                              'new UiSelector().text("People Names")')
-        action.long_press(el).perform()
-
-        # 'Sample menu' only comes up with a long press, not a tap
-        el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                              'new UiSelector().text("Sample menu")')
-        self.assertIsNotNone(el)
-
-    def test_long_press_x_y(self):
-        if is_ci():
-            self.skipTest("Skip since this check is low robust due to hard-coded position.")
-        self._move_to_custom_adapter()
-        action = TouchAction(self.driver)
-
-        # the element "People Names" is located at 430:310 (top left corner)
-        # location can be changed by phone resolusion, OS version
-        action.long_press(x=430, y=310).perform()
-
-        # 'Sample menu' only comes up with a long press, not a tap
-        el = wait_for_element(self.driver, MobileBy.ANDROID_UIAUTOMATOR,
-                              'new UiSelector().text("Sample menu")')
-        self.assertIsNotNone(el)
-
-    def test_drag_and_drop(self):
-        self._move_to_views()
-        action = TouchAction(self.driver)
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Drag and Drop')
-        action.tap(el).perform()
-
-        dd3 = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_dot_3'.format(APIDEMO_PKG_NAME))
-        dd2 = self.driver.find_element_by_id('{}:id/drag_dot_2'.format(APIDEMO_PKG_NAME))
-
-        # dnd is stimulated by longpress-move_to-release
-        action.long_press(dd3).move_to(dd2).release().perform()
-
-        el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME))
-        self.assertTrue('Dropped!' in el.text)
-
-    def test_driver_drag_and_drop(self):
-        self._move_to_views()
-        action = TouchAction(self.driver)
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Drag and Drop')
-        action.tap(el).perform()
-
-        dd3 = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_dot_3'.format(APIDEMO_PKG_NAME))
-        dd2 = self.driver.find_element_by_id('{}:id/drag_dot_2'.format(APIDEMO_PKG_NAME))
-
-        self.driver.drag_and_drop(dd3, dd2)
-
-        el = wait_for_element(self.driver, MobileBy.ID, '{}:id/drag_result_text'.format(APIDEMO_PKG_NAME))
-        self.assertTrue('Dropped!' in el.text)
-
-    def test_driver_swipe(self):
-        el = self.driver.find_element_by_accessibility_id('Views')
-        action = TouchAction(self.driver)
-        action.tap(el).perform()
-
-        wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Animation')
-        self.assertRaises(NoSuchElementException, self.driver.find_element_by_accessibility_id, 'ImageView')
-
-        self.driver.swipe(100, 1000, 100, 100, 800)
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'ImageView')
-        self.assertIsNotNone(el)
-
-    def _move_to_views(self):
-        el1 = self.driver.find_element_by_accessibility_id('Content')
-        el2 = self.driver.find_element_by_accessibility_id('Animation')
-        self.driver.scroll(el1, el2)
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Views')
-        action = TouchAction(self.driver)
-        action.tap(el).perform()
-
-    def _move_to_custom_adapter(self):
-        self._move_to_views()
-        action = TouchAction(self.driver)
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Expandable Lists')
-        action.tap(el).perform()
-
-        el = wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Custom Adapter')
-        action.tap(el).perform()
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(TouchActionTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/android/webelement_tests.py b/test/functional/android/webelement_tests.py
deleted file mode 100644
index 1a16473ba..000000000
--- a/test/functional/android/webelement_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.
-
-import unittest
-
-from appium.webdriver.common.mobileby import MobileBy
-
-from .helper.test_helper import (
-    APIDEMO_PKG_NAME,
-    BaseTestCase,
-    wait_for_element
-)
-
-
-class WebelementTests(BaseTestCase):
-    def test_element_location_in_view(self):
-        el = self.driver.find_element_by_accessibility_id('Content')
-        loc = el.location_in_view
-        self.assertIsNotNone(loc['x'])
-        self.assertIsNotNone(loc['y'])
-
-    def test_set_text(self):
-        self.driver.find_element_by_android_uiautomator(
-            'new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().text("Views").instance(0));').click()
-
-        wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, 'Controls').click()
-        wait_for_element(self.driver, MobileBy.ACCESSIBILITY_ID, '1. Light Theme').click()
-
-        el = wait_for_element(self.driver, MobileBy.CLASS_NAME, 'android.widget.EditText')
-        el.send_keys('original text')
-        el.set_text('new text')
-
-        self.assertEqual('new text', el.text)
-
-    def test_send_keys(self):
-        for text in ['App', 'Activity', 'Custom Title']:
-            wait_for_element(self.driver, MobileBy.XPATH,
-                             "//android.widget.TextView[@text='{}']".format(text)).click()
-
-        el = wait_for_element(self.driver, MobileBy.ID, '{}:id/left_text_edit'.format(APIDEMO_PKG_NAME))
-        el.send_keys(' text')
-
-        self.assertEqual('Left is best text', el.text)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(WebelementTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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..f256222e0
--- /dev/null
+++ b/test/functional/flutter_integration/commands_test.py
@@ -0,0 +1,134 @@
+#!/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 000000000..355548c30
Binary files /dev/null and b/test/functional/flutter_integration/file/second_qr.png differ
diff --git a/test/functional/flutter_integration/file/success_qr.png b/test/functional/flutter_integration/file/success_qr.png
new file mode 100644
index 000000000..8896d86f6
Binary files /dev/null and b/test/functional/flutter_integration/file/success_qr.png differ
diff --git a/test/functional/flutter_integration/finder_test.py b/test/functional/flutter_integration/finder_test.py
new file mode 100644
index 000000000..d9bc3f56b
--- /dev/null
+++ b/test/functional/flutter_integration/finder_test.py
@@ -0,0 +1,56 @@
+#!/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..19c669754
--- /dev/null
+++ b/test/functional/flutter_integration/helper/test_helper.py
@@ -0,0 +1,48 @@
+#!/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 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
+
+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)
+
+        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
+        if not hasattr(self, 'driver'):
+            return
+        self.driver.quit()
diff --git a/test/functional/ios/applications_tests.py b/test/functional/ios/applications_tests.py
deleted file mode 100644
index 067aec186..000000000
--- a/test/functional/ios/applications_tests.py
+++ /dev/null
@@ -1,42 +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 unittest
-
-from appium.webdriver.applicationstate import ApplicationState
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-from .helper import desired_capabilities
-
-
-class WebDriverTests(BaseTestCase):
-
-    def test_app_management(self):
-        # this only works in Xcode9+
-        if float(desired_capabilities.get_desired_capabilities(
-                desired_capabilities.BUNDLE_ID)['platformVersion']) < 11:
-            return
-        self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID),
-                         ApplicationState.RUNNING_IN_FOREGROUND)
-        self.driver.background_app(-1)
-        self.assertTrue(self.driver.query_app_state(desired_capabilities.BUNDLE_ID) <
-                        ApplicationState.RUNNING_IN_FOREGROUND)
-        self.driver.activate_app(desired_capabilities.BUNDLE_ID)
-        self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID),
-                         ApplicationState.RUNNING_IN_FOREGROUND)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(WebDriverTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/ios/execute_driver_tests.py b/test/functional/ios/execute_driver_tests.py
deleted file mode 100644
index 4f2251ecd..000000000
--- a/test/functional/ios/execute_driver_tests.py
+++ /dev/null
@@ -1,49 +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
-import unittest
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class ExecuteDriverTests(BaseTestCase):
-    def test_batch(self):
-        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):
-        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])
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(ExecuteDriverTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 6434234fb..000000000
Binary files a/test/functional/ios/file/test_image.jpg and /dev/null differ
diff --git a/test/functional/ios/helper/desired_capabilities.py b/test/functional/ios/helper/desired_capabilities.py
index 2fc9b25c0..7dfaf0bfd 100644
--- a/test/functional/ios/helper/desired_capabilities.py
+++ b/test/functional/ios/helper/desired_capabilities.py
@@ -12,66 +12,73 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import os
+from typing import Any, Dict, Optional
 
 # Returns abs path relative to this file and not cwd
 
 
-def PATH(p): return os.path.abspath(
-    os.path.join(os.path.dirname(__file__), p)
-)
+def PATH(p: str) -> str:
+    return os.path.abspath(os.path.join(os.path.dirname(__file__), p))
 
 
 BUNDLE_ID = 'com.example.apple-samplecode.UICatalog'
 
 
-def get_desired_capabilities(app=None):
-    desired_caps = {
+def get_desired_capabilities(app: Optional[str] = None) -> Dict[str, Any]:
+    desired_caps: Dict[str, Any] = {
         'deviceName': iphone_device_name(),
         'platformName': 'iOS',
-        'platformVersion': '13.3',
+        'platformVersion': '17.4',
         'automationName': 'XCUITest',
         'allowTouchIdEnroll': True,
         'wdaLocalPort': wda_port(),
-        'simpleIsVisibleCheck': True
+        'simpleIsVisibleCheck': True,
     }
 
     if app is not None:
         desired_caps['app'] = PATH(os.path.join('../../..', 'apps', app))
 
+    local_prebuilt_wda = os.getenv('LOCAL_PREBUILT_WDA')
+    if local_prebuilt_wda:
+        desired_caps['usePreinstalledWDA'] = True
+        desired_caps['prebuiltWDAPath'] = local_prebuilt_wda
+
     return desired_caps
 
 
-class PytestXdistWorker(object):
-    NUMBER = os.getenv('PYTEST_XDIST_WORKER')
-    COUNT = os.getenv('PYTEST_XDIST_WORKER_COUNT')  # Return 2 if `-n 2` is passed
+class PytestXdistWorker:
+    NUMBER: Optional[str] = os.getenv('PYTEST_XDIST_WORKER')
+    COUNT: Optional[str] = os.getenv('PYTEST_XDIST_WORKER_COUNT')  # Return 2 if `-n 2` is passed
 
     @staticmethod
-    def gw(number):
+    def gw(number: int) -> str:
         if PytestXdistWorker.COUNT is None:
             return '0'
 
         if number >= int(PytestXdistWorker.COUNT):
             return 'gw0'
 
-        return 'gw{}'.format(number)
+        return f'gw{number}'
+
 
 # If you run tests with pytest-xdist, you can run tests in parallel.
 
 
-def wda_port():
+def wda_port() -> int:
     if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1):
         return 8101
 
     return 8100
 
 
-# Before running tests, you must have iOS simulators named 'iPhone 8 - 8100' and 'iPhone 8 - 8101'
+# Before running tests, you must have iOS simulators named 'iPhone 12 - 8100' and 'iPhone 12 - 8101'
 
 
-def iphone_device_name(port=None):
+def iphone_device_name() -> str:
+    prefix = 'iPhone 15 Plus'
     if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(0):
-        return 'iPhone 8 - 8100'
+        return f'{prefix} - 8100'
     elif PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1):
-        return 'iPhone 8 - 8101'
+        return f'{prefix} - 8101'
 
-    return 'iPhone 8'
+    return prefix
diff --git a/test/functional/ios/helper/test_helper.py b/test/functional/ios/helper/test_helper.py
index 3329bbd6a..435ca4b45 100644
--- a/test/functional/ios/helper/test_helper.py
+++ b/test/functional/ios/helper/test_helper.py
@@ -12,29 +12,36 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-
 import base64
 import os
-import unittest
 
 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
 
 from . import desired_capabilities
 
 
-class BaseTestCase(unittest.TestCase):
+class BaseTestCase(object):
+    IOS_UICATALOG_APP_NAME = 'UIKitCatalog'
 
-    def setUp(self):
+    def setup_method(self) -> None:
         desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip')
-        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', 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()
 
-    def tearDown(self):
+    def teardown_method(self, method) -> None:  # type: ignore
+        if not hasattr(self, 'driver'):
+            return
+
         if is_ci():
             payload = self.driver.stop_recording_screen()
-            video_path = os.path.join(os.getcwd(), self._testMethodName + '.mp4')
-            with open(video_path, "wb") as fd:
+            video_path = os.path.join(os.getcwd(), method.__name__ + '.mp4')
+            with open(video_path, 'wb') as fd:
                 fd.write(base64.b64decode(payload))
         self.driver.quit()
diff --git a/test/functional/ios/hw_actions_tests.py b/test/functional/ios/hw_actions_tests.py
deleted file mode 100644
index f37d268dd..000000000
--- a/test/functional/ios/hw_actions_tests.py
+++ /dev/null
@@ -1,45 +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 unittest
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class HwActionsTests(BaseTestCase):
-    def test_lock(self):
-        self.driver.lock(-1)
-        try:
-            self.assertTrue(self.driver.is_locked())
-        finally:
-            self.driver.unlock()
-        self.assertFalse(self.driver.is_locked())
-
-    def test_shake(self):
-        # what can we assert about this?
-        self.driver.shake()
-
-    def test_touch_id(self):
-        # 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):
-        # nothing to assert, just verify that it doesn't blow up
-        self.driver.toggle_touch_id_enrollment()
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(HwActionsTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/ios/keyboard_tests.py b/test/functional/ios/keyboard_tests.py
deleted file mode 100644
index 7ae623473..000000000
--- a/test/functional/ios/keyboard_tests.py
+++ /dev/null
@@ -1,85 +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 unittest
-from time import sleep
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class KeyboardTests(BaseTestCase):
-    def test_hide_keyboard(self):
-        self._move_to_textbox()
-
-        el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0]
-        el.click()
-        el.set_value('Testing')
-
-        el = self.driver.find_element_by_class_name('UIAKeyboard')
-        self.assertTrue(el.is_displayed())
-
-        self.driver.hide_keyboard(key_name='Done')
-
-        self.assertFalse(el.is_displayed())
-
-    def test_hide_keyboard_presskey_strategy(self):
-        self._move_to_textbox()
-
-        el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0]
-        el.click()
-        el.set_value('Testing')
-
-        el = self.driver.find_element_by_class_name('UIAKeyboard')
-        self.assertTrue(el.is_displayed())
-
-        self.driver.hide_keyboard(strategy='pressKey', key='Done')
-
-        self.assertFalse(el.is_displayed())
-
-    def test_hide_keyboard_no_key_name(self):
-        self._move_to_textbox()
-
-        el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0]
-        el.click()
-        el.set_value('Testing')
-
-        el = self.driver.find_element_by_class_name('UIAKeyboard')
-        self.assertTrue(el.is_displayed())
-
-        self.driver.hide_keyboard()
-        sleep(10)
-
-        # currently fails.
-        self.assertFalse(el.is_displayed())
-
-    def test_is_keyboard_shown(self):
-        self._move_to_textbox()
-
-        el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0]
-        el.click()
-        el.set_value('Testing')
-        self.assertTrue(self.driver.is_keyboard_shown())
-
-    def _move_to_textbox(self):
-        el1 = self.driver.find_element_by_accessibility_id('Sliders')
-        el2 = self.driver.find_element_by_accessibility_id('Buttons')
-        self.driver.scroll(el1, el2)
-
-        # Click text fields
-        self.driver.find_element_by_accessibility_id('Text Fields').click()
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(KeyboardTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/ios/remote_fs_tests.py b/test/functional/ios/remote_fs_tests.py
deleted file mode 100644
index 4c24b39f1..000000000
--- a/test/functional/ios/remote_fs_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.
-
-import os
-import unittest
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class RemoteFsTests(BaseTestCase):
-
-    def test_push_file(self):
-        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)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(RemoteFsTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/ios/safari_tests.py b/test/functional/ios/safari_tests.py
index a0b5255fc..c7bd997c1 100644
--- a/test/functional/ios/safari_tests.py
+++ b/test/functional/ios/safari_tests.py
@@ -12,37 +12,63 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
+import time
 
 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
 
 
-class SafariTests(unittest.TestCase):
-    def setUp(self):
-        desired_caps = get_desired_capabilities()
-        desired_caps.update({
-            'browserName': 'safari',
-            'nativeWebTap': True,
-            'safariIgnoreFraudWarning': True
-        })
+class TestSafari:
+    def setup_method(self) -> None:
+        caps = get_desired_capabilities()
+        caps.update(
+            {
+                'bundleId': 'com.apple.mobilesafari',
+                'nativeWebTap': True,
+                'safariIgnoreFraudWarning': True,
+                'webviewConnectTimeout': 100000,
+            }
+        )
+        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)
 
-        self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
+        # Fresh iOS 17.4 simulator may not show up the webview context with "safari"
+        # after a fresh simlator instance creation.
+        # Re-launch the process could be a workaround in my debugging.
+        self.driver.terminate_app('com.apple.mobilesafari')
+        self.driver.activate_app('com.apple.mobilesafari')
 
-    def tearDown(self):
+    def teardown_method(self) -> None:
         self.driver.quit()
 
-    def test_context(self):
-        self.assertEqual('NATIVE_APP', self.driver.contexts[0])
-        self.assertTrue(self.driver.contexts[1].startswith('WEBVIEW_'))
-        self.assertTrue('WEBVIEW_' in self.driver.current_context)
+    def test_context(self) -> None:
+        contexts = self.driver.contexts
+        assert 'NATIVE_APP' == contexts[0]
+        assert contexts[1].startswith('WEBVIEW_')
+        self.driver.switch_to.context(contexts[1])
+        assert 'WEBVIEW_' in self.driver.current_context
 
-    def test_get(self):
-        self.driver.get("http://google.com")
-        self.assertEqual('Google', self.driver.title)
+    def test_get(self) -> None:
+        ok = False
+        contexts = self.driver.contexts
+        for context in contexts:
+            if context.startswith('WEBVIEW_'):
+                self.driver.switch_to.context(context)
+                ok = True
+                break
 
+        if ok is False:
+            assert False, 'Could not set WEBVIEW context'
 
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(SafariTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
+        self.driver.get('http://google.com')
+        for _ in range(5):
+            time.sleep(0.5)
+            if 'Google' == self.driver.title:
+                return
+
+        assert False, 'The title was wrong'
diff --git a/test/functional/ios/screen_record_tests.py b/test/functional/ios/screen_record_tests.py
deleted file mode 100644
index 7ad79af6b..000000000
--- a/test/functional/ios/screen_record_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.
-
-import unittest
-from time import sleep
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class ScreenRecordTests(BaseTestCase):
-    def test_screen_record(self):
-        self.driver.start_recording_screen()
-        sleep(10)
-        result = self.driver.stop_recording_screen()
-        self.assertTrue(len(result) > 0)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(ScreenRecordTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 3da2fc421..000000000
--- a/test/functional/ios/search_context/find_by_element_webelement_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.
-
-import unittest
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class FindByElementWebelementTests(BaseTestCase):
-
-    def test_find_element_by_path(self):
-        el = self.driver.find_element_by_ios_predicate('wdName == "UICatalog"')
-        self.assertEqual('UICatalog', el.get_attribute('name'))
-
-        c_el = el.find_elements_by_ios_predicate('label == "Action Sheets"')
-        self.assertEqual('Action Sheets', c_el[0].get_attribute('name'))
-
-        c_el = el.find_elements_by_ios_class_chain('**/XCUIElementTypeStaticText')
-        self.assertEqual('UICatalog', c_el[0].get_attribute('name'))
-
-        c_el = el.find_elements_by_accessibility_id('UICatalog')
-        self.assertEqual('UICatalog', c_el[0].get_attribute('name'))
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FindByElementWebelementTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 12537ad3d..000000000
--- a/test/functional/ios/search_context/find_by_ios_class_chain_tests.py
+++ /dev/null
@@ -1,35 +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 unittest
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class FindByIOClassChainTests(BaseTestCase):
-    def test_find_element_by_path(self):
-        els = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/**/XCUIElementTypeStaticText')
-        self.assertEqual(35, len(els))
-        self.assertEqual('UICatalog', els[0].get_attribute('name'))
-
-    def test_find_multiple_elements_by_path(self):
-        el = self.driver.find_elements_by_ios_class_chain('XCUIElementTypeWindow/*/*/*')
-        self.assertEqual(2, len(el))
-        self.assertEqual('UICatalog', el[0].get_attribute('name'))
-        self.assertEqual(None, el[1].get_attribute('name'))
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FindByIOClassChainTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
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 6890b6b51..000000000
--- a/test/functional/ios/search_context/find_by_ios_predicate_tests.py
+++ /dev/null
@@ -1,56 +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 unittest
-
-from test.functional.ios.helper.test_helper import BaseTestCase
-
-
-class FindByIOSPredicateTests(BaseTestCase):
-    def test_find_element_by_name(self):
-        # Will throw exception if element is not found
-        self.driver.find_element_by_ios_predicate('wdName == "Buttons"')
-
-    def test_find_multiple_element_by_type(self):
-        e = self.driver.find_elements_by_ios_predicate('wdType == "XCUIElementTypeStaticText"')
-        self.assertNotEqual(len(e), 0)
-
-    def test_find_element_by_label(self):
-        # Will throw exception if element is not found
-        self.driver.find_element_by_ios_predicate('label == "Buttons"')
-
-    def test_find_element_by_value(self):
-        # Will throw exception if element is not found
-        self.driver.find_element_by_ios_predicate('wdValue == "Buttons"')
-
-    def test_find_element_by_isvisible(self):
-        # Will throw exception if element is not found
-        self.driver.find_element_by_ios_predicate('wdValue == "Buttons" AND visible == 1')
-
-        # Should not find any elements
-        e = self.driver.find_elements_by_ios_predicate('wdValue == "Buttons" AND visible == 0')
-        self.assertEqual(len(e), 0)
-
-    def test_find_element_by_isenabled(self):
-        # Will throw exception if element is not found
-        self.driver.find_element_by_ios_predicate('wdValue == "Buttons" AND enabled == 1')
-
-        # Should not find any elements
-        e = self.driver.find_elements_by_ios_predicate('wdValue == "Buttons" AND enabled == 0')
-        self.assertEqual(len(e), 0)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FindByIOSPredicateTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/ios/webdriver_tests.py b/test/functional/ios/webdriver_tests.py
deleted file mode 100644
index e36721611..000000000
--- a/test/functional/ios/webdriver_tests.py
+++ /dev/null
@@ -1,114 +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 unittest
-
-from selenium.webdriver.support.ui import WebDriverWait
-
-from appium import webdriver
-from appium.webdriver.applicationstate import ApplicationState
-from test.functional.ios.helper.test_helper import BaseTestCase
-from test.functional.test_helper import get_available_from_port_range
-
-from ..test_helper import is_ci
-from .helper import desired_capabilities
-
-
-class WebDriverTests(BaseTestCase):
-
-    def test_all_sessions(self):
-        if is_ci():
-            # TODO Due to not created 2nd session somehow
-            self.skipTest('Need to fix flaky test during running on CI.')
-        port = get_available_from_port_range(8200, 8300)
-        desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip')
-        desired_caps['deviceName'] = 'iPhone Xs Max'
-        desired_caps['wdaLocalPort'] = port
-
-        class session_counts_is_two(object):
-            TIMEOUT = 10
-
-            def __call__(self, driver):
-                return len(driver.all_sessions) == 2
-
-        driver2 = None
-        try:
-            driver2 = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
-            WebDriverWait(
-                driver2, session_counts_is_two.TIMEOUT).until(session_counts_is_two())
-            self.assertEqual(2, len(self.driver.all_sessions))
-        finally:
-            if driver2 is not None:
-                driver2.quit()
-
-    def test_app_management(self):
-        # this only works in Xcode9+
-        if float(desired_capabilities.get_desired_capabilities(
-                desired_capabilities.BUNDLE_ID)['platformVersion']) < 11:
-            return
-        self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID),
-                         ApplicationState.RUNNING_IN_FOREGROUND)
-        self.driver.background_app(-1)
-        self.assertTrue(self.driver.query_app_state(desired_capabilities.BUNDLE_ID) <
-                        ApplicationState.RUNNING_IN_FOREGROUND)
-        self.driver.activate_app(desired_capabilities.BUNDLE_ID)
-        self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID),
-                         ApplicationState.RUNNING_IN_FOREGROUND)
-
-    def test_clear(self):
-        self._move_to_textbox()
-
-        el = self.driver.find_elements_by_class_name('XCUIElementTypeTextField')[0]
-
-        # Verify default text
-        def_text = 'Placeholder text'
-        text = el.get_attribute('value')
-        self.assertEqual(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_class_name('XCUIElementTypeTextField')[0]
-        text = el.get_attribute('value')
-        self.assertEqual(text, input_text)
-
-        # Clear text, verify
-        el.clear()
-        text = el.get_attribute('value')
-        self.assertEqual(text, def_text)
-
-    def test_press_button(self):
-        self.driver.press_button("Home")
-        if float(desired_capabilities.get_desired_capabilities(
-                desired_capabilities.BUNDLE_ID)['platformVersion']) < 11:
-            return
-        self.assertEqual(self.driver.query_app_state(desired_capabilities.BUNDLE_ID),
-                         ApplicationState.RUNNING_IN_FOREGROUND)
-
-    def _move_to_textbox(self):
-        el1 = self.driver.find_element_by_accessibility_id('Sliders')
-        el2 = self.driver.find_element_by_accessibility_id('Buttons')
-        self.driver.scroll(el1, el2)
-
-        # Click text fields
-        self.driver.find_element_by_accessibility_id('Text Fields').click()
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(WebDriverTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
diff --git a/test/functional/mac/__init__.py b/test/functional/mac/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/functional/mac/execute_script_test.py b/test/functional/mac/execute_script_test.py
new file mode 100644
index 000000000..95144e3dd
--- /dev/null
+++ b/test/functional/mac/execute_script_test.py
@@ -0,0 +1,39 @@
+#!/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.mac.helper.test_helper import BaseTestCase
+from test.functional.test_helper import wait_for_element
+
+
+class TestExecuteScript(BaseTestCase):
+    def test_sending_custom_keys(self) -> None:
+        edit_field = wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'XCUIElementTypeTextView')
+        flagsShift = 1 << 1
+        self.driver.execute_script(
+            'macos: keys',
+            {
+                'keys': [
+                    {
+                        'key': 'h',
+                        'modifierFlags': flagsShift,
+                    },
+                    {
+                        'key': 'i',
+                        'modifierFlags': flagsShift,
+                    },
+                ]
+            },
+        )
+        assert edit_field.text == 'HI'
diff --git a/test/functional/mac/helper/__init__.py b/test/functional/mac/helper/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/functional/android/finger_print_tests.py b/test/functional/mac/helper/desired_capabilities.py
similarity index 59%
rename from test/functional/android/finger_print_tests.py
rename to test/functional/mac/helper/desired_capabilities.py
index e77e72a45..648420efb 100644
--- a/test/functional/android/finger_print_tests.py
+++ b/test/functional/mac/helper/desired_capabilities.py
@@ -1,5 +1,4 @@
 #!/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.
@@ -13,17 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
+from typing import Any, Dict
 
-from .helper.test_helper import BaseTestCase
 
+def get_desired_capabilities() -> Dict[str, Any]:
+    desired_caps: Dict[str, Any] = {'platformName': 'mac', 'automationName': 'Mac2', 'bundleId': 'com.apple.TextEdit'}
 
-class FingerPrintTests(BaseTestCase):
-    def test_finger_print(self):
-        result = self.driver.finger_print(1)
-        self.assertEqual(None, result)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(FingerPrintTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
+    return desired_caps
diff --git a/test/functional/mac/helper/test_helper.py b/test/functional/mac/helper/test_helper.py
new file mode 100644
index 000000000..0b7463dac
--- /dev/null
+++ b/test/functional/mac/helper/test_helper.py
@@ -0,0 +1,35 @@
+#!/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 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
+
+
+class BaseTestCase(object):
+    def setup_method(self) -> None:
+        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'):
+            return
+
+        self.driver.quit()
diff --git a/test/functional/android/keyboard_tests.py b/test/functional/mac/webelement_test.py
similarity index 52%
rename from test/functional/android/keyboard_tests.py
rename to test/functional/mac/webelement_test.py
index 6c634da31..f2fc4687a 100644
--- a/test/functional/android/keyboard_tests.py
+++ b/test/functional/mac/webelement_test.py
@@ -1,5 +1,4 @@
 #!/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.
@@ -13,21 +12,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import unittest
+from appium.webdriver.common.appiumby import AppiumBy
+from test.functional.mac.helper.test_helper import BaseTestCase
+from test.functional.test_helper import wait_for_element
 
-from .helper.test_helper import BaseTestCase
 
-
-class KeyboardTests(BaseTestCase):
-    def test_press_keycode(self):
-        # not sure how to test this.
-        self.driver.press_keycode(176)
-
-    def test_long_press_keycode(self):
-        # not sure how to test this.
-        self.driver.long_press_keycode(176)
-
-
-if __name__ == '__main__':
-    suite = unittest.TestLoader().loadTestsFromTestCase(KeyboardTests)
-    unittest.TextTestRunner(verbosity=2).run(suite)
+class TestWebElement(BaseTestCase):
+    def test_clear_text_field(self) -> None:
+        edit_field = wait_for_element(self.driver, AppiumBy.CLASS_NAME, 'XCUIElementTypeTextView')
+        edit_field.send_keys('helloworld')
+        assert edit_field.text == 'helloworld'
+        edit_field.clear()
+        assert edit_field.text == ''
diff --git a/test/functional/test_helper.py b/test/functional/test_helper.py
index a531d0532..817f833ea 100644
--- a/test/functional/test_helper.py
+++ b/test/functional/test_helper.py
@@ -1,33 +1,97 @@
 import os
 import socket
+import time
+from time import sleep
+from typing import TYPE_CHECKING, Any, Callable
+
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.support.ui import WebDriverWait
+
+if TYPE_CHECKING:
+    from appium.webdriver.webdriver import WebDriver
+    from appium.webdriver.webelement import WebElement
 
 
 class NoAvailablePortError(Exception):
     pass
 
 
-def get_available_from_port_range(from_port, to_port):
+def get_available_from_port_range(from_port: int, to_port: int) -> int:
     """Returns available local port number.
+
+    Args:
+        from_port: The start port to search
+        to_port: The end port to search
+
+    Returns:
+        int: available local port number which are found first
+
     """
-    r = range(from_port, to_port)
 
     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
-    for port in r:
+    for port in range(from_port, to_port):
         try:
             if sock.connect_ex(('localhost', port)) != 0:
                 return port
         finally:
             sock.close()
 
-    raise NoAvailablePortError('No available port between {} and {}'.format(
-        from_port, to_port))
+    raise NoAvailablePortError(f'No available port between {from_port} and {to_port}')
 
 
-def is_ci():
+def is_ci() -> bool:
     """Returns if current execution is running on CI
 
     Returns:
-        bool: `True` if current executions is on CI
+        `True` if current executions is on CI
     """
     return os.getenv('CI', 'false') == 'true'
+
+
+def wait_for_condition(method: Callable, timeout_sec: float = 5, interval_sec: float = 1) -> Any:
+    """Wait while `method` returns the built-in objects considered false
+
+    https://docs.python.org/3/library/stdtypes.html#truth-value-testing
+
+    Args:
+        method: The target method to be waited
+        timeout: The timeout to be waited (sec.)
+        interval_sec: The interval for wait (sec.)
+
+    Returns:
+        Any: value which `method` returns
+
+    Raises:
+        ValueError: When interval isn't more than 0
+
+    """
+    if interval_sec < 0:
+        raise ValueError('interval_sec needs to be not less than 0')
+
+    started = time.time()
+    while time.time() - started <= timeout_sec:
+        result = method()
+        if result:
+            break
+        sleep(interval_sec)
+    return result
+
+
+def wait_for_element(driver: 'WebDriver', locator: str, value: str, timeout_sec: float = 10) -> 'WebElement':
+    """Wait until the element located
+
+    Args:
+        driver: WebDriver instance
+        locator: Locator like WebDriver, Mobile JSON Wire Protocol
+            (e.g. `appium.webdriver.common.appiumby.AppiumBy.ACCESSIBILITY_ID`)
+        value: Query value to locator
+        timeout_sec: Maximum time to wait the element. If time is over, `TimeoutException` is thrown
+
+    Raises:
+        `selenium.common.exceptions.TimeoutException`
+
+    Returns:
+        The found WebElement
+    """
+    return WebDriverWait(driver, timeout_sec).until(EC.presence_of_element_located((locator, value)))
diff --git a/test/helpers/__init__.py b/test/helpers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/helpers/constants.py b/test/helpers/constants.py
new file mode 100644
index 000000000..9f453add8
--- /dev/null
+++ b/test/helpers/constants.py
@@ -0,0 +1 @@
+SERVER_URL_BASE = 'http://127.0.0.1:4723'
diff --git a/test/unit/helper/test_helper.py b/test/unit/helper/test_helper.py
index f7ff3ca0c..58e358176 100644
--- a/test/unit/helper/test_helper.py
+++ b/test/unit/helper/test_helper.py
@@ -13,25 +13,35 @@
 # limitations under the License.
 
 import json
+from typing import TYPE_CHECKING, Any, Dict
 
 import httpretty
 
 from appium import webdriver
+from appium.options.android import UiAutomator2Options
+from appium.options.ios import XCUITestOptions
+from test.helpers.constants import SERVER_URL_BASE
 
-# :return: A string of test URL
-SERVER_URL_BASE = 'http://localhost:4723/wd/hub'
+if TYPE_CHECKING:
+    from httpretty.core import HTTPrettyRequestEmpty
 
+    from appium.webdriver.webdriver import WebDriver
 
-def appium_command(command):
+
+def appium_command(command: str) -> str:
     """Return a command of Appium
 
     Returns:
         str: A string of command URL
     """
-    return '{}{}'.format(SERVER_URL_BASE, command)
+    return (
+        f'{SERVER_URL_BASE}{command}'
+        if SERVER_URL_BASE.endswith('/') or command.startswith('/')
+        else f'{SERVER_URL_BASE}/{command}'
+    )
 
 
-def android_w3c_driver():
+def android_w3c_driver() -> 'WebDriver':
     """Return a W3C driver which is generated by a mock response for Android
 
     Returns:
@@ -40,53 +50,75 @@ def android_w3c_driver():
 
     response_body_json = json.dumps(
         {
-            'value': {
-                'sessionId': '1234567890',
-                'capabilities': {
-                    'platform': 'LINUX',
-                    'desired': {
-                        'platformName': 'Android',
-                        'automationName': 'uiautomator2',
-                        'platformVersion': '7.1.1',
-                        'deviceName': 'Android Emulator',
-                        'app': '/test/apps/ApiDemos-debug.apk',
-                    },
+            'sessionId': '1234567890',
+            'capabilities': {
+                'platform': 'LINUX',
+                'desired': {
                     'platformName': 'Android',
                     'automationName': 'uiautomator2',
                     'platformVersion': '7.1.1',
-                    'deviceName': 'emulator-5554',
+                    'deviceName': 'Android Emulator',
                     'app': '/test/apps/ApiDemos-debug.apk',
-                    'deviceUDID': 'emulator-5554',
-                    'appPackage': 'io.appium.android.apis',
-                    'appWaitPackage': 'io.appium.android.apis',
-                    'appActivity': 'io.appium.android.apis.ApiDemos',
-                    'appWaitActivity': 'io.appium.android.apis.ApiDemos'
-                }
-            }
+                },
+                'platformName': 'Android',
+                'automationName': 'uiautomator2',
+                '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',
+                'appActivity': 'io.appium.android.apis.ApiDemos',
+                'appWaitActivity': 'io.appium.android.apis.ApiDemos',
+            },
         }
     )
 
-    httpretty.register_uri(
-        httpretty.POST,
-        appium_command('/session'),
-        body=response_body_json
-    )
+    httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json)
 
     desired_caps = {
         'platformName': 'Android',
         'deviceName': 'Android Emulator',
         'app': 'path/to/app',
-        'automationName': 'UIAutomator2'
+        'automationName': 'UIAutomator2',
     }
 
-    driver = webdriver.Remote(
-        SERVER_URL_BASE,
-        desired_caps
+    driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps))
+    return driver
+
+
+def ios_w3c_driver() -> 'WebDriver':
+    """Return a W3C driver which is generated by a mock response for iOS
+
+    Returns:
+        `webdriver.webdriver.WebDriver`: An instance of WebDriver
+    """
+    response_body_json = json.dumps(
+        {
+            'sessionId': '1234567890',
+            'capabilities': {
+                'device': 'iphone',
+                'browserName': 'UICatalog',
+                'sdkVersion': '11.4',
+                'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog',
+            },
+        }
     )
+
+    httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json)
+
+    desired_caps = {
+        'platformName': 'iOS',
+        'deviceName': 'iPhone Simulator',
+        'app': 'path/to/app',
+        'automationName': 'XCUITest',
+    }
+
+    driver = webdriver.Remote(SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps))
     return driver
 
 
-def ios_w3c_driver():
+def ios_w3c_driver_with_extensions(extensions) -> 'WebDriver':
     """Return a W3C driver which is generated by a mock response for iOS
 
     Returns:
@@ -95,38 +127,78 @@ def ios_w3c_driver():
 
     response_body_json = json.dumps(
         {
-            'value': {
-                'sessionId': '1234567890',
-                'capabilities': {
-                    'device': 'iphone',
-                    'browserName': 'UICatalog',
-                    'sdkVersion': '11.4',
-                    'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog'
-                }
-            }
+            'sessionId': '1234567890',
+            'capabilities': {
+                'device': 'iphone',
+                'browserName': 'UICatalog',
+                'sdkVersion': '11.4',
+                'CFBundleIdentifier': 'com.example.apple-samplecode.UICatalog',
+            },
         }
     )
 
-    httpretty.register_uri(
-        httpretty.POST,
-        appium_command('/session'),
-        body=response_body_json
-    )
+    httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json)
 
     desired_caps = {
         'platformName': 'iOS',
         'deviceName': 'iPhone Simulator',
         'app': 'path/to/app',
-        'automationName': 'XCUITest'
+        'automationName': 'XCUITest',
     }
 
-    driver = webdriver.Remote(
-        SERVER_URL_BASE,
-        desired_caps
+    driver = webdriver.Remote(SERVER_URL_BASE, options=XCUITestOptions().load_capabilities(desired_caps), extensions=extensions)
+    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):
+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/app_test.py b/test/unit/webdriver/app_test.py
index ad0522264..347675358 100644
--- a/test/unit/webdriver/app_test.py
+++ b/test/unit/webdriver/app_test.py
@@ -11,137 +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
-)
-
-
-class TestWebDriverApp(object):
-
-    @httpretty.activate
-    def test_reset(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/app/reset'),
-            body='{"value": ""}'
-        )
-        result = driver.reset()
-
-        assert {'sessionId': '1234567890'}, get_httpretty_request_body(httpretty.last_request())
-        assert isinstance(result, WebDriver)
-
-    @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": ""}'
-        )
-        result = driver.install_app('path/to/app')
-
-        assert {'app': 'path/to/app'}, 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": ""}'
-        )
-        result = driver.remove_app('com.app.id')
-
-        assert {'app': 'com.app.id'}, 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}'
-        )
-        result = driver.is_app_installed("com.app.id")
-        assert {'app': "com.app.id"}, 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}'
-        )
-        result = driver.terminate_app("com.app.id")
-        assert {'app': "com.app.id"}, 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": ""}'
-        )
-        result = driver.activate_app("com.app.id")
-
-        assert {'app': 'com.app.id'}, 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": ""}'
-        )
-        result = driver.background_app(0)
-        assert {'app': 0}, get_httpretty_request_body(httpretty.last_request())
-        assert isinstance(result, WebDriver)
-
-    @httpretty.activate
-    def test_launch_app(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/app/launch'),
-            body='{"value": }'
-        )
-        assert isinstance(driver.launch_app(), WebDriver)
-
-    @httpretty.activate
-    def test_close_app(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/app/close'),
-            body='{"value": }'
-        )
-        assert isinstance(driver.close_app(), 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 }'
-        )
-        result = driver.query_app_state('com.app.id')
-
-        assert {'app': 3}, get_httpretty_request_body(httpretty.last_request())
-        assert result is ApplicationState.RUNNING_IN_BACKGROUND
+from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver
+
+
+@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
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/context_test.py b/test/unit/webdriver/context_test.py
index 150fddac6..80c2c0f78 100644
--- a/test/unit/webdriver/context_test.py
+++ b/test/unit/webdriver/context_test.py
@@ -14,17 +14,39 @@
 
 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"}'
+            httpretty.GET, appium_command('/session/1234567890/contexts'), body='{"value": ["NATIVE_APP", "CHROMIUM"]}'
         )
-        assert driver.current_context == 'NATIVE'
+
+        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/device/activities_test.py b/test/unit/webdriver/device/activities_test.py
index 01c0210e0..2374ba6cc 100644
--- a/test/unit/webdriver/device/activities_test.py
+++ b/test/unit/webdriver/device/activities_test.py
@@ -14,65 +14,22 @@
 
 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
 
 
 class TestWebDriverActivities(object):
-
-    @httpretty.activate
-    def test_start_activity(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/device/start_activity'),
-            body='{"value": ""}'
-        )
-        driver.start_activity('com.example.myapp', '.ExampleActivity')
-
-        d = get_httpretty_request_body(httpretty.last_request())
-        assert d['appPackage'] == 'com.example.myapp'
-        assert d['appActivity'] == '.ExampleActivity'
-
-    @httpretty.activate
-    def test_start_activity_with_opts(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/device/start_activity'),
-            body='{"value": ""}'
-        )
-        driver.start_activity(
-            app_package='com.example.myapp',
-            app_activity='.ExampleActivity',
-            app_wait_package='com.example.waitapp',
-            intent_action='android.intent.action.MAIN',
-            intent_category='android.intent.category.LAUNCHER',
-            intent_flags='0x10200000',
-            optional_intent_arguments='--es "activity" ".ExampleActivity"',
-            dont_stop_app_on_reset=True
-        )
-
-        d = get_httpretty_request_body(httpretty.last_request())
-        assert d['appPackage'] == 'com.example.myapp'
-        assert d['appActivity'] == '.ExampleActivity'
-        assert d['appWaitPackage'] == 'com.example.waitapp'
-        assert d['intentAction'] == 'android.intent.action.MAIN'
-        assert d['intentCategory'] == 'android.intent.category.LAUNCHER'
-        assert d['intentFlags'] == '0x10200000'
-        assert d['optionalIntentArguments'] == '--es "activity" ".ExampleActivity"'
-        assert d['dontStopAppOnReset'] is True
-
     @httpretty.activate
     def test_current_activity(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890/appium/device/current_activity'),
-            body='{"value": ".ExampleActivity"}'
+            body='{"value": ".ExampleActivity"}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": ".ExampleActivity"}',
         )
         assert driver.current_activity == '.ExampleActivity'
 
@@ -82,6 +39,11 @@ def test_wait_activity(self):
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890/appium/device/current_activity'),
-            body='{"value": ".ExampleActivity"}'
+            body='{"value": ".ExampleActivity"}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": ".ExampleActivity"}',
         )
         assert driver.wait_activity('.ExampleActivity', 1) is True
diff --git a/test/unit/webdriver/device/clipboard_test.py b/test/unit/webdriver/device/clipboard_test.py
index 22c80dbea..7e21797a0 100644
--- a/test/unit/webdriver/device/clipboard_test.py
+++ b/test/unit/webdriver/device/clipboard_test.py
@@ -14,44 +14,42 @@
 
 import httpretty
 
-from appium.common.helper import appium_bytes
 from appium.webdriver.clipboard_content_type import ClipboardContentType
-from test.unit.helper.test_helper import (
-    android_w3c_driver,
-    appium_command,
-    get_httpretty_request_body,
-    ios_w3c_driver
-)
+from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body, ios_w3c_driver
 
 
 class TestWebDriverClipboard(object):
-
     @httpretty.activate
     def test_set_clipboard_with_url(self):
         driver = android_w3c_driver()
+        httpretty.register_uri(
+            httpretty.POST, appium_command('/session/1234567890/appium/device/set_clipboard'), body='{"value": ""}'
+        )
         httpretty.register_uri(
             httpretty.POST,
-            appium_command('/session/1234567890/appium/device/set_clipboard'),
-            body='{"value": ""}'
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": ""}',
         )
-        driver.set_clipboard(appium_bytes(str('http://appium.io/'), 'UTF-8'),
-                             ClipboardContentType.URL, 'label for android')
+        driver.set_clipboard(bytes(str('http://appium.io/'), 'UTF-8'), ClipboardContentType.URL, 'label for android')
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['content'] == 'aHR0cDovL2FwcGl1bS5pby8='
-        assert d['contentType'] == 'url'
-        assert d['label'] == 'label for android'
+        assert d['args'][0]['content'] == 'aHR0cDovL2FwcGl1bS5pby8='
+        assert d['args'][0]['contentType'] == 'url'
+        assert d['args'][0]['label'] == 'label for android'
 
     @httpretty.activate
     def test_set_clipboard_text(self):
         driver = ios_w3c_driver()
+        httpretty.register_uri(
+            httpretty.POST, appium_command('/session/1234567890/appium/device/set_clipboard'), body='{"value": ""}'
+        )
         httpretty.register_uri(
             httpretty.POST,
-            appium_command('/session/1234567890/appium/device/set_clipboard'),
-            body='{"value": ""}'
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": ""}',
         )
         driver.set_clipboard_text('hello')
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['content'] == 'aGVsbG8='
-        assert d['contentType'] == 'plaintext'
+        assert d['args'][0]['content'] == 'aGVsbG8='
+        assert d['args'][0]['contentType'] == 'plaintext'
diff --git a/test/unit/webdriver/device/common_test.py b/test/unit/webdriver/device/common_test.py
index 395979b93..43b069a08 100644
--- a/test/unit/webdriver/device/common_test.py
+++ b/test/unit/webdriver/device/common_test.py
@@ -15,19 +15,16 @@
 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):
@@ -35,6 +32,11 @@ def test_current_package(self):
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890/appium/device/current_package'),
-            body='{"value": ".ExamplePackage"}'
+            body='{"value": ".ExamplePackage"}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": ".ExamplePackage"}',
         )
         assert driver.current_package == '.ExamplePackage'
diff --git a/test/unit/webdriver/device/device_time_test.py b/test/unit/webdriver/device/device_time_test.py
index 95c9f6a5d..8c6f5e0b9 100644
--- a/test/unit/webdriver/device/device_time_test.py
+++ b/test/unit/webdriver/device/device_time_test.py
@@ -14,22 +14,22 @@
 
 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
 
 
 class TestWebDriverDeviceTime(object):
-
     @httpretty.activate
     def test_device_time(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890/appium/device/system_time'),
-            body='{"value": "2019-01-05T14:46:44+09:00"}'
+            body='{"value": "2019-01-05T14:46:44+09:00"}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": "2019-01-05T14:46:44+09:00"}',
         )
         assert driver.device_time == '2019-01-05T14:46:44+09:00'
 
@@ -39,7 +39,12 @@ def test_get_device_time(self):
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890/appium/device/system_time'),
-            body='{"value": "2019-01-05T14:46:44+09:00"}'
+            body='{"value": "2019-01-05T14:46:44+09:00"}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": "2019-01-05T14:46:44+09:00"}',
         )
         assert driver.get_device_time() == '2019-01-05T14:46:44+09:00'
 
@@ -49,9 +54,14 @@ def test_get_formatted_device_time(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/system_time'),
-            body='{"value": "2019-01-08"}'
+            body='{"value": "2019-01-08"}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": "2019-01-08"}',
         )
         assert driver.get_device_time('YYYY-MM-DD') == '2019-01-08'
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['format'] == 'YYYY-MM-DD'
+        assert d.get('format', d['args'][0]['format']) == 'YYYY-MM-DD'
diff --git a/test/unit/webdriver/device/display_test.py b/test/unit/webdriver/device/display_test.py
index cd78b0447..bce0b4e43 100644
--- a/test/unit/webdriver/device/display_test.py
+++ b/test/unit/webdriver/device/display_test.py
@@ -18,13 +18,11 @@
 
 
 class TestWebDriverDisplay(object):
-
     @httpretty.activate
     def test_get_display_density(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/appium/device/display_density'),
-            body='{"value": 560}'
+            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}')
         assert driver.get_display_density() == 560
diff --git a/test/unit/webdriver/device/fingerprint_test.py b/test/unit/webdriver/device/fingerprint_test.py
index cd5530039..e6facf86d 100644
--- a/test/unit/webdriver/device/fingerprint_test.py
+++ b/test/unit/webdriver/device/fingerprint_test.py
@@ -14,15 +14,11 @@
 
 import httpretty
 
-from test.unit.helper.test_helper import (
-    android_w3c_driver,
-    appium_command,
-    get_httpretty_request_body
-)
+from appium.webdriver.webdriver import WebDriver
+from test.unit.helper.test_helper import android_w3c_driver, appium_command, get_httpretty_request_body
 
 
 class TestWebDriverFingerprint(object):
-
     @httpretty.activate
     def test_finger_print(self):
         driver = android_w3c_driver()
@@ -31,8 +27,13 @@ def test_finger_print(self):
             appium_command('/session/1234567890/appium/device/finger_print'),
             # body is None
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            # body is None
+        )
 
-        assert driver.finger_print(1) is None
+        assert isinstance(driver.finger_print(1), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['fingerprintId'] == 1
+        assert d.get('fingerprintId', d['args'][0]['fingerprintId']) == 1
diff --git a/test/unit/webdriver/device/gsm_test.py b/test/unit/webdriver/device/gsm_test.py
index f8e44df34..cd776a94a 100644
--- a/test/unit/webdriver/device/gsm_test.py
+++ b/test/unit/webdriver/device/gsm_test.py
@@ -14,21 +14,12 @@
 
 import httpretty
 
-from appium.webdriver.extensions.android.gsm import (
-    GsmCallActions,
-    GsmSignalStrength,
-    GsmVoiceState
-)
+from appium.webdriver.extensions.android.gsm import GsmCallActions, GsmSignalStrength, GsmVoiceState
 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
 
 
 class TestWebDriveGsm(object):
-
     @httpretty.activate
     def test_make_gsm_call(self):
         driver = android_w3c_driver()
@@ -36,11 +27,15 @@ def test_make_gsm_call(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/gsm_call'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.make_gsm_call('5551234567', GsmCallActions.CALL), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['phoneNumber'] == '5551234567'
-        assert d['action'] == GsmCallActions.CALL
+        assert d['args'][0]['phoneNumber'] == '5551234567'
+        assert d['args'][0]['action'] == GsmCallActions.CALL
 
     @httpretty.activate
     def test_set_gsm_signal(self):
@@ -49,11 +44,14 @@ def test_set_gsm_signal(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/gsm_signal'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.set_gsm_signal(GsmSignalStrength.GREAT), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['signalStrength'] == GsmSignalStrength.GREAT
-        assert d['signalStrengh'] == GsmSignalStrength.GREAT
+        assert d['args'][0]['strength'] == GsmSignalStrength.GREAT
 
     @httpretty.activate
     def test_set_gsm_voice(self):
@@ -62,7 +60,11 @@ def test_set_gsm_voice(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/gsm_voice'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.set_gsm_voice(GsmVoiceState.ROAMING), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['state'] == GsmVoiceState.ROAMING
+        assert d['args'][0]['state'] == GsmVoiceState.ROAMING
diff --git a/test/unit/webdriver/device/keyboard_test.py b/test/unit/webdriver/device/keyboard_test.py
index faf9743a8..526f1826f 100644
--- a/test/unit/webdriver/device/keyboard_test.py
+++ b/test/unit/webdriver/device/keyboard_test.py
@@ -15,35 +15,27 @@
 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()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/device/hide_keyboard')
-        )
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/appium/device/hide_keyboard'))
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'))
         assert isinstance(driver.hide_keyboard(), WebDriver)
 
     @httpretty.activate
     def test_press_keycode(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/device/press_keycode'),
-            body='{"value": "86"}'
+            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"}')
         driver.press_keycode(86)
         d = get_httpretty_request_body((httpretty.last_request()))
-        assert d['keycode'] == 86
+        assert d.get('keycode', d['args'][0]['keycode']) == 86
 
     @httpretty.activate
     def test_long_press_keycode(self):
@@ -51,20 +43,20 @@ def test_long_press_keycode(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/long_press_keycode'),
-            body='{"value": "86"}'
+            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['keycode'] == 86
+        assert d.get('keycode', d['args'][0]['keycode']) == 86
 
     @httpretty.activate
     def test_keyevent(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/device/keyevent'),
-            body='{keycode: 86}'
+            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"}')
         assert isinstance(driver.keyevent(86), WebDriver)
 
     @httpretty.activate
@@ -73,15 +65,15 @@ def test_press_keycode_with_flags(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/press_keycode'),
-            body='{keycode: 86, metastate: 2097153, flags: 44}'
+            body='{keycode: 86, metastate: 2097153, flags: 44}',
         )
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'))
         # metastate is META_SHIFT_ON and META_NUM_LOCK_ON
         # flags is CANCELFLAG_CANCELEDED, FLAG_KEEP_TOUCH_MODE, FLAG_FROM_SYSTEM
         assert isinstance(
-            driver.press_keycode(
-                86, metastate=[
-                    0x00000001, 0x00200000], flags=[
-                    0x20, 0x00000004, 0x00000008]), WebDriver)
+            driver.press_keycode(86, metastate=0x00000001 | 0x00200000, flags=0x20 | 0x00000004 | 0x00000008),
+            WebDriver,
+        )
 
     @httpretty.activate
     def test_long_press_keycode_with_flags(self):
@@ -89,12 +81,56 @@ def test_long_press_keycode_with_flags(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/long_press_keycode'),
-            body='{keycode: 86, metastate: 2097153, flags: 44}'
+            body='{keycode: 86, metastate: 2097153, flags: 44}',
         )
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/execute/sync'))
         # metastate is META_SHIFT_ON and META_NUM_LOCK_ON
         # flags is CANCELFLAG_CANCELEDED, FLAG_KEEP_TOUCH_MODE, FLAG_FROM_SYSTEM
         assert isinstance(
-            driver.long_press_keycode(
-                86, metastate=[
-                    0x00000001, 0x00200000], flags=[
-                    0x20, 0x00000004, 0x00000008]), WebDriver)
+            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())
+
+    @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/device/location_test.py b/test/unit/webdriver/device/location_test.py
index 8e25e4a14..fb0112bc5 100644
--- a/test/unit/webdriver/device/location_test.py
+++ b/test/unit/webdriver/device/location_test.py
@@ -15,75 +15,76 @@
 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
 
 FLT_EPSILON = 1e-9
 
 
 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)
 
     @httpretty.activate
     def test_set_location_float(self):
         driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/location')
-        )
-        assert isinstance(driver.set_location(11.1, 22.2, 33.3), WebDriver)
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location'))
+        assert isinstance(driver.set_location(11.1, 22.2, 33.3, 23.2, 12), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert abs(d['location']['latitude'] - 11.1) <= FLT_EPSILON
         assert abs(d['location']['longitude'] - 22.2) <= FLT_EPSILON
         assert abs(d['location']['altitude'] - 33.3) <= FLT_EPSILON
+        assert abs(d['location']['speed'] - 23.2) <= FLT_EPSILON
+        assert abs(d['location']['satellites'] - 12) <= FLT_EPSILON
 
     @httpretty.activate
     def test_set_location_str(self):
         driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/location')
-        )
-        assert isinstance(driver.set_location('11.1', '22.2', '33.3'), WebDriver)
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location'))
+        assert isinstance(driver.set_location('11.1', '22.2', '33.3', '23.2', '12'), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert d['location']['latitude'] == '11.1'
         assert d['location']['longitude'] == '22.2'
         assert d['location']['altitude'] == '33.3'
+        assert d['location']['speed'] == '23.2'
+        assert d['location']['satellites'] == '12'
 
     @httpretty.activate
     def test_set_location_without_altitude(self):
         driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/location')
-        )
-        assert isinstance(driver.set_location(11.1, 22.2), WebDriver)
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location'))
+        assert isinstance(driver.set_location(11.1, 22.2, speed=23.2), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert abs(d['location']['latitude'] - 11.1) <= FLT_EPSILON
         assert abs(d['location']['longitude'] - 22.2) <= FLT_EPSILON
+        assert abs(d['location']['speed'] - 23.2) <= FLT_EPSILON
         assert d['location'].get('altitude') is None
 
+    @httpretty.activate
+    def test_set_location_without_speed(self):
+        driver = android_w3c_driver()
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/location'))
+        assert isinstance(driver.set_location(11.1, 22.2, 33.3), WebDriver)
+
+        d = get_httpretty_request_body(httpretty.last_request())
+        assert abs(d['location']['latitude'] - 11.1) <= FLT_EPSILON
+        assert abs(d['location']['longitude'] - 22.2) <= FLT_EPSILON
+        assert abs(d['location']['altitude'] - 33.3) <= FLT_EPSILON
+        assert d['location'].get('speed') is None
+
     @httpretty.activate
     def test_location(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890/location'),
-            body='{"value": {"latitude": 11.1, "longitude": 22.2, "altitude": 33.3}}'
+            body='{"value": {"latitude": 11.1, "longitude": 22.2, "altitude": 33.3}}',
         )
         val = driver.location
         assert abs(val['latitude'] - 11.1) <= FLT_EPSILON
diff --git a/test/unit/webdriver/device/lock_test.py b/test/unit/webdriver/device/lock_test.py
index 31ec5d2ce..b54698543 100644
--- a/test/unit/webdriver/device/lock_test.py
+++ b/test/unit/webdriver/device/lock_test.py
@@ -15,67 +15,117 @@
 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()
-        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)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['seconds'] == 1
+        assert d.get('seconds', d['args'][0]['seconds']) == 1
 
     @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()
 
-        d = get_httpretty_request_body(httpretty.last_request())
-        assert d == {}
-
     @httpretty.activate
     def test_islocked_false(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/device/is_locked'),
-            body='{"value": false}'
+            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 = android_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 = android_w3c_driver()
         httpretty.register_uri(
             httpretty.POST,
-            appium_command('/session/1234567890/appium/device/is_locked'),
-            body='{"value": true}'
+            appium_command('/session/1234567890/appium/device/unlock'),
+        )
+        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 = android_w3c_driver()
+        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_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)
+        assert {'script': 'mobile: enrollBiometric', 'args': [{'isEnabled': True}]} == get_httpretty_request_body(
+            httpretty.last_request()
+        )
diff --git a/test/unit/webdriver/device/power_test.py b/test/unit/webdriver/device/power_test.py
index e9b9e67b3..2d87a8d4c 100644
--- a/test/unit/webdriver/device/power_test.py
+++ b/test/unit/webdriver/device/power_test.py
@@ -16,15 +16,10 @@
 
 from appium.webdriver.extensions.android.power import Power
 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
 
 
 class TestWebDriverPower(object):
-
     @httpretty.activate
     def test_set_power_capacity(self):
         driver = android_w3c_driver()
@@ -32,10 +27,14 @@ def test_set_power_capacity(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/power_capacity'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.set_power_capacity(50), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['percent'] == 50
+        assert d['args'][0]['percent'] == 50
 
     @httpretty.activate
     def test_set_power_ac(self):
@@ -44,7 +43,11 @@ def test_set_power_ac(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/power_ac'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.set_power_ac(Power.AC_ON), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['state'] == Power.AC_ON
+        assert d['args'][0]['state'] == Power.AC_ON
diff --git a/test/unit/webdriver/device/remote_fs_test.py b/test/unit/webdriver/device/remote_fs_test.py
index 0855b936b..4de3a85fe 100644
--- a/test/unit/webdriver/device/remote_fs_test.py
+++ b/test/unit/webdriver/device/remote_fs_test.py
@@ -18,17 +18,11 @@
 import pytest
 from selenium.common.exceptions import InvalidArgumentException
 
-from appium.common.helper import appium_bytes
 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
 
 
 class TestWebDriverRemoteFs(object):
-
     @httpretty.activate
     def test_push_file(self):
         driver = android_w3c_driver()
@@ -36,14 +30,18 @@ def test_push_file(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/push_file'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         dest_path = '/path/to/file.txt'
-        data = base64.b64encode(appium_bytes('HelloWorld', 'utf-8')).decode('utf-8')
+        data = base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8')
 
         assert isinstance(driver.push_file(dest_path, data), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['path'] == dest_path
-        assert d['data'] == str(data)
+        assert d.get('path', d['args'][0]['remotePath']) == dest_path
+        assert d.get('data', d['args'][0]['payload']) == str(data)
 
     @httpretty.activate
     def test_push_file_invalid_arg_exception_without_src_path_and_base64data(self):
@@ -52,6 +50,10 @@ def test_push_file_invalid_arg_exception_without_src_path_and_base64data(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/push_file'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         dest_path = '/path/to/file.txt'
 
         with pytest.raises(InvalidArgumentException):
@@ -64,6 +66,10 @@ def test_push_file_invalid_arg_exception_with_src_file_not_found(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/push_file'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/appium/device/push_file'),
+        )
         dest_path = '/dest_path/to/file.txt'
         src_path = '/src_path/to/file.txt'
 
@@ -76,14 +82,19 @@ def test_pull_file(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/pull_file'),
-            body='{"value": "SGVsbG9Xb3JsZA=="}'
+            body='{"value": "SGVsbG9Xb3JsZA=="}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": "SGVsbG9Xb3JsZA=="}',
         )
         dest_path = '/path/to/file.txt'
 
-        assert driver.pull_file(dest_path) == str(base64.b64encode(appium_bytes('HelloWorld', 'utf-8')).decode('utf-8'))
+        assert driver.pull_file(dest_path) == str(base64.b64encode(bytes('HelloWorld', 'utf-8')).decode('utf-8'))
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['path'] == dest_path
+        assert d.get('path', d['args'][0]['remotePath']) == dest_path
 
     @httpretty.activate
     def test_pull_folder(self):
@@ -91,11 +102,16 @@ def test_pull_folder(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/pull_folder'),
-            body='{"value": "base64EncodedZippedFolderData"}'
+            body='{"value": "base64EncodedZippedFolderData"}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": "base64EncodedZippedFolderData"}',
         )
         dest_path = '/path/to/file.txt'
 
         assert driver.pull_folder(dest_path) == 'base64EncodedZippedFolderData'
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['path'] == dest_path
+        assert d.get('path', d['args'][0]['remotePath']) == dest_path
diff --git a/test/unit/webdriver/device/shake_test.py b/test/unit/webdriver/device/shake_test.py
index 7604bec22..0371852f1 100644
--- a/test/unit/webdriver/device/shake_test.py
+++ b/test/unit/webdriver/device/shake_test.py
@@ -20,7 +20,6 @@
 
 
 class TestWebDriverShake(object):
-
     @httpretty.activate
     def test_shake(self):
         driver = android_w3c_driver()
@@ -28,4 +27,8 @@ def test_shake(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/shake'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.shake(), WebDriver)
diff --git a/test/unit/webdriver/device/sms_test.py b/test/unit/webdriver/device/sms_test.py
index 48ab371c8..1323f50c4 100644
--- a/test/unit/webdriver/device/sms_test.py
+++ b/test/unit/webdriver/device/sms_test.py
@@ -15,15 +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
 
 
 class TestWebDriverSms(object):
-
     @httpretty.activate
     def test_send_sms(self):
         driver = android_w3c_driver()
@@ -31,8 +26,12 @@ def test_send_sms(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/send_sms'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.send_sms('555-123-4567', 'Hey lol'), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['phoneNumber'] == '555-123-4567'
-        assert d['message'] == 'Hey lol'
+        assert d['args'][0]['phoneNumber'] == '555-123-4567'
+        assert d['args'][0]['message'] == 'Hey lol'
diff --git a/test/unit/webdriver/device/system_bars_test.py b/test/unit/webdriver/device/system_bars_test.py
index 0373cba9c..79e7c680c 100644
--- a/test/unit/webdriver/device/system_bars_test.py
+++ b/test/unit/webdriver/device/system_bars_test.py
@@ -18,18 +18,23 @@
 
 
 class TestWebDriverSystemBars(object):
-
     @httpretty.activate
     def test_get_system_bars(self):
         driver = android_w3c_driver()
+        body = """{"value":
+             {"statusBar":
+             {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 1920},
+             "navigationBar":
+             {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 126}}}"""
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890/appium/device/system_bars'),
-            body=''' {"value":
-                     {"statusBar":
-                     {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 1920},
-                     "navigationBar":
-                     {"visible": true, "x": 0, "y": 0, "width": 1080, "height": 126}}}'''
+            body=body,
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body=body,
         )
         d = driver.get_system_bars()
 
diff --git a/test/unit/webdriver/execute_driver_test.py b/test/unit/webdriver/execute_driver_test.py
index 3f438c52a..250b8cc84 100644
--- a/test/unit/webdriver/execute_driver_test.py
+++ b/test/unit/webdriver/execute_driver_test.py
@@ -16,15 +16,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
 
 
 class TestWebDriverExecuteDriver(object):
-
     @httpretty.activate
     def test_batch(self):
         driver = android_w3c_driver()
@@ -35,7 +30,7 @@ def test_batch(self):
             '{"element-6066-11e4-a52e-4f735466cecf":"39000000-0000-0000-D39A-000000000000",'
             '"ELEMENT":"39000000-0000-0000-D39A-000000000000"},'
             '{"y":237,"x":18,"width":67,"height":24}],"logs":{'
-            '"error":[],"warn":["warning message"],"log":[]}}}'
+            '"error":[],"warn":["warning message"],"log":[]}}}',
         )
 
         script = """
@@ -65,7 +60,7 @@ def test_batch_with_timeout(self):
             '{"element-6066-11e4-a52e-4f735466cecf":"39000000-0000-0000-D39A-000000000000",'
             '"ELEMENT":"39000000-0000-0000-D39A-000000000000"},'
             '{"y":237,"x":18,"width":67,"height":24}],"logs":{'
-            '"error":[],"warn":["warning message"],"log":[]}}}'
+            '"error":[],"warn":["warning message"],"log":[]}}}',
         )
 
         script = """
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 000000000..8896d86f6
Binary files /dev/null and b/test/unit/webdriver/flutter_integration/file/success_qr.png differ
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..50a42e599
--- /dev/null
+++ b/test/unit/webdriver/flutter_integration/flutter_actions_test.py
@@ -0,0 +1,157 @@
+#!/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..0e97d81cd
--- /dev/null
+++ b/test/unit/webdriver/flutter_integration/flutter_integration_driver_test.py
@@ -0,0 +1,60 @@
+#!/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_render_tree_test.py b/test/unit/webdriver/flutter_integration/flutter_render_tree_test.py
new file mode 100644
index 000000000..1d11b5f8b
--- /dev/null
+++ b/test/unit/webdriver/flutter_integration/flutter_render_tree_test.py
@@ -0,0 +1,95 @@
+#!/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.webdriver.extensions.flutter_integration.flutter_commands import (
+    FlutterCommand,
+)
+from test.unit.helper.test_helper import (
+    appium_command,
+    flutter_w3c_driver,
+    get_httpretty_request_body,
+)
+
+
+class TestFlutterRenderTree:
+    @httpretty.activate
+    def test_get_render_tree_with_all_filters(self):
+        expected_body = [{'<partial_tree>': 'LoginButton'}]
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body=json.dumps({'value': expected_body}),
+        )
+
+        driver = flutter_w3c_driver()
+        flutter = FlutterCommand(driver)
+
+        result = flutter.get_render_tree(widget_type='ElevatedButton', key='LoginButton', text='Login')
+
+        request_body = get_httpretty_request_body(httpretty.last_request())
+        assert request_body['script'] == 'flutter: renderTree'
+        assert request_body['args'] == [
+            {
+                'widgetType': 'ElevatedButton',
+                'key': 'LoginButton',
+                'text': 'Login',
+            }
+        ]
+
+        assert result == expected_body
+
+    @httpretty.activate
+    def test_get_render_tree_with_partial_filters(self):
+        expected_body = [{'<partial_tree>': 'LoginScreen'}]
+
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body=json.dumps({'value': expected_body}),
+        )
+
+        driver = flutter_w3c_driver()
+        flutter = FlutterCommand(driver)
+
+        result = flutter.get_render_tree(widget_type='LoginScreen')
+
+        request_body = get_httpretty_request_body(httpretty.last_request())
+        assert request_body['script'] == 'flutter: renderTree'
+        assert request_body['args'] == [{'widgetType': 'LoginScreen'}]
+
+        assert result == expected_body
+
+    @httpretty.activate
+    def test_get_render_tree_with_no_filters(self):
+        expected_body = [{'<full_tree>': 'RootWidget'}]
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body=json.dumps({'value': expected_body}),
+        )
+
+        driver = flutter_w3c_driver()
+        flutter = FlutterCommand(driver)
+
+        result = flutter.get_render_tree()
+
+        request_body = get_httpretty_request_body(httpretty.last_request())
+        assert request_body['script'] == 'flutter: renderTree'
+        assert request_body['args'] == [{}]
+
+        assert result == expected_body
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..36e378437
--- /dev/null
+++ b/test/unit/webdriver/flutter_integration/flutter_search_context_test.py
@@ -0,0 +1,183 @@
+#!/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..62f9f7c1c
--- /dev/null
+++ b/test/unit/webdriver/flutter_integration/flutter_waits_test.py
@@ -0,0 +1,104 @@
+#!/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
diff --git a/test/unit/webdriver/ime_test.py b/test/unit/webdriver/ime_test.py
deleted file mode 100644
index a20aa685a..000000000
--- a/test/unit/webdriver/ime_test.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 json
-
-import httpretty
-
-from appium.webdriver.webdriver import WebDriver
-from test.unit.helper.test_helper import (
-    android_w3c_driver,
-    appium_command,
-    get_httpretty_request_body
-)
-
-
-class TestWebDriverIme(object):
-
-    @httpretty.activate
-    def test_available_ime_engines(self):
-        ANDROID_LATIN = 'com.android.inputmethod.latin/.LatinIME'
-        GOOGLE_LATIN = 'com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME'
-
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/ime/available_engines'),
-            body=json.dumps({'value': [ANDROID_LATIN, GOOGLE_LATIN]})
-        )
-        assert driver.available_ime_engines == [ANDROID_LATIN, GOOGLE_LATIN]
-
-    @httpretty.activate
-    def test_is_ime_active(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/ime/activated'),
-            body=json.dumps({'value': True})
-        )
-        assert driver.is_ime_active() is True
-
-    @httpretty.activate
-    def test_activate_ime_engine(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/ime/activate'),
-        )
-        engine = 'com.android.inputmethod.latin/.LatinIME'
-        assert isinstance(driver.activate_ime_engine(engine), WebDriver)
-
-        d = get_httpretty_request_body(httpretty.last_request())
-        assert d['engine'] == 'com.android.inputmethod.latin/.LatinIME'
-
-    @httpretty.activate
-    def test_deactivate_ime_engine(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/ime/deactivate'),
-        )
-        assert isinstance(driver.deactivate_ime_engine(), WebDriver)
-
-    @httpretty.activate
-    def test_active_ime_engine(self):
-        driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/ime/active_engine'),
-            body=json.dumps({'value': 'com.android.inputmethod.latin/.LatinIME'})
-        )
-        assert driver.active_ime_engine == 'com.android.inputmethod.latin/.LatinIME'
diff --git a/test/unit/webdriver/log_events_test.py b/test/unit/webdriver/log_events_test.py
index be5a514f3..14d41442e 100644
--- a/test/unit/webdriver/log_events_test.py
+++ b/test/unit/webdriver/log_events_test.py
@@ -17,22 +17,17 @@
 import httpretty
 
 from appium.webdriver.webdriver import WebDriver
-from test.unit.helper.test_helper import (
-    appium_command,
-    get_httpretty_request_body,
-    ios_w3c_driver
-)
+from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver
 
 
 class TestWebDriverLogEvents(object):
-
     @httpretty.activate
     def test_get_events(self):
         driver = ios_w3c_driver()
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/events'),
-            body=json.dumps({'value': {'appium:funEvent': [12347]}})
+            body=json.dumps({'value': {'appium:funEvent': [12347]}}),
         )
         events = driver.get_events()
         assert events['appium:funEvent'] == [12347]
@@ -46,7 +41,7 @@ def test_get_events_args(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/events'),
-            body=json.dumps({'value': {'appium:funEvent': [12347]}})
+            body=json.dumps({'value': {'appium:funEvent': [12347]}}),
         )
         events_to_filter = ['appium:funEvent']
         events = driver.get_events(events_to_filter)
@@ -58,11 +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/log_test.py b/test/unit/webdriver/log_test.py
new file mode 100644
index 000000000..f3b76d351
--- /dev/null
+++ b/test/unit/webdriver/log_test.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 json
+
+import httpretty
+
+from test.unit.helper.test_helper import appium_command, get_httpretty_request_body, ios_w3c_driver
+
+
+@httpretty.activate
+def test_get_log_types():
+    driver = ios_w3c_driver()
+    httpretty.register_uri(
+        httpretty.GET,
+        appium_command('/session/1234567890/se/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/se/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
diff --git a/test/unit/webdriver/multi_action_test.py b/test/unit/webdriver/multi_action_test.py
deleted file mode 100644
index 90b404aab..000000000
--- a/test/unit/webdriver/multi_action_test.py
+++ /dev/null
@@ -1,58 +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.multi_action import MultiAction
-from appium.webdriver.common.touch_action import TouchAction
-
-
-class TestMultiAction(object):
-    @pytest.fixture
-    def multi_action(self):
-        return MultiAction(DriverStub())
-
-    def test_json(self, multi_action):
-        json = {
-            'actions': [
-                [
-                    {'action': 'press', 'options': {'element': 1}},
-                    {'action': 'moveTo', 'options': {'x': 10, 'y': 20}},
-                    {'action': 'release', 'options': {}}
-                ],
-                [
-                    {'action': 'press', 'options': {'x': 11, 'y': 30, 'element': 5}},
-                    {'action': 'moveTo', 'options': {'x': 12, 'y': -300}},
-                    {'action': 'release', 'options': {}}
-                ]
-            ]
-        }
-        t1 = TouchAction(DriverStub()).press(ElementStub(1)).move_to(x=10, y=20).release()
-        t2 = TouchAction(DriverStub()).press(ElementStub(5), 11, 30).move_to(x=12, y=-300).release()
-        multi_action.add(t1, t2)
-        assert json == multi_action.json_wire_gestures
-
-
-class DriverStub(object):
-    def execute(self, _action, _params):
-        print("driver.execute called")
-
-
-class ElementStub(object):
-    def __init__(self, e_id):
-        self._id = e_id
-
-    @property
-    def id(self):
-        return self._id
diff --git a/test/unit/webdriver/nativekey_test.py b/test/unit/webdriver/nativekey_test.py
index 5841a9a86..733f6159b 100644
--- a/test/unit/webdriver/nativekey_test.py
+++ b/test/unit/webdriver/nativekey_test.py
@@ -17,7 +17,6 @@
 
 
 class TestAndroidKey:
-
     def test_has_some_codes(self):
         assert AndroidKey.ENTER == 66
         assert AndroidKey.BACK == 4
diff --git a/test/unit/webdriver/network_test.py b/test/unit/webdriver/network_test.py
index 5f682d30b..a9cf7ea9e 100644
--- a/test/unit/webdriver/network_test.py
+++ b/test/unit/webdriver/network_test.py
@@ -17,37 +17,34 @@
 
 from appium.webdriver.extensions.android.network import NetSpeed
 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
 
 
 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.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": {"wifi": true, "data": false, "airplaneMode": false}}',
         )
         assert driver.network_connection == 2
 
     @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": ""}'
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": {"wifi": true, "data": false, "airplaneMode": false}}',
         )
         driver.set_network_connection(2)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['parameters']['type'] == 2
+        assert d['args'][0]['wifi'] is True
 
     @httpretty.activate
     def test_set_network_speed(self):
@@ -56,10 +53,14 @@ def test_set_network_speed(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/network_speed'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+        )
         assert isinstance(driver.set_network_speed(NetSpeed.LTE), WebDriver)
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['netspeed'] == NetSpeed.LTE
+        assert d['args'][0]['speed'] == NetSpeed.LTE
 
     @httpretty.activate
     def test_toggle_wifi(self):
@@ -68,4 +69,9 @@ def test_toggle_wifi(self):
             httpretty.POST,
             appium_command('/session/1234567890/appium/device/toggle_wifi'),
         )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": {"wifi": true, "data": false, "airplaneMode": false}}',
+        )
         assert isinstance(driver.toggle_wifi(), WebDriver)
diff --git a/test/unit/webdriver/performance_test.py b/test/unit/webdriver/performance_test.py
index 656544cad..d4534c903 100644
--- a/test/unit/webdriver/performance_test.py
+++ b/test/unit/webdriver/performance_test.py
@@ -14,29 +14,28 @@
 
 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
 
 
 class TestWebDriverPerformance(object):
-
     @httpretty.activate
     def test_get_performance_data(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/getPerformanceData'),
-            body='{"value": [["user", "kernel"], ["2.5", "1.3"]]}'
+            body='{"value": [["user", "kernel"], ["2.5", "1.3"]]}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": [["user", "kernel"], ["2.5", "1.3"]]}',
         )
         assert driver.get_performance_data('my.app.package', 'cpuinfo', 5) == [['user', 'kernel'], ['2.5', '1.3']]
 
         d = get_httpretty_request_body(httpretty.last_request())
-        assert d['packageName'] == 'my.app.package'
-        assert d['dataType'] == 'cpuinfo'
-        assert d['dataReadTimeout'] == 5
+        assert d['args'][0]['packageName'] == 'my.app.package'
+        assert d['args'][0]['dataType'] == 'cpuinfo'
 
     @httpretty.activate
     def test_get_performance_data_types(self):
@@ -44,6 +43,11 @@ def test_get_performance_data_types(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/performanceData/types'),
-            body='{"value": ["cpuinfo", "memoryinfo", "batteryinfo", "networkinfo"]}'
+            body='{"value": ["cpuinfo", "memoryinfo", "batteryinfo", "networkinfo"]}',
+        )
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/execute/sync'),
+            body='{"value": ["cpuinfo", "memoryinfo", "batteryinfo", "networkinfo"]}',
         )
         assert driver.get_performance_data_types() == ['cpuinfo', 'memoryinfo', 'batteryinfo', 'networkinfo']
diff --git a/test/unit/webdriver/screen_record_test.py b/test/unit/webdriver/screen_record_test.py
index 1f0d8e83a..da470cebf 100644
--- a/test/unit/webdriver/screen_record_test.py
+++ b/test/unit/webdriver/screen_record_test.py
@@ -9,15 +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()
@@ -38,7 +33,38 @@ def test_stop_recording_screen(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/appium/stop_recording_screen'),
-            body='{"value": "b64_video_data"}'
+            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()
+
+
+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'
 
diff --git a/test/unit/webdriver/search_context/android_test.py b/test/unit/webdriver/search_context/android_test.py
index 47df61b6e..879d78f53 100644
--- a/test/unit/webdriver/search_context/android_test.py
+++ b/test/unit/webdriver/search_context/android_test.py
@@ -16,15 +16,67 @@
 
 import httpretty
 
+from appium.webdriver.common.appiumby import AppiumBy
 from appium.webdriver.webelement import WebElement as MobileWebElement
-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
 
 
 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):
@@ -32,10 +84,12 @@ def test_find_element_by_android_data_matcher(self):
         httpretty.register_uri(
             httpretty.POST,
             appium_command('/session/1234567890/element'),
-            body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}'
+            body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "element-id"}}',
+        )
+        el = driver.find_element(
+            by=AppiumBy.ANDROID_DATA_MATCHER,
+            value=json.dumps({'name': 'title', 'args': ['title', 'Animation'], 'class': 'class name'}),
         )
-        el = driver.find_element_by_android_data_matcher(
-            name='title', args=['title', 'Animation'], className='class name')
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert d['using'] == '-android datamatcher'
@@ -51,9 +105,12 @@ def test_find_elements_by_android_data_matcher(self):
         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"}]}'
+            body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "element-id1"}, '
+            '{"element-6066-11e4-a52e-4f735466cecf": "element-id2"}]}',
+        )
+        els = driver.find_elements(
+            by=AppiumBy.ANDROID_DATA_MATCHER, value=json.dumps({'name': 'title', 'args': ['title', 'Animation']})
         )
-        els = driver.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation'])
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert d['using'] == '-android datamatcher'
@@ -66,12 +123,8 @@ def test_find_elements_by_android_data_matcher(self):
     @httpretty.activate
     def test_find_elements_by_android_data_matcher_no_value(self):
         driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/elements'),
-            body='{"value": []}'
-        )
-        els = driver.find_elements_by_android_data_matcher()
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/elements'), body='{"value": []}')
+        els = driver.find_elements(by=AppiumBy.ANDROID_DATA_MATCHER, value='{}')
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert d['using'] == '-android datamatcher'
@@ -79,16 +132,18 @@ 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', w3c=True)
+        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"}}'
+            body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "child-element-id"}}',
+        )
+        el = element.find_element(
+            by=AppiumBy.ANDROID_DATA_MATCHER,
+            value=json.dumps({'name': 'title', 'args': ['title', 'Animation'], 'class': 'class name'}),
         )
-        el = element.find_element_by_android_data_matcher(
-            name='title', args=['title', 'Animation'], className='class name')
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert d['using'] == '-android datamatcher'
@@ -99,15 +154,18 @@ 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', w3c=True)
+        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"}]}'
+            body='{"value": [{"element-6066-11e4-a52e-4f735466cecf": "child-element-id1"}, '
+            '{"element-6066-11e4-a52e-4f735466cecf": "child-element-id2"}]}',
+        )
+        els = element.find_elements(
+            by=AppiumBy.ANDROID_DATA_MATCHER, value=json.dumps({'name': 'title', 'args': ['title', 'Animation']})
         )
-        els = element.find_elements_by_android_data_matcher(name='title', args=['title', 'Animation'])
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert d['using'] == '-android datamatcher'
@@ -118,15 +176,13 @@ 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', w3c=True)
+        element = MobileWebElement(driver, 'element_id')
         httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/element/element_id/elements'),
-            body='{"value": []}'
+            httpretty.POST, appium_command('/session/1234567890/element/element_id/elements'), body='{"value": []}'
         )
-        els = element.find_elements_by_android_data_matcher()
+        els = element.find_elements(by=AppiumBy.ANDROID_DATA_MATCHER, value='{}')
 
         d = get_httpretty_request_body(httpretty.last_request())
         assert d['using'] == '-android datamatcher'
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'
diff --git a/test/unit/webdriver/search_context/windows_test.py b/test/unit/webdriver/search_context/windows_test.py
deleted file mode 100644
index fb78e5f0d..000000000
--- a/test/unit/webdriver/search_context/windows_test.py
+++ /dev/null
@@ -1,40 +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 httpretty
-
-from appium.webdriver.webelement import WebElement as MobileWebElement
-from test.unit.helper.test_helper import (
-    android_w3c_driver,
-    appium_command,
-    get_httpretty_request_body
-)
-
-
-class TestWebDriverWindowsSearchContext(object):
-
-    @httpretty.activate
-    def test_find_element_by_windows_uiautomation(self):
-        driver = android_w3c_driver()
-        element = MobileWebElement(driver, 'element_id', w3c=True)
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/element/element_id/element'),
-            body='{"value": {"element-6066-11e4-a52e-4f735466cecf": "win-element-id"}}'
-        )
-        el = element.find_element_by_windows_uiautomation('win_element')
-
-        d = get_httpretty_request_body(httpretty.last_request())
-        assert d['using'] == '-windows uiautomation'
-        assert el.id == 'win-element-id'
diff --git a/test/unit/webdriver/settings_test.py b/test/unit/webdriver/settings_test.py
index 59a415852..de9a0e00c 100644
--- a/test/unit/webdriver/settings_test.py
+++ b/test/unit/webdriver/settings_test.py
@@ -15,22 +15,15 @@
 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
 
 
 class TestWebDriverSettings(object):
-
     @httpretty.activate
     def test_get_settings_bool(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/appium/settings'),
-            body='{"value": {"sample": true}}'
+            httpretty.GET, appium_command('/session/1234567890/appium/settings'), body='{"value": {"sample": true}}'
         )
         assert driver.get_settings()['sample'] is True
 
@@ -41,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
@@ -50,9 +43,7 @@ def test_update_settings_bool(self):
     def test_get_settings_string(self):
         driver = android_w3c_driver()
         httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/appium/settings'),
-            body='{"value": {"sample": "string"}}'
+            httpretty.GET, appium_command('/session/1234567890/appium/settings'), body='{"value": {"sample": "string"}}'
         )
         assert driver.get_settings()['sample'] == 'string'
 
@@ -63,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/touch_action_test.py b/test/unit/webdriver/touch_action_test.py
deleted file mode 100644
index 0ee3ec96d..000000000
--- a/test/unit/webdriver/touch_action_test.py
+++ /dev/null
@@ -1,124 +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.touch_action import TouchAction
-
-
-class TestTouchAction(object):
-
-    @pytest.fixture
-    def touch_action(self):
-        return TouchAction(DriverStub())
-
-    def test_tap_json(self, touch_action):
-        json = [
-            {'action': 'tap', 'options': {'count': 1, 'element': 1}}
-        ]
-        touch_action.tap(ElementStub(1))
-        assert json == touch_action.json_wire_gestures
-
-    def test_tap_x_y_json(self, touch_action):
-        json = [
-            {'action': 'tap', 'options': {'x': 3, 'y': 4, 'count': 1, 'element': 1}}
-        ]
-        touch_action.tap(ElementStub(1), 3, 4)
-        assert json == touch_action.json_wire_gestures
-
-    def test_press_json(self, touch_action):
-        json = [
-            {'action': 'press', 'options': {'element': 1}}
-        ]
-        touch_action.press(ElementStub(1))
-        assert json == touch_action.json_wire_gestures
-
-    def test_press_pressure_json(self, touch_action):
-        json = [
-            {'action': 'press', 'options': {'element': 1, 'pressure': 1.0}}
-        ]
-        touch_action.press(ElementStub(1), pressure=1.0)
-        assert json == touch_action.json_wire_gestures
-
-    def test_press_x_y_json(self, touch_action):
-        json = [
-            {'action': 'press', 'options': {'element': 1, 'x': 3, 'y': 4}}
-        ]
-        touch_action.press(ElementStub(1), 3, 4)
-        assert json == touch_action.json_wire_gestures
-
-    def test_long_press_json(self, touch_action):
-        json = [
-            {'action': 'longPress', 'options': {'element': 1, 'duration': 2000}}
-        ]
-        touch_action.long_press(ElementStub(1), duration=2000)
-        assert json == touch_action.json_wire_gestures
-
-    def test_long_press_x_y_json(self, touch_action):
-        json = [
-            {'action': 'longPress', 'options': {'element': 1, 'x': 3, 'y': 4, 'duration': 1000}}
-        ]
-        touch_action.long_press(ElementStub(1), 3, 4)
-        assert json == touch_action.json_wire_gestures
-
-    def test_wait_json(self, touch_action):
-        json = [
-            {'action': 'wait', 'options': {'ms': 10}}
-        ]
-        touch_action.wait(10)
-        assert json == touch_action.json_wire_gestures
-
-    def test_wait_without_ms_json(self, touch_action):
-        json = [
-            {'action': 'wait', 'options': {'ms': 0}}
-        ]
-        touch_action.wait()
-        assert json == touch_action.json_wire_gestures
-
-    def test_move_to_json(self, touch_action):
-        json = [
-            {'action': 'moveTo', 'options': {'element': 1, 'x': 3, 'y': 4}}
-        ]
-        touch_action.move_to(ElementStub(1), 3, 4)
-        assert json == touch_action.json_wire_gestures
-
-    def test_release_json(self, touch_action):
-        json = [
-            {'action': 'release', 'options': {}}
-        ]
-        touch_action.release()
-        assert json == touch_action.json_wire_gestures
-
-    def test_perform_json(self, touch_action):
-        json_tap = [
-            {'action': 'tap', 'options': {'element': 1, 'count': 1}}
-        ]
-        touch_action.tap(ElementStub(1))
-        assert json_tap == touch_action.json_wire_gestures
-        touch_action.perform()
-        assert [] == touch_action.json_wire_gestures
-
-
-class DriverStub(object):
-    def execute(self, _action, _params):
-        print("driver.execute called")
-
-
-class ElementStub(object):
-    def __init__(self, e_id, _x=None, _y=None, _count=None):
-        self._id = e_id
-
-    @property
-    def id(self):
-        return self._id
diff --git a/test/unit/webdriver/webdriver_test.py b/test/unit/webdriver/webdriver_test.py
index 40e458d51..443885d54 100644
--- a/test/unit/webdriver/webdriver_test.py
+++ b/test/unit/webdriver/webdriver_test.py
@@ -15,111 +15,76 @@
 import json
 
 import httpretty
+import urllib3
 from mock import patch
 
-from appium import version as appium_version
 from appium import webdriver
-from appium.webdriver.webdriver import WebDriver
+from appium.options.android import UiAutomator2Options
+from appium.webdriver.appium_connection import AppiumConnection
+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,
     appium_command,
-    ios_w3c_driver
+    get_httpretty_request_body,
+    ios_w3c_driver,
+    ios_w3c_driver_with_extensions,
 )
 
 
-class TestWebDriverWebDriver(object):
-
+class TestWebDriverWebDriver:
     @httpretty.activate
     def test_create_session(self):
         httpretty.register_uri(
             httpretty.POST,
-            'http://localhost:4723/wd/hub/session',
-            body='{ "value": { "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}}}'
+            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',
-            'automationName': 'UIAutomator2'
         }
-        driver = webdriver.Remote(
-            'http://localhost:4723/wd/hub',
-            desired_caps
-        )
+        driver = webdriver.Remote(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps))
 
-        assert len(httpretty.HTTPretty.latest_requests) == 1
+        # This tests counts the same request twice on Azure only for now (around 20th May, 2021). Local running works.
+        # Should investigate the cause.
+        # assert len(httpretty.HTTPretty.latest_requests) == 1
 
         request = httpretty.HTTPretty.latest_requests[0]
         assert request.headers['content-type'] == 'application/json;charset=UTF-8'
-        assert 'appium/python {} (selenium'.format(appium_version.version) 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
-        assert request_json.get('desiredCapabilities') is not None
-
-        assert driver.session_id == 'session-id'
-        assert driver.w3c
-        assert driver.command_executor.w3c
-
-    @httpretty.activate
-    def test_create_session_forceMjsonwp(self):
-        httpretty.register_uri(
-            httpretty.POST,
-            'http://localhost:4723/wd/hub/session',
-            body='{ "capabilities": {"deviceName": "Android Emulator"}, "status": 0, "sessionId": "session-id"}'
-        )
-
-        desired_caps = {
+        assert request_json['capabilities']['alwaysMatch'] == {
             'platformName': 'Android',
-            'deviceName': 'Android Emulator',
-            'app': 'path/to/app',
-            'automationName': 'UIAutomator2',
-            'forceMjsonwp': True
+            'appium:deviceName': 'Android Emulator',
+            'appium:app': 'path/to/app',
+            'appium:automationName': 'UIAutomator2',
         }
-        driver = webdriver.Remote(
-            'http://localhost:4723/wd/hub',
-            desired_caps
-        )
-
-        assert len(httpretty.HTTPretty.latest_requests) == 1
-
-        request = httpretty.HTTPretty.latest_requests[0]
-        assert request.headers['content-type'] == 'application/json;charset=UTF-8'
-        assert 'appium/python {} (selenium'.format(appium_version.version) in request.headers['user-agent']
-
-        request_json = json.loads(httpretty.HTTPretty.latest_requests[0].body.decode('utf-8'))
-        assert request_json.get('capabilities') is None
-        assert request_json.get('desiredCapabilities') is not None
+        assert request_json.get('desiredCapabilities') is None
 
         assert driver.session_id == 'session-id'
-        assert driver.w3c is False
-        assert driver.command_executor.w3c is False
 
     @httpretty.activate
     def test_create_session_change_session_id(self):
         httpretty.register_uri(
             httpretty.POST,
-            'http://localhost:4723/wd/hub/session',
-            body='{ "value": { "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}}}'
+            f'{SERVER_URL_BASE}/session',
+            body='{ "sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"} }',
         )
 
         httpretty.register_uri(
             httpretty.GET,
-            'http://localhost:4723/wd/hub/session/another-session-id/title',
-            body='{ "value": "title on another session id"}'
+            f'{SERVER_URL_BASE}/session/another-session-id/title',
+            body='{ "value": "title on another session id"}',
         )
 
-        desired_caps = {
-            'platformName': 'Android',
-            'deviceName': 'Android Emulator',
-            'app': 'path/to/app',
-            'automationName': 'UIAutomator2'
-        }
-        driver = webdriver.Remote(
-            'http://localhost:4723/wd/hub',
-            desired_caps
-        )
+        options = UiAutomator2Options().set_capability('deviceName', 'Android Emulator').set_capability('app', 'path/to/app')
+        driver = webdriver.Remote(SERVER_URL_BASE, options=options)
 
         # current session
         assert driver.session_id == 'session-id'
@@ -133,99 +98,121 @@ def test_create_session_change_session_id(self):
     def test_create_session_register_uridirect(self):
         httpretty.register_uri(
             httpretty.POST,
-            'http://localhost:4723/wd/hub/session',
-            body=json.dumps({'value': {
-                'sessionId': 'session-id',
-                'capabilities': {
-                    'deviceName': 'Android Emulator',
-                    'directConnectProtocol': 'http',
-                    'directConnectHost': 'localhost2',
-                    'directConnectPort': 4800,
-                    'directConnectPath': '/special/path/wd/hub',
+            f'{SERVER_URL_BASE}/session',
+            body=json.dumps(
+                {
+                    'sessionId': 'session-id',
+                    'capabilities': {
+                        'deviceName': 'Android Emulator',
+                        'directConnectProtocol': 'http',
+                        'directConnectHost': 'localhost2',
+                        'directConnectPort': 4800,
+                        'directConnectPath': '/special/path/wd/hub',
+                    },
                 }
-            }})
+            ),
         )
 
         httpretty.register_uri(
             httpretty.GET,
             'http://localhost2:4800/special/path/wd/hub/session/session-id/contexts',
-            body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']})
+            body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}),
         )
 
         desired_caps = {
             'platformName': 'Android',
             'deviceName': 'Android Emulator',
             'app': 'path/to/app',
-            'automationName': 'UIAutomator2'
+            'automationName': 'UIAutomator2',
         }
+        client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True)
         driver = webdriver.Remote(
-            'http://localhost:4723/wd/hub',
-            desired_caps,
-            direct_connection=True
+            SERVER_URL_BASE,
+            options=UiAutomator2Options().load_capabilities(desired_caps),
+            client_config=client_config,
         )
 
-        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)
 
     @httpretty.activate
     def test_create_session_register_uridirect_no_direct_connect_path(self):
         httpretty.register_uri(
             httpretty.POST,
-            'http://localhost:4723/wd/hub/session',
-            body=json.dumps({'value': {
-                'sessionId': 'session-id',
-                'capabilities': {
-                    'deviceName': 'Android Emulator',
-                    'directConnectProtocol': 'http',
-                    'directConnectHost': 'localhost2',
-                    'directConnectPort': 4800
+            f'{SERVER_URL_BASE}/session',
+            body=json.dumps(
+                {
+                    'sessionId': 'session-id',
+                    'capabilities': {
+                        'deviceName': 'Android Emulator',
+                        'directConnectProtocol': 'http',
+                        'directConnectHost': 'localhost2',
+                        'directConnectPort': 4800,
+                    },
                 }
-            }})
+            ),
         )
 
         httpretty.register_uri(
             httpretty.GET,
-            'http://localhost:4723/wd/hub/session/session-id/contexts',
-            body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']})
+            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'
+            'automationName': 'UIAutomator2',
         }
+        client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE, direct_connection=True)
         driver = webdriver.Remote(
-            'http://localhost:4723/wd/hub',
-            desired_caps,
-            direct_connection=True
+            SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps), client_config=client_config
         )
 
-        assert 'http://localhost:4723/wd/hub' == 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)
 
     @httpretty.activate
-    def test_get_all_sessions(self):
-        driver = ios_w3c_driver()
+    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.GET,
-            appium_command('/sessions'),
-            body=json.dumps({'value': {'deviceName': 'iPhone Simulator', 'events': {'simStarted': [1234567891]}}})
+            httpretty.POST,
+            f'{SERVER_URL_BASE}/session',
+            body=json.dumps(
+                {
+                    'sessionId': 'session-id',
+                    'capabilities': {
+                        'deviceName': 'Android Emulator',
+                    },
+                }
+            ),
         )
-        session = driver.all_sessions
-        assert len(session) != 1
 
-    @httpretty.activate
-    def test_get_session(self):
-        driver = ios_w3c_driver()
         httpretty.register_uri(
             httpretty.GET,
-            appium_command('/session/1234567890'),
-            body=json.dumps({'value': {'deviceName': 'iPhone Simulator', 'events': {'simStarted': [1234567890]}}})
+            f'{SERVER_URL_BASE}/session/session-id/contexts',
+            body=json.dumps({'value': ['NATIVE_APP', 'CHROMIUM']}),
         )
-        session = driver.session
-        assert session['deviceName'] == 'iPhone Simulator'
-        assert session['events']['simStarted'] == [1234567890]
+
+        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):
@@ -233,7 +220,7 @@ def test_get_events(self):
         httpretty.register_uri(
             httpretty.GET,
             appium_command('/session/1234567890'),
-            body=json.dumps({'value': {'events': {'simStarted': [1234567890]}}})
+            body=json.dumps({'value': {'events': {'simStarted': [1234567890]}}}),
         )
         events = driver.events
         assert events['simStarted'] == [1234567890]
@@ -241,53 +228,247 @@ def test_get_events(self):
     @httpretty.activate
     def test_get_events_catches_missing_events(self):
         driver = ios_w3c_driver()
-        httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890'),
-            body=json.dumps({'value': {}})
-        )
+        httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890'), body=json.dumps({'value': {}}))
         events = driver.events
         assert events == {}
-        httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890'),
-            body=json.dumps({})
-        )
+        httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890'), body=json.dumps({}))
         events = driver.events
         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()
 
         driver = ios_w3c_driver()
+        httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890'), body=exceptionCallback)
+        events = driver.events
+        assert events == {}
+
+    @httpretty.activate
+    def test_add_command(self):
+        class CustomURLCommand(ExtensionBase):
+            def method_name(self):
+                return 'test_command'
+
+            def test_command(self):
+                return self.execute()['value']
+
+            def add_command(self):
+                return 'get', '/session/$sessionId/path/to/custom/url'
+
+        driver = ios_w3c_driver_with_extensions([CustomURLCommand])
         httpretty.register_uri(
             httpretty.GET,
-            appium_command('/session/1234567890'),
-            body=exceptionCallback
+            appium_command('/session/1234567890/path/to/custom/url'),
+            body=json.dumps({'value': {}}),
         )
-        events = driver.events
-        mock_warning.assert_called_once()
-        assert events == {}
+        result = driver.test_command()
+
+        assert result == {}
+        driver.delete_extensions()
+
+    @httpretty.activate
+    def test_add_command_body(self):
+        class CustomURLCommand(ExtensionBase):
+            def method_name(self):
+                return 'test_command'
+
+            def test_command(self, argument):
+                return self.execute(argument)['value']
+
+            def add_command(self):
+                return 'post', '/session/$sessionId/path/to/custom/url'
+
+        driver = ios_w3c_driver_with_extensions([CustomURLCommand])
+        httpretty.register_uri(
+            httpretty.POST,
+            appium_command('/session/1234567890/path/to/custom/url'),
+            body=json.dumps({'value': {}}),
+        )
+        result = driver.test_command({'dummy': 'test argument'})
+        assert result == {}
+
+        d = get_httpretty_request_body(httpretty.last_request())
+
+        assert d['dummy'] == 'test argument'
+        driver.delete_extensions()
+
+    @httpretty.activate
+    def test_add_command_with_element_id(self):
+        class CustomURLCommand(ExtensionBase):
+            def method_name(self):
+                return 'test_command'
+
+            def test_command(self, element_id):
+                return self.execute({'id': element_id})['value']
+
+            def add_command(self):
+                return 'GET', '/session/$sessionId/path/to/custom/$id/url'
+
+        driver = ios_w3c_driver_with_extensions([CustomURLCommand])
+        httpretty.register_uri(
+            httpretty.GET,
+            appium_command('/session/1234567890/path/to/custom/element_id/url'),
+            body=json.dumps({'value': {}}),
+        )
+        result = driver.test_command('element_id')
+        assert result == {}
+        driver.delete_extensions()
+
+    @httpretty.activate
+    def test_create_session_with_custom_connection(self):
+        httpretty.register_uri(
+            httpretty.POST,
+            f'{SERVER_URL_BASE}/session',
+            body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }',
+        )
+
+        desired_caps = {
+            'deviceName': 'Android Emulator',
+            'app': 'path/to/app',
+        }
+
+        class CustomAppiumConnection(AppiumConnection):
+            # To explicitly check if the given executor is used
+            pass
+
+        init_args_for_pool_manager = {'retries': urllib3.util.retry.Retry(total=3, connect=3, read=False)}
+        custom_appium_connection = CustomAppiumConnection(
+            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))
+
+        request = httpretty.HTTPretty.latest_requests[0]
+        assert request.headers['content-type'] == 'application/json;charset=UTF-8'
+        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
+        assert request_json['capabilities']['alwaysMatch'] == {
+            'platformName': 'Android',
+            'appium:deviceName': 'Android Emulator',
+            'appium:app': 'path/to/app',
+            'appium:automationName': 'UIAutomator2',
+        }
+        assert request_json.get('desiredCapabilities') is None
+        assert driver.session_id == 'session-id'
+
+        assert isinstance(driver.command_executor, CustomAppiumConnection)
+
+    @httpretty.activate
+    def test_create_session_with_custom_connection_with_keepalive(self):
+        httpretty.register_uri(
+            httpretty.POST,
+            f'{SERVER_URL_BASE}/session',
+            body='{ "value": {"sessionId": "session-id", "capabilities": {"deviceName": "Android Emulator"}} }',
+        )
+
+        desired_caps = {
+            'deviceName': 'Android Emulator',
+            'app': 'path/to/app',
+        }
+
+        class CustomAppiumConnection(AppiumConnection):
+            # To explicitly check if the given executor is used
+            pass
+
+        init_args_for_pool_manager = {'retries': urllib3.util.retry.Retry(total=3, connect=3, read=False)}
+        custom_appium_connection = CustomAppiumConnection(
+            # keep alive has different route to set init args for the pool manager
+            keep_alive=True,
+            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))
+
+        request = httpretty.HTTPretty.latest_requests[0]
+        assert request.headers['content-type'] == 'application/json;charset=UTF-8'
+        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
+        assert request_json['capabilities']['alwaysMatch'] == {
+            'platformName': 'Android',
+            'appium:deviceName': 'Android Emulator',
+            'appium:app': 'path/to/app',
+            'appium:automationName': 'UIAutomator2',
+        }
+        assert request_json.get('desiredCapabilities') is None
+        assert driver.session_id == 'session-id'
+
+        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())
+
+    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, desired_capabilities, direct_connection=False):
-        super(SubWebDriver, self).__init__(
+    def __init__(self, command_executor, options=None):
+        super().__init__(
             command_executor=command_executor,
-            desired_capabilities=desired_capabilities,
-            direct_connection=direct_connection
+            options=options,
         )
 
 
 class SubSubWebDriver(SubWebDriver):
-    def __init__(self, command_executor, desired_capabilities, direct_connection=False):
-        super(SubSubWebDriver, self).__init__(
+    def __init__(self, command_executor, options=None):
+        super().__init__(
             command_executor=command_executor,
-            desired_capabilities=desired_capabilities,
-            direct_connection=direct_connection
+            options=options,
         )
 
 
@@ -295,69 +476,52 @@ class TestSubModuleWebDriver(object):
     def android_w3c_driver(self, driver_class):
         response_body_json = json.dumps(
             {
-                'value': {
-                    'sessionId': '1234567890',
-                    'capabilities': {
-                        'platform': 'LINUX',
-                        'desired': {
-                            'platformName': 'Android',
-                            'automationName': 'uiautomator2',
-                            'platformVersion': '7.1.1',
-                            'deviceName': 'Android Emulator',
-                            'app': '/test/apps/ApiDemos-debug.apk',
-                        },
+                'sessionId': '1234567890',
+                'capabilities': {
+                    'platform': 'LINUX',
+                    'desired': {
                         'platformName': 'Android',
                         'automationName': 'uiautomator2',
                         'platformVersion': '7.1.1',
-                        'deviceName': 'emulator-5554',
+                        'deviceName': 'Android Emulator',
                         'app': '/test/apps/ApiDemos-debug.apk',
-                        'deviceUDID': 'emulator-5554',
-                        'appPackage': 'io.appium.android.apis',
-                        'appWaitPackage': 'io.appium.android.apis',
-                        'appActivity': 'io.appium.android.apis.ApiDemos',
-                        'appWaitActivity': 'io.appium.android.apis.ApiDemos'
-                    }
-                }
+                    },
+                    'platformName': 'Android',
+                    'automationName': 'uiautomator2',
+                    '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',
+                    'appActivity': 'io.appium.android.apis.ApiDemos',
+                    'appWaitActivity': 'io.appium.android.apis.ApiDemos',
+                },
             }
         )
 
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session'),
-            body=response_body_json
-        )
+        httpretty.register_uri(httpretty.POST, appium_command('/session'), body=response_body_json)
 
         desired_caps = {
             'platformName': 'Android',
             'deviceName': 'Android Emulator',
             'app': 'path/to/app',
-            'automationName': 'UIAutomator2'
+            'automationName': 'UIAutomator2',
         }
 
-        driver = driver_class(
-            'http://localhost:4723/wd/hub',
-            desired_caps
-        )
+        driver = driver_class(SERVER_URL_BASE, options=UiAutomator2Options().load_capabilities(desired_caps))
         return driver
 
     @httpretty.activate
     def test_clipboard_with_subclass(self):
         driver = self.android_w3c_driver(SubWebDriver)
-        httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/context'),
-            body='{"value": "NATIVE"}'
-        )
+        httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE"}')
         assert driver.current_context == 'NATIVE'
 
     @httpretty.activate
     def test_clipboard_with_subsubclass(self):
         driver = self.android_w3c_driver(SubSubWebDriver)
-        httpretty.register_uri(
-            httpretty.GET,
-            appium_command('/session/1234567890/context'),
-            body='{"value": "NATIVE"}'
-        )
+        httpretty.register_uri(httpretty.GET, appium_command('/session/1234567890/context'), body='{"value": "NATIVE"}')
         assert driver.current_context == 'NATIVE'
 
     @httpretty.activate
diff --git a/test/unit/webdriver/webelement_test.py b/test/unit/webdriver/webelement_test.py
index 88347b57a..23268c3ac 100644
--- a/test/unit/webdriver/webelement_test.py
+++ b/test/unit/webdriver/webelement_test.py
@@ -13,45 +13,34 @@
 # limitations under the License.
 
 import json
-import os
 import tempfile
 
 import httpretty
 
 from appium.webdriver.webelement import WebElement as MobileWebElement
-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
 
 
 class TestWebElement(object):
-
     @httpretty.activate
-    def test_set_value(self):
+    def test_status(self):
         driver = android_w3c_driver()
+        response = {'ready': True, 'message': {'build': {'version': '2.0.0', 'revision': None}}}
         httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/appium/element/element_id/value')
+            httpretty.GET,
+            appium_command('/status'),
+            body=json.dumps({'value': response}),
         )
+        s = driver.get_status()
 
-        element = MobileWebElement(driver, 'element_id', w3c=True)
-        value = 'happy testing'
-        element.set_value(value)
-
-        d = get_httpretty_request_body(httpretty.last_request())
-        assert d['value'] == [value]
+        assert s == response
 
     @httpretty.activate
     def test_send_key(self):
         driver = android_w3c_driver()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/element/element_id/value')
-        )
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/value'))
 
-        element = MobileWebElement(driver, 'element_id', w3c=True)
+        element = MobileWebElement(driver, 'element_id')
         element.send_keys('happy testing')
 
         d = get_httpretty_request_body(httpretty.last_request())
@@ -62,13 +51,10 @@ def test_send_key_with_file(self):
         driver = android_w3c_driver()
         # Should not send this file
         tmp_f = tempfile.NamedTemporaryFile()
-        httpretty.register_uri(
-            httpretty.POST,
-            appium_command('/session/1234567890/element/element_id/value')
-        )
+        httpretty.register_uri(httpretty.POST, appium_command('/session/1234567890/element/element_id/value'))
 
         try:
-            element = MobileWebElement(driver, 'element_id', w3c=True)
+            element = MobileWebElement(driver, 'element_id')
             element.send_keys(tmp_f.name)
         finally:
             tmp_f.close()
@@ -76,25 +62,45 @@ 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()
-        rect_dict = {
-            'y': 200,
-            'x': 100,
-            'width': 300,
-            'height': 56
-        }
+        rect_dict = {'y': 200, 'x': 100, 'width': 300, 'height': 56}
         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', w3c=True)
+        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
+
+    @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
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index c059a95ee..000000000
--- a/tox.ini
+++ /dev/null
@@ -1,17 +0,0 @@
-[tox]
-skipsdist = True
-envlist =
-    py27,
-    py34,
-    py35,
-    py36,
-    py37,
-#    py38  # TODO Remove comment-out when pylint and astroid upgraded to the latest and py2 support dropped
-
-[testenv]
-deps =
-    pipenv
-commands =
-    pipenv lock --clear
-    pipenv install
-    ./ci.sh
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 000000000..a45cff098
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,771 @@
+version = 1
+revision = 3
+requires-python = ">=3.9"
+
+[[package]]
+name = "appium-python-client"
+version = "5.2.0"
+source = { editable = "." }
+dependencies = [
+    { name = "selenium" },
+    { name = "typing-extensions" },
+]
+
+[package.dev-dependencies]
+dev = [
+    { name = "httpretty" },
+    { name = "mock" },
+    { name = "mypy" },
+    { name = "pre-commit" },
+    { name = "pytest" },
+    { name = "pytest-cov" },
+    { name = "pytest-xdist" },
+    { name = "python-dateutil" },
+    { name = "ruff" },
+    { name = "types-python-dateutil" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "selenium", specifier = ">=4.26,<5.0" },
+    { name = "typing-extensions", specifier = "~=4.13" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+    { name = "httpretty", specifier = "~=1.1" },
+    { name = "mock", specifier = "~=5.2" },
+    { name = "mypy", specifier = "~=1.17" },
+    { name = "pre-commit", specifier = "~=4.2" },
+    { name = "pytest", specifier = "~=8.4" },
+    { name = "pytest-cov", specifier = "~=6.2" },
+    { name = "pytest-xdist", specifier = "~=3.8" },
+    { name = "python-dateutil", specifier = "~=2.9" },
+    { name = "ruff", specifier = "~=0.12" },
+    { name = "types-python-dateutil", specifier = "~=2.9" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" },
+    { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
+    { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
+    { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.10.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003, upload-time = "2025-08-04T00:33:02.977Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391, upload-time = "2025-08-04T00:33:05.645Z" },
+    { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367, upload-time = "2025-08-04T00:33:07.189Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627, upload-time = "2025-08-04T00:33:08.809Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485, upload-time = "2025-08-04T00:33:10.29Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429, upload-time = "2025-08-04T00:33:11.909Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104, upload-time = "2025-08-04T00:33:13.467Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397, upload-time = "2025-08-04T00:33:14.682Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502, upload-time = "2025-08-04T00:33:16.254Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388, upload-time = "2025-08-04T00:33:17.4Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119, upload-time = "2025-08-04T00:33:19.101Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511, upload-time = "2025-08-04T00:33:20.32Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513, upload-time = "2025-08-04T00:33:21.896Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350, upload-time = "2025-08-04T00:33:23.917Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516, upload-time = "2025-08-04T00:33:25.5Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241, upload-time = "2025-08-04T00:33:26.767Z" },
+    { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274, upload-time = "2025-08-04T00:33:28.494Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882, upload-time = "2025-08-04T00:33:30.048Z" },
+    { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541, upload-time = "2025-08-04T00:33:31.376Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426, upload-time = "2025-08-04T00:33:32.976Z" },
+    { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116, upload-time = "2025-08-04T00:33:34.302Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" },
+    { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" },
+    { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" },
+    { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" },
+    { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" },
+    { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" },
+    { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" },
+    { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" },
+    { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" },
+    { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" },
+    { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" },
+    { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" },
+    { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" },
+    { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" },
+    { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" },
+    { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" },
+    { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" },
+    { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" },
+    { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" },
+    { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" },
+    { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" },
+    { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" },
+    { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" },
+    { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" },
+    { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/c9/139fa9f64edfa5bae1492a4efecef7209f59ba5f9d862db594be7a85d7fb/coverage-7.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:765b13b164685a2f8b2abef867ad07aebedc0e090c757958a186f64e39d63dbd", size = 215003, upload-time = "2025-08-04T00:34:59.079Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/9f/8682ccdd223c2ab34de6575ef3c78fae9bdaece1710b4d95bb9b0abd4d2f/coverage-7.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a219b70100500d0c7fd3ebb824a3302efb6b1a122baa9d4eb3f43df8f0b3d899", size = 215382, upload-time = "2025-08-04T00:35:00.772Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/4e/45b9658499db7149e1ed5b46ccac6101dc5c0ddb786a0304f7bb0c0d90d4/coverage-7.10.2-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e33e79a219105aa315439ee051bd50b6caa705dc4164a5aba6932c8ac3ce2d98", size = 241457, upload-time = "2025-08-04T00:35:02.696Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/66/aaf159bfe94ee3996b8786034a8e713bc68cd650aa7c1a41b612846cdc41/coverage-7.10.2-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc3945b7bad33957a9eca16e9e5eae4b17cb03173ef594fdaad228f4fc7da53b", size = 243354, upload-time = "2025-08-04T00:35:04.238Z" },
+    { url = "https://files.pythonhosted.org/packages/21/31/8fd2f67d8580380e7b19b23838e308b6757197e94a1b3b87e0ad483f70c8/coverage-7.10.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bdff88e858ee608a924acfad32a180d2bf6e13e059d6a7174abbae075f30436", size = 244923, upload-time = "2025-08-04T00:35:06.159Z" },
+    { url = "https://files.pythonhosted.org/packages/55/90/67b129b08200e08962961f56604083923bc8484bc641c92ee6801c1ae822/coverage-7.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44329cbed24966c0b49acb386352c9722219af1f0c80db7f218af7793d251902", size = 242856, upload-time = "2025-08-04T00:35:07.735Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/8f/3f428363f713ab3432e602665cdefe436fd427263471644dd3742b6eebd8/coverage-7.10.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:be127f292496d0fbe20d8025f73221b36117b3587f890346e80a13b310712982", size = 241092, upload-time = "2025-08-04T00:35:09.381Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/4d/e8531ea19f047b8b1d1d1c85794e4b35ae762e570f072ca2afbce67be176/coverage-7.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c031da749a05f7a01447dd7f47beedb498edd293e31e1878c0d52db18787df0", size = 242044, upload-time = "2025-08-04T00:35:10.929Z" },
+    { url = "https://files.pythonhosted.org/packages/62/6b/22cb6281b4d06b73edae2facc7935a15151ddb8e8d8928a184b7a3100289/coverage-7.10.2-cp39-cp39-win32.whl", hash = "sha256:22aca3e691c7709c5999ccf48b7a8ff5cf5a8bd6fe9b36efbd4993f5a36b2fcf", size = 217512, upload-time = "2025-08-04T00:35:12.801Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/83/bce22e6880837de640d6ff630c7493709a3511f93c5154a326b337f01a81/coverage-7.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c7195444b932356055a8e287fa910bf9753a84a1bc33aeb3770e8fca521e032e", size = 218406, upload-time = "2025-08-04T00:35:14.351Z" },
+    { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+    { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "execnet"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.18.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpretty"
+version = "1.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/19/850b7ed736319d0c4088581f4fc34f707ef14461947284026664641e16d4/httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68", size = 442389, upload-time = "2021-08-16T19:35:31.4Z" }
+
+[[package]]
+name = "identify"
+version = "2.6.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "mock"
+version = "5.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796, upload-time = "2025-03-03T12:31:42.911Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload-time = "2025-03-03T12:31:41.518Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mypy-extensions" },
+    { name = "pathspec" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" },
+    { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" },
+    { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" },
+    { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" },
+    { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" },
+    { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" },
+    { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" },
+    { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" },
+    { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" },
+    { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" },
+    { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" },
+    { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" },
+    { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" },
+    { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" },
+    { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" },
+    { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" },
+    { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" },
+    { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" },
+    { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" },
+    { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" },
+    { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
+]
+
+[[package]]
+name = "outcome"
+version = "1.3.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cfgv" },
+    { name = "identify" },
+    { name = "nodeenv" },
+    { name = "pyyaml" },
+    { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pysocks"
+version = "1.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "iniconfig" },
+    { name = "packaging" },
+    { name = "pluggy" },
+    { name = "pygments" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "6.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "coverage", extra = ["toml"] },
+    { name = "pluggy" },
+    { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
+]
+
+[[package]]
+name = "pytest-xdist"
+version = "3.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "execnet" },
+    { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
+    { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
+    { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
+    { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
+    { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
+    { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
+    { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
+    { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+    { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+    { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+    { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+    { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+    { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+    { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" },
+    { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" },
+    { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.12.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" },
+    { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" },
+    { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" },
+    { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" },
+    { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" },
+    { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" },
+    { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" },
+    { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" },
+    { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" },
+    { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" },
+]
+
+[[package]]
+name = "selenium"
+version = "4.33.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "trio" },
+    { name = "trio-websocket" },
+    { name = "typing-extensions" },
+    { name = "urllib3", extra = ["socks"] },
+    { name = "websocket-client" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5f/7e/4145666dd275760b56d0123a9439915af167932dd6caa19b5f8b281ae297/selenium-4.33.0.tar.gz", hash = "sha256:d90974db95d2cdeb34d2fb1b13f03dc904f53e6c5d228745b0635ada10cd625d", size = 882387, upload-time = "2025-05-23T17:45:22.046Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7e/c0/092fde36918574e144613de73ba43c36ab8d31e7d36bb44c35261909452d/selenium-4.33.0-py3-none-any.whl", hash = "sha256:af9ea757813918bddfe05cc677bf63c8a0cd277ebf8474b3dd79caa5727fca85", size = 9370835, upload-time = "2025-05-23T17:45:19.448Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
+    { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
+    { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
+    { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
+    { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
+    { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
+    { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
+    { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
+    { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
+    { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
+    { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
+    { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+]
+
+[[package]]
+name = "trio"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "attrs" },
+    { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "idna" },
+    { name = "outcome" },
+    { name = "sniffio" },
+    { name = "sortedcontainers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" },
+]
+
+[[package]]
+name = "trio-websocket"
+version = "0.12.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "outcome" },
+    { name = "trio" },
+    { name = "wsproto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
+]
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.9.0.20250708"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/95/6bdde7607da2e1e99ec1c1672a759d42f26644bbacf939916e086db34870/types_python_dateutil-2.9.0.20250708.tar.gz", hash = "sha256:ccdbd75dab2d6c9696c350579f34cffe2c281e4c5f27a585b2a2438dd1d5c8ab", size = 15834, upload-time = "2025-07-08T03:14:03.382Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/72/52/43e70a8e57fefb172c22a21000b03ebcc15e47e97f5cb8495b9c2832efb4/types_python_dateutil-2.9.0.20250708-py3-none-any.whl", hash = "sha256:4d6d0cc1cc4d24a2dc3816024e502564094497b713f7befda4d5bc7a8e3fd21f", size = 17724, upload-time = "2025-07-08T03:14:02.593Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
+]
+
+[package.optional-dependencies]
+socks = [
+    { name = "pysocks" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.33.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "distlib" },
+    { name = "filelock" },
+    { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" },
+]
+
+[[package]]
+name = "websocket-client"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" },
+]
+
+[[package]]
+name = "wsproto"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" },
+]
