"""管理端 fixture — Playwright 浏览器实例 + 登录态复用。 核心机制: skip_login: true → 加载 storage_state,跳过验证码 skip_login: false → 执行完整 UI 登录 初始化: python tests/web/admin/save_auth.py """ from datetime import datetime from pathlib import Path import pytest from playwright.sync_api import sync_playwright, Page SCREENSHOTS_DIR = Path("output/screenshots") @pytest.fixture(params=["chromium", "firefox"]) def browser_name(admin_config, request): """每个浏览器执行一轮。""" browsers = admin_config.get("browsers", ["chromium", "firefox"]) request.param = browsers[len(getattr(request, "_index", 0))] return request.param @pytest.fixture def page(admin_config, browser_name, request) -> Page: """Playwright Page,加载登录态,失败自动截图。""" viewport = admin_config.get("viewport", {"width": 1920, "height": 1080}) timeout = admin_config.get("timeout_seconds", 120) * 1000 skip_login = admin_config.get("skip_login", True) storage_state_dir = Path(admin_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_kwargs = { "viewport": {"width": viewport["width"], "height": viewport["height"]}, "locale": "zh-CN", } 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) page._auth_role = loaded_role yield page if 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(admin_config, page, config): """确保管理端已登录。""" 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 = admin_config["base_url"] skip_login = admin_config.get("skip_login", True) if skip_login: auth_role = getattr(page, "_auth_role", None) if auth_role == role: page.goto(base_url) page.wait_for_timeout(2000) if "#/login" not in page.url and "/login" not in page.url: return raise RuntimeError( f"登录态已过期!请运行: python tests/web/admin/save_auth.py --role {role}" ) if auth_role and auth_role != role: raise RuntimeError( f"当前 storage_state 角色为 '{auth_role}',请求 '{role}'。\n" f"请运行: python tests/web/admin/save_auth.py --role {role}" ) raise RuntimeError( f"未找到角色 '{role}' 的登录态文件!请运行:\n" f" python tests/web/admin/save_auth.py --role {role}" ) # skip_login: false 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( "登录失败!可能存在图形验证码。请改为手动登录: python tests/web/admin/save_auth.py" ) return _ensure_login def _load_auth_state(context_kwargs: dict, storage_dir: Path, browser: str) -> str | 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] context_kwargs["storage_state"] = str(auth_file) return auth_file.stem.replace(f"_{browser}", "") @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)