Metadata-Version: 2.4
Name: tquality-py-appium
Version: 0.2.0
Summary: Appium integration for the tquality test automation framework, built on tquality-py-core.
Project-URL: Homepage, https://github.com/Tquality-ru/tquality-py-appium
Project-URL: Repository, https://github.com/Tquality-ru/tquality-py-appium
Project-URL: Issues, https://github.com/Tquality-ru/tquality-py-appium/issues
Project-URL: Changelog, https://github.com/Tquality-ru/tquality-py-appium/blob/master/CHANGELOG.md
Author: ООО «Точка качества»
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: allure,android,appium,ios,mobile,page-object,qa,test-automation,testing,tquality
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: appium-python-client>=4.0
Requires-Dist: pydantic>=2.0.1
Requires-Dist: pytest-xdist>=3.8.0
Requires-Dist: pytest>=9.0.3
Requires-Dist: selenium>=4.25
Requires-Dist: static-dependency-injector>=0.3.8
Requires-Dist: tquality-py-core[screencast]>=0.2.4
Description-Content-Type: text/markdown

# tquality-py-appium

[![PyPI](https://img.shields.io/pypi/v/tquality-py-appium)](https://pypi.org/project/tquality-py-appium/)
[![License](https://img.shields.io/pypi/l/tquality-py-appium)](https://github.com/Tquality-ru/tquality-py-appium/blob/master/LICENSE)
[![GitHub](https://img.shields.io/badge/GitHub-Tquality--ru%2Ftquality--py--appium-blue?logo=github)](https://github.com/Tquality-ru/tquality-py-appium)

Appium integration for the [tquality](https://tquality.ru) test automation
framework, built on
[tquality-py-core](https://github.com/Tquality-ru/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.

[Русская версия](README.ru.md)

## Installation

```bash
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

```bash
# 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):

```json5
{
    "$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:

```bash
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.

```python
# 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:

```python
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:

```python
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:

```python
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.

```python
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.):

```python
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):

```python
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.                              |

```python
# 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)
```

```python
# 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](https://github.com/Tquality-ru/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](LICENSE) and [NOTICE](NOTICE).
