Skip to main content

Appium integration for the tquality test automation framework, built on tquality-py-core.

Project description

tquality-py-appium

PyPI License GitHub

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 from capabilities.json5
  • AppiumDriverService — manages the appium.webdriver.Remote session
  • ContextManager / ContextWaiter — native/webview switching, alerts, windows
  • ElementFactory — typed element creation (Button / Input / Label / CheckBox)
  • CollectionFactory + DomField — extract list[PydanticModel] from UI lists
  • ExecuteDriver — batched commands via execute_driver_script
  • AppiumServices — DI composition root

License

Apache-2.0. See LICENSE and NOTICE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tquality_py_appium-0.2.0.tar.gz (110.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tquality_py_appium-0.2.0-py3-none-any.whl (93.8 kB view details)

Uploaded Python 3

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

Hashes for tquality_py_appium-0.2.0.tar.gz
Algorithm Hash digest
SHA256 dd22ee1593ce3f46555876d1b7d7919d144e6fbf2f5e3fed5cb7697977e6f0ac
MD5 a36663bc430d6a0a80f9550f171012c2
BLAKE2b-256 c2ca2979eb08e5b4896b6183c312c63c4014a82fb5769fcdc115b225f2c5e4b9

See more details on using hashes here.

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

Hashes for tquality_py_appium-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fe69504fccfc01e7709f05cbf16d710869c838cace2b7a90698fd6128033f43a
MD5 3ef05c1fb6945cd70e99ec65b77a3ded
BLAKE2b-256 79b353f83825c2ee741e15bf48b20dbf9006f9f96f007bb85696db1d6e95ec11

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page