Files
Yb-QaAutomationHub/tests/web/conftest.py
T

181 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Web 端根级 fixture:浏览器实例 + 登录态复用 + 失败自动截图。
核心机制 — 跳过图形验证码:
配置 web.skip_login: true 时,读取已保存的 storage_statecookies + localStorage),
跳过 UI 登录步骤。首次需要手动登录一次来生成 auth 文件。
首次初始化流程:
1. python tests/web/save_auth.py
2. 打开的浏览器中手动完成验证码登录
3. 登录态自动保存到 tests/web/auth/<role>.json
后续用例执行时自动复用,过期后重新运行 save_auth.py 即可。
提供:
browser_name — 参数化 fixture,每个浏览器执行一轮
page — Playwright Page 对象(已加载登录态)
ensure_login — 确保指定角色的登录态有效,未登录则报错提示
"""
from datetime import datetime
from pathlib import Path
import pytest
from playwright.sync_api import sync_playwright, Page, BrowserContext
SCREENSHOTS_DIR = Path("output/screenshots")
AUTH_DIR = Path(__file__).parent / "auth"
@pytest.fixture(scope="session")
def web_config(config):
"""session 级:Web 配置段。"""
return config["web"]
@pytest.fixture(params=["chromium", "firefox"])
def browser_name(web_config):
"""每个浏览器执行一次同一套用例。
可从命令行覆盖: pytest --browser=chromium tests/web/
"""
return request.param
@pytest.fixture
def page(web_config, browser_name, request) -> Page:
"""每个用例独立的 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)
skip_login = web_config.get("skip_login", True)
storage_state_dir = Path(web_config.get("storage_state_dir", "tests/web/auth"))
with sync_playwright() as p:
browser_launcher = getattr(p, browser_name)
browser = browser_launcher.launch(headless=True)
# ── 构建 context ──
context_kwargs = {
"viewport": {"width": viewport["width"], "height": viewport["height"]},
"locale": "zh-CN",
}
# 如果启用跳过登录,尝试加载 storage_state
loaded_role = None
if skip_login:
loaded_role = _load_auth_state(context_kwargs, storage_state_dir, browser_name)
context = browser.new_context(**context_kwargs)
page = context.new_page()
page.set_default_timeout(timeout)
# 将当前 context 的登录角色注入 page(用例中可检查)
page._auth_role = loaded_role
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 ensure_login(web_config, page, config):
"""确保已登录状态,未登录则自动执行 UI 登录(含验证码则报错)。
用法一(推荐 — 配置 skip_login: true:
def test_xxx(page, ensure_login):
ensure_login("super_admin")
# page 已处于登录后的管理后台
用法二(skip_login: false — 完整 UI 登录):
def test_xxx(page, ensure_login):
ensure_login("super_admin") # 执行完整 UI 登录
"""
def _ensure_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"]
skip_login = web_config.get("skip_login", True)
# 如果 storage_state 已加载且角色匹配,直接验证
if skip_login:
auth_role = getattr(page, "_auth_role", None)
if auth_role == role:
# 验证登录态是否仍然有效
page.goto(base_url)
# Hash 路由模式下判断是否重定向到登录页
page.wait_for_timeout(2000) # 等 hash 路由完成
if "/#/login" not in page.url and "/login" not in page.url:
return # 登录态有效
# 登录态过期,给出明确指引
raise RuntimeError(
f"登录态已过期或未生成!请运行以下命令重新生成:\n"
f" python tests/web/save_auth.py --role {role} --browser {_get_browser_name(page)}"
)
if auth_role and auth_role != role:
raise RuntimeError(
f"当前 storage_state 角色为 '{auth_role}',请求为 '{role}'。\n"
f"请运行: python tests/web/save_auth.py --role {role}"
)
# 没有加载任何 auth,报错
raise RuntimeError(
f"未找到角色 '{role}' 的登录态文件!请运行:\n"
f" python tests/web/save_auth.py --role {role}"
)
# ── skip_login: false,执行 UI 登录(可能有验证码,会失败) ──
page.goto(f"{base_url}/#/login")
page.get_by_placeholder("请输入用户名").fill(account["phone"])
page.get_by_placeholder("请输入密码").fill(account["password"])
page.get_by_role("button", name="登录").click()
# 等几秒让页面处理路由跳转
page.wait_for_timeout(3000)
if "/#/login" in page.url or "/login" in page.url:
raise RuntimeError(
"登录失败!可能存在图形验证码。\n"
"请改为手动登录生成 auth 文件: python tests/web/save_auth.py --role {role}"
)
def _load_auth_state(context_kwargs: dict, storage_dir: Path, browser: str) -> str | None:
"""尝试加载登录态,返回角色名或 None。"""
auth_files = list(storage_dir.glob(f"*_{browser}.json")) if storage_dir.exists() else []
if not auth_files:
return None
auth_file = auth_files[0] # 加载第一个匹配的 auth 文件
context_kwargs["storage_state"] = str(auth_file)
# 从文件名提取角色名(格式: <role>_<browser>.json
role = auth_file.stem.replace(f"_{browser}", "")
return role
def _get_browser_name(page: Page) -> str:
"""从 page 推测浏览器名(用于错误提示)。"""
return "chromium" # 默认提示 chromium
# ── pytest 报告 hook ──
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""记录用例执行结果,供失败截图使用。"""
outcome = yield
rep = outcome.get_result()
setattr(item, f"rep_{rep.when}", rep)