Skip to content

Commit

Permalink
NEW: appium device support (POC)
Browse files Browse the repository at this point in the history
  • Loading branch information
yashaka committed Oct 24, 2024
1 parent b3792d3 commit a6b1bd4
Show file tree
Hide file tree
Showing 22 changed files with 1,570 additions and 60 deletions.
Empty file.
83 changes: 83 additions & 0 deletions examples/run_cross_platform_android_ios/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from enum import Enum
from typing import Optional

import dotenv
import pydantic

from examples.run_cross_platform_android_ios.wikipedia_app_tests.support import path


class EnvContext(Enum):
android = 'android'
ios = 'ios'
# bstack_android = 'bstack_android'
# bstack_ios = 'bstack_ios'
# local_android = 'local_android'
# local = 'local'
# local_ios = 'local_ios'


class Config(pydantic.BaseSettings):
context: EnvContext = EnvContext.android
driver_remote_url: str = 'http://127.0.0.1:4723'

app_package: str = 'org.wikipedia.alpha'
app = './app-alpha-universal-release.apk'
appWaitActivity = 'org.wikipedia.*'
deviceName: Optional[str] = None
bstack_userName: Optional[str] = None
bstack_accessKey: Optional[str] = None
platformVersion: Optional[str] = None

@property
def bstack_creds(self):
return {
'userName': self.bstack_userName,
'accessKey': self.bstack_accessKey,
}

@property
def runs_on_bstack(self):
return self.app.startswith('bs://')

def to_driver_options(self):
if self.context is EnvContext.android:
from appium.options.android import UiAutomator2Options

options = UiAutomator2Options()

if self.deviceName:
options.set_capability('deviceName', self.deviceName)

if self.appWaitActivity:
options.set_capability('appWaitActivity', self.appWaitActivity)

options.set_capability(
'app',
(
self.app
if (self.app.startswith('/') or self.runs_on_bstack)
else path.relative_from_root(self.app)
),
)

if self.platformVersion:
options.set_capability('platformVersion', self.platformVersion)

if self.runs_on_bstack:
options.set_capability(
'bstack:options',
{
'projectName': 'Wikipedia App Tests',
'buildName': 'browserstack-build-1', # TODO: use some unique value
'sessionName': 'BStack first_test', # TODO: use some unique value
**self.bstack_creds,
},
)

return options
else:
raise ValueError(f'Unsupported context: {self.context}')


config = Config(dotenv.find_dotenv()) # type: ignore
Empty file.
22 changes: 22 additions & 0 deletions examples/run_cross_platform_android_ios/tests/acceptance_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from selene import have, be
from selene.support._mobile import device


def test_wikipedia_searches():
# GIVEN
device.element('fragment_onboarding_skip_button').tap()

# WHEN
device.element(drd='Search Wikipedia').tap()
device.element('search_src_text').type('Appium')

# THEN
results = device.all('page_list_item_title')
results.should(have.size_greater_than(0))
results.first.should(have.text('Appium'))

# WHEN
results.first.tap()

# THEN
device.element('text=Appium').should(be.visible)
17 changes: 17 additions & 0 deletions examples/run_cross_platform_android_ios/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest

from examples.run_cross_platform_android_ios import project
from examples.run_cross_platform_android_ios.wikipedia_app_tests import support
from selene.support._mobile import device


@pytest.fixture(scope='function', autouse=True)
def driver_management():
device.config.driver_options = project.config.to_driver_options()
device.config.driver_remote_url = project.config.driver_remote_url
device.config.selector_to_by_strategy = support.mobile_selectors.to_by_strategy
device.config.timeout = 8.0

yield

device.driver.quit()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import mobile_selectors
from . import path
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import re
from binascii import Error

from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.common.by import By

from examples.run_cross_platform_android_ios import project


def is_android_id(selector):
return re.match(r'^[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-_]+)+:id/[a-zA-Z0-9-_]+$', selector)


def is_word_with_dashes_underscores_or_numbers(selector):
return re.match(r'^[a-zA-Z_\d\-]+$', selector)


def are_words_with_dashes_underscores_or_numbers_separated_by_space(selector):
return re.match(r'^[a-zA-Z_\d\- ]+$', selector)


def is_xpath_like(selector: str):
return (
selector.startswith('/')
or selector.startswith('./')
or selector.startswith('..')
or selector.startswith('(')
or selector.startswith('*/')
)


def to_app_package_wise_by(selector: str):
return AppiumBy.ID, (
f'{project.config.app_package}:id/{selector}'
if project.config.app_package
else selector
)


def to_by_strategy(selector: str):
if is_xpath_like(selector):
return By.XPATH, selector

# BY EXPLICIT ANDROID ID
if selector.startswith('#') and is_word_with_dashes_underscores_or_numbers(
selector[1:]
):
if project.config.context is project.EnvContext.android:
return to_app_package_wise_by(selector[1:])
else:
raise Error(
f'Unsupported selector: {selector}, for platform: {project.config.context}'
)

# BY MATCHED ANDROID ID
if is_android_id(selector):
return AppiumBy.ID, selector

# BY EXACT TEXT
if (selector.startswith('text="') and selector.endswith('"')) or (
selector.startswith('text=`\'') and selector.endswith('\'')
):
return (
(
AppiumBy.ANDROID_UIAUTOMATOR,
f'new UiSelector().text("{selector[6:-1]}")',
)
if project.config.context is project.EnvContext.android
else (AppiumBy.IOS_PREDICATE, f'label == "{selector[6:-1]}"')
)

# BY PARTIAL TEXT
if selector.startswith('text='):
return (
(
AppiumBy.ANDROID_UIAUTOMATOR,
f'new UiSelector().textContains("{selector[5:]}")',
)
if project.config.context is project.EnvContext.android
else (
AppiumBy.IOS_CLASS_CHAIN,
f'**/*[`label CONTAINS "{selector[5:]}"`][-1]',
)
)

# BY CLASS NAME (SAME for IOS and ANDROID)
if any(
selector.lower().startswith(prefix)
for prefix in [
'uia',
'xcuielementtype',
'cyi',
'android.widget',
'android.view',
]
):
return AppiumBy.CLASS_NAME, selector

# BY IMPLICIT ID (single word, no spaces)
if is_word_with_dashes_underscores_or_numbers(selector):
if project.config.context is project.EnvContext.android:
return to_app_package_wise_by(selector)
else:
return AppiumBy.ACCESSIBILITY_ID, selector

# BY IMPLICIT ACCESSIBILITY ID (SAME for IOS and ANDROID)
if are_words_with_dashes_underscores_or_numbers_separated_by_space(selector):
return AppiumBy.ACCESSIBILITY_ID, selector

raise Error(f'Unsupported selector: {selector}')
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def relative_from_root(path: str):
from examples.run_cross_platform_android_ios import wikipedia_app_tests
from pathlib import Path

return (
Path(wikipedia_app_tests.__file__)
.parent.parent.joinpath(path)
.absolute()
.__str__()
)
18 changes: 9 additions & 9 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ codecov = "*"
mypy = "*"
pydantic = "^1.10.7"
python-dotenv = "0.21.1"
Appium-Python-Client = "^2.9.0"
Appium-Python-Client = "^4.2.0"
pyperclip = "^1.8.2"
setuptools = "^70.0.0"

Expand Down
Loading

0 comments on commit a6b1bd4

Please sign in to comment.