refactor(tests): restructure into layered POM architecture with Allure reporting
- Split framework into core/ (config, logger, db, driver factories, screenshot, allure integration) and pages/ (PO layer: base/admin/driver); flatten test files per platform (web/admin, app/driver_android, cross_end) - Slim root conftest.py 656 -> ~100 lines: drop inline CN HTML report, add Allure severity mapping (p0/p1/p2 -> blocker/normal/minor) and environment.properties; support both tests.*-prefixed and bare imports - Preserve TC-D2S-001/002 cross-end E2E cases with business logic unchanged, add allure epic/feature/story/step annotations - Deduplicate: single makereport hook, shared failure-screenshot helpers, single SCREENSHOTS_DIR constant; deterministic auth-state role loading - Move DB credentials from db_helper.py into test_config.yaml (database section) - Move pytest.ini to repo root; switch reporting to allure-pytest (drop pytest-html/junitxml); update requirements.txt accordingly - Delete deprecated scaffolding: test_freight_full_flow + 6 stale page objects, iOS/shipper empty shells, dead utils - Fix flaky vxe-table action-row wait: wait for a visible td instead of hidden placeholder rows (fixed shipper-payment timeouts) - Verified end-to-end against live env: both E2E cases pass (2 passed 3m12s); run artifacts (logs/screenshots/allure) included per repo convention
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""Playwright 支持 — 浏览器 Page 工厂 + 快速关闭 + 登录态加载 + 失败截图。
|
||||
|
||||
快速关闭背景: 本机 Windows 环境下,Playwright 关闭浏览器或退出驱动时会先做
|
||||
最长约 30s 的优雅关闭等待(等 Chrome 进程自然退出),导致脚本/用例结束慢。
|
||||
fast_shutdown 强杀浏览器进程树 + node 驱动,让 p.stop() 立即返回。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.core.allure_support import attach_png
|
||||
from tests.core.paths import SCREENSHOTS_DIR
|
||||
|
||||
# 本机 %LOCALAPPDATA%\ms-playwright 下 Chrome for Testing 存在 SxS 激活失败问题,
|
||||
# 浏览器迁移到了自定义目录;仅当该目录存在时兜底设置,不影响其他机器。
|
||||
_CUSTOM_BROWSERS_PATH = "C:/Users/23184/pw-playwright"
|
||||
if os.path.isdir(_CUSTOM_BROWSERS_PATH):
|
||||
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", _CUSTOM_BROWSERS_PATH)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Page 工厂
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@contextmanager
|
||||
def admin_browser_page(admin_config: dict, browser_name: str = "chromium",
|
||||
role: str | None = None):
|
||||
"""创建管理端 Playwright Page(skip_login 时加载指定角色的登录态)。
|
||||
|
||||
Yields:
|
||||
(page, loaded_role): loaded_role 为实际加载登录态的角色,未加载为 None
|
||||
|
||||
退出时自动 fast_shutdown,跳过本机 ~30s 优雅关闭等待。
|
||||
"""
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
pytest.skip("Playwright 未安装。")
|
||||
|
||||
viewport = admin_config.get("viewport", {"width": 1920, "height": 1080})
|
||||
timeout = admin_config.get("timeout_seconds", 120) * 1000
|
||||
storage_state_dir = Path(admin_config.get("storage_state_dir", "tests/web/auth"))
|
||||
skip_login = admin_config.get("skip_login", True)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = getattr(p, browser_name).launch(headless=True)
|
||||
|
||||
context_kwargs = {
|
||||
"viewport": {"width": viewport["width"], "height": viewport["height"]},
|
||||
"locale": "zh-CN",
|
||||
}
|
||||
|
||||
loaded_role = None
|
||||
if skip_login and role:
|
||||
auth_file = storage_state_dir / f"{role}_{browser_name}.json"
|
||||
if auth_file.exists():
|
||||
context_kwargs["storage_state"] = str(auth_file)
|
||||
loaded_role = role
|
||||
|
||||
context = browser.new_context(**context_kwargs)
|
||||
page = context.new_page()
|
||||
page.set_default_timeout(timeout)
|
||||
|
||||
try:
|
||||
yield page, loaded_role
|
||||
finally:
|
||||
fast_shutdown(p)
|
||||
|
||||
|
||||
def pick_available_role(storage_state_dir: Path, browser_name: str) -> str | None:
|
||||
"""选择可用的登录态角色:优先 super_admin,否则按文件名排序取第一个。"""
|
||||
if not storage_state_dir.exists():
|
||||
return None
|
||||
files = sorted(storage_state_dir.glob(f"*_{browser_name}.json"))
|
||||
if not files:
|
||||
return None
|
||||
for f in files:
|
||||
if f.stem == f"super_admin_{browser_name}":
|
||||
return "super_admin"
|
||||
return files[0].stem[: -len(f"_{browser_name}")]
|
||||
|
||||
|
||||
def save_web_failure_screenshot(page, request, tag: str) -> str | None:
|
||||
"""用例失败时整页截图(存盘 + attach Allure),返回路径;未失败返回 None。
|
||||
|
||||
Args:
|
||||
page: Playwright Page
|
||||
request: pytest request(依赖根 conftest 的 makereport hook 提供 rep_call)
|
||||
tag: 文件名标识,如 "chromium" / "admin_chromium"
|
||||
"""
|
||||
if not (hasattr(request.node, "rep_call") and request.node.rep_call.failed):
|
||||
return None
|
||||
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_{tag}_{ts}.png")
|
||||
try:
|
||||
page.screenshot(path=path, full_page=True)
|
||||
except Exception:
|
||||
return None
|
||||
attach_png(path, name=f"失败截图-{tag}")
|
||||
return path
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# 快速关闭 (Windows)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
def _driver_proc(p):
|
||||
"""取 sync_playwright 对象内部的 node 驱动子进程对象。"""
|
||||
try:
|
||||
return p._impl_obj._connection._transport._proc
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def kill_browser_tree(driver_pid: int) -> list:
|
||||
"""杀掉 node 驱动直接拉起的浏览器主进程树(chrome / chrome-headless-shell)。"""
|
||||
query = (
|
||||
"Get-CimInstance Win32_Process | Where-Object { "
|
||||
f"$_.ParentProcessId -eq {driver_pid} -and "
|
||||
"($_.Name -like 'chrome*' -or $_.Name -like 'headless*') } "
|
||||
"| Select-Object -ExpandProperty ProcessId"
|
||||
)
|
||||
try:
|
||||
ps = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", query],
|
||||
capture_output=True, text=True, timeout=20,
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
pids = [x.strip() for x in ps.stdout.split() if x.strip().isdigit()]
|
||||
for pid in pids:
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", pid], capture_output=True, timeout=20)
|
||||
return pids
|
||||
|
||||
|
||||
def fast_shutdown(p) -> None:
|
||||
"""强杀浏览器进程树 + node 驱动,跳过 Playwright 的 30s 优雅关闭等待。
|
||||
|
||||
必须在浏览器已用完、且后续不再使用 p 的操作时调用。
|
||||
"""
|
||||
proc = _driver_proc(p)
|
||||
if proc is None:
|
||||
return
|
||||
try:
|
||||
kill_browser_tree(proc.pid)
|
||||
finally:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user