Skip to content

Commit b9564c7

Browse files
test: Update Android tests to the canonical pytest format (#1180)
1 parent ed27b1f commit b9564c7

File tree

10 files changed

+126
-147
lines changed

10 files changed

+126
-147
lines changed

.github/workflows/functional-test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ jobs:
134134
distribution: 'temurin'
135135
java-version: '17'
136136

137+
- name: Install ffmpeg
138+
run: |
139+
sudo apt-get update
140+
sudo apt-get install -y ffmpeg
141+
137142
- name: Install Node.js
138143
uses: actions/setup-node@v4
139144
with:

test/apps/ApiDemos-debug.apk.zip

-3.36 MB
Binary file not shown.

test/functional/android/appium_service_tests.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
@pytest.fixture
2424
def appium_service() -> Generator[AppiumService, None, None]:
25+
"""Create and configure Appium service for testing."""
2526
service = AppiumService()
2627
service.start(
2728
args=[
@@ -33,12 +34,13 @@ def appium_service() -> Generator[AppiumService, None, None]:
3334
'/wd/hub',
3435
]
3536
)
36-
try:
37-
yield service
38-
finally:
39-
service.stop()
37+
38+
yield service
39+
40+
service.stop()
4041

4142

4243
def test_appium_service(appium_service: AppiumService) -> None:
44+
"""Test that Appium service is running and listening."""
4345
assert appium_service.is_running
4446
assert appium_service.is_listening

test/functional/android/bidi_tests.py

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,25 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import TYPE_CHECKING, Generator
16+
1517
import pytest
1618
from selenium.webdriver.common.bidi.common import command_builder
1719

1820
from appium import webdriver
19-
from appium.options.common import AppiumOptions
2021
from appium.webdriver.client_config import AppiumClientConfig
2122
from test.functional.test_helper import is_ci
2223
from test.helpers.constants import SERVER_URL_BASE
2324

24-
from .helper.desired_capabilities import get_desired_capabilities
25+
from .options import make_options
26+
27+
if TYPE_CHECKING:
28+
from appium.webdriver.webdriver import WebDriver
2529

2630

2731
class AppiumLogEntry:
32+
"""Represents a log entry from Appium BiDi."""
33+
2834
event_class = 'log.entryAdded'
2935

3036
def __init__(self, level, text, timestamp, source, type):
@@ -49,36 +55,36 @@ def from_json(cls, json: dict):
4955
)
5056

5157

52-
class TestChromeWithBiDi:
53-
"""This test requires selenium python client which supports 'command_builder'"""
58+
@pytest.fixture
59+
def driver() -> Generator['WebDriver', None, None]:
60+
"""Create and configure Chrome driver with BiDi support for testing."""
61+
client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE)
62+
client_config.timeout = 600
63+
options = make_options()
64+
options.web_socket_url = True
65+
driver = webdriver.Remote(SERVER_URL_BASE, options=options, client_config=client_config)
5466

55-
def setup_method(self) -> None:
56-
client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE)
57-
client_config.timeout = 600
58-
caps = get_desired_capabilities()
59-
caps['webSocketUrl'] = True
60-
self.driver = webdriver.Remote(
61-
SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps), client_config=client_config
62-
)
67+
yield driver
68+
69+
driver.quit()
6370

64-
def teardown_method(self) -> None:
65-
self.driver.quit()
6671

67-
@pytest.mark.skipif(is_ci(), reason='Flaky on CI')
68-
def test_bidi_log(self) -> None:
69-
log_entries = []
70-
bidi_log_param = {'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']}
72+
@pytest.mark.skipif(is_ci(), reason='Flaky on CI')
73+
def test_bidi_log(driver: 'WebDriver') -> None:
74+
"""Test BiDi logging functionality with Chrome driver."""
75+
log_entries = []
76+
bidi_log_param = {'events': ['log.entryAdded'], 'contexts': ['NATIVE_APP']}
7177

72-
self.driver.script.conn.execute(command_builder('session.subscribe', bidi_log_param))
78+
driver.script.conn.execute(command_builder('session.subscribe', bidi_log_param))
7379

74-
def _log(entry: AppiumLogEntry):
75-
# 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}
76-
log_entries.append(entry.json)
80+
def _log(entry: AppiumLogEntry):
81+
# 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}
82+
log_entries.append(entry.json)
7783

78-
try:
79-
callback_id = self.driver.script.conn.add_callback(AppiumLogEntry, _log)
80-
self.driver.page_source
81-
assert len(log_entries) != 0
82-
self.driver.script.conn.remove_callback(AppiumLogEntry, callback_id)
83-
finally:
84-
self.driver.script.conn.execute(command_builder('session.unsubscribe', bidi_log_param))
84+
try:
85+
callback_id = driver.script.conn.add_callback(AppiumLogEntry, _log)
86+
driver.page_source
87+
assert len(log_entries) != 0
88+
driver.script.conn.remove_callback(AppiumLogEntry, callback_id)
89+
finally:
90+
driver.script.conn.execute(command_builder('session.unsubscribe', bidi_log_param))

