Appium integration for the tquality test automation framework, built on tquality-py-core.
Project description
tquality-py-appium
Appium integration for the tquality test automation framework, built on tquality-py-core.
Page-object style mobile UI testing for Android and iOS: typed elements with built-in smart waits, a dependency-injection composition root, cascading JSON5 config, native/webview switching, and Allure reporting with screencasts.
Installation
pip install tquality-py-appium
# or
uv add tquality-py-appium
Requires Python >= 3.12 and a running Appium server (local or remote grid / cloud such as BrowserStack, SauceLabs).
Quickstart
1. Generate config files
# config.json5 - framework behavior defaults (timeouts, context, screencast)
tquality-appium-config init
# capabilities.json5 - devices + applications
tquality-appium-config caps-init
2. Describe your devices and apps
capabilities.json5 (json5: comments and trailing commas allowed):
{
"$schema": "https://cdn.jsdelivr.net/gh/Tquality-ru/tquality-py-appium@master/schema/capabilities.schema.json",
"selectedDevice": "local_android",
"selectedApplication": "demo_app",
"devices": {
"local_android": {
"location": "http://127.0.0.1:4723",
"capabilities": {
"appium:platformName": "Android",
"appium:automationName": "UiAutomator2",
"appium:deviceName": "emulator",
"appium:autoGrantPermissions": true,
},
},
},
"applications": {
"demo_app": {
"appium:appPackage": "com.example.shop",
"appium:appActivity": "com.example.shop.MainActivity",
},
},
}
The active device/app pair is picked via selectedDevice /
selectedApplication, or overridden at runtime — handy for a CI device
matrix:
TEST_SELECTED_DEVICE=pixel_8 TEST_SELECTED_APPLICATION=demo_app_prod pytest
3. Wire up the composition root
A per-test fixture tears down the Appium session. config.json5 /
capabilities.json5 next to the test are picked up automatically: a per-test
core plugin points config resolution at the test's directory. No setup()
needed.
# tests/conftest.py
import pytest
from tquality_appium import AppiumServices
@pytest.fixture(autouse=True)
def appium_session():
yield
if AppiumServices.is_driver_started():
AppiumServices.driver.quit() # driver is testlocal - auto-reset per test
# driver / config / capabilities / logger / waiter are TestContextSingleton -
# instances are auto-reset per test by the bundled static-di plugin.
Key features
Page objects with BaseForm
A screen is a class; its unique_element defines "this screen is shown".
Tests call business methods, never raw elements:
from tquality_appium import BaseForm, By
class LoginForm(BaseForm):
def __init__(self) -> None:
super().__init__(
unique_element=self.element_factory.label(
By.xpath('//*[@text="Sign in"]'), "Login form",
),
name="Login form",
)
self._phone = self.element_factory.input(
By.accessibility_id("phone_input"), "Phone number",
)
self._submit = self.element_factory.button(
By.accessibility_id("login_button"), "Login",
)
def login(self, phone: str) -> None:
self._phone.type_text(phone)
self._submit.click()
Typed elements and locator strategies
element_factory builds Button / Input / Label / CheckBox. The By
factory covers cross-platform and native strategies:
from tquality_appium import By, UiScrollable, UiSelector
By.accessibility_id("login_button")
By.xpath('//XCUIElementTypeButton[@name="Continue"]')
By.ios_predicate("name == 'Login'")
# Android UiAutomator via a fluent builder
By.android_uiautomator(UiSelector().resource_id("com.example.shop:id/ok"))
# Scroll until the element comes into view
By.android_uiautomator(
UiScrollable().scroll_into_view(UiSelector().text_contains("Checkout"))
)
Elements expose intent-level actions — click(), type_text(),
check() / toggle(), .text, .is_displayed — each with built-in waits.
Smart waits and element state
Every interaction waits for the right precondition first. Each element
carries an ElementState (e.g. buttons default to CLICKABLE, labels to
DISPLAYED); override per element when the default gets in the way:
from tquality_appium import By, ElementState
# A tab whose unique element reports displayed == false — only require it to exist
self._tab = self.element_factory.button(
By.xpath('//*[contains(@resource-id, "tab_profile")]'),
"Profile tab",
state=ElementState.EXISTS_IN_ANY_STATE,
)
# Explicit waits are available when you need them
self._submit.wait.until_visible(timeout=10)
assert LoginForm().wait_for_displayed(raise_on_timeout=True)
Allure steps with screencast
@step records an Allure step. At LogLevel.WITH_SCREENCAST the framework
captures video for that step (native start_recording_screen, with an
automatic webm fallback when on-device recording is blocked). Page source is
attached to the report on failure.
import allure
from tquality_appium import LogLevel, step
from myapp.screens.home_page import HomePage
from myapp.screens.login_form import LoginForm
class TestNavigation:
@allure.title("Profile tab opens the login form for a guest")
@step("Open the profile tab", LogLevel.WITH_SCREENCAST)
def test_profile_requires_login(self) -> None:
home = HomePage()
assert home.wait_for_displayed(), "Home not displayed on launch"
home.navigation_menu.open_profile()
assert LoginForm().wait_for_displayed(), "Login form not displayed"
Native ↔ webview, alerts and windows
ContextManager handles native/webview switching; context.wait.for_alert
waits on system dialogs (permission prompts, iOS ATT, etc.):
from tquality_appium import AppiumServices, ContextManager
ctx = AppiumServices.get_service(ContextManager)
with ctx.context("WEBVIEW_com.example.shop"):
... # interact inside the embedded webview
# Wait for a system permission dialog and accept it
alert = AppiumServices.driver.context.wait.for_alert(
lambda a: "Allow" in a.text,
message="permission prompt",
)
if alert:
alert.accept()
Collections → Pydantic models
Pull a whole UI list into typed models in a single round-trip (via
execute_driver_script, with an automatic per-field fallback if the server
disables it):
from pydantic import BaseModel
from tquality_appium import AppiumServices, By, CollectionFactory, DomField
class Product(BaseModel):
title: str = DomField.id("com.example.shop:id/title")
price: str = DomField.id("com.example.shop:id/price")
factory = AppiumServices.get_service(CollectionFactory)
products = factory.from_container(Product, By.class_name("android.widget.ListView"))
Dependency-injection composition root
AppiumServices is the single composition root. Resolve services by type
(rename-safe), or subclass with @copy to add/replace services. The scope is
defined by the static-dependency-injector provider type; providers are
declared as typed attributes and read as values (Services.driver, not
.driver()):
| Scope | Provider | Lifetime |
|---|---|---|
| global | Singleton |
One instance per pytest process. |
| test | TestContextSingleton |
A new instance per test; auto-reset by the bundled plugin. |
| session | ContextLocalSingleton + reset in a scope="session" fixture |
One instance per contextvars context, reset on exit. |
| transient | Factory |
A fresh instance on every access. |
# my_project/services.py
from static_dependency_injector.containers import copy
from static_dependency_injector.static_providers import (
ContextLocalSingleton,
Factory,
Singleton,
TestContextSingleton,
)
from tquality_appium import AppiumDriverService, AppiumServices, ContextManager
from my_project.clients import ApiClient, CurrentUser, SessionData, TempDirFactory
@copy(AppiumServices)
class ProjectServices(AppiumServices):
# Global: one API client per process.
api_client: ApiClient = Singleton(ApiClient)
# Test: fresh state per test, auto-reset by the bundled static-di plugin.
current_user: CurrentUser = TestContextSingleton(CurrentUser)
# Session: data shared across a run, reset in a session fixture.
session_data: SessionData = ContextLocalSingleton(SessionData)
# Transient: a fresh instance on every access.
temp_dir: TempDirFactory = Factory(TempDirFactory)
# Replacing an existing service (rewired onto the parent's config/capabilities
# by @copy; reference the inherited providers via `.provider.<name>`):
# driver: AppiumDriverService = TestContextSingleton(
# MyDriverService,
# config=AppiumServices.provider.config,
# capabilities=AppiumServices.provider.capabilities,
# )
# Resolve by type, not by provider name
ctx = ProjectServices.get_service(ContextManager)
# conftest.py
import pytest
from my_project.services import ProjectServices
# current_user is TestContextSingleton - auto-reset per test, no fixture needed.
@pytest.fixture(scope="session", autouse=True)
def _reset_session_scoped_services():
"""Session-scoped ContextLocalSingleton providers reset at the end of the run."""
yield
ProjectServices.provider.session_data.reset()
@pytest.fixture(autouse=True)
def appium_session():
yield
if ProjectServices.is_driver_started():
ProjectServices.driver.quit() # driver is testlocal - instance auto-reset per test
Cascading JSON5 config and per-test devices
config.json5 (framework behavior) and capabilities.json5 (infrastructure)
are resolved upward from the test's directory to the workspace root. This lets
tests/ios/ and tests/android/ each ship their own capabilities.json5
(iOS simulator vs Android emulator) while common settings live at the root —
configs are rebuilt per test automatically.
Documentation
See tquality-py-core for the
driver-agnostic concepts (BaseConfig, Logger, BaseForm, BaseElement,
JSON-schema cascading config) — everything from core is re-exported here.
Appium-specific:
AppiumConfig— framework behavior (timeouts, default context, screencast)CapabilitiesConfig— devices + applications, loaded fromcapabilities.json5AppiumDriverService— manages theappium.webdriver.RemotesessionContextManager/ContextWaiter— native/webview switching, alerts, windowsElementFactory— typed element creation (Button/Input/Label/CheckBox)CollectionFactory+DomField— extractlist[PydanticModel]from UI listsExecuteDriver— batched commands viaexecute_driver_scriptAppiumServices— DI composition root
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tquality_py_appium-0.2.0.tar.gz.
File metadata
- Download URL: tquality_py_appium-0.2.0.tar.gz
- Upload date:
- Size: 110.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd22ee1593ce3f46555876d1b7d7919d144e6fbf2f5e3fed5cb7697977e6f0ac
|
|
| MD5 |
a36663bc430d6a0a80f9550f171012c2
|
|
| BLAKE2b-256 |
c2ca2979eb08e5b4896b6183c312c63c4014a82fb5769fcdc115b225f2c5e4b9
|
File details
Details for the file tquality_py_appium-0.2.0-py3-none-any.whl.
File metadata
- Download URL: tquality_py_appium-0.2.0-py3-none-any.whl
- Upload date:
- Size: 93.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe69504fccfc01e7709f05cbf16d710869c838cace2b7a90698fd6128033f43a
|
|
| MD5 |
3ef05c1fb6945cd70e99ec65b77a3ded
|
|
| BLAKE2b-256 |
79b353f83825c2ee741e15bf48b20dbf9006f9f96f007bb85696db1d6e95ec11
|