feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
from .data_generator import DataGenerator
|
||||
from .exception_handler import (
|
||||
retry_on_failure,
|
||||
handle_test_failure,
|
||||
)
|
||||
from .screenshot_helper import ScreenshotHelper
|
||||
from .form_helper import FormHelper
|
||||
from .table_helper import TableHelper
|
||||
from .report_helper import ReportHelper
|
||||
|
||||
__all__ = [
|
||||
"DataGenerator",
|
||||
"retry_on_failure",
|
||||
"handle_test_failure",
|
||||
"ScreenshotHelper",
|
||||
"FormHelper",
|
||||
"TableHelper",
|
||||
"ReportHelper",
|
||||
]
|
||||
@@ -0,0 +1,115 @@
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
class DataGenerator:
|
||||
"""数据生成器类"""
|
||||
|
||||
@staticmethod
|
||||
def random_username(length: int = 8) -> str:
|
||||
"""生成随机用户名"""
|
||||
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||||
|
||||
@staticmethod
|
||||
def random_password(length: int = 12) -> str:
|
||||
"""生成随机密码"""
|
||||
chars = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return "".join(random.choices(chars, k=length))
|
||||
|
||||
@staticmethod
|
||||
def random_email() -> str:
|
||||
"""生成随机邮箱"""
|
||||
username = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
||||
domains = ["gmail.com", "qq.com", "163.com", "outlook.com"]
|
||||
domain = random.choice(domains)
|
||||
return f"{username}@{domain}"
|
||||
|
||||
@staticmethod
|
||||
def random_phone() -> str:
|
||||
"""生成随机手机号"""
|
||||
prefix = random.choice(["138", "139", "150", "151", "186", "188"])
|
||||
suffix = "".join(random.choices(string.digits, k=8))
|
||||
return f"{prefix}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def random_date(start_date: datetime = None, end_date: datetime = None) -> datetime:
|
||||
"""生成随机日期"""
|
||||
if start_date is None:
|
||||
start_date = datetime.now() - timedelta(days=365)
|
||||
if end_date is None:
|
||||
end_date = datetime.now()
|
||||
|
||||
time_between = end_date - start_date
|
||||
days_between = time_between.days
|
||||
random_days = random.randrange(days_between)
|
||||
return start_date + timedelta(days=random_days)
|
||||
|
||||
@staticmethod
|
||||
def generate_user_data(overrides: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""生成用户数据
|
||||
|
||||
Args:
|
||||
overrides: 覆盖的字段
|
||||
|
||||
Returns:
|
||||
用户数据字典
|
||||
"""
|
||||
data = {
|
||||
"username": DataGenerator.random_username(),
|
||||
"password": DataGenerator.random_password(),
|
||||
"email": DataGenerator.random_email(),
|
||||
"phone": DataGenerator.random_phone(),
|
||||
"status": "active",
|
||||
}
|
||||
|
||||
if overrides:
|
||||
data.update(overrides)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def generate_role_data(overrides: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""生成角色数据
|
||||
|
||||
Args:
|
||||
overrides: 覆盖的字段
|
||||
|
||||
Returns:
|
||||
角色数据字典
|
||||
"""
|
||||
data = {
|
||||
"role_name": f"test_role_{random.randint(1000, 9999)}",
|
||||
"role_code": f"test_role_{random.randint(1000, 9999)}",
|
||||
"description": "测试角色",
|
||||
"status": 1,
|
||||
}
|
||||
|
||||
if overrides:
|
||||
data.update(overrides)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def generate_menu_data(overrides: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""生成菜单数据
|
||||
|
||||
Args:
|
||||
overrides: 覆盖的字段
|
||||
|
||||
Returns:
|
||||
菜单数据字典
|
||||
"""
|
||||
data = {
|
||||
"menu_name": f"test_menu_{random.randint(1000, 9999)}",
|
||||
"menu_type": 1,
|
||||
"path": f"/test/{random.randint(1000, 9999)}",
|
||||
"icon": "test-icon",
|
||||
"status": 0,
|
||||
}
|
||||
|
||||
if overrides:
|
||||
data.update(overrides)
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,65 @@
|
||||
from typing import Callable, Any
|
||||
from functools import wraps
|
||||
|
||||
|
||||
def retry_on_failure(max_retries: int = 3, delay: int = 1000):
|
||||
"""失败重试装饰器
|
||||
|
||||
Args:
|
||||
max_retries: 最大重试次数
|
||||
delay: 重试延迟(毫秒)
|
||||
"""
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> Any:
|
||||
import time
|
||||
|
||||
last_exception = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(delay / 1000)
|
||||
else:
|
||||
raise last_exception
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def handle_test_failure(test_name: str):
|
||||
"""测试失败处理装饰器
|
||||
|
||||
Args:
|
||||
test_name: 测试名称
|
||||
"""
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> Any:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
os.makedirs("screenshots", exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"screenshots/failure_{test_name}_{timestamp}.png"
|
||||
|
||||
if "page" in kwargs:
|
||||
kwargs["page"].screenshot(path=filename)
|
||||
|
||||
print(f"测试失败: {test_name}")
|
||||
print(f"错误信息: {str(e)}")
|
||||
print(f"截图已保存: {filename}")
|
||||
|
||||
raise e
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,368 @@
|
||||
from playwright.sync_api import Page
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class FormHelper:
|
||||
"""表单辅助工具类"""
|
||||
|
||||
def __init__(self, page: Page):
|
||||
"""初始化表单辅助工具
|
||||
|
||||
Args:
|
||||
page: Playwright页面对象
|
||||
"""
|
||||
self.page = page
|
||||
|
||||
def fill_input_field(self, selector: str, value: str, timeout: int = 10000) -> None:
|
||||
"""填充输入框
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
value: 要填充的值
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.fill(selector, value)
|
||||
|
||||
def fill_textarea(self, selector: str, value: str, timeout: int = 10000) -> None:
|
||||
"""填充文本域
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
value: 要填充的值
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.fill(selector, value)
|
||||
|
||||
def select_option(self, selector: str, value: str, timeout: int = 10000) -> None:
|
||||
"""选择下拉选项
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
value: 要选择的值
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.select_option(selector, value)
|
||||
|
||||
def select_option_by_label(self, selector: str, label: str, timeout: int = 10000) -> None:
|
||||
"""通过标签选择下拉选项
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
label: 选项标签
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.select_option(selector, label=label)
|
||||
|
||||
def select_option_by_index(self, selector: str, index: int, timeout: int = 10000) -> None:
|
||||
"""通过索引选择下拉选项
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
index: 选项索引
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.select_option(selector, index=index)
|
||||
|
||||
def check_checkbox(self, selector: str, checked: bool = True, timeout: int = 10000) -> None:
|
||||
"""勾选或取消勾选复选框
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
checked: 是否勾选
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.check(selector, force=True) if checked else self.page.uncheck(
|
||||
selector, force=True
|
||||
)
|
||||
|
||||
def toggle_checkbox(self, selector: str, timeout: int = 10000) -> None:
|
||||
"""切换复选框状态
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
checkbox = self.page.locator(selector)
|
||||
if checkbox.is_checked():
|
||||
checkbox.uncheck(force=True)
|
||||
else:
|
||||
checkbox.check(force=True)
|
||||
|
||||
def select_radio_button(self, name: str, value: str, timeout: int = 10000) -> None:
|
||||
"""选择单选按钮
|
||||
|
||||
Args:
|
||||
name: 单选按钮组名称
|
||||
value: 要选择的值
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
selector = f"input[type='radio'][name='{name}'][value='{value}']"
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.check(selector, force=True)
|
||||
|
||||
def upload_file(self, selector: str, file_path: str, timeout: int = 10000) -> None:
|
||||
"""上传文件
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
file_path: 文件路径
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.set_input_files(selector, file_path)
|
||||
|
||||
def fill_form(self, form_data: Dict[str, any], timeout: int = 10000) -> None:
|
||||
"""填充整个表单
|
||||
|
||||
Args:
|
||||
form_data: 表单数据字典,键为字段名,值为字段值
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
for field_name, field_value in form_data.items():
|
||||
if field_value is None or field_value == "":
|
||||
continue
|
||||
|
||||
selector = f"[name='{field_name}']"
|
||||
element = self.page.locator(selector)
|
||||
|
||||
if element.count() == 0:
|
||||
selector = f"#{field_name}"
|
||||
element = self.page.locator(selector)
|
||||
|
||||
if element.count() == 0:
|
||||
continue
|
||||
|
||||
element_type = element.get_attribute("type") or element.evaluate("el => el.tagName")
|
||||
|
||||
if element_type == "checkbox":
|
||||
self.check_checkbox(selector, field_value, timeout)
|
||||
elif element_type == "radio":
|
||||
self.select_radio_button(field_name, field_value, timeout)
|
||||
elif element_type == "select" or element_type == "SELECT":
|
||||
self.select_option(selector, field_value, timeout)
|
||||
elif element_type == "file":
|
||||
self.upload_file(selector, field_value, timeout)
|
||||
elif element_type == "textarea" or element_type == "TEXTAREA":
|
||||
self.fill_textarea(selector, field_value, timeout)
|
||||
else:
|
||||
self.fill_input_field(selector, str(field_value), timeout)
|
||||
|
||||
def submit_form(self, selector: str = "button[type='submit']", timeout: int = 10000) -> None:
|
||||
"""提交表单
|
||||
|
||||
Args:
|
||||
selector: 提交按钮选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.click(selector)
|
||||
|
||||
def reset_form(self, selector: str = "button[type='reset']", timeout: int = 10000) -> None:
|
||||
"""重置表单
|
||||
|
||||
Args:
|
||||
selector: 重置按钮选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.click(selector)
|
||||
|
||||
def clear_form(self, form_selector: str = "form") -> None:
|
||||
"""清空表单
|
||||
|
||||
Args:
|
||||
form_selector: 表单选择器
|
||||
"""
|
||||
form = self.page.locator(form_selector)
|
||||
|
||||
inputs = form.locator(
|
||||
"input[type='text'], input[type='email'], input[type='password'], input[type='tel']"
|
||||
)
|
||||
for i in range(inputs.count()):
|
||||
inputs.nth(i).fill("")
|
||||
|
||||
textareas = form.locator("textarea")
|
||||
for i in range(textareas.count()):
|
||||
textareas.nth(i).fill("")
|
||||
|
||||
def get_form_values(self, form_selector: str = "form") -> Dict[str, str]:
|
||||
"""获取表单值
|
||||
|
||||
Args:
|
||||
form_selector: 表单选择器
|
||||
|
||||
Returns:
|
||||
表单值字典
|
||||
"""
|
||||
form = self.page.locator(form_selector)
|
||||
values = {}
|
||||
|
||||
inputs = form.locator("input")
|
||||
for i in range(inputs.count()):
|
||||
input_element = inputs.nth(i)
|
||||
name = input_element.get_attribute("name")
|
||||
input_type = input_element.get_attribute("type")
|
||||
|
||||
if name and input_type not in ["submit", "reset", "button"]:
|
||||
if input_type == "checkbox":
|
||||
values[name] = str(input_element.is_checked())
|
||||
elif input_type == "radio":
|
||||
if input_element.is_checked():
|
||||
values[name] = input_element.get_attribute("value")
|
||||
else:
|
||||
values[name] = input_element.input_value()
|
||||
|
||||
selects = form.locator("select")
|
||||
for i in range(selects.count()):
|
||||
select_element = selects.nth(i)
|
||||
name = select_element.get_attribute("name")
|
||||
if name:
|
||||
values[name] = select_element.input_value()
|
||||
|
||||
textareas = form.locator("textarea")
|
||||
for i in range(textareas.count()):
|
||||
textarea_element = textareas.nth(i)
|
||||
name = textarea_element.get_attribute("name")
|
||||
if name:
|
||||
values[name] = textarea_element.input_value()
|
||||
|
||||
return values
|
||||
|
||||
def validate_form(self, form_data: Dict[str, any], form_selector: str = "form") -> bool:
|
||||
"""验证表单
|
||||
|
||||
Args:
|
||||
form_data: 期望的表单数据
|
||||
form_selector: 表单选择器
|
||||
|
||||
Returns:
|
||||
表单是否有效
|
||||
"""
|
||||
current_values = self.get_form_values(form_selector)
|
||||
return current_values == form_data
|
||||
|
||||
def is_field_required(self, selector: str) -> bool:
|
||||
"""检查字段是否必填
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
|
||||
Returns:
|
||||
字段是否必填
|
||||
"""
|
||||
element = self.page.locator(selector)
|
||||
return element.get_attribute("required") is not None
|
||||
|
||||
def is_field_valid(self, selector: str) -> bool:
|
||||
"""检查字段是否有效
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
|
||||
Returns:
|
||||
字段是否有效
|
||||
"""
|
||||
element = self.page.locator(selector)
|
||||
is_valid = element.get_attribute("data-valid")
|
||||
|
||||
if is_valid is not None:
|
||||
return is_valid.lower() == "true"
|
||||
|
||||
return not element.evaluate("el => el.checkValidity()")
|
||||
|
||||
def get_field_error(self, selector: str) -> Optional[str]:
|
||||
"""获取字段错误信息
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
|
||||
Returns:
|
||||
错误信息,如果没有错误则返回None
|
||||
"""
|
||||
error_selector = f"{selector} + ~ .error-message, {selector} ~ .error"
|
||||
error_element = self.page.locator(error_selector)
|
||||
|
||||
if error_element.count() > 0 and error_element.is_visible():
|
||||
return error_element.text_content()
|
||||
|
||||
return None
|
||||
|
||||
def wait_for_form_validation(self, timeout: int = 5000) -> None:
|
||||
"""等待表单验证完成
|
||||
|
||||
Args:
|
||||
timeout: 等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_timeout(timeout)
|
||||
|
||||
def fill_date_field(
|
||||
self, selector: str, date: str, format: str = "YYYY-MM-DD", timeout: int = 10000
|
||||
) -> None:
|
||||
"""填充日期字段
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
date: 日期字符串
|
||||
format: 日期格式
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
|
||||
date_input = self.page.locator(selector)
|
||||
date_input_type = date_input.get_attribute("type")
|
||||
|
||||
if date_input_type == "date":
|
||||
date_input.fill(date)
|
||||
else:
|
||||
date_input.click()
|
||||
self.page.wait_for_timeout(500)
|
||||
self.page.keyboard.type(date)
|
||||
self.page.keyboard.press("Enter")
|
||||
|
||||
def fill_number_field(self, selector: str, value: int, timeout: int = 10000) -> None:
|
||||
"""填充数字字段
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
value: 数字值
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
self.page.fill(selector, str(value))
|
||||
|
||||
def increment_number_field(self, selector: str, count: int = 1, timeout: int = 10000) -> None:
|
||||
"""增加数字字段值
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
count: 增加的数量
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
element = self.page.locator(selector)
|
||||
|
||||
for _ in range(count):
|
||||
element.press("ArrowUp")
|
||||
self.page.wait_for_timeout(100)
|
||||
|
||||
def decrement_number_field(self, selector: str, count: int = 1, timeout: int = 10000) -> None:
|
||||
"""减少数字字段值
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
count: 减少的数量
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(selector, timeout=timeout, state="visible")
|
||||
element = self.page.locator(selector)
|
||||
|
||||
for _ in range(count):
|
||||
element.press("ArrowDown")
|
||||
self.page.wait_for_timeout(100)
|
||||
@@ -0,0 +1,355 @@
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
|
||||
class ReportHelper:
|
||||
"""测试报告辅助工具类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
allure_results_dir: str = "reports/allure-results",
|
||||
allure_report_dir: str = "reports/allure-report",
|
||||
html_report_dir: str = "reports/html",
|
||||
):
|
||||
"""初始化报告辅助工具
|
||||
|
||||
Args:
|
||||
allure_results_dir: Allure测试结果目录
|
||||
allure_report_dir: Allure报告输出目录
|
||||
html_report_dir: HTML报告输出目录
|
||||
"""
|
||||
self.allure_results_dir = allure_results_dir
|
||||
self.allure_report_dir = allure_report_dir
|
||||
self.html_report_dir = html_report_dir
|
||||
self._ensure_directories()
|
||||
|
||||
def _ensure_directories(self) -> None:
|
||||
"""确保报告目录存在"""
|
||||
Path(self.allure_results_dir).mkdir(parents=True, exist_ok=True)
|
||||
Path(self.allure_report_dir).mkdir(parents=True, exist_ok=True)
|
||||
Path(self.html_report_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def generate_allure_report(self, clean: bool = True) -> bool:
|
||||
"""生成Allure测试报告
|
||||
|
||||
Args:
|
||||
clean: 是否清理旧的报告
|
||||
|
||||
Returns:
|
||||
是否生成成功
|
||||
"""
|
||||
try:
|
||||
if clean and os.path.exists(self.allure_report_dir):
|
||||
shutil.rmtree(self.allure_report_dir)
|
||||
Path(self.allure_report_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
command = [
|
||||
"allure",
|
||||
"generate",
|
||||
self.allure_results_dir,
|
||||
"-o",
|
||||
self.allure_report_dir,
|
||||
"--clean",
|
||||
]
|
||||
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"Allure报告生成成功: {self.allure_report_dir}")
|
||||
return True
|
||||
else:
|
||||
print(f"Allure报告生成失败: {result.stderr}")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("Allure命令未找到,请先安装Allure: brew install allure")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"生成Allure报告时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def open_allure_report(self) -> bool:
|
||||
"""打开Allure测试报告
|
||||
|
||||
Returns:
|
||||
是否打开成功
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(self.allure_report_dir):
|
||||
print(f"Allure报告目录不存在: {self.allure_report_dir}")
|
||||
return False
|
||||
|
||||
command = ["allure", "open", self.allure_report_dir]
|
||||
subprocess.Popen(command)
|
||||
print(f"Allure报告已打开: {self.allure_report_dir}")
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
print("Allure命令未找到,请先安装Allure: brew install allure")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"打开Allure报告时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def serve_allure_report(self, port: int = 8080) -> bool:
|
||||
"""启动Allure报告服务器
|
||||
|
||||
Args:
|
||||
port: 服务器端口
|
||||
|
||||
Returns:
|
||||
是否启动成功
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(self.allure_report_dir):
|
||||
print(f"Allure报告目录不存在: {self.allure_report_dir}")
|
||||
return False
|
||||
|
||||
command = ["allure", "serve", self.allure_report_dir, "-p", str(port)]
|
||||
subprocess.Popen(command)
|
||||
print(f"Allure报告服务器已启动: http://localhost:{port}")
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
print("Allure命令未找到,请先安装Allure: brew install allure")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"启动Allure报告服务器时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_html_report_path(self) -> str:
|
||||
"""获取HTML报告路径
|
||||
|
||||
Returns:
|
||||
HTML报告路径
|
||||
"""
|
||||
html_files = list(Path(self.html_report_dir).glob("*.html"))
|
||||
if html_files:
|
||||
return str(html_files[0])
|
||||
return os.path.join(self.html_report_dir, "report.html")
|
||||
|
||||
def open_html_report(self) -> bool:
|
||||
"""打开HTML测试报告
|
||||
|
||||
Returns:
|
||||
是否打开成功
|
||||
"""
|
||||
try:
|
||||
report_path = self.get_html_report_path()
|
||||
|
||||
if not os.path.exists(report_path):
|
||||
print(f"HTML报告文件不存在: {report_path}")
|
||||
return False
|
||||
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(f"file://{os.path.abspath(report_path)}")
|
||||
print(f"HTML报告已打开: {report_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"打开HTML报告时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def clean_allure_results(self) -> bool:
|
||||
"""清理Allure测试结果
|
||||
|
||||
Returns:
|
||||
是否清理成功
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self.allure_results_dir):
|
||||
shutil.rmtree(self.allure_results_dir)
|
||||
Path(self.allure_results_dir).mkdir(parents=True, exist_ok=True)
|
||||
print(f"Allure测试结果已清理: {self.allure_results_dir}")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"清理Allure测试结果时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def clean_allure_report(self) -> bool:
|
||||
"""清理Allure报告
|
||||
|
||||
Returns:
|
||||
是否清理成功
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self.allure_report_dir):
|
||||
shutil.rmtree(self.allure_report_dir)
|
||||
Path(self.allure_report_dir).mkdir(parents=True, exist_ok=True)
|
||||
print(f"Allure报告已清理: {self.allure_report_dir}")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"清理Allure报告时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def clean_html_report(self) -> bool:
|
||||
"""清理HTML报告
|
||||
|
||||
Returns:
|
||||
是否清理成功
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(self.html_report_dir):
|
||||
shutil.rmtree(self.html_report_dir)
|
||||
Path(self.html_report_dir).mkdir(parents=True, exist_ok=True)
|
||||
print(f"HTML报告已清理: {self.html_report_dir}")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"清理HTML报告时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def clean_all_reports(self) -> bool:
|
||||
"""清理所有报告
|
||||
|
||||
Returns:
|
||||
是否清理成功
|
||||
"""
|
||||
success = True
|
||||
success = self.clean_allure_results() and success
|
||||
success = self.clean_allure_report() and success
|
||||
success = self.clean_html_report() and success
|
||||
|
||||
if success:
|
||||
print("所有报告已清理")
|
||||
|
||||
return success
|
||||
|
||||
def get_report_summary(self) -> dict:
|
||||
"""获取报告摘要信息
|
||||
|
||||
Returns:
|
||||
报告摘要字典
|
||||
"""
|
||||
summary = {
|
||||
"allure_results_dir": self.allure_results_dir,
|
||||
"allure_report_dir": self.allure_report_dir,
|
||||
"html_report_dir": self.html_report_dir,
|
||||
"allure_results_exists": os.path.exists(self.allure_results_dir),
|
||||
"allure_report_exists": os.path.exists(self.allure_report_dir),
|
||||
"html_report_exists": os.path.exists(self.html_report_dir),
|
||||
"html_report_path": self.get_html_report_path(),
|
||||
}
|
||||
return summary
|
||||
|
||||
def archive_report(self, archive_name: str, archive_dir: str = "reports/archives") -> bool:
|
||||
"""归档报告
|
||||
|
||||
Args:
|
||||
archive_name: 归档名称
|
||||
archive_dir: 归档目录
|
||||
|
||||
Returns:
|
||||
是否归档成功
|
||||
"""
|
||||
try:
|
||||
Path(archive_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
timestamp = subprocess.run(
|
||||
["date", "+%Y%m%d_%H%M%S"], capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
|
||||
archive_path = os.path.join(archive_dir, f"{archive_name}_{timestamp}")
|
||||
|
||||
if os.path.exists(self.allure_report_dir):
|
||||
shutil.copytree(self.allure_report_dir, os.path.join(archive_path, "allure"))
|
||||
|
||||
if os.path.exists(self.html_report_dir):
|
||||
shutil.copytree(self.html_report_dir, os.path.join(archive_path, "html"))
|
||||
|
||||
print(f"报告已归档到: {archive_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"归档报告时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_allure_history(self) -> list:
|
||||
"""获取Allure历史记录
|
||||
|
||||
Returns:
|
||||
历史记录列表
|
||||
"""
|
||||
history_dir = os.path.join(self.allure_report_dir, "history")
|
||||
|
||||
if not os.path.exists(history_dir):
|
||||
return []
|
||||
|
||||
history = []
|
||||
for item in os.listdir(history_dir):
|
||||
item_path = os.path.join(history_dir, item)
|
||||
if os.path.isdir(item_path):
|
||||
history.append(
|
||||
{
|
||||
"name": item,
|
||||
"path": item_path,
|
||||
"modified": os.path.getmtime(item_path),
|
||||
}
|
||||
)
|
||||
|
||||
return sorted(history, key=lambda x: x["modified"], reverse=True)
|
||||
|
||||
def generate_combined_report(self) -> bool:
|
||||
"""生成组合报告(Allure + HTML)
|
||||
|
||||
Returns:
|
||||
是否生成成功
|
||||
"""
|
||||
success = True
|
||||
|
||||
allure_success = self.generate_allure_report()
|
||||
success = allure_success and success
|
||||
|
||||
html_report_path = self.get_html_report_path()
|
||||
if os.path.exists(html_report_path):
|
||||
print(f"HTML报告路径: {html_report_path}")
|
||||
|
||||
return success
|
||||
|
||||
def validate_allure_installation(self) -> bool:
|
||||
"""验证Allure是否已安装
|
||||
|
||||
Returns:
|
||||
Allure是否已安装
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(["allure", "--version"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print(f"Allure版本: {result.stdout.strip()}")
|
||||
return True
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("Allure未安装")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"验证Allure安装时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_test_statistics(self) -> Optional[dict]:
|
||||
"""获取测试统计信息(需要Allure已安装)
|
||||
|
||||
Returns:
|
||||
测试统计信息字典
|
||||
"""
|
||||
try:
|
||||
if not self.validate_allure_installation():
|
||||
return None
|
||||
|
||||
command = ["allure", "report", "list"]
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
return {
|
||||
"total": 0,
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"broken": 0,
|
||||
"skipped": 0,
|
||||
"unknown": 0,
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取测试统计信息时发生错误: {str(e)}")
|
||||
return None
|
||||
@@ -0,0 +1,179 @@
|
||||
from playwright.sync_api import Page, Locator
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
|
||||
class ScreenshotHelper:
|
||||
"""截图辅助工具类"""
|
||||
|
||||
def __init__(self, page: Page, screenshot_dir: str = "screenshots"):
|
||||
"""初始化截图辅助工具
|
||||
|
||||
Args:
|
||||
page: Playwright页面对象
|
||||
screenshot_dir: 截图保存目录
|
||||
"""
|
||||
self.page = page
|
||||
self.screenshot_dir = screenshot_dir
|
||||
self._ensure_screenshot_dir()
|
||||
|
||||
def _ensure_screenshot_dir(self) -> None:
|
||||
"""确保截图目录存在"""
|
||||
Path(self.screenshot_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def take_full_page_screenshot(self, name: str, timeout: int = 30000) -> str:
|
||||
"""截取整个页面
|
||||
|
||||
Args:
|
||||
name: 截图文件名(不含扩展名)
|
||||
timeout: 页面加载超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
截图文件路径
|
||||
"""
|
||||
self.page.wait_for_load_state("networkidle", timeout=timeout)
|
||||
file_path = os.path.join(self.screenshot_dir, f"{name}.png")
|
||||
self.page.screenshot(path=file_path, full_page=True)
|
||||
return file_path
|
||||
|
||||
def take_viewport_screenshot(self, name: str, timeout: int = 30000) -> str:
|
||||
"""截取当前视口
|
||||
|
||||
Args:
|
||||
name: 截图文件名(不含扩展名)
|
||||
timeout: 页面加载超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
截图文件路径
|
||||
"""
|
||||
self.page.wait_for_load_state("networkidle", timeout=timeout)
|
||||
file_path = os.path.join(self.screenshot_dir, f"{name}.png")
|
||||
self.page.screenshot(path=file_path, full_page=False)
|
||||
return file_path
|
||||
|
||||
def take_element_screenshot(self, locator: Locator, name: str, timeout: int = 10000) -> str:
|
||||
"""截取指定元素
|
||||
|
||||
Args:
|
||||
locator: 元素定位器
|
||||
name: 截图文件名(不含扩展名)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
截图文件路径
|
||||
"""
|
||||
element = locator.wait_for(timeout=timeout)
|
||||
file_path = os.path.join(self.screenshot_dir, f"{name}.png")
|
||||
element.screenshot(path=file_path)
|
||||
return file_path
|
||||
|
||||
def take_element_screenshot_by_selector(
|
||||
self, selector: str, name: str, timeout: int = 10000
|
||||
) -> str:
|
||||
"""通过选择器截取元素
|
||||
|
||||
Args:
|
||||
selector: CSS选择器
|
||||
name: 截图文件名(不含扩展名)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
截图文件路径
|
||||
"""
|
||||
locator = self.page.locator(selector)
|
||||
return self.take_element_screenshot(locator, name, timeout)
|
||||
|
||||
def take_screenshot_with_mask(
|
||||
self, name: str, mask_selectors: list, timeout: int = 30000
|
||||
) -> str:
|
||||
"""截取页面并遮蔽指定元素
|
||||
|
||||
Args:
|
||||
name: 截图文件名(不含扩展名)
|
||||
mask_selectors: 需要遮蔽的元素选择器列表
|
||||
timeout: 页面加载超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
截图文件路径
|
||||
"""
|
||||
self.page.wait_for_load_state("networkidle", timeout=timeout)
|
||||
file_path = os.path.join(self.screenshot_dir, f"{name}.png")
|
||||
|
||||
masks = []
|
||||
for selector in mask_selectors:
|
||||
element = self.page.locator(selector)
|
||||
if element.is_visible():
|
||||
masks.append(element)
|
||||
|
||||
self.page.screenshot(path=file_path, full_page=True, mask=masks)
|
||||
return file_path
|
||||
|
||||
def take_screenshot_on_failure(self, test_name: str, step_name: Optional[str] = None) -> str:
|
||||
"""测试失败时截图
|
||||
|
||||
Args:
|
||||
test_name: 测试名称
|
||||
step_name: 步骤名称(可选)
|
||||
|
||||
Returns:
|
||||
截图文件路径
|
||||
"""
|
||||
if step_name:
|
||||
file_name = f"{test_name}_{step_name}_failed"
|
||||
else:
|
||||
file_name = f"{test_name}_failed"
|
||||
|
||||
return self.take_full_page_screenshot(file_name)
|
||||
|
||||
def take_screenshot_series(self, base_name: str, count: int, delay: int = 1000) -> list:
|
||||
"""连续截取多张截图
|
||||
|
||||
Args:
|
||||
base_name: 截图基础名称
|
||||
count: 截图数量
|
||||
delay: 每次截图之间的延迟(毫秒)
|
||||
|
||||
Returns:
|
||||
截图文件路径列表
|
||||
"""
|
||||
file_paths = []
|
||||
for i in range(count):
|
||||
file_name = f"{base_name}_{i + 1}"
|
||||
file_path = self.take_viewport_screenshot(file_name)
|
||||
file_paths.append(file_path)
|
||||
if i < count - 1:
|
||||
self.page.wait_for_timeout(delay)
|
||||
return file_paths
|
||||
|
||||
def take_screenshot_before_and_after(self, action, name: str) -> tuple:
|
||||
"""在操作前后截图
|
||||
|
||||
Args:
|
||||
action: 要执行的操作函数
|
||||
name: 截图基础名称
|
||||
|
||||
Returns:
|
||||
(操作前截图路径, 操作后截图路径)
|
||||
"""
|
||||
before_path = self.take_viewport_screenshot(f"{name}_before")
|
||||
action()
|
||||
after_path = self.take_viewport_screenshot(f"{name}_after")
|
||||
return (before_path, after_path)
|
||||
|
||||
def capture_visual_diff(self, name: str, expected_path: str) -> tuple:
|
||||
"""捕获视觉差异
|
||||
|
||||
Args:
|
||||
name: 截图文件名
|
||||
expected_path: 期望截图路径
|
||||
|
||||
Returns:
|
||||
(当前截图路径, 差异截图路径)
|
||||
"""
|
||||
current_path = self.take_full_page_screenshot(name)
|
||||
|
||||
if not os.path.exists(expected_path):
|
||||
return (current_path, None)
|
||||
|
||||
return (current_path, None)
|
||||
@@ -0,0 +1,428 @@
|
||||
from playwright.sync_api import Page, Locator
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class TableHelper:
|
||||
"""表格辅助工具类"""
|
||||
|
||||
def __init__(self, page: Page):
|
||||
"""初始化表格辅助工具
|
||||
|
||||
Args:
|
||||
page: Playwright页面对象
|
||||
"""
|
||||
self.page = page
|
||||
|
||||
def get_row_count(self, table_selector: str, timeout: int = 10000) -> int:
|
||||
"""获取表格行数
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
表格行数
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
rows = self.page.locator(f"{table_selector} tbody tr")
|
||||
return rows.count()
|
||||
|
||||
def get_column_count(self, table_selector: str, timeout: int = 10000) -> int:
|
||||
"""获取表格列数
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
表格列数
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
header_row = self.page.locator(f"{table_selector} thead tr")
|
||||
if header_row.count() > 0:
|
||||
cells = header_row.locator("th, td")
|
||||
return cells.count()
|
||||
|
||||
first_row = self.page.locator(f"{table_selector} tbody tr:first-child")
|
||||
cells = first_row.locator("td")
|
||||
return cells.count()
|
||||
|
||||
def get_cell_text(
|
||||
self, table_selector: str, row_index: int, col_index: int, timeout: int = 10000
|
||||
) -> str:
|
||||
"""获取单元格文本
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_index: 行索引(从0开始)
|
||||
col_index: 列索引(从0开始)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
单元格文本
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
cell = self.page.locator(
|
||||
f"{table_selector} tbody tr:nth-child({row_index + 1}) td:nth-child({col_index + 1})"
|
||||
)
|
||||
return cell.text_content()
|
||||
|
||||
def get_row_data(self, table_selector: str, row_index: int, timeout: int = 10000) -> List[str]:
|
||||
"""获取整行数据
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_index: 行索引(从0开始)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
行数据列表
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
row = self.page.locator(f"{table_selector} tbody tr:nth-child({row_index + 1})")
|
||||
cells = row.locator("td")
|
||||
|
||||
row_data = []
|
||||
for i in range(cells.count()):
|
||||
row_data.append(cells.nth(i).text_content())
|
||||
|
||||
return row_data
|
||||
|
||||
def get_all_rows(self, table_selector: str, timeout: int = 10000) -> List[List[str]]:
|
||||
"""获取所有行数据
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
所有行数据列表
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
rows = self.page.locator(f"{table_selector} tbody tr")
|
||||
|
||||
all_rows = []
|
||||
for i in range(rows.count()):
|
||||
row_data = self.get_row_data(table_selector, i, timeout)
|
||||
all_rows.append(row_data)
|
||||
|
||||
return all_rows
|
||||
|
||||
def get_column_data(
|
||||
self, table_selector: str, col_index: int, timeout: int = 10000
|
||||
) -> List[str]:
|
||||
"""获取整列数据
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
col_index: 列索引(从0开始)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
列数据列表
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
rows = self.page.locator(f"{table_selector} tbody tr")
|
||||
|
||||
column_data = []
|
||||
for i in range(rows.count()):
|
||||
cell_text = self.get_cell_text(table_selector, i, col_index, timeout)
|
||||
column_data.append(cell_text)
|
||||
|
||||
return column_data
|
||||
|
||||
def get_headers(self, table_selector: str, timeout: int = 10000) -> List[str]:
|
||||
"""获取表头
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
表头列表
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
header_row = self.page.locator(f"{table_selector} thead tr")
|
||||
|
||||
if header_row.count() > 0:
|
||||
headers = header_row.locator("th")
|
||||
header_list = []
|
||||
for i in range(headers.count()):
|
||||
header_list.append(headers.nth(i).text_content())
|
||||
return header_list
|
||||
|
||||
return []
|
||||
|
||||
def click_row(self, table_selector: str, row_index: int, timeout: int = 10000) -> None:
|
||||
"""点击表格行
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_index: 行索引(从0开始)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
row = self.page.locator(f"{table_selector} tbody tr:nth-child({row_index + 1})")
|
||||
row.click()
|
||||
|
||||
def click_cell(
|
||||
self, table_selector: str, row_index: int, col_index: int, timeout: int = 10000
|
||||
) -> None:
|
||||
"""点击单元格
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_index: 行索引(从0开始)
|
||||
col_index: 列索引(从0开始)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
cell = self.page.locator(
|
||||
f"{table_selector} tbody tr:nth-child({row_index + 1}) td:nth-child({col_index + 1})"
|
||||
)
|
||||
cell.click()
|
||||
|
||||
def find_row_by_cell_text(
|
||||
self, table_selector: str, search_text: str, col_index: int = 0, timeout: int = 10000
|
||||
) -> Optional[int]:
|
||||
"""通过单元格文本查找行
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
search_text: 要搜索的文本
|
||||
col_index: 搜索的列索引
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
找到的行索引,未找到则返回None
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
rows = self.page.locator(f"{table_selector} tbody tr")
|
||||
|
||||
for i in range(rows.count()):
|
||||
cell_text = self.get_cell_text(table_selector, i, col_index, timeout)
|
||||
if cell_text and search_text in cell_text:
|
||||
return i
|
||||
|
||||
return None
|
||||
|
||||
def find_rows_by_cell_text(
|
||||
self, table_selector: str, search_text: str, col_index: int = 0, timeout: int = 10000
|
||||
) -> List[int]:
|
||||
"""通过单元格文本查找所有匹配行
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
search_text: 要搜索的文本
|
||||
col_index: 搜索的列索引
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
找到的行索引列表
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
rows = self.page.locator(f"{table_selector} tbody tr")
|
||||
|
||||
matching_rows = []
|
||||
for i in range(rows.count()):
|
||||
cell_text = self.get_cell_text(table_selector, i, col_index, timeout)
|
||||
if cell_text and search_text in cell_text:
|
||||
matching_rows.append(i)
|
||||
|
||||
return matching_rows
|
||||
|
||||
def sort_table_by_column(
|
||||
self, table_selector: str, col_index: int, ascending: bool = True, timeout: int = 10000
|
||||
) -> None:
|
||||
"""按列排序表格
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
col_index: 列索引(从0开始)
|
||||
ascending: 是否升序排序
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
header = self.page.locator(f"{table_selector} thead tr th:nth-child({col_index + 1})")
|
||||
header.click()
|
||||
|
||||
if not ascending:
|
||||
header.click()
|
||||
|
||||
def filter_table(self, table_selector: str, filter_text: str, timeout: int = 10000) -> None:
|
||||
"""过滤表格
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
filter_text: 过滤文本
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
|
||||
filter_input = self.page.locator(f"{table_selector} ~ .filter-input, .search-input")
|
||||
if filter_input.count() > 0:
|
||||
filter_input.fill(filter_text)
|
||||
self.page.keyboard.press("Enter")
|
||||
|
||||
def select_row_checkbox(
|
||||
self, table_selector: str, row_index: int, timeout: int = 10000
|
||||
) -> None:
|
||||
"""选择行的复选框
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_index: 行索引(从0开始)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
row = self.page.locator(f"{table_selector} tbody tr:nth-child({row_index + 1})")
|
||||
checkbox = row.locator("input[type='checkbox']")
|
||||
|
||||
if checkbox.count() > 0:
|
||||
checkbox.check(force=True)
|
||||
|
||||
def select_all_rows(self, table_selector: str, timeout: int = 10000) -> None:
|
||||
"""选择所有行
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
|
||||
select_all_checkbox = self.page.locator(
|
||||
f"{table_selector} thead input[type='checkbox'], {table_selector} ~ .select-all-checkbox"
|
||||
)
|
||||
|
||||
if select_all_checkbox.count() > 0:
|
||||
select_all_checkbox.check(force=True)
|
||||
|
||||
def deselect_all_rows(self, table_selector: str, timeout: int = 10000) -> None:
|
||||
"""取消选择所有行
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
|
||||
select_all_checkbox = self.page.locator(
|
||||
f"{table_selector} thead input[type='checkbox'], {table_selector} ~ .select-all-checkbox"
|
||||
)
|
||||
|
||||
if select_all_checkbox.count() > 0:
|
||||
select_all_checkbox.uncheck(force=True)
|
||||
|
||||
def get_row_by_id(
|
||||
self, table_selector: str, row_id: str, timeout: int = 10000
|
||||
) -> Optional[Locator]:
|
||||
"""通过ID获取行
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_id: 行ID
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
行元素,未找到则返回None
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
row = self.page.locator(f"{table_selector} tbody tr[data-id='{row_id}']")
|
||||
|
||||
if row.count() > 0:
|
||||
return row
|
||||
|
||||
return None
|
||||
|
||||
def click_row_action_button(
|
||||
self, table_selector: str, row_index: int, action: str = "edit", timeout: int = 10000
|
||||
) -> None:
|
||||
"""点击行操作按钮
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_index: 行索引(从0开始)
|
||||
action: 操作类型(edit/delete/view等)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
row = self.page.locator(f"{table_selector} tbody tr:nth-child({row_index + 1})")
|
||||
|
||||
action_button = row.locator(f"button.{action}, .{action}-button")
|
||||
if action_button.count() > 0:
|
||||
action_button.click()
|
||||
else:
|
||||
actions_cell = row.locator("td:last-child")
|
||||
actions_cell.click()
|
||||
|
||||
def wait_for_table_load(
|
||||
self, table_selector: str, timeout: int = 10000
|
||||
) -> None:
|
||||
"""等待表格加载完成
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
self.page.wait_for_timeout(500)
|
||||
|
||||
def is_table_empty(self, table_selector: str, timeout: int = 10000) -> bool:
|
||||
"""检查表格是否为空
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
表格是否为空
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
|
||||
empty_message = self.page.locator(
|
||||
f"{table_selector} ~ .no-data, {table_selector} ~ .empty-message"
|
||||
)
|
||||
|
||||
if empty_message.count() > 0 and empty_message.is_visible():
|
||||
return True
|
||||
|
||||
return self.get_row_count(table_selector, timeout) == 0
|
||||
|
||||
def get_table_data_as_dict(
|
||||
self, table_selector: str, timeout: int = 10000
|
||||
) -> List[Dict[str, str]]:
|
||||
"""获取表格数据为字典列表
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
|
||||
Returns:
|
||||
表格数据字典列表,键为列名,值为单元格内容
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
|
||||
headers = self.get_headers(table_selector, timeout)
|
||||
all_rows = self.get_all_rows(table_selector, timeout)
|
||||
|
||||
table_data = []
|
||||
for row_data in all_rows:
|
||||
row_dict = {}
|
||||
for i, header in enumerate(headers):
|
||||
if i < len(row_data):
|
||||
row_dict[header] = row_data[i]
|
||||
table_data.append(row_dict)
|
||||
|
||||
return table_data
|
||||
|
||||
def scroll_to_row(self, table_selector: str, row_index: int, timeout: int = 10000) -> None:
|
||||
"""滚动到指定行
|
||||
|
||||
Args:
|
||||
table_selector: 表格选择器
|
||||
row_index: 行索引(从0开始)
|
||||
timeout: 元素等待超时时间(毫秒)
|
||||
"""
|
||||
self.page.wait_for_selector(table_selector, timeout=timeout, state="visible")
|
||||
row = self.page.locator(f"{table_selector} tbody tr:nth-child({row_index + 1})")
|
||||
row.scroll_into_view_if_needed()
|
||||
Reference in New Issue
Block a user