Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
0
01SaaS-test
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
吴珊婧
01SaaS-test
Commits
fba5321b
Commit
fba5321b
authored
Jul 02, 2026
by
wushanjing
Browse files
Options
Browse Files
Download
Plain Diff
解决lujuan自动接单合并冲突
parents
5009d8b8
14cfa621
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
1505 additions
and
11 deletions
+1505
-11
wanzhanggui_order_page.py
e2e/pages/wanzhanggui_order_page.py
+937
-11
test_e2e_auto_acceptance_manual_order_dispatch.py
...ts/智能接单/test_e2e_auto_acceptance_manual_order_dispatch.py
+568
-0
No files found.
e2e/pages/wanzhanggui_order_page.py
View file @
fba5321b
...
...
@@ -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;
}
}
...
...
e2e/tests/智能接单/test_e2e_auto_acceptance_manual_order_dispatch.py
0 → 100644
View file @
fba5321b
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}"
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment