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, 已结算, 双支付完成)。
This commit is contained in:
xst
2026-09-02 14:59:27 +08:00
parent 2b4b2a4050
commit dd0877653e
110 changed files with 386 additions and 94 deletions
+26
View File
@@ -10,6 +10,7 @@ Usage:
self.login_btn = page.get_by_role("button", name="登录")
"""
import time
from datetime import datetime
from typing import Optional
@@ -26,6 +27,31 @@ class BasePage:
self.page = page
self.timeout = timeout_ms
# ── 多候选定位 ──
def first_matching(self, *locators: Locator, timeout_ms: int = 5_000) -> Locator:
"""依次尝试多个候选定位器,返回第一个实际命中的;全部落空则抛错。
用于"语义文本锚点优先、结构路径兜底"的健壮定位:
页面文案通常比 DOM 结构稳定,文案锚点命中就用它,
结构变了还有旧路径兜底。
"""
deadline = time.monotonic() + timeout_ms / 1000
while True:
for loc in locators:
try:
if loc.count() > 0 and loc.first.is_visible():
return loc
except Exception:
continue
if time.monotonic() >= deadline:
break
self.page.wait_for_timeout(200)
raise TimeoutError(
f"所有候选定位器在 {timeout_ms}ms 内均未命中 "
f"({len(locators)} 个候选)"
)
# ── 通用等待 ──
def wait_visible(self, locator: Locator, timeout_ms: Optional[int] = None) -> None: