{"id":35694008,"url":"https://github.com/molokov-klim/Appium-Python-Client-Shadowstep","last_synced_at":"2026-01-11T20:00:33.429Z","repository":{"id":221770449,"uuid":"714459652","full_name":"molokov-klim/Appium-Python-Client-Shadowstep","owner":"molokov-klim","description":"UI Testing Framework powered by Appium Python Client","archived":false,"fork":false,"pushed_at":"2025-12-16T23:13:53.000Z","size":9116,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2025-12-20T13:37:46.725Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/molokov-klim.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSES/MIT.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-11-04T22:51:48.000Z","updated_at":"2025-12-16T23:13:56.000Z","dependencies_parsed_at":"2024-06-11T02:16:08.418Z","dependency_job_id":"2d2959eb-d494-4888-9ffb-757f199c1f64","html_url":"https://github.com/molokov-klim/Appium-Python-Client-Shadowstep","commit_stats":null,"previous_names":["molokov-klim/shadowstep","molokov-klim/appium-python-client-shadowstep"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/molokov-klim/Appium-Python-Client-Shadowstep","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molokov-klim%2FAppium-Python-Client-Shadowstep","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molokov-klim%2FAppium-Python-Client-Shadowstep/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molokov-klim%2FAppium-Python-Client-Shadowstep/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molokov-klim%2FAppium-Python-Client-Shadowstep/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/molokov-klim","download_url":"https://codeload.github.com/molokov-klim/Appium-Python-Client-Shadowstep/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molokov-klim%2FAppium-Python-Client-Shadowstep/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28321261,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-11T18:42:50.174Z","status":"ssl_error","status_checked_at":"2026-01-11T18:39:13.842Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-06T00:00:56.926Z","updated_at":"2026-01-11T20:00:33.283Z","avatar_url":"https://github.com/molokov-klim.png","language":"Python","funding_links":[],"categories":["Project Categories"],"sub_categories":[],"readme":"\u003c!--\nSPDX-FileCopyrightText: 2023 Molokov Klim\n\nSPDX-License-Identifier: MIT\n--\u003e\n\n# Shadowstep\n\n**Shadowstep** — a modern Python framework for Android test automation. Powered\nby Appium.  \n_Write tests, not boilerplate._\n\n___\n\n\u003c!-- markdownlint-disable MD013 --\u003e\n[![License][badge-license]][link-license]\n[![License Check][badge-license-check]][workflow-license-check]\n\n___\n\n[![Ask DeepWiki][badge-deepwiki]][link-deepwiki]\n[![Watch in Action][badge-youtube]][link-youtube]\n\n___\n\n[![PyPI version][badge-pypi]][link-pypi]\n[![Downloads][badge-downloads]][link-downloads]\n[![Python][badge-python]][link-python]\n[![Appium][badge-appium]][link-appium]\n\n___\n\n[![Pyright Type Check][badge-pyright]][workflow-pyright]\n[![Ruff Lint][badge-ruff]][workflow-ruff]\n[![Unit Tests][badge-unit-tests]][workflow-unit-tests]\n[![Integration Tests][badge-integration]][workflow-integration]\n\n___\n\n[badge-license]: https://img.shields.io/badge/license-MIT-blue\n[link-license]: LICENSES/MIT.txt\n[badge-license-check]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/license-check.yml/badge.svg\n[workflow-license-check]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/license-check.yml\n[badge-deepwiki]: https://deepwiki.com/badge.svg\n[link-deepwiki]: https://deepwiki.com/molokov-klim/Appium-Python-Client-Shadowstep\n[badge-youtube]: https://img.shields.io/badge/YouTube-red?logo=youtube\n[link-youtube]: https://www.youtube.com/playlist?list=PLGFbKpf3cI31d1TLlQXCszl88dutdruKx\n[badge-pypi]: https://badge.fury.io/py/appium-python-client-shadowstep.svg\n[link-pypi]: https://badge.fury.io/py/appium-python-client-shadowstep\n[badge-downloads]: https://pepy.tech/badge/appium-python-client-shadowstep\n[link-downloads]: https://pepy.tech/project/appium-python-client-shadowstep\n[badge-python]: https://img.shields.io/badge/python-3.9%2B-blue\n[link-python]: https://www.python.org\n[badge-appium]: https://img.shields.io/badge/appium-5.2.2%2B-blue\n[link-appium]: https://appium.io\n[badge-pyright]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/pyright.yml/badge.svg\n[workflow-pyright]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/pyright.yml\n[badge-ruff]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/ruff.yml/badge.svg\n[workflow-ruff]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/ruff.yml\n[badge-unit-tests]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/unit_tests.yml/badge.svg\n[workflow-unit-tests]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/unit_tests.yml\n[badge-integration]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/integration_tests.yml/badge.svg\n[workflow-integration]: https://github.com/molokov-klim/Appium-Python-Client-Shadowstep/actions/workflows/integration_tests.yml\n\u003c!-- markdownlint-enable MD013 --\u003e\n\n## Table of Contents\n\n- [Key Features](#key-features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Architecture](#architecture)\n- [Core API](#core-api)\n  - [Shadowstep (Facade)](#shadowstep-facade)\n  - [Element (Facade)](#element-facade)\n  - [PageBase](#pagebase)\n- [Additional Modules](#additional-modules)\n  - [Navigator](#navigator)\n  - [Locator System](#locator-system)\n  - [Terminal](#terminal)\n  - [Logcat](#logcat)\n  - [Image Recognition](#image-recognition)\n  - [Page Object Generator](#page-object-generator)\n- [Usage Examples](#usage-examples)\n- [Quality Tools](#quality-tools)\n\n___\n\n## Key Features\n\n### Architectural Patterns\n\n- **Facade Pattern** — simplified interface for Appium interactions\n- **Page Object Pattern** — structured UI representation\n- **Singleton Pattern** — single point of access to driver\n- **Navigator Pattern** — graph-based page navigation\n- **DSL over locator syntax** — type-safe fluent API for UiSelector with IDE autocomplete  \n- **Flexible locator system** — dict, xpath, UiSelector with auto-conversion\n\n### Functionality\n\n- **Flexible locator system** — dict, xpath, UiSelector with auto-conversion\n- **Rich DOM navigation** — parent, sibling, cousin relationships\n- **Advanced gestures** — tap, swipe, fling, scroll, pinch, zoom\n- **Lazy/Greedy element search** — performance optimization\n- **Fail-safe decorators** — automatic error handling and reconnection\n- **Built-in logging** — Loguru-style colored output\n- **Image Recognition** — find elements by images (OpenCV)\n- **Logcat Streaming** — capture logs via WebSocket\n- **Page Object Generator** — auto-generate page objects from XML\n- **SSH/ADB Support** — remote command execution\n\n___\n\n## Installation\n\n### Requirements\n\n- Python 3.9+\n- Appium Server 2.x\n- UiAutomator2 Driver\n- Android Device/Emulator\n\n### Install via pip\n\n```bash\npip install appium-python-client-shadowstep\n```\n\n### Install via uv (recommended)\n\n```bash\n# Install uv\npip install uv\n\n# Create virtual environment and install dependencies\nuv venv\nsource .venv/bin/activate  # Linux/Mac\n# or\n.venv\\Scripts\\activate     # Windows\n\nuv pip install appium-python-client-shadowstep\n```\n\n### Dependencies\n\nCore:\n\n- `Appium-Python-Client \u003e= 5.2.2`\n- `selenium \u003e= 4.36`\n- `networkx \u003e= 3.2.1` — navigation\n- `opencv-python \u003e= 4.12.0.88` — image recognition\n- `paramiko \u003e= 4.0.0` — SSH\n- `websocket-client \u003e= 1.8.0` — logcat\n\nAdditional:\n\n- `lxml \u003e= 6.0.2` — XML parsing\n- `jinja2 \u003e= 3.1.6` — template engine\n- `pytesseract \u003e= 0.3.10` — OCR\n\n___\n\n## Quick Start\n\n### 1. Start Appium Server\n\n```bash\nappium --use-drivers=uiautomator2\n```\n\n### 2. Basic Example\n\n```python\nfrom shadowstep import Shadowstep\n\n# Connect to device\napp = Shadowstep()\napp.connect(\n    capabilities={\n        \"platformName\": \"Android\",\n        \"appium:automationName\": \"UiAutomator2\",\n        \"appium:deviceName\": \"emulator-5554\",\n        \"appium:appPackage\": \"com.android.settings\",\n        \"appium:appActivity\": \".Settings\",\n    }\n)\n\n# Find and interact with element\nelement = app.get_element({\"text\": \"Network \u0026 internet\"})\nelement.tap()\n\n# Wait for element\nelement.wait_visible(timeout=10)\n\n# Check properties\nprint(element.text)  # \"Network \u0026 internet\"\nprint(element.is_displayed())  # True\n\n# Disconnect\napp.disconnect()\n```\n\n### 3. Page Object Example\n\n```python\nfrom shadowstep import PageBaseShadowstep, Element\n\n\nclass PageSettings(PageBaseShadowstep):\n    @property\n    def edges(self):\n        return {\n            \"PageNetworkInternet\": self.to_network_internet,\n        }\n\n    @property\n    def title(self) -\u003e Element:\n        return self.shadowstep.get_element({\n            \"text\": \"Settings\",\n            \"resource-id\": \"com.android.settings:id/homepage_title\"\n        })\n\n    @property\n    def network_internet(self) -\u003e Element:\n        return self.recycler.scroll_to_element({\n            \"text\": \"Network \u0026 internet\"\n        })\n\n    @property\n    def recycler(self) -\u003e Element:\n        return self.shadowstep.get_element({\n            \"resource-id\": \"com.android.settings:id/settings_homepage_container\"\n        })\n\n    def to_network_internet(self):\n        self.network_internet.tap()\n        return self.shadowstep.get_page(\"PageNetworkInternet\")\n\n    def is_current_page(self) -\u003e bool:\n        return self.title.is_visible()\n\n\n# Usage\napp = Shadowstep()\n# ... connect ...\n\npage = app.get_page(\"PageSettings\")\nassert page.is_current_page()\npage.to_network_internet()\n```\n\n___\n\n## Architecture\n\n### Facade Pattern\n\nThe project implements **Facade Pattern** at two levels:\n\n#### 1. Shadowstep (Main Facade)\n\n`Shadowstep` — the main facade that hides the complexity of Appium WebDriver\ninteractions and provides a simple API.\n\n```python\nclass Shadowstep(ShadowstepBase):\n    \"\"\"Main Facade for mobile automation.\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.navigator = PageNavigator(self)\n        self.converter = LocatorConverter()\n        self.mobile_commands = MobileCommands()\n```\n\n**Hidden subsystems:**\n\n- `ShadowstepBase` — WebDriver management, connections\n- `PageNavigator` — page navigation\n- `LocatorConverter` — locator conversion\n- `MobileCommands` — UiAutomator2 commands\n- `Terminal/Transport` — ADB and SSH\n- `ShadowstepLogcat` — logging\n\n#### 2. Element (Element Facade)\n\n`Element` — facade for working with mobile elements, combining multiple\nspecialized classes.\n\n```python\nclass Element(ElementBase):\n    \"\"\"Public API for Element.\"\"\"\n\n    def __init__(self, locator, shadowstep, ...):\n        super().__init__(...)\n        self.utilities = ElementUtilities(self)\n        self.properties = ElementProperties(self)\n        self.dom = ElementDOM(self)\n        self.actions = ElementActions(self)\n        self.gestures = ElementGestures(self)\n        self.coordinates = ElementCoordinates(self)\n        self.screenshots = ElementScreenshots(self)\n        self.waiting = ElementWaiting(self)\n```\n\n**Hidden subsystems:**\n\n- `ElementDOM` — finding related elements (parent, sibling, cousin)\n- `ElementActions` — text input, clearing\n- `ElementGestures` — tap, swipe, scroll, fling\n- `ElementProperties` — attributes, states\n- `ElementCoordinates` — coordinates, center\n- `ElementScreenshots` — screenshots\n- `ElementWaiting` — waits\n- `ElementUtilities` — helper functions\n\n### Architecture Diagram\n\n```text\n ┌─────────────────────────────────────────────────────────────┐\n │                     User/Test Code                          │\n └──────────────────────┬──────────────────────────────────────┘\n                       │\n         ┌─────────────┴─────────────┐\n         │                           │\n         ▼                           ▼\n ┌────────────────────┐      ┌──────────────────┐\n │  Shadowstep        │◄─────┤  PageBase        │\n │  (Main Facade)     │      │  (Page Objects)  │\n └────────┬───────────┘      └──────────────────┘\n         │\n         ├─► Navigator (Page Graph)\n         ├─► LocatorConverter\n         ├─► MobileCommands\n         ├─► Terminal/Transport\n         └─► ShadowstepLogcat\n         │\n         ▼\n ┌────────────────────┐\n │  Element (Facade)  │\n └────────┬───────────┘\n         │\n         ├─► ElementDOM\n         ├─► ElementActions\n         ├─► ElementGestures\n         ├─► ElementProperties\n         ├─► ElementCoordinates\n         ├─► ElementScreenshots\n         └─► ElementWaiting\n         │\n         ▼\n ┌────────────────────┐\n │  Appium/Selenium   │\n │  (WebDriver)       │\n └────────────────────┘\n```\n\n___\n\n## Core API\n\n### Shadowstep (Facade)\n\nMain facade class for managing mobile testing.\n\n#### Device Connection\n\n```python\n# Via capabilities\napp.connect(\n    capabilities={\n        \"platformName\": \"Android\",\n        \"appium:automationName\": \"UiAutomator2\",\n        \"appium:deviceName\": \"emulator-5554\",\n        \"appium:appPackage\": \"com.android.settings\",\n        \"appium:appActivity\": \".Settings\",\n    },\n    server_ip=\"127.0.0.1\",\n    server_port=4723\n)\n\n# Via options\nfrom appium.options.android import UiAutomator2Options\n\noptions = UiAutomator2Options()\noptions.platform_name = \"Android\"\noptions.device_name = \"emulator-5554\"\noptions.app_package = \"com.android.settings\"\n\napp.connect(\n    capabilities={},\n    options=options\n)\n\n# With SSH for remote server\napp.connect(\n    capabilities={...},\n    server_ip=\"192.168.1.100\",\n    ssh_user=\"user\",\n    ssh_password=\"password\"\n)\n\n# Check connection\nif app.is_connected():\n    print(\"Connected successfully\")\n\n# Reconnect\napp.reconnect()\n\n# Disconnect\napp.disconnect()\n```\n\n#### Finding Elements\n\n```python\n# Via dict\nelement = app.get_element({\n    \"text\": \"Network \u0026 internet\",\n    \"resource-id\": \"android:id/title\"\n})\n\n# Via xpath\nelement = app.get_element((\"xpath\", '//android.widget.TextView[@text=\"Settings\"]'))\n\n# Via UiSelector\nfrom shadowstep.locator import UiSelector\n\nelement = app.get_element(UiSelector().text(\"Settings\"))\n\n# Multiple search (greedy)\nelements = app.get_elements({\"class\": \"android.widget.TextView\"})\nfor el in elements:\n    print(el.text)\n\n# With timeout and polling\nelement = app.get_element(\n    locator={\"text\": \"Network\"},\n    timeout=30,\n    poll_frequency=0.5\n)\n```\n\n#### Screen-Level Gestures\n\n```python\n# Tap by coordinates\napp.tap(x=500, y=1000, duration=100)\n\n# Click\napp.click(x=500, y=1000)\n\n# Double click\napp.double_click(x=500, y=1000)\n\n# Long click\napp.long_click(x=500, y=1000, duration=1000)\n\n# Swipe\napp.swipe(\n    left=100, top=500,\n    width=800, height=400,\n    direction=\"up\",\n    percent=0.75,\n    speed=5000\n)\n\n# Swipe shortcuts\napp.swipe_up(percent=0.75, speed=5000)\napp.swipe_down(percent=0.75)\napp.swipe_left()\napp.swipe_right()\n\n# Scroll\napp.scroll(\n    left=100, top=500,\n    width=800, height=400,\n    direction=\"down\",\n    percent=0.5,\n    speed=2000\n)\n\n# Drag\napp.drag(start_x=500, start_y=1000, end_x=500, end_y=500, speed=2500)\n\n# Fling\napp.fling(\n    left=100, top=500,\n    width=800, height=400,\n    direction=\"up\",\n    speed=7500\n)\n\n# Pinch (zoom)\napp.pinch_open(left=100, top=500, width=800, height=600, percent=0.5)\napp.pinch_close(left=100, top=500, width=800, height=600, percent=0.5)\n```\n\n#### Screenshots and Page Source\n\n```python\n# Get screenshot\nscreenshot = app.get_screenshot()  # bytes\n\n# Save screenshot\napp.save_screenshot(path=\"/tmp\", filename=\"screen.png\")\n\n# Save page source\napp.save_source(path=\"/tmp\", filename=\"page.xml\")\n```\n\n#### Application Management\n\n```python\n# Start activity\napp.start_activity(\n    intent=\"com.android.settings/.Settings\",\n    component=\"com.android.settings/.Settings\"\n)\n\n# Get current application\npackage = app.get_current_package()  # \"com.android.settings\"\nactivity = app.get_current_activity()  # \".Settings\"\n\n# Background/Foreground\napp.background_app(seconds=2)\napp.activate_app(app_id=\"com.android.settings\")\n\n# Check installation\nis_installed = app.is_app_installed(app_id=\"com.android.settings\")\n\n# Application state\nstate = app.query_app_state(app_id=\"com.android.settings\")\n# 0=not installed, 1=not running, 2=background, 3=background+suspended, 4=foreground\n\n# Terminate application\napp.terminate_app(app_id=\"com.android.settings\")\n\n# Clear data\napp.clear_app(app_id=\"com.android.settings\")\n```\n\n#### System Commands\n\n```python\n# Press keys\napp.press_key(keycode=3)  # HOME\napp.press_key(keycode=4)  # BACK\n\n# Open notifications\napp.open_notifications()\n\n# Lock/unlock\napp.lock()\napp.unlock(key=\"1234\", unlock_type=\"pin\")\nis_locked = app.is_locked()\n\n# Shell commands\nresult = app.shell(\"echo test\")\n\n# Type text\napp.type(text=\"test input\")\n\n# Keyboard\nis_shown = app.is_keyboard_shown()\napp.hide_keyboard()\n```\n\n#### File Operations\n\n```python\nimport base64\n\n# Push file\ncontent = base64.b64encode(b\"test content\").decode()\napp.push_file(remote_path=\"/sdcard/test.txt\", payload=content)\n\n# Pull file\ncontent = app.pull_file(remote_path=\"/sdcard/test.txt\")\ndecoded = base64.b64decode(content)\n\n# Pull folder\nfolder_data = app.pull_folder(remote_path=\"/sdcard/Android\")\n\n# Delete file\napp.delete_file(remote_path=\"/sdcard/test.txt\")\n\n# Via ADB wrapper\napp.push(source_file_path=\"local.txt\", destination_file_path=\"/sdcard/test.txt\")\n```\n\n#### Clipboard\n\n```python\nimport base64\n\n# Set clipboard text\ntext = \"test clipboard\"\nencoded = base64.b64encode(text.encode()).decode()\napp.set_clipboard(content=encoded)\n\n# Get clipboard text\nclipboard = app.get_clipboard()\ndecoded = base64.b64decode(clipboard).decode()\n```\n\n#### Screen Recording\n\n```python\n# Start recording\napp.start_recording_screen()\n\n# Stop recording\nvideo_bytes = app.stop_recording_screen()\n\n# Save video\nwith open(\"recording.mp4\", \"wb\") as f:\n    f.write(video_bytes)\n```\n\n#### Network Settings\n\n```python\n# Get network state\nconnectivity = app.get_connectivity(services=[\"wifi\", \"data\"])\n\n# Set state\napp.set_connectivity(wifi=True, data=False)\n\n# Bluetooth\napp.bluetooth(action=\"enable\")\napp.bluetooth(action=\"disable\")\n\n# GPS\napp.toggle_gps()\nis_enabled = app.is_gps_enabled()\n\n# NFC\napp.nfc(action=\"enable\")\napp.nfc(action=\"disable\")\n\n# Geolocation\napp.set_geolocation(latitude=37.7749, longitude=-122.4194, altitude=10.0)\nlocation = app.get_geolocation(latitude=37.7749, longitude=-122.4194, altitude=10.0)\napp.reset_geolocation()\napp.refresh_gps_cache(timeout_ms=5000)\n```\n\n#### Device Information\n\n```python\n# Battery\nbattery = app.battery_info()\n# {\"level\": 80, \"state\": 2, \"temperature\": 25, ...}\n\n# Device\ndevice = app.device_info()\n\n# Display density\ndensity = app.get_display_density()  # 420\n\n# System bars\nbars = app.get_system_bars()\n# {\"statusBar\": {...}, \"navigationBar\": {...}}\n\n# Device time\ntime_str = app.get_device_time()\n\n# Performance data\ntypes = app.get_performance_data_types()\nperf_data = app.get_performance_data(\n    package_name=\"com.android.settings\",\n    data_type=\"cpuinfo\"\n)\n```\n\n#### Page Navigation\n\n```python\n# Get page instance\nsettings_page = app.get_page(\"PageSettings\")\n\n# Navigate via graph\nsettings_page = app.get_page(\"PageSettings\")\nnetwork_page = settings_page.to_network_internet()\n\n# Resolve page\npage = app.resolve_page(\"PageNetworkInternet\")\n```\n\n___\n\n### Element (Facade)\n\nFacade class for interacting with UI elements.\n\n#### Creating Element\n\n```python\n# Via Shadowstep\nelement = app.get_element({\"text\": \"Settings\"})\n\n# Directly\nfrom shadowstep.element import Element\n\nelement = Element(\n    locator={\"text\": \"Settings\"},\n    shadowstep=app,\n    timeout=30,\n    poll_frequency=0.5\n)\n\n# With native WebElement\nfrom appium.webdriver.webelement import WebElement\n\nnative_el = driver.find_element(...)\nelement = Element(\n    locator={\"text\": \"Settings\"},\n    shadowstep=app,\n    native=native_el\n)\n```\n\n#### DOM Navigation\n\n```python\nelement = app.get_element({\"text\": \"Network \u0026 internet\"})\n\n# Search within element (lazy)\ninner = element.get_element({\"class\": \"android.widget.TextView\"})\n\n# Multiple search (greedy)\nchildren = element.get_elements({\"class\": \"android.widget.TextView\"})\n\n# Parent\nparent = element.get_parent()\nall_parents = element.get_parents()\n\n# Sibling\nsibling = element.get_sibling({\"resource-id\": \"android:id/summary\"})\nall_siblings = element.get_siblings({\"class\": \"android.widget.TextView\"})\n\n# Cousin (sibling of parent)\ncousin = element.get_cousin(\n    cousin_locator={\"text\": \"Apps\"},\n    depth_to_parent=1  # go up 1 level\n)\ncousins = element.get_cousins(\n    cousin_locator={\"class\": \"android.widget.TextView\"},\n    depth_to_parent=2\n)\n```\n\n#### Actions (input)\n\n```python\nelement = app.get_element({\"resource-id\": \"search_field\"})\n\n# Send keys\nelement.send_keys(\"test query\")\n\n# Clear\nelement.clear()\n\n# Set value (not supported in UiAutomator2)\nelement.set_value(\"new value\")\n\n# Submit (not supported in UiAutomator2)\nelement.submit()\n```\n\n#### Gestures\n\n```python\nelement = app.get_element({\"text\": \"Settings\"})\n\n# Tap\nelement.tap()\nelement.tap(duration=3000)  # long tap\n\n# Tap and move\nelement.tap_and_move(x=100, y=500)\nelement.tap_and_move(locator={\"text\": \"Apps\"})\nelement.tap_and_move(direction=0, distance=1000)  # up\n\n# Click\nelement.click()\nelement.click(duration=3000)\nelement.double_click()\n\n# Drag\nelement.drag(end_x=500, end_y=1000, speed=2500)\n\n# Fling\nelement.fling(speed=2500, direction=\"up\")\nelement.fling_up(speed=2500)\nelement.fling_down()\nelement.fling_left()\nelement.fling_right()\n\n# Scroll\nrecycler = app.get_element({\"resource-id\": \"recycler_view\"})\nrecycler.scroll(direction=\"down\", percent=0.7, speed=2000)\nrecycler.scroll_down(percent=0.7)\nrecycler.scroll_up()\nrecycler.scroll_left()\nrecycler.scroll_right()\n\n# Scroll to top/bottom\nrecycler.scroll_to_top(percent=0.7, speed=8000)\nrecycler.scroll_to_bottom()\n\n# Scroll to element\ntarget = recycler.scroll_to_element(\n    locator={\"text\": \"About phone\"},\n    max_swipes=30\n)\n\n# Swipe\nelement.swipe(direction=\"up\", percent=0.75, speed=5000)\nelement.swipe_up()\nelement.swipe_down()\nelement.swipe_left()\nelement.swipe_right()\n\n# Zoom\nelement.zoom(percent=0.75, speed=2500)\nelement.unzoom(percent=0.75, speed=2500)\n```\n\n#### Properties\n\n```python\nelement = app.get_element({\"text\": \"Network \u0026 internet\"})\n\n# Attributes\ntext = element.get_attribute(\"text\")\nattrs = element.get_attributes()  # all attributes from XML\n\n# DOM attribute\ncontent_desc = element.get_dom_attribute(\"content-desc\")\n\n# Property (not supported in UiAutomator2)\nprop = element.get_property(\"checked\")\n\n# States\nis_displayed = element.is_displayed()\nis_visible = element.is_visible()\nis_enabled = element.is_enabled()\nis_selected = element.is_selected()\n\n# Check containment\nhas_child = element.is_contains({\"class\": \"android.widget.TextView\"})\n\n# Properties via property\ntag = element.tag_name\nall_attrs = element.attributes\ntext = element.text\nresource_id = element.resource_id\nclass_name = element.class_name\nclass_ = element.class_  # alternative\nindex = element.index\npackage = element.package\nbounds = element.bounds\n\n# Boolean properties\nchecked = element.checked\ncheckable = element.checkable\nenabled = element.enabled\nfocusable = element.focusable\nfocused = element.focused\nlong_clickable = element.long_clickable\npassword = element.password\nscrollable = element.scrollable\nselected = element.selected\ndisplayed = element.displayed\n\n# Size and position\nsize = element.size  # {\"width\": 800, \"height\": 100}\nlocation = element.location  # {\"x\": 100, \"y\": 500}\nrect = element.rect  # {\"x\": 100, \"y\": 500, \"width\": 800, \"height\": 100}\nlocation_in_view = element.location_in_view\n\n# Shadow root (not supported in UiAutomator2)\nshadow_root = element.shadow_root\n\n# CSS (not supported in UiAutomator2)\ncss_value = element.value_of_css_property(\"color\")\n\n# ARIA (not supported in UiAutomator2)\naria_role = element.aria_role\naccessible_name = element.accessible_name\n```\n\n#### Coordinates\n\n```python\nelement = app.get_element({\"text\": \"Settings\"})\n\n# Coordinates (x, y, width, height)\nx, y, width, height = element.get_coordinates()\n\n# Element center\ncenter_x, center_y = element.get_center()\n\n# Location in view\nloc = element.location_in_view  # {\"x\": 100, \"y\": 500}\n\n# Location once scrolled (not supported in UiAutomator2)\nloc = element.location_once_scrolled_into_view\n```\n\n#### Screenshots\n\n```python\nelement = app.get_element({\"text\": \"Settings\"})\n\n# Base64\nscreenshot_b64 = element.screenshot_as_base64\n\n# PNG bytes\nscreenshot_png = element.screenshot_as_png\n\n# Save to file\nsuccess = element.save_screenshot(\"/tmp/element.png\")\n```\n\n#### Waiting\n\n```python\nelement = app.get_element({\"text\": \"Network \u0026 internet\"})\n\n# Wait until present\nelement.wait(timeout=10, poll_frequency=0.5)\n# or return bool\nsuccess = element.wait(timeout=10, return_bool=True)\n\n# Wait until visible\nelement.wait_visible(timeout=10)\n\n# Wait until clickable\nelement.wait_clickable(timeout=10)\n\n# Wait until NOT present\nelement.wait_for_not(timeout=10)\n\n# Wait until NOT visible\nelement.wait_for_not_visible(timeout=10)\n\n# Wait until NOT clickable\nelement.wait_for_not_clickable(timeout=10)\n```\n\n#### Should (DSL assertions)\n\n```python\nelement = app.get_element({\"text\": \"Settings\"})\n\n# Fluent assertions\nelement.should.be_visible()\nelement.should.be_enabled()\nelement.should.have_text(\"Settings\")\nelement.should.have_attribute(\"text\", \"Settings\")\nelement.should.be_displayed()\nelement.should.be_clickable()\n\n# Negative checks\nelement.should.not_be_visible()\nelement.should.not_have_text(\"Other\")\n```\n\n#### Native WebElement\n\n```python\nelement = app.get_element({\"text\": \"Settings\"})\n\n# Get native WebElement\nnative = element.get_native()\nnative.click()\n```\n\n___\n\n### PageBase\n\nAbstract base class for Page Object pattern with automatic navigation.\n\n#### Creating Page Object\n\n```python\nfrom shadowstep import PageBaseShadowstep, Element\n\n\nclass PageSettings(PageBaseShadowstep):\n    \"\"\"Settings page representation.\"\"\"\n\n    # Required: define relationships with other pages\n    @property\n    def edges(self):\n        return {\n            \"PageNetworkInternet\": self.to_network_internet,\n            \"PageAboutPhone\": self.to_about_phone,\n        }\n\n    # Page name\n    @property\n    def name(self) -\u003e str:\n        return \"Settings\"\n\n    # Title element for page verification\n    @property\n    def title(self) -\u003e Element:\n        return self.shadowstep.get_element({\n            \"text\": \"Settings\",\n            \"resource-id\": \"com.android.settings:id/homepage_title\"\n        })\n\n    # Recycler (scrollable container)\n    @property\n    def recycler(self) -\u003e Element:\n        return self.shadowstep.get_element({\n            \"resource-id\": \"com.android.settings:id/settings_homepage_container\"\n        })\n\n    # Page elements\n    @property\n    def network_internet(self) -\u003e Element:\n        return self.recycler.scroll_to_element({\n            \"text\": \"Network \u0026 internet\",\n            \"resource-id\": \"android:id/title\"\n        })\n\n    @property\n    def network_internet_summary(self) -\u003e Element:\n        return self.network_internet.get_sibling({\n            \"resource-id\": \"android:id/summary\"\n        })\n\n    @property\n    def about_phone(self) -\u003e Element:\n        return self.recycler.scroll_to_element({\n            \"text\": \"About phone\"\n        })\n\n    # Navigation methods\n    def to_network_internet(self):\n        \"\"\"Navigate to Network \u0026 Internet page.\"\"\"\n        self.network_internet.tap()\n        return self.shadowstep.get_page(\"PageNetworkInternet\")\n\n    def to_about_phone(self):\n        \"\"\"Navigate to About Phone page.\"\"\"\n        self.about_phone.tap()\n        return self.shadowstep.get_page(\"PageAboutPhone\")\n\n    # Required: check current page\n    def is_current_page(self) -\u003e bool:\n        \"\"\"Check if Settings page is currently displayed.\"\"\"\n        try:\n            return self.title.is_visible()\n        except Exception:\n            return False\n```\n\n#### Using Page Objects\n\n```python\n# Get instance (singleton)\nsettings = app.get_page(\"PageSettings\")\n\n# Check current page\nassert settings.is_current_page()\n\n# Interact with elements\nprint(settings.network_internet.text)\nprint(settings.network_internet_summary.text)\n\n# Navigate\nnetwork_page = settings.to_network_internet()\nassert network_page.is_current_page()\n\n# Clear singleton\nPageSettings.clear_instance()\n```\n\n#### Automatic Navigation (Navigator)\n\nNavigator automatically finds paths between pages through the graph.\n\n```python\nfrom shadowstep.navigator import PageNavigator\n\n# Navigator is created automatically in Shadowstep\n# app.navigator = PageNavigator(app)\n\n# List registered pages\napp.navigator.list_registered_pages()\n\n# Navigate with automatic pathfinding\ncurrent_page = app.get_page(\"PageSettings\")\ntarget_page = app.get_page(\"PageAboutPhone\")\n\n# Navigator will find shortest path through graph\nsuccess = app.navigator.navigate(\n    from_page=current_page,\n    to_page=target_page,\n    timeout=10\n)\n```\n\n___\n\n## Additional Modules\n\n### Navigator\n\nGraph-based navigation system between pages.\n\n#### How it Works\n\n1. Each page defines `edges` — relationships with other pages\n2. Navigator builds a graph from all pages\n3. During navigation, uses shortest path algorithm (NetworkX or BFS fallback)\n\n```python\nfrom shadowstep.navigator import PageNavigator\n\nnavigator = PageNavigator(app)\n\n# Auto-discover pages in sys.path\nnavigator.auto_discover_pages()\n\n# Add page manually\npage = PageSettings()\nnavigator.add_page(page, edges=page.edges)\n\n# Find path\npath = navigator.find_path(\n    start=PageSettings(),\n    target=PageAboutPhone()\n)\n# [\"PageSettings\", \"PageNetworkInternet\", \"PageAboutPhone\"]\n\n# Navigate through path\nnavigator.perform_navigation(path, timeout=10)\n\n# Direct navigation\nsuccess = navigator.navigate(\n    from_page=PageSettings(),\n    to_page=PageAboutPhone(),\n    timeout=10\n)\n```\n\n___\n\n### Locator System\n\nFlexible locator system supporting three formats: dict, xpath, UiSelector.\n\n#### Locator Types\n\n##### 1. Dictionary (Shadowstep Dict)\n\n```python\n# Simple locator\nlocator = {\"text\": \"Settings\"}\n\n# Compound locator\nlocator = {\n    \"text\": \"Network \u0026 internet\",\n    \"resource-id\": \"android:id/title\",\n    \"class\": \"android.widget.TextView\"\n}\n\n# With contains\nlocator = {\"textContains\": \"Network\"}\n\n# With starts-with\nlocator = {\"textStartsWith\": \"Net\"}\n\n# With matches (regex)\nlocator = {\"textMatches\": \"Net.*\"}\n\n# All UiSelector attributes supported\nlocator = {\n    \"text\": \"Settings\",\n    \"clickable\": True,\n    \"index\": 0,\n    \"instance\": 0\n}\n```\n\n##### 2. XPath\n\n```python\n# Simple xpath\nlocator = (\"xpath\", '//android.widget.TextView[@text=\"Settings\"]')\n\n# With functions\nlocator = (\"xpath\", '//android.widget.TextView[contains(@text, \"Network\")]')\nlocator = (\"xpath\", '//android.widget.TextView[starts-with(@text, \"Net\")]')\n\n# With attributes\nlocator = (\"xpath\", '//*[@resource-id=\"android:id/title\" and @text=\"Settings\"]')\n\n# With indices\nlocator = (\"xpath\", '(//android.widget.TextView)[1]')\n\n# Parent/child\nlocator = (\"xpath\", '//android.widget.ScrollView//android.widget.TextView')\n```\n\n##### 3. UiSelector\n\n```python\nfrom shadowstep.locator import UiSelector\n\n# Simple selector\nlocator = UiSelector().text(\"Settings\")\n\n# Chaining\nlocator = (UiSelector()\n           .text(\"Network \u0026 internet\")\n           .resourceId(\"android:id/title\")\n           .className(\"android.widget.TextView\"))\n\n# Contains\nlocator = UiSelector().textContains(\"Network\")\n\n# Starts with\nlocator = UiSelector().textStartsWith(\"Net\")\n\n# Matches (regex)\nlocator = UiSelector().textMatches(\"Net.*\")\n\n# Boolean properties\nlocator = UiSelector().clickable(True).enabled(True)\n\n# Index and instance\nlocator = UiSelector().className(\"android.widget.TextView\").index(0)\nlocator = UiSelector().className(\"android.widget.TextView\").instance(2)\n\n# Description\nlocator = UiSelector().description(\"Phone\")\nlocator = UiSelector().descriptionContains(\"Pho\")\n\n# Package\nlocator = UiSelector().packageName(\"com.android.settings\")\n\n# Child selector\nparent = UiSelector().className(\"android.widget.ScrollView\")\nchild = UiSelector().text(\"Settings\")\nlocator = parent.childSelector(child)\n\n# From parent\nlocator = UiSelector().text(\"Settings\").fromParent(UiSelector().className(\"android.widget.LinearLayout\"))\n```\n\n#### Locator Conversion\n\n```python\nfrom shadowstep.locator import LocatorConverter\n\nconverter = LocatorConverter()\n\n# Dict -\u003e XPath\ndict_loc = {\"text\": \"Settings\", \"class\": \"android.widget.TextView\"}\nxpath = converter.dict_to_xpath(dict_loc)\n# '//*[@text=\"Settings\" and @class=\"android.widget.TextView\"]'\n\n# Dict -\u003e UiSelector\nui_selector = converter.dict_to_ui_selector(dict_loc)\n# 'new UiSelector().text(\"Settings\").className(\"android.widget.TextView\")'\n\n# UiSelector -\u003e Dict\nui_loc = UiSelector().text(\"Settings\").clickable(True)\ndict_loc = converter.ui_selector_to_dict(str(ui_loc))\n# {\"text\": \"Settings\", \"clickable\": True}\n\n# UiSelector -\u003e XPath\nxpath = converter.ui_selector_to_xpath(str(ui_loc))\n\n# XPath -\u003e Dict\nxpath = '//android.widget.TextView[@text=\"Settings\"]'\ndict_loc = converter.xpath_to_dict(xpath)\n# {\"text\": \"Settings\", \"class\": \"android.widget.TextView\"}\n\n# XPath -\u003e UiSelector\nui_selector = converter.xpath_to_ui_selector(xpath)\n```\n\n___\n\n### Terminal\n\nTwo options for command execution: via Appium (Terminal) and via SSH (Transport).\n\n#### Terminal (via Appium)\n\n```python\n# Terminal is created automatically on connect()\nterminal = app.terminal\n\n# Shell commands\nresult = terminal.adb_shell(command=\"dumpsys\", args=\"window windows\")\nresult = terminal.adb_shell(command=\"pm\", args=\"list packages\")\n\n# Application management\nterminal.start_activity(package=\"com.android.settings\", activity=\".Settings\")\nterminal.close_app(package=\"com.android.settings\")\nterminal.reboot_app(package=\"com.android.settings\", activity=\".Settings\")\n\npackage = terminal.get_current_app_package()\n\n# Check installation\nis_installed = terminal.is_app_installed(package=\"com.android.settings\")\nterminal.uninstall_app(package=\"com.android.settings\")\n\n# Buttons\nterminal.press_home()\nterminal.press_back()\nterminal.press_menu()\n\n# Input\nterminal.input_keycode(keycode=\"KEYCODE_ENTER\")\nterminal.input_keycode_num_(num=5)\nterminal.input_text(text=\"hello\")\n\n# Gestures\nterminal.tap(x=500, y=1000)\nterminal.swipe(start_x=500, start_y=1000, end_x=500, end_y=500, duration=300)\nterminal.swipe_right_to_left(duration=300)\nterminal.swipe_left_to_right()\nterminal.swipe_top_to_bottom()\nterminal.swipe_bottom_to_top()\n\n# VPN\nis_connected = terminal.check_vpn(ip_address=\"192.168.1.1\")\n\n# Processes\npid = terminal.know_pid(name=\"logcat\")\nexists = terminal.is_process_exist(name=\"logcat\")\nterminal.kill_by_pid(pid=1234)\nterminal.kill_by_name(name=\"logcat\")\nterminal.kill_all(name=\"logcat\")\nterminal.run_background_process(command=\"logcat\", args=\"-v time\", process=\"logcat\")\n\n# Files\nterminal.delete_file_from_internal_storage(path=\"/sdcard\", filename=\"test.txt\")\nterminal.delete_files_from_internal_storage(path=\"/sdcard/Download\")\n\n# Video\nterminal.record_video(time_limit=180000)\nvideo_bytes = terminal.stop_video()\n\n# System information\nterminal.reboot()\nwidth, height = terminal.get_screen_resolution()\nproperties = terminal.get_prop()\nhardware = terminal.get_prop_hardware()\nmodel = terminal.get_prop_model()\nserial = terminal.get_prop_serial()\nbuild = terminal.get_prop_build()\ndevice = terminal.get_prop_device()\n\n# Packages\npackages = terminal.get_packages()\n\n# WiFi IP\nwifi_ip = terminal.get_wifi_ip()\n\n# Paste text (via clipboard)\nterminal.past_text(text=\"Hello World\", tries=3)\n```\n\n#### Transport (via SSH)\n\n**IMPORTANT:** SSH was removed from Terminal and is now only available via Transport.\n\n```python\n# Transport is created when connect() is called with SSH credentials\napp.connect(\n    capabilities={...},\n    server_ip=\"192.168.1.100\",\n    ssh_user=\"user\",\n    ssh_password=\"password\"\n)\n\n# Access SSH client (paramiko)\nssh_client = app.transport.ssh\n\n# Execute command\nstdin, stdout, stderr = ssh_client.exec_command(\"adb devices\")\noutput = stdout.read().decode()\n\n# Access SCP client\nscp_client = app.transport.scp\n\n# Upload file to server\nscp_client.put(\"local_file.txt\", remote_path=\"/tmp/remote_file.txt\")\n\n# Download file from server\nscp_client.get(\"/tmp/remote_file.txt\", local_path=\"downloaded_file.txt\")\n\n# Recursive folder upload\nscp_client.put(\"local_folder\", remote_path=\"/tmp/remote_folder\", recursive=True)\n```\n\n#### ADB (local)\n\n```python\n# ADB is created automatically on connect()\nadb = app.adb\n\n# Get device list\ndevices = adb.get_devices()  # [\"emulator-5554\", \"192.168.1.100:5555\"]\n\n# Device model\nmodel = adb.get_device_model(udid=\"emulator-5554\")\n\n# Push/Pull files\nadb.push(source=\"local.txt\", destination=\"/sdcard/file.txt\", udid=\"emulator-5554\")\nadb.pull(source=\"/sdcard/file.txt\", destination=\"local.txt\", udid=\"emulator-5554\")\n\n# Install APK\nadb.install_app(source=\"app.apk\", udid=\"emulator-5554\")\nadb.is_app_installed(package=\"com.example.app\")\nadb.uninstall_app(package=\"com.example.app\")\n\n# Application management\nadb.start_activity(package=\"com.android.settings\", activity=\".Settings\")\nadb.get_current_activity()\nadb.get_current_package()\nadb.close_app(package=\"com.android.settings\")\nadb.reboot_app(package=\"com.android.settings\", activity=\".Settings\")\n\n# Buttons\nadb.press_home()\nadb.press_back()\nadb.press_menu()\n\n# Input\nadb.input_keycode(keycode=\"KEYCODE_ENTER\")\nadb.input_keycode_num_(num=5)\nadb.input_text(text=\"hello\")\n\n# Gestures\nadb.tap(x=500, y=1000)\nadb.swipe(start_x=500, start_y=1000, end_x=500, end_y=500, duration=300)\n\n# VPN\nadb.check_vpn(ip_address=\"192.168.1.1\")\n\n# Processes\nadb.stop_logcat()\nadb.is_process_exist(name=\"logcat\")\nadb.run_background_process(command=\"logcat -v time \u0026\", process=\"logcat\")\npid = adb.know_pid(name=\"logcat\")\nadb.kill_by_pid(pid=1234)\nadb.kill_by_name(name=\"logcat\")\nadb.kill_all(name=\"logcat\")\n\n# ADB server\nadb.reload_adb()\n\n# Files\nadb.delete_files_from_internal_storage(path=\"/sdcard/Download\")\n\n# Video\nprocess = adb.record_video(path=\"/sdcard/Movies\", filename=\"recording.mp4\")\n# ... wait ...\nadb.stop_video()\nadb.pull_video(source=\"/sdcard/Movies\", destination=\"./videos\", delete=True)\n\n# System information\nadb.reboot()\nwidth, height = adb.get_screen_resolution()\npackages = adb.get_packages_list()\n\n# Execute arbitrary command\noutput = adb.execute(command=\"shell getprop ro.build.version.release\")\n```\n\n___\n\n### Logcat\n\nAndroid log capture via WebSocket with filtering and automatic reconnection.\n\n```python\n# Start log capture\napp.start_logcat(filename=\"logcat.log\")\n\n# With tag filtering\napp._logcat.filters = [\"ActivityManager\", \"System.out\"]\napp.start_logcat(filename=\"filtered_logcat.log\")\n\n# Stop capture\napp.stop_logcat()\n\n# Context manager\nwith app._logcat:\n    app._logcat.start(filename=\"logcat.log\")\n    # ... run tests ...\n    # automatically stops on exit\n\n# Configuration\nlogcat = app._logcat\nlogcat.filters = [\"MyApp\", \"Firebase\"]  # filter by tags\n# logcat works in background thread with auto-reconnection\n```\n\n**Features:**\n\n- Works via WebSocket to Appium server\n- Automatic reconnection on connection drops\n- Buffered file writing (buffering=1)\n- Tag filtering with regex\n- Graceful shutdown with proper file closing\n\n___\n\n### Image Recognition\n\nFind elements by images using OpenCV.\n\n```python\n# Get ShadowstepImage\nimage_path = \"tests/_test_data/connected_devices.png\"\nimage = app.get_image(\n    image=image_path,\n    threshold=0.5,  # match accuracy [0-1]\n    timeout=5.0  # search timeout\n)\n\n# Can pass bytes, ndarray, PIL.Image or file path\nfrom PIL import Image\n\npil_image = Image.open(\"icon.png\")\nimage = app.get_image(image=pil_image, threshold=0.8)\n\n# Tap on image\nimage.tap()\n\n# Wait for appearance\nimage.wait(timeout=10)\n\n# Check visibility\nif image.is_visible():\n    print(\"Image found on screen\")\n\n# Coordinates\nx, y = image.get_center()\ncoords = image.get_coordinates()\n\n# Multiple search\nimages = app.get_images(image=image_path, threshold=0.7)\nfor img in images:\n    img.tap()\n\n# Screenshot + matching\nscreenshot = app.get_screenshot()  # bytes\n# image.match(screenshot) - internal method\n```\n\n___\n\n### Page Object Generator\n\nAutomatic generation of Page Object classes from UI XML dump.\n\n```python\nfrom shadowstep.page_object import (\n    PageObjectGenerator,\n    PageObjectParser,\n    UiElementNode\n)\n\n# 1. Get XML page source\nxml_source = app.driver.page_source\n\n# 2. Parse XML into element tree\nparser = PageObjectParser()\nui_tree: UiElementNode = parser.parse(xml_source)\n\n# 3. Generate Page Object\ngenerator = PageObjectGenerator()\noutput_path, class_name = generator.generate(\n    ui_element_tree=ui_tree,\n    output_dir=\"./generated_pages\",\n    filename_prefix=\"page_\"\n)\n\nprint(f\"Generated: {output_path}\")\nprint(f\"Class: {class_name}\")\n\n# Result: page_settings.py\n# class PageSettings(PageBaseShadowstep):\n#     @property\n#     def title(self) -\u003e Element: ...\n#     @property\n#     def network_internet(self) -\u003e Element: ...\n#     ...\n```\n\n**Capabilities:**\n\n- Auto-detection of title, recycler\n- Recognition of anchor-switcher pairs (for switch elements)\n- Recognition of anchor-summary pairs\n- Filtering structural containers\n- Generation of navigation methods\n- Uses Jinja2 templates\n- Supports translator (optional)\n\n**Page Object Merger:**\n\n```python\nfrom shadowstep.page_object import PageObjectMerger\n\n# Merge multiple dumps of same screen\nmerger = PageObjectMerger()\n\n# Add dumps\nmerger.add_dump(xml_source_1)\nmerger.add_dump(xml_source_2)\nmerger.add_dump(xml_source_3)\n\n# Get merged tree\nmerged_tree = merger.merge()\n\n# Generate from merged tree\ngenerator.generate(\n    ui_element_tree=merged_tree,\n    output_dir=\"./pages\"\n)\n```\n\n**Page Object Test Generator:**\n\n```python\nfrom shadowstep.page_object import PageObjectTestGenerator\n\n# Generate tests for Page Object\ntest_generator = PageObjectTestGenerator()\ntest_path = test_generator.generate(\n    page_class_name=\"PageSettings\",\n    output_dir=\"./tests\",\n    page_module=\"pages.page_settings\"\n)\n```\n\n___\n\n## Usage Examples\n\n### Basic Testing\n\n```python\nfrom shadowstep import Shadowstep\n\n\ndef test_settings_navigation():\n    app = Shadowstep()\n    app.connect(\n        capabilities={\n            \"platformName\": \"Android\",\n            \"appium:automationName\": \"UiAutomator2\",\n            \"appium:deviceName\": \"emulator-5554\",\n            \"appium:appPackage\": \"com.android.settings\",\n            \"appium:appActivity\": \".Settings\",\n        }\n    )\n\n    # Find element\n    network = app.get_element({\n        \"text\": \"Network \u0026 internet\",\n        \"resource-id\": \"android:id/title\"\n    })\n\n    # Check visibility\n    assert network.is_visible()\n\n    # Interact\n    network.tap()\n\n    # Verify navigation\n    title = app.get_element({\"text\": \"Network \u0026 internet\"})\n    assert title.wait_visible(timeout=5)\n\n    app.disconnect()\n```\n\n### Working with Forms\n\n```python\ndef test_search_form():\n    app = Shadowstep()\n    # ... connect ...\n\n    # Find search field\n    search_field = app.get_element({\n        \"resource-id\": \"com.android.quicksearchbox:id/search_widget_text\"\n    })\n    search_field.tap()\n\n    # Wait for input to appear\n    search_input = app.get_element({\n        \"resource-id\": \"com.android.quicksearchbox:id/search_src_text\"\n    })\n    search_input.wait_visible(timeout=3)\n\n    # Enter text\n    search_input.send_keys(\"test query\")\n\n    # Check value\n    assert \"test query\" in search_input.text\n\n    # Clear\n    search_input.clear()\n    assert search_input.text == \"\"\n```\n\n### Scrolling and Search\n\n```python\ndef test_scroll_to_element():\n    app = Shadowstep()\n    # ... connect to Settings ...\n\n    # Get scrollable container\n    recycler = app.get_element({\n        \"resource-id\": \"com.android.settings:id/settings_homepage_container\"\n    })\n\n    # Scroll to element\n    about_phone = recycler.scroll_to_element(\n        locator={\"text\": \"About phone\"},\n        max_swipes=30\n    )\n\n    # Check element found\n    assert about_phone.is_visible()\n\n    # Interact\n    about_phone.tap()\n```\n\n### DOM Navigation Example\n\n```python\ndef test_dom_navigation():\n    app = Shadowstep()\n    # ... connect to Settings ...\n\n    # Find anchor element\n    network = app.get_element({\n        \"text\": \"Network \u0026 internet\",\n        \"resource-id\": \"android:id/title\"\n    })\n\n    # Find sibling (summary)\n    summary = network.get_sibling({\n        \"resource-id\": \"android:id/summary\"\n    })\n    print(f\"Summary: {summary.text}\")\n\n    # Get parent\n    parent = network.get_parent()\n    print(f\"Parent class: {parent.class_name}\")\n\n    # Find cousin (same level, different parent)\n    cousin = network.get_cousin(\n        cousin_locator={\"resource-id\": \"android:id/summary\"},\n        depth_to_parent=1\n    )\n```\n\n### Multiple Elements\n\n```python\ndef test_multiple_elements():\n    app = Shadowstep()\n    # ... connect to Settings ...\n\n    # Find all TextView\n    textviews = app.get_elements({\n        \"class\": \"android.widget.TextView\"\n    })\n\n    # Process each\n    for tv in textviews:\n        text = tv.text\n        if text and \"Settings\" not in text:\n            print(f\"Found: {text}\")\n```\n\n### Gestures and Animations\n\n```python\ndef test_gestures():\n    app = Shadowstep()\n    # ... connect ...\n\n    # Get element\n    icon = app.get_element({\"content-desc\": \"Gallery\"})\n\n    # Remember position\n    x1, y1 = icon.get_center()\n\n    # Drag\n    icon.drag(end_x=x1 + 200, end_y=y1, speed=2500)\n\n    # Check new position\n    x2, y2 = icon.get_center()\n    assert x2 \u003e x1\n\n    # Drag back\n    icon.drag(end_x=x1, end_y=y1, speed=2500)\n\n    # Fling gesture\n    recycler = app.get_element({\"resource-id\": \"recycler_view\"})\n    recycler.fling_up(speed=5000)\n```\n\n### Page Object with Navigation\n\n```python\nfrom shadowstep import PageBaseShadowstep, Element\n\n\nclass PageSettings(PageBaseShadowstep):\n    @property\n    def edges(self):\n        return {\n            \"PageNetwork\": self.to_network,\n            \"PageApps\": self.to_apps,\n        }\n\n    @property\n    def recycler(self) -\u003e Element:\n        return self.shadowstep.get_element({\n            \"resource-id\": \"com.android.settings:id/settings_homepage_container\"\n        })\n\n    @property\n    def network(self) -\u003e Element:\n        return self.recycler.scroll_to_element({\"text\": \"Network \u0026 internet\"})\n\n    @property\n    def apps(self) -\u003e Element:\n        return self.recycler.scroll_to_element({\"text\": \"Apps\"})\n\n    def to_network(self):\n        self.network.tap()\n        return self.shadowstep.get_page(\"PageNetwork\")\n\n    def to_apps(self):\n        self.apps.tap()\n        return self.shadowstep.get_page(\"PageApps\")\n\n    def is_current_page(self) -\u003e bool:\n        title = self.shadowstep.get_element({\"text\": \"Settings\"})\n        return title.is_visible()\n\n\n# Test\ndef test_page_navigation():\n    app = Shadowstep()\n    # ... connect ...\n\n    settings = app.get_page(\"PageSettings\")\n    assert settings.is_current_page()\n\n    # Automatic navigation via Navigator\n    network = settings.to_network()\n    assert network.is_current_page()\n```\n\n### Screenshots and Logs\n\n```python\ndef test_with_logs_and_screenshots():\n    app = Shadowstep()\n    # ... connect ...\n\n    # Start logcat\n    app.start_logcat(filename=\"test_logs.log\")\n\n    try:\n        # Perform actions\n        element = app.get_element({\"text\": \"Settings\"})\n        element.tap()\n\n        # Take screenshot\n        app.save_screenshot(path=\"./screenshots\", filename=\"settings.png\")\n\n        # Element screenshot\n        element.save_screenshot(\"./screenshots/element.png\")\n\n    finally:\n        # Stop logcat\n        app.stop_logcat()\n        app.disconnect()\n```\n\n### Working with Images\n\n```python\ndef test_image_recognition():\n    app = Shadowstep()\n    # ... connect ...\n\n    # Search by image\n    icon = app.get_image(\n        image=\"icons/settings_icon.png\",\n        threshold=0.8,\n        timeout=10\n    )\n\n    # Check visibility\n    if icon.is_visible():\n        # Tap on image\n        icon.tap()\n\n    # Coordinates\n    x, y = icon.get_center()\n    print(f\"Icon center: {x}, {y}\")\n```\n\n### Working with ADB and SSH\n\n```python\ndef test_adb_commands():\n    app = Shadowstep()\n    # ... connect ...\n\n    # Via Terminal (Appium)\n    app.terminal.start_activity(\n        package=\"com.android.settings\",\n        activity=\".Settings\"\n    )\n\n    # Check current application\n    package = app.terminal.get_current_app_package()\n    assert \"settings\" in package.lower()\n\n    # Via local ADB\n    devices = app.adb.get_devices()\n    print(f\"Connected devices: {devices}\")\n\n    model = app.adb.get_device_model(udid=\"emulator-5554\")\n    print(f\"Device model: {model}\")\n\n\ndef test_ssh_commands():\n    app = Shadowstep()\n    app.connect(\n        capabilities={...},\n        server_ip=\"192.168.1.100\",\n        ssh_user=\"user\",\n        ssh_password=\"password\"\n    )\n\n    # SSH commands via transport\n    stdin, stdout, stderr = app.transport.ssh.exec_command(\"adb devices\")\n    output = stdout.read().decode()\n    print(output)\n\n    # SCP files\n    app.transport.scp.put(\"local.txt\", remote_path=\"/tmp/remote.txt\")\n    app.transport.scp.get(\"/tmp/remote.txt\", local_path=\"downloaded.txt\")\n```\n\n___\n\n## Quality Tools\n\nThe project uses modern tools to ensure code quality:\n\n### Linters and Formatters\n\n```bash\n# Ruff - fast linter and formatter\nuv run ruff check .\nuv run ruff format .\n\n# Pyright - strict typing\nuv run pyright\n```\n\n### Testing\n\n```bash\n# Run all tests\nuv run pytest\n\n# Only unit tests\nuv run pytest tests/test_unit\n\n# Only integration tests\nuv run pytest tests/test_integro\n\n# With coverage\nuv run pytest --cov=shadowstep --cov-report=html\n\n# With rerun failed\nuv run pytest --reruns 3 --reruns-delay 1\n```\n\n### Pre-commit Hooks\n\n```bash\n# Install\nuv run pre-commit install\n\n# Manual run\nuv run pre-commit run --all-files\n```\n\n### Configuration\n\nTool settings are in `pyproject.toml`:\n\n- **Ruff:** `select = [\"ALL\"]` with docstring style conflict ignoring\n- **Pyright:** `typeCheckingMode = \"strict\"` for maximum type safety\n- **Pytest:** logging, short traceback, setup show\n\n___\n\n## Additional Information\n\n### Supported Python Versions\n\n- Python 3.9+\n- Python 3.10\n- Python 3.11\n- Python 3.12\n- Python 3.13\n\n### Links\n\n- [GitHub Repository](https://github.com/molokov-klim/Appium-Python-Client-Shadowstep)\n- [Appium Documentation](https://appium.io/docs/en/latest/)\n- [UiAutomator2 Driver](https://github.com/appium/appium-uiautomator2-driver)\n\n### License\n\nMIT License\n\n___\n\n## Contributing\n\nThe project follows:\n\n- **Clean Architecture** — separation of concerns\n- **Clean Code** — readability and maintainability\n- **Best Practices** — design patterns\n- **Type Safety** — strict typing (Pyright strict mode)\n- **PEP 8** — Python coding style\n\nWhen developing, use:\n\n- Strict typing with `typing`\n- Docstrings in English\n- Comments in English\n- Type hints for all functions and methods\n- Pyright strict mode\n- Ruff for linting\n\n___\n\n**Author:** Molokov Klim  \n**Email:** [ultrakawaii9654449192@gmail.com](mailto:ultrakawaii9654449192@gmail.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolokov-klim%2FAppium-Python-Client-Shadowstep","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmolokov-klim%2FAppium-Python-Client-Shadowstep","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolokov-klim%2FAppium-Python-Client-Shadowstep/lists"}