feat: 基于 UI 自动化最佳实践的测试框架骨架搭建
- tests/config: 环境配置 (.yaml) + 配置加载器 - tests/utils: 日志工具 + 测试数据生成器 - tests/web: Playwright conftest + Page Object 基类 + 登录页 + 导航栏组件 + 登录用例(6条) - tests/app: Appium conftest + Page Object 基类 + 司机端首页 + 登录用例(3条) - pytest.ini: markers(p0/p1/p2/smoke/web/app) + 运行配置 - 所有用例按最佳实践采用 Page Object 模式 + data-testid 优先定位 + 失败自动截图
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Web 端根级 fixture:浏览器实例 + 失败自动截图。
|
||||
|
||||
提供:
|
||||
browser_name — 参数化 fixture,每个浏览器执行一轮
|
||||
page — Playwright Page 对象(每个用例独立)
|
||||
login — 可复用的登录 helper
|
||||
|
||||
Usage:
|
||||
def test_something(page, browser_name):
|
||||
page.goto("/admin")
|
||||
...
|
||||
|
||||
def test_admin_flow(page, login):
|
||||
login("super_admin") # 以管理员身份登录
|
||||
...
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
# 截图根目录
|
||||
SCREENSHOTS_DIR = Path("output/screenshots")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def web_config(config):
|
||||
"""session 级:Web 配置段。"""
|
||||
return config["web"]
|
||||
|
||||
|
||||
@pytest.fixture(params=["chromium", "firefox"])
|
||||
def browser_name(request):
|
||||
"""每个浏览器执行一次同一套用例。"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def page(web_config, browser_name, request):
|
||||
"""每个用例独立的 Playwright Page,失败自动截图。"""
|
||||
viewport = web_config.get("viewport", {"width": 1920, "height": 1080})
|
||||
timeout = web_config.get("timeout_seconds", 120) * 1000
|
||||
screenshot_on_failure = web_config.get("screenshot_on_failure", True)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser_launcher = getattr(p, browser_name)
|
||||
browser = browser_launcher.launch(headless=True)
|
||||
context = browser.new_context(
|
||||
viewport={"width": viewport["width"], "height": viewport["height"]},
|
||||
locale="zh-CN",
|
||||
)
|
||||
page = context.new_page()
|
||||
page.set_default_timeout(timeout)
|
||||
|
||||
yield page
|
||||
|
||||
# ── 失败自动截图 ──
|
||||
if screenshot_on_failure and hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
scenario_dir = SCREENSHOTS_DIR / request.node.name
|
||||
scenario_dir.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
path = str(scenario_dir / f"FAIL_{browser_name}_{ts}.png")
|
||||
page.screenshot(path=path, full_page=True)
|
||||
|
||||
context.close()
|
||||
browser.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def login(page, web_config, config):
|
||||
"""返回一个登录 helper 函数,按角色名自动填入账号并等待登录完成。
|
||||
|
||||
Usage:
|
||||
def test_admin(page, login):
|
||||
login("super_admin")
|
||||
page.getByTestId("nav-dashboard").click()
|
||||
"""
|
||||
|
||||
def _login(role: str = "super_admin") -> None:
|
||||
account = config["accounts"].get(role)
|
||||
if not account:
|
||||
raise ValueError(f"未知角色 '{role}',可用: {list(config['accounts'].keys())}")
|
||||
|
||||
base_url = web_config["base_url"]
|
||||
# 如果已经打开某个页面且未过期,直接返回
|
||||
if page.url and not page.url.startswith("about:") and "/login" not in page.url:
|
||||
return
|
||||
|
||||
page.goto(f"{base_url}/login")
|
||||
page.getByPlaceholder("请输入用户名").fill(account["phone"])
|
||||
page.getByPlaceholder("请输入密码").fill(account["password"])
|
||||
page.getByRole("button", name="登录").click()
|
||||
|
||||
# 等待登录成功跳转(10 秒超时)
|
||||
page.wait_for_url(f"{base_url}/**", timeout=10_000)
|
||||
|
||||
return _login
|
||||
|
||||
|
||||
# ── pytest 报告 hook:记录每个用例的失败状态 ──
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
"""在每个用例执行后记录结果,供 fixture 中的失败截图逻辑使用。"""
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
setattr(item, f"rep_{rep.when}", rep)
|
||||
Reference in New Issue
Block a user