定位健壮性原则: 语义文本 > 组件库稳定 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, 已结算, 双支付完成)。
259 lines
12 KiB
Python
259 lines
12 KiB
Python
"""司机 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()
|