
Security News
ESLint Adds Official Support for Linting HTML
ESLint now supports HTML linting with 48 new rules, expanding its language plugin system to cover more of the modern web development stack.
appium-python-client-shadowstep
Advanced tools
Shadowstep is a modular UI automation framework for Android applications, built on top of Appium.
It provides:
should.have
, should.be
)Elements
)pip install appium-python-client-shadowstep
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)
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()
el = app.get_element({"resource-id": "android:id/title"})
el.tap()
el.text
el.get_attribute("enabled")
Lazy DOM tree navigation (declarative)
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:
find_element
only called on interaction)dict
and XPath locatorstap
, click
, scroll_to
, get_sibling
, get_parent
, drag_to
, send_keys
, wait_visible
, etc.Elements
)Returned by get_elements()
(generator-based):
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)
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:
items.should.have.count(minimum=3)
items.should.have.text("Battery")
items.should.be.all_visible()
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
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
pages/page_*.py
Page
, inherits from PageBase
edges
propertyself.shadowstep.navigator.navigate(source_page=self.page_main, target_page=self.page_display)
assert self.page_display.is_current_page()
app.adb.press_home()
app.adb.install_apk("path/to/app.apk")
app.adb.input_text("hello")
subprocess
app.terminal.start_activity(package="com.example", activity=".MainActivity")
app.terminal.tap(x=1345, y=756)
app.terminal.past_text(text='hello')
mobile: shell
) or SSH backend (will separate in future)InvalidSessionIdException
, etc.)FAQs
UI Testing Framework powered by Appium Python Client
We found that appium-python-client-shadowstep demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ESLint now supports HTML linting with 48 new rules, expanding its language plugin system to cover more of the modern web development stack.
Security News
CISA is discontinuing official RSS support for KEV and cybersecurity alerts, shifting updates to email and social media, disrupting automation workflows.
Security News
The MCP community is launching an official registry to standardize AI tool discovery and let agents dynamically find and install MCP servers.