""" 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)