Metadata-Version: 2.4
Name: tquality-py-appium
Version: 0.1.7
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: dependency-injector>=4.49
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: tquality-py-core[screencast]>=0.1.14
Description-Content-Type: text/markdown

# 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

`conftest.py` at your project root registers the DI container once:

```python
from pathlib import Path

from tquality_appium import AppiumServices

AppiumServices.setup(config_dir=Path(__file__).resolve().parent)
```

A per-test fixture starts and tears down the Appium session:

```python
# tests/conftest.py
from collections.abc import Iterator

import pytest

from tquality_appium import AppiumDriverService, AppiumServices


@pytest.fixture(autouse=True)
def appium_session() -> Iterator[AppiumDriverService]:
    svc = AppiumServices.driver()
    try:
        yield svc
    finally:
        svc.quit()
        AppiumServices.driver.reset()
        AppiumServices.logger.reset()
        AppiumServices.waiter.reset()
```

## 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 to add/replace services:

```python
from dependency_injector import providers

from tquality_appium import AppiumServices, ContextManager


class ProjectServices(AppiumServices):
    my_service = providers.Singleton(MyService)


# Resolve by type, not by provider name
ctx = AppiumServices.get_service(ContextManager)
```

### 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).
