Files
Yb-QaAutomationHub/tests/pages/driver/freight_flow_page.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

259 lines
12 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.
"""司机 APP 货运流程 Page Object — 细粒度操作 + 流程编排。
结构:
__init__ 定位器集中声明(与 resources/selectors/正常流程-司机.md 对应)
基础操作 显式等待封装(WebDriverWait),无常驻固定 sleep
细粒度操作 单一职责的页面操作(open_freight_tab / fill_quantity / ...)
流程编排 accept_order_flow / load_goods_flow / ... 供用例调用,仅做步骤组合
"""
import logging
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from tests.pages.base.app_base_page import AppBasePage
logger = logging.getLogger(__name__)
class DriverFreightFlowPage(AppBasePage):
"""司机端 APP - 货运完整流程(货源→接单→装货→卸货→获取单号)。"""
def __init__(self, driver, platform: str = "android"):
super().__init__(driver, platform)
# ── 底部导航 ──
# 文本锚点优先(登录流程每轮运行都在验证 text("货源") 可用),
# 位置序号兜底(0=首页, 1=货源, 2=消息, 3=我的)
self.freight_source_tab_candidates = (
(AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("货源")'),
(AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("com.arpa.ynchenggangdriver:id/fixed_bottom_navigation_icon").instance(1)'),
)
# ── 货源单列表 ──
# 锚定 RecyclerView 第一个子项(语义: 列表第一行),全局序号兜底
self.freight_order_item_candidates = (
(AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("com.arpa.ynchenggangdriver:id/lRrcyclerView")'
'.childSelector(new UiSelector().className("android.widget.RelativeLayout").instance(0))'),
(AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().className("android.widget.RelativeLayout").instance(3)'),
)
# ── 数量 ──
self.quantity_input = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/et_num")
# ── 协议勾选 ──
self.agreement_checkbox = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/check_xieyi")
# ── 接单 ──
self.accept_order_btn = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/tv_bian")
# ── 吨位 ──
self.weight_input = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/et_dun")
# ── 上传图片 ──
self.select_image_btn = (
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().className("android.widget.ImageView").instance(2)',
)
self.album_btn = (AppiumBy.ANDROID_UIAUTOMATOR, 'new UiSelector().text("相册")')
self.image_checkbox = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/cb_check")
self.done_btn = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/btn_ok")
# ── 装货/卸货确认 ──
self.load_unload_confirm = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/tv_nick")
# ── 合同 ──
self.contract_webview = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/textHtml")
self.acknowledge_btn = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/tv_affirm")
self.confirm_btn = (AppiumBy.ID, "com.arpa.ynchenggangdriver:id/ok_tv3")
# ════════════════════════════════════════════════════════════
# 基础操作(显式等待)
# ════════════════════════════════════════════════════════════
def _wait_and_click(self, locator, timeout_s: float = 10.0):
wait = WebDriverWait(self.driver, timeout=timeout_s)
wait.until(EC.visibility_of_element_located(locator))
self.driver.find_element(*locator).click()
def _wait_and_fill(self, locator, value: str, timeout_s: float = 10.0):
wait = WebDriverWait(self.driver, timeout=timeout_s)
wait.until(EC.visibility_of_element_located(locator))
el = self.driver.find_element(*locator)
el.clear()
el.send_keys(value)
def _tap_by_text(self, text: str, timeout_s: float = 10.0):
locator = (AppiumBy.ANDROID_UIAUTOMATOR, f'new UiSelector().text("{text}")')
self._wait_and_click(locator, timeout_s)
def _tap_by_text_if_present(self, text: str, timeout_s: float = 5.0):
"""可选元素点击:不出现时记录日志并跳过(不静默吞掉)。"""
try:
self._tap_by_text(text, timeout_s)
except Exception:
logger.warning(f"可选元素 '{text}' 在 {timeout_s}s 内未出现,跳过点击")
def _tap_first_matching(self, *locators, timeout_s: float = 5.0):
"""依次尝试多个候选定位器点击,第一个成功的生效;全部失败抛最后一个异常。"""
last_exc: Exception | None = None
for loc in locators:
try:
self._wait_and_click(loc, timeout_s)
return
except Exception as e:
last_exc = e
raise last_exc if last_exc else RuntimeError("无候选定位器")
def _swipe_up(self, locator=None, duration_ms: int = 800):
"""向上滑动。指定 locator 时在元素内滑动,否则全屏滑动。"""
if locator:
try:
wait = WebDriverWait(self.driver, timeout=5.0)
el = wait.until(EC.presence_of_element_located(locator))
size = el.size
loc = el.location
start_x = loc["x"] + size["width"] // 2
start_y = loc["y"] + int(size["height"] * 0.8)
end_y = loc["y"] + int(size["height"] * 0.2)
self.driver.swipe(start_x, start_y, start_x, end_y, duration_ms)
return
except Exception:
pass
# 全屏滑动
size = self.driver.get_window_size()
self.driver.swipe(
start_x=size["width"] // 2,
start_y=int(size["height"] * 0.7),
end_x=size["width"] // 2,
end_y=int(size["height"] * 0.3),
duration=duration_ms,
)
# ════════════════════════════════════════════════════════════
# 细粒度操作: 接单
# ════════════════════════════════════════════════════════════
def open_freight_tab(self):
"""切换到底部导航的货源 tab。"""
self._tap_first_matching(*self.freight_source_tab_candidates)
def select_freight_item(self):
"""选择货源单列表中的一项。"""
self._tap_first_matching(*self.freight_order_item_candidates)
def fill_quantity(self, quantity: str):
"""填写接单数量。"""
self._wait_and_fill(self.quantity_input, quantity)
def check_agreement(self):
"""勾选协议。"""
self._wait_and_click(self.agreement_checkbox)
def tap_accept_order(self):
"""点击接单按钮。"""
self._wait_and_click(self.accept_order_btn)
# ════════════════════════════════════════════════════════════
# 细粒度操作: 装货/卸货
# ════════════════════════════════════════════════════════════
def open_load_dialog(self):
"""点击"装货"入口。"""
self._tap_by_text("装货")
def open_unload_dialog(self):
"""点击"卸货"入口。"""
self._tap_by_text("卸货")
def fill_weight(self, weight: str):
"""输入吨位。"""
self._wait_and_fill(self.weight_input, weight)
def upload_photo_from_album(self):
"""上传图片: 选择图片 → 相册 → 勾选图片 → 完成。"""
self._wait_and_click(self.select_image_btn)
self._wait_and_click(self.album_btn)
self._wait_and_click(self.image_checkbox)
self._wait_and_click(self.done_btn)
def confirm_loading(self):
"""装货确认: 勾选协议 → 点击装货确认。"""
self._wait_and_click(self.agreement_checkbox)
self._wait_and_click(self.load_unload_confirm)
def confirm_unloading(self):
"""卸货确认: 点击卸货确认。"""
self._wait_and_click(self.load_unload_confirm)
def sign_contract(self):
"""签署合同: 滑动合同 webview → 已知晓(可选) → 确定(可选)。"""
self._swipe_up(self.contract_webview)
self._tap_by_text_if_present("已知晓")
self._tap_by_text_if_present("确定")
def tap_confirm_if_present(self):
"""点击"确定"弹窗按钮(可选,部分流程不弹)。"""
self._tap_by_text_if_present("确定")
# ════════════════════════════════════════════════════════════
# 查询
# ════════════════════════════════════════════════════════════
def get_transport_order_no(self, timeout_s: float = 10.0) -> str:
"""获取运输单号。从"运输单号:RHJC..."中正则提取纯单号。"""
import re
locator = (
AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().textContains("运输单号")',
)
wait = WebDriverWait(self.driver, timeout=timeout_s)
el = wait.until(EC.visibility_of_element_located(locator))
full_text = el.text
match = re.search(r"RHJC\S+", full_text)
if match:
return match.group()
return full_text.replace("运输单号:", "").replace("运输单号:", "").strip()
# ════════════════════════════════════════════════════════════
# 流程编排(供用例调用)
# ════════════════════════════════════════════════════════════
def accept_order_flow(self, quantity: str = "1"):
"""接单完整流程:货源 tab → 货源单 → 数量 → 协议 → 接单。"""
self.open_freight_tab()
self.select_freight_item()
self.fill_quantity(quantity)
self.check_agreement()
self.tap_accept_order()
def load_goods_flow(self, weight: str = "10"):
"""装货完整流程:装货 → 吨位 → 拍照上传 → 确认 → 合同。"""
self.open_load_dialog()
self.fill_weight(weight)
self.upload_photo_from_album()
self.confirm_loading()
self.sign_contract()
def unload_goods_flow(self, weight: str = "10"):
"""卸货完整流程:卸货 → 吨位 → 拍照上传 → 确认 → 确定。"""
self.open_unload_dialog()
self.fill_weight(weight)
self.upload_photo_from_album()
self.confirm_unloading()
self.tap_confirm_if_present()
def full_driver_flow(self, quantity: str = "1", load_weight: str = "10",
unload_weight: str = "10") -> str:
"""执行完整司机端流程:接单→装货→卸货→返回运输单号。"""
self.accept_order_flow(quantity)
self.load_goods_flow(load_weight)
self.unload_goods_flow(unload_weight)
return self.get_transport_order_no()