diff --git a/.github/actions/setup-appium-server/action.yml b/.github/actions/setup-appium-server/action.yml new file mode 100644 index 000000000..a9177d4e8 --- /dev/null +++ b/.github/actions/setup-appium-server/action.yml @@ -0,0 +1,65 @@ +name: 'Start Appium Server' +description: 'Start Appium server with configurable arguments and wait for startup' + +inputs: + port: + description: 'Appium server port' + required: false + default: '4723' + host: + description: 'Appium server host' + required: false + default: '127.0.0.1' + timeout: + description: 'Timeout in seconds to wait for server startup' + required: false + default: '25' + server_args: + description: 'Additional server arguments (space-separated)' + required: false + default: '' + log_file: + description: 'Log file name' + required: false + default: 'appium.log' + +runs: + using: 'composite' + steps: + - name: Start Appium server + shell: bash + run: | + nohup appium server \ + --port=${{ inputs.port }} \ + --address=${{ inputs.host }} \ + --log-no-colors \ + --log-timestamp \ + --keep-alive-timeout 1200 \ + ${{ inputs.server_args }} \ + 2>&1 > ${{ inputs.log_file }} & + + - name: Wait for Appium server to start + shell: bash + run: | + TIMEOUT_SEC=${{ inputs.timeout }} + INTERVAL_SEC=1 + + start_time=$(date +%s) + while true; do + current_time=$(date +%s) + elapsed=$((current_time - start_time)) + + if nc -z ${{ inputs.host }} ${{ inputs.port }}; then + echo "Appium server is running after $elapsed seconds" + cat ${{ inputs.log_file }} + exit 0 + fi + + if [[ "$elapsed" -ge "$TIMEOUT_SEC" ]]; then + echo "${elapsed} seconds timeout reached: Appium server is NOT running" + exit 1 + fi + + echo "Waiting $elapsed seconds for Appium server to start..." + sleep "$INTERVAL_SEC" + done diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index 15ac808df..abf6e7ad6 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -3,8 +3,6 @@ name: Functional Tests on: # Run by manual at this time workflow_dispatch: - push: - branches: [ master ] pull_request: branches: [ master ] @@ -12,6 +10,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + APPIUM_TEST_SERVER_PORT: '4723' + APPIUM_TEST_SERVER_HOST: '127.0.0.1' + jobs: ios_test: strategy: @@ -26,9 +28,10 @@ jobs: # Please make sure the available Xcode versions and iOS versions # on the runner images. https://github.com/actions/runner-images env: - XCODE_VERSION: 16.4 - IOS_VERSION: 18.5 - IPHONE_MODEL: iPhone 16 Plus + XCODE_VERSION: '16.4' + IOS_VERSION: '18.5' + IPHONE_MODEL: 'iPhone 16 Plus' + PREBUILT_WDA_PATH: ${{ github.workspace }}/wda/WebDriverAgentRunner-Runner.app steps: - uses: actions/checkout@v4 @@ -42,43 +45,61 @@ jobs: 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 + - name: Prepare iOS simulator + uses: futureware-tech/simulator-action@v4 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 }} + wait_for_boot: true + shutdown_after_job: false - # needed? - - run: brew install ffmpeg - - # Start Appium - - run: npm install -g appium - - run: | + - name: Install Appium and drivers + run: | + npm install -g appium 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: Start Appium server + uses: ./.github/actions/setup-appium-server + with: + port: ${{ env.APPIUM_TEST_SERVER_PORT }} + host: ${{ env.APPIUM_TEST_SERVER_HOST }} + server_args: '--relaxed-security' - - name: Set up Python 3.12 + - name: Downloading prebuilt WDA + run: | + appium driver run xcuitest download-wda-sim --platform=ios --outdir=$(dirname "$PREBUILT_WDA_PATH") + + - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.12 + - name: Cache uv modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- + - 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 + 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 + LOCAL_PREBUILT_WDA: ${{ env.PREBUILT_WDA_PATH }} - name: Save server output if: ${{ always() }} @@ -115,13 +136,19 @@ jobs: with: node-version: 'lts/*' - # Start Appium - - run: npm install -g appium - - run: | + - name: Install Appium and drivers + run: | + npm install -g appium 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: Start Appium server + uses: ./.github/actions/setup-appium-server + with: + port: ${{ env.APPIUM_TEST_SERVER_PORT }} + host: ${{ env.APPIUM_TEST_SERVER_HOST }} + server_args: '--relaxed-security --use-plugins=execute-driver' - name: Enable KVM group perms run: | @@ -154,6 +181,16 @@ jobs: with: python-version: 3.12 + - name: Cache uv modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- + - name: run tests uses: reactivecircus/android-emulator-runner@v2 with: @@ -230,15 +267,31 @@ jobs: with: node-version: 'lts/*' - - name: Install Appium - run: npm install --location=global appium - - - name: Install Android drivers and Run Appium + - name: Install Appium and drivers if: matrix.e2e-tests == 'flutter-android' run: | + npm install --location=global appium 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: Start Appium server for Android + if: matrix.e2e-tests == 'flutter-android' + uses: ./.github/actions/setup-appium-server + with: + port: ${{ env.APPIUM_TEST_SERVER_PORT }} + host: ${{ env.APPIUM_TEST_SERVER_HOST }} + server_args: '--relaxed-security' + log_file: 'appium_flutter_android.log' + + - name: Cache uv modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- - name: Run Android tests if: matrix.e2e-tests == 'flutter-android' @@ -265,28 +318,40 @@ jobs: with: xcode-version: ${{ env.XCODE_VERSION }} - - uses: futureware-tech/simulator-action@v3 + - uses: futureware-tech/simulator-action@v4 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 }} + wait_for_boot: true + shutdown_after_job: false - - name: install dependencies - if: matrix.e2e-tests == 'flutter-ios' - run: brew install ffmpeg - - - name: Install IOS drivers and Run Appium + - name: Install Appium and drivers if: matrix.e2e-tests == 'flutter-ios' run: | + npm install --location=global appium 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: Start Appium server for iOS + if: matrix.e2e-tests == 'flutter-ios' + uses: ./.github/actions/setup-appium-server + with: + port: ${{ env.APPIUM_TEST_SERVER_PORT }} + host: ${{ env.APPIUM_TEST_SERVER_HOST }} + server_args: '--relaxed-security' + log_file: '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 + 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/unit-test.yml b/.github/workflows/unit-test.yml index de0fc07bd..70611b8c2 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -19,6 +19,15 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Cache uv modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + .venv + key: ${{ runner.os }}-uv-shared-${{ hashFiles('**/uv.lock') }} + restore-keys: | + ${{ runner.os }}-uv-shared- - name: Install uv run: make install-uv - name: Run Checks diff --git a/Makefile b/Makefile index ad2c96828..4ed823f49 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ install-uv: .PHONY: sync-dev sync-dev: - uv sync + uv sync --dev .PHONY: unittest unittest: ## Run unittest diff --git a/appium/options/ios/xcuitest/base.py b/appium/options/ios/xcuitest/base.py index 02143a095..0d349934c 100644 --- a/appium/options/ios/xcuitest/base.py +++ b/appium/options/ios/xcuitest/base.py @@ -72,6 +72,7 @@ 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.prebuilt_wda_path_option import PrebuiltWdaPathOption from .wda.process_arguments_option import ProcessArgumentsOption from .wda.result_bundle_path_option import ResultBundlePathOption from .wda.screenshot_quality_option import ScreenshotQualityOption @@ -83,6 +84,7 @@ 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_preinstalled_wda_option import UsePreinstalledWdaOption 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 @@ -171,6 +173,7 @@ class XCUITestOptions( KeychainPathOption, MaxTypingFrequencyOption, MjpegServerPortOption, + PrebuiltWdaPathOption, ProcessArgumentsOption, ResultBundlePathOption, ScreenshotQualityOption, @@ -182,6 +185,7 @@ class XCUITestOptions( UseNativeCachingStrategyOption, UseNewWdaOption, UsePrebuiltWdaOption, + UsePreinstalledWdaOption, UseSimpleBuildTestOption, UseXctestrunFileOption, WaitForIdleTimeoutOption, diff --git a/appium/options/ios/xcuitest/wda/prebuilt_wda_path_option.py b/appium/options/ios/xcuitest/wda/prebuilt_wda_path_option.py new file mode 100644 index 000000000..f26d12f0a --- /dev/null +++ b/appium/options/ios/xcuitest/wda/prebuilt_wda_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 + +PREBUILT_WDA_PATH = 'prebuiltWDAPath' + + +class PrebuiltWdaPathOption(SupportsCapabilities): + @property + def prebuilt_wda_path(self) -> Optional[str]: + """ + The path to the prebuilt WebDriverAgent. + """ + return self.get_capability(PREBUILT_WDA_PATH) + + @prebuilt_wda_path.setter + def prebuilt_wda_path(self, value: str) -> None: + """ + The path to the prebuilt WebDriverAgent. This should be the path to the + WebDriverAgent.xcarchive file or the WebDriverAgent.app bundle. + """ + self.set_capability(PREBUILT_WDA_PATH, value) diff --git a/appium/options/ios/xcuitest/wda/use_preinstalled_wda_option.py b/appium/options/ios/xcuitest/wda/use_preinstalled_wda_option.py new file mode 100644 index 000000000..547d87f90 --- /dev/null +++ b/appium/options/ios/xcuitest/wda/use_preinstalled_wda_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_PREINSTALLED_WDA = 'usePreinstalledWDA' + + +class UsePreinstalledWdaOption(SupportsCapabilities): + @property + def use_preinstalled_wda(self) -> Optional[bool]: + """ + Whether to use a preinstalled WebDriverAgent. + """ + return self.get_capability(USE_PREINSTALLED_WDA) + + @use_preinstalled_wda.setter + def use_preinstalled_wda(self, value: bool) -> None: + """ + Whether to use a preinstalled WebDriverAgent. If true, Appium will not + build and install the WebDriverAgent, but will use an existing one. + Defaults to false. + """ + self.set_capability(USE_PREINSTALLED_WDA, value) diff --git a/test/apps/TestApp.app.zip b/test/apps/TestApp.app.zip deleted file mode 100644 index 19668e47f..000000000 Binary files a/test/apps/TestApp.app.zip and /dev/null differ diff --git a/test/apps/UICatalog.app.zip b/test/apps/UICatalog.app.zip deleted file mode 100644 index 479d92e50..000000000 Binary files a/test/apps/UICatalog.app.zip and /dev/null differ diff --git a/test/functional/ios/helper/desired_capabilities.py b/test/functional/ios/helper/desired_capabilities.py deleted file mode 100644 index c26fc7a89..000000000 --- a/test/functional/ios/helper/desired_capabilities.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -from typing import Any, Dict, Optional - -# Returns abs path relative to this file and not cwd - - -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: Optional[str] = None) -> Dict[str, Any]: - desired_caps: Dict[str, Any] = { - 'deviceName': iphone_device_name(), - 'platformName': 'iOS', - 'platformVersion': os.getenv('IOS_VERSION') or '17.4', - 'automationName': 'XCUITest', - 'allowTouchIdEnroll': True, - 'wdaLocalPort': wda_port(), - '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: - 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: int) -> str: - if PytestXdistWorker.COUNT is None: - return '0' - - if number >= int(PytestXdistWorker.COUNT): - return 'gw0' - - return f'gw{number}' - - -# If you run tests with pytest-xdist, you can run tests in parallel. - - -def wda_port() -> int: - if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1): - return 8101 - - return 8100 - - -# Before running tests, you must have iOS simulators named 'iPhone 12 - 8100' and 'iPhone 12 - 8101' - - -def iphone_device_name() -> str: - prefix = os.getenv('IPHONE_MODEL') or 'iPhone 15 Plus' - if PytestXdistWorker.NUMBER == PytestXdistWorker.gw(0): - return f'{prefix} - 8100' - elif PytestXdistWorker.NUMBER == PytestXdistWorker.gw(1): - return f'{prefix} - 8101' - - return prefix diff --git a/test/functional/ios/helper/options.py b/test/functional/ios/helper/options.py new file mode 100644 index 000000000..c3a46e1c5 --- /dev/null +++ b/test/functional/ios/helper/options.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from dataclasses import dataclass +from typing import Optional + +from appium.options.ios import XCUITestOptions + + +def PATH(p: str) -> str: + """Get the absolute path of a file relative to the folder where this file is located.""" + return os.path.abspath(os.path.join(os.path.dirname(__file__), p)) + + +def make_options(app: Optional[str] = None) -> XCUITestOptions: + """Get XCUITest options configured for iOS testing with parallel execution support.""" + options = XCUITestOptions() + + # Set basic iOS capabilities + options.device_name = iphone_device_name() + options.platform_version = os.getenv('IOS_VERSION') or '17.4' + options.allow_touch_id_enroll = True + options.wda_local_port = wda_port() + options.simple_is_visible_check = True + + if app is not None: + options.app = PATH(os.path.join('..', '..', '..', 'apps', app)) + + local_prebuilt_wda = os.getenv('LOCAL_PREBUILT_WDA') + if local_prebuilt_wda: + options.use_preinstalled_wda = True + options.prebuilt_wda_path = local_prebuilt_wda + + return options + + +@dataclass +class WorkerInfo: + """Information about the current test worker in parallel execution.""" + + worker_number: Optional[int] + total_workers: Optional[int] + + @property + def is_parallel(self) -> bool: + """Check if running in parallel mode.""" + return self.worker_number is not None and self.total_workers is not None + + +def _get_worker_info() -> WorkerInfo: + """ + Get current worker number and total worker count from pytest-xdist environment variables. + + Returns: + WorkerInfo: Worker information or None values if not running in parallel + """ + worker_number = os.getenv('PYTEST_XDIST_WORKER') + worker_count = os.getenv('PYTEST_XDIST_WORKER_COUNT') + + if worker_number and worker_count: + # Extract number from worker string like 'gw0', 'gw1', etc. + try: + worker_num = int(worker_number.replace('gw', '')) + total_workers = int(worker_count) + return WorkerInfo(worker_number=worker_num, total_workers=total_workers) + except (ValueError, AttributeError): + pass + + return WorkerInfo(worker_number=None, total_workers=None) + + +def wda_port() -> int: + """ + Get a unique WDA port for the current worker. + Uses base port 8100 and increments by worker number. + """ + worker_info = _get_worker_info() + return 8100 + (worker_info.worker_number or 0) + + +def iphone_device_name() -> str: + """ + Get a unique device name for the current worker. + Uses the base device name and appends the port number for uniqueness. + """ + prefix = os.getenv('IPHONE_MODEL') or 'iPhone 15 Plus' + worker_info = _get_worker_info() + + if worker_info.is_parallel: + port = wda_port() + return f'{prefix} - {port}' + + return prefix diff --git a/test/functional/ios/helper/test_helper.py b/test/functional/ios/helper/test_helper.py deleted file mode 100644 index 435ca4b45..000000000 --- a/test/functional/ios/helper/test_helper.py +++ /dev/null @@ -1,47 +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 - -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(object): - IOS_UICATALOG_APP_NAME = 'UIKitCatalog' - - def setup_method(self) -> None: - desired_caps = desired_capabilities.get_desired_capabilities('UICatalog.app.zip') - 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_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(), method.__name__ + '.mp4') - with open(video_path, 'wb') as fd: - fd.write(base64.b64decode(payload)) - self.driver.quit() diff --git a/test/functional/ios/safari_tests.py b/test/functional/ios/safari_tests.py index c7bd997c1..624979f80 100644 --- a/test/functional/ios/safari_tests.py +++ b/test/functional/ios/safari_tests.py @@ -13,62 +13,67 @@ # limitations under the License. import time +from typing import TYPE_CHECKING, Generator + +import pytest 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 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) - - # 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_method(self) -> None: - self.driver.quit() - - 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) -> 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' - - 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' +from .helper.options import make_options + +if TYPE_CHECKING: + from appium.webdriver.webdriver import WebDriver + + +@pytest.fixture +def driver() -> Generator['WebDriver', None, None]: + """Create and configure Safari driver for testing.""" + options = make_options() + options.bundle_id = 'com.apple.mobilesafari' + options.native_web_tap = True + options.safari_ignore_fraud_warning = True + options.webview_connect_timeout = 100000 + + client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE) + client_config.timeout = 600 + driver = webdriver.Remote(options=options, client_config=client_config) + + # 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. + driver.terminate_app('com.apple.mobilesafari') + driver.activate_app('com.apple.mobilesafari') + + yield driver + + driver.quit() + + +def test_context(driver: 'WebDriver') -> None: + """Test Safari context switching.""" + contexts = driver.contexts + assert 'NATIVE_APP' == contexts[0] + assert contexts[1].startswith('WEBVIEW_') + driver.switch_to.context(contexts[1]) + assert 'WEBVIEW_' in driver.current_context + + +def test_navigation(driver: 'WebDriver') -> None: + """Test Safari navigation to Google.""" + contexts = driver.contexts + for context in contexts: + if context.startswith('WEBVIEW_'): + driver.switch_to.context(context) + break + else: + pytest.fail('Could not set WEBVIEW context') + + driver.get('http://google.com') + for _ in range(5): + time.sleep(0.5) + if 'Google' == driver.title: + return + + pytest.fail('The title was wrong') diff --git a/test/helpers/constants.py b/test/helpers/constants.py index 9f453add8..f2cff27db 100644 --- a/test/helpers/constants.py +++ b/test/helpers/constants.py @@ -1 +1,3 @@ -SERVER_URL_BASE = 'http://127.0.0.1:4723' +import os + +SERVER_URL_BASE = f'http://{os.getenv("APPIUM_TEST_SERVER_HOST", "127.0.0.1")}:{os.getenv("APPIUM_TEST_SERVER_PORT", "4723")}'