appium-python-client-shadowstep


Nameappium-python-client-shadowstep JSON
Version 0.31.99 PyPI version JSON
download
home_pageNone
SummaryUI Testing Framework powered by Appium Python Client
upload_time2025-07-23 14:39:17
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseNone
keywords appium testing uiautomator2 android automation framework
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Shadowstep (in development)

**Shadowstep** is a modular UI automation framework for Android applications, built on top of Appium.

It provides:

* Lazy element lookup and interaction (without driver interaction)
* PageObject navigation engine
* Reconnect logic on session failure
* ADB and Appium terminal integration
* DSL-style assertions (`should.have`, `should.be`)

---

## Contents

* [Installation](#installation)
* [Quick Start](#quick-start)
* [Test Setup (Pytest)](#test-setup-pytest)
* [Element API](#element-api)
* [Collections API (`Elements`)](#collections-api-elements)
* [Page Objects and Navigation](#page-objects-and-navigation)
* [ADB and Terminal](#adb-and-terminal)
* [Architecture Notes](#architecture-notes)
* [Limitations](#limitations)
* [License](#license)

---

## Installation

```bash
pip install appium-python-client-shadowstep
```

---

## Quick Start

```python
from shadowstep.shadowstep import Shadowstep

application = Shadowstep()
capabilities = {
    "platformName": "android",
    "appium:automationName": "uiautomator2",
    "appium:UDID": 123456789,
    "appium:noReset": True,
    "appium:autoGrantPermissions": True,
    "appium:newCommandTimeout": 900,
}
application.connect(server_ip='127.0.0.1', server_port=4723, capabilities=capabilities)
```

---

## Test Setup (Pytest)

```python
import pytest
from shadowstep.shadowstep import Shadowstep


@pytest.fixture()
def app():
    shadowstep = Shadowstep()
    shadowstep.connect(capabilities=Config.APPIUM_CAPABILITIES,
                       command_executor=Config.APPIUM_COMMAND_EXECUTOR,
                       server_ip=Config.APPIUM_IP,
                       server_port=Config.APPIUM_PORT,
                       ssh_user=Config.SSH_USERNAME,
                       ssh_password=Config.SSH_PASSWORD, )
    yield shadowstep
    shadowstep.disconnect()
```

---

## Element API

```python
el = app.get_element({"resource-id": "android:id/title"})
el.tap()
el.text
el.get_attribute("enabled")
```

Lazy DOM tree navigation (declarative)

```python
el = app.get_element({'class': 'android.widget.ImageView'}).
    get_parent().get_sibling({'resource-id': 'android:id/summary'}).
    from_parent(
    ancestor_locator={'text': 'title', 'resource-id': 'android:id/title'},
    cousin_locator={'resource-id': 'android:id/summary'}
).get_element(
    {"resource-id": "android:id/switch_widget"})

```

**Key features:**

* Lazy evaluation (`find_element` only called on interaction)
* Support for `dict` and XPath locators
* Built-in retry and session reconnect
* Rich API: `tap`, `click`, `scroll_to`, `get_sibling`, `get_parent`, `drag_to`, `send_keys`, `wait_visible`, etc.

---

## ## Element Collections (`Elements`)

Returned by `get_elements()` (generator-based):

```python
elements = app.get_element({'class': 'android.widget.ImageView'}).get_elements({"class": "android.widget.TextView"})

first = elements.first()
all_items = elements.to_list()

filtered = elements.filter(lambda e: "Wi-Fi" in (e.text or ""))
filtered.should.have.count(minimum=1)
```

```python
els = app.get_elements({'class': 'android.widget.TextView'})    # lazy

els.first.get_attributes()     # driver interaction with first element only
...     # some logic
els.next.get_attributes()    # driver interation with second element only
```


**DSL assertions:**

```python
items.should.have.count(minimum=3)
items.should.have.text("Battery")
items.should.be.all_visible()
```

---

## Page Objects and Navigation

### Defining a Page

```python
import logging
from shadowstep.element.element import Element
from shadowstep.page_base import PageBaseShadowstep


class PageAbout(PageBaseShadowstep):
    def __init__(self):
        super().__init__()
        self.logger = logging.getLogger(__name__)

    def __repr__(self):
        return f"{self.name} ({self.__class__.__name__})"

    @property
    def edges(self):
        return {"PageMain": self.to_main}

    def to_main(self):
        self.shadowstep.terminal.press_back()
        return self.shadowstep.get_page("PageMain")

    @property
    def name(self) -> str:
        return "About"

    @property
    def title(self) -> Element:
        return self.shadowstep.get_element(locator={'text': 'About', 'class': 'android.widget.TextView'})

    def is_current_page(self) -> bool:
        try:
            return self.title.is_visible()
        except Exception as e:
            self.logger.error(e)
            return False
```

```python
import logging
import inspect
import os
import traceback
from typing import Dict, Any, Callable
from shadowstep.element.element import Element
from shadowstep.page_base import PageBaseShadowstep

logger = logging.getLogger(__name__)

class PageEtalon(PageBaseShadowstep):
    def __init__(self):
        super().__init__()
        self.current_path = os.path.dirname(os.path.abspath(inspect.getframeinfo(inspect.currentframe()).filename))

    def __repr__(self):
        return f"{self.name} ({self.__class__.__name__})"

    @property
    def edges(self) -> dict[str, Callable[[], None]]:
        return {}

    @property
    def name(self) -> str:
        return "PageEtalon"

    # --- Title bar ---

    @property
    def title_locator(self) -> Dict[str, Any]:
        return {
            "package": "com.android.launcher3",
            "class": "android.widget.FrameLayout",
            "text": "",
            "resource-id": "android:id/content",
        }

    @property
    def title(self) -> Element:
        logger.info(f"{inspect.currentframe().f_code.co_name}")
        return self.shadowstep.get_element(locator=self.title_locator)

    # --- Main scrollable container ---

    @property
    def recycler_locator(self):
        # self.logger.info(f"{inspect.currentframe().f_code.co_name}")
        return {"scrollable": "true"}

    @property
    def recycler(self):
        # self.logger.info(f"{inspect.currentframe().f_code.co_name}")
        return self.shadowstep.get_element(locator=self.recycler_locator)

    def _recycler_get(self, locator):
        # self.logger.info(f"{inspect.currentframe().f_code.co_name}")
        return self.recycler.scroll_to_element(locator=locator)

    # --- Search button (if present) ---

    @property
    def search_button_locator(self) -> Dict[str, Any]:
        return {'text': 'Search'}

    @property
    def search_button(self) -> Element:
        logger.info(f"{inspect.currentframe().f_code.co_name}")
        return self.shadowstep.get_element(locator=self.search_button_locator)

    # --- Back button button (if present) ---

    @property
    def back_button_locator(self) -> Dict[str, Any]:
        return {'text': 'back'}

    @property
    def back_button(self) -> Element:
        logger.info(f"{inspect.currentframe().f_code.co_name}")
        return self.shadowstep.get_element(locator=self.back_button_locator)

    # --- Elements in scrollable container ---

    @property
    def element_text_view_locator(self) -> dict:
        return {"text": "Element in scrollable container"}

    @property
    def element_text_view(self) -> Element:
        logger.info(f"{inspect.currentframe().f_code.co_name}")
        return self.recycler.scroll_to_element(self.element_text_view_locator)

    @property
    def summary_element_text_view(self) -> str:
        logger.info(f"{inspect.currentframe().f_code.co_name}")
        return self._get_summary_text(self.element_text_view)

    # --- PRIVATE METHODS ---

    def _get_summary_text(self, element: Element) -> str:
        try:
            summary = element.get_sibling({"resource-id": "android:id/summary"})
            return self.recycler.scroll_to_element(summary.locator).get_attribute("text")
        except Exception as error:
            logger.error(f"Error:\n{error}\n{traceback.format_exc()}")
            return ""

    # --- is_current_page (always in bottom) ---

    def is_current_page(self) -> bool:
        try:
            if self.title.is_visible():
                return True
            return False
        except Exception as error:
            logger.info(f"{inspect.currentframe().f_code.co_name}: {error}")
            return False
```

### Auto-discovery Requirements

* File: `pages/page_*.py`
* Class: starts with `Page`, inherits from `PageBase`
* Must define `edges` property

### Navigation Example

```python
self.shadowstep.navigator.navigate(source_page=self.page_main, target_page=self.page_display)
assert self.page_display.is_current_page()
```

---

## ADB and Terminal

### ADB Usage

```python
app.adb.press_home()
app.adb.install_apk("path/to/app.apk")
app.adb.input_text("hello")
```

* Direct ADB via `subprocess`
* Supports input, app install/uninstall, screen record, file transfer, etc.

### Terminal Usage

```python
app.terminal.start_activity(package="com.example", activity=".MainActivity")
app.terminal.tap(x=1345, y=756)
app.terminal.past_text(text='hello')
```

* Uses driver.execute_script(`mobile: shell`) or SSH backend (will separate in future)
* Backend selected based on SSH credentials

---

## Architecture Notes

* All interactions are lazy (nothing fetched before usage)
* Reconnects on session loss (`InvalidSessionIdException`, etc.)
* Supports pytest and CI/CD workflows
* Designed for extensibility and modularity

---

## Limitations

* Android only (no iOS or web support)

---

## License
[MIT License](LICENSE)

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "appium-python-client-shadowstep",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "appium, testing, uiautomator2, android, automation, framework",
    "author": null,
    "author_email": "Klim Molokov <klim.molokov@yandex.ru>",
    "download_url": "https://files.pythonhosted.org/packages/8e/43/1f19d5884adc530237500cbf4634725e57dc3ee3800d036730c60cdf6a04/appium_python_client_shadowstep-0.31.99.tar.gz",
    "platform": null,
    "description": "# Shadowstep (in development)\n\n**Shadowstep** is a modular UI automation framework for Android applications, built on top of Appium.\n\nIt provides:\n\n* Lazy element lookup and interaction (without driver interaction)\n* PageObject navigation engine\n* Reconnect logic on session failure\n* ADB and Appium terminal integration\n* DSL-style assertions (`should.have`, `should.be`)\n\n---\n\n## Contents\n\n* [Installation](#installation)\n* [Quick Start](#quick-start)\n* [Test Setup (Pytest)](#test-setup-pytest)\n* [Element API](#element-api)\n* [Collections API (`Elements`)](#collections-api-elements)\n* [Page Objects and Navigation](#page-objects-and-navigation)\n* [ADB and Terminal](#adb-and-terminal)\n* [Architecture Notes](#architecture-notes)\n* [Limitations](#limitations)\n* [License](#license)\n\n---\n\n## Installation\n\n```bash\npip install appium-python-client-shadowstep\n```\n\n---\n\n## Quick Start\n\n```python\nfrom shadowstep.shadowstep import Shadowstep\n\napplication = Shadowstep()\ncapabilities = {\n    \"platformName\": \"android\",\n    \"appium:automationName\": \"uiautomator2\",\n    \"appium:UDID\": 123456789,\n    \"appium:noReset\": True,\n    \"appium:autoGrantPermissions\": True,\n    \"appium:newCommandTimeout\": 900,\n}\napplication.connect(server_ip='127.0.0.1', server_port=4723, capabilities=capabilities)\n```\n\n---\n\n## Test Setup (Pytest)\n\n```python\nimport pytest\nfrom shadowstep.shadowstep import Shadowstep\n\n\n@pytest.fixture()\ndef app():\n    shadowstep = Shadowstep()\n    shadowstep.connect(capabilities=Config.APPIUM_CAPABILITIES,\n                       command_executor=Config.APPIUM_COMMAND_EXECUTOR,\n                       server_ip=Config.APPIUM_IP,\n                       server_port=Config.APPIUM_PORT,\n                       ssh_user=Config.SSH_USERNAME,\n                       ssh_password=Config.SSH_PASSWORD, )\n    yield shadowstep\n    shadowstep.disconnect()\n```\n\n---\n\n## Element API\n\n```python\nel = app.get_element({\"resource-id\": \"android:id/title\"})\nel.tap()\nel.text\nel.get_attribute(\"enabled\")\n```\n\nLazy DOM tree navigation (declarative)\n\n```python\nel = app.get_element({'class': 'android.widget.ImageView'}).\n    get_parent().get_sibling({'resource-id': 'android:id/summary'}).\n    from_parent(\n    ancestor_locator={'text': 'title', 'resource-id': 'android:id/title'},\n    cousin_locator={'resource-id': 'android:id/summary'}\n).get_element(\n    {\"resource-id\": \"android:id/switch_widget\"})\n\n```\n\n**Key features:**\n\n* Lazy evaluation (`find_element` only called on interaction)\n* Support for `dict` and XPath locators\n* Built-in retry and session reconnect\n* Rich API: `tap`, `click`, `scroll_to`, `get_sibling`, `get_parent`, `drag_to`, `send_keys`, `wait_visible`, etc.\n\n---\n\n## ## Element Collections (`Elements`)\n\nReturned by `get_elements()` (generator-based):\n\n```python\nelements = app.get_element({'class': 'android.widget.ImageView'}).get_elements({\"class\": \"android.widget.TextView\"})\n\nfirst = elements.first()\nall_items = elements.to_list()\n\nfiltered = elements.filter(lambda e: \"Wi-Fi\" in (e.text or \"\"))\nfiltered.should.have.count(minimum=1)\n```\n\n```python\nels = app.get_elements({'class': 'android.widget.TextView'})    # lazy\n\nels.first.get_attributes()     # driver interaction with first element only\n...     # some logic\nels.next.get_attributes()    # driver interation with second element only\n```\n\n\n**DSL assertions:**\n\n```python\nitems.should.have.count(minimum=3)\nitems.should.have.text(\"Battery\")\nitems.should.be.all_visible()\n```\n\n---\n\n## Page Objects and Navigation\n\n### Defining a Page\n\n```python\nimport logging\nfrom shadowstep.element.element import Element\nfrom shadowstep.page_base import PageBaseShadowstep\n\n\nclass PageAbout(PageBaseShadowstep):\n    def __init__(self):\n        super().__init__()\n        self.logger = logging.getLogger(__name__)\n\n    def __repr__(self):\n        return f\"{self.name} ({self.__class__.__name__})\"\n\n    @property\n    def edges(self):\n        return {\"PageMain\": self.to_main}\n\n    def to_main(self):\n        self.shadowstep.terminal.press_back()\n        return self.shadowstep.get_page(\"PageMain\")\n\n    @property\n    def name(self) -> str:\n        return \"About\"\n\n    @property\n    def title(self) -> Element:\n        return self.shadowstep.get_element(locator={'text': 'About', 'class': 'android.widget.TextView'})\n\n    def is_current_page(self) -> bool:\n        try:\n            return self.title.is_visible()\n        except Exception as e:\n            self.logger.error(e)\n            return False\n```\n\n```python\nimport logging\nimport inspect\nimport os\nimport traceback\nfrom typing import Dict, Any, Callable\nfrom shadowstep.element.element import Element\nfrom shadowstep.page_base import PageBaseShadowstep\n\nlogger = logging.getLogger(__name__)\n\nclass PageEtalon(PageBaseShadowstep):\n    def __init__(self):\n        super().__init__()\n        self.current_path = os.path.dirname(os.path.abspath(inspect.getframeinfo(inspect.currentframe()).filename))\n\n    def __repr__(self):\n        return f\"{self.name} ({self.__class__.__name__})\"\n\n    @property\n    def edges(self) -> dict[str, Callable[[], None]]:\n        return {}\n\n    @property\n    def name(self) -> str:\n        return \"PageEtalon\"\n\n    # --- Title bar ---\n\n    @property\n    def title_locator(self) -> Dict[str, Any]:\n        return {\n            \"package\": \"com.android.launcher3\",\n            \"class\": \"android.widget.FrameLayout\",\n            \"text\": \"\",\n            \"resource-id\": \"android:id/content\",\n        }\n\n    @property\n    def title(self) -> Element:\n        logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return self.shadowstep.get_element(locator=self.title_locator)\n\n    # --- Main scrollable container ---\n\n    @property\n    def recycler_locator(self):\n        # self.logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return {\"scrollable\": \"true\"}\n\n    @property\n    def recycler(self):\n        # self.logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return self.shadowstep.get_element(locator=self.recycler_locator)\n\n    def _recycler_get(self, locator):\n        # self.logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return self.recycler.scroll_to_element(locator=locator)\n\n    # --- Search button (if present) ---\n\n    @property\n    def search_button_locator(self) -> Dict[str, Any]:\n        return {'text': 'Search'}\n\n    @property\n    def search_button(self) -> Element:\n        logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return self.shadowstep.get_element(locator=self.search_button_locator)\n\n    # --- Back button button (if present) ---\n\n    @property\n    def back_button_locator(self) -> Dict[str, Any]:\n        return {'text': 'back'}\n\n    @property\n    def back_button(self) -> Element:\n        logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return self.shadowstep.get_element(locator=self.back_button_locator)\n\n    # --- Elements in scrollable container ---\n\n    @property\n    def element_text_view_locator(self) -> dict:\n        return {\"text\": \"Element in scrollable container\"}\n\n    @property\n    def element_text_view(self) -> Element:\n        logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return self.recycler.scroll_to_element(self.element_text_view_locator)\n\n    @property\n    def summary_element_text_view(self) -> str:\n        logger.info(f\"{inspect.currentframe().f_code.co_name}\")\n        return self._get_summary_text(self.element_text_view)\n\n    # --- PRIVATE METHODS ---\n\n    def _get_summary_text(self, element: Element) -> str:\n        try:\n            summary = element.get_sibling({\"resource-id\": \"android:id/summary\"})\n            return self.recycler.scroll_to_element(summary.locator).get_attribute(\"text\")\n        except Exception as error:\n            logger.error(f\"Error:\\n{error}\\n{traceback.format_exc()}\")\n            return \"\"\n\n    # --- is_current_page (always in bottom) ---\n\n    def is_current_page(self) -> bool:\n        try:\n            if self.title.is_visible():\n                return True\n            return False\n        except Exception as error:\n            logger.info(f\"{inspect.currentframe().f_code.co_name}: {error}\")\n            return False\n```\n\n### Auto-discovery Requirements\n\n* File: `pages/page_*.py`\n* Class: starts with `Page`, inherits from `PageBase`\n* Must define `edges` property\n\n### Navigation Example\n\n```python\nself.shadowstep.navigator.navigate(source_page=self.page_main, target_page=self.page_display)\nassert self.page_display.is_current_page()\n```\n\n---\n\n## ADB and Terminal\n\n### ADB Usage\n\n```python\napp.adb.press_home()\napp.adb.install_apk(\"path/to/app.apk\")\napp.adb.input_text(\"hello\")\n```\n\n* Direct ADB via `subprocess`\n* Supports input, app install/uninstall, screen record, file transfer, etc.\n\n### Terminal Usage\n\n```python\napp.terminal.start_activity(package=\"com.example\", activity=\".MainActivity\")\napp.terminal.tap(x=1345, y=756)\napp.terminal.past_text(text='hello')\n```\n\n* Uses driver.execute_script(`mobile: shell`) or SSH backend (will separate in future)\n* Backend selected based on SSH credentials\n\n---\n\n## Architecture Notes\n\n* All interactions are lazy (nothing fetched before usage)\n* Reconnects on session loss (`InvalidSessionIdException`, etc.)\n* Supports pytest and CI/CD workflows\n* Designed for extensibility and modularity\n\n---\n\n## Limitations\n\n* Android only (no iOS or web support)\n\n---\n\n## License\n[MIT License](LICENSE)\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "UI Testing Framework powered by Appium Python Client",
    "version": "0.31.99",
    "project_urls": {
        "Repository": "https://github.com/molokov-klim/Appium-Python-Client-Shadowstep"
    },
    "split_keywords": [
        "appium",
        " testing",
        " uiautomator2",
        " android",
        " automation",
        " framework"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "4521a9b98b07520d0c8a63596687a44f20547cfab74fc6f4091d86ff375a2eae",
                "md5": "e61a448c9319b571a1c264fe9d632b75",
                "sha256": "b0f78dc5fcc73db1d602eeed7ea7cf834762c21945fe53778c6242c0fa786569"
            },
            "downloads": -1,
            "filename": "appium_python_client_shadowstep-0.31.99-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "e61a448c9319b571a1c264fe9d632b75",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 142040,
            "upload_time": "2025-07-23T14:39:15",
            "upload_time_iso_8601": "2025-07-23T14:39:15.110570Z",
            "url": "https://files.pythonhosted.org/packages/45/21/a9b98b07520d0c8a63596687a44f20547cfab74fc6f4091d86ff375a2eae/appium_python_client_shadowstep-0.31.99-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "8e431f19d5884adc530237500cbf4634725e57dc3ee3800d036730c60cdf6a04",
                "md5": "2df97f3939970c16226f38ed66e35aa9",
                "sha256": "7f66b219215eadc8b5287285aef143106cad0712502ac6ddc095b33ca6e1eee8"
            },
            "downloads": -1,
            "filename": "appium_python_client_shadowstep-0.31.99.tar.gz",
            "has_sig": false,
            "md5_digest": "2df97f3939970c16226f38ed66e35aa9",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 131958,
            "upload_time": "2025-07-23T14:39:17",
            "upload_time_iso_8601": "2025-07-23T14:39:17.278316Z",
            "url": "https://files.pythonhosted.org/packages/8e/43/1f19d5884adc530237500cbf4634725e57dc3ee3800d036730c60cdf6a04/appium_python_client_shadowstep-0.31.99.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-07-23 14:39:17",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "molokov-klim",
    "github_project": "Appium-Python-Client-Shadowstep",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "appium-python-client-shadowstep"
}
        
Elapsed time: 1.24045s