refactor: SmartLocator with __getattr__ proxying, integrate into BasePage
Ultraworked with Sisyphus(https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
||||
"""Page Object Model base classes for UI testing.
|
||||
|
||||
Provides both synchronous (:class:`BasePage`) and asynchronous
|
||||
(:class:`AsyncBasePage`) base page objects.
|
||||
(:class:`AsyncBasePage`) base page objects with SmartLocator integration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Self
|
||||
|
||||
from pytfm.web.locator import AsyncSmartLocator, SmartLocator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from playwright.async_api import Page as AsyncPage
|
||||
@@ -16,80 +18,236 @@ if TYPE_CHECKING:
|
||||
class BasePage:
|
||||
"""Synchronous base class for all page objects.
|
||||
|
||||
This is the **default** implementation. For async tests use
|
||||
:class:`AsyncBasePage`.
|
||||
Uses :class:`SmartLocator` internally for all element access.
|
||||
The :meth:`locator` method (aliased as :meth:`loc`) provides a shorthand
|
||||
for creating ``data-testid``-based locators bound to the current page.
|
||||
|
||||
Example:
|
||||
>>> class LoginPage(BasePage):
|
||||
... path = "/login"
|
||||
... def login(self, user: str, pwd: str) -> None:
|
||||
... self.loc("username").fill(user)
|
||||
... self.loc("password").fill(pwd)
|
||||
... self.loc("submit").click()
|
||||
"""
|
||||
|
||||
path: str = ""
|
||||
|
||||
def __init__(self, page: "SyncPage", base_url: str) -> None:
|
||||
def __init__(self, page: SyncPage, base_url: str) -> None:
|
||||
"""Initialize the page object.
|
||||
|
||||
Args:
|
||||
page: Playwright sync ``Page`` instance.
|
||||
base_url: Base URL of the application.
|
||||
"""
|
||||
self.page = page
|
||||
self.base_url = base_url.rstrip("/")
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Full page URL composed from ``base_url`` and ``path``."""
|
||||
return f"{self.base_url}{self.path}"
|
||||
|
||||
def open(self) -> BasePage:
|
||||
def locator(
|
||||
self,
|
||||
testid: str,
|
||||
timeout: int = 5000,
|
||||
visible_only: bool = True,
|
||||
) -> SmartLocator:
|
||||
"""Create a :class:`SmartLocator` by ``data-testid`` bound to this page.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` value to locate.
|
||||
timeout: Default wait timeout in milliseconds.
|
||||
visible_only: Only match visible elements.
|
||||
|
||||
Returns:
|
||||
SmartLocator bound to this page for chaining.
|
||||
"""
|
||||
return SmartLocator.by_testid(
|
||||
testid, timeout=timeout, visible_only=visible_only
|
||||
).with_page(self.page)
|
||||
|
||||
loc = locator
|
||||
|
||||
def open(self) -> Self:
|
||||
"""Navigate to the page URL and return self for chaining.
|
||||
|
||||
Returns:
|
||||
Self for method chaining.
|
||||
"""
|
||||
self.page.goto(self.url)
|
||||
return self
|
||||
|
||||
def wait_for_element(self, selector: str, timeout: int = 5000) -> None:
|
||||
self.page.wait_for_selector(selector, state="visible", timeout=timeout)
|
||||
def wait_for_element(
|
||||
self,
|
||||
testid: str,
|
||||
timeout: int | None = None,
|
||||
) -> None:
|
||||
"""Wait for an element to become visible.
|
||||
|
||||
def click(self, selector: str) -> None:
|
||||
self.page.click(selector)
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
timeout: Timeout in milliseconds. Defaults to 5000.
|
||||
"""
|
||||
self.locator(testid, timeout=timeout or 5000).wait_for(state="visible")
|
||||
|
||||
def fill(self, selector: str, value: str) -> None:
|
||||
self.page.fill(selector, value)
|
||||
def click(self, testid: str) -> None:
|
||||
"""Click an element.
|
||||
|
||||
def get_text(self, selector: str) -> str:
|
||||
element = self.page.wait_for_selector(selector)
|
||||
if element:
|
||||
return element.text_content() or ""
|
||||
return ""
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
"""
|
||||
self.locator(testid).click()
|
||||
|
||||
def is_visible(self, selector: str) -> bool:
|
||||
element = self.page.query_selector(selector)
|
||||
if element:
|
||||
return element.is_visible()
|
||||
return False
|
||||
def fill(self, testid: str, value: str) -> None:
|
||||
"""Fill an element with text.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
value: Text to fill.
|
||||
"""
|
||||
self.locator(testid).fill(value)
|
||||
|
||||
def get_text(self, testid: str) -> str:
|
||||
"""Get the text content of an element.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
|
||||
Returns:
|
||||
Text content or empty string.
|
||||
"""
|
||||
text = self.locator(testid).text_content()
|
||||
return text if text is not None else ""
|
||||
|
||||
def is_visible(self, testid: str) -> bool:
|
||||
"""Check if an element is visible.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
|
||||
Returns:
|
||||
``True`` if the element is visible.
|
||||
"""
|
||||
return self.locator(testid)._get_locator().is_visible()
|
||||
|
||||
|
||||
class AsyncBasePage:
|
||||
"""Asynchronous base class for page objects using ``playwright.async_api``."""
|
||||
"""Asynchronous base class for page objects using ``playwright.async_api``.
|
||||
|
||||
Uses :class:`AsyncSmartLocator` internally for all element access.
|
||||
The :meth:`locator` method (aliased as :meth:`loc`) provides an async-safe
|
||||
shorthand for creating ``data-testid``-based locators bound to the page.
|
||||
|
||||
Example:
|
||||
>>> class LoginPage(AsyncBasePage):
|
||||
... path = "/login"
|
||||
... async def login(self, user: str, pwd: str) -> None:
|
||||
... await self.loc("username").fill(user)
|
||||
... await self.loc("password").fill(pwd)
|
||||
... await self.loc("submit").click()
|
||||
"""
|
||||
|
||||
path: str = ""
|
||||
|
||||
def __init__(self, page: "AsyncPage", base_url: str) -> None:
|
||||
def __init__(self, page: AsyncPage, base_url: str) -> None:
|
||||
"""Initialize the async page object.
|
||||
|
||||
Args:
|
||||
page: Playwright async ``Page`` instance.
|
||||
base_url: Base URL of the application.
|
||||
"""
|
||||
self.page = page
|
||||
self.base_url = base_url.rstrip("/")
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Full page URL composed from ``base_url`` and ``path``."""
|
||||
return f"{self.base_url}{self.path}"
|
||||
|
||||
async def open(self) -> AsyncBasePage:
|
||||
def locator(
|
||||
self,
|
||||
testid: str,
|
||||
timeout: int = 5000,
|
||||
visible_only: bool = True,
|
||||
) -> AsyncSmartLocator:
|
||||
"""Create an :class:`AsyncSmartLocator` by ``data-testid`` bound to this page.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` value to locate.
|
||||
timeout: Default wait timeout in milliseconds.
|
||||
visible_only: Only match visible elements.
|
||||
|
||||
Returns:
|
||||
AsyncSmartLocator bound to this page.
|
||||
"""
|
||||
return AsyncSmartLocator.by_testid(
|
||||
testid, timeout=timeout, visible_only=visible_only
|
||||
).with_page(self.page)
|
||||
|
||||
loc = locator
|
||||
|
||||
async def open(self) -> Self:
|
||||
"""Navigate to the page URL and return self for chaining.
|
||||
|
||||
Returns:
|
||||
Self for method chaining.
|
||||
"""
|
||||
await self.page.goto(self.url)
|
||||
return self
|
||||
|
||||
async def wait_for_element(self, selector: str, timeout: int = 5000) -> None:
|
||||
await self.page.wait_for_selector(selector, state="visible", timeout=timeout)
|
||||
async def wait_for_element(
|
||||
self,
|
||||
testid: str,
|
||||
timeout: int | None = None,
|
||||
) -> None:
|
||||
"""Wait for an element to become visible.
|
||||
|
||||
async def click(self, selector: str) -> None:
|
||||
await self.page.click(selector)
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
timeout: Timeout in milliseconds. Defaults to 5000.
|
||||
"""
|
||||
await self.locator(
|
||||
testid, timeout=timeout or 5000
|
||||
).wait_for(state="visible")
|
||||
|
||||
async def fill(self, selector: str, value: str) -> None:
|
||||
await self.page.fill(selector, value)
|
||||
async def click(self, testid: str) -> None:
|
||||
"""Click an element.
|
||||
|
||||
async def get_text(self, selector: str) -> str:
|
||||
element = await self.page.wait_for_selector(selector)
|
||||
if element:
|
||||
return await element.text_content() or ""
|
||||
return ""
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
"""
|
||||
await self.locator(testid).click()
|
||||
|
||||
async def is_visible(self, selector: str) -> bool:
|
||||
element = await self.page.query_selector(selector)
|
||||
if element:
|
||||
return await element.is_visible()
|
||||
return False
|
||||
async def fill(self, testid: str, value: str) -> None:
|
||||
"""Fill an element with text.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
value: Text to fill.
|
||||
"""
|
||||
await self.locator(testid).fill(value)
|
||||
|
||||
async def get_text(self, testid: str) -> str:
|
||||
"""Get the text content of an element.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
|
||||
Returns:
|
||||
Text content or empty string.
|
||||
"""
|
||||
text = await self.locator(testid).text_content()
|
||||
return text if text is not None else ""
|
||||
|
||||
async def is_visible(self, testid: str) -> bool:
|
||||
"""Check if an element is visible.
|
||||
|
||||
Args:
|
||||
testid: The ``data-testid`` of the element.
|
||||
|
||||
Returns:
|
||||
``True`` if the element is visible.
|
||||
"""
|
||||
return await self.locator(testid)._get_locator().is_visible()
|
||||
|
||||
Reference in New Issue
Block a user