Files
QaAutomationHub/tests/core/playwright_support.py
T
xst 18890c0437 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
2026-08-17 14:29:52 +08:00

158 lines
6.1 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.
"""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