# huskyPO
---
## Copyright
- Developer: Johnny Chou
---
## Overview
- **huskyPO** stands for "husky Page Object", named "husky" to reflect
the package’s elegance and agility, as well as the author’s fondness.
- **huskyPO** is a UI Page Object package built with Python’s data descriptors.
- **huskyPO** can simplify and enhance the creation of
Python Selenium and Appium UI automation scripts.
---
## Usage
- **Build page objects** simply and elegantly using the `Page` and `Element(s)` classes.
- **Write test scripts** simply and elegantly using the constructed Page objects.
---
## Page Object Example Code
1. Construct a Page object in any Python file, each page recommended to be a separate Page class.
```python
# my_page.py
from huskypo import Page, Element, Elements
from huskypo import By
from huskypo import dynamic
class MyPage(Page):
# Static element: The most common way to set up Page objects.
# Element is a data descriptor of Page, allowing simple setup as shown below:
search_field = Element(By.NAME, 'q', remark='Search input box')
search_results = Elements(By.TAG_NAME, 'h3', remark='All search results')
search_result1 = Element(By.XPATH, '//h3[1]', remark='First search result')
# Dynamic element: Rarely used, typically determined during test case execution.
# Must use @dynamic to enable the descriptor's functionality.
@dynamic
def search_result(self, order: int = 1):
return Element(By.XPATH, f'//h3[{order}]', remark=f'Search result no.{order}')
# For dynamic elements as properties, use the following format:
@property
@dynamic
def keyword_results(self):
return Elements(By.XPATH, f'//*[contains(text(), "{Keyword.text1}")]')
# To record dynamic elements statically, use standard data descriptor dynamic assignment:
# 1. Create a data descriptor object (e.g., static_element1).
static_element1 = Element()
# 2. Define a function to assign a tuple to the descriptor,
# the parameter logic is the same as Element(s).
def dynamic_element(self, par) -> Element:
self.static_element1 = (By.XPATH, f'//*[contains(text(), "{par}")]', f'This is {par}')
return self.static_element1
# 3. Alternatively, use a dict for clearer parameter names.
def dynamic_element(self, par) -> Element:
self.static_element1 = {
'by': By.XPATH,
'value': f'//*[contains(text(), "{par}")]',
'remark': f'This is {par}'}
return self.static_element1
```
2. After constructing the Page object, you can begin writing test cases.
```python
# test_my_page.py
from selenium import webdriver
from my_page import MyPage
class TestMyPage:
driver = webdriver.Chrome()
# Set up a page object. All actions will be triggered from this object.
my_page = MyPage(driver)
# The Page object can also call driver-related methods.
my_page.get("https://google.com")
# Example of a wait usage:
# Wait until an element is visible, then take a screenshot.
my_page.search_field.wait_visible()
my_page.save_screenshot("my/file/image1.png")
# All actions automatically handle explicit waits.
# No need to manually call wait methods unless required,
# e.g. Equivalent to:
# my_page.search_field.wait_clickable().send_keys(keyword).wait_clickable().submit()
my_page.search_field.send_keys(keyword).submit()
# Various wait states are available.
my_page.loading_image.wait_absent()
my_page.search_results.wait_all_visible()
my_page.save_screenshot("my/file/image2.png")
# Assertions can be made directly:
search_keyword = 'dinner'
assert my_page.keyword_results(search_keyword).quantity > 1
assert search_keyword in my_page.search_result1.text
# Reuse found elements through existing sessions:
# Once an element (e.g., `my_page.search_result1`) is located,
# it will use the same session unless the element becomes stale.
# No need to store the found element in a separate variable.
# Just perform actions directly:
my_page.search_result1.click()
...
driver.close()
```
---
## Timeout Global Settings
1. In addition to setting timeouts for individual elements and methods,
a global timeout setting is also available. See the example below:
```python
from huskyPO import Timeout
# Set the default timeout for all Elements.
# The huskyPO default is 30 seconds. You can change it as needed:
Timeout.DEFAULT = 60
# If you don’t want any waiting, you can also set it to 0:
Timeout.DEFAULT = 0
# Set the reraise behavior for timeouts on all Elements.
# The huskyPO default is True, with the following logic:
# True: Raise a TimeoutException if the element times out.
# False: Return False if the element times out, without raising a TimeoutException.
Timeout.RERAISE = False
```
2. The priority order for timeout values is as follows:
- P1: Method-Level:
- `page.element.wait_method(timeout=20)`
- P2: Element-Level:
- `element = Element(..., timeout=10, ...)`
- P3: Global-Level:
- `Timeout.DEFAULT = 60`
3. The priority order for timeout reraise behavior is as follows:
- P1: Method-Level:
- `page.element.wait_method(reraise=True)`
- P2: Global-Level:
- `Timeout.RERAISE = False`
---
## Wait Actions
We offer a comprehensive set of wait methods,
extending the official expected_conditions in `ec_extension.py`
and encapsulating them into corresponding methods.
Below are the extended methods for Element(s):
```python
# Element
page.element.wait_present()
page.element.wait_absent()
page.element.wait_visible()
page.element.wait_invisible()
page.element.wait_clickable()
page.element.wait_unclickable()
page.element.wait_selected()
page.element.wait_unselected()
# Elements
page.elements.wait_all_present()
page.elements.wait_all_absent()
page.elements.wait_all_visible()
page.elements.wait_any_visible()
```
---
## Appium Extended Actions
We have extended Appium with highly convenient action features, including:
```python
# Offset allows you to define swipe directions.
# It supports eight standard directions:
# UP, DOWN, LEFT, RIGHT, UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT.
# Area lets you define the swipeable range,
# defaulting to the full device screen (FULL), or you can customize it.
from huskypo import Offset, Area
# Page swiping. Refer to function docstrings for details.
page.swipe_by(Offset.UP, Area.FULL)
page.swipe_by(Offset.DOWN)
page.swipe_by(Offset.LEFT)
page.swipe_by(Offset.RIGHT)
page.swipe_by(Offset.UPPER_LEFT)
page.swipe_by(Offset.UPPER_RIGHT)
page.swipe_by(Offset.LOWER_LEFT)
page.swipe_by(Offset.LOWER_RIGHT)
# Page flicking. Refer to function docstrings for details.
# All Offset directions are supported.
page.flick_by(Offset.UP, Area.FULL)
# Element swiping until an element is visible.
# All Offset directions are supported.
page.element.swipe_by(Offset.UP, Area.FULL)
# Element flicking until an element is visible.
# All Offset directions are supported.
page.element.flick_by(Offset.UP, Area.FULL)
# Page draw lines.
# Define dots coordinates
dots = [{"x": x1, "y": y1}, {"x": x2, "y": y2}, {"x": x3, "y": y3}, ...]
# Alternatively, use element locations if available
dots = page.elements.locations
page.draw_lines(dots)
# Page draw gesture.
# 9-grid coordinates, or define your own
dots = page.elements.locations
# 9-grid gesture string (1–9 represent grid positions). This example draws a reverse Z.
gesture = "9875321"
page.draw_gesture(dots, gesture)
```
## Other Actions
All Selenium and Appium features are included. Here are some examples:
```python
# ActionChains
page.element.scroll_to_element().perform()
page.element.move_to_element().drag_and_drop().perform()
# Temporarily store ActionChains and execute later
page.element.move_to_element().drag_and_drop()
... # Process other logic before executing perform()
page.element.perform()
# Select options
page.element.options
page.element.select_by_value()
```
## Logstack
Using logstack to log specific frame information.
The logstack module extends logging functionality,
allowing you to capture information for specific frames,
such as those starting with a designated prefix (e.g., test),
without tracing all frames manually.
```python
from huskypo import logstack, logconfig
import logging # Optional: You can also use logging for basic configuration.
# Configure logging using either logging.basicConfig() or logconfig.basic()
logconfig.basic(file="./log.log") # Simplified configuration with common settings
# Use logstack in your code to log specific frames
def some_func():
...
# Logs information from the first frame with the prefix (default: test)
logstack.info("Log from some function.", prefix="test")
def test_func():
...
# Logs frame info for test_func, not some_func
some_func()
# Example log output:
# 2025-01-04 18:20:48 | INFO | testing.py:32 | test_func | Log from some function.
```
## TODO
- Continuously monitor new features in Selenium 4.0 and Appium 2.0.
- Other optimizations.
Raw data
{
"_id": null,
"home_page": "https://github.com/uujohnnyuu/huskyPO",
"name": "huskypo",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "huskypo, page object, selenium, appium, automation",
"author": "Johnny",
"author_email": "johnny071531@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/95/68/9c0b0d87dc6197fa87bb074770b42618c64a5df477afb24e0b33b9d7ce47/huskypo-1.4.0.tar.gz",
"platform": null,
"description": "# huskyPO\n---\n\n## Copyright\n- Developer: Johnny Chou\n---\n\n## Overview\n- **huskyPO** stands for \"husky Page Object\", named \"husky\" to reflect \nthe package\u2019s elegance and agility, as well as the author\u2019s fondness.\n- **huskyPO** is a UI Page Object package built with Python\u2019s data descriptors.\n- **huskyPO** can simplify and enhance the creation of \nPython Selenium and Appium UI automation scripts.\n---\n\n## Usage\n- **Build page objects** simply and elegantly using the `Page` and `Element(s)` classes.\n- **Write test scripts** simply and elegantly using the constructed Page objects.\n---\n\n## Page Object Example Code\n1. Construct a Page object in any Python file, each page recommended to be a separate Page class.\n```python\n# my_page.py\n\n\nfrom huskypo import Page, Element, Elements\nfrom huskypo import By\nfrom huskypo import dynamic\n\n\nclass MyPage(Page):\n \n # Static element: The most common way to set up Page objects.\n # Element is a data descriptor of Page, allowing simple setup as shown below:\n search_field = Element(By.NAME, 'q', remark='Search input box')\n search_results = Elements(By.TAG_NAME, 'h3', remark='All search results')\n search_result1 = Element(By.XPATH, '//h3[1]', remark='First search result')\n \n # Dynamic element: Rarely used, typically determined during test case execution.\n # Must use @dynamic to enable the descriptor's functionality.\n @dynamic\n def search_result(self, order: int = 1):\n return Element(By.XPATH, f'//h3[{order}]', remark=f'Search result no.{order}')\n \n # For dynamic elements as properties, use the following format:\n @property\n @dynamic\n def keyword_results(self):\n return Elements(By.XPATH, f'//*[contains(text(), \"{Keyword.text1}\")]')\n \n # To record dynamic elements statically, use standard data descriptor dynamic assignment:\n # 1. Create a data descriptor object (e.g., static_element1).\n static_element1 = Element()\n \n # 2. Define a function to assign a tuple to the descriptor, \n # the parameter logic is the same as Element(s).\n def dynamic_element(self, par) -> Element:\n self.static_element1 = (By.XPATH, f'//*[contains(text(), \"{par}\")]', f'This is {par}')\n return self.static_element1\n \n # 3. Alternatively, use a dict for clearer parameter names.\n def dynamic_element(self, par) -> Element:\n self.static_element1 = {\n 'by': By.XPATH, \n 'value': f'//*[contains(text(), \"{par}\")]', \n 'remark': f'This is {par}'}\n return self.static_element1\n```\n\n2. After constructing the Page object, you can begin writing test cases.\n```python\n# test_my_page.py\n\n\nfrom selenium import webdriver\nfrom my_page import MyPage\n\n\nclass TestMyPage:\n \n driver = webdriver.Chrome()\n\n # Set up a page object. All actions will be triggered from this object.\n my_page = MyPage(driver)\n\n # The Page object can also call driver-related methods.\n my_page.get(\"https://google.com\")\n\n # Example of a wait usage:\n # Wait until an element is visible, then take a screenshot.\n my_page.search_field.wait_visible()\n my_page.save_screenshot(\"my/file/image1.png\")\n\n # All actions automatically handle explicit waits.\n # No need to manually call wait methods unless required, \n # e.g. Equivalent to: \n # my_page.search_field.wait_clickable().send_keys(keyword).wait_clickable().submit()\n my_page.search_field.send_keys(keyword).submit()\n\n # Various wait states are available.\n my_page.loading_image.wait_absent()\n my_page.search_results.wait_all_visible()\n my_page.save_screenshot(\"my/file/image2.png\")\n\n # Assertions can be made directly:\n search_keyword = 'dinner'\n assert my_page.keyword_results(search_keyword).quantity > 1\n assert search_keyword in my_page.search_result1.text\n\n # Reuse found elements through existing sessions:\n # Once an element (e.g., `my_page.search_result1`) is located, \n # it will use the same session unless the element becomes stale.\n # No need to store the found element in a separate variable.\n # Just perform actions directly:\n my_page.search_result1.click()\n ...\n\n driver.close()\n```\n---\n\n## Timeout Global Settings\n1.\tIn addition to setting timeouts for individual elements and methods, \na global timeout setting is also available. See the example below:\n```python\nfrom huskyPO import Timeout\n\n\n# Set the default timeout for all Elements.\n# The huskyPO default is 30 seconds. You can change it as needed:\nTimeout.DEFAULT = 60\n\n# If you don\u2019t want any waiting, you can also set it to 0:\nTimeout.DEFAULT = 0\n\n# Set the reraise behavior for timeouts on all Elements.\n# The huskyPO default is True, with the following logic:\n# True: Raise a TimeoutException if the element times out.\n# False: Return False if the element times out, without raising a TimeoutException.\nTimeout.RERAISE = False\n```\n\n2.\tThe priority order for timeout values is as follows:\n- P1: Method-Level:\n - `page.element.wait_method(timeout=20)`\n- P2: Element-Level:\n - `element = Element(..., timeout=10, ...)`\n- P3: Global-Level:\n - `Timeout.DEFAULT = 60`\n\n3.\tThe priority order for timeout reraise behavior is as follows:\n- P1: Method-Level:\n - `page.element.wait_method(reraise=True)`\n- P2: Global-Level:\n - `Timeout.RERAISE = False`\n---\n\n## Wait Actions\nWe offer a comprehensive set of wait methods, \nextending the official expected_conditions in `ec_extension.py` \nand encapsulating them into corresponding methods. \nBelow are the extended methods for Element(s):\n```python\n# Element\npage.element.wait_present()\npage.element.wait_absent()\npage.element.wait_visible()\npage.element.wait_invisible()\npage.element.wait_clickable()\npage.element.wait_unclickable()\npage.element.wait_selected()\npage.element.wait_unselected()\n\n# Elements\npage.elements.wait_all_present()\npage.elements.wait_all_absent()\npage.elements.wait_all_visible()\npage.elements.wait_any_visible()\n```\n---\n\n## Appium Extended Actions\nWe have extended Appium with highly convenient action features, including:\n```python\n# Offset allows you to define swipe directions. \n# It supports eight standard directions: \n# UP, DOWN, LEFT, RIGHT, UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT.\n# Area lets you define the swipeable range, \n# defaulting to the full device screen (FULL), or you can customize it.\n\nfrom huskypo import Offset, Area\n\n# Page swiping. Refer to function docstrings for details.\npage.swipe_by(Offset.UP, Area.FULL)\npage.swipe_by(Offset.DOWN)\npage.swipe_by(Offset.LEFT)\npage.swipe_by(Offset.RIGHT)\npage.swipe_by(Offset.UPPER_LEFT)\npage.swipe_by(Offset.UPPER_RIGHT)\npage.swipe_by(Offset.LOWER_LEFT)\npage.swipe_by(Offset.LOWER_RIGHT)\n\n# Page flicking. Refer to function docstrings for details.\n# All Offset directions are supported.\npage.flick_by(Offset.UP, Area.FULL)\n\n# Element swiping until an element is visible.\n# All Offset directions are supported.\npage.element.swipe_by(Offset.UP, Area.FULL)\n\n# Element flicking until an element is visible.\n# All Offset directions are supported.\npage.element.flick_by(Offset.UP, Area.FULL)\n\n# Page draw lines.\n# Define dots coordinates\ndots = [{\"x\": x1, \"y\": y1}, {\"x\": x2, \"y\": y2}, {\"x\": x3, \"y\": y3}, ...]\n# Alternatively, use element locations if available\ndots = page.elements.locations\npage.draw_lines(dots)\n\n# Page draw gesture.\n# 9-grid coordinates, or define your own\ndots = page.elements.locations \n# 9-grid gesture string (1\u20139 represent grid positions). This example draws a reverse Z.\ngesture = \"9875321\" \npage.draw_gesture(dots, gesture)\n```\n\n## Other Actions\nAll Selenium and Appium features are included. Here are some examples:\n```python\n# ActionChains\npage.element.scroll_to_element().perform()\npage.element.move_to_element().drag_and_drop().perform()\n\n# Temporarily store ActionChains and execute later\npage.element.move_to_element().drag_and_drop()\n... # Process other logic before executing perform()\npage.element.perform()\n\n# Select options\npage.element.options\npage.element.select_by_value()\n```\n\n## Logstack\nUsing logstack to log specific frame information.\nThe logstack module extends logging functionality, \nallowing you to capture information for specific frames, \nsuch as those starting with a designated prefix (e.g., test), \nwithout tracing all frames manually.\n```python\nfrom huskypo import logstack, logconfig\nimport logging # Optional: You can also use logging for basic configuration.\n\n# Configure logging using either logging.basicConfig() or logconfig.basic()\nlogconfig.basic(file=\"./log.log\") # Simplified configuration with common settings\n\n# Use logstack in your code to log specific frames\ndef some_func():\n ...\n # Logs information from the first frame with the prefix (default: test)\n logstack.info(\"Log from some function.\", prefix=\"test\")\n\ndef test_func():\n ...\n # Logs frame info for test_func, not some_func\n some_func()\n\n# Example log output:\n# 2025-01-04 18:20:48 | INFO | testing.py:32 | test_func | Log from some function.\n```\n\n## TODO\n- Continuously monitor new features in Selenium 4.0 and Appium 2.0.\n- Other optimizations.\n",
"bugtrack_url": null,
"license": "Apache 2.0",
"summary": "UI Automation Page Objects design pattern.",
"version": "1.4.0",
"project_urls": {
"Homepage": "https://github.com/uujohnnyuu/huskyPO"
},
"split_keywords": [
"huskypo",
" page object",
" selenium",
" appium",
" automation"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "712ee42d9e78e9515a044040a20dda89981ff0c3ef3d155d026ad532ea0c856c",
"md5": "fb0d3a5121b262f8aebe410864373361",
"sha256": "5117d83a8c531f49f6638e1522518109186b2c898be3d8f211702ffca5e7d836"
},
"downloads": -1,
"filename": "huskypo-1.4.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "fb0d3a5121b262f8aebe410864373361",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 55918,
"upload_time": "2025-01-07T02:56:44",
"upload_time_iso_8601": "2025-01-07T02:56:44.950990Z",
"url": "https://files.pythonhosted.org/packages/71/2e/e42d9e78e9515a044040a20dda89981ff0c3ef3d155d026ad532ea0c856c/huskypo-1.4.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "95689c0b0d87dc6197fa87bb074770b42618c64a5df477afb24e0b33b9d7ce47",
"md5": "8594ede7c089462284701d22168c421d",
"sha256": "c2fb272fcfd082c6c8f34841045167dae7a24c363087fa83f15b6535d919e7c6"
},
"downloads": -1,
"filename": "huskypo-1.4.0.tar.gz",
"has_sig": false,
"md5_digest": "8594ede7c089462284701d22168c421d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 54895,
"upload_time": "2025-01-07T02:56:47",
"upload_time_iso_8601": "2025-01-07T02:56:47.593427Z",
"url": "https://files.pythonhosted.org/packages/95/68/9c0b0d87dc6197fa87bb074770b42618c64a5df477afb24e0b33b9d7ce47/huskypo-1.4.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-01-07 02:56:47",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "uujohnnyuu",
"github_project": "huskyPO",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "huskypo"
}