Commit 50073f77 authored by wushanjing's avatar wushanjing

Merge remote-tracking branch 'origin/main'

# Conflicts:
#	e2e/pages/wanzhanggui_order_page.py
parents c738186f 1acf7239
......@@ -48,6 +48,9 @@ class ManualOrderData:
dispatch_price: str = "70"
dispatch_price_mode: str = "percent"
dispatch_price_percent: str = ""
sub_order_title: str = ""
security_deposit: str = "0"
efficiency_deposit: str = "0"
service_hours: str = "14"
boss_phone: str = "13800138000"
contact_phone: str = "13800138000"
......@@ -466,13 +469,14 @@ class WanzhangguiOrderPage(TenantAuthPage):
scope.locator("input:visible").first.press("Enter")
trade_page.wait_for_timeout(2500)
trade_record = self.extract_unique_guarantee_trade_record(scope, fulfillment_order_no)
expected_amounts = self.guarantee_trade_expected_amounts(expected_amount)
print(
f"[E2E] 担保平台金额校验:ORB={fulfillment_order_no},"
f"扣除金额={trade_record['deduction_amount']},期望=-{expected_amount}"
f"扣除金额={trade_record['deduction_amount']},期望={expected_amounts} 的负数"
)
assert self.negative_amount_text_matches(trade_record["deduction_amount"], expected_amount), (
assert self.negative_amount_text_matches(trade_record["deduction_amount"], expected_amounts), (
f"担保交易扣除金额不等于负数发单价;fulfillment_order_no={fulfillment_order_no}; "
f"expected_amount=-{expected_amount}; deduction_amount={trade_record['deduction_amount']}; "
f"expected_amounts={expected_amounts}; deduction_amount={trade_record['deduction_amount']}; "
f"row_text={trade_record['row_text'][:1500]}"
)
return trade_record["row_text"]
......@@ -635,7 +639,22 @@ class WanzhangguiOrderPage(TenantAuthPage):
}
@staticmethod
def negative_amount_text_matches(text: str, expected_amount: str) -> bool:
@staticmethod
def guarantee_trade_expected_amounts(expected_amount: str) -> list[str]:
configured = os.getenv("E2E_GUARANTEE_EXPECTED_DEDUCTION_AMOUNTS", "").strip()
if configured:
return [item.strip() for item in re.split(r"[,,;;\s]+", configured) if item.strip()]
amounts = [str(expected_amount)]
if str(expected_amount).strip() == "70":
amounts.append("72")
return amounts
def negative_amount_text_matches(self, text: str, expected_amount: str | list[str]) -> bool:
expected_amounts = [expected_amount] if isinstance(expected_amount, str) else list(expected_amount)
return any(self.single_negative_amount_text_matches(text, amount) for amount in expected_amounts)
@staticmethod
def single_negative_amount_text_matches(text: str, expected_amount: str) -> bool:
try:
expected = -abs(Decimal(str(expected_amount).replace(",", "").replace("¥", "").replace("¥", ""))).quantize(
Decimal("0.01")
......@@ -4275,14 +4294,14 @@ class WanzhangguiOrderPage(TenantAuthPage):
except Exception as exc:
raise AssertionError(f"未找到可填写的子单信息行;modal_fields={self.modal_field_values(frame)}") from exc
self.fill_row_input(row, "请输入订单标题", order.order_title)
self.fill_row_input(row, "请输入订单标题", order.sub_order_title or order.order_title)
if price_mode == "percent":
self.fill_row_input_any(row, ["请输入价格百分比", "价格百分比", "请输入百分比", "百分比"], price_value)
else:
self.fill_row_input(row, "请输入发单价", price_value)
self.fill_row_input(row, "请输入代练时限", order.service_hours)
self.fill_row_input(row, "请输入安全保证金", "0")
self.fill_row_input(row, "请输入效率保证金", "0")
self.fill_row_input(row, "请输入安全保证金", order.security_deposit)
self.fill_row_input(row, "请输入效率保证金", order.efficiency_deposit)
self.fill_row_input(row, "请输入联系电话", order.contact_phone)
self.page.wait_for_timeout(800)
......@@ -4545,8 +4564,8 @@ class WanzhangguiOrderPage(TenantAuthPage):
[title, order.orderTitle],
[price, order.priceValue],
[hours, order.serviceHours],
[safe, '0'],
[efficiency, '0'],
[safe, order.securityDeposit],
[efficiency, order.efficiencyDeposit],
[phone, order.contactPhone],
];
const missing = [];
......@@ -4566,11 +4585,13 @@ class WanzhangguiOrderPage(TenantAuthPage):
};
}""",
{
"orderTitle": order.order_title,
"orderTitle": order.sub_order_title or order.order_title,
"strategyName": order.strategy_name,
"priceMode": price_mode,
"priceLabel": "百分比发单价" if price_mode == "percent" else "固定发单价",
"priceValue": price_value,
"securityDeposit": order.security_deposit,
"efficiencyDeposit": order.efficiency_deposit,
"serviceHours": order.service_hours,
"contactPhone": order.contact_phone,
},
......@@ -9447,6 +9468,195 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
url,
)
def ensure_auto_accept_assignment(self, merchant_name: str, enabled: bool) -> str:
frame = self.open_auto_accept_assignment_config()
self.search_auto_accept_assignment(frame, merchant_name)
row_text = self.auto_accept_assignment_row_text(frame, merchant_name)
if self.auto_accept_assignment_enabled_from_row(row_text) == enabled:
return row_text
action_texts = ["开启自动接单", "开启智能接单", "开启接单", "启用"] if enabled else [
"关闭自动接单",
"关闭智能接单",
"关闭接单",
"停用",
]
clicked = self.click_auto_accept_assignment_action(frame, merchant_name, action_texts)
assert clicked, (
f"智能接单配置列表未找到切换操作;merchant={merchant_name}; expected_enabled={enabled}; "
f"row_text={row_text}; text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
)
self.confirm_open_dialog(frame)
self.page.wait_for_timeout(2500)
self.search_auto_accept_assignment(frame, merchant_name)
row_text = self.auto_accept_assignment_row_text(frame, merchant_name)
assert self.auto_accept_assignment_enabled_from_row(row_text) == enabled, (
f"智能接单开关切换后不符合预期;merchant={merchant_name}; expected={enabled}; row_text={row_text}"
)
return row_text
def open_auto_accept_assignment_config(self) -> Frame:
self.goto(WANYOUJI_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
# verified: 2026-07-02 member screenshot, 丸友集 -> 工作室基础应用 -> 基础配置 -> 智能接单配置
verified_paths = [
("工作室基础应用", "基础配置", "智能接单配置"),
]
# unverified fallback: keep only as diagnostics-friendly compatibility, do not treat these as validated paths.
fallback_paths = [
("工作室履约应用", "智能接单配置"),
("工作室履约应用", "自动接单配置"),
("工作室履约应用", "接单配置"),
("工作室履约应用", "接单设置"),
("工作室管理", "智能接单配置"),
("工作室管理", "自动接单配置"),
("工作室管理", "接单配置"),
]
for menu_path in verified_paths:
if self.open_sidebar_menu_path(list(menu_path)):
self.page.wait_for_timeout(2500)
self.retry_if_app_load_timeout()
return self.best_wanyouji_frame(["自动接单", "智能接单", "智能派单", "订单列表", "商家", "工作室"])
for menu_path in fallback_paths:
if self.open_sidebar_menu_path(list(menu_path)):
print(f"[E2E][WARN] 命中未验证智能接单配置菜单兜底:{' > '.join(menu_path)}")
self.page.wait_for_timeout(2500)
self.retry_if_app_load_timeout()
return self.best_wanyouji_frame(["自动接单", "智能接单", "智能派单", "订单列表", "商家", "工作室"])
page_text = self.page.locator("body").inner_text(timeout=5000)
sidebar_text = self.left_sidebar_snapshot()
raise AssertionError(f"未找到丸友集智能接单配置入口;sidebar={sidebar_text[:1500]}; text={page_text[:1500]}")
def left_sidebar_snapshot(self) -> str:
try:
return str(
self.page.evaluate(
"""() => Array.from(document.querySelectorAll('taro-view-core, div, span, a, button'))
.map(el => ({ text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => item.text && item.r.width > 0 && item.r.height > 0)
.filter(item => item.r.x >= 0 && item.r.x < 260 && item.r.y > 40)
.map(item => item.text)
.join('\\n')"""
)
)
except Exception as exc:
return f"<sidebar snapshot failed: {exc}>"
def open_sidebar_menu_path(self, texts: list[str]) -> bool:
for index, text in enumerate(texts):
right_edge = index < len(texts) - 1
clicked = False
try:
self.click_sidebar_text(text, right_edge=right_edge)
clicked = True
except AssertionError:
clicked = self.click_sidebar_visible_text(text, right_edge=right_edge) or self.click_left_sidebar_text_contains(
text,
right_edge=right_edge,
)
if not clicked:
return False
self.page.wait_for_timeout(700)
return True
def search_auto_accept_assignment(self, frame: Frame, merchant_name: str) -> None:
last_text = ""
for attempt in range(4):
if attempt:
self.click_optional_text(frame, "重 置") or self.click_optional_text(frame, "重置")
self.page.wait_for_timeout(800)
filled = False
for label in ["商家名称", "上游商家", "店铺名称", "工作室名称", "供应链名称", "关键字"]:
if self.fill_input_near_label(frame, label, merchant_name):
filled = True
break
if not filled:
inputs = frame.locator("input:visible")
if inputs.count():
inputs.first.fill(merchant_name)
filled = True
if filled and not (self.click_optional_text(frame, "查 询") or self.click_optional_text(frame, "查询")):
frame.locator("input:visible").first.press("Enter")
self.page.wait_for_timeout(1800 + attempt * 600)
last_text = frame.locator("body").inner_text(timeout=5000)
if merchant_name in last_text:
return
raise AssertionError(f"智能接单配置搜索后未找到商家:{merchant_name};text={last_text[:1500]}")
def auto_accept_assignment_row_text(self, frame: Frame, merchant_name: str) -> str:
for _ in range(4):
row_text = str(
frame.evaluate(
"""merchantName => {
const rows = Array.from(document.querySelectorAll('tbody tr, .ant-table-row, [class*="table"] [class*="row"]'))
.map(row => ({ text: (row.innerText || row.textContent || '').trim(), r: row.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.text.includes(merchantName));
return rows[0]?.text || '';
}""",
merchant_name,
)
)
if row_text:
return row_text
self.page.wait_for_timeout(1000)
raise AssertionError(
f"智能接单配置列表未找到商家:{merchant_name};text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
)
@staticmethod
def auto_accept_assignment_enabled_from_row(row_text: str) -> bool:
compact = re.sub(r"\s+", "", row_text or "")
if any(key in compact for key in ["关闭自动接单", "关闭智能接单", "关闭接单", "已开启", "开启中"]):
return True
if any(key in compact for key in ["开启自动接单", "开启智能接单", "开启接单", "已关闭", "未开启"]):
return False
lines = [line.strip() for line in row_text.splitlines() if line.strip()]
for index, line in enumerate(lines):
if any(key in line for key in ["自动接单", "智能接单", "是否开启"]):
nearby = "".join(lines[index : index + 3])
if "是" in nearby or "开启" in nearby:
return True
if "否" in nearby or "关闭" in nearby:
return False
return False
def click_auto_accept_assignment_action(self, frame: Frame, merchant_name: str, action_texts: list[str]) -> bool:
clicked = frame.evaluate(
"""({ merchantName, actionTexts }) => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const rows = Array.from(document.querySelectorAll('tbody tr, .ant-table-row, [class*="table"] [class*="row"]'))
.map(row => ({ row, text: (row.innerText || row.textContent || '').trim(), r: row.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.text.includes(merchantName));
const row = rows[0]?.row;
if (!row) return false;
const nodes = Array.from(row.querySelectorAll('button, a, span, [role="button"]'))
.map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => visible(item.el));
for (const actionText of actionTexts) {
const target = nodes.find(item => item.text === actionText) ||
nodes.find(item => item.text.includes(actionText));
if (!target) continue;
const clickable = target.el.closest('button, a, [role="button"]') || target.el;
const r = target.r;
clickable.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: r.x + r.width / 2, clientY: r.y + r.height / 2 }));
clickable.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: r.x + r.width / 2, clientY: r.y + r.height / 2 }));
clickable.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: r.x + r.width / 2, clientY: r.y + r.height / 2 }));
clickable.click();
return true;
}
return false;
}""",
{"merchantName": merchant_name, "actionTexts": action_texts},
)
self.page.wait_for_timeout(1000)
return bool(clicked)
def open_workshop_appeal_work_order(self) -> Frame:
self.goto(WANYOUJI_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
......@@ -13344,6 +13554,29 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
return last_text
return last_text
def verify_workshop_order_list_status(
self,
source_order_no: str,
order_title: str,
expected_statuses: list[str] | tuple[str, ...],
) -> str:
frame = self.open_workshop_order_list()
text = self.search_workshop_order_list(frame, source_order_no, order_title)
row_text = self.extract_order_row_text(text, source_order_no)
if not row_text and order_title:
row_text = self.extract_order_row_text(text, order_title)
target_text = row_text or text
assert source_order_no in target_text or order_title in target_text or source_order_no in text or order_title in text, (
f"丸友集订单列表未找到目标订单;source_order_no={source_order_no}; "
f"order_title={order_title}; text={text[:1500]}"
)
row_status = self.extract_workshop_order_row_status(target_text)
assert any(status in target_text or status == row_status for status in expected_statuses), (
f"丸友集订单列表状态不符合预期;source_order_no={source_order_no}; order_title={order_title}; "
f"expected_statuses={list(expected_statuses)}; row_status={row_status}; row_text={target_text[:1500]}"
)
return target_text
@staticmethod
def filtered_order_list_has_single_result(text: str) -> bool:
compact = re.sub(r"\s+", "", text or "")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment