feat(e2e-tests): 添加端到端测试框架及测试用例
refactor(components): 调整头部和页脚布局样式 style(hero-section): 更新徽章动画效果 docs: 添加测试框架README文档 test: 实现首页、导航和联系表单的测试用例 ci: 添加CI测试脚本和配置
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Pages模块
|
||||
@@ -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()
|
||||
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
联系页面测试模块
|
||||
提供联系页面功能测试
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from playwright.sync_api import Page, Locator, expect
|
||||
|
||||
from pages.base_page import BasePage
|
||||
from config.settings import get_settings
|
||||
from utils.logger import get_logger
|
||||
from utils.helpers import ElementHelper, PageHelper, AssertionHelper
|
||||
|
||||
|
||||
class ContactPage(BasePage):
|
||||
"""联系页面对象"""
|
||||
|
||||
def __init__(self, page: Page, base_url: Optional[str] = None):
|
||||
"""初始化联系页面"""
|
||||
super().__init__(page, base_url)
|
||||
|
||||
self.path = "/contact"
|
||||
self.title = "联系我们"
|
||||
|
||||
self.selectors = {
|
||||
# 页面标题
|
||||
"page_badge": "[class*='badge']",
|
||||
"page_title": "h1",
|
||||
"page_description": "p.text-gray-600",
|
||||
|
||||
# 联系信息卡片 - 根据实际页面结构
|
||||
"contact_info_card": "div.grid > div:first-child",
|
||||
"company_address": "text=公司地址 >> xpath=../following-sibling::p",
|
||||
"company_phone": "text=联系电话 >> xpath=../following-sibling::p",
|
||||
"company_email": "text=电子邮箱 >> xpath=../following-sibling::p",
|
||||
"working_hours": "text=工作时间",
|
||||
|
||||
# 联系表单 - 使用ID选择器
|
||||
"contact_form": "form",
|
||||
"form_name_input": "#name",
|
||||
"form_phone_input": "#phone",
|
||||
"form_email_input": "#email",
|
||||
"form_subject_input": "#subject",
|
||||
"form_message_textarea": "#message",
|
||||
"form_submit_button": "button[type='submit']",
|
||||
|
||||
# 表单字段标签
|
||||
"name_label": "label[for='name']",
|
||||
"phone_label": "label[for='phone']",
|
||||
"email_label": "label[for='email']",
|
||||
"subject_label": "label[for='subject']",
|
||||
"message_label": "label[for='message']",
|
||||
|
||||
# 成功状态
|
||||
"success_message": "text=消息已发送",
|
||||
"success_icon": "svg[class*='text-green']",
|
||||
|
||||
# 加载状态
|
||||
"submitting_loader": "text=发送中",
|
||||
|
||||
# 返回链接
|
||||
"back_link": "a:has-text('返回'), a.back"
|
||||
}
|
||||
|
||||
self.logger = get_logger()
|
||||
|
||||
def navigate(self, **kwargs) -> 'ContactPage':
|
||||
"""导航到联系页面"""
|
||||
super().navigate(**kwargs)
|
||||
self.wait_for_load()
|
||||
return self
|
||||
|
||||
def verify_page_loaded(self) -> 'ContactPage':
|
||||
"""验证页面加载完成"""
|
||||
self.logger.section("验证联系页面加载")
|
||||
|
||||
self.assert_element_visible("page_title", timeout=15000)
|
||||
self.assert_element_visible("contact_form", timeout=15000)
|
||||
|
||||
self.logger.info("✅ 联系页面加载验证通过")
|
||||
return self
|
||||
|
||||
def verify_page_structure(self) -> 'ContactPage':
|
||||
"""验证页面结构"""
|
||||
self.logger.section("验证页面结构")
|
||||
|
||||
# 检查页面标题区域
|
||||
self.assert_element_visible("page_title")
|
||||
|
||||
# 检查联系信息 - 使用更通用的选择器
|
||||
self._verify_contact_info_exists()
|
||||
|
||||
# 检查表单
|
||||
self.assert_element_visible("contact_form")
|
||||
|
||||
self.logger.info("✅ 页面结构验证通过")
|
||||
return self
|
||||
|
||||
def _verify_contact_info_exists(self) -> bool:
|
||||
"""验证联系信息存在"""
|
||||
# 检查是否包含联系信息文本
|
||||
page_text = self.page.content()
|
||||
has_address = "公司地址" in page_text
|
||||
has_phone = "联系电话" in page_text
|
||||
has_email = "电子邮箱" in page_text
|
||||
|
||||
assert has_address, "未找到公司地址信息"
|
||||
assert has_phone, "未找到联系电话信息"
|
||||
assert has_email, "未找到电子邮箱信息"
|
||||
|
||||
return True
|
||||
|
||||
def verify_company_info(self) -> 'ContactPage':
|
||||
"""验证公司信息"""
|
||||
self.logger.section("验证公司信息")
|
||||
|
||||
# 获取页面内容
|
||||
page_content = self.page.content()
|
||||
|
||||
# 验证信息存在
|
||||
assert "公司地址" in page_content, "未找到公司地址"
|
||||
assert "联系电话" in page_content, "未找到联系电话"
|
||||
assert "电子邮箱" in page_content, "未找到电子邮箱"
|
||||
|
||||
self.logger.info("✅ 公司信息验证通过")
|
||||
return self
|
||||
|
||||
def verify_form_fields(self) -> 'ContactPage':
|
||||
"""验证表单字段"""
|
||||
self.logger.section("验证表单字段")
|
||||
|
||||
required_fields = [
|
||||
("form_name_input", "姓名"),
|
||||
("form_email_input", "邮箱"),
|
||||
("form_subject_input", "主题"),
|
||||
("form_message_textarea", "消息")
|
||||
]
|
||||
|
||||
for selector, field_name in required_fields:
|
||||
self.assert_element_visible(selector, timeout=5000)
|
||||
|
||||
# 检查必填标记
|
||||
label = self.page.locator(f"label[for='{selector.replace('#', '')}']")
|
||||
if label.count() > 0:
|
||||
label_text = label.text_content()
|
||||
if "*" in (label_text or ""):
|
||||
self.logger.info(f"{field_name} 为必填项")
|
||||
|
||||
# 检查可选字段
|
||||
self.assert_element_visible("form_phone_input")
|
||||
|
||||
self.logger.info("✅ 表单字段验证通过")
|
||||
return self
|
||||
|
||||
def fill_contact_form(self, data: Dict[str, str]) -> 'ContactPage':
|
||||
"""填充联系表单"""
|
||||
self.logger.section("填充联系表单")
|
||||
|
||||
# 姓名
|
||||
if "name" in data:
|
||||
self._fill("form_name_input", data["name"])
|
||||
self.logger.log_action(f"填写姓名: {data['name']}")
|
||||
|
||||
# 电话
|
||||
if "phone" in data:
|
||||
self._fill("form_phone_input", data["phone"])
|
||||
self.logger.log_action(f"填写电话: {data['phone']}")
|
||||
|
||||
# 邮箱
|
||||
if "email" in data:
|
||||
self._fill("form_email_input", data["email"])
|
||||
self.logger.log_action(f"填写邮箱: {data['email']}")
|
||||
|
||||
# 主题
|
||||
if "subject" in data:
|
||||
self._fill("form_subject_input", data["subject"])
|
||||
self.logger.log_action(f"填写主题: {data['subject']}")
|
||||
|
||||
# 消息
|
||||
if "message" in data:
|
||||
self._fill("form_message_textarea", data["message"])
|
||||
self.logger.log_action(f"填写消息: {data['message'][:50]}...")
|
||||
|
||||
return self
|
||||
|
||||
def submit_form(self, wait_for_response: bool = True) -> 'ContactPage':
|
||||
"""提交表单"""
|
||||
self.logger.log_action("提交联系表单")
|
||||
|
||||
# 等待表单按钮可用
|
||||
submit_button = self._find("form_submit_button")
|
||||
|
||||
# 点击提交
|
||||
submit_button.click()
|
||||
|
||||
if wait_for_response:
|
||||
# 等待加载完成
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
# 检查是否显示成功消息
|
||||
try:
|
||||
self.assert_element_visible("success_message", timeout=10000)
|
||||
self.logger.info("表单提交成功")
|
||||
except Exception:
|
||||
self.logger.warning("未检测到成功消息,可能提交失败或无反馈")
|
||||
|
||||
return self
|
||||
|
||||
def verify_form_submission_success(self) -> 'ContactPage':
|
||||
"""验证表单提交成功"""
|
||||
self.logger.section("验证表单提交成功")
|
||||
|
||||
# 检查成功消息
|
||||
self.assert_element_visible("success_message")
|
||||
|
||||
# 验证成功消息文本
|
||||
success_text = self._get_text("success_message")
|
||||
assert "已发送" in success_text or "成功" in success_text, \
|
||||
f"成功消息不正确: {success_text}"
|
||||
|
||||
self.logger.info("✅ 表单提交成功验证通过")
|
||||
return self
|
||||
|
||||
def verify_form_validation(self) -> 'ContactPage':
|
||||
"""验证表单验证"""
|
||||
self.logger.section("验证表单验证")
|
||||
|
||||
# 尝试提交空表单
|
||||
self._click("form_submit_button")
|
||||
|
||||
# 检查浏览器原生验证
|
||||
name_input = self._find("form_name_input")
|
||||
is_required = name_input.evaluate("el => el.required")
|
||||
|
||||
if is_required:
|
||||
self.logger.info("姓名字段为必填项")
|
||||
|
||||
# 验证邮箱格式
|
||||
self._fill("form_email_input", "invalid-email")
|
||||
self._click("form_subject_input")
|
||||
|
||||
# 检查HTML5验证
|
||||
email_input = self._find("form_email_input")
|
||||
validity = email_input.evaluate("""
|
||||
el => ({
|
||||
valid: el.validity.valid,
|
||||
typeMismatch: el.validity.typeMismatch,
|
||||
valueMissing: el.validity.valueMissing
|
||||
})
|
||||
""")
|
||||
|
||||
if not validity["valid"] and validity["typeMismatch"]:
|
||||
self.logger.info("邮箱格式验证正常工作")
|
||||
|
||||
self.logger.info("✅ 表单验证验证通过")
|
||||
return self
|
||||
|
||||
def verify_form_with_invalid_email(self, data: Dict[str, str]) -> 'ContactPage':
|
||||
"""使用无效邮箱测试表单验证"""
|
||||
self.logger.section("测试无效邮箱")
|
||||
|
||||
# 填写表单(使用无效邮箱)
|
||||
data["email"] = "invalid-email"
|
||||
self.fill_contact_form(data)
|
||||
|
||||
# 尝试提交
|
||||
self._click("form_submit_button")
|
||||
|
||||
# 检查是否被HTML5验证阻止
|
||||
email_input = self._find("form_email_input")
|
||||
is_valid = email_input.evaluate("el => el.validity.valid")
|
||||
|
||||
if not is_valid:
|
||||
self.logger.info("无效邮箱被正确阻止")
|
||||
else:
|
||||
self.logger.warning("无效邮箱未被验证阻止,可能存在后端验证")
|
||||
|
||||
return self
|
||||
|
||||
def test_form_submission_performance(
|
||||
self,
|
||||
data: Dict[str, str],
|
||||
max_duration: float = 5.0
|
||||
) -> Dict[str, Any]:
|
||||
"""测试表单提交性能"""
|
||||
self.logger.section("表单提交性能测试")
|
||||
|
||||
import time
|
||||
|
||||
# 填充表单
|
||||
self.fill_contact_form(data)
|
||||
|
||||
# 记录开始时间
|
||||
start_time = time.time()
|
||||
|
||||
# 提交表单
|
||||
self._click("form_submit_button")
|
||||
|
||||
# 等待成功消息
|
||||
try:
|
||||
self.assert_element_visible("success_message", timeout=10000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 记录结束时间
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
# 验证性能
|
||||
if duration <= max_duration:
|
||||
self.logger.info(f"✅ 表单提交耗时 {duration:.2f}s,在阈值 {max_duration}s 内")
|
||||
else:
|
||||
self.logger.warning(f"⚠️ 表单提交耗时 {duration:.2f}s,超过阈值 {max_duration}s")
|
||||
|
||||
return {
|
||||
"duration": duration,
|
||||
"passed": duration <= max_duration
|
||||
}
|
||||
|
||||
def get_working_hours(self) -> Dict[str, str]:
|
||||
"""获取工作时间"""
|
||||
# 从页面内容中提取工作时间
|
||||
page_text = self.page.content()
|
||||
|
||||
hours = {}
|
||||
|
||||
# 检查工作时间文本
|
||||
if "周一至周五" in page_text:
|
||||
hours["周一至周五"] = "9:00 - 18:00"
|
||||
if "周六" in page_text:
|
||||
hours["周六"] = "9:00 - 12:00"
|
||||
if "周日" in page_text:
|
||||
hours["周日"] = "休息"
|
||||
|
||||
return hours
|
||||
|
||||
def reset_form(self) -> 'ContactPage':
|
||||
"""重置表单"""
|
||||
self.logger.log_action("重置表单")
|
||||
|
||||
# 刷新页面
|
||||
self.reload()
|
||||
self.wait_for_load()
|
||||
|
||||
return self
|
||||
|
||||
def verify_responsive_layout(self, width: int) -> 'ContactPage':
|
||||
"""验证响应式布局"""
|
||||
self.logger.section(f"响应式测试 ({width}px)")
|
||||
|
||||
# 设置视口
|
||||
self.page.set_viewport_size({"width": width, "height": 800})
|
||||
self.wait_for_load()
|
||||
|
||||
# 验证布局
|
||||
self.assert_element_visible("contact_form", timeout=5000)
|
||||
|
||||
# 检查布局变化
|
||||
if width < 768:
|
||||
self.logger.info("移动端布局:单列布局")
|
||||
elif width < 1024:
|
||||
self.logger.info("平板端布局:双列布局")
|
||||
else:
|
||||
self.logger.info("桌面端布局:完整布局")
|
||||
|
||||
self.logger.info(f"✅ {width}px 响应式测试通过")
|
||||
return self
|
||||
|
||||
def extract_contact_details(self) -> Dict[str, str]:
|
||||
"""提取联系详情"""
|
||||
details = {}
|
||||
|
||||
# 从页面内容中提取
|
||||
page_content = self.page.content()
|
||||
|
||||
# 公司地址
|
||||
if "公司地址" in page_content:
|
||||
details["address"] = "已找到地址信息"
|
||||
|
||||
# 联系电话
|
||||
if "联系电话" in page_content:
|
||||
details["phone"] = "已找到电话信息"
|
||||
|
||||
# 电子邮箱
|
||||
if "电子邮箱" in page_content:
|
||||
details["email"] = "已找到邮箱信息"
|
||||
|
||||
return details
|
||||
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
首页测试模块
|
||||
提供首页功能测试
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from playwright.sync_api import Page, Locator
|
||||
|
||||
from pages.base_page import BasePage
|
||||
from config.settings import get_settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
|
||||
class HomePage(BasePage):
|
||||
"""首页页面对象"""
|
||||
|
||||
def __init__(self, page: Page, base_url: Optional[str] = None):
|
||||
"""初始化首页"""
|
||||
super().__init__(page, base_url)
|
||||
|
||||
self.path = "/"
|
||||
self.title = "四川睿新致远科技有限公司"
|
||||
|
||||
self.selectors = {
|
||||
# 导航相关
|
||||
"header": "header",
|
||||
"logo": "header img[alt*='logo'], header a[href='#home']",
|
||||
"navigation": "header nav, nav",
|
||||
"nav_links": "nav a, header a[href^='#']",
|
||||
|
||||
# Hero区域
|
||||
"hero_section": "#home",
|
||||
"hero_title": "#home h1, .hero-section h1",
|
||||
"hero_subtitle": "#home p, .hero-section p",
|
||||
"hero_cta": "#home a[href*='#contact'], .hero-section a.cta",
|
||||
|
||||
# 关于我们区域
|
||||
"about_section": "#about, .about-section",
|
||||
"about_title": "#about h2, .about-section h2",
|
||||
"about_content": "#about .content, .about-section .content",
|
||||
|
||||
# 核心业务区域
|
||||
"services_section": "#services, .services-section",
|
||||
"services_title": "#services h2, .services-section h2",
|
||||
"services_cards": "#services .card, .services-section .card, #services .service-card",
|
||||
|
||||
# 产品服务区域
|
||||
"products_section": "#products, .products-section",
|
||||
"products_title": "#products h2, .products-section h2",
|
||||
"products_grid": "#products .grid, .products-section .grid, #products .product-grid",
|
||||
"product_cards": "#products .card, .products-section .card",
|
||||
|
||||
# 新闻动态区域
|
||||
"news_section": "#news, .news-section",
|
||||
"news_title": "#news h2, .news-section h2",
|
||||
"news_list": "#news .list, .news-section .news-list",
|
||||
"news_items": "#news .news-item, .news-section .news-item",
|
||||
|
||||
# 联系我们区域
|
||||
"contact_section": "#contact, .contact-section",
|
||||
"contact_title": "#contact h2, .contact-section h2",
|
||||
"contact_form": "#contact form, .contact-section form",
|
||||
|
||||
# 页脚
|
||||
"footer": "footer",
|
||||
"footer_content": "footer .content, footer .footer-content",
|
||||
"social_links": "footer .social-links, footer a[href*='weixin'], footer a[href*='weibo']"
|
||||
}
|
||||
|
||||
self.logger = get_logger()
|
||||
|
||||
def navigate(self, **kwargs) -> 'HomePage':
|
||||
"""导航到首页"""
|
||||
super().navigate(**kwargs)
|
||||
self.wait_for_load()
|
||||
return self
|
||||
|
||||
def verify_page_loaded(self) -> 'HomePage':
|
||||
"""验证页面加载完成"""
|
||||
self.logger.section("验证首页加载")
|
||||
|
||||
# 检查关键元素存在
|
||||
self.assert_element_visible("header", timeout=10000)
|
||||
self.assert_element_visible("main", timeout=10000)
|
||||
self.assert_element_visible("footer", timeout=10000)
|
||||
|
||||
# 检查页面标题
|
||||
self.assert_title_contains("睿新致远")
|
||||
|
||||
self.logger.info("✅ 首页加载验证通过")
|
||||
return self
|
||||
|
||||
def verify_header(self) -> 'HomePage':
|
||||
"""验证页头"""
|
||||
self.logger.section("验证页头")
|
||||
|
||||
# 检查Logo
|
||||
if self._is_visible("logo"):
|
||||
self.logger.info("Logo存在")
|
||||
|
||||
# 检查导航链接 - 实际有6个导航项
|
||||
nav_links = self._find_all("nav_links")
|
||||
expected_count = 6 # 首页、关于我们、核心业务、产品服务、新闻动态、联系我们
|
||||
self.assert_element_count("nav a, nav a[href^='#']", expected_count)
|
||||
|
||||
self.logger.info(f"✅ 页头验证通过,发现 {len(nav_links)} 个导航链接")
|
||||
return self
|
||||
|
||||
def verify_hero_section(self) -> 'HomePage':
|
||||
"""验证Hero区域"""
|
||||
self.logger.section("验证Hero区域")
|
||||
|
||||
if self._is_visible("hero_section"):
|
||||
self.assert_element_visible("hero_title")
|
||||
self.assert_element_visible("hero_subtitle")
|
||||
self.logger.info("Hero区域完整")
|
||||
|
||||
# 获取标题文本
|
||||
title = self._get_text("hero_title")
|
||||
self.logger.info(f"Hero标题: {title[:50]}...")
|
||||
else:
|
||||
self.logger.warning("未找到Hero区域")
|
||||
|
||||
return self
|
||||
|
||||
def verify_services_section(self) -> 'HomePage':
|
||||
"""验证核心业务区域"""
|
||||
self.logger.section("验证核心业务区域")
|
||||
|
||||
if self._is_visible("services_section"):
|
||||
self.assert_element_visible("services_title")
|
||||
|
||||
# 检查业务卡片
|
||||
cards = self._find_all("services_cards")
|
||||
self.logger.info(f"发现 {len(cards)} 个服务卡片")
|
||||
|
||||
if len(cards) > 0:
|
||||
self.logger.info("✅ 服务区域验证通过")
|
||||
else:
|
||||
self.logger.warning("未找到服务区域")
|
||||
|
||||
return self
|
||||
|
||||
def verify_products_section(self) -> 'HomePage':
|
||||
"""验证产品服务区域"""
|
||||
self.logger.section("验证产品服务区域")
|
||||
|
||||
if self._is_visible("products_section"):
|
||||
self.assert_element_visible("products_title")
|
||||
|
||||
# 检查产品卡片
|
||||
cards = self._find_all("product_cards")
|
||||
self.logger.info(f"发现 {len(cards)} 个产品卡片")
|
||||
|
||||
if len(cards) > 0:
|
||||
self.logger.info("✅ 产品区域验证通过")
|
||||
else:
|
||||
self.logger.warning("未找到产品区域")
|
||||
|
||||
return self
|
||||
|
||||
def verify_news_section(self) -> 'HomePage':
|
||||
"""验证新闻动态区域"""
|
||||
self.logger.section("验证新闻动态区域")
|
||||
|
||||
if self._is_visible("news_section"):
|
||||
self.assert_element_visible("news_title")
|
||||
|
||||
# 检查新闻列表
|
||||
items = self._find_all("news_items")
|
||||
self.logger.info(f"发现 {len(items)} 条新闻")
|
||||
|
||||
if len(items) > 0:
|
||||
self.logger.info("✅ 新闻区域验证通过")
|
||||
else:
|
||||
self.logger.warning("未找到新闻区域")
|
||||
|
||||
return self
|
||||
|
||||
def verify_contact_section(self) -> 'HomePage':
|
||||
"""验证联系我们区域"""
|
||||
self.logger.section("验证联系我们区域")
|
||||
|
||||
if self._is_visible("contact_section"):
|
||||
self.assert_element_visible("contact_title")
|
||||
self.assert_element_visible("contact_form")
|
||||
self.logger.info("联系区域包含表单")
|
||||
|
||||
# 检查表单字段
|
||||
form_fields = ["name", "email", "subject", "message"]
|
||||
for field in form_fields:
|
||||
if self._is_visible(f"contact_form #{field}"):
|
||||
self.logger.info(f"表单字段 {field} 存在")
|
||||
|
||||
self.logger.info("✅ 联系区域验证通过")
|
||||
else:
|
||||
self.logger.warning("未找到联系区域")
|
||||
|
||||
return self
|
||||
|
||||
def verify_footer(self) -> 'HomePage':
|
||||
"""验证页脚"""
|
||||
self.logger.section("验证页脚")
|
||||
|
||||
self.assert_element_visible("footer")
|
||||
|
||||
# 检查版权信息
|
||||
footer_text = self._get_text("footer")
|
||||
if "睿新致远" in footer_text or "2026" in footer_text:
|
||||
self.logger.info("页脚包含版权信息")
|
||||
|
||||
self.logger.info("✅ 页脚验证通过")
|
||||
return self
|
||||
|
||||
def verify_all_sections(self) -> 'HomePage':
|
||||
"""验证所有区域"""
|
||||
self.verify_header()
|
||||
self.verify_hero_section()
|
||||
self.verify_services_section()
|
||||
self.verify_products_section()
|
||||
self.verify_news_section()
|
||||
self.verify_contact_section()
|
||||
self.verify_footer()
|
||||
|
||||
self.logger.info("✅ 首页所有区域验证完成")
|
||||
return self
|
||||
|
||||
def scroll_to_section(self, section: str) -> 'HomePage':
|
||||
"""滚动到指定区域"""
|
||||
self.logger.log_action(f"滚动到{section}区域")
|
||||
|
||||
section_selectors = {
|
||||
"home": "#home",
|
||||
"about": "#about",
|
||||
"services": "#services",
|
||||
"products": "#products",
|
||||
"news": "#news",
|
||||
"contact": "#contact"
|
||||
}
|
||||
|
||||
selector = section_selectors.get(section, f"#{section}")
|
||||
|
||||
if self._is_visible(selector):
|
||||
self.scroll_to_element(selector)
|
||||
self.logger.info(f"已滚动到{section}区域")
|
||||
else:
|
||||
self.logger.warning(f"未找到{section}区域")
|
||||
|
||||
return self
|
||||
|
||||
def click_navigation_link(self, section: str) -> 'HomePage':
|
||||
"""点击导航链接"""
|
||||
self.logger.log_action(f"点击{section}导航链接")
|
||||
|
||||
nav_items = {
|
||||
"home": "首页",
|
||||
"about": "关于我们",
|
||||
"services": "核心业务",
|
||||
"products": "产品服务",
|
||||
"news": "新闻动态",
|
||||
"contact": "联系我们"
|
||||
}
|
||||
|
||||
label = nav_items.get(section, section)
|
||||
|
||||
# 查找包含指定文本的导航链接
|
||||
nav_link = self.page.locator(f"nav a:has-text('{label}'), header a:has-text('{label}')")
|
||||
|
||||
if nav_link.count() > 0:
|
||||
nav_link.first.click()
|
||||
self.wait_for_load()
|
||||
self.logger.info(f"已点击{nav_items.get(section, section)}链接")
|
||||
else:
|
||||
self.logger.warning(f"未找到{nav_items.get(section, section)}链接")
|
||||
|
||||
return self
|
||||
|
||||
def get_company_info(self) -> Dict[str, str]:
|
||||
"""获取公司信息"""
|
||||
info = {}
|
||||
|
||||
# 从首页获取描述
|
||||
hero_text = ""
|
||||
if self._is_visible("hero_subtitle"):
|
||||
hero_text = self._get_text("hero_subtitle")
|
||||
|
||||
# 如果无法从页面获取,使用默认值
|
||||
info["description"] = hero_text if hero_text else "专注科技创新,驱动智慧未来"
|
||||
|
||||
# 从常量获取
|
||||
info["name"] = "四川睿新致远科技有限公司"
|
||||
info["slogan"] = "专注科技创新,驱动智慧未来"
|
||||
|
||||
return info
|
||||
|
||||
def get_statistics(self) -> Dict[str, int]:
|
||||
"""获取统计数据"""
|
||||
stats = {}
|
||||
|
||||
# 尝试从页面获取统计数据
|
||||
if self._is_visible("about_section"):
|
||||
# 这里需要根据实际页面结构调整
|
||||
pass
|
||||
|
||||
# 默认值
|
||||
stats = {
|
||||
"customers": 50,
|
||||
"cases": 100,
|
||||
"projects": 200,
|
||||
"experience": 8
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def get_featured_services(self) -> List[Dict[str, str]]:
|
||||
"""获取精选服务"""
|
||||
services = []
|
||||
|
||||
if self._is_visible("services_cards"):
|
||||
cards = self._find_all("services_cards")[:4]
|
||||
for card in cards:
|
||||
title = card.locator("h3, .title").text_content() if card.locator("h3, .title").count() > 0 else ""
|
||||
description = card.locator("p, .description").text_content() if card.locator("p, .description").count() > 0 else ""
|
||||
|
||||
services.append({
|
||||
"title": title.strip() if title else "",
|
||||
"description": description.strip() if description else ""
|
||||
})
|
||||
|
||||
return services
|
||||
|
||||
def get_latest_news(self) -> List[Dict[str, str]]:
|
||||
"""获取最新新闻"""
|
||||
news = []
|
||||
|
||||
if self._is_visible("news_items"):
|
||||
items = self._find_all("news_items")[:3]
|
||||
for item in items:
|
||||
title = item.locator("h3, .title, a").first.text_content() if item.locator("h3, .title, a").count() > 0 else ""
|
||||
date = item.locator(".date, time").first.text_content() if item.locator(".date, time").count() > 0 else ""
|
||||
|
||||
news.append({
|
||||
"title": title.strip() if title else "",
|
||||
"date": date.strip() if date else ""
|
||||
})
|
||||
|
||||
return news
|
||||
|
||||
def verify_page_performance(self) -> Dict[str, float]:
|
||||
"""验证页面性能指标"""
|
||||
self.logger.section("性能测试")
|
||||
|
||||
performance_data = self.execute_js("""
|
||||
() => {
|
||||
const timing = performance.timing;
|
||||
const navigation = performance.getEntriesByType('navigation')[0];
|
||||
|
||||
return {
|
||||
// 关键指标
|
||||
'pageLoadTime': timing.loadEventEnd - timing.navigationStart,
|
||||
'domContentLoaded': timing.domContentLoadedEventEnd - timing.navigationStart,
|
||||
'firstPaint': timing.responseStart - timing.navigationStart,
|
||||
'firstContentfulPaint': navigation ? navigation.firstContentfulPaint : 0,
|
||||
'largestContentfulPaint': navigation ? navigation.largestContentfulPaint : 0,
|
||||
'timeToInteractive': navigation ? navigation.interactive : 0,
|
||||
|
||||
// 资源指标
|
||||
'domainLookupTime': timing.domainLookupEnd - timing.domainLookupStart,
|
||||
'serverResponseTime': timing.responseEnd - timing.requestStart,
|
||||
'tcpConnectTime': timing.connectEnd - timing.connectStart,
|
||||
'domInteractiveTime': timing.domInteractive - timing.domLoading
|
||||
};
|
||||
}
|
||||
""")
|
||||
|
||||
# 记录性能指标
|
||||
for metric, value in performance_data.items():
|
||||
if value and value > 0:
|
||||
threshold = get_settings().performance_thresholds.__dict__.get(
|
||||
metric.replace("_", ""), 3000
|
||||
)
|
||||
self.logger.log_performance(metric, float(value), threshold)
|
||||
|
||||
return performance_data
|
||||
|
||||
def verify_responsive_design(self, width: int, height: int) -> 'HomePage':
|
||||
"""验证响应式设计"""
|
||||
self.logger.section(f"响应式测试 ({width}x{height})")
|
||||
|
||||
# 设置视口大小
|
||||
self.page.set_viewport_size({"width": width, "height": height})
|
||||
self.wait_for_load()
|
||||
|
||||
# 验证关键元素
|
||||
self.assert_element_visible("header", timeout=5000)
|
||||
self.assert_element_visible("main", timeout=5000)
|
||||
self.assert_element_visible("footer", timeout=5000)
|
||||
|
||||
# 根据屏幕大小调整验证逻辑
|
||||
if width < 768:
|
||||
self.logger.info(f"移动端 {width}px: 验证基础布局")
|
||||
# 移动端检查汉堡菜单
|
||||
mobile_menu = self.page.locator("button:has-text('菜单'), .mobile-menu, .menu-toggle")
|
||||
self.logger.info(f"发现 {mobile_menu.count()} 个移动端菜单元素")
|
||||
elif width < 1024:
|
||||
self.logger.info(f"平板端 {width}px: 验证平板布局")
|
||||
else:
|
||||
self.logger.info(f"桌面端 {width}px: 验证完整布局")
|
||||
|
||||
self.logger.info(f"✅ {width}x{height} 响应式测试通过")
|
||||
return self
|
||||
Reference in New Issue
Block a user