Commit 072be617 authored by lujuan's avatar lujuan

新增智能接单自动化脚本

parent 50073f77
......@@ -319,14 +319,34 @@ class WanzhangguiOrderPage(TenantAuthPage):
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.ensure_sidebar_child_visible("履约工作室/平台", "工作室管理")
self.ensure_sidebar_child_visible("工作室管理", "工作室授权")
self.click_sidebar_text("工作室授权")
self.page.wait_for_timeout(2500)
return self.futong_frame_by_path("/merchantSupplyAuth", target_url=WANZHANGGUI_SUPPLY_AUTH_URL)
def ensure_sidebar_child_visible(self, parent_text: str, child_text: str) -> None:
last_text = ""
for _ in range(4):
if self.sidebar_text_visible(child_text):
return
self.click_sidebar_text(parent_text, right_edge=True)
self.page.wait_for_timeout(1000)
last_text = self.page.locator("body").inner_text(timeout=5000)
if child_text in last_text:
return
raise AssertionError(
f"左侧菜单 {parent_text} 未成功展开,未找到子菜单 {child_text};"
f"当前页面文本:{last_text[:1000]}"
)
def sidebar_text_visible(self, text: str) -> bool:
try:
self.sidebar_box(text, parent=False)
return True
except AssertionError:
return False
def futong_frame_by_path(self, path: str, target_url: str | None = None) -> Frame:
if target_url:
for frame in self.page.frames:
......@@ -343,7 +363,6 @@ class WanzhangguiOrderPage(TenantAuthPage):
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
......@@ -356,7 +375,6 @@ class WanzhangguiOrderPage(TenantAuthPage):
)
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}"
......@@ -389,11 +407,32 @@ class WanzhangguiOrderPage(TenantAuthPage):
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 || '';
const norm = text => String(text || '').replace(/\\s+/g, '').trim();
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const rowNameCell = row => {
const cells = Array.from(row.querySelectorAll('td, [role="cell"]'));
const exactCell = cells.find(cell => norm(cell.innerText || cell.textContent) === norm(workshopName));
if (exactCell) return exactCell;
const table = row.closest('table');
if (!table) return null;
const headers = Array.from(table.querySelectorAll('thead th, [role="columnheader"]'))
.map((th, index) => ({ index, text: norm(th.innerText || th.textContent) }));
const header = headers.find(item => item.text === '工作室名称') ||
headers.find(item => item.text.includes('工作室名称'));
if (!header) return null;
return cells[header.index] || null;
};
const rows = Array.from(document.querySelectorAll('tbody tr, .ant-table-row'))
.map(row => ({ row, text: (row.innerText || row.textContent || '').trim() }))
.filter(item => visible(item.row));
const exactRows = rows.filter(item => {
const cell = rowNameCell(item.row);
return cell && norm(cell.innerText || cell.textContent) === norm(workshopName);
});
return exactRows[0]?.text || '';
}""",
workshop_name,
)
......@@ -421,10 +460,31 @@ class WanzhangguiOrderPage(TenantAuthPage):
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 norm = text => String(text || '').replace(/\\s+/g, '').trim();
const visible = el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};
const rowNameCell = row => {
const cells = Array.from(row.querySelectorAll('td, [role="cell"]'));
const exactCell = cells.find(cell => norm(cell.innerText || cell.textContent) === norm(workshopName));
if (exactCell) return exactCell;
const table = row.closest('table');
if (!table) return null;
const headers = Array.from(table.querySelectorAll('thead th, [role="columnheader"]'))
.map((th, index) => ({ index, text: norm(th.innerText || th.textContent) }));
const header = headers.find(item => item.text === '工作室名称') ||
headers.find(item => item.text.includes('工作室名称'));
if (!header) return null;
return cells[header.index] || null;
};
const rows = Array.from(document.querySelectorAll('tbody tr, .ant-table-row'))
.map(row => ({ row, text: (row.innerText || row.textContent || '').trim() }))
.filter(item => visible(item.row))
.filter(item => {
const cell = rowNameCell(item.row);
return cell && norm(cell.innerText || cell.textContent) === norm(workshopName);
});
const row = rows[0]?.row;
if (!row) return false;
const nodes = Array.from(row.querySelectorAll('button, a, span, [role="button"]'))
......@@ -9472,7 +9532,7 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
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:
if self.auto_accept_assignment_enabled(frame, merchant_name) == enabled:
return row_text
action_texts = ["开启自动接单", "开启智能接单", "开启接单", "启用"] if enabled else [
......@@ -9481,16 +9541,24 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
"关闭接单",
"停用",
]
clicked = self.click_auto_accept_assignment_action(frame, merchant_name, action_texts)
clicked_switch = self.click_auto_accept_assignment_switch(frame, merchant_name)
clicked = clicked_switch or 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)
if clicked_switch:
self.confirm_dialog_if_visible(frame)
else:
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, (
assert self.auto_accept_assignment_enabled(frame, merchant_name) == enabled, (
f"智能接单开关切换后不符合预期;merchant={merchant_name}; expected={enabled}; row_text={row_text}"
)
return row_text
......@@ -9622,6 +9690,137 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
return False
return False
def auto_accept_assignment_enabled(self, frame: Frame, merchant_name: str) -> bool:
value = frame.evaluate(
"""merchantName => {
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 null;
const findAssignmentCell = row => {
const table = row.closest('table');
if (!table) return null;
const headers = Array.from(table.querySelectorAll('thead th'))
.map((th, index) => ({ index, text: (th.innerText || th.textContent || '').trim() }));
const header = headers.find(item => item.text.includes('自动接指派单')) ||
headers.find(item => item.text.includes('接指派单')) ||
headers.find(item => item.text.includes('自动接单'));
if (!header) return null;
return Array.from(row.querySelectorAll('td'))[header.index] || null;
};
const switchCandidates = root => Array.from(root.querySelectorAll('.ant-switch, [role="switch"], input[type="checkbox"], button'))
.filter(visible)
.filter(el => {
const cls = String(el.className || '');
return cls.includes('switch') || el.getAttribute('role') === 'switch' || el.tagName === 'INPUT';
});
const cell = findAssignmentCell(row) || row;
const switches = switchCandidates(cell);
const sw = switches[0] || switchCandidates(row)[0];
if (!sw) return null;
const cls = String(sw.className || '');
if (cls.includes('ant-switch-checked') || cls.includes('is-checked')) return true;
const aria = sw.getAttribute('aria-checked');
if (aria === 'true') return true;
if (aria === 'false') return false;
if (sw.tagName === 'INPUT') return Boolean(sw.checked);
const checked = sw.querySelector('.ant-switch-checked, [aria-checked="true"], input:checked');
if (checked) return true;
const style = window.getComputedStyle(sw);
const color = style.backgroundColor || style.borderColor || '';
const parts = color.match(/\\d+(?:\\.\\d+)?/g)?.map(Number) || [];
if (parts.length >= 3) {
const [r, g, b] = parts;
if (g > r + 20 && g > b + 20 && g >= 120) return true;
if (Math.abs(r - g) < 25 && Math.abs(g - b) < 25) return false;
}
return false;
}""",
merchant_name,
)
assert value is not None, (
f"智能接单配置未找到商家自动接指派单开关;merchant={merchant_name}; "
f"text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
)
return bool(value)
def click_auto_accept_assignment_switch(self, frame: Frame, merchant_name: str) -> bool:
clicked = frame.evaluate(
"""merchantName => {
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 findAssignmentCell = row => {
const table = row.closest('table');
if (!table) return null;
const headers = Array.from(table.querySelectorAll('thead th'))
.map((th, index) => ({ index, text: (th.innerText || th.textContent || '').trim() }));
const header = headers.find(item => item.text.includes('自动接指派单')) ||
headers.find(item => item.text.includes('接指派单')) ||
headers.find(item => item.text.includes('自动接单'));
if (!header) return null;
return Array.from(row.querySelectorAll('td'))[header.index] || null;
};
const switchCandidates = root => Array.from(root.querySelectorAll('.ant-switch, [role="switch"], input[type="checkbox"], button'))
.map(el => ({ el, r: el.getBoundingClientRect(), cls: String(el.className || '') }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.cls.includes('switch') || item.el.getAttribute('role') === 'switch' || item.el.tagName === 'INPUT');
const cell = findAssignmentCell(row) || row;
const target = (switchCandidates(cell)[0] || switchCandidates(row)[0])?.el;
if (!target) return false;
const clickable = target.closest('button, [role="switch"], label') || target;
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;
}""",
merchant_name,
)
self.page.wait_for_timeout(1000)
return bool(clicked)
def confirm_dialog_if_visible(self, frame: Frame) -> bool:
clicked = frame.evaluate(
"""() => {
const roots = Array.from(document.querySelectorAll('.ant-modal, .ant-popover, .ant-popconfirm'))
.filter(el => {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
});
if (!roots.length) return false;
const root = roots[roots.length - 1];
const buttons = Array.from(root.querySelectorAll('button'))
.map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0);
const positiveTexts = ['确认', '确定', '是'];
const target = buttons.find(item =>
positiveTexts.some(text => item.text === text || item.text.includes(text))
);
if (!target) return false;
const clickable = target.el.closest('button, a, [role="button"]') || target.el;
clickable.click();
return true;
}"""
)
if clicked:
self.page.wait_for_timeout(1000)
return bool(clicked)
def click_auto_accept_assignment_action(self, frame: Frame, merchant_name: str, action_texts: list[str]) -> bool:
clicked = frame.evaluate(
"""({ merchantName, actionTexts }) => {
......
......@@ -24,6 +24,7 @@ class AutoAcceptanceCase:
sub_order_title_suffix: str
verify_mode: str = "hall"
merchant_name: str = ""
workshop_name: str = ""
@dataclass(frozen=True)
......@@ -39,11 +40,14 @@ class GuaranteeAutoAcceptanceCase:
sub_order_title_suffix: str
receive_price: str = "80"
dispatch_price: str = "70"
dispatch_price_mode: str = "percent"
security_deposit: str = "0"
efficiency_deposit: str = "0"
expected_wanzhanggui_statuses: tuple[str, ...] = ("待接单", "代练中")
expected_scope: str = "私单"
verify_mode: str = "create"
close_guarantee_after_case: bool = False
merchant_name: str = "测试001"
CASES = [
......@@ -60,6 +64,7 @@ CASES = [
sub_order_title_suffix="私单标识",
verify_mode="branch",
merchant_name="测试001",
workshop_name="旺仔工作室",
),
# 用例1.2:关闭自动接指派单后创建私单,校验丸掌柜待接单且丸友集抢单大厅-私单可见。
AutoAcceptanceCase(
......@@ -74,6 +79,7 @@ CASES = [
sub_order_title_suffix="私单标识",
verify_mode="hall",
merchant_name="测试001",
workshop_name="旺仔工作室",
),
# 用例1.1:开启自动接指派单后创建私单,校验丸掌柜代练中且丸友集订单列表已接单。
AutoAcceptanceCase(
......@@ -88,6 +94,7 @@ CASES = [
sub_order_title_suffix="私单标识",
verify_mode="accepted",
merchant_name="测试001",
workshop_name="旺仔工作室",
),
# 用例2:创建公池单,校验丸友集抢单大厅-公池单可见。
AutoAcceptanceCase(
......@@ -101,24 +108,45 @@ CASES = [
expected_scope="公池单",
sub_order_title_suffix="公池单标识",
verify_mode="hall",
workshop_name="旺仔工作室",
),
]
GUARANTEE_CASES = [
# 用例3.1:独立创建一笔担保私单;搜索到丸掌柜状态为代练中后点击详情提取ORB,再校验担保交易扣除金额;结束后不关闭担保。
# 用例3.1:独立创建一笔担保私单;私单应进入代练中,随后点击详情提取ORB并校验担保交易扣除金额;结束后不关闭担保。
GuaranteeAutoAcceptanceCase(
use_case_no="3.1",
case_id="AUTO-ACCEPT-GUARANTEE-TRADE-AMOUNT",
title="担保单代练中后进详情提取ORB并核对担保交易扣除金额",
title="担保单代练中后进详情提取ORB并核对担保交易扣除金额",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
workshop_name="旺仔工作室",
strategy_name="指定-旺仔",
strategy_type="单平台指定",
sub_order_title_suffix="担保私单标识",
dispatch_price="0.1",
dispatch_price_mode="fixed",
expected_wanzhanggui_statuses=("代练中",),
verify_mode="trade_amount",
),
# 用例3.1:独立创建一笔担保公池单;公池单应保持待接单,并在丸友集抢单大厅-公池单可见;结束后不关闭担保。
GuaranteeAutoAcceptanceCase(
use_case_no="3.1",
case_id="AUTO-ACCEPT-GUARANTEE-POOL",
title="担保公池单待接单并在丸友集公池单可见",
wanzhanggui_phone="17788888888",
wanyouji_phone="18800000001",
workshop_name="旺仔工作室",
strategy_name="多发测试",
strategy_type="多工作室同时分发",
sub_order_title_suffix="担保公池单标识",
dispatch_price="0.1",
dispatch_price_mode="fixed",
expected_wanzhanggui_statuses=("待接单",),
expected_scope="公池单",
verify_mode="hall",
),
# 用例3.2:检查担保已开启,若关闭则开启;独立创建一笔发单价80000的担保私单,结束后不关闭担保。
GuaranteeAutoAcceptanceCase(
use_case_no="3.2",
......@@ -282,6 +310,14 @@ def test_自动接单用例_普通单发单和分支校验(
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
try:
if case.workshop_name:
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
order_page.ensure_workshop_guarantee(case.workshop_name, enabled=False)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
location=f"{case.case_id} 发单前关闭工作室担保 {case.workshop_name}",
)
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)
......@@ -461,6 +497,7 @@ def test_自动接单用例_担保单发单和担保交易校验(
strategy_type=case.strategy_type,
receive_price=case.receive_price,
dispatch_price=case.dispatch_price,
dispatch_price_mode=case.dispatch_price_mode,
security_deposit=case.security_deposit,
efficiency_deposit=case.efficiency_deposit,
role_name=f"AAG角色{timestamp[-4:]}",
......@@ -468,6 +505,15 @@ def test_自动接单用例_担保单发单和担保交易校验(
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
try:
if case.merchant_name:
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, 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}自动接指派单开启",
)
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
bug_reporter.record_if_errors(
account=case.wanzhanggui_phone,
......@@ -524,10 +570,10 @@ def test_自动接单用例_担保单发单和担保交易校验(
)
if case.verify_mode != "trade_amount":
fulfillment_page.verify_order_reached_hall(source_order_no, order_title, "私单")
fulfillment_page.verify_order_reached_hall(source_order_no, order_title, case.expected_scope)
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
location=f"{case.case_id} 丸友集抢单大厅-私单可见 {source_order_no}",
location=f"{case.case_id} 丸友集抢单大厅-{case.expected_scope}可见 {source_order_no}",
)
store.mark_order_dispatched(
......
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