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
31fb1489
Commit
31fb1489
authored
Jul 31, 2026
by
wushanjing
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
新增丸掌柜丸友集授权简化自动化用例
parent
84caf17f
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
1184 additions
and
0 deletions
+1184
-0
auth_simplification_page.py
e2e/pages/auth_simplification_page.py
+334
-0
test_e2e_auth_simplification_flows.py
e2e/tests/丸掌柜丸友集授权简化/test_e2e_auth_simplification_flows.py
+504
-0
test_e2e_auth_simplification_player_settlement.py
...丸友集授权简化/test_e2e_auth_simplification_player_settlement.py
+68
-0
丸掌柜 × 丸友集授权简化提测文档.pdf
e2e/tests/丸掌柜丸友集授权简化/丸掌柜 × 丸友集授权简化提测文档.pdf
+0
-0
丸掌柜丸友集授权简化.pdf
e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化.pdf
+0
-0
丸掌柜丸友集授权简化_测试点及自动化用例.xlsx
e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化_测试点及自动化用例.xlsx
+0
-0
丸掌柜丸友集授权简化_测试点及自动化用例_自动化脚本版.xlsx
e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化_测试点及自动化用例_自动化脚本版.xlsx
+0
-0
generate_wanzhanggui_wanyouji_auth_simplification_excel.py
...enerate_wanzhanggui_wanyouji_auth_simplification_excel.py
+278
-0
No files found.
e2e/pages/auth_simplification_page.py
0 → 100644
View file @
31fb1489
from
__future__
import
annotations
from
pathlib
import
Path
from
typing
import
Iterable
from
playwright.sync_api
import
Frame
,
Page
,
expect
from
e2e.pages.tenant_auth_page
import
TenantAuthPage
from
e2e.pages.wanzhanggui_order_page
import
WANYOUJI_CONSOLE_URL
from
e2e.pages.wanzhanggui_push_page
import
WANZHANGGUI_CONSOLE_URL
from
e2e.utils.bug_reporter
import
BugReporter
class
AuthorizationSimplificationPage
(
TenantAuthPage
):
"""Interactions shared by the new Wanyouji/Wanzhanggui authorization pages."""
def
__init__
(
self
,
page
:
Page
,
tenant_home_url
:
str
,
bug_reporter
:
BugReporter
|
None
=
None
):
super
()
.
__init__
(
page
,
tenant_home_url
,
bug_reporter
)
def
open_wanyouji_merchant_recommendation
(
self
)
->
Frame
:
required
=
[
"发单商户推荐"
,
"发单商户授权"
,
"待我审核"
]
last_error
:
AssertionError
|
None
=
None
for
_
in
range
(
3
):
try
:
self
.
goto
(
WANYOUJI_CONSOLE_URL
)
self
.
wait_for_enterprise_console_shell
()
self
.
page
.
wait_for_timeout
(
1200
)
self
.
click_sidebar_text
(
"工作室基础应用"
,
right_edge
=
True
)
self
.
click_sidebar_text
(
"发单商户推荐"
)
return
self
.
_business_frame
(
required
)
except
AssertionError
as
error
:
last_error
=
error
self
.
page
.
wait_for_timeout
(
1200
)
raise
last_error
or
AssertionError
(
"未能加载发单商户推荐页面"
)
def
open_wanzhanggui_workshop_authorization_new
(
self
)
->
Frame
:
required
=
[
"工作室推荐"
,
"工作室授权"
,
"待我审核"
]
last_error
:
AssertionError
|
None
=
None
for
_
in
range
(
3
):
try
:
self
.
goto
(
WANZHANGGUI_CONSOLE_URL
)
self
.
wait_for_enterprise_console_shell
()
self
.
page
.
wait_for_timeout
(
1200
)
self
.
click_sidebar_text
(
"商家管理"
,
right_edge
=
True
)
self
.
click_sidebar_text
(
"工作室授权新"
)
return
self
.
_business_frame
(
required
)
except
AssertionError
as
error
:
last_error
=
error
self
.
page
.
wait_for_timeout
(
1200
)
raise
last_error
or
AssertionError
(
"未能加载工作室授权新页面"
)
def
_business_frame
(
self
,
required_texts
:
Iterable
[
str
])
->
Frame
:
required
=
list
(
required_texts
)
for
_
in
range
(
36
):
for
frame
in
reversed
(
self
.
page
.
frames
):
try
:
body
=
frame
.
locator
(
"body"
)
.
inner_text
(
timeout
=
1200
)
except
Exception
:
continue
if
all
(
text
in
body
for
text
in
required
):
return
frame
self
.
page
.
wait_for_timeout
(
800
)
snapshots
:
list
[
str
]
=
[]
for
frame
in
self
.
page
.
frames
:
try
:
snapshots
.
append
(
f
"{frame.url}: {frame.locator('body').inner_text(timeout=600)[:600]}"
)
except
Exception
:
continue
raise
AssertionError
(
f
"未找到授权简化业务 iframe;required={required}; frames={snapshots}"
)
def
click_tab
(
self
,
frame
:
Frame
,
tab_text
:
str
)
->
None
:
clicked
=
frame
.
evaluate
(
"""text => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const compact = value => (value || '').replace(/
\
s+/g, '').trim();
const nodes = Array.from(document.querySelectorAll('[role=tab], .ant-tabs-tab, button, a, span, div'))
.filter(visible)
.filter(el => {
const value = compact(el.innerText || el.textContent || '');
return value === compact(text) || value.startsWith(compact(text));
});
const target = nodes.sort((a, b) => {
const ar = a.getBoundingClientRect(), br = b.getBoundingClientRect();
return ar.width * ar.height - br.width * br.height;
})[0];
if (!target) return false;
(target.closest('[role=tab], .ant-tabs-tab, button, a') || target).click();
return true;
}"""
,
tab_text
,
)
assert
clicked
,
f
"授权页未找到Tab {tab_text};text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
self
.
page
.
wait_for_timeout
(
900
)
def
search_by_name
(
self
,
frame
:
Frame
,
label
:
str
,
value
:
str
)
->
None
:
found
=
frame
.
evaluate
(
"""({ label, value }) => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const inputs = Array.from(document.querySelectorAll('input')).filter(visible);
const target = inputs.find(input => {
const placeholder = input.getAttribute('placeholder') || '';
const scope = input.parentElement?.parentElement?.innerText || '';
return placeholder.includes(label) || scope.includes(label);
}) || inputs[0];
if (!target) return false;
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
setter?.call(target, value);
target.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
target.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}"""
,
{
"label"
:
label
,
"value"
:
value
},
)
assert
found
,
f
"授权页未找到 {label} 搜索框"
self
.
_click_text
(
frame
,
[
"查询"
,
"查 询"
])
self
.
page
.
wait_for_timeout
(
1400
)
def
row_text
(
self
,
frame
:
Frame
,
name
:
str
)
->
str
:
return
str
(
frame
.
evaluate
(
"""name => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const rows = Array.from(document.querySelectorAll('tr, .ant-table-row, [class*=table-row]'))
.filter(visible)
.map(row => (row.innerText || row.textContent || '').trim());
return rows.find(text => text.includes(name)) || '';
}"""
,
name
,
)
)
def
require_row
(
self
,
frame
:
Frame
,
name
:
str
,
expected_text
:
str
=
""
)
->
str
:
for
_
in
range
(
5
):
text
=
self
.
row_text
(
frame
,
name
)
if
text
and
(
not
expected_text
or
expected_text
in
text
):
return
text
self
.
page
.
wait_for_timeout
(
900
)
body
=
frame
.
locator
(
"body"
)
.
inner_text
(
timeout
=
5000
)
raise
AssertionError
(
f
"授权页未找到记录或状态不匹配;name={name}; expected={expected_text}; text={body[:1800]}"
)
def
click_row_action
(
self
,
frame
:
Frame
,
name
:
str
,
actions
:
list
[
str
])
->
None
:
clicked
=
frame
.
evaluate
(
"""({ name, actions }) => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const rows = Array.from(document.querySelectorAll('tr, .ant-table-row, [class*=table-row]'))
.filter(visible)
.filter(row => (row.innerText || row.textContent || '').includes(name));
for (const row of rows) {
const controls = Array.from(row.querySelectorAll('button, a, [role=button], span, div')).filter(visible);
const target = controls.find(el => actions.some(action => (el.innerText || el.textContent || '').trim() === action));
if (target) { (target.closest('button, a, [role=button]') || target).click(); return true; }
}
return false;
}"""
,
{
"name"
:
name
,
"actions"
:
actions
},
)
assert
clicked
,
f
"授权页记录未找到操作;name={name}; actions={actions}; row={self.row_text(frame, name)}"
self
.
page
.
wait_for_timeout
(
700
)
def
confirm_visible_modal
(
self
,
frame
:
Frame
,
actions
:
list
[
str
]
|
None
=
None
)
->
None
:
action_texts
=
actions
or
[
"同意"
,
"确认"
,
"确定"
,
"保存"
]
clicked
=
frame
.
evaluate
(
"""actions => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const roots = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer, [role=dialog]')).filter(visible);
const root = roots[roots.length - 1] || document;
const controls = Array.from(root.querySelectorAll('button, [role=button], span, div')).filter(visible);
const compact = value => (value || '').replace(/
\
s+/g, '').trim();
const target = controls.find(el => actions.some(action => compact(el.innerText || el.textContent || '') === compact(action)));
if (!target) return false;
(target.closest('button, [role=button]') || target).click();
return true;
}"""
,
action_texts
,
)
assert
clicked
,
f
"未找到确认操作 {action_texts};modal={frame.locator('body').inner_text(timeout=5000)[:1500]}"
self
.
page
.
wait_for_timeout
(
1200
)
def
click_display_config
(
self
,
frame
:
Frame
)
->
None
:
for
_
in
range
(
3
):
self
.
_click_text
(
frame
,
[
"对外展示配置"
])
dialog
=
frame
.
locator
(
".ant-modal:visible, [role=dialog]:visible"
)
.
last
try
:
expect
(
dialog
)
.
to_be_visible
(
timeout
=
3000
)
return
except
AssertionError
:
self
.
page
.
wait_for_timeout
(
500
)
raise
AssertionError
(
"未能打开对外展示配置弹窗"
)
def
save_display_config
(
self
,
frame
:
Frame
,
data
:
dict
[
str
,
str
],
qr_code
:
Path
|
None
=
None
)
->
None
:
root
=
frame
.
locator
(
".ant-modal:visible, [role=dialog]:visible"
)
.
last
expect
(
root
)
.
to_be_visible
(
timeout
=
5000
)
self
.
_fill_modal_field
(
root
,
[
"简介"
],
data
[
"intro"
],
textarea
=
True
)
self
.
_select_modal_option
(
frame
,
root
,
"擅长游戏"
,
data
[
"game"
])
self
.
_select_modal_option
(
frame
,
root
,
"结算周期"
,
data
[
"settlement"
])
self
.
_fill_modal_field
(
root
,
[
"联系人"
],
data
[
"contact"
])
self
.
_fill_modal_field
(
root
,
[
"联系手机"
,
"手机号"
],
data
[
"phone"
])
self
.
_fill_modal_field
(
root
,
[
"微信号"
],
data
[
"wechat"
])
self
.
_fill_modal_field
(
root
,
[
"QQ号"
,
"QQ 号"
],
data
[
"qq"
])
if
qr_code
:
upload
=
root
.
locator
(
"input[type=file]"
)
.
first
if
upload
.
count
():
upload
.
set_input_files
(
str
(
qr_code
))
uploading
=
root
.
get_by_text
(
"Uploading..."
,
exact
=
False
)
if
uploading
.
count
():
expect
(
uploading
)
.
to_be_hidden
(
timeout
=
30000
)
self
.
confirm_visible_modal
(
frame
,
[
"保存"
])
expect
(
root
)
.
to_be_hidden
(
timeout
=
8000
)
def
toggle_external_display
(
self
,
frame
:
Frame
,
enabled
:
bool
=
True
)
->
None
:
result
=
frame
.
evaluate
(
"""enabled => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const label = Array.from(document.querySelectorAll('span, div, label')).find(el => visible(el) && (el.innerText || el.textContent || '').trim() === '对外展示');
if (!label) return false;
const scope = label.parentElement?.parentElement || label.parentElement || document;
const input = scope.querySelector('input[type=checkbox]');
if (input && Boolean(input.checked) !== enabled) { input.click(); return true; }
const control = scope.querySelector('.ant-switch, [role=switch]');
if (control) { control.click(); return true; }
return Boolean(input?.checked) === enabled;
}"""
,
enabled
,
)
assert
result
,
"未找到或未能切换对外展示开关"
self
.
page
.
wait_for_timeout
(
900
)
def
_fill_modal_field
(
self
,
root
,
labels
:
list
[
str
],
value
:
str
,
*
,
textarea
:
bool
=
False
)
->
None
:
selector
=
"textarea"
if
textarea
else
"input"
for
label
in
labels
:
candidate
=
root
.
locator
(
f
"{selector}[placeholder*='{label}']"
)
.
first
if
candidate
.
count
():
candidate
.
fill
(
value
)
return
inputs
=
root
.
locator
(
selector
)
for
index
in
range
(
inputs
.
count
()):
candidate
=
inputs
.
nth
(
index
)
try
:
belongs_to_label
=
candidate
.
evaluate
(
"""(element, labels) => {
for (let node = element.parentElement; node && node !== document.body; node = node.parentElement) {
const text = node.innerText || node.textContent || '';
const controls = node.querySelectorAll('input, textarea');
if (controls.length === 1 && labels.some(label => text.includes(label))) return true;
}
return false;
}"""
,
labels
,
)
except
Exception
:
belongs_to_label
=
False
if
belongs_to_label
:
candidate
.
fill
(
value
)
return
controls
=
root
.
evaluate
(
"""node => Array.from(node.querySelectorAll('input, textarea, [contenteditable=true]'))
.map(el => ({ tag: el.tagName, placeholder: el.getAttribute('placeholder') || '', name: el.getAttribute('name') || '' }))"""
)
raise
AssertionError
(
f
"展示配置弹窗未找到字段 {labels}; text={root.inner_text(timeout=3000)[:1200]}; controls={controls}"
)
def
_select_modal_option
(
self
,
frame
:
Frame
,
root
,
label
:
str
,
value
:
str
)
->
None
:
comboboxes
=
root
.
get_by_role
(
"combobox"
)
index
=
0
if
label
==
"擅长游戏"
else
1
if
comboboxes
.
count
()
>
index
:
combobox
=
comboboxes
.
nth
(
index
)
combobox
.
click
(
timeout
=
3000
,
force
=
True
)
self
.
page
.
wait_for_timeout
(
500
)
option
=
self
.
page
.
locator
(
f
'[role="option"][aria-label="{value}"]:visible'
)
.
first
try
:
expect
(
option
)
.
to_be_visible
(
timeout
=
1500
)
option
.
click
()
except
AssertionError
:
combobox
.
press
(
"ArrowDown"
)
combobox
.
press
(
"Enter"
)
combobox
.
press
(
"Escape"
)
self
.
page
.
wait_for_timeout
(
300
)
return
selected
=
root
.
evaluate
(
r"""(root, { label, value }) => {
const norm = text => String(text || '').replace(/\s+/g, '').trim();
const labels = Array.from(root.querySelectorAll('*'))
.map(el => ({ el, text: norm(el.innerText || el.textContent || '') }))
.filter(item => item.text === norm(label))
.sort((a, b) => a.text.length - b.text.length);
const target = labels[0]?.el;
if (!target) return false;
for (let node = target.parentElement; node; node = node.parentElement) {
const selector = node.querySelector('.ant-select-selector, [role=combobox], input:not([type=file])');
if (selector) {
selector.click();
return true;
}
}
return false;
}"""
,
{
"label"
:
label
,
"value"
:
value
},
)
if
not
selected
:
structure
=
root
.
evaluate
(
r"""(root, label) => Array.from(root.querySelectorAll('*'))
.filter(el => (el.innerText || el.textContent || '').trim() === label)
.slice(0, 3)
.map(el => {
const items = [];
for (let node = el; node && items.length < 5; node = node.parentElement) {
items.push({ tag: node.tagName, className: String(node.className || ''), text: (node.innerText || node.textContent || '').trim().slice(0, 300) });
}
return items;
})"""
,
label
,
)
raise
AssertionError
(
f
"展示配置弹窗未找到下拉字段 {label}; structure={structure}"
)
self
.
page
.
wait_for_timeout
(
500
)
option
=
frame
.
locator
(
".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content"
)
.
filter
(
has_text
=
value
)
.
first
expect
(
option
)
.
to_be_visible
(
timeout
=
4000
)
option
.
click
()
self
.
page
.
wait_for_timeout
(
300
)
def
_click_text
(
self
,
frame
:
Frame
,
texts
:
list
[
str
])
->
None
:
clicked
=
frame
.
evaluate
(
"""texts => {
const visible = el => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
const nodes = Array.from(document.querySelectorAll('button, a, [role=button], span, div')).filter(visible);
const target = nodes.find(el => texts.some(text => (el.innerText || el.textContent || '').trim() === text));
if (!target) return false;
(target.closest('button, a, [role=button]') || target).click();
return true;
}"""
,
texts
,
)
assert
clicked
,
f
"未找到操作 {texts};text={frame.locator('body').inner_text(timeout=5000)[:1500]}"
e2e/tests/丸掌柜丸友集授权简化/test_e2e_auth_simplification_flows.py
0 → 100644
View file @
31fb1489
from
__future__
import
annotations
import
os
from
datetime
import
datetime
from
pathlib
import
Path
import
pytest
from
playwright.sync_api
import
Page
from
e2e.pages.auth_simplification_page
import
AuthorizationSimplificationPage
from
e2e.pages.admin_page
import
AdminPage
from
e2e.pages.product_solution_admin_page
import
ProductSolutionAdminPage
from
e2e.pages.tenant_registration_page
import
AccountAlreadyRegistered
,
TenantRegistrationPage
from
e2e.pages.tenant_subscription_page
import
TenantSubscriptionPage
from
e2e.pages.wanzhanggui_order_page
import
ManualOrderData
,
WanyoujiFulfillmentPage
,
WanzhangguiOrderPage
from
e2e.pages.wanzhanggui_push_page
import
WanzhangguiPushPage
from
e2e.utils.account_store
import
E2EAccount
,
account_store
from
e2e.utils.data_factory
import
EnterpriseRegistrationData
,
enterprise_data
,
manual_order_receive_price
from
e2e.utils.image_captcha
import
ImageCaptchaSolver
from
e2e.utils.verification_code
import
FeishuConfig
,
VerificationCodeProvider
CASE_PREFIX
=
"E2E-AUTH-SIMPLIFY"
FIXED_PHONE
=
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_FIXED_PHONE"
,
"19159770995"
)
FIXED_PASSWORD
=
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_FIXED_PASSWORD"
,
"test123456"
)
FIXED_MERCHANT_NAME
=
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_FIXED_MERCHANT_NAME"
,
"个体工商户1378"
)
RUN_OPENING
=
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_ENABLE_OPENING"
,
"0"
)
.
strip
()
.
lower
()
in
{
"1"
,
"true"
,
"yes"
}
FORCE_NEW_ENTERPRISE
=
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_FORCE_NEW_ENTERPRISE"
,
"0"
)
.
strip
()
.
lower
()
in
{
"1"
,
"true"
,
"yes"
}
def
pytest_configure
(
config
):
config
.
addinivalue_line
(
"markers"
,
"e2e_auth_simplification: Wanyouji/Wanzhanggui authorization simplification flows"
)
@
pytest
.
mark
.
e2e_auth_simplification
@
pytest
.
mark
.
e2e_seed
@
pytest
.
mark
.
e2e_ledger
def
test_auth_simplify_001_new_wanyouji_display_is_visible_to_wanzhanggui_and_admin
(
page
:
Page
,
settings
,
bug_reporter
,
dual_account_pages
)
->
None
:
"""Create one enterprise, open Wanyouji, and verify its display profile across three surfaces."""
account
=
create_certified_enterprise
(
page
,
settings
,
bug_reporter
)
ensure_product_opened
(
page
,
settings
,
bug_reporter
,
account
,
"丸友集"
)
account
=
account_store
()
.
mark_wanyouji_opened
(
account
)
profile
=
display_profile
(
account
)
# This page is later reused for SaaS-admin verification. Keep the headed
# pytest window useful while the dual-account contexts perform tenant work.
page
.
goto
(
settings
.
admin_url
,
wait_until
=
"domcontentloaded"
)
wanyouji_page
,
wanzhanggui_page
=
dual_account_pages
.
pair
(
account
.
phone
,
FIXED_PHONE
)
wanyouji
=
AuthorizationSimplificationPage
(
wanyouji_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanyouji
.
login_main_account
(
account
.
phone
,
account
.
password
)
if
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_SKIP_DISPLAY_CONFIG"
,
"0"
)
.
strip
()
.
lower
()
not
in
{
"1"
,
"true"
,
"yes"
}:
frame
=
wanyouji
.
open_wanyouji_merchant_recommendation
()
wanyouji
.
click_display_config
(
frame
)
wanyouji
.
save_display_config
(
frame
,
profile
,
qr_code
=
ensure_qr_placeholder
())
wanyouji
.
toggle_external_display
(
frame
)
wanzhanggui
=
AuthorizationSimplificationPage
(
wanzhanggui_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanzhanggui
.
login_main_account
(
FIXED_PHONE
,
FIXED_PASSWORD
)
verify_recommendation_detail
(
wanzhanggui
,
account
.
enterprise_name
,
"工作室名称"
,
profile
)
if
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_SKIP_ADMIN_RECOMMENDATION_CHECK"
,
"0"
)
.
strip
()
.
lower
()
not
in
{
"1"
,
"true"
,
"yes"
}:
verify_admin_recommendation
(
page
,
settings
,
account
.
enterprise_name
,
profile
)
save_flow_artifact
(
account
,
profile
,
"wanyouji_display_ready"
)
@
pytest
.
mark
.
e2e_auth_simplification
@
pytest
.
mark
.
e2e_chain
@
pytest
.
mark
.
e2e_ledger
def
test_auth_simplify_002_same_enterprise_wanzhanggui_display_and_supply_initialization
(
page
:
Page
,
settings
,
bug_reporter
,
dual_account_pages
)
->
None
:
"""Open Wanzhanggui for the seeded enterprise and validate merchant recommendation/admin supply records."""
account
,
profile
=
latest_flow_account
(
"wanyouji_display_ready"
)
ensure_product_opened
(
page
,
settings
,
bug_reporter
,
account
,
"丸掌柜"
)
account
=
account_store
()
.
mark_wanzhanggui_opened
(
account
)
profile
=
{
**
profile
,
"intro"
:
f
"丸掌柜-{profile['intro']}"
}
wanzhanggui_page
,
wanyouji_page
=
dual_account_pages
.
pair
(
account
.
phone
,
FIXED_PHONE
)
wanzhanggui
=
AuthorizationSimplificationPage
(
wanzhanggui_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanzhanggui
.
login_main_account
(
account
.
phone
,
account
.
password
)
frame
=
wanzhanggui
.
open_wanzhanggui_workshop_authorization_new
()
wanzhanggui
.
click_display_config
(
frame
)
wanzhanggui
.
save_display_config
(
frame
,
profile
,
qr_code
=
ensure_qr_placeholder
())
wanzhanggui
.
toggle_external_display
(
frame
)
wanyouji
=
AuthorizationSimplificationPage
(
wanyouji_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanyouji
.
login_main_account
(
FIXED_PHONE
,
FIXED_PASSWORD
)
verify_recommendation_detail
(
wanyouji
,
account
.
enterprise_name
,
"商户名称"
,
profile
)
verify_admin_recommendation
(
page
,
settings
,
account
.
enterprise_name
,
profile
)
verify_admin_merchant_supply
(
page
,
settings
,
account
.
enterprise_name
)
save_flow_artifact
(
account
,
profile
,
"wanzhanggui_display_ready"
)
@
pytest
.
mark
.
e2e_auth_simplification
@
pytest
.
mark
.
e2e_chain
@
pytest
.
mark
.
e2e_ledger
def
test_auth_simplify_003_wanyouji_apply_wanzhanggui_approve_and_dispatch
(
page
:
Page
,
settings
,
bug_reporter
,
dual_account_pages
)
->
None
:
"""Wanyouji applies to the fixed merchant, then approval produces an enabled strategy and a hall order."""
account
,
_
=
latest_flow_account
(
"wanyouji_display_ready"
)
wanyouji_page
,
wanzhanggui_page
=
dual_account_pages
.
pair
(
account
.
phone
,
FIXED_PHONE
)
wanyouji
=
AuthorizationSimplificationPage
(
wanyouji_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanzhanggui
=
AuthorizationSimplificationPage
(
wanzhanggui_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanyouji
.
login_main_account
(
account
.
phone
,
account
.
password
)
wanzhanggui
.
login_main_account
(
FIXED_PHONE
,
FIXED_PASSWORD
)
wanyouji_frame
=
wanyouji
.
open_wanyouji_merchant_recommendation
()
try
:
wanyouji
.
require_row
(
wanyouji_frame
,
FIXED_MERCHANT_NAME
)
target_row
=
wanyouji_frame
.
locator
(
"tr, .ant-table-row, [class*=table-row]"
)
.
filter
(
has_text
=
FIXED_MERCHANT_NAME
)
.
first
target_row
.
get_by_role
(
"button"
,
name
=
"申请发单"
,
exact
=
True
)
.
click
(
timeout
=
5000
)
submit
=
wanyouji_frame
.
get_by_role
(
"button"
,
name
=
"提交申请"
,
exact
=
True
)
.
last
submit
.
click
(
timeout
=
5000
)
wanyouji_page
.
wait_for_timeout
(
1200
)
except
AssertionError
:
wanyouji
.
click_tab
(
wanyouji_frame
,
"发单商户授权"
)
wanyouji
.
require_row
(
wanyouji_frame
,
FIXED_MERCHANT_NAME
)
wanzhanggui_frame
=
wanzhanggui
.
open_wanzhanggui_workshop_authorization_new
()
wanzhanggui
.
click_tab
(
wanzhanggui_frame
,
"待我审核"
)
wanzhanggui
.
search_by_name
(
wanzhanggui_frame
,
"工作室名称"
,
account
.
enterprise_name
)
review_row
=
wanzhanggui_frame
.
locator
(
"tr, .ant-table-row, [class*=table-row]"
)
.
filter
(
has_text
=
account
.
enterprise_name
)
.
first
review_row
.
get_by_role
(
"button"
,
name
=
"审核"
,
exact
=
True
)
.
click
(
timeout
=
5000
)
wanzhanggui
.
confirm_visible_modal
(
wanzhanggui_frame
,
[
"同意"
])
assert_authorized_and_dispatch
(
wanzhanggui_page
,
wanyouji_page
,
settings
,
bug_reporter
,
account
)
save_flow_artifact
(
account
,
{},
"authorized_and_dispatched"
)
@
pytest
.
mark
.
e2e_auth_simplification
@
pytest
.
mark
.
e2e_chain
@
pytest
.
mark
.
e2e_ledger
def
test_auth_simplify_004_close_reapply_approve_and_dispatch_again
(
page
:
Page
,
settings
,
bug_reporter
,
dual_account_pages
)
->
None
:
"""Close the established relation, exercise re-application, then dispatch again after approval."""
account
,
_
=
latest_flow_account
(
"authorized_and_dispatched"
)
wanyouji_page
,
fixed_wanzhanggui_page
=
dual_account_pages
.
pair
(
account
.
phone
,
FIXED_PHONE
)
wanyouji
=
AuthorizationSimplificationPage
(
wanyouji_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanzhanggui
=
AuthorizationSimplificationPage
(
wanyouji_page
,
settings
.
tenant_home_url
,
bug_reporter
)
wanyouji
.
login_main_account
(
account
.
phone
,
account
.
password
)
wanzhanggui
.
login_main_account
(
account
.
phone
,
account
.
password
)
frame
=
wanyouji
.
open_wanyouji_merchant_recommendation
()
wanyouji
.
click_tab
(
frame
,
"发单商户授权"
)
wanyouji
.
search_by_name
(
frame
,
"商户名称"
,
FIXED_MERCHANT_NAME
)
wanyouji
.
click_row_action
(
frame
,
FIXED_MERCHANT_NAME
,
[
"关闭授权"
])
wanyouji
.
confirm_visible_modal
(
frame
,
[
"确认"
,
"确定"
])
frame
=
wanzhanggui
.
open_wanzhanggui_workshop_authorization_new
()
wanzhanggui
.
search_by_name
(
frame
,
"工作室名称"
,
FIXED_MERCHANT_NAME
)
wanzhanggui
.
click_row_action
(
frame
,
FIXED_MERCHANT_NAME
,
[
"申请发单"
,
"申请推单"
])
wanzhanggui
.
confirm_visible_modal
(
frame
,
[
"确认"
,
"确定"
])
frame
=
wanyouji
.
open_wanyouji_merchant_recommendation
()
wanyouji
.
click_tab
(
frame
,
"待我审核"
)
wanyouji
.
search_by_name
(
frame
,
"商户名称"
,
FIXED_MERCHANT_NAME
)
wanyouji
.
click_row_action
(
frame
,
FIXED_MERCHANT_NAME
,
[
"审核"
])
wanyouji
.
confirm_visible_modal
(
frame
,
[
"同意"
])
assert_authorized_and_dispatch
(
fixed_wanzhanggui_page
,
wanyouji_page
,
settings
,
bug_reporter
,
account
)
def
create_certified_enterprise
(
page
:
Page
,
settings
,
bug_reporter
)
->
E2EAccount
:
resume_phone
=
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_RESUME_PHONE"
,
""
)
.
strip
()
if
resume_phone
:
account
=
account_store
()
.
find_main
(
resume_phone
)
assert
account
,
f
"未在账号台账找到授权简化恢复账号:{resume_phone}"
assert
account
.
certification_status
==
"approved"
,
f
"恢复账号未完成认证:{resume_phone}"
return
account
if
not
RUN_OPENING
:
pytest
.
skip
(
"Set E2E_AUTH_SIMPLIFICATION_ENABLE_OPENING=1 to create a new enterprise and open products."
)
resumable
=
None
if
FORCE_NEW_ENTERPRISE
else
next
(
(
item
for
item
in
reversed
(
account_store
()
.
load
())
if
item
.
account_type
==
"main"
and
item
.
enterprise_name
.
startswith
(
"E2E授权简化"
)
and
item
.
certification_status
==
"approved"
and
item
.
wanyouji_status
!=
"opened"
),
None
,
)
if
resumable
:
return
resumable
provider
=
VerificationCodeProvider
(
mode
=
settings
.
verification_code_mode
,
static_code
=
settings
.
verification_code_static
,
feishu_config
=
FeishuConfig
(
app_id
=
settings
.
feishu_app_id
,
app_secret
=
settings
.
feishu_app_secret
,
chat_id
=
settings
.
feishu_chat_id
,
timeout_seconds
=
settings
.
feishu_timeout_seconds
,
poll_interval_seconds
=
settings
.
feishu_poll_interval_seconds
,
),
manual_code_file
=
settings
.
manual_code_file
,
manual_timeout_seconds
=
settings
.
manual_code_timeout_seconds
,
manual_poll_interval_seconds
=
settings
.
manual_code_poll_interval_seconds
,
)
tenant
=
TenantRegistrationPage
(
page
,
settings
.
tenant_home_url
,
provider
,
ImageCaptchaSolver
(
mode
=
settings
.
image_captcha_mode
),
bug_reporter
)
for
_
in
range
(
settings
.
max_register_attempts
):
data
=
enterprise_data
(
settings
.
register_password
,
settings
.
phone_prefix
,
"E2E授权简化"
)
try
:
tenant
.
register_enterprise
(
data
)
admin
=
AdminPage
(
page
,
settings
.
admin_url
)
admin
.
login
(
settings
.
admin_username
,
settings
.
admin_password
)
admin
.
approve_enterprise_certification
(
data
.
contact_name
)
account
=
account_store
()
.
mark_certified
(
data
)
return
account
except
AccountAlreadyRegistered
:
tenant
.
logout_if_needed
()
raise
AssertionError
(
"无法创建新的授权简化测试企业"
)
def
ensure_product_opened
(
page
:
Page
,
settings
,
bug_reporter
,
account
:
E2EAccount
,
product_name
:
str
)
->
None
:
if
product_name
==
"丸友集"
and
account
.
wanyouji_status
==
"opened"
:
return
if
product_name
==
"丸掌柜"
and
account
.
wanzhanggui_status
==
"opened"
:
return
if
not
RUN_OPENING
:
pytest
.
skip
(
f
"{product_name}未开通;设置E2E_AUTH_SIMPLIFICATION_ENABLE_OPENING=1后运行开通链路。"
)
tenant_login
=
TenantRegistrationPage
(
page
,
settings
.
tenant_home_url
,
VerificationCodeProvider
(
mode
=
settings
.
verification_code_mode
),
ImageCaptchaSolver
(
mode
=
settings
.
image_captcha_mode
),
bug_reporter
,
)
subscription
=
TenantSubscriptionPage
(
page
,
settings
.
tenant_home_url
,
tenant_login
,
bug_reporter
)
if
product_name
==
"丸友集"
:
open_wanyouji_by_admin_assign
(
page
,
settings
,
subscription
,
account
)
else
:
subscription
.
login_certified_tenant
(
account
.
phone
,
account
.
password
)
subscription
.
apply_wanzhanggui
(
account
.
phone
)
assert
subscription
.
wanzhanggui_available
(),
"丸掌柜未进入已开通状态;当前环境可能要求支付/专用指定开通。"
def
open_wanyouji_by_admin_assign
(
page
:
Page
,
settings
,
subscription
:
TenantSubscriptionPage
,
account
:
E2EAccount
)
->
None
:
"""Open the Wanyouji internal-test version through SaaS-admin's merchant subscription panel."""
from
e2e
.
tests
.
SaaS
订阅邀请登录
.
test_prodsol_assign_001_admin_assign_open
import
(
click_assign_open_entry
,
close_visible_modals
,
open_merchant_list_new
,
open_merchant_subscription_modal_strict
,
read_subscription_modal_products
,
read_subscription_row_text
,
search_and_find_merchant_row
,
submit_assign_open
,
visible_modal_text
,
)
admin
=
ProductSolutionAdminPage
(
page
,
settings
.
admin_url
)
admin
.
login
(
settings
.
admin_username
,
settings
.
admin_password
)
close_visible_modals
(
page
)
open_merchant_list_new
(
page
)
merchant_row
=
search_and_find_merchant_row
(
page
,
account
)
open_merchant_subscription_modal_strict
(
page
,
merchant_row
)
existing_row
=
read_subscription_row_text
(
page
,
"丸友集"
)
if
"丸友集"
not
in
read_subscription_modal_products
(
page
)
and
"丸友集"
not
in
existing_row
:
click_assign_open_entry
(
page
)
selected_product
,
selected_version
=
select_wanyouji_internal_assign_version
(
page
)
assert
selected_product
==
"丸友集"
,
f
"指定开通选择的产品错误:{selected_product}"
assert
selected_version
==
"内测版"
,
f
"指定开通选择的版本错误:{selected_version}"
if
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_PAUSE_BEFORE_ASSIGN_SUBMIT"
,
"0"
)
.
strip
()
.
lower
()
in
{
"1"
,
"true"
,
"yes"
}:
page
.
pause
()
submit_assign_open
(
page
)
close_visible_modals
(
page
)
open_merchant_list_new
(
page
)
merchant_row
=
search_and_find_merchant_row
(
page
,
account
)
open_merchant_subscription_modal_strict
(
page
,
merchant_row
)
row_text
=
read_subscription_row_text
(
page
,
"丸友集"
)
if
"内测版"
not
in
row_text
:
switch_wanyouji_subscription_to_internal_test
(
page
,
account
.
enterprise_name
)
close_visible_modals
(
page
)
open_merchant_list_new
(
page
)
merchant_row
=
search_and_find_merchant_row
(
page
,
account
)
open_merchant_subscription_modal_strict
(
page
,
merchant_row
)
row_text
=
read_subscription_row_text
(
page
,
"丸友集"
)
assert
"丸友集"
in
row_text
and
"内测版"
in
row_text
,
(
f
"后台指定开通后订阅未展示丸友集内测版:merchant={account.enterprise_name}; row={row_text}; "
f
"modal={visible_modal_text(page)[:1200]}"
)
close_visible_modals
(
page
)
if
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_SKIP_TENANT_AVAILABILITY_CHECK"
,
"0"
)
.
strip
()
.
lower
()
in
{
"1"
,
"true"
,
"yes"
}:
return
availability_attempts
=
int
(
os
.
getenv
(
"E2E_AUTH_SIMPLIFICATION_AVAILABILITY_ATTEMPTS"
,
"40"
))
for
_
in
range
(
availability_attempts
):
subscription
.
login_certified_tenant
(
account
.
phone
,
account
.
password
)
try
:
if
subscription
.
wanyouji_available
():
return
except
AssertionError
:
pass
page
.
wait_for_timeout
(
3000
)
raise
AssertionError
(
f
"后台指定开通丸友集内测版后{availability_attempts * 3}秒内未显示已开通"
)
def
switch_wanyouji_subscription_to_internal_test
(
page
:
Page
,
enterprise_name
:
str
)
->
None
:
"""Correct a default-offered Wanyouji subscription through the admin version-change action."""
from
e2e
.
tests
.
SaaS
订阅邀请登录
.
test_prodsol_wyjadjust_001_wanyouji_adjust_default
import
(
click_subscription_product_action
,
fill_adjust_reason
,
)
from
e2e
.
tests
.
SaaS
订阅邀请登录
.
test_prodsol_wyjmenu_001_wanyouji_menu_button_config
import
(
select_internal_test_version
,
submit_internal_test_version_adjustment
,
)
click_subscription_product_action
(
page
,
"丸友集"
,
[
"调整版本"
])
select_internal_test_version
(
page
)
fill_adjust_reason
(
page
,
f
"{CASE_PREFIX} {enterprise_name} 切换内测版"
)
submit_internal_test_version_adjustment
(
page
)
def
select_wanyouji_internal_assign_version
(
page
:
Page
)
->
tuple
[
str
,
str
]:
"""Select exactly 丸友集 / 内测版 in the active 指定开通 modal."""
selected_product
=
str
(
page
.
evaluate
(
"""({ expected, side }) => {
const root = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
.reverse().find(el => el.offsetParent !== null) || document.body;
const compact = value => (value || '').replace(/
\\
s+/g, '').trim();
const rootRect = root.getBoundingClientRect();
const candidates = Array.from(root.querySelectorAll('*'))
.filter(el => el.offsetParent !== null)
.filter(el => compact(el.innerText || el.textContent) === expected)
.filter(el => {
const rect = el.getBoundingClientRect();
return side === 'left' ? rect.x < rootRect.x + rootRect.width * 0.55 : rect.x >= rootRect.x + rootRect.width * 0.38;
})
.sort((a, b) => {
const ar = a.getBoundingClientRect(); const br = b.getBoundingClientRect();
return (ar.width * ar.height) - (br.width * br.height);
});
const target = candidates[0];
if (!target) return '';
const clickable = target.closest('label, .ant-radio-wrapper, .ant-checkbox-wrapper, li, .ant-list-item, .ant-card, .ant-table-row, tr, button, a, [role="button"]') || target;
clickable.scrollIntoView({ block: 'center', inline: 'center' });
clickable.click();
return expected;
}"""
,
{
"expected"
:
"丸友集"
,
"side"
:
"left"
}))
assert
selected_product
==
"丸友集"
,
"指定开通弹框未找到丸友集产品候选"
page
.
wait_for_timeout
(
900
)
selected_version
=
str
(
page
.
evaluate
(
"""({ expected }) => {
const root = Array.from(document.querySelectorAll('.ant-modal, .ant-drawer'))
.reverse().find(el => el.offsetParent !== null) || document.body;
const compact = value => (value || '').replace(/
\\
s+/g, '').trim();
const rootRect = root.getBoundingClientRect();
const candidates = Array.from(root.querySelectorAll('*'))
.filter(el => el.offsetParent !== null)
.filter(el => compact(el.innerText || el.textContent) === expected)
.filter(el => el.getBoundingClientRect().x >= rootRect.x + rootRect.width * 0.38)
.sort((a, b) => {
const ar = a.getBoundingClientRect(); const br = b.getBoundingClientRect();
return (ar.width * ar.height) - (br.width * br.height);
});
const target = candidates[0];
if (!target) return '';
const clickable = target.closest('label, .ant-radio-wrapper, .ant-checkbox-wrapper, li, .ant-list-item, .ant-card, .ant-table-row, tr, button, a, [role="button"]') || target;
clickable.scrollIntoView({ block: 'center', inline: 'center' });
clickable.click();
return expected;
}"""
,
{
"expected"
:
"内测版"
}))
return
selected_product
,
selected_version
def
assert_authorized_and_dispatch
(
wanzhanggui_page
:
Page
,
wanyouji_page
:
Page
,
settings
,
bug_reporter
,
workshop
:
E2EAccount
)
->
None
:
push
=
WanzhangguiPushPage
(
wanzhanggui_page
,
settings
.
tenant_home_url
,
bug_reporter
)
push
.
login_main_account
(
FIXED_PHONE
,
FIXED_PASSWORD
)
authorization_frame
=
push
.
open_workshop_authorization
()
assert
push
.
workshop_authorized
(
authorization_frame
,
workshop
.
enterprise_name
),
"工作室授权未启用"
strategy_frame
=
push
.
open_dispatch_strategy
()
assert
workshop
.
enterprise_name
in
strategy_frame
.
locator
(
"body"
)
.
inner_text
(
timeout
=
5000
),
"未找到自动生成的工作室策略"
stamp
=
datetime
.
now
()
.
strftime
(
"
%
m
%
d
%
H
%
M
%
S"
)
order
=
ManualOrderData
(
source_order_no
=
f
"AUTH{stamp}"
,
order_title
=
f
"授权简化{stamp}"
,
strategy_name
=
workshop
.
enterprise_name
,
strategy_type
=
"单平台指定"
,
receive_price
=
manual_order_receive_price
(),
dispatch_price
=
"1"
,
role_name
=
f
"E2E角色{stamp[-4:]}"
,
)
order_page
=
WanzhangguiOrderPage
(
wanzhanggui_page
,
settings
.
tenant_home_url
,
bug_reporter
)
order_page
.
login_main_account
(
FIXED_PHONE
,
FIXED_PASSWORD
)
order_page
.
create_manual_order
(
order
)
fulfillment
=
WanyoujiFulfillmentPage
(
wanyouji_page
,
settings
.
tenant_home_url
,
bug_reporter
)
fulfillment
.
login_main_account
(
workshop
.
phone
,
workshop
.
password
)
fulfillment
.
verify_order_reached_hall
(
order
.
source_order_no
,
order
.
order_title
,
"私单"
)
account_store
()
.
mark_workshop_authorized
(
workshop
,
FIXED_PHONE
)
account_store
()
.
mark_dispatch_strategy_created
(
workshop
,
FIXED_PHONE
,
workshop
.
enterprise_name
,
"单平台指定"
)
account_store
()
.
mark_order_dispatched
(
wanzhanggui_phone
=
FIXED_PHONE
,
source_order_no
=
order
.
source_order_no
,
order_title
=
order
.
order_title
,
strategy_name
=
order
.
strategy_name
,
strategy_type
=
order
.
strategy_type
,
wanyouji_phones
=
[
workshop
.
phone
],
)
def
verify_recommendation_detail
(
app
:
AuthorizationSimplificationPage
,
name
:
str
,
label
:
str
,
profile
:
dict
[
str
,
str
])
->
None
:
last_error
:
AssertionError
|
None
=
None
for
attempt
in
range
(
2
):
frame
=
app
.
open_wanzhanggui_workshop_authorization_new
()
if
label
==
"工作室名称"
else
app
.
open_wanyouji_merchant_recommendation
()
app
.
search_by_name
(
frame
,
label
,
name
)
try
:
text
=
app
.
require_row
(
frame
,
name
)
for
value
in
profile
.
values
():
assert
value
in
text
,
f
"推荐记录未展示配置值 {value};text={text[:1800]}"
return
except
AssertionError
as
exc
:
last_error
=
exc
if
attempt
==
0
:
app
.
page
.
reload
(
wait_until
=
"domcontentloaded"
)
app
.
page
.
wait_for_timeout
(
1200
)
raise
last_error
or
AssertionError
(
f
"推荐页未找到工作室:{name}"
)
def
verify_admin_recommendation
(
page
:
Page
,
settings
,
name
:
str
,
profile
:
dict
[
str
,
str
])
->
None
:
admin_search
(
page
,
settings
,
"推荐评分配置"
,
name
)
text
=
page
.
locator
(
"body"
)
.
inner_text
(
timeout
=
5000
)
assert
name
in
text
,
f
"Admin推荐评分配置未找到对象 {name}"
for
value
in
[
profile
[
"intro"
],
profile
[
"contact"
],
profile
[
"phone"
],
profile
[
"wechat"
],
profile
[
"qq"
]]:
assert
value
in
text
,
f
"Admin推荐评分配置未展示 {value}"
def
verify_admin_merchant_supply
(
page
:
Page
,
settings
,
name
:
str
)
->
None
:
admin_search
(
page
,
settings
,
"商户供应链管理"
,
name
)
assert
name
in
page
.
locator
(
"body"
)
.
inner_text
(
timeout
=
5000
),
f
"商户供应链管理未搜索到 {name}"
def
admin_search
(
page
:
Page
,
settings
,
menu
:
str
,
keyword
:
str
)
->
None
:
from
e2e.pages.admin_page
import
AdminPage
admin
=
AdminPage
(
page
,
settings
.
admin_url
)
admin
.
login
(
settings
.
admin_username
,
settings
.
admin_password
)
admin
.
click_exact_text
(
"供应链管理"
,
timeout
=
5000
)
admin
.
click_exact_text
(
menu
,
timeout
=
5000
)
page
.
wait_for_timeout
(
1200
)
field
=
page
.
locator
(
"#objectName"
)
if
menu
==
"推荐评分配置"
else
page
.
locator
(
"input[placeholder*='名称'], input[placeholder*='商户']"
)
.
first
if
not
field
.
is_visible
(
timeout
=
1500
):
field
=
page
.
locator
(
"input:not([type='hidden']):not([disabled]):not([readonly])"
)
.
first
field
.
fill
(
keyword
)
if
menu
==
"推荐评分配置"
:
clicked
=
page
.
evaluate
(
"""() => {
const wanted = '
\\
u67e5
\\
u8be2';
const visible = el => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"], span, div'))
.filter(visible)
.filter(el => (el.innerText || el.textContent || '').replace(/
\\
s+/g, '') === wanted)
.sort((left, right) => {
const a = left.getBoundingClientRect();
const b = right.getBoundingClientRect();
return a.width * a.height - b.width * b.height;
});
const target = nodes[0];
if (!target) return false;
(target.closest('button, a, [role="button"]') || target).click();
return true;
}"""
)
assert
clicked
,
"推荐评分配置未找到查询按钮"
else
:
try
:
admin
.
click_exact_text
(
"搜索"
,
timeout
=
1500
)
except
AssertionError
:
field
.
press
(
"Enter"
)
page
.
wait_for_timeout
(
1200
)
def
display_profile
(
account
:
E2EAccount
)
->
dict
[
str
,
str
]:
suffix
=
account
.
phone
[
-
4
:]
return
{
"intro"
:
f
"E2E授权展示{suffix}"
,
"game"
:
"三角洲行动"
,
"settlement"
:
"周结"
,
"contact"
:
f
"联系人{suffix}"
,
"phone"
:
f
"1390000{suffix}"
,
"wechat"
:
f
"e2e-wx-{suffix}"
,
"qq"
:
f
"9{suffix}"
,
}
def
ensure_qr_placeholder
()
->
Path
:
path
=
Path
(
"runtime/auth_simplification_qr.png"
)
path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
if
not
path
.
exists
():
path
.
write_bytes
(
bytes
.
fromhex
(
"89504e470d0a1a0a0000000d4948445200000001000000010802000000907753de0000000c4944415408d763f8ffff3f0005fe02fea73581e80000000049454e44ae426082"
))
return
path
.
resolve
()
def
save_flow_artifact
(
account
:
E2EAccount
,
profile
:
dict
[
str
,
str
],
stage
:
str
)
->
None
:
path
=
Path
(
"runtime/auth_simplification_flow.json"
)
path
.
write_text
(
__import__
(
"json"
)
.
dumps
({
"phone"
:
account
.
phone
,
"enterprise_name"
:
account
.
enterprise_name
,
"profile"
:
profile
,
"stage"
:
stage
},
ensure_ascii
=
False
),
encoding
=
"utf-8"
,
)
def
latest_flow_account
(
required_stage
:
str
)
->
tuple
[
E2EAccount
,
dict
[
str
,
str
]]:
import
json
path
=
Path
(
"runtime/auth_simplification_flow.json"
)
if
not
path
.
exists
():
pytest
.
skip
(
"未找到授权简化前序流程产物,请先执行AUTO-FLOW-AUTH-001/002/003。"
)
payload
=
json
.
loads
(
path
.
read_text
(
encoding
=
"utf-8"
))
if
payload
.
get
(
"stage"
)
!=
required_stage
:
pytest
.
skip
(
f
"前序流程未达到 {required_stage},当前={payload.get('stage')}"
)
account
=
account_store
()
.
find_main
(
str
(
payload
[
"phone"
]))
assert
account
,
f
"台账未找到前序企业账号 {payload['phone']}"
return
account
,
dict
(
payload
.
get
(
"profile"
)
or
{})
e2e/tests/丸掌柜丸友集授权简化/test_e2e_auth_simplification_player_settlement.py
0 → 100644
View file @
31fb1489
from
__future__
import
annotations
import
os
import
pytest
from
playwright.sync_api
import
Page
from
e2e
.
tests
.
履约验收
.
test_e2e_delivery_001_player_submit_acceptance
import
(
test_
打手确认上号上传首图并提交验收后主账号查看
as
run_delivery_001
,
)
from
e2e
.
tests
.
履约验收
.
test_e2e_delivery_002_upstream_acceptance_settlement
import
(
test_
丸友集验收打手子单并校验打手收入
as
run_delivery_002
,
)
from
e2e
.
tests
.
订单状态流转
.
order_flow_member_helpers
import
ensure_wanyouji_player_member
from
e2e
.
tests
.
订单流转
.
test_e2e_task_001_external_order_dispatch_to_player
import
(
test_
丸掌柜外部派单由丸友集接单并发到打手抢单大厅
as
run_task_001
,
)
from
e2e
.
tests
.
结算
.
test_e2e_settle_player_account_001_payment_account
import
(
test_
打手完善打款账户信息
as
run_settle_player_account_001
,
)
from
e2e
.
tests
.
结算
.
test_e2e_settle_player_apply_001_create_bill
import
(
test_
打手在我的收入中点击一键结算并提交账单
as
run_settle_player_apply_001
,
)
from
e2e.utils.account_store
import
account_store
WORKSHOP_PHONE
=
"19041265882"
WANZHANGGUI_PHONE
=
"19159770995"
PLAYER_PHONE
=
"19012864298"
@
pytest
.
mark
.
e2e_ledger
@
pytest
.
mark
.
e2e_chain
def
test_auth_simplification_player_to_settlement
(
page
:
Page
,
settings
,
bug_reporter
,
monkeypatch
)
->
None
:
store
=
account_store
()
workshop
=
store
.
find_main
(
WORKSHOP_PHONE
)
assert
workshop
and
workshop
.
wanyouji_status
==
"opened"
,
"授权简化工作室未处于丸友集已开通状态"
monkeypatch
.
setenv
(
"E2E_PLAYER_PHONE"
,
PLAYER_PHONE
)
player
=
ensure_wanyouji_player_member
(
page
=
page
,
settings
=
settings
,
bug_reporter
=
bug_reporter
,
parent
=
workshop
)
latest_order
=
next
(
(
order
for
order
in
reversed
(
store
.
load_payload
()
.
get
(
"dispatched_orders"
,
[]))
if
order
.
get
(
"wanzhanggui_phone"
)
==
WANZHANGGUI_PHONE
and
WORKSHOP_PHONE
in
order
.
get
(
"wanyouji_phones"
,
[])
and
order
.
get
(
"status"
)
==
"dispatched"
),
None
,
)
assert
latest_order
,
"未找到授权简化策略最近分发且尚未被打手处理的订单"
monkeypatch
.
setenv
(
"E2E_TASK_WANZHANGGUI_PHONE"
,
WANZHANGGUI_PHONE
)
monkeypatch
.
setenv
(
"E2E_TASK_WANYOUJI_PHONE"
,
WORKSHOP_PHONE
)
monkeypatch
.
setenv
(
"E2E_TASK_PLAYER_PHONE"
,
player
.
phone
)
monkeypatch
.
setenv
(
"E2E_TASK_SOURCE_ORDER_NO"
,
latest_order
[
"source_order_no"
])
monkeypatch
.
setenv
(
"E2E_TASK_ASSIGNMENT_METHOD"
,
"抢单大厅"
)
monkeypatch
.
setenv
(
"E2E_DELIVERY_SOURCE_ORDER_NO"
,
latest_order
[
"source_order_no"
])
monkeypatch
.
setenv
(
"E2E_REUSE_TENANT_LOGIN"
,
"1"
)
monkeypatch
.
setenv
(
"E2E_REUSE_PLAYER_LOGIN"
,
"1"
)
monkeypatch
.
setenv
(
"E2E_SETTLE_PLAYER_APPLY_SKIP_OWNER_SUMMARY"
,
"1"
)
monkeypatch
.
setenv
(
"E2E_PLAYER_SETTLEMENT_FAST_SEARCH"
,
"1"
)
run_task_001
(
page
,
settings
,
bug_reporter
)
run_delivery_001
(
page
,
settings
,
bug_reporter
)
run_delivery_002
(
page
,
settings
,
bug_reporter
)
run_settle_player_account_001
(
page
,
settings
,
bug_reporter
)
run_settle_player_apply_001
(
page
,
settings
,
bug_reporter
)
e2e/tests/丸掌柜丸友集授权简化/丸掌柜 × 丸友集授权简化提测文档.pdf
0 → 100644
View file @
31fb1489
File added
e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化.pdf
0 → 100644
View file @
31fb1489
File added
e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化_测试点及自动化用例.xlsx
0 → 100644
View file @
31fb1489
File added
e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化_测试点及自动化用例_自动化脚本版.xlsx
0 → 100644
View file @
31fb1489
File added
scripts/generate_wanzhanggui_wanyouji_auth_simplification_excel.py
0 → 100644
View file @
31fb1489
from
pathlib
import
Path
from
openpyxl
import
Workbook
from
openpyxl.formatting.rule
import
FormulaRule
from
openpyxl.styles
import
Alignment
,
Font
,
PatternFill
,
Side
from
openpyxl.utils
import
get_column_letter
OUTPUT
=
Path
(
"e2e/tests/丸掌柜丸友集授权简化/丸掌柜丸友集授权简化_测试点及自动化用例.xlsx"
)
FEATURE_POINTS
=
[
(
"FP-001"
,
"菜单与迁移"
,
"旧授权页下线与数据迁移"
,
"旧页面入口不可访问;原生效中授权同步到新授权列表且状态为已授权"
,
"P0"
,
"自动化"
),
(
"FP-002"
,
"丸掌柜授权页"
,
"菜单与Tab"
,
"工作室授权菜单可访问,包含工作室推荐、工作室授权、待我审核三个Tab"
,
"P0"
,
"自动化"
),
(
"FP-003"
,
"丸友集授权页"
,
"菜单与Tab"
,
"发单商户授权菜单可访问,包含发单商户推荐、发单商户授权、待我审核三个Tab"
,
"P0"
,
"自动化"
),
(
"FP-004"
,
"推荐列表"
,
"展示范围"
,
"仅展示开启对外展示的合作方;推荐Tab中展示可申请记录"
,
"P0"
,
"自动化"
),
(
"FP-005"
,
"授权列表"
,
"展示范围"
,
"授权Tab仅展示发生过授权关系的审核中、已授权、已拒绝、已关闭记录,不展示可申请"
,
"P0"
,
"自动化"
),
(
"FP-006"
,
"待我审核"
,
"展示范围"
,
"仅展示需要当前登录方审核的对向申请;无关申请不可见"
,
"P0"
,
"自动化"
),
(
"FP-007"
,
"推荐列表"
,
"排序"
,
"Admin 配置置顶、推荐、评分后,前台按置顶优先、推荐优先、评分倒序排序"
,
"P1"
,
"自动化"
),
(
"FP-008"
,
"推荐列表"
,
"评分展示"
,
"用户侧仅展示评分,不展示置顶/推荐后台标识"
,
"P1"
,
"自动化"
),
(
"FP-009"
,
"丸掌柜申请"
,
"申请推单"
,
"丸掌柜可对可申请工作室发起申请推单,目标工作室进入待我审核,申请方状态为审核中"
,
"P0"
,
"自动化"
),
(
"FP-010"
,
"丸友集申请"
,
"申请接单"
,
"丸友集可对可申请发单商户发起申请接单,目标商户进入待我审核,申请方状态为审核中"
,
"P0"
,
"自动化"
),
(
"FP-011"
,
"审核"
,
"同意申请"
,
"待我审核中同意后,双方授权列表均更新为已授权"
,
"P0"
,
"自动化"
),
(
"FP-012"
,
"审核"
,
"拒绝申请"
,
"待我审核中拒绝后,双方授权列表均更新为已拒绝,对方可再次申请"
,
"P0"
,
"自动化"
),
(
"FP-013"
,
"授权状态"
,
"关闭授权"
,
"已授权关系可关闭;关闭后双方状态为已关闭,且不可作为可发单协作关系"
,
"P0"
,
"自动化"
),
(
"FP-014"
,
"授权状态"
,
"状态隔离"
,
"审核中/已拒绝/已关闭关系均不可用于供应链默认发单和自动推单策略"
,
"P0"
,
"自动化"
),
(
"FP-015"
,
"联系配置"
,
"入口与弹窗"
,
"授权列表右上角存在联系方法配置入口;点击打开配置弹窗"
,
"P1"
,
"自动化"
),
(
"FP-016"
,
"联系配置"
,
"保存与展示"
,
"双方保存联系方法后,对方在申请、审核及协作场景可查看最新配置"
,
"P1"
,
"自动化"
),
(
"FP-017"
,
"联系配置"
,
"保存交互"
,
"联系人、联系手机、微信号、服务时段编辑后可保存、取消不保存且再次打开正确回显"
,
"P1"
,
"自动化"
),
(
"FP-018"
,
"列表检索"
,
"空态与加载"
,
"筛选无结果时正确展示空态;多条结果的分页/加载不丢失筛选条件和记录"
,
"P1"
,
"自动化"
),
(
"FP-019"
,
"供应链"
,
"授权同步"
,
"授权成功的全部丸友集自动出现在供应链授权列表,默认可发单,无需二次授权"
,
"P0"
,
"自动化"
),
(
"FP-020"
,
"供应链"
,
"名称同步"
,
"供应链列表展示工作室名称和主体;工作室改名后列表名称同步更新"
,
"P1"
,
"自动化"
),
(
"FP-021"
,
"供应链筛选"
,
"默认值"
,
"供应链筛选默认选中丸友集"
,
"P1"
,
"自动化"
),
(
"FP-022"
,
"供应链筛选"
,
"前置校验"
,
"未选择供应链即按工作室名称筛选时提示请先选择供应链,且供应链筛选框标红"
,
"P1"
,
"自动化"
),
(
"FP-023"
,
"推单策略"
,
"自动生成"
,
"每个已授权工作室自动生成一条指定工作室推单策略,策略名称为工作室名称"
,
"P0"
,
"自动化"
),
(
"FP-024"
,
"推单策略"
,
"授权变更联动"
,
"新增授权后创建对应策略;关闭授权后策略可用性按产品规则校验"
,
"P1"
,
"需产品确认关闭后策略规则"
),
(
"FP-025"
,
"推单策略"
,
"工作室选择"
,
"支持一键全选;列表最多显示5行,悬停浮层可查看更多"
,
"P1"
,
"自动化"
),
(
"FP-026"
,
"Admin"
,
"默认供应链"
,
"开通丸掌柜时默认开通所有供应链"
,
"P0"
,
"自动化"
),
(
"FP-027"
,
"Admin"
,
"关闭供应链"
,
"运营可关闭某个供应链;前台可用范围与后台关闭结果一致"
,
"P0"
,
"自动化"
),
(
"FP-028"
,
"权限与异常"
,
"重复/并发申请"
,
"同一合作关系重复提交、双方同时提交时无重复授权记录,状态一致"
,
"P1"
,
"自动化"
),
(
"FP-029"
,
"权限与异常"
,
"访问控制"
,
"未开通对应产品、无菜单权限或非主体账号不可越权查看/审核/关闭授权"
,
"P1"
,
"自动化"
),
(
"FP-030"
,
"对外展示配置"
,
"展示名称规则"
,
"提测文档要求可保存展示名称;旧PRD要求移除展示名称/标题并直接取商户名称,二者冲突,测试前必须确认最终规则"
,
"P0"
,
"待需求确认"
),
(
"FP-031"
,
"丸掌柜授权列表"
,
"列表字段"
,
"工作室名称、工作室主体、联系方式、授权状态、最近申请、操作字段均正确展示"
,
"P0"
,
"自动化"
),
(
"FP-032"
,
"丸友集授权列表"
,
"列表字段"
,
"商户名称、商户主体、联系方式、授权状态、最近申请、操作字段均正确展示"
,
"P0"
,
"自动化"
),
(
"FP-033"
,
"授权列表"
,
"最近申请"
,
"最近申请展示最近一次申请方向及申请时间;新一次申请后按最新记录更新"
,
"P1"
,
"自动化"
),
(
"FP-034"
,
"授权列表筛选"
,
"丸掌柜多条件检索"
,
"支持按工作室名称、工作室主体、手机号、联系人搜索,并支持按授权状态筛选"
,
"P1"
,
"自动化"
),
(
"FP-035"
,
"授权列表筛选"
,
"丸友集多条件检索"
,
"支持按商户名称、商户主体、手机号、联系人搜索,并支持按授权状态筛选"
,
"P1"
,
"自动化"
),
(
"FP-036"
,
"联系配置"
,
"配置字段"
,
"联系方法配置弹窗包含联系人、联系手机、微信号、服务时段四个字段,并按保存结果回显"
,
"P0"
,
"自动化"
),
(
"FP-037"
,
"联系配置"
,
"手机格式"
,
"联系手机为空、合法手机号、非法长度/字符等输入按产品校验,保存失败不污染已保存数据"
,
"P1"
,
"自动化"
),
(
"FP-038"
,
"授权状态"
,
"审核中防重"
,
"当前账号发起的审核中申请仅显示等待审核,不可重复申请"
,
"P0"
,
"自动化"
),
(
"FP-039"
,
"授权状态"
,
"待我审核隔离"
,
"对方发起的待我审核记录仅出现在待我审核Tab,不在授权Tab展示"
,
"P0"
,
"自动化"
),
(
"FP-040"
,
"授权状态"
,
"重新开启权限"
,
"已关闭关系可重新开启授权,且仅最初发起申请的一方有此操作权限"
,
"P0"
,
"自动化"
),
(
"FP-041"
,
"授权状态"
,
"重新开启流转"
,
"发起方重新开启已关闭授权后,双方关系恢复已授权并恢复协作资格"
,
"P0"
,
"自动化"
),
(
"FP-042"
,
"供应链"
,
"二次授权豁免范围"
,
"仅丸友集的已授权合作关系免填写信息二次授权;代练丸子及其他外部代练平台仍按原规则填写信息授权"
,
"P0"
,
"自动化"
),
(
"FP-043"
,
"供应链"
,
"授权范围一致性"
,
"供应链列表包含所有且仅包含授权成功的丸友集;审核中、拒绝、关闭和未授权工作室不出现/不可发单"
,
"P0"
,
"自动化"
),
(
"FP-044"
,
"推单策略"
,
"策略类型与对象"
,
"自动生成策略为指定工作室策略,供应链为丸友集,授权商户号为对应单个工作室"
,
"P0"
,
"自动化"
),
(
"FP-045"
,
"推单策略"
,
"创建幂等"
,
"对同一工作室的授权同步/重复事件不产生重复同名自动策略"
,
"P1"
,
"自动化"
),
(
"FP-046"
,
"推单策略"
,
"选项范围"
,
"授权商户号下拉仅可选已授权工作室;选择全部后与全部可用工作室一致"
,
"P1"
,
"自动化"
),
(
"FP-047"
,
"Admin推荐配置"
,
"配置项生效"
,
"Admin 的评分编辑、推荐状态、置顶状态保存后立即影响前台推荐排序"
,
"P1"
,
"自动化"
),
(
"FP-048"
,
"Admin供应链"
,
"后台入口变更"
,
"商户供应链管理保留,供应链授权入口下线;默认开通和单项关闭在保留入口中生效"
,
"P1"
,
"自动化"
),
(
"FP-049"
,
"展示边界"
,
"对外展示开关"
,
"关闭对外展示的商户/工作室不进入对方推荐列表;重新开启后可按排序规则出现"
,
"P0"
,
"自动化"
),
(
"FP-050"
,
"状态与数据一致性"
,
"双端一致性"
,
"每次申请、审核、关闭、重开后,两端记录的关系状态、联系人信息、最近申请方向与时间一致"
,
"P0"
,
"自动化"
),
(
"FP-051"
,
"对外展示配置"
,
"丸掌柜资料维护"
,
"首次进入可查看当前配置;未配置为空态;保存、修改、重新进入均正确回显;不能修改其他商户数据"
,
"P0"
,
"自动化"
),
(
"FP-052"
,
"对外展示配置"
,
"展示资料字段"
,
"按最终确认规则校验展示名称/商户名称、简介、擅长游戏、结算方式、联系人和联系方式的保存、修改、回显及对方可见"
,
"P0"
,
"自动化"
),
(
"FP-053"
,
"对外展示配置"
,
"丸友集候选影响"
,
"工作室关闭对外展示后不再进入丸掌柜候选列表;已有已授权关系保留,仅影响新候选与新申请"
,
"P0"
,
"自动化"
),
(
"FP-054"
,
"关闭授权"
,
"关联配置失效"
,
"关闭已授权关系后,对应供应链授权和发单策略同步失效,不能再用于新发单"
,
"P0"
,
"自动化"
),
(
"FP-055"
,
"历史授权迁移"
,
"迁移范围"
,
"仅原授权管理中已生效授权迁入新模型,并同步工作室授权和供应链配置"
,
"P0"
,
"自动化"
),
(
"FP-056"
,
"历史授权迁移"
,
"迁移结果对照"
,
"迁移完成生成结果表;源数据、新授权、供应链配置逐条可对照,失败数据可定位"
,
"P0"
,
"自动化"
),
(
"FP-057"
,
"历史授权迁移"
,
"迁移幂等"
,
"重复调用迁移接口不产生重复授权关系或重复供应链配置"
,
"P0"
,
"自动化"
),
(
"FP-058"
,
"策略配置"
,
"授权对象可选性"
,
"授权通过后可在策略配置选择对应工作室、保存并回显;未授权工作室不可选"
,
"P0"
,
"自动化"
),
(
"FP-059"
,
"WYJ新发单"
,
"授权状态拦截"
,
"仅已授权工作室允许创建WYJ新订单;审核中、已拒绝、已关闭、未申请均拦截且提示清晰"
,
"P0"
,
"自动化"
),
(
"FP-060"
,
"WYJ新发单"
,
"多发子单校验"
,
"多工作室/多子单推送时,每个实际子单均依据其目标工作室的当前授权状态独立校验"
,
"P0"
,
"自动化"
),
(
"FP-061"
,
"WYJ新发单"
,
"新旧授权切换"
,
"WYJ新发单只依赖新双边授权模型,不依赖旧授权码、license或appSecret"
,
"P0"
,
"自动化"
),
(
"FP-062"
,
"发单回归"
,
"非WYJ供应链不受影响"
,
"丸子及其他非WYJ供应链仍可按原规则发单,不能被WYJ新授权校验误拦截"
,
"P0"
,
"自动化"
),
(
"FP-063"
,
"存量商户补偿"
,
"供应链服务初始化"
,
"存量丸掌柜调用服务补偿后,丸友集、丸掌柜、一起玩供应链/服务均开通,且生成结果表"
,
"P0"
,
"自动化"
),
(
"FP-064"
,
"存量商户补偿"
,
"补偿可追溯与幂等"
,
"补偿结果可逐条对照、失败可追踪;重复调用不产生重复服务或供应链配置"
,
"P0"
,
"自动化"
),
(
"FP-065"
,
"订阅自动初始化"
,
"订阅触发"
,
"新商户订阅开通丸掌柜成功后自动触发供应链和服务初始化,无需人工补偿"
,
"P0"
,
"自动化"
),
(
"FP-066"
,
"订阅自动初始化"
,
"初始化可靠性"
,
"重复订阅消息不产生重复配置;初始化失败日志可追踪并支持重试"
,
"P0"
,
"自动化"
),
(
"FP-067"
,
"影响范围"
,
"旧授权页面下线"
,
"原license授权信息页面/后台耦合路径下线或不再作为新发单依据;新双边授权页面承接业务"
,
"P1"
,
"自动化"
),
(
"FP-068"
,
"范围控制"
,
"非本次范围回归"
,
"订阅取消、订阅到期不影响现有双边授权;存量订单售后/撤销/验收/申诉/结算与大厅多工作室模式不纳入本次功能变更"
,
"P2"
,
"手工回归"
),
]
CASES
=
[
(
"AUTO-AUTH-001"
,
"历史生效授权迁移"
,
"丸掌柜、丸友集存在旧授权页生效中关系"
,
"发布新版本后进入双方新授权页"
,
"双方授权Tab均存在对应关系且状态已授权;旧入口下线"
,
"FP-001"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-002"
,
"丸掌柜申请推单-工作室同意"
,
"丸掌柜与对外展示工作室无授权关系"
,
"丸掌柜推荐Tab申请推单;工作室待我审核同意;双方回查"
,
"双方状态由可申请/审核中变为已授权;出现供应链记录和同名指定工作室策略"
,
"FP-004,FP-006,FP-009,FP-011,FP-019,FP-023"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-003"
,
"丸友集申请接单-商户同意"
,
"丸友集与对外展示商户无授权关系"
,
"丸友集推荐Tab申请接单;商户待我审核同意;双方回查"
,
"双方状态变已授权;供应链与策略自动同步"
,
"FP-003,FP-004,FP-006,FP-010,FP-011,FP-019,FP-023"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-004"
,
"申请拒绝与重新申请"
,
"存在待审核申请"
,
"审核方拒绝;申请方确认已拒绝;再次发起申请"
,
"拒绝后双方状态已拒绝,可重新申请并重新进入审核中"
,
"FP-012"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-005"
,
"关闭授权及协作隔离"
,
"双方已授权且供应链、策略已生成"
,
"任一方关闭授权;回查双方授权、供应链和策略可用性"
,
"双方已关闭;关闭关系不再用于默认可发单或自动化协作"
,
"FP-013,FP-014,FP-024"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-006"
,
"Tab数据隔离"
,
"准备可申请、审核中、已授权、已拒绝、已关闭及非当前方审核数据"
,
"分别打开推荐、授权、待我审核Tab"
,
"两端菜单及三个Tab均可访问;各Tab仅展示规定数据集合,无关待审数据不可见"
,
"FP-002,FP-003,FP-004,FP-005,FP-006"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-007"
,
"推荐排序与前台隐藏配置"
,
"Admin配置多条记录的置顶、推荐、评分"
,
"打开两端推荐列表,记录顺序和展示字段"
,
"顺序为置顶、推荐、评分倒序;仅评分对用户可见"
,
"FP-007,FP-008"
,
"P1"
,
"Admin+双账号"
),
(
"AUTO-AUTH-008"
,
"联系方法配置及对方可见"
,
"双方可进入授权列表"
,
"丸掌柜保存联系方法;取消修改后重开确认未保存;丸友集在申请/审核详情查看;反向执行"
,
"入口与弹窗可用,保存后对方展示最新信息;取消不改变已保存数据"
,
"FP-015,FP-016,FP-017"
,
"P1"
,
"双账号"
),
(
"AUTO-AUTH-009"
,
"供应链名称同步与筛选"
,
"已授权工作室存在,且可改名"
,
"改工作室名称;进入供应链列表;验证默认筛选;不选供应链直接搜索"
,
"名称和主体同步;默认丸友集;提示请先选择供应链且筛选框标红"
,
"FP-020,FP-021,FP-022"
,
"P1"
,
"双账号"
),
(
"AUTO-AUTH-010"
,
"推单策略展示与全选"
,
"存在6家以上已授权工作室"
,
"打开策略配置,触发全选并查看列表第6项"
,
"全选生效;列表最多5行;悬停可查看更多;策略名称与工作室名一致"
,
"FP-023,FP-025"
,
"P1"
,
"单账号"
),
(
"AUTO-AUTH-011"
,
"丸掌柜开通默认供应链与后台关闭"
,
"待开通丸掌柜的商户;Admin可操作供应链"
,
"开通丸掌柜;检查供应链;Admin关闭其中一项;回查前台"
,
"开通后默认全开;关闭项在前台不可用/不可见,其他项正常"
,
"FP-026,FP-027"
,
"P0"
,
"Admin+单账号"
),
(
"AUTO-AUTH-012"
,
"重复与并发申请防重"
,
"双方无授权关系,可分别发起申请"
,
"同方重复点击申请;双方近同时申请;回查记录"
,
"仅保留一条关系,不产生重复待审/授权记录,双方状态一致"
,
"FP-028"
,
"P1"
,
"双账号"
),
(
"AUTO-AUTH-013"
,
"权限控制"
,
"准备无菜单权限、未开通产品、非主体账号"
,
"尝试访问授权页、申请、审核和关闭"
,
"无权限用户不能执行越权操作,接口和页面均受控"
,
"FP-029"
,
"P1"
,
"多账号"
),
(
"AUTO-AUTH-014"
,
"配置名称简化与列表字段"
,
"准备丸掌柜及丸友集主体,并配置对外展示"
,
"进入对外展示配置和两端授权列表;修改商户名称后回查"
,
"无展示名称/标题字段;对外名称取商户名称;双方列表字段完整且名称同步"
,
"FP-030,FP-031,FP-032"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-015"
,
"授权列表五维筛选与最近申请"
,
"存在名称、主体、联系人、手机号、状态不同的多条关系,且数量超过单页"
,
"两端逐一按4个文本条件和授权状态筛选;执行无结果查询和翻页;产生新申请后回查最近申请"
,
"每项筛选仅返回匹配记录;无结果正确空态;翻页后筛选条件保持;最近申请方向与时间更新且双端一致"
,
"FP-018,FP-033,FP-034,FP-035,FP-050"
,
"P1"
,
"双账号"
),
(
"AUTO-AUTH-016"
,
"联系方法字段与校验"
,
"双方可打开联系方法配置"
,
"依次填联系人、手机、微信、服务时段保存;再输入非法手机保存"
,
"四字段均回显给对方;非法输入被拦截且不覆盖旧数据"
,
"FP-036,FP-037"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-017"
,
"审核中与待我审核Tab边界"
,
"A 向 B 发起申请"
,
"A 查看授权Tab和推荐Tab;B 查看授权Tab与待我审核Tab;A 再点击申请"
,
"A 显示审核中且不可重复申请;B 待审记录只在待我审核Tab"
,
"FP-005,FP-006,FP-038,FP-039"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-018"
,
"关闭后重开授权的发起方权限"
,
"A 发起并获批的已授权关系"
,
"A 关闭;B 尝试重开;A 重开;双方回查供应链和策略"
,
"仅 A 可重开;重开后状态已授权并恢复协作资格"
,
"FP-040,FP-041,FP-050"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-019"
,
"丸友集供应链二次授权豁免边界"
,
"准备已授权丸友集、外部代练平台及未授权丸友集"
,
"进入供应链授权/发单配置,分别选择三类对象"
,
"仅已授权丸友集默认可发单且无需填授权信息;外部平台仍需填写;未授权对象不可用"
,
"FP-019,FP-042,FP-043"
,
"P0"
,
"单账号"
),
(
"AUTO-AUTH-020"
,
"自动策略属性、幂等与授权范围"
,
"新建一个可授权工作室且无同名策略"
,
"完成授权;读取新策略属性;重复触发同步;打开策略授权商户号下拉"
,
"生成一条指定工作室/丸友集/对应工作室的策略;不重复生成;下拉仅有已授权工作室"
,
"FP-023,FP-044,FP-045,FP-046"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-021"
,
"对外展示开关与Admin推荐配置"
,
"Admin 可配置目标的对外展示、置顶、推荐、评分"
,
"依次关闭/开启对外展示,设置评分、推荐、置顶后查看前台"
,
"关闭时前台不可见;开启后出现;排序按置顶、推荐、评分倒序,配置变更即时生效"
,
"FP-004,FP-007,FP-047,FP-049"
,
"P0"
,
"Admin+双账号"
),
(
"AUTO-AUTH-022"
,
"Admin供应链入口保留与单项关闭"
,
"丸掌柜已开通,Admin可管理供应链"
,
"检查后台菜单;在商户供应链管理关闭一条丸友集供应链并访问旧供应链授权入口"
,
"商户供应链管理可用;旧供应链授权入口下线;关闭项前台不可发单"
,
"FP-026,FP-027,FP-048"
,
"P1"
,
"Admin+单账号"
),
(
"AUTO-AUTH-023"
,
"双端对外展示配置全流程"
,
"丸掌柜和丸友集工作室均可进入展示配置"
,
"验证空态;保存资料;刷新回显;修改;关闭/开启展示;尝试跨主体访问"
,
"资料可保存、回显、修改;关闭后从对方候选移除但既有授权保留;不可跨主体操作"
,
"FP-051,FP-052,FP-053"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-024"
,
"关闭授权后配置及新单失效"
,
"双方已授权,供应链和策略均可用"
,
"关闭授权;检查供应链、策略;用原策略创建WYJ新单;重新申请并审核通过后重试"
,
"关闭后关联配置失效且新单被拦截;重新授权成功后恢复可用"
,
"FP-013,FP-054,FP-059"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-025"
,
"历史授权迁移与幂等"
,
"准备旧模型已生效、非生效和异常授权数据;可调用迁移接口"
,
"调用迁移;读取新关系、供应链配置和结果表;再次调用迁移"
,
"仅已生效数据迁入;结果可对照并可追踪失败;重复迁移不产生重复数据"
,
"FP-001,FP-055,FP-056,FP-057"
,
"P0"
,
"接口+双账号"
),
(
"AUTO-AUTH-026"
,
"策略配置授权范围"
,
"存在一个已授权和一个未授权丸友集工作室"
,
"新建指定工作室策略;查看下拉;选择已授权工作室保存并重新进入;尝试选择未授权工作室"
,
"已授权工作室可选且策略正确回显;未授权工作室不能选择;关闭后策略不可新发单"
,
"FP-058,FP-054"
,
"P0"
,
"单账号"
),
(
"AUTO-AUTH-027"
,
"WYJ新发单授权状态矩阵"
,
"准备已授权、审核中、已拒绝、已关闭、未申请的工作室及策略"
,
"分别针对五种状态创建WYJ新单,记录页面与接口结果"
,
"仅已授权成功;其余四种均失败且提示清晰"
,
"FP-059,FP-061"
,
"P0"
,
"双账号"
),
(
"AUTO-AUTH-028"
,
"WYJ多子单与非WYJ回归"
,
"准备多工作室发单数据,其中至少一方非已授权;同时有非WYJ供应链"
,
"发起多子单推送;再通过非WYJ供应链发单"
,
"每个子单独立按目标授权判断;非WYJ发单不受新校验影响"
,
"FP-060,FP-062"
,
"P0"
,
"单账号"
),
(
"AUTO-AUTH-029"
,
"存量丸掌柜服务补偿"
,
"准备历史已开通丸掌柜商户;可调用initMerchantSupply接口"
,
"调用补偿;查询商户服务、供应链配置和结果表;重复调用"
,
"丸友集、丸掌柜、一起玩服务均完整;结果可对照;重复调用无重复配置"
,
"FP-063,FP-064"
,
"P0"
,
"接口+Admin"
),
(
"AUTO-AUTH-030"
,
"新订阅自动初始化与失败追踪"
,
"准备未开通丸掌柜的新商户;可观察订阅消息和日志"
,
"完成订阅;等待异步任务;检查服务/供应链;重放消息;构造失败并检查日志与重试"
,
"订阅自动初始化;重放幂等;失败可追踪并可恢复"
,
"FP-065,FP-066"
,
"P0"
,
"订阅链路+Admin"
),
(
"AUTO-AUTH-031"
,
"新旧页面及非改造范围回归"
,
"存在旧授权入口、现有已授权关系和订阅状态变化条件"
,
"访问旧页面和新页面;验证新订单不读取旧凭据;执行取消/到期后回查双边授权"
,
"旧路径不承接新发单;新模型生效;取消/到期不影响现有双边授权"
,
"FP-067,FP-068"
,
"P1"
,
"单账号+接口"
),
]
DETAILED_CASES
=
[
(
"DT-001"
,
"丸掌柜对外展示配置保存、回显与隔离"
,
"丸掌柜商户A、商户B均可登录;A有配置入口"
,
"展示名称规则以产品最终确认结论为准;简介、游戏、结算方式、联系人、手机、微信、服务时段"
,
"1. 以A登录,进入工作室授权-展示配置。
\n
2. 首次未配置时核对空态和保存入口。
\n
3. 填写全部展示资料并保存。
\n
4. 刷新或退出后重进,逐项核对回显。
\n
5. 修改两项资料并保存,再次重进核对最新值。
\n
6. 切换为B登录,尝试通过页面/接口查看或修改A配置。"
,
"A资料保存、修改、回显一致;B无法读取或修改A私有配置;展示名称字段是否出现按需求确认结论验收。"
,
"保存前后截图、接口请求/响应、A/B主体标识"
),
(
"DT-002"
,
"展示开关对候选与既有授权的影响"
,
"A与工作室W已授权;另有未授权且开启展示的工作室W2"
,
"A、W、W2的展示开关和授权状态"
,
"1. A进入工作室推荐,确认W2可见且可申请。
\n
2. W2关闭对外展示。
\n
3. A刷新推荐列表并按名称搜索W2。
\n
4. A回查与W的已授权记录和发单能力。
\n
5. W2重新开启展示,回查推荐列表。"
,
"关闭展示后W2不在候选/不可申请;既有W授权不丢失;重新开启后W2按排序规则恢复候选。"
,
"推荐列表截图、授权列表截图、筛选结果"
),
(
"DT-003"
,
"丸掌柜发起申请及审核中防重"
,
"丸掌柜A和开启展示的丸友集工作室W无授权关系"
,
"W名称、联系方式、主体"
,
"1. A进入工作室推荐,按名称/主体/联系人/手机号筛选W。
\n
2. 打开W详情,核对展示资料和联系方式。
\n
3. 点击申请推单并确认。
\n
4. 刷新页面,核对状态和最近申请方向/时间。
\n
5. 再次点击申请,或并发提交同一申请。
\n
6. W登录,进入待我审核查看申请。"
,
"A显示审核中/待审核且不能重复生成记录;W仅在待我审核看到该申请;双方最近申请信息一致。"
,
"申请ID、双端列表截图、重复点击提示、接口记录数"
),
(
"DT-004"
,
"丸友集审核通过后的授权、供应链和策略联动"
,
"DT-003申请处于待我审核"
,
"申请ID、目标工作室W"
,
"1. W进入待我审核,核对申请方展示资料。
\n
2. 点击同意并确认。
\n
3. A和W分别进入授权Tab,核对状态已授权。
\n
4. A进入供应链列表,核对W已同步且默认可发单。
\n
5. A进入策略配置,核对W可选及自动策略/指定工作室策略属性。
\n
6. 用该策略创建WYJ新订单。"
,
"双端均已授权;供应链和策略可用;使用W的WYJ新单创建成功。"
,
"双端状态、供应链记录、策略详情、订单号"
),
(
"DT-005"
,
"审核拒绝与重新申请"
,
"存在A对W的待审核申请"
,
"申请ID"
,
"1. W在待我审核点击拒绝。
\n
2. A、W分别刷新授权列表。
\n
3. A核对状态已拒绝及可重新申请按钮。
\n
4. A重新发起申请。
\n
5. W核对新待审记录和最近申请时间。"
,
"双方状态一致为已拒绝;拒绝状态不可发单;重新申请产生新的待审流程而不覆盖历史追溯信息。"
,
"拒绝操作记录、重申请记录、订单创建拦截截图"
),
(
"DT-006"
,
"关闭授权、失效与重新授权"
,
"A和W已授权,且有可用供应链和策略"
,
"授权关系、策略ID"
,
"1. 任选有权限的一方关闭授权并确认。
\n
2. 双端回查状态已关闭。
\n
3. A检查供应链和策略状态。
\n
4. A用原策略创建WYJ新单。
\n
5. 按最终规则重新申请/重新开启并由对方审核通过。
\n
6. 再次创建WYJ新单。"
,
"关闭后供应链/策略失效,新单被拦截;重新审核通过后恢复可发单。重开权限以产品确认规则验收。"
,
"状态变更、策略状态、失败提示、恢复后的订单号"
),
(
"DT-007"
,
"历史有效授权迁移与结果对照"
,
"准备旧授权模型:已生效、未生效、异常数据;具备migrateLicense接口权限"
,
"旧授权主键、商户、工作室、预期供应链配置"
,
"1. 记录迁移前源数据基线。
\n
2. 调用migrateLicense迁移接口。
\n
3. 查询新双边授权、供应链配置和迁移结果表。
\n
4. 逐条对照已生效源授权与新关系。
\n
5. 检查未生效源数据未误迁。
\n
6. 再次调用接口并比较记录数。"
,
"仅有效授权迁入并可发单;供应链同步;结果表可追踪失败;重复迁移不产生重复记录。"
,
"接口响应、迁移结果表、源/目标对照、重复调用前后计数"
),
(
"DT-008"
,
"策略配置可选范围与回显"
,
"A至少有一个已授权工作室W和一个未授权工作室U"
,
"W、U、策略名称"
,
"1. A进入发单策略配置,新建指定工作室策略。
\n
2. 选择丸友集供应链。
\n
3. 打开授权商户号下拉,记录选项。
\n
4. 选择W,填写必填项后保存。
\n
5. 重新打开策略核对名称、供应链、工作室和状态。
\n
6. 尝试通过UI和接口选择U。"
,
"W可选且保存后完整回显;U不可选择/保存被拦截;关闭授权后该策略不可新发单。"
,
"下拉选项、策略详情、非法提交响应"
),
(
"DT-009"
,
"WYJ新发单授权状态矩阵"
,
"为五个工作室准备已授权、审核中、已拒绝、已关闭、未申请状态和相应订单数据"
,
"五个工作室、统一订单模板"
,
"1. 分别指定五个工作室创建WYJ新订单。
\n
2. 记录页面提示、请求响应和订单落库结果。
\n
3. 对已授权工作室继续完成推单。
\n
4. 对四个非已授权状态核对未产生有效订单/子单。"
,
"仅已授权成功;审核中、拒绝、关闭、未申请全部拦截且提示明确。"
,
"五组结果表、订单号/子单号、拦截提示"
),
(
"DT-010"
,
"WYJ多子单独立授权校验与非WYJ回归"
,
"多发策略含多个目标工作室,其中至少一个非已授权;同时有丸子或其他非WYJ供应链"
,
"多子单策略、非WYJ订单"
,
"1. 通过多发策略发起订单。
\n
2. 逐个检查子单的目标工作室和创建结果。
\n
3. 通过非WYJ供应链创建相同类型订单。
\n
4. 检查WYJ新单请求未传/未校验旧授权码、license、appSecret。"
,
"每个WYJ子单独立校验目标授权;非WYJ链路不受影响;WYJ新单不依赖旧凭据。"
,
"子单明细、非WYJ订单号、请求日志/链路日志"
),
(
"DT-011"
,
"存量丸掌柜服务补偿"
,
"存量已开通丸掌柜商户M;具备initMerchantSupply接口权限"
,
"商户M、补偿前服务清单"
,
"1. 记录补偿前供应链和服务。
\n
2. 调用initMerchantSupply。
\n
3. 查询M的商户服务、供应链配置及初始化结果表。
\n
4. 核对丸友集、丸掌柜、一起玩均已开通。
\n
5. 重复调用并检查数据计数。
\n
6. 人为准备失败样本并核对结果可追踪。"
,
"三类服务/供应链完整;结果可对照;重复调用幂等;失败可定位。"
,
"接口响应、服务清单、初始化结果表、前后计数"
),
(
"DT-012"
,
"新订阅自动初始化、幂等与失败恢复"
,
"新商户N未开通丸掌柜;可查看订阅消息/初始化日志"
,
"商户N、订阅单号、消息ID"
,
"1. 完成N的丸掌柜订阅开通。
\n
2. 等待异步初始化完成。
\n
3. 登录N,核对供应链和服务。
\n
4. 执行一次授权申请、策略配置和发单冒烟。
\n
5. 重放同一订阅消息。
\n
6. 模拟初始化失败,检查日志并执行重试。"
,
"无需人工补偿即可初始化;后续链路可用;消息重放无重复配置;失败日志含可检索关联信息且重试可恢复。"
,
"订阅单、消息ID、初始化日志、服务清单、冒烟订单号"
),
(
"DT-013"
,
"Admin推荐排序与用户侧可见性"
,
"至少三条展示记录,Admin可设置评分、推荐和置顶"
,
"三个对象及评分/推荐/置顶组合"
,
"1. 在Admin设置不同评分、推荐、置顶组合并保存。
\n
2. 进入丸掌柜和丸友集推荐列表。
\n
3. 记录排序和用户可见字段。
\n
4. 修改一条配置后刷新前台。"
,
"前台按置顶、推荐、评分倒序;用户仅见允许展示的评分/资料;配置变更正确生效。"
,
"Admin配置截图、两端列表排序截图"
),
(
"DT-014"
,
"新旧入口切换与订阅生命周期不影响授权"
,
"已有双边已授权关系,且可模拟订阅取消/到期"
,
"授权关系、订阅状态"
,
"1. 访问旧license授权入口及新双边授权入口。
\n
2. 验证新WYJ发单只通过新关系判断。
\n
3. 对订阅执行取消或到期模拟。
\n
4. 回查双边授权状态、列表和发单资格。"
,
"旧页面不再作为新发单授权依据;取消/到期不改变现有双边授权模型。"
,
"页面访问结果、发单请求、订阅状态与授权状态对照"
),
]
AUTOMATION_FLOW_CASES
=
[
(
"AUTO-FLOW-AUTH-001"
,
"新建丸友集工作室对外展示配置在丸掌柜与SaaS-admin双端可见"
,
"P0"
,
"已生成脚本,待测试环境首跑"
,
"测试环境;SMS/图形验证码可用;SaaS-admin账号可用;固定丸掌柜账号19159770995可登录且具备工作室授权新菜单权限。"
,
"企业名称:E2E授权展示_{时间戳};注册手机号:运行时新生成;简介:E2E自动化展示简介;擅长游戏:至少选择1项;结算周期:月结;联系人/手机/微信号/QQ号:运行时唯一值;二维码:使用运行时PNG测试文件。"
,
"1. 使用企业注册与认证既有流程创建新的主账号,并记录手机号、企业名称、商户ID。
\n
2. 登录SaaS-admin,进入商户管理-商户列表新,查询该企业并打开查看订阅;点击指定开通,精确选择产品丸友集和版本内测版后提交。
\n
3. 重新打开查看订阅,断言当前生效订阅展示丸友集-内测版;租户端登录后断言丸友集已开通。
\n
4. 登录新建企业,进入丸友集-工作室基础应用-发单商户推荐。
\n
5. 点击右上角对外展示配置,填写简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号并上传二维码。
\n
6. 点击保存,断言弹窗关闭;重新打开配置,断言全部字段回显与本次输入一致。
\n
7. 开启对外展示开关,刷新页面并断言开关为开启状态。
\n
8. 通过dual_account_pages创建新企业丸友集页与19159770995丸掌柜页的隔离上下文。
\n
9. 在19159770995侧进入商家管理-工作室授权新-工作室推荐,按新企业名称查询。
\n
10. 打开查询结果的查看详情,逐项核对工作室名称/主体、简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号和二维码。
\n
11. 打开独立SaaS-admin页,进入供应链管理-推荐评分配置,按对象名称查询新企业。
\n
12. 核对Admin展示信息和联系方式与第5步提交的数据完全一致。
\n
13. 保存三端截图、关键接口响应、企业标识和测试数据;将账号写入台账,供后续授权申请用例复用。"
,
"对外展示配置保存后弹窗关闭且可完整回显;开关开启后,新工作室出现在固定丸掌柜的工作室推荐搜索结果;详情页和SaaS-admin推荐评分配置的展示资料、联系方式、二维码均与新建企业配置一致;三个端的对象名称/主体一致。"
,
"注册认证:e2e/tests/test_e2e_saas_001_enterprise_certification.py;丸友集专用开通:e2e/tests/SaaS订阅邀请登录/test_prodsol_wyjtrial_001_wanyouji_free_trial.py;双账号fixture:e2e/conftest.py;账号台账:e2e/utils/account_store.py。"
,
"新建:WanyoujiDisplayConfigPage(配置、开关、回显);WanzhangguiWorkshopAuthorizationPage(搜索、详情断言);AdminRecommendationScorePage(对象搜索、详情断言)。测试函数使用dual_account_pages,Admin使用独立Page;标记e2e_seed、e2e_chain。"
,
"注册账号、开通版本/订阅记录、配置提交值、两端查询结果、Admin查询结果、截图路径、接口响应摘要。"
,
),
(
"AUTO-FLOW-AUTH-002"
,
"复用新建企业开通丸掌柜并验证商户展示、Admin推荐及供应链初始化"
,
"P0"
,
"已生成脚本,待测试环境首跑"
,
"AUTO-FLOW-AUTH-001已成功执行并将新企业主账号、企业名称、商户ID写入台账;该企业已开通丸友集;固定账号19159770995可登录丸友集且具备发单商户推荐菜单权限;SaaS-admin账号可用。"
,
"复用AUTO-FLOW-AUTH-001的企业名称、主账号、商户ID;丸掌柜展示配置使用与丸友集不同的运行时唯一简介、联系人、手机、微信号、QQ号和二维码;擅长游戏至少选择1项;结算周期:月结。"
,
"1. 从账号台账读取AUTO-FLOW-AUTH-001创建的企业主账号、企业名称和商户ID,先断言该账号已开通丸友集。
\n
2. 复用丸掌柜专用订阅/支付开通链路为同一企业开通丸掌柜,并回查企业概览或订阅记录为已开通。
\n
3. 以该企业主账号登录丸掌柜,进入商家管理-工作室授权新。
\n
4. 点击右上角对外展示配置,填写简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号并上传二维码。
\n
5. 点击保存,断言配置弹框关闭;重新打开弹框,断言全部字段回显正确。
\n
6. 勾选对外展示开关,刷新后断言开关仍为开启。
\n
7. 通过dual_account_pages创建新企业丸掌柜页与19159770995丸友集页的隔离上下文。
\n
8. 在19159770995侧进入丸友集-工作室基础应用-发单商户推荐,按新企业商户名称查询。
\n
9. 打开查询结果的查看详情,逐项核对商户名称/主体、简介、擅长游戏、结算周期、联系人、联系手机、微信号、QQ号和二维码。
\n
10. 打开独立SaaS-admin页,进入供应链管理-推荐评分配置,按对象名称查询该商户,核对展示信息和联系方式与第4步完全一致。
\n
11. 在SaaS-admin进入供应链管理-商户供应链管理,按商户名称搜索。
\n
12. 断言至少返回一条与新企业名称匹配的商户记录;记录商户ID及其支持的供应链。
\n
13. 保存三端截图、关键接口响应和台账链接,供授权申请、策略和发单用例复用。"
,
"丸掌柜开通成功;展示配置保存后弹框关闭且可完整回显;开启对外展示后,固定丸友集账号19159770995可在发单商户推荐中搜索到该商户;详情页和推荐评分配置的展示资料、联系方式、二维码与丸掌柜配置一致;商户供应链管理按商户名至少返回一条对应记录。"
,
"前置账号与企业标识:AUTO-FLOW-AUTH-001台账产物;丸掌柜开通应复用其专用订阅/支付开通链路;双账号fixture:e2e/conftest.py;账号台账:e2e/utils/account_store.py。通用指定开通用例排除丸掌柜,不能直接调用。"
,
"新建:WanzhangguiDisplayConfigPage(配置、开关、回显);WanyoujiMerchantAuthorizationPage(商户搜索、详情断言);AdminRecommendationScorePage(对象搜索、详情断言);AdminMerchantSupplyPage(商户搜索、至少一条结果断言)。测试函数使用dual_account_pages,Admin使用独立Page;标记e2e_chain。"
,
"开通前后订阅状态、丸掌柜配置提交值、丸友集详情结果、推荐评分配置结果、商户供应链管理搜索结果、商户ID/供应链清单、截图路径、接口响应摘要。"
,
),
(
"AUTO-FLOW-AUTH-003"
,
"新建丸友集申请固定丸掌柜并审核后自动生成策略发单至抢单大厅"
,
"P0"
,
"已生成脚本,待测试环境首跑"
,
"AUTO-FLOW-AUTH-001与AUTO-FLOW-AUTH-002均成功;新企业已同时开通丸友集和丸掌柜且已开启两端对外展示;固定账号19159770995已开通丸掌柜、丸友集且具备审核、策略配置、手工录单和抢单大厅权限;测试数据满足手工录单必填条件。"
,
"复用前两条用例的新企业主账号、企业名称和商户ID;固定商户:19159770995(个体工商户1378);自动生成策略名称:新企业名称;订单标题、来源单号、联系电话使用运行时唯一值。"
,
"1. 从台账读取新企业账号和名称,使用dual_account_pages创建新企业丸友集页及19159770995丸掌柜页。
\n
2. 新企业登录丸友集,进入工作室基础应用-发单商户推荐。
\n
3. 按商户名称搜索19159770995对应的个体工商户1378,断言唯一/目标结果存在。
\n
4. 如需核对展示资料,先点击查看详情;随后点击申请推单并确认。
\n
5. 断言新企业丸友集侧申请状态变为审核中;记录申请时间和关联商户。
\n
6. 19159770995丸掌柜侧进入商家管理-工作室授权新,切换待我审核,按新企业名称查询。
\n
7. 点击审核,在审核弹窗点击同意并确认;分别刷新两端授权页,断言状态均为已授权。
\n
8. 19159770995进入履约工作室/平台-工作室授权,按新企业名称查询,断言工作室记录状态为启用。
\n
9. 进入履约工作室/平台-推单配置,按新企业名称查询,断言自动新增/可用策略名称为新企业名称、状态为启用、策略类型为单平台指定,且目标工作室为新企业。
\n
10. 进入商家管理-订单管理-手工录单,填写运行时唯一订单数据,选择第9步策略并提交推单。
\n
11. 断言丸掌柜侧订单创建/推单成功,记录主单号、子单号、来源单号和策略名称。
\n
12. 新企业丸友集进入工作室履约应用-抢单大厅,使用来源单号或订单标题查询。
\n
13. 断言大厅存在对应订单,且订单标题、来源单号、上游商户、目标工作室与丸掌柜侧记录一致。
\n
14. 保存申请、审核、工作室授权、策略详情、订单创建和抢单大厅的截图/接口响应;更新台账授权关系、策略和订单产物。"
,
"新企业可向个体工商户1378发起申请;19159770995审核同意后双方已授权;新企业名称对应的工作室授权为启用;自动生成或可用的推单策略为启用且类型为单平台指定;使用该策略手工录单推单成功;新企业丸友集抢单大厅可查询到完全匹配的订单。"
,
"双账号fixture:e2e/conftest.py;工作室授权/策略:e2e/pages/wanzhanggui_push_page.py、e2e/tests/推单配置/test_e2e_push_001_wanzhanggui_workshop_authorization.py、test_e2e_push_003_dispatch_strategy.py;手工录单与大厅回查:e2e/pages/wanzhanggui_order_page.py、e2e/tests/订单流转/test_e2e_order_001_manual_dispatch.py(如文件路径以当前仓库为准)。"
,
"新建:WanyoujiMerchantRecommendationPage(搜索、详情、申请推单);WanzhangguiWorkshopAuthorizationNewPage(待审、同意、状态);WanzhangguiSupplyAndStrategyPage(工作室启用、自动策略类型);复用WanzhangguiOrderPage创建订单与WanyoujiFulfillmentPage抢单大厅查询。测试函数标记e2e_chain,输入依赖前两条台账产物。"
,
"申请ID与时间、双端授权状态、工作室授权记录、策略ID/类型/状态、主单号/子单号/来源单号、抢单大厅查询结果、截图路径、接口响应摘要。"
,
),
(
"AUTO-FLOW-AUTH-004"
,
"关闭既有授权后重新申请审核并恢复单平台策略发单闭环"
,
"P0"
,
"已生成脚本,待测试环境首跑"
,
"AUTO-FLOW-AUTH-003已成功,且新企业与19159770995间当前关系为已授权;新企业已开通丸友集和丸掌柜;19159770995具备丸掌柜工作室授权、策略及手工录单权限;新企业丸友集具备待我审核和抢单大厅权限。"
,
"复用新企业账号、企业名称和商户ID;固定商户:19159770995(个体工商户1378);重新申请记录ID;恢复后的工作室名称/策略名称:新企业名称;手工录单来源单号、订单标题、联系电话均使用运行时唯一值。"
,
"1. 新企业登录丸友集,进入工作室基础应用-发单商户推荐,切换发单商户授权Tab。
\n
2. 按19159770995对应企业商户搜索,确认当前状态为已授权后点击关闭授权并确认。
\n
3. 刷新两端授权列表,断言该关系状态均为已关闭,并记录关闭前的授权关系ID、策略ID和状态。
\n
4. 新企业切换进入丸掌柜-商家管理-工作室授权新,按19159770995对应企业商户查询。
\n
5. 点击页面操作【申请发单】并确认,记录本次重新申请的申请ID、申请方和被申请方。
\n
6. 新企业切换进入丸友集-工作室基础应用-发单商户推荐,切换待我审核Tab,按关联商户/申请ID查询。
\n
7. 断言存在第5步产生的待审核记录,点击审核,在弹窗点击同意并确认。
\n
8. 刷新新企业丸掌柜与丸友集授权页,断言双方状态恢复为已授权;同时核对申请方向、最近申请时间和审核人记录与步骤5至7一致。
\n
9. 19159770995进入丸掌柜-履约工作室/平台-工作室授权,按新企业名称查询,断言对应工作室状态为启用。
\n
10. 进入履约工作室/平台-推单配置,按新企业名称查询,断言对应策略状态为启用、策略类型为单平台指定、目标工作室为新企业。
\n
11. 进入商家管理-订单管理-手工录单,填写唯一订单数据,选择第10步单平台策略并提交推单。
\n
12. 断言19159770995丸掌柜侧发单成功,记录主单号、子单号、来源单号和策略名称。
\n
13. 新企业丸友集进入工作室履约应用-抢单大厅,按来源单号或订单标题查询。
\n
14. 断言大厅存在本次订单,核对订单标题、来源单号、上游商户、目标工作室、订单状态与丸掌柜侧一致。
\n
15. 保存关闭授权、重新申请、审核、授权恢复、工作室授权、策略、订单和抢单大厅的截图/接口响应,并更新台账关系和订单产物。"
,
"关闭后关系为已关闭;重新申请产生唯一待审记录且由新企业丸友集待我审核可见;同意后双方恢复已授权;19159770995侧工作室授权和新企业名称的单平台指定策略均为启用;使用该策略再次发单成功,新企业丸友集抢单大厅可检索并核对到对应订单。"
,
"前置授权与策略产物:AUTO-FLOW-AUTH-003;双账号fixture:e2e/conftest.py;授权/策略复用:e2e/pages/wanzhanggui_push_page.py与e2e/pages/wanzhanggui_order_page.py;手工录单与抢单大厅复用既有订单发单脚本。"
,
"新建:AuthorizationStateTransitionPage(关闭、按申请ID追踪、待审/同意、双端状态断言);复用WanzhangguiSupplyAndStrategyPage、WanzhangguiOrderPage和WanyoujiFulfillmentPage。测试函数标记e2e_chain,执行前校验申请方向与待审归属。"
,
"关闭授权前后状态、关系ID、重新申请ID/方向/时间、审核记录、策略ID/类型/状态、主单号/子单号/来源单号、抢单大厅结果、截图路径、接口响应摘要。"
,
),
]
def
style_sheet
(
ws
,
widths
):
header_fill
=
PatternFill
(
"solid"
,
fgColor
=
"1F4E78"
)
header_font
=
Font
(
color
=
"FFFFFF"
,
bold
=
True
)
thin
=
Side
(
style
=
"thin"
,
color
=
"D9E2F3"
)
for
cell
in
ws
[
1
]:
cell
.
fill
=
header_fill
cell
.
font
=
header_font
cell
.
alignment
=
Alignment
(
horizontal
=
"center"
,
vertical
=
"center"
,
wrap_text
=
True
)
for
row
in
ws
.
iter_rows
(
min_row
=
2
):
for
cell
in
row
:
cell
.
alignment
=
Alignment
(
vertical
=
"top"
,
wrap_text
=
True
)
cell
.
border
=
__import__
(
"openpyxl"
)
.
styles
.
Border
(
bottom
=
thin
)
for
index
,
width
in
enumerate
(
widths
,
1
):
ws
.
column_dimensions
[
get_column_letter
(
index
)]
.
width
=
width
ws
.
freeze_panes
=
"A2"
ws
.
auto_filter
.
ref
=
ws
.
dimensions
ws
.
row_dimensions
[
1
]
.
height
=
30
def
main
():
wb
=
Workbook
()
ws
=
wb
.
active
ws
.
title
=
"测试点"
ws
.
append
([
"测试点ID"
,
"模块"
,
"测试主题"
,
"测试点/预期"
,
"优先级"
,
"自动化建议"
])
for
row
in
FEATURE_POINTS
:
ws
.
append
(
row
)
style_sheet
(
ws
,
[
15
,
16
,
20
,
68
,
10
,
22
])
ws
.
conditional_formatting
.
add
(
"E2:E200"
,
FormulaRule
(
formula
=
[
'E2="P0"'
],
fill
=
PatternFill
(
"solid"
,
fgColor
=
"F4CCCC"
)))
ws
=
wb
.
create_sheet
(
"自动化用例"
)
ws
.
append
([
"用例ID"
,
"用例名称"
,
"前置条件"
,
"自动化步骤"
,
"预期结果"
,
"覆盖测试点"
,
"优先级"
,
"执行模型"
])
for
row
in
CASES
:
ws
.
append
(
row
)
style_sheet
(
ws
,
[
18
,
30
,
38
,
62
,
54
,
34
,
10
,
16
])
ws
.
conditional_formatting
.
add
(
"G2:G200"
,
FormulaRule
(
formula
=
[
'G2="P0"'
],
fill
=
PatternFill
(
"solid"
,
fgColor
=
"F4CCCC"
)))
ws
=
wb
.
create_sheet
(
"自动化流程明细"
)
ws
.
append
([
"流程用例ID"
,
"用例名称"
,
"优先级"
,
"实现状态"
,
"环境与前置条件"
,
"运行时测试数据"
,
"自动化详细步骤"
,
"预期结果"
,
"可复用现有资产"
,
"建议新增自动化资产"
,
"执行证据"
,
])
for
row
in
AUTOMATION_FLOW_CASES
:
ws
.
append
(
row
)
style_sheet
(
ws
,
[
18
,
36
,
10
,
12
,
42
,
50
,
96
,
58
,
55
,
66
,
46
])
ws
.
conditional_formatting
.
add
(
"C2:C200"
,
FormulaRule
(
formula
=
[
'C2="P0"'
],
fill
=
PatternFill
(
"solid"
,
fgColor
=
"F4CCCC"
)))
ws
=
wb
.
create_sheet
(
"详细测试步骤"
)
ws
.
append
([
"详细用例ID"
,
"用例名称"
,
"前置条件"
,
"测试数据"
,
"详细测试步骤"
,
"预期结果"
,
"建议保留证据"
])
for
row
in
DETAILED_CASES
:
ws
.
append
(
row
)
style_sheet
(
ws
,
[
15
,
30
,
30
,
30
,
72
,
52
,
34
])
ws
=
wb
.
create_sheet
(
"需求追溯"
)
ws
.
append
([
"需求编号"
,
"需求摘要"
,
"来源页"
,
"覆盖测试点"
,
"覆盖自动化用例"
,
"备注"
])
rows
=
[
(
"REQ-01"
,
"旧授权页下线,生效中授权同步新列表"
,
"3"
,
"FP-001"
,
"AUTO-AUTH-001"
,
""
),
(
"REQ-02"
,
"展示名称规则冲突"
,
"旧PRD第1页;新提测第4页"
,
"FP-030"
,
"DT-001"
,
"旧PRD要求去掉展示名称/标题;新提测要求保存展示名称。测试前须由产品确认最终规则。"
),
(
"REQ-03"
,
"两端推荐/授权/待审核Tab范围、字段、筛选与推荐排序"
,
"1-5"
,
"FP-002~FP-008,FP-031~FP-035,FP-047,FP-049"
,
"AUTO-AUTH-002,003,006,007,014,015,021"
,
"页面结构按7月10日变更日志采用三Tab口径"
),
(
"REQ-04"
,
"双向申请、审核、拒绝、关闭、重开的状态流转与权限"
,
"3-6"
,
"FP-009~FP-014,FP-038~FP-041,FP-050"
,
"AUTO-AUTH-002~006,012,017,018"
,
"已关闭仅发起方可重新开启;待我审核从授权Tab移除"
),
(
"REQ-05"
,
"联系方法配置及字段"
,
"5-6"
,
"FP-015~FP-018,FP-036~FP-037"
,
"AUTO-AUTH-008,016"
,
"联系人、联系手机、微信号、服务时段"
),
(
"REQ-06"
,
"授权成功同步供应链及二次授权豁免边界"
,
"6-7"
,
"FP-019~FP-022,FP-042~FP-043"
,
"AUTO-AUTH-002,003,009,019"
,
"外部代练平台仍沿用填写信息授权"
),
(
"REQ-07"
,
"授权工作室自动生成指定工作室推单策略"
,
"7-8"
,
"FP-023~FP-025,FP-044~FP-046"
,
"AUTO-AUTH-002,003,005,010,020"
,
"关闭授权后的策略处置待产品确认"
),
(
"REQ-08"
,
"Admin默认开通全部供应链、单项关闭及入口简化"
,
"9"
,
"FP-026~FP-027,FP-048"
,
"AUTO-AUTH-011,022"
,
"商户供应链管理保留,供应链授权入口去掉"
),
(
"REQ-09"
,
"权限、重复与并发保护"
,
"补充质量保障"
,
"FP-028~FP-029"
,
"AUTO-AUTH-012,013"
,
"非PDF显式条目,作为授权关系核心风险补充"
),
(
"REQ-10"
,
"双端展示配置保存、开关及数据隔离"
,
"新提测4-5"
,
"FP-051~FP-053"
,
"AUTO-AUTH-023;DT-001~002"
,
""
),
(
"REQ-11"
,
"关闭后供应链/策略失效与重授权"
,
"新提测6-7"
,
"FP-054"
,
"AUTO-AUTH-024;DT-006"
,
"新提测明确要求关闭后失效,替代旧表中的待确认表述。"
),
(
"REQ-12"
,
"历史授权迁移及迁移结果表"
,
"新提测7-10"
,
"FP-055~FP-057"
,
"AUTO-AUTH-025;DT-007"
,
"接口migrateLicense"
),
(
"REQ-13"
,
"策略配置与WYJ新发单授权校验"
,
"新提测11-13"
,
"FP-058~FP-062"
,
"AUTO-AUTH-026~028;DT-008~010"
,
"WYJ新单不依赖旧授权码/license/appSecret;非WYJ需回归。"
),
(
"REQ-14"
,
"存量商户补偿和订阅自动初始化"
,
"新提测13-15"
,
"FP-063~FP-066"
,
"AUTO-AUTH-029~030;DT-011~012"
,
"接口initMerchantSupply和订阅异步链路。"
),
(
"REQ-15"
,
"旧入口切换、范围边界"
,
"新提测15-16"
,
"FP-067~FP-068"
,
"AUTO-AUTH-031;DT-014"
,
"存量订单售后等不属于本次改造范围。"
),
]
for
row
in
rows
:
ws
.
append
(
row
)
style_sheet
(
ws
,
[
14
,
42
,
12
,
28
,
38
,
42
])
ws
=
wb
.
create_sheet
(
"说明"
)
ws
.
append
([
"项目"
,
"内容"
])
ws
.
append
([
"需求来源"
,
"当前主依据:丸掌柜 × 丸友集授权简化提测文档.pdf(16页);历史参照:丸掌柜丸友集授权简化.pdf。"
])
ws
.
append
([
"自动化实现建议"
,
"双账号场景使用仓库 dual_account_pages fixture,分别创建丸掌柜和丸友集 Page Object;不在同一页面切换登录账号。"
])
ws
.
append
([
"范围说明"
,
"已逐页核对当前提测PDF。关闭授权后策略/供应链失效已由新提测明确。仅展示名称是否保留与历史PRD冲突,已在需求追溯和FP-030标记为P0待确认。"
])
ws
.
append
([
"建议执行顺序"
,
"AUTO-AUTH-001 -> 002/003 -> 004/005/006 -> 009/010 -> 007/008/011 -> 012/013"
])
style_sheet
(
ws
,
[
24
,
110
])
OUTPUT
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
wb
.
save
(
OUTPUT
)
print
(
OUTPUT
.
resolve
())
if
__name__
==
"__main__"
:
main
()
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