f14002559e
refactor(components): 调整头部和页脚布局样式 style(hero-section): 更新徽章动画效果 docs: 添加测试框架README文档 test: 实现首页、导航和联系表单的测试用例 ci: 添加CI测试脚本和配置
319 lines
10 KiB
Python
319 lines
10 KiB
Python
"""
|
|
页面对象基类
|
|
提供页面对象模式的基础框架
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
from urllib.parse import urljoin, urlparse
|
|
|
|
from playwright.sync_api import Page, Locator, FrameLocator
|
|
from playwright.sync_api import Error as PlaywrightError, TimeoutError as PlaywrightTimeoutError
|
|
|
|
from config.settings import get_settings
|
|
from utils.logger import get_logger
|
|
from utils.helpers import ElementHelper, PageHelper, AssertionHelper, UrlHelper
|
|
|
|
|
|
class BasePage:
|
|
"""页面对象基类"""
|
|
|
|
def __init__(self, page: Page, base_url: Optional[str] = None):
|
|
"""
|
|
初始化页面对象
|
|
|
|
Args:
|
|
page: Playwright Page实例
|
|
base_url: 基础URL
|
|
"""
|
|
self.page = page
|
|
self.base_url = base_url or get_settings().get_base_url()
|
|
self.logger = get_logger()
|
|
|
|
# 初始化辅助类
|
|
self.element = ElementHelper(page)
|
|
self.page_helper = PageHelper(page)
|
|
self.assertion = AssertionHelper(page)
|
|
self.url_helper = UrlHelper()
|
|
|
|
# 页面URL路径(子类覆盖)
|
|
self.path: Optional[str] = None
|
|
|
|
# 页面标题(子类覆盖)
|
|
self.title: Optional[str] = None
|
|
|
|
# 页面元素选择器(子类覆盖)
|
|
self.selectors: Dict[str, str] = {}
|
|
|
|
def _resolve_selector(self, selector: str) -> str:
|
|
"""解析选择器名称为实际选择器字符串"""
|
|
if selector in self.selectors:
|
|
return self.selectors[selector]
|
|
return selector
|
|
|
|
def _get_full_url(self, path: str) -> str:
|
|
"""获取完整URL"""
|
|
if self.url_helper.is_absolute_url(path):
|
|
return path
|
|
return urljoin(self.base_url, path)
|
|
|
|
def navigate(self, path: Optional[str] = None, **kwargs) -> 'BasePage':
|
|
"""
|
|
导航到页面
|
|
|
|
Args:
|
|
path: 页面路径,如果为None则使用self.path
|
|
**kwargs: 传递给page.goto的参数
|
|
|
|
Returns:
|
|
self
|
|
"""
|
|
path = path or self.path
|
|
if not path:
|
|
raise ValueError("页面路径未指定")
|
|
|
|
url = self._get_full_url(path)
|
|
self.logger.log_action(f"导航到页面: {url}")
|
|
|
|
self.page_helper.navigate(url, **kwargs)
|
|
|
|
return self
|
|
|
|
def reload(self) -> 'BasePage':
|
|
"""刷新页面"""
|
|
self.page_helper.reload_page()
|
|
return self
|
|
|
|
def go_back(self) -> 'BasePage':
|
|
"""返回上一页"""
|
|
self.page_helper.go_back()
|
|
return self
|
|
|
|
def go_forward(self) -> 'BasePage':
|
|
"""前进到下一页"""
|
|
self.page_helper.go_forward()
|
|
return self
|
|
|
|
def get_url(self) -> str:
|
|
"""获取当前URL"""
|
|
return self.page_helper.get_current_url()
|
|
|
|
def get_title(self) -> str:
|
|
"""获取页面标题"""
|
|
return self.page_helper.get_page_title()
|
|
|
|
def wait_for_load(self, state: str = "networkidle") -> 'BasePage':
|
|
"""等待页面加载完成"""
|
|
self.page_helper.wait_for_load_state(state)
|
|
return self
|
|
|
|
def wait_for_selector(
|
|
self,
|
|
selector: str,
|
|
timeout: Optional[int] = None,
|
|
state: str = "visible"
|
|
) -> Locator:
|
|
"""等待选择器"""
|
|
return self.element.wait_for_selector(selector, timeout, state)
|
|
|
|
def scroll_to_top(self) -> 'BasePage':
|
|
"""滚动到页面顶部"""
|
|
self.page_helper.scroll_to_top()
|
|
return self
|
|
|
|
def scroll_to_bottom(self) -> 'BasePage':
|
|
"""滚动到页面底部"""
|
|
self.page_helper.scroll_to_bottom()
|
|
return self
|
|
|
|
def scroll_to_element(self, selector: str) -> 'BasePage':
|
|
"""滚动到指定元素"""
|
|
self.page_helper.scroll_to_element(selector)
|
|
return self
|
|
|
|
def take_screenshot(
|
|
self,
|
|
name: str,
|
|
full_page: bool = False
|
|
) -> str:
|
|
"""截取截图"""
|
|
return self.page_helper.take_screenshot(
|
|
f"{name}_{self._get_timestamp()}.png",
|
|
full_page=full_page
|
|
)
|
|
|
|
def execute_js(self, script: str, *args) -> Any:
|
|
"""执行JavaScript"""
|
|
return self.page_helper.execute_javascript(script, *args)
|
|
|
|
def _get_timestamp(self) -> str:
|
|
"""获取时间戳"""
|
|
from datetime import datetime
|
|
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
def _find(self, selector: str, timeout: Optional[int] = None) -> Locator:
|
|
"""查找元素"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
return self.element.find_element(resolved_selector, timeout)
|
|
|
|
def _find_all(self, selector: str) -> List[Locator]:
|
|
"""查找所有匹配的元素"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
return self.element.find_elements(resolved_selector)
|
|
|
|
def _click(self, selector: str, **kwargs) -> 'BasePage':
|
|
"""点击元素"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
self.element.click_element(resolved_selector, **kwargs)
|
|
return self
|
|
|
|
def _fill(self, selector: str, value: str, **kwargs) -> 'BasePage':
|
|
"""填充输入框"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
self.element.fill_input(resolved_selector, value, **kwargs)
|
|
return self
|
|
|
|
def _type(self, selector: str, text: str, **kwargs) -> 'BasePage':
|
|
"""输入文本"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
self.element.type_text(resolved_selector, text, **kwargs)
|
|
return self
|
|
|
|
def _get_text(self, selector: str, **kwargs) -> str:
|
|
"""获取元素文本"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
return self.element.get_element_text(resolved_selector, **kwargs)
|
|
|
|
def _get_attr(self, selector: str, attribute: str, **kwargs) -> Optional[str]:
|
|
"""获取元素属性"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
return self.element.get_element_attribute(resolved_selector, attribute, **kwargs)
|
|
|
|
def _is_visible(self, selector: str, **kwargs) -> bool:
|
|
"""检查元素是否可见"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
return self.element.is_element_visible(resolved_selector, **kwargs)
|
|
|
|
def _is_enabled(self, selector: str, **kwargs) -> bool:
|
|
"""检查元素是否可用"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
return self.element.is_element_enabled(resolved_selector, **kwargs)
|
|
|
|
# 断言方法
|
|
def assert_title_contains(self, expected: str, message: Optional[str] = None) -> 'BasePage':
|
|
"""断言标题包含预期文本"""
|
|
self.assertion.assert_page_title_contains(expected, message)
|
|
return self
|
|
|
|
def assert_url_contains(self, expected: str, message: Optional[str] = None) -> 'BasePage':
|
|
"""断言URL包含预期文本"""
|
|
self.assertion.assert_url_contains(expected, message)
|
|
return self
|
|
|
|
def assert_url_equals(self, expected: str, message: Optional[str] = None) -> 'BasePage':
|
|
"""断言URL等于预期URL"""
|
|
self.assertion.assert_url_equals(expected, message)
|
|
return self
|
|
|
|
def assert_element_visible(self, selector: str, **kwargs) -> 'BasePage':
|
|
"""断言元素可见"""
|
|
resolved_selector = self._resolve_selector(selector)
|
|
self.assertion.assert_element_visible(resolved_selector, **kwargs)
|
|
return self
|
|
|
|
def assert_element_hidden(self, selector: str, **kwargs) -> 'BasePage':
|
|
"""断言元素隐藏"""
|
|
self.assertion.assert_element_hidden(selector, **kwargs)
|
|
return self
|
|
|
|
def assert_element_text_contains(
|
|
self,
|
|
selector: str,
|
|
expected: str,
|
|
**kwargs
|
|
) -> 'BasePage':
|
|
"""断言元素文本包含预期文本"""
|
|
self.assertion.assert_element_text_contains(selector, expected, **kwargs)
|
|
return self
|
|
|
|
def assert_element_text_equals(
|
|
self,
|
|
selector: str,
|
|
expected: str,
|
|
**kwargs
|
|
) -> 'BasePage':
|
|
"""断言元素文本等于预期文本"""
|
|
self.assertion.assert_element_text_equals(selector, expected, **kwargs)
|
|
return self
|
|
|
|
def assert_element_count(
|
|
self,
|
|
selector: str,
|
|
expected: int,
|
|
message: Optional[str] = None
|
|
) -> 'BasePage':
|
|
"""断言元素数量"""
|
|
self.assertion.assert_element_count(selector, expected, message)
|
|
return self
|
|
|
|
def assert_element_attribute_equals(
|
|
self,
|
|
selector: str,
|
|
attribute: str,
|
|
expected: str,
|
|
**kwargs
|
|
) -> 'BasePage':
|
|
"""断言元素属性等于预期值"""
|
|
self.assertion.assert_element_attribute_equals(
|
|
selector, attribute, expected, **kwargs
|
|
)
|
|
return self
|
|
|
|
def should_have_url(self, url: str, **kwargs) -> 'BasePage':
|
|
"""检查URL"""
|
|
self.assert_url_equals(url, **kwargs)
|
|
return self
|
|
|
|
def should_have_title(self, title: str, **kwargs) -> 'BasePage':
|
|
"""检查标题"""
|
|
self.assert_title_contains(title, **kwargs)
|
|
return self
|
|
|
|
def should_contain_text(self, text: str, **kwargs) -> 'BasePage':
|
|
"""检查页面包含文本"""
|
|
self.page.wait_for_load_state("domcontentloaded")
|
|
content = self.page_helper.get_page_source()
|
|
assert text in content, f"页面不包含文本: {text}"
|
|
return self
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<{self.__class__.__name__}: {self.path or 'unknown'}>"
|
|
|
|
|
|
class PageRegistry:
|
|
"""页面注册表,用于管理页面对象"""
|
|
|
|
_instance: Optional['PageRegistry'] = None
|
|
_pages: Dict[str, BasePage] = {}
|
|
|
|
def __new__(cls) -> 'PageRegistry':
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
return cls._instance
|
|
|
|
def register(self, name: str, page: BasePage) -> None:
|
|
"""注册页面"""
|
|
self._pages[name] = page
|
|
|
|
def get(self, name: str) -> Optional[BasePage]:
|
|
"""获取页面"""
|
|
return self._pages.get(name)
|
|
|
|
def clear(self) -> None:
|
|
"""清空注册表"""
|
|
self._pages.clear()
|
|
|
|
|
|
def get_page_registry() -> PageRegistry:
|
|
"""获取页面注册表"""
|
|
return PageRegistry()
|