# huskium
---
## Copyright
- Developer: Johnny Chou
---
## Overview
- **huskium** is a Page Object framework built on Selenium and Appium.
- **huskium** leverages Python’s data descriptors to simplify and enhance UI automation.
---
## 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 huskium import Page, Element, Elements
from huskium import By
from huskium 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_element).
static_element = Element()
# 2. Define a function and call "dynamic" to assign a value to "static_element".
# The logic for dynamic parameters is the same as in Element.
# After calling "dynamic_element", you can also use "static_element" to operate it.
def dynamic_element(self, par):
return self.static_element.dynamic(By.XPATH, f'//*[contains(text(), "{par}")]')
# 3. Use the standard method for a data descriptor.
def dynamic_element(self, par):
self.static_element = Element(By.XPATH, f'//*[contains(text(), "{par}")]')
return self.static_element
```
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 huskium import Timeout
# Set the default timeout for all Elements.
# The huskium 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 huskium 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`
---
## Cache Global Settings
1. Cache determines whether the `Element` class should reference a previously located `WebElement`
for actions or locate the element again for each action.
2. The benefits of Cache are evident when the same `Element` is accessed multiple times,
such as performing `.text` followed by `.click()`.
3. Note that `Elements` does not support cache.
For multiple elements, the state can be highly unstable,
so each action must locate the elements again to ensure expected behavior.
```python
from huskium import Cache
# Set the default cache for all Element.
# The default is True. You can change it as needed:
Cache.ELEMENT = False
# You can also configure the cache for an individual Element:
element = Element(..., cache=False)
```
4. The priority order for cache value is as follows:
- P1: Element-Level:
- `element = Element(..., cache=False)`
- P2: Global-Level:
- `Cache.ELEMENT = 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()
# You can set default timeout and reraise behavior for all wait functions.
page.element.wait_visible(timeout=10, reraise=True)
# Recommended to use default settings (timeout=30, reraise=True) for simplicity.
page.element.wait_visible()
# For reverse conditions like invisible and unclickable,
# use the "present" parameter to define if existence is required.
# Element must be present and invisible (default).
page.element.wait_invisible(present=True)
# Element can be absent or [present and invisible].
page.element.wait_invisible(present=False)
# Element must be present and unclickable (default).
page.element.wait_unclickable(present=True)
# Element can be absent or [present and unclickable].
page.element.wait_unclickable(present=False)
# Selection states are tied to user actions,
# so the element must be present; no "present" parameter is provided.
page.element.wait_selected()
page.element.wait_unselected()
```
---
## 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 huskium 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 huskium import logstack
# Configure logging using either logging.basicConfig() or logstack.config().
# logstack.config() simplifies the default settings. You can use it as shown below
# to output the log file to "./log.log".
logstack.config()
# 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.
```
## Inheritance
You can also extend the Page and Element classes to include custom methods.
There’s no need to manually handle descriptors, and the inheritance usage remains unchanged.
```python
from huskium import Page as HuskyPage
from huskium import Element as HuskyElement
class Page(HuskyPage):
def extended_func(self, par):
...
class Element(HuskyElement):
def extended_func(self, par):
...
```
---
## TODO
Keep tracking the Appium version.
Raw data
{
"_id": null,
"home_page": "https://github.com/uujohnnyuu/huskium",
"name": "huskium",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "huskium, huskypo, selenium, appium, page object, automation",
"author": "Johnny",
"author_email": "johnny071531@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/4b/6d/8fe33a3c392cfde5d05bbafb409ca494971bb9f4050e19b79fd5384ad23b/huskium-1.0.2.tar.gz",
"platform": null,
"description": "# huskium\n---\n\n## Copyright\n- Developer: Johnny Chou\n---\n\n## Overview\n- **huskium** is a Page Object framework built on Selenium and Appium.\n- **huskium** leverages Python\u2019s data descriptors to simplify and enhance UI automation.\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 huskium import Page, Element, Elements\nfrom huskium import By\nfrom huskium 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_element).\n static_element = Element()\n \n # 2. Define a function and call \"dynamic\" to assign a value to \"static_element\".\n # The logic for dynamic parameters is the same as in Element.\n # After calling \"dynamic_element\", you can also use \"static_element\" to operate it.\n def dynamic_element(self, par):\n return self.static_element.dynamic(By.XPATH, f'//*[contains(text(), \"{par}\")]')\n \n # 3. Use the standard method for a data descriptor.\n def dynamic_element(self, par):\n self.static_element = Element(By.XPATH, f'//*[contains(text(), \"{par}\")]')\n return self.static_element\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 huskium import Timeout\n\n\n# Set the default timeout for all Elements.\n# The huskium 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 huskium 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## Cache Global Settings\n1. Cache determines whether the `Element` class should reference a previously located `WebElement` \nfor actions or locate the element again for each action. \n2. The benefits of Cache are evident when the same `Element` is accessed multiple times, \nsuch as performing `.text` followed by `.click()`.\n3. Note that `Elements` does not support cache. \nFor multiple elements, the state can be highly unstable, \nso each action must locate the elements again to ensure expected behavior. \n```python\nfrom huskium import Cache\n\n\n# Set the default cache for all Element.\n# The default is True. You can change it as needed:\nCache.ELEMENT = False\n\n\n# You can also configure the cache for an individual Element:\nelement = Element(..., cache=False)\n```\n\n4.\tThe priority order for cache value is as follows:\n- P1: Element-Level:\n - `element = Element(..., cache=False)`\n- P2: Global-Level:\n - `Cache.ELEMENT = 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# You can set default timeout and reraise behavior for all wait functions.\npage.element.wait_visible(timeout=10, reraise=True)\n# Recommended to use default settings (timeout=30, reraise=True) for simplicity.\npage.element.wait_visible()\n\n# For reverse conditions like invisible and unclickable, \n# use the \"present\" parameter to define if existence is required.\n# Element must be present and invisible (default).\npage.element.wait_invisible(present=True)\n# Element can be absent or [present and invisible].\npage.element.wait_invisible(present=False)\n# Element must be present and unclickable (default).\npage.element.wait_unclickable(present=True)\n# Element can be absent or [present and unclickable].\npage.element.wait_unclickable(present=False)\n\n# Selection states are tied to user actions, \n# so the element must be present; no \"present\" parameter is provided.\npage.element.wait_selected()\npage.element.wait_unselected()\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 huskium 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\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\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 huskium import logstack\n\n# Configure logging using either logging.basicConfig() or logstack.config().\n# logstack.config() simplifies the default settings. You can use it as shown below\n# to output the log file to \"./log.log\".\nlogstack.config()\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## Inheritance\nYou can also extend the Page and Element classes to include custom methods. \nThere\u2019s no need to manually handle descriptors, and the inheritance usage remains unchanged.\n```python\nfrom huskium import Page as HuskyPage\nfrom huskium import Element as HuskyElement\n\n\nclass Page(HuskyPage):\n\n def extended_func(self, par):\n ...\n\n\nclass Element(HuskyElement):\n\n def extended_func(self, par):\n ...\n```\n---\n\n## TODO\nKeep tracking the Appium version. \n",
"bugtrack_url": null,
"license": "Apache 2.0",
"summary": "UI Automation Page Objects design pattern.",
"version": "1.0.2",
"project_urls": {
"Homepage": "https://github.com/uujohnnyuu/huskium"
},
"split_keywords": [
"huskium",
" huskypo",
" selenium",
" appium",
" page object",
" automation"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "95050449c74b2f8c669c2e22b3e26e5f06b550b187e8a7649f136c480bb5796f",
"md5": "01acd1e882b5b658338cd6f1c58379e4",
"sha256": "112ff46013c11286bba14f47a16f8c46aa4a8c200b52a6c74a842019ea617f53"
},
"downloads": -1,
"filename": "huskium-1.0.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "01acd1e882b5b658338cd6f1c58379e4",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 45572,
"upload_time": "2025-01-21T02:57:30",
"upload_time_iso_8601": "2025-01-21T02:57:30.630685Z",
"url": "https://files.pythonhosted.org/packages/95/05/0449c74b2f8c669c2e22b3e26e5f06b550b187e8a7649f136c480bb5796f/huskium-1.0.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "4b6d8fe33a3c392cfde5d05bbafb409ca494971bb9f4050e19b79fd5384ad23b",
"md5": "e983e8f04997b617c2ef47f1c3c6881e",
"sha256": "9670aa463bb7e0169d918d6dc3854c0aea20f920ed5d4d5d53a69ae9e597890d"
},
"downloads": -1,
"filename": "huskium-1.0.2.tar.gz",
"has_sig": false,
"md5_digest": "e983e8f04997b617c2ef47f1c3c6881e",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 45751,
"upload_time": "2025-01-21T02:57:33",
"upload_time_iso_8601": "2025-01-21T02:57:33.638065Z",
"url": "https://files.pythonhosted.org/packages/4b/6d/8fe33a3c392cfde5d05bbafb409ca494971bb9f4050e19b79fd5384ad23b/huskium-1.0.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-01-21 02:57:33",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "uujohnnyuu",
"github_project": "huskium",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "huskium"
}