pyplaykit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/__init__.py +0 -0
- core/base_page.py +94 -0
- core/base_test.py +27 -0
- core/browser_factory.py +30 -0
- core/playwright_manager.py +40 -0
- dist_pkg/__init__.py +5 -0
- dist_pkg/distribution.py +51 -0
- integrations/__init__.py +21 -0
- integrations/adapters.py +155 -0
- orchestration/__init__.py +5 -0
- orchestration/planner.py +137 -0
- pages/__init__.py +0 -0
- pages/dashboard_page.py +24 -0
- pages/login_page.py +31 -0
- pages/orangehrm_login_page.py +58 -0
- pages/saucedemo_inventory_page.py +100 -0
- pages/saucedemo_login_page.py +39 -0
- plugins/__init__.py +16 -0
- plugins/api_plugin.py +11 -0
- plugins/base.py +21 -0
- plugins/data_plugin.py +11 -0
- plugins/registry.py +77 -0
- plugins/security_plugin.py +11 -0
- pyplaykit-0.1.0.dist-info/METADATA +340 -0
- pyplaykit-0.1.0.dist-info/RECORD +42 -0
- pyplaykit-0.1.0.dist-info/WHEEL +5 -0
- pyplaykit-0.1.0.dist-info/top_level.txt +8 -0
- resilience/__init__.py +5 -0
- resilience/locator_recovery.py +72 -0
- utils/__init__.py +0 -0
- utils/api_client.py +125 -0
- utils/assertion_helper.py +21 -0
- utils/data_loader.py +36 -0
- utils/data_validator.py +426 -0
- utils/environment_validator.py +105 -0
- utils/file_reader.py +35 -0
- utils/logger.py +36 -0
- utils/observability.py +164 -0
- utils/response_validator.py +58 -0
- utils/screenshot_helper.py +17 -0
- utils/tdm.py +213 -0
- utils/yaml_config_reader.py +104 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from playwright.sync_api import Page
|
|
2
|
+
from core.base_page import BasePage
|
|
3
|
+
from utils.yaml_config_reader import ConfigReader
|
|
4
|
+
from utils.logger import get_logger
|
|
5
|
+
|
|
6
|
+
class OrangeHRMLoginPage(BasePage):
|
|
7
|
+
"""
|
|
8
|
+
Page object for the OrangeHRM Login page.
|
|
9
|
+
Encapsulates locators and actions related to the login functionality.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Locators
|
|
13
|
+
USERNAME_INPUT = "#app > div.orangehrm-login-layout > div > div.orangehrm-login-container > div > div.orangehrm-login-slot > div.orangehrm-login-form > form > div:nth-child(2) > div > div:nth-child(2) > input"
|
|
14
|
+
PASSWORD_INPUT = "#app > div.orangehrm-login-layout > div > div.orangehrm-login-container > div > div.orangehrm-login-slot > div.orangehrm-login-form > form > div:nth-child(3) > div > div:nth-child(2) > input"
|
|
15
|
+
LOGIN_BUTTON = "#app > div.orangehrm-login-layout > div > div.orangehrm-login-container > div > div.orangehrm-login-slot > div.orangehrm-login-form > form > div.oxd-form-actions.orangehrm-login-action > button"
|
|
16
|
+
DASHBOARD_HEADER = "h6.oxd-text.oxd-text--h6.oxd-topbar-header-breadcrumb-module" # A locator to verify successful login
|
|
17
|
+
INVALID_CREDENTIALS_MESSAGE = "div.oxd-alert-content.oxd-alert-content--error > p.oxd-text--p"
|
|
18
|
+
|
|
19
|
+
def __init__(self, page: Page, config: ConfigReader):
|
|
20
|
+
super().__init__(page)
|
|
21
|
+
self.config = config
|
|
22
|
+
self.logger = get_logger(OrangeHRMLoginPage.__name__)
|
|
23
|
+
self.base_url = self.config.get_base_url(self.config.get_env()) # Get base URL for the current environment
|
|
24
|
+
|
|
25
|
+
def navigate(self):
|
|
26
|
+
"""Navigates to the OrangeHRM login page."""
|
|
27
|
+
self.page.goto(self.base_url + "/web/index.php/auth/login")
|
|
28
|
+
self.logger.info(f"Navigated to: {self.page.url}")
|
|
29
|
+
|
|
30
|
+
def login(self, username, password):
|
|
31
|
+
"""
|
|
32
|
+
Performs a login action with the given username and password.
|
|
33
|
+
"""
|
|
34
|
+
self.fill(self.USERNAME_INPUT, username)
|
|
35
|
+
self.fill(self.PASSWORD_INPUT, password)
|
|
36
|
+
self.click(self.LOGIN_BUTTON)
|
|
37
|
+
self.logger.info(f"Attempted login with username: {username}")
|
|
38
|
+
|
|
39
|
+
def verify_login_success(self):
|
|
40
|
+
"""
|
|
41
|
+
Verifies that the login was successful by checking for a dashboard element.
|
|
42
|
+
"""
|
|
43
|
+
# Playwright's expect.to_be_visible() is implicitly used by is_visible() which auto-waits
|
|
44
|
+
is_visible = self.is_visible(self.DASHBOARD_HEADER)
|
|
45
|
+
self.logger.info(f"Dashboard header visibility: {is_visible}")
|
|
46
|
+
return is_visible
|
|
47
|
+
|
|
48
|
+
def get_dashboard_header_text(self):
|
|
49
|
+
"""
|
|
50
|
+
Gets the text of the dashboard header after successful login.
|
|
51
|
+
"""
|
|
52
|
+
return self.text(self.DASHBOARD_HEADER)
|
|
53
|
+
|
|
54
|
+
def is_error_message_visible(self) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Checks if the invalid credentials error message is visible.
|
|
57
|
+
"""
|
|
58
|
+
return self.is_visible(self.INVALID_CREDENTIALS_MESSAGE)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""SauceDemo Inventory Page Object for UI Validation Examples."""
|
|
2
|
+
|
|
3
|
+
from playwright.sync_api import Page, Locator
|
|
4
|
+
from core.base_page import BasePage
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SauceDemoInventoryPage(BasePage):
|
|
9
|
+
"""Page object for SauceDemo inventory/products page."""
|
|
10
|
+
|
|
11
|
+
# Locators
|
|
12
|
+
APP_LOGO = 'div.app_logo'
|
|
13
|
+
INVENTORY_CONTAINER = 'div.inventory_container'
|
|
14
|
+
INVENTORY_ITEMS = 'div.inventory_item'
|
|
15
|
+
PRODUCT_NAME = 'div.inventory_item_name'
|
|
16
|
+
PRODUCT_PRICE = 'div.inventory_item_price'
|
|
17
|
+
ADD_TO_CART_BUTTON = 'button[data-test^="add-to-cart"]'
|
|
18
|
+
REMOVE_FROM_CART_BUTTON = 'button[data-test^="remove"]'
|
|
19
|
+
SHOPPING_CART_BADGE = 'span.shopping_cart_badge'
|
|
20
|
+
SHOPPING_CART_LINK = 'a.shopping_cart_link'
|
|
21
|
+
PRODUCT_SORT_DROPDOWN = 'select.product_sort_container'
|
|
22
|
+
BURGER_MENU = 'button#react-burger-menu-btn'
|
|
23
|
+
LOGOUT_LINK = 'a#logout_sidebar_link'
|
|
24
|
+
|
|
25
|
+
def __init__(self, page: Page, resilience_policy: dict[str, Any] | None = None):
|
|
26
|
+
"""Initialize SauceDemo inventory page."""
|
|
27
|
+
super().__init__(page, resilience_policy)
|
|
28
|
+
|
|
29
|
+
def is_on_inventory_page(self) -> bool:
|
|
30
|
+
"""Verify user is on inventory page."""
|
|
31
|
+
return self.is_visible(self.INVENTORY_CONTAINER, timeout=10000)
|
|
32
|
+
|
|
33
|
+
def get_page_title(self) -> str:
|
|
34
|
+
"""Get page title text from app logo."""
|
|
35
|
+
return self.text(self.APP_LOGO)
|
|
36
|
+
|
|
37
|
+
def get_product_count(self) -> int:
|
|
38
|
+
"""Get total number of products displayed."""
|
|
39
|
+
items = self.page.locator(self.INVENTORY_ITEMS).all()
|
|
40
|
+
return len(items)
|
|
41
|
+
|
|
42
|
+
def get_product_names(self) -> list[str]:
|
|
43
|
+
"""Get list of all product names."""
|
|
44
|
+
names = []
|
|
45
|
+
items = self.page.locator(self.INVENTORY_ITEMS).all()
|
|
46
|
+
for item in items:
|
|
47
|
+
name_locator = item.locator(self.PRODUCT_NAME)
|
|
48
|
+
names.append(name_locator.inner_text())
|
|
49
|
+
return names
|
|
50
|
+
|
|
51
|
+
def get_product_prices(self) -> list[float]:
|
|
52
|
+
"""Get list of all product prices as floats."""
|
|
53
|
+
prices = []
|
|
54
|
+
items = self.page.locator(self.INVENTORY_ITEMS).all()
|
|
55
|
+
for item in items:
|
|
56
|
+
price_locator = item.locator(self.PRODUCT_PRICE)
|
|
57
|
+
price_text = price_locator.inner_text().replace("$", "")
|
|
58
|
+
prices.append(float(price_text))
|
|
59
|
+
return prices
|
|
60
|
+
|
|
61
|
+
def add_product_to_cart_by_name(self, product_name: str) -> None:
|
|
62
|
+
"""Add specific product to cart by product name."""
|
|
63
|
+
product_selector = f'{self.INVENTORY_ITEMS}:has-text("{product_name}")'
|
|
64
|
+
product_item = self.page.locator(product_selector)
|
|
65
|
+
add_button = product_item.locator(self.ADD_TO_CART_BUTTON)
|
|
66
|
+
add_button.click()
|
|
67
|
+
|
|
68
|
+
def add_all_products_to_cart(self) -> int:
|
|
69
|
+
"""Add all products to cart and return count."""
|
|
70
|
+
buttons = self.page.locator(self.ADD_TO_CART_BUTTON).all()
|
|
71
|
+
for button in buttons:
|
|
72
|
+
button.click()
|
|
73
|
+
return len(buttons)
|
|
74
|
+
|
|
75
|
+
def get_cart_badge_count(self) -> int:
|
|
76
|
+
"""Get number displayed in shopping cart badge."""
|
|
77
|
+
if not self.is_visible(self.SHOPPING_CART_BADGE, timeout=2000):
|
|
78
|
+
return 0
|
|
79
|
+
badge_text = self.text(self.SHOPPING_CART_BADGE)
|
|
80
|
+
return int(badge_text)
|
|
81
|
+
|
|
82
|
+
def sort_products(self, sort_option: str) -> None:
|
|
83
|
+
"""Sort products by option (az, za, lohi, hilo)."""
|
|
84
|
+
self.page.select_option(self.PRODUCT_SORT_DROPDOWN, sort_option)
|
|
85
|
+
|
|
86
|
+
def open_burger_menu(self) -> None:
|
|
87
|
+
"""Open burger menu."""
|
|
88
|
+
self.click(self.BURGER_MENU)
|
|
89
|
+
|
|
90
|
+
def logout(self) -> None:
|
|
91
|
+
"""Logout from application."""
|
|
92
|
+
self.open_burger_menu()
|
|
93
|
+
self.click(self.LOGOUT_LINK)
|
|
94
|
+
|
|
95
|
+
def is_product_in_cart(self, product_name: str) -> bool:
|
|
96
|
+
"""Check if specific product has been added to cart (Remove button visible)."""
|
|
97
|
+
product_selector = f'{self.INVENTORY_ITEMS}:has-text("{product_name}")'
|
|
98
|
+
product_item = self.page.locator(product_selector)
|
|
99
|
+
remove_button = product_item.locator(self.REMOVE_FROM_CART_BUTTON)
|
|
100
|
+
return remove_button.is_visible()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""SauceDemo Login Page Object for UI Validation Examples."""
|
|
2
|
+
|
|
3
|
+
from playwright.sync_api import Page
|
|
4
|
+
from core.base_page import BasePage
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SauceDemoLoginPage(BasePage):
|
|
9
|
+
"""Page object for SauceDemo login page (https://www.saucedemo.com)."""
|
|
10
|
+
|
|
11
|
+
# Locators
|
|
12
|
+
USERNAME_INPUT = 'input[data-test="username"]'
|
|
13
|
+
PASSWORD_INPUT = 'input[data-test="password"]'
|
|
14
|
+
LOGIN_BUTTON = 'input[data-test="login-button"]'
|
|
15
|
+
ERROR_MESSAGE = 'h3[data-test="error"]'
|
|
16
|
+
ERROR_BUTTON = 'button.error-button'
|
|
17
|
+
|
|
18
|
+
def __init__(self, page: Page, resilience_policy: dict[str, Any] | None = None):
|
|
19
|
+
"""Initialize SauceDemo login page."""
|
|
20
|
+
super().__init__(page, resilience_policy)
|
|
21
|
+
|
|
22
|
+
def login(self, username: str, password: str) -> None:
|
|
23
|
+
"""Perform login with credentials."""
|
|
24
|
+
self.fill(self.USERNAME_INPUT, username)
|
|
25
|
+
self.fill(self.PASSWORD_INPUT, password)
|
|
26
|
+
self.click(self.LOGIN_BUTTON)
|
|
27
|
+
|
|
28
|
+
def is_error_displayed(self) -> bool:
|
|
29
|
+
"""Check if error message is displayed."""
|
|
30
|
+
return self.is_visible(self.ERROR_MESSAGE, timeout=5000)
|
|
31
|
+
|
|
32
|
+
def get_error_message(self) -> str:
|
|
33
|
+
"""Get error message text."""
|
|
34
|
+
return self.text(self.ERROR_MESSAGE)
|
|
35
|
+
|
|
36
|
+
def clear_login_form(self) -> None:
|
|
37
|
+
"""Clear login form fields."""
|
|
38
|
+
self.fill(self.USERNAME_INPUT, "")
|
|
39
|
+
self.fill(self.PASSWORD_INPUT, "")
|
plugins/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Plugin architecture exports for lifecycle extensibility."""
|
|
2
|
+
|
|
3
|
+
from .api_plugin import ApiPlugin
|
|
4
|
+
from .base import FrameworkPlugin
|
|
5
|
+
from .data_plugin import DataPlugin
|
|
6
|
+
from .registry import PluginRegistry, build_plugin_registry
|
|
7
|
+
from .security_plugin import SecurityPlugin
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"FrameworkPlugin",
|
|
11
|
+
"PluginRegistry",
|
|
12
|
+
"build_plugin_registry",
|
|
13
|
+
"ApiPlugin",
|
|
14
|
+
"DataPlugin",
|
|
15
|
+
"SecurityPlugin",
|
|
16
|
+
]
|
plugins/api_plugin.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Built-in API plugin placeholder for lifecycle-driven extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from plugins.base import FrameworkPlugin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiPlugin(FrameworkPlugin):
|
|
9
|
+
"""Default API plugin extension point."""
|
|
10
|
+
|
|
11
|
+
name = "api_plugin"
|
plugins/base.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Plugin contracts for framework lifecycle extensibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FrameworkPlugin(ABC):
|
|
10
|
+
"""Base plugin contract with lifecycle hooks."""
|
|
11
|
+
|
|
12
|
+
name: str = "plugin"
|
|
13
|
+
|
|
14
|
+
def on_session_start(self, context: Mapping[str, Any]) -> None:
|
|
15
|
+
"""Hook called when a framework session starts."""
|
|
16
|
+
|
|
17
|
+
def on_test_result(self, context: Mapping[str, Any]) -> None:
|
|
18
|
+
"""Hook called after each test call-phase result."""
|
|
19
|
+
|
|
20
|
+
def on_session_finish(self, context: Mapping[str, Any]) -> None:
|
|
21
|
+
"""Hook called when a framework session ends."""
|
plugins/data_plugin.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Built-in data plugin placeholder for lifecycle-driven extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from plugins.base import FrameworkPlugin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataPlugin(FrameworkPlugin):
|
|
9
|
+
"""Default data plugin extension point."""
|
|
10
|
+
|
|
11
|
+
name = "data_plugin"
|
plugins/registry.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Plugin registry and configuration-driven plugin loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from plugins.api_plugin import ApiPlugin
|
|
8
|
+
from plugins.base import FrameworkPlugin
|
|
9
|
+
from plugins.data_plugin import DataPlugin
|
|
10
|
+
from plugins.security_plugin import SecurityPlugin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PluginRegistry:
|
|
14
|
+
"""Manage plugin registration and lifecycle hook dispatch."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, logger=None) -> None:
|
|
17
|
+
self.logger = logger
|
|
18
|
+
self._plugins: dict[str, FrameworkPlugin] = {}
|
|
19
|
+
|
|
20
|
+
def register(self, plugin: FrameworkPlugin) -> None:
|
|
21
|
+
"""Register one plugin instance by unique name."""
|
|
22
|
+
name = getattr(plugin, "name", "")
|
|
23
|
+
if not name:
|
|
24
|
+
raise ValueError("Plugin name must be a non-empty string")
|
|
25
|
+
if name in self._plugins:
|
|
26
|
+
raise ValueError(f"Plugin '{name}' is already registered")
|
|
27
|
+
self._plugins[name] = plugin
|
|
28
|
+
|
|
29
|
+
def names(self) -> list[str]:
|
|
30
|
+
"""Return registered plugin names in insertion order."""
|
|
31
|
+
return list(self._plugins.keys())
|
|
32
|
+
|
|
33
|
+
def dispatch(self, hook_name: str, context: Mapping[str, Any]) -> list[str]:
|
|
34
|
+
"""Dispatch lifecycle hook to all registered plugins."""
|
|
35
|
+
invoked: list[str] = []
|
|
36
|
+
for plugin in self._plugins.values():
|
|
37
|
+
hook = getattr(plugin, hook_name, None)
|
|
38
|
+
if not callable(hook):
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
hook(context)
|
|
42
|
+
invoked.append(plugin.name)
|
|
43
|
+
except Exception as error: # pragma: no cover - defensive, covered by unit test via generic Exception
|
|
44
|
+
raise RuntimeError(f"Plugin hook failed: plugin={plugin.name}, hook={hook_name}") from error
|
|
45
|
+
|
|
46
|
+
if self.logger and invoked:
|
|
47
|
+
self.logger.info("Plugin hook dispatched: %s | plugins=%s", hook_name, ",".join(invoked))
|
|
48
|
+
return invoked
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
BUILTIN_PLUGIN_FACTORIES = {
|
|
52
|
+
"api_plugin": ApiPlugin,
|
|
53
|
+
"data_plugin": DataPlugin,
|
|
54
|
+
"security_plugin": SecurityPlugin,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_plugin_registry(config: Mapping[str, Any], logger=None) -> PluginRegistry:
|
|
59
|
+
"""Build plugin registry from framework configuration."""
|
|
60
|
+
registry = PluginRegistry(logger=logger)
|
|
61
|
+
if not isinstance(config, Mapping):
|
|
62
|
+
return registry
|
|
63
|
+
|
|
64
|
+
if not bool(config.get("enabled", False)):
|
|
65
|
+
return registry
|
|
66
|
+
|
|
67
|
+
enabled_plugins = config.get("enabled_plugins", [])
|
|
68
|
+
if not isinstance(enabled_plugins, list):
|
|
69
|
+
raise ValueError("plugins.enabled_plugins must be a list")
|
|
70
|
+
|
|
71
|
+
for plugin_name in enabled_plugins:
|
|
72
|
+
factory = BUILTIN_PLUGIN_FACTORIES.get(str(plugin_name).strip())
|
|
73
|
+
if factory is None:
|
|
74
|
+
raise ValueError(f"Unsupported plugin name: {plugin_name}")
|
|
75
|
+
registry.register(factory())
|
|
76
|
+
|
|
77
|
+
return registry
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Built-in security plugin placeholder for lifecycle-driven extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from plugins.base import FrameworkPlugin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecurityPlugin(FrameworkPlugin):
|
|
9
|
+
"""Default security plugin extension point."""
|
|
10
|
+
|
|
11
|
+
name = "security_plugin"
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyplaykit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Enterprise Python + Playwright automation framework
|
|
5
|
+
Author-email: Shan Konduru <shankonduru@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ShanKonduru/pyplaykit
|
|
8
|
+
Project-URL: Repository, https://github.com/ShanKonduru/pyplaykit
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/ShanKonduru/pyplaykit/issues
|
|
10
|
+
Keywords: playwright,pytest,automation,framework,enterprise,testing,page-object-model
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Framework :: Pytest
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: playwright==1.54.0
|
|
23
|
+
Requires-Dist: pytest==8.4.1
|
|
24
|
+
Requires-Dist: pytest-cov==6.2.1
|
|
25
|
+
Requires-Dist: pytest-xdist==3.6.1
|
|
26
|
+
Requires-Dist: pytest-rerunfailures==15.1
|
|
27
|
+
Requires-Dist: pytest-html==4.1.1
|
|
28
|
+
Requires-Dist: allure-pytest==2.13.5
|
|
29
|
+
Requires-Dist: PyYAML==6.0.2
|
|
30
|
+
|
|
31
|
+
# PyPlayKit - Enterprise Test Automation Framework
|
|
32
|
+
|
|
33
|
+
Tagline: A modular, scalable, and CI/CD-ready Python + Playwright framework for enterprise automation.
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
PyPlayKit is a layered and extensible enterprise UI automation framework built with Python, Playwright, and Pytest.
|
|
37
|
+
|
|
38
|
+
It is designed to provide:
|
|
39
|
+
- standardized test automation practices
|
|
40
|
+
- strong separation between framework code and business test logic
|
|
41
|
+
- deterministic execution with reusable browser lifecycle management
|
|
42
|
+
- CI/CD-friendly reporting and diagnostics
|
|
43
|
+
|
|
44
|
+
## Architecture Layers
|
|
45
|
+
- Test Layer: business-readable test flows using Pytest
|
|
46
|
+
- Page Object Layer: encapsulated locators, page actions, and validations
|
|
47
|
+
- Core Engine Layer: Playwright lifecycle and browser/context management
|
|
48
|
+
- Utilities Layer: logging, data loading, assertions, and screenshots
|
|
49
|
+
- API and Data Validation Layer: reusable API client, response validators, and data validators
|
|
50
|
+
- Observability and Readiness Layer: execution metrics, KPI outputs, and environment readiness checks
|
|
51
|
+
- Configuration Layer: externalized YAML configuration and environment mapping
|
|
52
|
+
- Test Data Layer: JSON data sources with environment variable placeholders
|
|
53
|
+
- Reporting and Logging Layer: HTML report output and structured logs
|
|
54
|
+
|
|
55
|
+
## Project Structure
|
|
56
|
+
pyplaykit/
|
|
57
|
+
- config/
|
|
58
|
+
- config.yaml
|
|
59
|
+
- environments.yaml
|
|
60
|
+
- core/
|
|
61
|
+
- base_page.py
|
|
62
|
+
- base_test.py
|
|
63
|
+
- browser_factory.py
|
|
64
|
+
- playwright_manager.py
|
|
65
|
+
- integrations/
|
|
66
|
+
- adapters.py
|
|
67
|
+
- orchestration/
|
|
68
|
+
- planner.py
|
|
69
|
+
- packaging/
|
|
70
|
+
- distribution.py
|
|
71
|
+
- plugins/
|
|
72
|
+
- base.py
|
|
73
|
+
- registry.py
|
|
74
|
+
- api_plugin.py
|
|
75
|
+
- data_plugin.py
|
|
76
|
+
- security_plugin.py
|
|
77
|
+
- resilience/
|
|
78
|
+
- locator_recovery.py
|
|
79
|
+
- pages/
|
|
80
|
+
- login_page.py
|
|
81
|
+
- dashboard_page.py
|
|
82
|
+
- tests/
|
|
83
|
+
- test_login.py
|
|
84
|
+
- test_dashboard.py
|
|
85
|
+
- utils/
|
|
86
|
+
- logger.py
|
|
87
|
+
- data_loader.py
|
|
88
|
+
- assertion_helper.py
|
|
89
|
+
- screenshot_helper.py
|
|
90
|
+
- api_client.py
|
|
91
|
+
- response_validator.py
|
|
92
|
+
- data_validator.py
|
|
93
|
+
- config_reader.py
|
|
94
|
+
- observability.py
|
|
95
|
+
- environment_validator.py
|
|
96
|
+
- tdm.py
|
|
97
|
+
- test_data/
|
|
98
|
+
- login_test_data.json
|
|
99
|
+
- reports/
|
|
100
|
+
- conftest.py
|
|
101
|
+
- pyproject.toml
|
|
102
|
+
- MANIFEST.in
|
|
103
|
+
- pytest.ini
|
|
104
|
+
- requirements.txt
|
|
105
|
+
- README.md
|
|
106
|
+
- .gitignore
|
|
107
|
+
|
|
108
|
+
## Prerequisites
|
|
109
|
+
- Python 3.11 or higher
|
|
110
|
+
- pip
|
|
111
|
+
|
|
112
|
+
## Setup
|
|
113
|
+
1. Create and activate virtual environment.
|
|
114
|
+
|
|
115
|
+
Windows PowerShell:
|
|
116
|
+
```powershell
|
|
117
|
+
python -m venv .venv
|
|
118
|
+
.\.venv\Scripts\Activate.ps1
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
2. Install framework dependencies.
|
|
122
|
+
|
|
123
|
+
```powershell
|
|
124
|
+
pip install -r requirements.txt
|
|
125
|
+
playwright install
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
3. Install dev security tooling.
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
pip install -r requirements-dev.txt
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
4. Set credentials for tests requiring valid login.
|
|
135
|
+
|
|
136
|
+
```powershell
|
|
137
|
+
$env:PYPLAYKIT_TEST_USERNAME="standard_user"
|
|
138
|
+
$env:PYPLAYKIT_TEST_PASSWORD="secret_sauce"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Runtime Configuration
|
|
142
|
+
Primary execution settings are in config/config.yaml.
|
|
143
|
+
|
|
144
|
+
Supported runtime overrides:
|
|
145
|
+
- --pyplaykit-env dev|qa|uat|prod
|
|
146
|
+
- --pyplaykit-browser chromium|firefox|webkit
|
|
147
|
+
- --pyplaykit-headed
|
|
148
|
+
- --pyplaykit-base-url <url>
|
|
149
|
+
- --pyplaykit-readiness-check
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
|
|
153
|
+
```powershell
|
|
154
|
+
pytest --pyplaykit-env qa --pyplaykit-browser chromium
|
|
155
|
+
pytest --pyplaykit-env uat --pyplaykit-browser firefox --pyplaykit-headed
|
|
156
|
+
pytest -m "smoke"
|
|
157
|
+
pytest -m "regression"
|
|
158
|
+
pytest -n 2
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Reporting and Diagnostics
|
|
162
|
+
- HTML report: reports/report.html
|
|
163
|
+
- Framework logs: reports/framework.log
|
|
164
|
+
- Failure screenshots: reports/screenshots/
|
|
165
|
+
- Videos (if enabled): reports/videos/
|
|
166
|
+
- Observability summary: reports/observability/summary.json
|
|
167
|
+
- KPI summary: reports/observability/kpi_summary.json
|
|
168
|
+
- Integration export (Jira): reports/integrations/jira_export.json
|
|
169
|
+
- Integration export (Test Management): reports/integrations/test_management_export.json
|
|
170
|
+
|
|
171
|
+
## Data-Driven Support
|
|
172
|
+
- JSON test data is stored under test_data/.
|
|
173
|
+
- Placeholder values in the form ${ENV_VAR} are resolved from environment variables.
|
|
174
|
+
|
|
175
|
+
## API and Data Validation Support
|
|
176
|
+
- API client utility in utils/api_client.py supports GET/POST requests, retry handling, and normalized response objects.
|
|
177
|
+
- API response validators in utils/response_validator.py support status, response time, key presence, and JSON path value checks.
|
|
178
|
+
- Data validators in utils/data_validator.py support required-field validation, type validation, record comparison, and collection subset checks.
|
|
179
|
+
|
|
180
|
+
## Wave 1 Status (Implemented)
|
|
181
|
+
Wave 1 foundation controls from the enterprise roadmap are now implemented:
|
|
182
|
+
- Observability tracker for execution metrics, failure classification, flaky identification, and KPI summaries.
|
|
183
|
+
- Environment readiness checks for URL availability, API health, and optional DB connectivity.
|
|
184
|
+
- Session-level observability report generation integrated into pytest lifecycle.
|
|
185
|
+
|
|
186
|
+
Execution notes:
|
|
187
|
+
- Readiness checks are optional by default and can be enabled with `--pyplaykit-readiness-check`.
|
|
188
|
+
- Readiness and observability defaults are configured in config/config.yaml.
|
|
189
|
+
|
|
190
|
+
## Wave 2 Status (Implemented)
|
|
191
|
+
Wave 2 includes Test Data Management utilities:
|
|
192
|
+
- Deterministic synthetic data generation by schema/seed
|
|
193
|
+
- Field-level masking utilities for sensitive data
|
|
194
|
+
- Reusable data reset helper for list/dict/callback stores
|
|
195
|
+
- Environment-specific data provisioning helper
|
|
196
|
+
- Config-driven TDM policy wiring with parser and validator integrated into runtime options
|
|
197
|
+
|
|
198
|
+
Wave 2 Multi-level Reporting is now implemented:
|
|
199
|
+
- Engineering report JSON output
|
|
200
|
+
- QA functional report JSON output
|
|
201
|
+
- Leadership KPI summary JSON output
|
|
202
|
+
|
|
203
|
+
Wave 2 Integration Layer has started with file-based adapters:
|
|
204
|
+
- Integration adapter interface contract for enterprise connectors
|
|
205
|
+
- Jira file export adapter
|
|
206
|
+
- Test management file export adapter
|
|
207
|
+
|
|
208
|
+
Wave 2 Integration Layer now also includes API adapters:
|
|
209
|
+
- Jira API export adapter
|
|
210
|
+
- Test management API export adapter
|
|
211
|
+
|
|
212
|
+
## Wave 3 Status (Implemented)
|
|
213
|
+
Wave 3 Plugin Architecture is now implemented:
|
|
214
|
+
- Plugin registry with lifecycle hook dispatch
|
|
215
|
+
- Built-in starter plugins: api_plugin, data_plugin, security_plugin
|
|
216
|
+
- Config-gated plugin enablement to preserve default behavior
|
|
217
|
+
|
|
218
|
+
Wave 3 Orchestration Layer is now implemented:
|
|
219
|
+
- Config-driven suite orchestration contract
|
|
220
|
+
- Dependency-aware execution graph with cycle and schema validation
|
|
221
|
+
- Ordered pipelines with stage-based execution plans
|
|
222
|
+
|
|
223
|
+
## Wave 4 Status (In Progress)
|
|
224
|
+
Wave 4 self-healing implementation is now complete:
|
|
225
|
+
- Optional locator fallback chain support
|
|
226
|
+
- DOM re-evaluation retry support for resilient element resolution
|
|
227
|
+
- Config contract for resilience policy in config/config.yaml
|
|
228
|
+
- Confidence-scored fallback selection based on retries and fallback depth
|
|
229
|
+
- JSONL audit log output for each recovery action when enabled
|
|
230
|
+
|
|
231
|
+
Wave 4 distribution packaging baseline is now implemented:
|
|
232
|
+
- PEP 621 package metadata in pyproject.toml
|
|
233
|
+
- Source and wheel artifact manifest support via MANIFEST.in
|
|
234
|
+
- Internal package build helper and wrappers:
|
|
235
|
+
- scripts/build_internal_package.py
|
|
236
|
+
- scripts/build_internal_package.bat
|
|
237
|
+
- scripts/build_internal_package.sh
|
|
238
|
+
|
|
239
|
+
Remaining for Wave 4:
|
|
240
|
+
- Semantic versioning, release channels, and compatibility matrix governance
|
|
241
|
+
- Automated release notes from conventional commit history
|
|
242
|
+
|
|
243
|
+
## CI/CD Notes
|
|
244
|
+
Framework supports GitHub Actions, Jenkins, and Azure DevOps pipelines through:
|
|
245
|
+
- deterministic command-line execution
|
|
246
|
+
- marker-based selective runs
|
|
247
|
+
- parallel execution with pytest-xdist
|
|
248
|
+
- rerun support for flaky tests via pytest-rerunfailures
|
|
249
|
+
|
|
250
|
+
## Security Scan Reports
|
|
251
|
+
Security scan tooling includes pip-audit, bandit, gitleaks, and sec-report-kit.
|
|
252
|
+
|
|
253
|
+
The security scripts are self-bootstrapping and do the following automatically:
|
|
254
|
+
- create .venv if missing
|
|
255
|
+
- activate the virtual environment
|
|
256
|
+
- upgrade pip
|
|
257
|
+
- install dependencies from requirements.txt and requirements-dev.txt
|
|
258
|
+
- run pip-audit, bandit, gitleaks detect (committed history), and gitleaks dir (working tree) and generate JSON outputs
|
|
259
|
+
- render individual HTML reports and a consolidated HTML report using sec-report-kit
|
|
260
|
+
|
|
261
|
+
Scripts:
|
|
262
|
+
- scripts/run_security_reports.bat
|
|
263
|
+
- scripts/run_security_reports.sh
|
|
264
|
+
|
|
265
|
+
Note:
|
|
266
|
+
- scripts prefer repo-local gitleaks binary if present (for example gitleaks_8.30.1/gitleaks.exe), otherwise they use PATH.
|
|
267
|
+
- the gitleaks dir scan is intentionally limited to tracked project source folders and key files, so third-party package code under .venv is not scanned.
|
|
268
|
+
|
|
269
|
+
Install gitleaks CLI:
|
|
270
|
+
|
|
271
|
+
Windows (winget):
|
|
272
|
+
```powershell
|
|
273
|
+
winget install --id Gitleaks.Gitleaks -e
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Windows (choco):
|
|
277
|
+
```powershell
|
|
278
|
+
choco install gitleaks
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
macOS (brew):
|
|
282
|
+
```bash
|
|
283
|
+
brew install gitleaks
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Linux (Homebrew/Linuxbrew):
|
|
287
|
+
```bash
|
|
288
|
+
brew install gitleaks
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Run on Windows:
|
|
292
|
+
|
|
293
|
+
```powershell
|
|
294
|
+
scripts\run_security_reports.bat
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Run on Linux/macOS:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
bash scripts/run_security_reports.sh
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Outputs:
|
|
304
|
+
- JSON reports: reports/secutiry_reports/json/
|
|
305
|
+
- pip_audit_report.json
|
|
306
|
+
- bandit_report.json
|
|
307
|
+
- gitleaks_git_report.json
|
|
308
|
+
- gitleaks_dir_report.json
|
|
309
|
+
- HTML reports: reports/secutiry_reports/html/
|
|
310
|
+
- pip_audit_report.html
|
|
311
|
+
- bandit_report.html
|
|
312
|
+
- gitleaks_git_report.html
|
|
313
|
+
- gitleaks_dir_report.html
|
|
314
|
+
- Consolidated HTML report: reports/secutiry_reports/html/security_consolidated_report.html
|
|
315
|
+
|
|
316
|
+
## Unit Test Coverage
|
|
317
|
+
Run framework unit tests with coverage metrics.
|
|
318
|
+
|
|
319
|
+
Windows:
|
|
320
|
+
|
|
321
|
+
```powershell
|
|
322
|
+
scripts\run_unit_tests_with_coverage.bat
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Linux/macOS:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
bash scripts/run_unit_tests_with_coverage.sh
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Coverage outputs:
|
|
332
|
+
- Terminal summary with missing lines
|
|
333
|
+
- HTML report: reports/coverage-html/index.html
|
|
334
|
+
- XML report: reports/coverage.xml
|
|
335
|
+
|
|
336
|
+
## Design and Extension Guidance
|
|
337
|
+
- Keep locators in page classes only.
|
|
338
|
+
- Keep tests business-readable and independent.
|
|
339
|
+
- Avoid hardcoded secrets and environment-specific values in test code.
|
|
340
|
+
- Extend under existing layers to preserve maintainability and onboarding simplicity.
|