refactor(pages): PO 层框架化 — 细粒度操作 + 显式等待 + 消除静默吞异常
DriverFreightFlowPage (APP): - 流程方法拆为单一职责的细粒度操作(open_freight_tab/fill_quantity/upload_photo_from_album/...), 流程方法仅做编排,公共 API 不变,用例零改动 - 移除全部 implicitly_wait 全局副作用,每步显式等待目标元素 - 容错点击(已知晓/确定)改为 _tap_by_text_if_present,跳过时记录 warning FreightAccountingPage (Web): - _go/_click_search 改显式等待(搜索框出现/列表数据行或空态),替代固定 2s/1s sleep - 表格等待升级为 td:visible,避开 vxe-table 固定列隐藏占位单元格 - 点击行操作按钮后显式等待弹窗出现,替代固定 800ms - 车队长下拉选择改显式等待下拉项渲染 - 可选弹窗操作统一收敛到 _try_click,跳过时记录 warning;关键确认按钮保持严格 两条 E2E 用例在真实环境回归通过(TC-D2S-001 116s / TC-D2S-002 112s)。
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
基于 resources/selectors/正常流程-web端.md 中的元素定位。
|
||||
使用直接 hash URL 导航(比菜单点击更可靠),按钮选择器带 fallback 兜底。
|
||||
|
||||
等待策略: 全部显式等待(搜索框出现 / 列表数据或空态 / 弹窗出现),
|
||||
无常驻固定 sleep;无法用条件表达的短暂渲染间隙保留少量 settle 毫秒并注明原因。
|
||||
|
||||
URL 路由:
|
||||
运费核算: #/tms/Carrier/freight
|
||||
运单审核: #/tms/Carrier/OrderDetailAudit
|
||||
@@ -10,14 +13,28 @@ URL 路由:
|
||||
垫资-货主打款: #/tms/Carrier/dzApplicationForPayment
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from tests.pages.base.web_base_page import BasePage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FreightAccountingPage(BasePage):
|
||||
"""管理端 - 运费核算 / 运单审核 / 垫资打款。"""
|
||||
|
||||
# 列表页统一的搜索输入框(4 个路由页面均有)
|
||||
_ORDER_INPUT = 'input[placeholder="请输入运输单号"]'
|
||||
# 列表稳定态: 可见数据行 或 vxe-table 空态占位(类名随版本略有差异, 全列出)
|
||||
_TABLE_READY_SELECTOR = (
|
||||
"table tbody tr td:visible, "
|
||||
".vxe-table--empty-block, "
|
||||
".vxe-table--empty-placeholder-wrapper, "
|
||||
".vxe-table--empty-placeholder"
|
||||
)
|
||||
|
||||
def __init__(self, page: Page, admin_base_url: str = ""):
|
||||
super().__init__(page)
|
||||
self.admin_base_url = admin_base_url
|
||||
@@ -27,10 +44,10 @@ class FreightAccountingPage(BasePage):
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
def _go(self, hash_route: str):
|
||||
"""直接 hash URL 跳转,比菜单点击稳定。"""
|
||||
"""直接 hash URL 跳转,并等待列表页搜索框出现(显式等待替代固定 sleep)。"""
|
||||
url = f"{self.admin_base_url}/#/{hash_route.lstrip('#').lstrip('/')}"
|
||||
self.page.goto(url, wait_until="domcontentloaded")
|
||||
self.page.wait_for_timeout(2000)
|
||||
self.wait_visible(self.page.locator(self._ORDER_INPUT).first)
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 通用操作
|
||||
@@ -38,22 +55,39 @@ class FreightAccountingPage(BasePage):
|
||||
|
||||
def _fill_order_no(self, order_no: str):
|
||||
"""搜索栏输入运输单号(多个同名 input 时取第一个)。"""
|
||||
input_el = self.page.locator('input[placeholder="请输入运输单号"]').first
|
||||
input_el = self.page.locator(self._ORDER_INPUT).first
|
||||
self.wait_visible(input_el)
|
||||
input_el.clear()
|
||||
input_el.fill(order_no)
|
||||
|
||||
def _click_search(self):
|
||||
"""点击查询按钮。"""
|
||||
"""点击查询按钮,并等待列表进入稳定态(数据行或空态)。"""
|
||||
btn = self.page.locator("button:has-text('查询')").first
|
||||
self.wait_visible(btn)
|
||||
btn.click()
|
||||
self.page.wait_for_timeout(1000)
|
||||
self._wait_table_ready()
|
||||
|
||||
def _wait_table_ready(self, timeout_ms: int = 10_000):
|
||||
"""等待查询结果渲染稳定: 出现可见数据行或"暂无数据"空态。
|
||||
|
||||
空态类名因 vxe-table 版本而异,超时不视为失败——
|
||||
由调用方通过 _table_has_data 按需判断。
|
||||
"""
|
||||
try:
|
||||
self.page.wait_for_selector(self._TABLE_READY_SELECTOR, timeout=timeout_ms)
|
||||
except Exception:
|
||||
logger.warning(f"等待列表稳定超时({timeout_ms}ms),继续按原逻辑执行")
|
||||
|
||||
def _table_has_data(self) -> bool:
|
||||
"""检查查询结果是否有数据。"""
|
||||
"""检查查询结果是否有可见数据行(在 _click_search 之后调用)。
|
||||
|
||||
用 :visible 避开 vxe-table 固定列渲染的隐藏占位单元格;
|
||||
数据未就绪时兜底再等 3s(兼容空态类名不匹配的情况)。
|
||||
"""
|
||||
if self.page.locator("table tbody tr td:visible").count() > 0:
|
||||
return True
|
||||
try:
|
||||
self.page.wait_for_selector("table tbody tr", timeout=3_000)
|
||||
self.page.wait_for_selector("table tbody tr td:visible", timeout=3_000)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -62,6 +96,7 @@ class FreightAccountingPage(BasePage):
|
||||
"""点击表格固定列操作按钮(纯图标 <i> 按钮,selector 文档路径 + fallback)。
|
||||
|
||||
vxe-table 操作列按钮结构: td > div > button:nth-child(N) > span > i
|
||||
点击后显式等待操作弹窗出现。
|
||||
"""
|
||||
# 精确路径(selector 文档原始定位)
|
||||
sel = (
|
||||
@@ -79,17 +114,14 @@ class FreightAccountingPage(BasePage):
|
||||
|
||||
for attempt in range(3):
|
||||
try:
|
||||
# 等待可见的数据单元格出现。vxe-table 固定列会渲染多个隐藏的重复
|
||||
# 单元格 (class="...fixed--hidden"),必须用 :visible 钉住真正可见的
|
||||
# 数据行,否则 wait_for_selector 会卡在第一个隐藏单元格上超时。
|
||||
self.page.wait_for_selector("table tbody tr td:visible", timeout=8_000)
|
||||
self.page.wait_for_timeout(300)
|
||||
self.page.wait_for_timeout(300) # 行渲染后按钮绑定有小延迟, 短暂 settle
|
||||
btn = self.page.locator(sel).first
|
||||
if btn.count() == 0:
|
||||
btn = self.page.locator(sel_fallback).first
|
||||
self.wait_visible(btn, timeout_ms=5_000)
|
||||
btn.click()
|
||||
self.page.wait_for_timeout(800)
|
||||
self.wait_visible(self._modal(), timeout_ms=10_000)
|
||||
return
|
||||
except Exception:
|
||||
if attempt == 2:
|
||||
@@ -107,7 +139,7 @@ class FreightAccountingPage(BasePage):
|
||||
modal = self._modal()
|
||||
self.wait_visible(modal)
|
||||
modal.evaluate("el => el.scrollTop = el.scrollHeight")
|
||||
self.page.wait_for_timeout(500)
|
||||
self.page.wait_for_timeout(300) # 滚动触发底部按钮渲染的 settle
|
||||
|
||||
def _retry_click_modal_btn(self, locator, timeout_ms: int = 5_000,
|
||||
retries: int = 3, wait_after: int = 1500):
|
||||
@@ -124,6 +156,18 @@ class FreightAccountingPage(BasePage):
|
||||
raise
|
||||
self.page.wait_for_timeout(1_000)
|
||||
|
||||
def _try_click(self, desc: str, click_fn, timeout_ms: int = 3_000,
|
||||
wait_after: int = 300):
|
||||
"""可选弹窗内操作:失败时记录日志并跳过(不静默吞掉)。
|
||||
|
||||
页面部分控件(银行单选/手机号单选/验证码)仅在特定单据状态下展示,
|
||||
缺失属正常分支,但跳过必须留痕。
|
||||
"""
|
||||
try:
|
||||
self._retry_click_modal_btn(click_fn(), timeout_ms=timeout_ms, wait_after=wait_after)
|
||||
except Exception as e:
|
||||
logger.warning(f"可选操作 '{desc}' 未完成,跳过: {type(e).__name__}")
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 运费核算
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@@ -187,9 +231,8 @@ class FreightAccountingPage(BasePage):
|
||||
).first
|
||||
self.wait_visible(selector_input, timeout_ms=5_000)
|
||||
selector_input.click()
|
||||
self.page.wait_for_timeout(800)
|
||||
|
||||
# Step 2: 在下拉列表中点击车队长选项(取最后一个下拉弹窗,它是最新渲染的)
|
||||
# Step 2: 等待下拉列表渲染后点击车队长选项(取最后一个下拉弹窗,它是最新渲染的)
|
||||
team_leader_option = self.page.locator(
|
||||
"body > div.el-select-dropdown.el-popper "
|
||||
"> div.el-scrollbar > div.el-select-dropdown__wrap.el-scrollbar__wrap "
|
||||
@@ -197,8 +240,6 @@ class FreightAccountingPage(BasePage):
|
||||
).nth(-1)
|
||||
self.wait_visible(team_leader_option, timeout_ms=5_000)
|
||||
team_leader_option.click()
|
||||
# 等待车队长选择完成后,收款模式下拉框解除 disabled
|
||||
self.page.wait_for_timeout(1200)
|
||||
|
||||
# Step 3: 等待收款模式 el-select 解除 disabled 后再点击
|
||||
# Element UI 的 disabled 加在 el-select 外层的 div 上,不是 input 本身
|
||||
@@ -220,9 +261,8 @@ class FreightAccountingPage(BasePage):
|
||||
else:
|
||||
raise RuntimeError("收款模式下拉框始终未解除 disabled,可能车队长选择未生效")
|
||||
payment_input.click()
|
||||
self.page.wait_for_timeout(800)
|
||||
|
||||
# Step 4: 在下拉列表中点击"车队长全额收款"
|
||||
# Step 4: 等待下拉项渲染后点击"车队长全额收款"
|
||||
full_payment_option = self.page.locator(
|
||||
"body > div.el-select-dropdown.el-popper "
|
||||
"> div.el-scrollbar > div.el-select-dropdown__wrap.el-scrollbar__wrap "
|
||||
@@ -230,7 +270,7 @@ class FreightAccountingPage(BasePage):
|
||||
).nth(-1)
|
||||
self.wait_visible(full_payment_option, timeout_ms=5_000)
|
||||
full_payment_option.click()
|
||||
self.page.wait_for_timeout(800)
|
||||
self.page.wait_for_timeout(500) # 下拉选中后表单联动的 settle
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 运单审核
|
||||
@@ -270,35 +310,32 @@ class FreightAccountingPage(BasePage):
|
||||
self._scroll_modal()
|
||||
|
||||
# 勾选光大银行 (selector 文档 Step 20)
|
||||
try:
|
||||
radio = self._modal().locator(
|
||||
def _click_guangda_radio():
|
||||
return self._modal().locator(
|
||||
"form > div:nth-child(9) > div > div:nth-child(2) "
|
||||
"> label > span.el-radio__input > span"
|
||||
).first
|
||||
self._retry_click_modal_btn(radio, timeout_ms=3_000, wait_after=300)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._try_click("勾选光大银行", _click_guangda_radio)
|
||||
|
||||
# 勾选手机号 (selector 文档 Step 21)
|
||||
try:
|
||||
radio = self.page.locator(
|
||||
def _click_phone_radio():
|
||||
return self.page.locator(
|
||||
"[id^='el-collapse-content-'] > div > div:nth-child(3) "
|
||||
"> label > span.el-radio__input > span"
|
||||
).first
|
||||
self._retry_click_modal_btn(radio, timeout_ms=3_000, wait_after=300)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._try_click("勾选手机号", _click_phone_radio)
|
||||
|
||||
# 获取验证码 (selector 文档 Step 22)
|
||||
try:
|
||||
btn = self._modal().locator(
|
||||
def _click_get_code():
|
||||
return self._modal().locator(
|
||||
"form > div.el-row > div.el-col.el-col-4 button span"
|
||||
).first
|
||||
self._retry_click_modal_btn(btn, timeout_ms=3_000, wait_after=500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 输入验证码 (selector 文档 Step 23)
|
||||
self._try_click("获取验证码", _click_get_code, wait_after=500)
|
||||
|
||||
# 输入验证码 (selector 文档 Step 23) — 无验证码输入框时记录并跳过
|
||||
try:
|
||||
inp = self._modal().locator(
|
||||
"form > div.el-row > div.el-col.el-col-20 input"
|
||||
@@ -306,10 +343,10 @@ class FreightAccountingPage(BasePage):
|
||||
self.wait_visible(inp)
|
||||
inp.clear()
|
||||
inp.fill(verify_code)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(f"可选操作 '输入验证码' 未完成,跳过: {type(e).__name__}")
|
||||
|
||||
# 确认打款 (selector 文档 Step 24)
|
||||
# 确认打款 (selector 文档 Step 24) — 关键步骤, 失败必须抛出
|
||||
btn = self._modal().locator(
|
||||
"form > div:nth-child(12) > div > button span"
|
||||
).first
|
||||
@@ -330,26 +367,24 @@ class FreightAccountingPage(BasePage):
|
||||
self._click_table_action(button_index=2)
|
||||
|
||||
# 勾选光大 (selector 文档 Step 31)
|
||||
try:
|
||||
radio = self._modal().locator(
|
||||
def _click_guangda_label():
|
||||
return self._modal().locator(
|
||||
"form > div > div:nth-child(3) > div > div > div:nth-child(3) "
|
||||
"> label > span.el-radio__label"
|
||||
).first
|
||||
self._retry_click_modal_btn(radio, timeout_ms=3_000, wait_after=300)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 确认打款 (selector 文档 Step 31-2)
|
||||
self._try_click("勾选光大", _click_guangda_label)
|
||||
|
||||
# 确认打款 (selector 文档 Step 31-2) — 关键步骤, 失败必须抛出
|
||||
btn = self._modal().locator("> div > div > button > span").first
|
||||
self._retry_click_modal_btn(btn, wait_after=1000)
|
||||
|
||||
# message-box 确认 (selector 文档 Step 32)
|
||||
try:
|
||||
msg = self.page.locator(
|
||||
def _click_msgbox_confirm():
|
||||
return self.page.locator(
|
||||
"body > div.el-message-box__wrapper .el-message-box__btns "
|
||||
"> button.el-button.el-button--default.el-button--small.el-button--primary > span"
|
||||
)
|
||||
self._retry_click_modal_btn(msg, timeout_ms=5_000, wait_after=1500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._try_click("message-box 确认", _click_msgbox_confirm, timeout_ms=5_000, wait_after=1500)
|
||||
self.screenshot("shipper_payment_done")
|
||||
|
||||
Reference in New Issue
Block a user