Commit fba5321b authored by wushanjing's avatar wushanjing

解决lujuan自动接单合并冲突

parents 5009d8b8 14cfa621
......@@ -11,7 +11,7 @@ from pathlib import Path
from playwright.sync_api import Frame, Page, expect
from e2e.pages.tenant_auth_page import TenantAuthPage
from e2e.pages.tenant_auth_page import ENTERPRISE_CONSOLE_URL, TenantAuthPage
from e2e.pages.wanzhanggui_push_page import WANZHANGGUI_CONSOLE_URL
from e2e.utils.bug_reporter import BugReporter
from e2e.utils.image_captcha import ImageCaptchaSolver, decode_data_image
......@@ -22,6 +22,8 @@ WANYOUJI_WORKSHOP_ORDER_LIST_URL = "https://test-orbit.lianlianwz.com/order"
WANZHANGGUI_ORDER_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/orderList"
WANZHANGGUI_MAIN_APPEAL_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/orderAppeal"
WANZHANGGUI_SUB_ORDER_PENALTY_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/subOrderPunish"
WANZHANGGUI_SUPPLY_AUTH_URL = "https://test-futong-pc.lianlianwz.com/merchantSupplyAuth"
GUARANTEE_PLATFORM_FUND_URL = "https://test-merchant-lianlianyun.lianlianwz.com/staticMerchant?menu=fund"
ORDER_SOURCE_SNAPSHOT_PATH = Path("runtime/order_source_options.json")
ORDER_SOURCE_HISTORICAL_EVIDENCE = [
"runtime/order_probe/05_点击_手工录单_iframe.txt",
......@@ -296,6 +298,388 @@ class WanzhangguiOrderPage(TenantAuthPage):
if closed:
self.page.wait_for_timeout(800)
def open_workshop_authorization(self) -> Frame:
self.goto(WANZHANGGUI_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
self.click_sidebar_text("履约工作室/平台", right_edge=True)
self.page.wait_for_timeout(800)
self.click_sidebar_text("工作室管理", right_edge=True)
self.page.wait_for_timeout(800)
self.click_sidebar_text("工作室授权")
self.page.wait_for_timeout(2500)
return self.futong_frame_by_path("/merchantSupplyAuth", target_url=WANZHANGGUI_SUPPLY_AUTH_URL)
def futong_frame_by_path(self, path: str, target_url: str | None = None) -> Frame:
if target_url:
for frame in self.page.frames:
if "test-futong-pc" in frame.url:
if path not in frame.url:
frame.goto(target_url, wait_until="domcontentloaded")
self.page.wait_for_timeout(1500)
return frame
for frame in self.page.frames:
if "test-futong-pc" in frame.url and path in frame.url:
return frame
urls = [frame.url for frame in self.page.frames]
raise AssertionError(f"未找到丸掌柜业务 iframe {path},当前 frames:{urls}")
def ensure_workshop_guarantee(self, workshop_name: str, enabled: bool) -> str:
frame = self.open_workshop_authorization()
self.search_workshop_authorization(frame, workshop_name)
row_text = self.workshop_authorization_row_text(frame, workshop_name)
if self.guarantee_enabled_from_row(row_text) == enabled:
return row_text
action_texts = ["开启担保", "启用担保"] if enabled else ["关闭担保", "停用担保"]
clicked = self.click_workshop_authorization_action(frame, workshop_name, action_texts)
assert clicked, (
f"工作室授权列表未找到{action_texts[0]}操作;workshop={workshop_name}; row_text={row_text}; "
f"text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
)
self.confirm_open_dialog(frame)
self.page.wait_for_timeout(2500)
self.search_workshop_authorization(frame, workshop_name)
row_text = self.workshop_authorization_row_text(frame, workshop_name)
assert self.guarantee_enabled_from_row(row_text) == enabled, (
f"工作室担保状态切换后不符合预期;workshop={workshop_name}; expected={enabled}; row_text={row_text}"
)
return row_text
def search_workshop_authorization(self, frame: Frame, workshop_name: str) -> None:
last_text = ""
for attempt in range(4):
if attempt:
self.click_optional_frame_text(frame, "重 置") or self.click_optional_frame_text(frame, "重置")
self.page.wait_for_timeout(800)
filled = self.fill_input_near_label(frame, "供应链名称", workshop_name) or self.fill_input_near_label(
frame,
"工作室名称",
workshop_name,
)
if filled and not (self.click_optional_frame_text(frame, "查 询") or self.click_optional_frame_text(frame, "查询")):
inputs = frame.locator("input:visible")
if inputs.count():
inputs.first.press("Enter")
self.page.wait_for_timeout(2000 + attempt * 700)
last_text = frame.locator("body").inner_text(timeout=5000)
if workshop_name in last_text:
return
raise AssertionError(f"工作室授权列表搜索后未找到工作室:{workshop_name};text={last_text[:1500]}")
def workshop_authorization_row_text(self, frame: Frame, workshop_name: str) -> str:
for _ in range(4):
row_text = str(
frame.evaluate(
"""workshopName => {
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(workshopName));
return rows[0]?.text || '';
}""",
workshop_name,
)
)
if row_text:
return row_text
self.page.wait_for_timeout(1000)
raise AssertionError(
f"工作室授权列表未找到工作室:{workshop_name};"
f"text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
)
@staticmethod
def guarantee_enabled_from_row(row_text: str) -> bool:
if "关闭担保" in row_text:
return True
if "开启担保" in row_text:
return False
lines = [line.strip() for line in row_text.splitlines() if line.strip()]
for index, line in enumerate(lines):
if "是否开启担保" in line and index + 1 < len(lines):
return lines[index + 1] == "是"
return "是" in lines and "否" not in lines
def click_workshop_authorization_action(self, frame: Frame, workshop_name: str, action_texts: list[str]) -> bool:
clicked = frame.evaluate(
"""({ workshopName, actionTexts }) => {
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(workshopName));
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 => item.r.width > 0 && item.r.height > 0);
for (const actionText of actionTexts) {
const target = nodes.find(item => item.text === actionText) ||
nodes.find(item => item.text.includes(actionText));
if (target) {
const clickable = target.el.closest('button, a, [role="button"]') || target.el;
clickable.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
clickable.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
clickable.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
clickable.click();
return true;
}
}
return false;
}""",
{"workshopName": workshop_name, "actionTexts": action_texts},
)
self.page.wait_for_timeout(1000)
return bool(clicked)
def verify_guarantee_trade_amount(self, fulfillment_order_no: str, expected_amount: str) -> str:
assert fulfillment_order_no.startswith("ORB") and "..." not in fulfillment_order_no, (
f"关联履约单号必须是完整 ORB 单号;fulfillment_order_no={fulfillment_order_no}"
)
print(f"[E2E] 进入企业管理-担保交易详情,准备在担保平台按ORB查询:{fulfillment_order_no}")
trade_page = self.open_guarantee_trade_detail_page()
scope = self.guarantee_trade_scope(trade_page)
filled = (
self.fill_input_by_labels(scope, ["关联履约单号", "关联履单单号", "关联履约单"], fulfillment_order_no)
or self.fill_input_near_label(scope, "关联履约单号", fulfillment_order_no)
or self.fill_input_near_label(scope, "关联履单单号", fulfillment_order_no)
)
assert filled, (
f"担保交易详情未找到关联履约/履单单号输入框;fulfillment_order_no={fulfillment_order_no}; "
f"text={scope.locator('body').inner_text(timeout=5000)[:1500]}"
)
if not (self.click_optional_frame_text(scope, "查 询") or self.click_optional_frame_text(scope, "查询")):
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)
print(
f"[E2E] 担保平台金额校验:ORB={fulfillment_order_no},"
f"扣除金额={trade_record['deduction_amount']},期望=-{expected_amount}"
)
assert self.negative_amount_text_matches(trade_record["deduction_amount"], expected_amount), (
f"担保交易扣除金额不等于负数发单价;fulfillment_order_no={fulfillment_order_no}; "
f"expected_amount=-{expected_amount}; deduction_amount={trade_record['deduction_amount']}; "
f"row_text={trade_record['row_text'][:1500]}"
)
return trade_record["row_text"]
def open_guarantee_trade_detail_page(self) -> Page:
self.goto(ENTERPRISE_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
try:
self.click_sidebar_text("企业概览")
except AssertionError:
assert self.click_page_text("企业概览"), (
f"未找到企业概览入口;text={self.page.locator('body').inner_text(timeout=5000)[:1500]}"
)
self.page.wait_for_timeout(2000)
before_pages = set(self.page.context.pages)
clicked = (
self.click_page_text("担保交易详情 >")
or self.click_page_text("担保交易详情")
or self.click_any_frame_text("担保交易详情 >")
or self.click_any_frame_text("担保交易详情")
)
trade_page = self.new_page_after_click(before_pages) or self.page
if not clicked and trade_page is self.page:
body_text = self.page.locator("body").inner_text(timeout=5000)
assert "企业概览" in body_text, f"企业概览未加载完成,无法进入担保交易详情;text={body_text[:1500]}"
trade_page.wait_for_load_state("domcontentloaded")
trade_page.wait_for_timeout(2500)
if not self.guarantee_trade_scope_or_none(trade_page):
print("[E2E] 未捕获到担保平台新页,直接进入担保平台资金管理页")
trade_page.goto(GUARANTEE_PLATFORM_FUND_URL, wait_until="domcontentloaded")
trade_page.wait_for_timeout(3000)
return trade_page
def new_page_after_click(self, before_pages: set[Page], timeout_ms: int = 5000) -> Page | None:
deadline = datetime.now().timestamp() + timeout_ms / 1000
while datetime.now().timestamp() < deadline:
pages = [page for page in self.page.context.pages if page not in before_pages]
if pages:
return pages[-1]
self.page.wait_for_timeout(300)
return None
def guarantee_trade_scope(self, trade_page: Page) -> Page | Frame:
scope = self.guarantee_trade_scope_or_none(trade_page)
if scope:
return scope
raise AssertionError(
f"担保交易详情页面未找到包含关联履约/履单单号的查询区域;url={trade_page.url}; "
f"text={trade_page.locator('body').inner_text(timeout=5000)[:1500]}"
)
def guarantee_trade_scope_or_none(self, trade_page: Page) -> Page | Frame | None:
for _ in range(15):
for scope in [trade_page, *[frame for frame in trade_page.frames if frame.url != "about:blank"]]:
try:
text = scope.locator("body").inner_text(timeout=1500)
except Exception:
continue
if (
("关联履约单号" in text or "关联履单单号" in text)
and ("金额" in text or "资金管理" in text or "担保交易" in text)
):
return scope
trade_page.wait_for_timeout(1000)
return None
def fill_input_by_labels(self, scope: Page | Frame, label_texts: list[str], value: str) -> bool:
return bool(
scope.evaluate(
"""({ labelTexts, value }) => {
const normalize = text => (text || '').replace(/\\s+/g, '').replace(/[::]/g, '').trim();
const expected = labelTexts.map(normalize);
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
let input = Array.from(document.querySelectorAll('input:not([disabled]), textarea:not([disabled])'))
.find(el => expected.some(text => normalize(el.getAttribute('placeholder') || '').includes(text))) || null;
if (!input) {
const labels = Array.from(document.querySelectorAll('label, span, div, taro-view-core'))
.map(el => ({ el, text: normalize(el.innerText || el.textContent || ''), r: el.getBoundingClientRect() }))
.filter(item => visible(item.el))
.filter(item => expected.some(text => item.text === text || item.text.includes(text)))
.sort((a, b) => a.r.y - b.r.y || a.r.x - b.r.x);
const label = labels[0];
if (label) {
input = Array.from(document.querySelectorAll('input:not([disabled]), textarea:not([disabled])'))
.map(el => ({ el, r: el.getBoundingClientRect() }))
.filter(item => visible(item.el))
.filter(item => Math.abs(item.r.y - label.r.y) < 72 && item.r.x >= label.r.x)
.sort((a, b) => Math.abs(a.r.y - label.r.y) - Math.abs(b.r.y - label.r.y) || a.r.x - b.r.x)[0]?.el || null;
}
}
if (!input) return false;
input.focus();
const proto = input.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(input, value);
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
return true;
}""",
{"labelTexts": label_texts, "value": value},
)
)
def extract_unique_guarantee_trade_row_text(self, scope: Page | Frame, fulfillment_order_no: str) -> str:
return self.extract_unique_guarantee_trade_record(scope, fulfillment_order_no)["row_text"]
def extract_unique_guarantee_trade_record(self, scope: Page | Frame, fulfillment_order_no: str) -> dict[str, str]:
matches = list(
scope.evaluate(
"""fulfillmentOrderNo => {
const normalize = text => String(text || '').replace(/\\s+/g, '').trim();
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const headerCells = Array.from(document.querySelectorAll('thead th, .ant-table-thead th, [role="columnheader"]'))
.map((el, index) => ({ index, text: normalize(el.innerText || el.textContent || ''), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0);
const amountIndex = headerCells.find(item => item.text.includes('扣除金额'))?.index ??
headerCells.find(item => item.text.includes('金额(元)'))?.index ??
headerCells.find(item => item.text.includes('金额'))?.index ??
-1;
const rows = Array.from(document.querySelectorAll('tbody tr, .ant-table-row, [role="row"], [class*="table"] [class*="row"]'))
.map(row => {
const cells = Array.from(row.querySelectorAll('td, .ant-table-cell, [role="cell"]'))
.map(cell => (cell.innerText || cell.textContent || '').replace(/\\s+/g, ' ').trim());
const text = (row.innerText || row.textContent || '').replace(/\\s+/g, ' ').trim();
const r = row.getBoundingClientRect();
const deductionAmount = amountIndex >= 0 && amountIndex < cells.length ? cells[amountIndex] : '';
return { text, deductionAmount, amountIndex, cells, r };
})
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.text.includes(fulfillmentOrderNo));
return rows.map(item => ({
row_text: item.text,
deduction_amount: item.deductionAmount,
amount_index: String(item.amountIndex),
cells: item.cells
}));
}""",
fulfillment_order_no,
)
)
assert len(matches) == 1, (
f"担保交易详情未按关联履约单号精确匹配到唯一记录;"
f"fulfillment_order_no={fulfillment_order_no}; match_count={len(matches)}; matches={matches[:5]}"
)
record = dict(matches[0])
assert record.get("deduction_amount"), (
f"担保交易详情唯一记录未提取到扣除金额/金额(元)列;"
f"fulfillment_order_no={fulfillment_order_no}; record={record}"
)
return {
"row_text": str(record.get("row_text", "")),
"deduction_amount": str(record.get("deduction_amount", "")),
}
@staticmethod
def negative_amount_text_matches(text: str, expected_amount: str) -> bool:
try:
expected = -abs(Decimal(str(expected_amount).replace(",", "").replace("¥", "").replace("¥", ""))).quantize(
Decimal("0.01")
)
except InvalidOperation:
return False
normalized = (
text.replace(",", "")
.replace("\u2212", "-")
.replace("\u2013", "-")
.replace("\u2014", "-")
.replace("-", "-")
.replace("﹣", "-")
)
for match in re.finditer(r"(?<!\d)([-+])\s*[¥¥]?\s*(\d+(?:\.\d{1,2})?)(?!\d)", normalized):
try:
value = Decimal(f"{match.group(1)}{match.group(2)}").quantize(Decimal("0.01"))
except InvalidOperation:
continue
if value == expected:
return True
amount = f"{abs(expected):.2f}"
return any(candidate in normalized for candidate in [f"-{amount}", f"-¥{amount}", f"-¥{amount}", f"¥-{amount}", f"¥-{amount}"])
def click_page_text(self, text: str) -> bool:
clicked = self.page.evaluate(
"""text => {
const nodes = Array.from(document.querySelectorAll('button, a, span, div, taro-view-core'))
.map(el => ({ el, value: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.value === text || item.value.includes(text))
.sort((a, b) => {
const aExact = a.value === text ? 0 : 1;
const bExact = b.value === text ? 0 : 1;
if (aExact !== bExact) return aExact - bExact;
return (a.r.width * a.r.height) - (b.r.width * b.r.height);
});
const target = nodes[0]?.el;
if (!target) return false;
const clickable = target.closest('button, a, [role="button"]') || target;
clickable.click();
return true;
}""",
text,
)
self.page.wait_for_timeout(1000)
return bool(clicked)
def click_any_frame_text(self, text: str) -> bool:
for frame in self.page.frames:
if frame.url == "about:blank":
continue
try:
if self.click_optional_frame_text(frame, text):
return True
except Exception:
continue
return False
def create_manual_order(self, order: ManualOrderData) -> None:
frame = self.open_order_management()
self.click_frame_button(frame, "手工录单")
......@@ -388,6 +772,301 @@ class WanzhangguiOrderPage(TenantAuthPage):
self.record_order_submit_errors(order)
return last_text
def refresh_order_list_and_get_row_text(self, source_order_no: str, order_title: str) -> str:
frame = self.open_order_management()
last_text = ""
for attempt in range(5):
self.search_order_by_best_identity(frame, source_order_no=source_order_no, order_title=order_title)
self.page.wait_for_timeout(1500)
last_text = frame.locator("body").inner_text(timeout=5000)
if source_order_no in last_text or order_title in last_text:
return self.extract_order_list_row_text(frame, source_order_no, order_title)
if attempt == 2 and order_title:
self.search_order_by_best_identity(frame, order_title=order_title)
self.page.wait_for_timeout(1500)
last_text = frame.locator("body").inner_text(timeout=5000)
if source_order_no in last_text or order_title in last_text:
return self.extract_order_list_row_text(frame, source_order_no, order_title)
self.click_optional_frame_text(frame, "刷 新") or self.click_optional_frame_text(frame, "刷新")
self.page.wait_for_timeout(1500)
raise AssertionError(
f"丸掌柜订单列表刷新后未找到订单;source_order_no={source_order_no}; "
f"order_title={order_title}; text={last_text[:1500]}"
)
def assert_order_list_status(
self,
source_order_no: str,
order_title: str,
expected_statuses: list[str],
) -> str:
row_text = self.refresh_order_list_and_get_row_text(source_order_no, order_title)
assert any(status in row_text for status in expected_statuses), (
f"丸掌柜订单列表状态不符合预期;source_order_no={source_order_no}; "
f"expected_statuses={expected_statuses}; row_text={row_text[:1500]}"
)
return row_text
def extract_order_list_row_text(self, frame: Frame, source_order_no: str, order_title: str) -> str:
return str(
frame.evaluate(
"""({ sourceOrderNo, orderTitle }) => {
const candidates = Array.from(document.querySelectorAll('tr, .ant-table-row, [class*="table"] [class*="row"]'));
const row = candidates.find(item => {
const text = item.innerText || item.textContent || '';
return text.includes(sourceOrderNo) || text.includes(orderTitle);
});
if (row) return row.innerText || row.textContent || '';
return document.body.innerText || '';
}""",
{"sourceOrderNo": source_order_no, "orderTitle": order_title},
)
)
def extract_fulfillment_order_no_from_wanzhanggui_order(self, source_order_no: str, order_title: str) -> str:
frame = self.open_order_management()
self.search_order_by_best_identity(frame, source_order_no=source_order_no, order_title=order_title)
self.page.wait_for_timeout(1500)
return self.extract_fulfillment_order_no_from_current_wanzhanggui_order_list(frame, source_order_no, order_title)
def extract_fulfillment_order_no_from_current_wanzhanggui_order_list(
self,
frame: Frame,
source_order_no: str,
order_title: str,
) -> str:
list_text = frame.locator("body").inner_text(timeout=5000)
assert source_order_no in list_text or order_title in list_text, (
f"丸掌柜订单列表未找到担保单,无法提取关联履约单号;source_order_no={source_order_no}; "
f"order_title={order_title}; text={list_text[:1500]}"
)
row_text = self.extract_order_list_row_text(frame, source_order_no, order_title)
assert "代练中" in row_text, (
f"3.1 获取ORB前置不满足:丸掌柜担保单搜索结果不是代练中;"
f"source_order_no={source_order_no}; row_text={row_text[:1500]}"
)
opened = self.open_order_detail_by_row_action(frame, source_order_no, order_title)
assert opened, (
f"丸掌柜订单列表未能点击担保单详情,无法提取关联履约单号;"
f"source_order_no={source_order_no}; row_text={row_text[:1500]}"
)
detail_text = self.wait_for_order_detail_orb_text(frame, source_order_no)
fulfillment_order_no = self.extract_orb_order_no(detail_text)
assert fulfillment_order_no, (
f"丸掌柜订单详情未提取到 ORB 开头的关联履约单号;"
f"source_order_no={source_order_no}; detail={detail_text[:1500]}"
)
return fulfillment_order_no
def wait_for_order_detail_orb_text(self, frame: Frame, source_order_no: str) -> str:
last_text = ""
for _ in range(8):
self.page.wait_for_timeout(1000)
last_text = frame.locator("body").inner_text(timeout=5000)
if "ORB" in last_text and ("订单详情" in last_text or "子单列表" in last_text or source_order_no in last_text):
return last_text
return last_text
def open_order_detail_by_row_action(self, frame: Frame, source_order_no: str, order_title: str) -> bool:
opened = self.click_order_row_main_order_link(frame, source_order_no, order_title)
if opened:
self.page.wait_for_timeout(1800)
return True
opened = self.click_order_row_action(frame, source_order_no, order_title, ["详情", "查看详情", "订单详情", "查看订单"])
if opened:
self.page.wait_for_timeout(1800)
return True
opened = self.click_current_filtered_order_detail_action(frame, source_order_no, order_title)
if opened:
self.page.wait_for_timeout(1800)
return True
self.click_order_row_action(frame, source_order_no, order_title, ["更多", "...", "操作"])
self.page.wait_for_timeout(800)
opened = self.click_visible_action_text(frame, ["详情", "查看详情", "订单详情", "查看订单"])
if not opened:
opened = self.click_last_target_order_action(frame, source_order_no, order_title)
self.page.wait_for_timeout(1800)
return opened
def click_order_row_main_order_link(self, frame: Frame, source_order_no: str, order_title: str) -> bool:
return bool(
frame.evaluate(
"""({ sourceOrderNo, orderTitle }) => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const textOf = el => (el.innerText || el.textContent || '').trim();
const rows = Array.from(document.querySelectorAll('tr, .ant-table-row, [class*="table"] [class*="row"]'))
.map(row => ({ row, text: textOf(row), r: row.getBoundingClientRect() }))
.filter(item => visible(item.row))
.filter(item =>
(sourceOrderNo && item.text.includes(sourceOrderNo)) ||
(orderTitle && item.text.includes(orderTitle))
);
const row = rows[0]?.row;
if (!row) return false;
const firstCell = row.querySelector('td, .ant-table-cell, [role="cell"], [class*="cell"]') || row;
const candidates = Array.from(firstCell.querySelectorAll('a, button, [role="button"], span, div'))
.map(el => ({ el, text: textOf(el), r: el.getBoundingClientRect(), cls: String(el.className || '') }))
.filter(item => visible(item.el))
.filter(item => item.text.includes('主单号') || /BO\\d{6,}/.test(item.text))
.sort((a, b) => a.r.x - b.r.x || a.r.y - b.r.y || a.text.length - b.text.length);
const target = candidates[0];
if (!target) return false;
const clickable = target.el.closest('a, button, [role="button"], .ant-btn, [class*="link"]') || target.el;
const r = clickable.getBoundingClientRect();
const x = r.left + Math.min(Math.max(18, r.width * 0.22), Math.max(18, r.width - 8));
const y = r.top + r.height / 2;
const pointTarget = document.elementFromPoint(x, y) || clickable;
pointTarget.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, clientX: x, clientY: y }));
pointTarget.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: x, clientY: y }));
pointTarget.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: x, clientY: y }));
pointTarget.click();
if (pointTarget !== clickable) clickable.click();
return true;
}""",
{"sourceOrderNo": source_order_no, "orderTitle": order_title},
)
)
def click_current_filtered_order_detail_action(self, frame: Frame, source_order_no: str, order_title: str) -> bool:
return bool(
frame.evaluate(
"""({ sourceOrderNo, orderTitle }) => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const textOf = el => (el.innerText || el.textContent || '').trim();
const bodyText = textOf(document.body);
if (!bodyText.includes(sourceOrderNo) && !bodyText.includes(orderTitle)) return false;
const actionTexts = ['查看详情', '订单详情', '详情'];
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"], span, div'))
.map(el => ({
el,
text: textOf(el),
r: el.getBoundingClientRect(),
tag: el.tagName.toLowerCase(),
role: el.getAttribute('role') || '',
cls: String(el.className || '')
}))
.filter(item => visible(item.el))
.filter(item => actionTexts.some(action => item.text === action || item.text.includes(action)))
.filter(item => {
let node = item.el;
for (let depth = 0; node && depth < 8; depth += 1, node = node.parentElement) {
const text = textOf(node);
if ((sourceOrderNo && text.includes(sourceOrderNo)) || (orderTitle && text.includes(orderTitle))) {
return true;
}
}
return true;
})
.sort((a, b) => {
const score = item => {
if (item.text === '查看详情') return 0;
if (item.text === '订单详情') return 1;
if (item.text === '详情') return 2;
if (item.tag === 'button' || item.tag === 'a' || item.role === 'button') return 3;
return 4;
};
return score(a) - score(b) || a.text.length - b.text.length || a.r.y - b.r.y || a.r.x - b.r.x;
});
const target = nodes[0];
if (!target) return false;
const clickable = target.el.closest('button, a, [role="button"], .ant-btn, [class*="link"]') || target.el;
const r = clickable.getBoundingClientRect();
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;
}""",
{"sourceOrderNo": source_order_no, "orderTitle": order_title},
)
)
def click_last_target_order_action(self, frame: Frame, source_order_no: str, order_title: str) -> bool:
return bool(
frame.evaluate(
"""({ sourceOrderNo, orderTitle }) => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const textOf = el => (el.innerText || el.textContent || '').trim();
const containers = Array.from(document.querySelectorAll(
'tr, .ant-table-row, [class*="table"] [class*="row"], [class*="card"], [class*="item"], [class*="list"] > div'
))
.map(el => ({ el, text: textOf(el), r: el.getBoundingClientRect() }))
.filter(item => visible(item.el))
.filter(item =>
(sourceOrderNo && item.text.includes(sourceOrderNo)) ||
(orderTitle && item.text.includes(orderTitle))
)
.sort((a, b) => (a.r.width * a.r.height) - (b.r.width * b.r.height));
const row = containers[0]?.el;
if (!row) return false;
const actionCandidates = Array.from(row.querySelectorAll('button, a, [role="button"], span, div'))
.map(el => ({
el,
text: textOf(el).replace(/\\s+/g, ''),
r: el.getBoundingClientRect(),
tag: el.tagName.toLowerCase(),
role: el.getAttribute('role') || '',
cls: String(el.className || '')
}))
.filter(item => visible(item.el))
.filter(item => item.text && item.text.length <= 12)
.filter(item => !/取消|编辑|备注|售后|撤销|删除|复制|刷新/.test(item.text))
.filter(item =>
/详情|查看|订单/.test(item.text) ||
item.tag === 'button' ||
item.tag === 'a' ||
item.role === 'button' ||
item.cls.includes('btn') ||
item.cls.includes('link')
)
.sort((a, b) => {
const score = item => {
if (/查看详情|订单详情|详情|查看订单/.test(item.text)) return 0;
if (/查看|订单/.test(item.text)) return 1;
return 2;
};
return score(a) - score(b) || b.r.x - a.r.x || b.r.y - a.r.y;
});
const target = actionCandidates[0];
if (!target) return false;
const clickable = target.el.closest('button, a, [role="button"], .ant-btn, [class*="link"]') || target.el;
const r = clickable.getBoundingClientRect();
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;
}""",
{"sourceOrderNo": source_order_no, "orderTitle": order_title},
)
)
@staticmethod
def extract_orb_order_no(text: str) -> str:
patterns = [
r"关联履约单号[::\s]*([A-Z]{2,}\d{8,})",
r"关联履单单号[::\s]*([A-Z]{2,}\d{8,})",
r"履约单号[::\s]*([A-Z]{2,}\d{8,})",
r"\b(ORB\d{8,})\b",
]
for pattern in patterns:
match = re.search(pattern, text or "")
if match:
value = match.group(1).strip()
if value.startswith("ORB") and "..." not in value:
return value
return ""
def revoke_dispatched_order(self, source_order_no: str, order_title: str) -> None:
frame = self.open_order_management()
self.search_order_if_possible(frame, source_order_no)
......@@ -697,12 +1376,66 @@ class WanzhangguiOrderPage(TenantAuthPage):
def search_order_by_labels(self, frame: Frame, keyword: str, labels: list[str]) -> bool:
for label in labels:
if self.fill_input_near_label(frame, label, keyword):
filled = self.fill_filter_input_by_label(frame, label, keyword) or self.fill_input_near_label(frame, label, keyword)
if filled:
self.click_query_button(frame)
self.page.wait_for_timeout(2000)
return True
return False
def fill_filter_input_by_label(self, frame: Frame, label_text: str, value: str) -> bool:
return bool(
frame.evaluate(
"""({ labelText, value }) => {
const normalize = text => String(text || '').replace(/\\s+/g, '');
const setValue = input => {
input.focus();
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(input, value);
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
return true;
};
const targetLabel = normalize(labelText);
const roots = Array.from(document.querySelectorAll('.ant-form-item, [class*="form-item"], .ant-row, [class*="search"], [class*="filter"], div'))
.map(el => ({ el, text: normalize(el.innerText || el.textContent || ''), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.r.y < 420)
.filter(item => item.text.includes(targetLabel))
.sort((a, b) => {
const aInput = a.el.querySelectorAll('input').length ? 0 : 1;
const bInput = b.el.querySelectorAll('input').length ? 0 : 1;
return aInput - bInput || (a.r.width * a.r.height) - (b.r.width * b.r.height);
});
for (const root of roots) {
const inputs = Array.from(root.el.querySelectorAll('input'))
.filter(input => {
const r = input.getBoundingClientRect();
return r.width > 0 && r.height > 0 && !input.disabled && input.getAttribute('type') !== 'hidden';
});
if (inputs.length) return setValue(inputs[0]);
}
const labels = Array.from(document.querySelectorAll('label, span, div'))
.map(el => ({ el, text: normalize(el.innerText || el.textContent || ''), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0 && item.r.y < 420)
.filter(item => item.text === targetLabel || item.text.includes(targetLabel))
.sort((a, b) => a.r.y - b.r.y || a.r.x - b.r.x);
const label = labels[0];
if (!label) return false;
const inputs = Array.from(document.querySelectorAll('input'))
.map(el => ({ el, r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => !item.el.disabled && item.el.getAttribute('type') !== 'hidden')
.filter(item => item.r.y >= label.r.y - 16 && item.r.y <= label.r.y + 56 && item.r.x >= label.r.x)
.sort((a, b) => Math.abs(a.r.y - label.r.y) - Math.abs(b.r.y - label.r.y) || a.r.x - b.r.x);
const input = inputs[0]?.el;
return input ? setValue(input) : false;
}""",
{"labelText": label_text, "value": value},
)
)
def click_query_button(self, frame: Frame) -> bool:
clicked = bool(
frame.evaluate(
......@@ -3396,6 +4129,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
self.page.wait_for_timeout(2500)
def fill_sub_order_row(self, frame: Frame, order: ManualOrderData) -> None:
self.select_sub_order_strategy(frame, order.strategy_name)
self.select_strategy(frame, order.strategy_name)
price_mode = self.resolve_sub_order_price_mode(order)
price_label = "百分比发单价" if price_mode == "percent" else "固定发单价"
......@@ -3407,7 +4141,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
".ant-modal:visible:has(input[placeholder*='请输入发单价']) table tbody tr, "
"body:has(input[placeholder*='请输入发单价']) table tbody tr"
).filter(has_text=order.strategy_name).first
if row.count() == 0:
if row.count() == 0 and not order.strategy_name:
row = frame.locator(
".ant-modal:visible:has(input[placeholder*='请输入发单价']) table tbody tr, "
"body:has(input[placeholder*='请输入发单价']) table tbody tr"
......@@ -3436,6 +4170,128 @@ class WanzhangguiOrderPage(TenantAuthPage):
assert not missing, f"子单信息仍有必填项未填写:{missing};modal_fields={self.modal_field_values(frame)}"
self.assert_sub_order_price_mode_ready(frame, order)
def select_sub_order_strategy(self, frame: Frame, strategy_name: str) -> None:
if not strategy_name:
return
open_result = frame.evaluate(
"""strategyName => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const modal = Array.from(document.querySelectorAll('.ant-modal'))
.filter(visible)
.reverse()
.find(el => (el.innerText || el.textContent || '').includes('请添加子单')) ||
Array.from(document.querySelectorAll('.ant-modal')).filter(visible).pop();
if (!modal) return { ok: false, reason: 'modal not found' };
const rows = Array.from(modal.querySelectorAll('table tbody tr'))
.map(row => ({ row, text: row.innerText || row.textContent || '', r: row.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0);
const row = rows[0]?.row;
if (!row) return { ok: false, reason: 'sub order row not found', text: modal.innerText || '' };
const cells = Array.from(row.querySelectorAll('td'));
let strategyIndex = 2;
const table = row.closest('table');
if (table) {
const headers = Array.from(table.querySelectorAll('thead th'))
.map((th, index) => ({ index, text: (th.innerText || th.textContent || '').replace(/\\s+/g, '') }));
const header = headers.find(item => item.text.includes('策略名称'));
if (header) strategyIndex = header.index;
}
const cell = cells[strategyIndex] || cells[2];
if (!cell) return { ok: false, reason: 'strategy cell not found', strategyIndex, text: row.innerText || '' };
const selector = cell.querySelector('.ant-select-selector') ||
cell.querySelector('.ant-select');
if (!selector) return { ok: false, reason: 'strategy selector not found', text: cell.innerText || '' };
if ((cell.innerText || cell.textContent || '').includes(strategyName)) return { ok: true, already: true };
selector.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
selector.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
selector.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
selector.click();
return { ok: true, already: false, strategyIndex, cellText: cell.innerText || cell.textContent || '' };
}""",
strategy_name,
)
assert open_result.get("ok"), (
f"未找到子单策略名称下拉;strategy={strategy_name}; result={open_result}; "
f"modal_fields={self.modal_field_values(frame)}"
)
if open_result.get("already"):
return
self.page.wait_for_timeout(800)
searched = self.search_open_select_dropdown(frame, strategy_name)
option = frame.locator(
".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
).filter(has_text=strategy_name).first
assert option.count(), (
f"子单策略名称下拉未找到目标策略:{strategy_name};searched={searched}; "
f"options={self.visible_dropdown_options(frame)}; modal_fields={self.modal_field_values(frame)}"
)
option.click(force=True)
self.page.wait_for_timeout(800)
assert self.sub_order_strategy_selected(frame, strategy_name), (
f"子单策略名称选择后不符合预期;expected={strategy_name}; modal_fields={self.modal_field_values(frame)}"
)
def search_open_select_dropdown(self, frame: Frame, keyword: str) -> bool:
searched = bool(
frame.evaluate(
"""keyword => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const dropdown = Array.from(document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)'))
.filter(visible)
.pop();
if (!dropdown) return false;
const input = Array.from(dropdown.querySelectorAll('input'))
.filter(input => visible(input) && !input.disabled && input.getAttribute('type') !== 'hidden')
.pop();
if (!input) return false;
input.focus();
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(input, keyword);
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: keyword }));
input.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}""",
keyword,
)
)
if not searched:
try:
self.page.keyboard.type(keyword, delay=20)
searched = True
except Exception:
searched = False
self.page.wait_for_timeout(800)
return searched
def sub_order_strategy_selected(self, frame: Frame, strategy_name: str) -> bool:
return bool(
frame.evaluate(
"""strategyName => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const modal = Array.from(document.querySelectorAll('.ant-modal'))
.filter(visible)
.reverse()
.find(el => (el.innerText || el.textContent || '').includes('请添加子单')) ||
Array.from(document.querySelectorAll('.ant-modal')).filter(visible).pop();
if (!modal) return false;
const rows = Array.from(modal.querySelectorAll('table tbody tr'))
.filter(row => visible(row));
const row = rows[0];
return Boolean(row && (row.innerText || row.textContent || '').includes(strategyName));
}""",
strategy_name,
)
)
def fill_embedded_sub_order_row(self, frame: Frame, order: ManualOrderData) -> bool:
price_mode = self.resolve_sub_order_price_mode(order)
price_value = self.resolve_sub_order_price_value(order)
......@@ -3467,7 +4323,14 @@ class WanzhangguiOrderPage(TenantAuthPage):
item.text.includes(order.strategyName) ||
item.text.includes('固定发单价') ||
item.text.includes('百分比发单价')
);
)
.sort((a, b) => {
const aExact = order.strategyName && a.text.includes(order.strategyName) ? 0 : 1;
const bExact = order.strategyName && b.text.includes(order.strategyName) ? 0 : 1;
const aPrice = a.text.includes(order.priceLabel) ? 0 : 1;
const bPrice = b.text.includes(order.priceLabel) ? 0 : 1;
return aExact - bExact || aPrice - bPrice || a.r.y - b.r.y;
});
const row = rows[0]?.row;
if (!row) return { ok: false, reason: 'editable row not found', text: modal.innerText };
......@@ -3834,12 +4697,53 @@ class WanzhangguiOrderPage(TenantAuthPage):
return
selector.click(force=True)
self.page.wait_for_timeout(800)
searched = self.search_open_strategy_dropdown(frame, strategy_name)
option = frame.locator(
".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option"
).filter(has_text=strategy_name).first
if option.count():
option.click(force=True)
self.page.wait_for_timeout(800)
assert option.count(), (
f"策略名称下拉未找到目标策略:{strategy_name};searched={searched}; "
f"options={self.visible_dropdown_options(frame)}; modal_fields={self.modal_field_values(frame)}"
)
option.click(force=True)
self.page.wait_for_timeout(800)
selected_text = selector.inner_text(timeout=5000)
assert strategy_name in selected_text, (
f"策略名称选择后不符合预期;expected={strategy_name}; selected={selected_text}; "
f"modal_fields={self.modal_field_values(frame)}"
)
def search_open_strategy_dropdown(self, frame: Frame, strategy_name: str) -> bool:
searched = bool(
frame.evaluate(
"""strategyName => {
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const dropdown = Array.from(document.querySelectorAll('.ant-select-dropdown:not(.ant-select-dropdown-hidden)'))
.filter(visible)
.pop();
const roots = [dropdown, document].filter(Boolean);
for (const root of roots) {
const inputs = Array.from(root.querySelectorAll('input'))
.filter(input => visible(input) && !input.disabled && input.getAttribute('type') !== 'hidden');
const input = inputs[inputs.length - 1];
if (!input) continue;
input.focus();
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(input, strategyName);
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: strategyName }));
input.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
return false;
}""",
strategy_name,
)
)
self.page.wait_for_timeout(800)
return searched
def select_first_available_if_placeholder(self, frame: Frame, text: str, field_name: str | None = None) -> None:
selectors = frame.locator(".ant-modal .ant-select-selector").filter(has_text=text)
......@@ -4480,15 +5384,37 @@ class WanzhangguiOrderPage(TenantAuthPage):
.filter(item => item.text.includes(sourceOrderNo) || item.text.includes(orderTitle));
const row = rows[0]?.row;
if (!row) return false;
const nodes = Array.from(row.querySelectorAll('button, a'))
.map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
const nodes = Array.from(row.querySelectorAll('button, a, [role="button"], span, div'))
.map(el => ({
el,
text: (el.innerText || el.textContent || '').trim(),
r: el.getBoundingClientRect(),
tag: el.tagName.toLowerCase(),
role: el.getAttribute('role') || '',
cls: String(el.className || '')
}))
.filter(item => item.r.width > 0 && item.r.height > 0)
.sort((a, b) => a.text.length - b.text.length || (a.r.width * a.r.height) - (b.r.width * b.r.height));
.filter(item => item.text)
.sort((a, b) => {
const score = item => {
if (item.tag === 'button') return 0;
if (item.tag === 'a') return 1;
if (item.role === 'button' || item.cls.includes('btn') || item.cls.includes('link')) return 2;
return 3;
};
return score(a) - score(b) || itemTextLength(a) - itemTextLength(b) || (a.r.width * a.r.height) - (b.r.width * b.r.height);
});
function itemTextLength(item) { return item.text.replace(/\\s+/g, '').length; }
for (const actionText of actionTexts) {
const target = nodes.find(item => item.text === actionText) ||
nodes.find(item => item.text.includes(actionText));
if (target) {
target.el.click();
const clickable = target.el.closest('button, a, [role="button"], .ant-btn') || target.el;
const r = clickable.getBoundingClientRect();
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;
}
}
......
from __future__ import annotations
import os
from dataclasses import dataclass
from datetime import datetime
import pytest
from playwright.sync_api import Page
from e2e.pages.wanzhanggui_order_page import ManualOrderData, WanyoujiFulfillmentPage, WanzhangguiOrderPage
from e2e.utils.account_store import account_store
@dataclass(frozen=True)
class AutoAcceptanceCase:
use_case_no: str
case_id: str
title: str
wanzhanggui_phone: str
wanyouji_phone: str
strategy_name: str
strategy_type: str
expected_scope: str
sub_order_title_suffix: str
verify_mode: str = "hall"
merchant_name: str = ""
@dataclass(frozen=True)
class GuaranteeAutoAcceptanceCase:
use_case_no: str
case_id: str
title: str
wanzhanggui_phone: str
wanyouji_phone: str
workshop_name: str
strategy_name: str
strategy_type: str
sub_order_title_suffix: str
receive_price: str = "80"
dispatch_price: str = "70"
security_deposit: str = "0"
efficiency_deposit: str = "0"
expected_wanzhanggui_statuses: tuple[str, ...] = ("待接单", "代练中")
verify_mode: str = "create"
close_guarantee_after_case: bool = False
CASES = [
# 用例1:关闭测试001自动接指派单执行1.2,再开启测试001自动接指派单执行1.1。
AutoAcceptanceCase(
use_case_no="1",
case_id="AUTO-ACCEPT-PRIVATE",
title="丸掌柜手工录单创建私单外来订单并发布到丸友集",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
strategy_name="指定-旺仔",
strategy_type="单平台指定",
expected_scope="私单",
sub_order_title_suffix="私单标识",
verify_mode="branch",
merchant_name="测试001",
),
# 用例1.2:关闭自动接指派单后创建私单,校验丸掌柜待接单且丸友集抢单大厅-私单可见。
AutoAcceptanceCase(
use_case_no="1.2",
case_id="AUTO-ACCEPT-PRIVATE-HALL",
title="私单未自动接单时丸友集私单大厅可见性验证",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
strategy_name="指定-旺仔",
strategy_type="单平台指定",
expected_scope="私单",
sub_order_title_suffix="私单标识",
verify_mode="hall",
merchant_name="测试001",
),
# 用例1.1:开启自动接指派单后创建私单,校验丸掌柜代练中且丸友集订单列表已接单。
AutoAcceptanceCase(
use_case_no="1.1",
case_id="AUTO-ACCEPT-PRIVATE-ACCEPTED",
title="私单自动接单后丸友集订单列表状态回查",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
strategy_name="指定-旺仔",
strategy_type="单平台指定",
expected_scope="私单",
sub_order_title_suffix="私单标识",
verify_mode="accepted",
merchant_name="测试001",
),
# 用例2:创建公池单,校验丸友集抢单大厅-公池单可见。
AutoAcceptanceCase(
use_case_no="2",
case_id="AUTO-ACCEPT-POOL",
title="丸掌柜手工录单创建公池单外来订单并发布到丸友集",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
strategy_name="多发测试",
strategy_type="多工作室同时分发",
expected_scope="公池单",
sub_order_title_suffix="公池单标识",
verify_mode="hall",
),
]
GUARANTEE_CASES = [
# 用例3.1:独立创建一笔担保私单;搜索到丸掌柜状态为代练中后点击详情提取ORB,再校验担保交易扣除金额;结束后不关闭担保。
GuaranteeAutoAcceptanceCase(
use_case_no="3.1",
case_id="AUTO-ACCEPT-GUARANTEE-TRADE-AMOUNT",
title="担保单代练中后进详情提取ORB并核对担保交易扣除金额",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
workshop_name="旺仔工作室",
strategy_name="指定-旺仔",
strategy_type="单平台指定",
sub_order_title_suffix="担保私单标识",
verify_mode="trade_amount",
),
# 用例3.2:检查担保已开启,若关闭则开启;独立创建一笔发单价80000的担保私单,结束后不关闭担保。
GuaranteeAutoAcceptanceCase(
use_case_no="3.2",
case_id="AUTO-ACCEPT-GUARANTEE-DISPATCH-PRICE-80000",
title="担保开启后80000发单价私单待接单并在丸友集可见",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
workshop_name="旺仔工作室",
strategy_name="指定-旺仔",
strategy_type="单平台指定",
sub_order_title_suffix="担保私单标识",
receive_price="80000",
dispatch_price="80000",
expected_wanzhanggui_statuses=("待接单",),
verify_mode="hall",
),
# 用例3.3:检查担保已开启,若关闭则开启;独立创建一笔子单保证金80000的担保私单,结束后关闭担保。
GuaranteeAutoAcceptanceCase(
use_case_no="3.3",
case_id="AUTO-ACCEPT-GUARANTEE-DEPOSIT-80000",
title="担保开启后子单保证金80000私单待接单并在丸友集可见",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
workshop_name="旺仔工作室",
strategy_name="指定-旺仔",
strategy_type="单平台指定",
sub_order_title_suffix="担保私单标识",
security_deposit="80000",
expected_wanzhanggui_statuses=("待接单",),
verify_mode="hall",
close_guarantee_after_case=True,
),
]
def create_auto_acceptance_order(
order_page: WanzhangguiOrderPage,
case: AutoAcceptanceCase,
wanzhanggui_password: str,
bug_reporter,
*,
status_id: str,
) -> dict[str, str]:
timestamp = datetime.now().strftime("%m%d%H%M%S")
source_order_no = (
os.getenv(f"E2E_{case.case_id}_SOURCE_ORDER_NO", "").strip()
or os.getenv("E2E_AUTO_ACCEPTANCE_SOURCE_ORDER_NO", "").strip()
or f"AAORDER{timestamp}"
)
order_title = (
os.getenv(f"E2E_{case.case_id}_ORDER_TITLE", "").strip()
or os.getenv("E2E_AUTO_ACCEPTANCE_ORDER_TITLE", "").strip()
or f"E2E自动接单{timestamp}"
)
if status_id:
suffix = "".join(ch for ch in status_id.upper() if ch.isalnum())[:6]
source_order_no = f"{source_order_no}{suffix}"
order_title = f"{order_title}{suffix}"
order = ManualOrderData(
source_order_no=source_order_no,
order_title=order_title,
sub_order_title=f"{order_title}{case.sub_order_title_suffix}",
strategy_name=case.strategy_name,
strategy_type=case.strategy_type,
role_name=f"AA角色{timestamp[-4:]}",
)
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_password)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 丸掌柜主账号登录",
)
order_page.create_manual_order(order)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 丸掌柜手工录单并选择策略 {case.strategy_name}: {source_order_no}",
)
row_text = ""
if case.expected_scope == "私单":
row_text = order_page.refresh_order_list_and_get_row_text(source_order_no, order_title)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 丸掌柜订单列表刷新查看私单状态 {source_order_no}",
)
return {"source_order_no": source_order_no, "order_title": order_title, "row_text": row_text}
def verify_private_order_waiting_in_hall(
fulfillment_page: WanyoujiFulfillmentPage,
case: AutoAcceptanceCase,
wanyouji_password: str,
source_order_no: str,
order_title: str,
bug_reporter,
) -> None:
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_password)
fulfillment_page.verify_order_reached_hall(source_order_no, order_title, "私单")
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集抢单大厅-私单可见 {source_order_no}",
)
def verify_private_order_accepted(
fulfillment_page: WanyoujiFulfillmentPage,
case: AutoAcceptanceCase,
wanyouji_password: str,
source_order_no: str,
order_title: str,
bug_reporter,
) -> str:
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_password)
row_text = fulfillment_page.verify_workshop_order_list_status(source_order_no, order_title, ["已接单", "接单"])
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集订单列表验证私单已接单 {source_order_no}",
)
return row_text
def mark_auto_acceptance_order_dispatched(
store,
case: AutoAcceptanceCase,
source_order_no: str,
order_title: str,
) -> None:
store.mark_order_dispatched(
wanzhanggui_phone=case.wanzhanggui_phone,
source_order_no=source_order_no,
order_title=order_title,
strategy_name=case.strategy_name,
strategy_type=case.strategy_type,
wanyouji_phones=[case.wanyouji_phone],
)
@pytest.mark.e2e_auto_acceptance
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
@pytest.mark.parametrize("case", CASES, ids=[case.case_id for case in CASES])
def test_自动接单用例_普通单发单和分支校验(
page: Page,
settings,
bug_reporter,
case: AutoAcceptanceCase,
) -> None:
store = account_store()
wanzhanggui_account = store.find(phone=case.wanzhanggui_phone)
wanyouji_account = store.find(phone=case.wanyouji_phone)
assert wanzhanggui_account is not None, f"台账中未找到丸掌柜账号 {case.wanzhanggui_phone}"
assert wanyouji_account is not None, f"台账中未找到丸友集账号 {case.wanyouji_phone}"
assert wanzhanggui_account.wanzhanggui_status == "opened", (
f"丸掌柜账号 {case.wanzhanggui_phone} 未开通丸掌柜,当前状态:{wanzhanggui_account.wanzhanggui_status}"
)
assert wanyouji_account.wanyouji_status == "opened", (
f"丸友集账号 {case.wanyouji_phone} 未开通丸友集,当前状态:{wanyouji_account.wanyouji_status}"
)
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
try:
if case.verify_mode == "branch" and case.merchant_name:
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
fulfillment_page.ensure_auto_accept_assignment(case.merchant_name, enabled=False)
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集关闭{case.merchant_name}自动接指派单",
)
waiting_order = create_auto_acceptance_order(
order_page,
case,
wanzhanggui_account.password,
bug_reporter,
status_id="case12",
)
assert "待接单" in waiting_order["row_text"], (
f"关闭自动接指派单后丸掌柜订单未保持待接单;"
f"source_order_no={waiting_order['source_order_no']}; row_text={waiting_order['row_text'][:1500]}"
)
verify_private_order_waiting_in_hall(
fulfillment_page,
case,
wanyouji_account.password,
waiting_order["source_order_no"],
waiting_order["order_title"],
bug_reporter,
)
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
fulfillment_page.ensure_auto_accept_assignment(case.merchant_name, enabled=True)
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集开启{case.merchant_name}自动接指派单",
)
accepted_order = create_auto_acceptance_order(
order_page,
case,
wanzhanggui_account.password,
bug_reporter,
status_id="case11",
)
assert "代练中" in accepted_order["row_text"], (
f"开启自动接指派单后丸掌柜订单未进入代练中;"
f"source_order_no={accepted_order['source_order_no']}; row_text={accepted_order['row_text'][:1500]}"
)
verify_private_order_accepted(
fulfillment_page,
case,
wanyouji_account.password,
accepted_order["source_order_no"],
accepted_order["order_title"],
bug_reporter,
)
mark_auto_acceptance_order_dispatched(store, case, waiting_order["source_order_no"], waiting_order["order_title"])
mark_auto_acceptance_order_dispatched(store, case, accepted_order["source_order_no"], accepted_order["order_title"])
return
if case.verify_mode == "hall" and case.merchant_name:
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
fulfillment_page.ensure_auto_accept_assignment(case.merchant_name, enabled=False)
order_info = create_auto_acceptance_order(
order_page,
case,
wanzhanggui_account.password,
bug_reporter,
status_id="case12",
)
assert "待接单" in order_info["row_text"], (
f"关闭自动接指派单后丸掌柜订单未保持待接单;"
f"source_order_no={order_info['source_order_no']}; row_text={order_info['row_text'][:1500]}"
)
verify_private_order_waiting_in_hall(
fulfillment_page,
case,
wanyouji_account.password,
order_info["source_order_no"],
order_info["order_title"],
bug_reporter,
)
mark_auto_acceptance_order_dispatched(store, case, order_info["source_order_no"], order_info["order_title"])
return
if case.verify_mode == "accepted" and case.merchant_name:
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
fulfillment_page.ensure_auto_accept_assignment(case.merchant_name, enabled=True)
order_info = create_auto_acceptance_order(
order_page,
case,
wanzhanggui_account.password,
bug_reporter,
status_id="case11",
)
assert "代练中" in order_info["row_text"], (
f"开启自动接指派单后丸掌柜订单未进入代练中;"
f"source_order_no={order_info['source_order_no']}; row_text={order_info['row_text'][:1500]}"
)
verify_private_order_accepted(
fulfillment_page,
case,
wanyouji_account.password,
order_info["source_order_no"],
order_info["order_title"],
bug_reporter,
)
mark_auto_acceptance_order_dispatched(store, case, order_info["source_order_no"], order_info["order_title"])
return
order_info = create_auto_acceptance_order(
order_page,
case,
wanzhanggui_account.password,
bug_reporter,
status_id="pool",
)
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
fulfillment_page.verify_order_reached_hall(order_info["source_order_no"], order_info["order_title"], case.expected_scope)
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集抢单大厅-{case.expected_scope} 验证订单 {order_info['source_order_no']}",
)
mark_auto_acceptance_order_dispatched(store, case, order_info["source_order_no"], order_info["order_title"])
finally:
if case.merchant_name:
try:
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
fulfillment_page.ensure_auto_accept_assignment(case.merchant_name, enabled=True)
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 收尾确认{case.merchant_name}自动接指派单开启",
)
except Exception as exc:
if bug_reporter:
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 收尾开启{case.merchant_name}自动接指派单失败:{exc}",
)
print(f"[E2E] 收尾开启{case.merchant_name}自动接指派单失败,需人工确认:{exc}")
@pytest.mark.e2e_auto_acceptance
@pytest.mark.e2e_ledger
@pytest.mark.e2e_chain
@pytest.mark.parametrize("case", GUARANTEE_CASES, ids=[case.case_id for case in GUARANTEE_CASES])
def test_自动接单用例_担保单发单和担保交易校验(
page: Page,
settings,
bug_reporter,
case: GuaranteeAutoAcceptanceCase,
) -> None:
store = account_store()
wanzhanggui_account = store.find(phone=case.wanzhanggui_phone)
wanyouji_account = store.find(phone=case.wanyouji_phone)
assert wanzhanggui_account is not None, f"台账中未找到丸掌柜账号 {case.wanzhanggui_phone}"
assert wanyouji_account is not None, f"台账中未找到丸友集账号 {case.wanyouji_phone}"
assert wanzhanggui_account.wanzhanggui_status == "opened", (
f"丸掌柜账号 {case.wanzhanggui_phone} 未开通丸掌柜,当前状态:{wanzhanggui_account.wanzhanggui_status}"
)
assert wanyouji_account.wanyouji_status == "opened", (
f"丸友集账号 {case.wanyouji_phone} 未开通丸友集,当前状态:{wanyouji_account.wanyouji_status}"
)
timestamp = datetime.now().strftime("%m%d%H%M%S")
source_order_no = (
os.getenv(f"E2E_{case.case_id}_SOURCE_ORDER_NO", "").strip()
or os.getenv("E2E_AUTO_ACCEPTANCE_SOURCE_ORDER_NO", "").strip()
or f"AAGUAR{timestamp}"
)
order_title = (
os.getenv(f"E2E_{case.case_id}_ORDER_TITLE", "").strip()
or os.getenv("E2E_AUTO_ACCEPTANCE_ORDER_TITLE", "").strip()
or f"E2E担保自动接单{timestamp}"
)
order = ManualOrderData(
source_order_no=source_order_no,
order_title=order_title,
sub_order_title=f"{order_title}{case.sub_order_title_suffix}",
strategy_name=case.strategy_name,
strategy_type=case.strategy_type,
receive_price=case.receive_price,
dispatch_price=case.dispatch_price,
security_deposit=case.security_deposit,
efficiency_deposit=case.efficiency_deposit,
role_name=f"AAG角色{timestamp[-4:]}",
)
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
try:
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 丸掌柜主账号登录",
)
order_page.ensure_workshop_guarantee(case.workshop_name, enabled=True)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 开启工作室担保 {case.workshop_name}",
)
order_page.create_manual_order(order)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 丸掌柜担保手工录单 {source_order_no}",
)
row_text = order_page.assert_order_list_status(
source_order_no,
order_title,
list(case.expected_wanzhanggui_statuses),
)
if "代练中" not in row_text:
assert "待接单" in row_text, (
f"担保单创建后丸掌柜订单状态不符合预期;source_order_no={source_order_no}; row_text={row_text[:1500]}"
)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 丸掌柜担保订单列表状态 {source_order_no}",
)
if case.verify_mode == "trade_amount" and "代练中" not in row_text:
pytest.skip(f"{case.case_id} 前置分支未满足:丸掌柜订单未进入代练中,当前行={row_text[:500]}")
if case.verify_mode == "trade_amount" and "代练中" in row_text:
fulfillment_order_no = order_page.extract_fulfillment_order_no_from_current_wanzhanggui_order_list(
order_page.order_frame(),
source_order_no,
order_title,
)
order_page.verify_guarantee_trade_amount(
fulfillment_order_no,
case.dispatch_price,
)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 丸掌柜担保交易金额校验 {fulfillment_order_no}",
)
else:
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集主账号登录",
)
if case.verify_mode != "trade_amount":
fulfillment_page.verify_order_reached_hall(source_order_no, order_title, "私单")
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集抢单大厅-私单可见 {source_order_no}",
)
store.mark_order_dispatched(
wanzhanggui_phone=case.wanzhanggui_phone,
source_order_no=source_order_no,
order_title=order_title,
strategy_name=case.strategy_name,
strategy_type=case.strategy_type,
wanyouji_phones=[case.wanyouji_phone],
)
finally:
if case.close_guarantee_after_case:
try:
try:
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
except Exception:
order_page.clear_login_state()
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
for attempt in range(2):
try:
order_page.ensure_workshop_guarantee(case.workshop_name, enabled=False)
break
except Exception:
if attempt:
raise
order_page.clear_login_state()
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 关闭工作室担保 {case.workshop_name}",
)
except Exception as exc:
if bug_reporter:
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 关闭工作室担保失败:{exc}",
)
print(f"[E2E] 关闭工作室担保失败,需人工确认担保状态:{exc}")
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