test/functional/android/chrome_tests.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,39 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import TYPE_CHECKING, Generator
16+
17+
import pytest
18+
1519
from appium import webdriver
16-
from appium.options.common import AppiumOptions
1720
from appium.webdriver.client_config import AppiumClientConfig
1821
from appium.webdriver.common.appiumby import AppiumBy
1922
from test.helpers.constants import SERVER_URL_BASE
2023

21-
from .helper.desired_capabilities import get_desired_capabilities
24+
from .options import make_options
25+
26+
if TYPE_CHECKING:
27+
from appium.webdriver.webdriver import WebDriver
28+
29+
30+
@pytest.fixture
31+
def driver() -> Generator['WebDriver', None, None]:
32+
"""Create and configure Chrome driver for testing."""
33+
client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE)
34+
client_config.timeout = 600
35+
options = make_options()
36+
options.browser_name = 'Chrome'
37+
driver = webdriver.Remote(SERVER_URL_BASE, options=options, client_config=client_config)
2238

39+
yield driver
2340

24-
class TestChrome(object):
25-
def setup_method(self) -> None:
26-
client_config = AppiumClientConfig(remote_server_addr=SERVER_URL_BASE)
27-
client_config.timeout = 600
28-
caps = get_desired_capabilities()
29-
caps['browserName'] = 'Chrome'
30-
self.driver = webdriver.Remote(
31-
SERVER_URL_BASE, options=AppiumOptions().load_capabilities(caps), client_config=client_config
32-
)
41+
driver.quit()
3342

34-
def teardown_method(self) -> None:
35-
self.driver.quit()
3643

37-
def test_find_single_element(self) -> None:
38-
e = self.driver.find_element(by=AppiumBy.XPATH, value='//body')
39-
assert e.text == ''
44+
def test_find_single_element(driver: 'WebDriver') -> None:
45+
"""Test finding a single element in Chrome browser."""
46+
e = driver.find_element(by=AppiumBy.XPATH, value='//body')
47+
assert e.text == ''
4048

41-
# Chrome browser's default page
42-
assert '<html><head></head><body></body></html>' in self.driver.page_source
49+
# Chrome browser's default page
50+
assert '<html><head></head><body></body></html>' in driver.page_source

test/functional/android/helper/__init__.py

Whitespace-only changes.

test/functional/android/helper/desired_capabilities.py

Lines changed: 0 additions & 38 deletions
This file was deleted.

test/functional/android/helper/test_helper.py

Lines changed: 0 additions & 52 deletions
This file was deleted.

test/functional/android/options.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
from typing import Optional
17+
18+
from appium.options.android import UiAutomator2Options
19+
from test.functional.test_helper import get_worker_info
20+
21+
22+
def make_options(app: Optional[str] = None) -> UiAutomator2Options:
23+
"""Get UiAutomator2 options configured for Android testing with parallel execution support."""
24+
options = UiAutomator2Options()
25+
26+
# Set basic Android capabilities
27+
options.device_name = android_device_name()
28+
options.platform_name = 'Android'
29+
options.automation_name = 'UIAutomator2'
30+
options.new_command_timeout = 240
31+
options.uiautomator2_server_install_timeout = 120000
32+
options.adb_exec_timeout = 120000
33+
34+
if app is not None:
35+
options.app = app
36+
37+
return options
38+
39+
40+
def android_device_name() -> str:
41+
"""
42+
Get a unique device name for the current worker.
43+
Uses the base device name and appends the port number for uniqueness.
44+
"""
45+
prefix = os.getenv('ANDROID_MODEL') or 'Android Emulator'
46+
worker_info = get_worker_info()
47+
48+
if worker_info.is_parallel:
49+
# For parallel execution, we can use different device names or ports
50+
# This is a simplified approach - in practice you might want to use different emulators
51+
return f'{prefix} - Worker {worker_info.worker_id}'
52+
53+
return prefix

test/functional/ios/helper/options.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,6 @@
1919
from test.functional.test_helper import get_wda_port, get_worker_info
2020

2121

22-
def PATH(p: str) -> str:
23-
"""Get the absolute path of a file relative to the folder where this file is located."""
24-
return os.path.abspath(os.path.join(os.path.dirname(__file__), p))
25-
26-
2722
def make_options(app: Optional[str] = None) -> XCUITestOptions:
2823
"""Get XCUITest options configured for iOS testing with parallel execution support."""
2924
options = XCUITestOptions()
@@ -36,7 +31,7 @@ def make_options(app: Optional[str] = None) -> XCUITestOptions:
3631
options.simple_is_visible_check = True
3732

3833
if app is not None:
39-
options.app = PATH(os.path.join('..', '..', '..', 'apps', app))
34+
options.app = app
4035

4136
if local_prebuilt_wda := os.getenv('LOCAL_PREBUILT_WDA'):
4237
options.use_preinstalled_wda = True

0 commit comments

Comments
 (0)