Files
Yb-QaAutomationHub/tests/core/playwright_support.py
T
xst dd0877653e refactor(locators): 文本锚点优先的多候选定位 + 失败 DOM 快照
定位健壮性原则: 语义文本 > 组件库稳定 class > 层级结构 > nth-child/绝对路径。

Web (freight_accounting_page):
- 新增 BasePage.first_matching(*候选): 文本锚点优先命中, 旧结构路径兜底
- 13 处定位器改造: 核算/审核通过/确认打款按钮(get_by_role 文本)、
  车队长/收款模式表单(.el-form-item label 锚定)、光大单选(label:has-text)、
  获取验证码/验证码输入(按钮同行锚定)、message-box 确定等
- 手机号单选保留旧结构路径优先(弹窗含多组电话单选, 文本有歧义)
- 已实况验证: 获取验证码/确认打款/光大锚点唯一命中

App (freight_flow_page):
- 货源 tab: text("货源") 文本锚点优先(登录流程持续验证), 位置序号兜底
- 货源单列表项: RecyclerView(lRrcyclerView) 第一个子项, 全局 instance(3) 兜底
- 选图按钮(ImageView instance(2))保留: 需在途订单才能探测, 当前可用

失败诊断:
- Web 失败时 attach 页面 DOM 快照, App 失败时 attach page_source 到 Allure

验收: 实况锚点解析全部唯一命中; 两条 E2E 通过且 DB 终态均为全流程完成
(status=5, 已结算, 双支付完成)。
2026-09-02 14:59:27 +08:00

164 lines
6.4 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 allure, attach_png, attach_text
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:
"""用例失败时整页截图 + DOM 快照(存盘/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:
pass
attach_png(path, name=f"失败截图-{tag}")
# DOM 快照: 定位器失效时直接看到当时的页面结构
try:
attach_text(page.content(), name=f"DOM快照-{tag}",
attachment_type=allure.attachment_type.HTML)
except Exception:
pass
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