import re import shutil import subprocess import time from pathlib import Path import allure import pytest from config.settings import settings from playwright.sync_api import Error as PlaywrightError from playwright.sync_api import TimeoutError as PlaywrightTimeoutError from playwright.sync_api import expect from utils.logger import get_logger logger = get_logger() PROJECT_ROOT = Path(__file__).resolve().parents[1] ALLURE_RAW_DIR = PROJECT_ROOT / "reports" / "allure-raw" ALLURE_HTML_DIR = PROJECT_ROOT / "reports" / "allure-results" def accept_cookie_if_present(page): privacy_link = page.locator('a[href="/privacy-policy-web"]').last try: if privacy_link.is_visible(timeout=1_000): cookie_container = privacy_link.locator("xpath=ancestor::div[.//button][1]") cookie_container.locator("button").first.click() return except PlaywrightTimeoutError: pass accept_button = page.locator("button", has_text=re.compile(r"Acce?pet|Accept", re.I)).first try: if accept_button.is_visible(timeout=1_000): accept_button.click() except PlaywrightTimeoutError: logger.info("未检测到 Cookie 操作区域,继续执行") except PlaywrightError as exc: logger.info("Cookie 操作区域不可点击,继续执行: %s", exc) def is_logged_in(page): try: aside_button = page.locator("aside button").first if not aside_button.is_visible(timeout=3_000): return False button_text = aside_button.inner_text(timeout=3_000).strip() return bool(re.search(r"Logout|Log out", button_text, re.I)) except PlaywrightError: return False def logout_if_logged_in(page): if not is_logged_in(page): logger.info("当前为游客状态,无需 Logout") return logger.info("当前已登录,点击 Logout 切换为游客状态") page.locator("aside button").first.click() expect(page.locator("aside button").first).not_to_contain_text(re.compile(r"Logout|Log out", re.I), timeout=settings.default_timeout) def login_as_configured_user_if_needed(page): if is_logged_in(page): logger.info("当前已登录,跳过重复登录") return logger.info("当前未登录,执行配置用户登录") login_inputs = page.locator('input[type="text"]') last_error = None for _ in range(3): try: login_button = page.locator("aside button").first expect(login_button).to_be_visible(timeout=settings.default_timeout) login_button.click() expect(login_inputs.first).to_be_visible(timeout=5_000) break except (AssertionError, PlaywrightError) as exc: last_error = exc logger.info("登录弹窗未打开,重试点击登录入口: %s", exc) page.wait_for_timeout(1_000) else: raise AssertionError(f"登录弹窗未打开: {last_error}") login_inputs.nth(0).fill(settings.login_email) login_inputs.nth(1).fill(settings.verification_code) confirmation_items = page.locator("div.inline-flex.cursor-pointer") expect(confirmation_items.nth(0)).to_be_visible(timeout=settings.default_timeout) expect(confirmation_items.nth(1)).to_be_visible(timeout=settings.default_timeout) confirmation_items.nth(0).click() confirmation_items.nth(1).click() submit_button = page.locator("button").last expect(submit_button).to_be_enabled(timeout=settings.default_timeout) submit_button.click() expect(page.locator("aside button").first).to_contain_text( re.compile(r"Logout|Log out", re.I), timeout=settings.default_timeout ) @pytest.fixture def ensure_guest_user(page): def _ensure_guest_user(): accept_cookie_if_present(page) logout_if_logged_in(page) return _ensure_guest_user @pytest.fixture def ensure_logged_in_user(page): def _ensure_logged_in_user(): accept_cookie_if_present(page) login_as_configured_user_if_needed(page) return _ensure_logged_in_user def pytest_sessionstart(session): for report_dir in (ALLURE_RAW_DIR, ALLURE_HTML_DIR): if report_dir.exists(): shutil.rmtree(report_dir) report_dir.mkdir(parents=True, exist_ok=True) logger.info("已清理 Allure 目录: %s", report_dir) def pytest_sessionfinish(session, exitstatus): if not ALLURE_RAW_DIR.exists() or not any(ALLURE_RAW_DIR.iterdir()): logger.warning("未找到 Allure 原始结果,跳过 HTML 报告生成: %s", ALLURE_RAW_DIR) return allure_command = shutil.which("allure") or shutil.which("allure.cmd") if not allure_command: logger.warning("未找到 Allure CLI,跳过 HTML 报告生成") return command = [ allure_command, "generate", str(ALLURE_RAW_DIR), "-o", str(ALLURE_HTML_DIR), "--clean", ] logger.info("开始生成 Allure HTML 报告: %s", " ".join(command)) result = subprocess.run( command, cwd=PROJECT_ROOT, capture_output=True, text=True, encoding="utf-8", errors="replace", ) if result.returncode != 0: logger.error("Allure HTML 报告生成失败: %s", result.stderr.strip()) return logger.info("Allure HTML 报告已生成: %s", ALLURE_HTML_DIR / "index.html") def _safe_attachment_name(name): return re.sub(r"[^0-9A-Za-z_.\-\u4e00-\u9fff]+", "_", name).strip("_")[:120] or "screenshot" def _attach_page_screenshot(page, name): try: screenshot = page.screenshot(full_page=True) allure.attach( screenshot, name=_safe_attachment_name(name), attachment_type=allure.attachment_type.PNG, ) except PlaywrightError as exc: logger.info("Allure 截图附件生成失败: %s", exc) @pytest.fixture def attach_page_screenshot(page): def _attach(name="页面截图"): _attach_page_screenshot(page, name) return _attach @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() setattr(item, "rep_" + report.when, report) if report.when != "call" or "page" not in item.fixturenames: return page = item.funcargs.get("page") if not page: return if report.failed: _attach_page_screenshot(page, f"失败截图_{item.name}") elif report.passed: _attach_page_screenshot(page, f"结束截图_{item.name}") @pytest.fixture(autouse=True) def test_run_logger(request): case_name = request.node.nodeid start_time = time.perf_counter() logger.info("用例开始: %s", case_name) try: yield except Exception: elapsed = time.perf_counter() - start_time logger.exception("用例异常: %s, 耗时: %.2fs", case_name, elapsed) raise else: elapsed = time.perf_counter() - start_time logger.info("用例通过: %s, 耗时: %.2fs", case_name, elapsed) @pytest.fixture(scope="session") def browser_context_args(browser_context_args): return { **browser_context_args, "base_url": settings.base_url, "viewport": {"width": settings.viewport_width, "height": settings.viewport_height}, } @pytest.fixture(scope="session") def browser_type_launch_args(browser_type_launch_args): return { **browser_type_launch_args, "headless": True, # 可视化执行配置:"headless": False, "slow_mo": 300, "timeout": settings.default_timeout, }