feat(e2e-tests): 添加端到端测试框架及测试用例

refactor(components): 调整头部和页脚布局样式
style(hero-section): 更新徽章动画效果

docs: 添加测试框架README文档
test: 实现首页、导航和联系表单的测试用例
ci: 添加CI测试脚本和配置
This commit is contained in:
张翔
2026-02-02 19:36:33 +08:00
parent 150024b6ac
commit f14002559e
30 changed files with 6377 additions and 17 deletions
+318
View File
@@ -0,0 +1,318 @@
"""
页面对象基类
提供页面对象模式的基础框架
"""
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()