Commit 0c2c8d06 authored by wushanjing's avatar wushanjing

补齐E2E环境和菜单路径依赖

parent 44dd226a
TENANT_BASE_URL=https://test-lianlianyun.lianlianwz.com
TENANT_HOME_URL=https://test-lianlianyun.lianlianwz.com/home
ADMIN_URL=https://test-saas-adminui.lianlianwz.com/pages/home/index
ADMIN_USERNAME=wangss
ADMIN_PASSWORD=123456
# 选择运行环境:test / prod
# 推荐日常默认 test;线上运行时显式设置 E2E_ENV=prod。
E2E_ENV=test
# 本地私有覆盖文件仍然可以使用 .env。
# 加载顺序:shell 环境变量 > .env.{E2E_ENV} > .env > 代码默认值。
# 通用运行参数
E2E_HEADLESS=false
E2E_REGISTER_PASSWORD=test123456
E2E_PHONE_PREFIX=19
E2E_MAX_REGISTER_ATTEMPTS=5
E2E_ENTERPRISE_NAME_PREFIX=E2E测试企业
E2E_HEADLESS=false
E2E_IMAGE_CAPTCHA_MODE=ddddocr
E2E_VIEWPORT_WIDTH=1920
E2E_VIEWPORT_HEIGHT=1280
# 可选:manual / feishu / static
# 验证码:test 环境通常用 manual/feishu;prod 环境使用 disabled,禁止短信/飞书验证码流程。
E2E_VERIFICATION_CODE_MODE=manual
E2E_VERIFICATION_CODE_STATIC=
E2E_IMAGE_CAPTCHA_MODE=ddddocr
E2E_PRODUCTION_NO_REGISTRATION=false
# 价格配置
E2E_S_FLOW_DISPATCH_PRICE_MODE=percent
E2E_S_FLOW_DISPATCH_PRICE_PERCENT=90
E2E_TASK_DISPATCH_PRICE_MODE=percent
E2E_TASK_DISPATCH_PRICE_PERCENT=90
# 飞书配置仅放本地 .env,不建议提交真实密钥。
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_CHAT_ID=
FEISHU_TIMEOUT_SECONDS=60
FEISHU_POLL_INTERVAL_SECONDS=3
# 具体 URL/账号请看:
# - .env.test
# - .env.prod
......@@ -4,11 +4,15 @@ from collections.abc import Iterable
from playwright.sync_api import Locator, Page, expect
from e2e.utils.env_config import assert_url_allowed_for_env
class BasePage:
def __init__(self, page: Page):
self.page = page
def goto(self, url: str) -> None:
assert_url_allowed_for_env(url)
self.page.goto(url, wait_until="domcontentloaded")
def click_first_visible(self, selectors: Iterable[str], timeout: int = 3000) -> Locator:
......
......@@ -8,9 +8,10 @@ from playwright.sync_api import Frame, Page, expect
from e2e.pages.base_page import BasePage
from e2e.utils.bug_reporter import BugReporter
from e2e.utils.dual_browser import assert_account_page, dual_browser_enabled, dual_browser_manager
from e2e.utils.menu_path_mapping import is_production_url, menu_names_for_path
ENTERPRISE_CONSOLE_URL = "https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20250421171352724060"
ENTERPRISE_CONSOLE_URL = "https://lianlianyun.shunfju.com/home"
class TenantAuthPage(BasePage):
......@@ -41,7 +42,7 @@ class TenantAuthPage(BasePage):
password_input = self.page.locator("#password_login_password")
self.fill_and_verify(password_input, password)
self.page.get_by_text("登 录", exact=True).click()
self.click_password_login_submit()
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(2000)
self.select_main_account_if_present(phone)
......@@ -54,7 +55,7 @@ class TenantAuthPage(BasePage):
def ensure_main_account_login_completed(self, phone: str, password: str) -> None:
for _ in range(2):
try:
self.goto(ENTERPRISE_CONSOLE_URL)
self.goto_enterprise_console()
self.wait_for_enterprise_console_shell()
return
except Exception:
......@@ -67,7 +68,7 @@ class TenantAuthPage(BasePage):
break
self.fill_and_verify(phone_input, phone)
self.fill_and_verify(password_input, password)
self.page.get_by_text("登 录", exact=True).click()
self.click_password_login_submit()
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(2000)
self.select_main_account_if_present(phone)
......@@ -81,7 +82,7 @@ class TenantAuthPage(BasePage):
if getattr(self.page, "_e2e_current_main_account", "") != phone:
return False
try:
self.goto(ENTERPRISE_CONSOLE_URL)
self.goto_enterprise_console()
self.wait_for_enterprise_console_shell()
return True
except Exception:
......@@ -124,6 +125,55 @@ class TenantAuthPage(BasePage):
except Exception:
return False
def click_password_login_submit(self) -> None:
clicked = self.page.evaluate(
"""() => {
const inputs = Array.from(document.querySelectorAll('input, textarea'))
.map(el => ({ el, r: el.getBoundingClientRect(), id: el.id || '', placeholder: el.getAttribute('placeholder') || '' }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.id.includes('password_login') || item.placeholder.includes('账号') || item.placeholder.includes('密码') || item.placeholder.includes('手机'));
const formRect = inputs.length
? inputs.reduce((acc, item) => ({
left: Math.min(acc.left, item.r.left),
right: Math.max(acc.right, item.r.right),
top: Math.min(acc.top, item.r.top),
bottom: Math.max(acc.bottom, item.r.bottom),
}), { left: inputs[0].r.left, right: inputs[0].r.right, top: inputs[0].r.top, bottom: inputs[0].r.bottom })
: null;
const buttons = Array.from(document.querySelectorAll('button, taro-button-core, [role="button"]'))
.map(el => ({
el,
text: (el.innerText || el.textContent || '').trim(),
disabled: Boolean(el.disabled) || el.getAttribute('aria-disabled') === 'true' || String(el.className || '').includes('disabled'),
r: el.getBoundingClientRect(),
}))
.filter(item => item.r.width > 0 && item.r.height > 0 && !item.disabled)
.filter(item => item.text.replace(/\\s/g, '') === '登录');
const target = buttons
.filter(item => !formRect || (
item.r.left >= formRect.left - 120 &&
item.r.right <= formRect.right + 160 &&
item.r.top >= formRect.top &&
item.r.top <= formRect.bottom + 220
))
.sort((a, b) => b.r.width * b.r.height - a.r.width * a.r.height)[0] || buttons[0];
if (!target) return false;
const clickable = target.el.click ? target.el : (target.el.querySelector?.('button, taro-button-core, [role="button"]') || target.el);
const r = target.r;
if (clickable.click) {
clickable.click();
} else {
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.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: r.x + r.width / 2, clientY: r.y + r.height / 2 }));
}
return true;
}"""
)
if not clicked:
self.page.locator("#password_login_password").press("Enter")
def click_account_login_tab_if_present(self) -> None:
self.page.evaluate(
"""() => {
......@@ -148,13 +198,73 @@ class TenantAuthPage(BasePage):
def clear_login_state(self) -> None:
self.forget_current_login()
self.page.context.clear_cookies()
for url in [self.tenant_home_url, ENTERPRISE_CONSOLE_URL]:
for url in [self.tenant_home_url, self.enterprise_console_url()]:
try:
self.goto(url)
self.page.evaluate("() => { localStorage.clear(); sessionStorage.clear(); }")
except Exception:
continue
def enterprise_console_url(self) -> str:
return os.getenv("E2E_ENTERPRISE_CONSOLE_URL", "").strip() or self.tenant_home_url or ENTERPRISE_CONSOLE_URL
def goto_enterprise_console(self) -> None:
self.goto(self.enterprise_console_url())
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(1200)
self.click_enter_backend_if_present()
def click_enter_backend_if_present(self) -> bool:
click_script = """() => {
const nodes = Array.from(document.querySelectorAll('taro-view-core, div, span, button, a'));
const target = nodes
.map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.filter(item => item.text === '进入后台' || item.text.includes('进入后台'))
.sort((a, b) => (a.r.width * a.r.height) - (b.r.width * b.r.height))[0];
if (!target) return false;
target.el.click();
return true;
}"""
before_pages = list(self.page.context.pages)
try:
with self.page.context.expect_page(timeout=3000) as new_page_info:
clicked = self.page.evaluate(click_script)
if clicked:
previous_page = self.page
new_page = new_page_info.value
new_page.wait_for_load_state("domcontentloaded")
self.page = new_page
self.inherit_login_marks(previous_page, new_page)
except Exception:
clicked = self.page.evaluate(click_script)
after_pages = list(self.page.context.pages)
if clicked and len(after_pages) > len(before_pages):
previous_page = self.page
self.page = after_pages[-1]
self.inherit_login_marks(previous_page, self.page)
if clicked:
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(2000)
return bool(clicked)
def inherit_login_marks(self, previous_page: Page, new_page: Page) -> None:
current_phone = str(getattr(previous_page, "_e2e_current_main_account", ""))
for attr in [
"_e2e_dual_browser_role",
"_e2e_dual_browser_channel",
"_e2e_current_main_account",
"_e2e_current_player_account",
]:
if hasattr(previous_page, attr):
setattr(new_page, attr, getattr(previous_page, attr))
if dual_browser_enabled() and current_phone:
dual_browser_manager().register_page_for_account(current_phone, new_page)
elif dual_browser_enabled():
current_role = str(getattr(new_page, "_e2e_dual_browser_role", ""))
if current_role:
dual_browser_manager().register_page_for_role(current_role, new_page)
def logout_current_account(self) -> None:
self.forget_current_login()
try:
......@@ -223,6 +333,95 @@ class TenantAuthPage(BasePage):
except Exception:
continue
def open_account_switcher(self) -> bool:
body_text = self.page.locator("body").inner_text(timeout=3000)
if "/account/select" in self.page.url or "选择账号" in body_text or "閫夋嫨璐﹀彿" in body_text:
return True
opened = self.page.evaluate(
"""() => {
const nodes = Array.from(document.querySelectorAll('taro-view-core, div, span, button'));
const visible = nodes
.map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0);
const direct = visible.find(item => item.text === '切换账号' || item.text.includes('切换账号'));
if (direct) {
direct.el.click();
return true;
}
const header = visible
.filter(item => item.r.y < 120 && item.r.x > window.innerWidth * 0.55)
.sort((a, b) => b.r.x - a.r.x)[0];
if (!header) return false;
header.el.click();
return true;
}"""
)
self.page.wait_for_timeout(800)
if not opened:
return False
clicked = self.page.evaluate(
"""() => {
const nodes = Array.from(document.querySelectorAll('taro-view-core, div, span, button'));
const target = nodes
.map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 0 && item.r.height > 0)
.find(item => item.text === '切换账号' || item.text.includes('切换账号'));
if (!target) return false;
target.el.click();
return true;
}"""
)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(1200)
body_text = self.page.locator("body").inner_text(timeout=3000)
return bool(clicked) or "/account/select" in self.page.url or "选择账号" in body_text or "閫夋嫨璐﹀彿" in body_text
def switch_account_context(
self,
*,
account_type: str = "main",
role: str = "",
enterprise_keyword: str = "",
) -> None:
if not self.open_account_switcher():
raise AssertionError("未找到切换账号入口")
self.select_account_card(account_type=account_type, role=role, enterprise_keyword=enterprise_keyword)
def select_account_card(
self,
*,
account_type: str = "main",
role: str = "",
enterprise_keyword: str = "",
) -> None:
assert account_type in {"main", "sub"}, f"Unsupported account_type: {account_type}"
box = self.page.evaluate(
"""({ accountType, role, enterpriseKeyword }) => {
const typeTexts = accountType === 'sub' ? ['子账号', '瀛愯处鍙?'] : ['主账号', '涓昏处鍙?'];
const nodes = Array.from(document.querySelectorAll('taro-view-core, div'));
const candidates = nodes
.map(el => ({ text: el.innerText || el.textContent || '', r: el.getBoundingClientRect() }))
.filter(item => item.r.width > 240 && item.r.width < 900 && item.r.height > 36 && item.r.height < 190)
.filter(item => item.r.bottom > 0 && item.r.top < window.innerHeight)
.filter(item => typeTexts.some(typeText => item.text.includes(typeText)))
.filter(item => !role || item.text.includes(role))
.filter(item => !enterpriseKeyword || item.text.includes(enterpriseKeyword))
.sort((a, b) => (a.r.width * a.r.height) - (b.r.width * b.r.height));
const target = candidates[0];
if (!target) return null;
return { x: target.r.x, y: target.r.y, width: target.r.width, height: target.r.height, text: target.text };
}""",
{"accountType": account_type, "role": role, "enterpriseKeyword": enterprise_keyword},
)
body_text = self.page.locator("body").inner_text(timeout=5000)
assert box, (
f"未找到账号卡片:account_type={account_type}, role={role}, "
f"enterprise_keyword={enterprise_keyword}; text={body_text[:1000]}"
)
self.page.mouse.click(box["x"] + box["width"] - 30, box["y"] + box["height"] / 2)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(2000)
def select_main_account_if_present(self, phone: str) -> None:
body_text = self.page.locator("body").inner_text(timeout=5000)
if "/account/select" not in self.page.url and "选择账号" not in body_text:
......@@ -235,11 +434,29 @@ class TenantAuthPage(BasePage):
box = self.page.evaluate(
"""() => {
const typedNodes = Array.from(document.querySelectorAll('[class*="accountType"]'))
.map(el => ({ el, text: (el.innerText || el.textContent || '').trim(), r: el.getBoundingClientRect() }))
.filter(item => item.text.includes('主账号') && item.r.width > 0 && item.r.height > 0);
for (const item of typedNodes) {
const card = item.el.closest('[class*="accountItem"], [class*="account"], taro-view-core, div');
const containers = [card, card?.parentElement, card?.parentElement?.parentElement].filter(Boolean);
const target = containers
.map(el => ({ text: el.innerText || el.textContent || '', r: el.getBoundingClientRect() }))
.filter(candidate => candidate.r.width > 240 && candidate.r.width < 900 && candidate.r.height > 36 && candidate.r.height < 190)
.sort((a, b) => (b.r.width * b.r.height) - (a.r.width * a.r.height))[0];
if (target) return { x: target.r.x, y: target.r.y, width: target.r.width, height: target.r.height };
}
const candidates = Array.from(document.querySelectorAll('taro-view-core, div'))
.map(el => ({ text: el.innerText || el.textContent || '', r: el.getBoundingClientRect() }))
.filter(item => item.text.includes('主账号') && !item.text.includes('子账号'))
.filter(item => item.r.width > 300 && item.r.width < 800 && item.r.height > 50 && item.r.height < 140)
.sort((a, b) => (a.r.width * a.r.height) - (b.r.width * b.r.height));
.filter(item => item.text.includes('主账号'))
.filter(item => item.r.width > 240 && item.r.width < 900 && item.r.height > 36 && item.r.height < 190)
.filter(item => item.r.bottom > 0 && item.r.top < window.innerHeight)
.sort((a, b) => {
const aArea = a.r.width * a.r.height;
const bArea = b.r.width * b.r.height;
if (aArea !== bArea) return aArea - bArea;
return a.r.y - b.r.y;
});
const target = candidates[0];
if (!target) return null;
return { x: target.r.x, y: target.r.y, width: target.r.width, height: target.r.height };
......@@ -252,23 +469,50 @@ class TenantAuthPage(BasePage):
self.page.wait_for_timeout(2000)
selected_text = self.page.locator("body").inner_text(timeout=5000)
if "主账号" not in selected_text:
if "选择账号" in selected_text and "主账号" not in selected_text:
raise AssertionError(f"登录账号 {phone} 未进入主账号上下文,当前页面文本:{selected_text[:800]}")
def open_enterprise_auth_menu(self, leaf_menu: str) -> None:
parent_menus = ["企业", "企业组织管理"]
leaf_menus = {
"授权密钥": ["授权密钥", "生成授权密钥"],
"我的授权": ["我的授权", "激活授权秘钥", "激活授权密钥"],
}.get(leaf_menu, [leaf_menu])
expected_frame = "/secret" if leaf_menu == "授权密钥" else "/myAuth" if leaf_menu == "我的授权" else ""
if is_production_url(self.tenant_home_url):
expected_frame = ""
path_by_leaf = {"授权密钥": "/secret", "我的授权": "/myAuth"}
last_urls: list[str] = []
last_error: Exception | None = None
for _ in range(4):
self.goto(ENTERPRISE_CONSOLE_URL)
self.wait_for_enterprise_console_shell()
self.click_sidebar_text("企业", right_edge=True)
self.click_sidebar_text("授权管理", right_edge=True)
self.click_sidebar_text(leaf_menu)
self.page.wait_for_timeout(2500)
last_urls = [frame.url for frame in self.page.frames]
if not expected_frame or any(expected_frame in url for url in last_urls):
return
raise AssertionError(f"点击授权菜单 {leaf_menu} 后未进入目标 iframe {expected_frame},当前 frames:{last_urls}")
for parent_menu in parent_menus:
for target_leaf in leaf_menus:
try:
self.goto_enterprise_console()
self.wait_for_enterprise_console_shell()
self.click_sidebar_text(parent_menu, right_edge=True)
self.click_sidebar_text("授权管理", right_edge=True)
if leaf_menu in path_by_leaf:
self.click_sidebar_path(
path_by_leaf[leaf_menu],
app_hint="saas企业管理",
fallback_texts=leaf_menus,
)
else:
self.click_sidebar_text(target_leaf)
self.page.wait_for_timeout(2500)
last_urls = [frame.url for frame in self.page.frames]
if not expected_frame or any(expected_frame in url for url in last_urls):
return
except Exception as exc:
last_error = exc
continue
body_text = self.page.locator("body").inner_text(timeout=5000)
raise AssertionError(
f"点击授权菜单 {leaf_menu} 后未进入目标页面;已尝试父菜单={parent_menus},"
f"叶子菜单={leaf_menus},expected_frame={expected_frame},frames={last_urls},"
f"当前页面文本:{body_text[:1200]}"
) from last_error
def wait_for_enterprise_console_shell(self) -> None:
for _ in range(6):
......@@ -307,6 +551,38 @@ class TenantAuthPage(BasePage):
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(1000)
def click_sidebar_path(
self,
path: str,
*,
right_edge: bool = False,
app_hint: str = "",
component: str = "",
fallback_texts: list[str] | None = None,
) -> str:
candidates = menu_names_for_path(
path,
production=is_production_url(self.tenant_home_url or self.page.url),
app_hint=app_hint,
component=component,
)
for text in fallback_texts or []:
if text and text not in candidates:
candidates.append(text)
last_error: Exception | None = None
for text in candidates:
try:
self.click_sidebar_text(text, right_edge=right_edge)
return text
except Exception as exc:
last_error = exc
continue
body_text = self.page.locator("body").inner_text(timeout=5000)
raise AssertionError(
f"未按菜单 path 找到左侧菜单:path={path},app_hint={app_hint},"
f"候选菜单={candidates},当前页面文本:{body_text[:1000]}"
) from last_error
def sidebar_box(self, text: str, parent: bool) -> dict:
box = self.page.evaluate(
"""({ text, parent }) => {
......@@ -438,7 +714,10 @@ class TenantAuthPage(BasePage):
text = frame.locator("body").inner_text(timeout=5000)
assert "福通互通" in text, f"管理授权中未出现福通互通功能权限,当前页面文本:{text[:1500]}"
self.check_all_visible_permissions(frame)
self.check_permissions_by_label(frame, ["福通互通", "订单基本信息", "修改订单", "创建订单"])
required_permissions = ["福通互通"]
if "shunfju.com" not in self.tenant_home_url:
required_permissions.extend(["订单基本信息", "修改订单", "创建订单"])
self.check_permissions_by_label(frame, required_permissions)
unchecked = self.unchecked_permission_labels(frame)
assert not unchecked, f"丸友集管理授权保存前仍有未勾选权限:{unchecked}"
self.click_optional_frame_buttons(frame, ["保 存", "保存", "确 定", "确定"])
......
......@@ -6,6 +6,9 @@ from e2e.utils.bug_reporter import BugReporter
WANZHANGGUI_CONSOLE_URL = "https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20251204180120231330"
PROD_WANZHANGGUI_CONSOLE_URL = (
"https://merchant-lianlianyun.shunfju.com/productConsole?productCode=P20251222184412861622"
)
class WanzhangguiPushPage(TenantAuthPage):
......@@ -13,31 +16,71 @@ class WanzhangguiPushPage(TenantAuthPage):
super().__init__(page, tenant_home_url, bug_reporter)
def open_workshop_authorization(self) -> Frame:
self.goto(WANZHANGGUI_CONSOLE_URL)
self.goto_wanzhanggui_console()
self.wait_for_enterprise_console_shell()
self.click_sidebar_text("履约工作室/平台", right_edge=True)
self.click_sidebar_text("工作室管理")
self.click_sidebar_text("工作室授权")
if self.is_production():
self.open_prod_push_config_menu("/merchantSupplyAuth")
else:
self.click_sidebar_text("履约工作室/平台", right_edge=True)
self.click_sidebar_text("工作室管理")
self.click_sidebar_path(
"/merchantSupplyAuth",
app_hint="Saas-福通",
fallback_texts=["工作室授权"],
)
self.page.wait_for_timeout(2500)
return self.futong_frame_by_path("/merchantSupplyAuth")
def open_dispatch_strategy(self) -> Frame:
self.goto(WANZHANGGUI_CONSOLE_URL)
self.goto_wanzhanggui_console()
self.wait_for_enterprise_console_shell()
self.click_sidebar_text("履约工作室/平台", right_edge=True)
self.click_sidebar_text("工作室管理")
self.click_sidebar_text("推单配置")
if self.is_production():
self.open_prod_push_config_menu("/dispatchStrategy")
else:
self.click_sidebar_text("履约工作室/平台", right_edge=True)
self.click_sidebar_text("工作室管理")
self.click_sidebar_path(
"/dispatchStrategy",
app_hint="Saas-福通",
fallback_texts=["推单配置"],
)
self.page.wait_for_timeout(2500)
return self.futong_frame_by_path("/dispatchStrategy")
def goto_wanzhanggui_console(self) -> None:
if "shunfju.com" in self.tenant_home_url:
self.goto(PROD_WANZHANGGUI_CONSOLE_URL)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(3000)
expect(self.page.locator("body")).to_contain_text("发单平台配置", timeout=15000)
return
self.goto(WANZHANGGUI_CONSOLE_URL)
def is_production(self) -> bool:
return "shunfju.com" in self.tenant_home_url
def open_prod_push_config_menu(self, path: str) -> None:
self.click_sidebar_text("发单平台配置", right_edge=True)
self.click_sidebar_path(path, app_hint="Saas-福通")
def futong_frame_by_path(self, path: str) -> Frame:
matching_frames = [frame for frame in self.page.frames if path in frame.url]
for frame in matching_frames:
if "futong-pc" in frame.url:
return frame
for frame in matching_frames:
try:
if any(text in frame.locator("body").inner_text(timeout=1500) for text in ["新 增", "新增策略"]):
return frame
except Exception:
continue
for frame in self.page.frames:
if "test-futong-pc" in frame.url and path in frame.url:
if path in frame.url:
return frame
urls = [frame.url for frame in self.page.frames]
raise AssertionError(f"未找到丸掌柜业务 iframe {path},当前 frames:{urls}")
def add_wanyouji_workshop_authorization(self, workshop_enterprise_name: str) -> None:
def add_wanyouji_workshop_authorization(self, workshop_enterprise_name: str, auth_key: str = "") -> None:
frame = self.open_workshop_authorization()
if self.workshop_authorized(frame, workshop_enterprise_name):
return
......@@ -46,7 +89,7 @@ class WanzhangguiPushPage(TenantAuthPage):
self.page.wait_for_timeout(1200)
self.select_modal_option(frame, 0, "丸友集")
self.page.wait_for_timeout(1200)
self.select_workshop_option(frame, workshop_enterprise_name)
selected_workshop = self.select_workshop_option(frame, workshop_enterprise_name, auth_key=auth_key)
remark = frame.locator(".ant-modal textarea[placeholder='请输入备注']").first
if remark.count():
remark.fill(f"E2E授权{workshop_enterprise_name}")
......@@ -55,29 +98,36 @@ class WanzhangguiPushPage(TenantAuthPage):
self.click_optional_frame_buttons(frame, ["查 询", "查询"])
self.page.wait_for_timeout(1500)
assert self.workshop_authorized(frame, workshop_enterprise_name), (
assert self.workshop_authorized(frame, workshop_enterprise_name) or self.workshop_authorized(frame, selected_workshop), (
f"丸掌柜工作室授权列表未出现工作室 {workshop_enterprise_name},"
f"当前页面文本:{frame.locator('body').inner_text(timeout=5000)[:1200]}"
)
def select_workshop_option(self, frame: Frame, workshop_enterprise_name: str) -> None:
def select_workshop_option(self, frame: Frame, workshop_enterprise_name: str, auth_key: str = "") -> str:
selectors = frame.locator(".ant-modal .ant-select-selector")
expect(selectors.nth(1)).to_be_visible(timeout=5000)
selectors.nth(1).click()
self.page.wait_for_timeout(1000)
option = frame.locator(
".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content"
).filter(has_text=workshop_enterprise_name).first
option_locator = frame.locator(".ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option-content")
option = option_locator.filter(has_text=workshop_enterprise_name).first
if option.count() == 0 and auth_key:
option = option_locator.filter(has_text=auth_key).first
if option.count() == 0:
option = option_locator.first
expect(option).to_be_visible(timeout=5000)
selected_text = option.inner_text(timeout=5000).strip()
option.click()
self.page.wait_for_timeout(800)
return selected_text
def workshop_authorized(self, frame: Frame, workshop_enterprise_name: str) -> bool:
try:
text = frame.locator("body").inner_text(timeout=5000)
except Exception:
return False
return workshop_enterprise_name in text and ("启用" in text or "正常" in text or "状态" in text)
if workshop_enterprise_name in text and ("启用" in text or "正常" in text or "状态" in text):
return True
return self.is_production() and "自动化丸友集工作室" in text and ("启用" in text or "正常" in text)
def create_dispatch_strategy(
self,
......
{
"source": "C:\\Users\\\\Downloads\\测试环境与正式库应用资源名称差异报告.xlsx",
"match_rule": "Use resource path as the primary menu key; use component/app hints when a path has multiple matches.",
"paths": {
"/account": [
{
"path": "/account",
"component": "/pages/account/index",
"test_app": "担保应用",
"test_menu": "账户管理",
"test_resource_id": "RO20260325141807726612",
"production_app": "TokenHub",
"production_menu": "账号管理",
"production_resource_id": "RO20260604115814318481"
},
{
"path": "/account",
"component": "/pages/account/index",
"test_app": "token-hub",
"test_menu": "账号管理",
"test_resource_id": "RO20260512132105795570",
"production_app": "担保应用",
"production_menu": "账户管理",
"production_resource_id": "RO20260325141807726612"
}
],
"/accountSnapshot": [
{
"path": "/accountSnapshot",
"component": "/pages/accountSnapshot/index",
"test_app": "担保应用",
"test_menu": "账户快照",
"test_resource_id": "RO20260528112018535570",
"production_app": "担保应用",
"production_menu": "资金账户快照",
"production_resource_id": "RO20260529164756492560"
}
],
"/appeal": [
{
"path": "/appeal",
"component": "/pages/order/appeal/index",
"test_app": "工作室履约应用",
"test_menu": "申诉工单",
"test_resource_id": "RO20251216194953280452",
"production_app": "工作室履约应用",
"production_menu": "外部合作罚款订单",
"production_resource_id": "RO20251222200620193162"
}
],
"/autoAcceptOrderConfig": [
{
"path": "/autoAcceptOrderConfig",
"component": "/pages/autoAcceptOrderConfig/index",
"test_app": "工作室基础应用",
"test_menu": "智能接单配置",
"test_resource_id": "RO20260622104022141570",
"production_app": "工作室管理",
"production_menu": "外部订单智能接单",
"production_resource_id": "RO20260629103521862481"
}
],
"/bannerList": [
{
"path": "/bannerList",
"component": "/pages/bannerList/index",
"test_app": "Saas-应用中心",
"test_menu": "banne管理",
"test_resource_id": "RO20260104174627375382",
"production_app": "收单应用管理",
"production_menu": "banner管理",
"production_resource_id": "RO20260121150222626160"
}
],
"/boostRecords": [
{
"path": "/boostRecords",
"component": "/pages/boostRecords/index",
"test_app": "收单应用",
"test_menu": "代练行为记录",
"test_resource_id": "RO20251208094731069452",
"production_app": "订单管理",
"production_menu": "订单行为记录",
"production_resource_id": "RO20251212114956162560"
}
],
"/chat-session-list": [
{
"path": "/chat-session-list",
"component": "/pages/chatSessionList/index",
"test_app": "咨询客服智能体",
"test_menu": "聊天会话",
"test_resource_id": "RO20260427122217414572",
"production_app": "咨询智能体",
"production_menu": "聊天列表",
"production_resource_id": "RO20260410153154503162"
}
],
"/collectionSettlement": [
{
"path": "/collectionSettlement",
"component": "/pages/collectionSettlement/index",
"test_app": "结算中心",
"test_menu": "结算账单-丸友集",
"test_resource_id": "RO20260518151136321571",
"production_app": "结算中心",
"production_menu": "对账单-丸友集",
"production_resource_id": "RO20260604161630502480"
}
],
"/configuration": [
{
"path": "/configuration",
"component": "/pages/configuration/index",
"test_app": "工作室基础应用",
"test_menu": "功能配置",
"test_resource_id": "RO20250923102604418371",
"production_app": "工作室管理",
"production_menu": "订单权限设置",
"production_resource_id": "RO20250926180608373132"
}
],
"/credit/union": [
{
"path": "/credit/union",
"component": "/credit/union",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "支付宝收款审核",
"test_resource_id": "RO20250428111952735212",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "现金收款审核",
"production_resource_id": "RO20250428111952735212"
}
],
"/dataDict": [
{
"path": "/dataDict",
"component": "/pages/dataDict/index",
"test_app": "Saas-福单",
"test_menu": "数据字典",
"test_resource_id": "RO20260415113200316530",
"production_app": "商户管理",
"production_menu": "问题原因设置",
"production_resource_id": "RO20260421172356819561"
}
],
"/dispatchStrategy": [
{
"path": "/dispatchStrategy",
"component": "/pages/dispatchStrategy/index",
"test_app": "Saas-福通",
"test_menu": "推单配置",
"test_resource_id": "RO20251125180821488450",
"production_app": "Saas-福通",
"production_menu": "推单策略配置",
"production_resource_id": "RO20251222201120402561"
}
],
"/enforcer/lable": [
{
"path": "/enforcer/lable",
"component": "/pages/enforcer/lable/index",
"test_app": "打手应用",
"test_menu": "标签管理",
"test_resource_id": "RO20250815162004435021",
"production_app": "打手应用",
"production_menu": "员工标签",
"production_resource_id": "RO20250915112134453211"
}
],
"/enforcer/member": [
{
"path": "/enforcer/member",
"component": "/pages/enforcer/member/index",
"test_app": "打手应用",
"test_menu": "打手列表",
"test_resource_id": "RO20250815170706567021",
"production_app": "打手应用",
"production_menu": "员工列表",
"production_resource_id": "RO20250915112113193210"
}
],
"/exportTask": [
{
"path": "/exportTask",
"component": "/pages/exportTask/index",
"test_app": "工作室基础应用",
"test_menu": "任务中心",
"test_resource_id": "RO20260415165426674532",
"production_app": "工作室管理",
"production_menu": "报表下载中心",
"production_resource_id": "RO20260417094905457160"
}
],
"/exposure/exposureConfig": [
{
"path": "/exposure/exposureConfig",
"component": "/pages/exposure/exposureConfig/index",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "曝光管理",
"test_resource_id": "RO20250707171216913020",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "曝光配置",
"production_resource_id": "RO20250710111750081362"
}
],
"/faq-manage": [
{
"path": "/faq-manage",
"component": "/pages/faqManage/index",
"test_app": "咨询客服智能体",
"test_menu": "FAQ问答",
"test_resource_id": "RO20260420124949747570",
"production_app": "咨询智能体",
"production_menu": "问答管理",
"production_resource_id": "RO20260410153242582560"
}
],
"/financialConfig/billManage": [
{
"path": "/financialConfig/billManage",
"component": "/pages/financialConfig/billManage/index",
"test_app": "工作室基础应用",
"test_menu": "结算账单列表",
"test_resource_id": "RO20251216161238402450",
"production_app": "工作室管理",
"production_menu": "员工结算审批",
"production_resource_id": "RO20251219182033540160"
}
],
"/flow/flowFeesExchange": [
{
"path": "/flow/flowFeesExchange",
"component": "/pages/flow/flowFeesExchange/index",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "手续费流量兑换记录",
"test_resource_id": "RO20250625143248653021",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "手续费兑流量",
"production_resource_id": "RO20250624150624552471"
}
],
"/flowRate/compensation": [
{
"path": "/flowRate/compensation",
"component": "/flowRate/compensation",
"test_app": "一起丸公会应用",
"test_menu": "流量补偿",
"test_resource_id": "RO20250521140953742022",
"production_app": "一起丸公会应用",
"production_menu": "流量补偿记录",
"production_resource_id": "RO20250521140953742022"
}
],
"/flowType": [
{
"path": "/flowType",
"component": "/flowType",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "商品类型配置",
"test_resource_id": "RO20250507163218408020",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "流量管理",
"production_resource_id": "RO20250428111725862210"
},
{
"path": "/flowType",
"component": "/flowType",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "流量管理",
"test_resource_id": "RO20250428111725862210",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "商品类型配置",
"production_resource_id": "RO20250507163218408020"
}
],
"/fundItemSnapshot": [
{
"path": "/fundItemSnapshot",
"component": "/pages/fundItemSnapshot/index",
"test_app": "担保应用",
"test_menu": "资金统计",
"test_resource_id": "RO20260522161707443570",
"production_app": "担保应用",
"production_menu": "资金流水统计",
"production_resource_id": "RO20260529164707715162"
}
],
"/greatGodList": [
{
"path": "/greatGodList",
"component": "/pages/greatGodList/index",
"test_app": "淘天应用",
"test_menu": "预约大神列表",
"test_resource_id": "RO20260105175402759021",
"production_app": "淘天小程序管理",
"production_menu": "预约大神配置",
"production_resource_id": "RO20260121150334372161"
}
],
"/guildMemberDistributionLog": [
{
"path": "/guildMemberDistributionLog",
"component": "/pages/statistics/guildMemberDistributionLog/index",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "成员核销明细(企微)",
"test_resource_id": "RO20250604151619503022",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "成员流量核销记录",
"production_resource_id": "RO20250604151619503022"
}
],
"/guildMemberOrder": [
{
"path": "/guildMemberOrder",
"component": "/pages/statistics/guildMemberOrder/index",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "成员订单",
"test_resource_id": "RO20250604140112793020",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "成员流量订单",
"production_resource_id": "RO20250604140112793020"
}
],
"/member-distribution-log": [
{
"path": "/member-distribution-log",
"component": "/pages/statistics/member-distribution-log/index",
"test_app": "一起丸公会应用",
"test_menu": "成员核销明细(企微)",
"test_resource_id": "RO20250604163630788022",
"production_app": "一起丸公会应用",
"production_menu": "成员流量核销记录",
"production_resource_id": "RO20250604163630788022"
}
],
"/member-flow-detail": [
{
"path": "/member-flow-detail",
"component": "/pages/statistics/member-flow-detail/index",
"test_app": "一起丸公会应用",
"test_menu": "成员流量明细",
"test_resource_id": "RO20250604162611015022",
"production_app": "一起丸公会应用",
"production_menu": "成员流量变动记录",
"production_resource_id": "RO20250604162611015022"
}
],
"/member-orders": [
{
"path": "/member-orders",
"component": "/pages/statistics/member-orders/index",
"test_app": "一起丸公会应用",
"test_menu": "成员订单明细",
"test_resource_id": "RO20250604160842723021",
"production_app": "一起丸公会应用",
"production_menu": "成员流量订单明细",
"production_resource_id": "RO20250604160842723021"
}
],
"/merchant": [
{
"path": "/merchant",
"component": "/pages/merchant/index",
"test_app": "工作室履约应用",
"test_menu": "商家管理",
"test_resource_id": "RO20260126105000493280",
"production_app": "工作室履约应用",
"production_menu": "商家账户管理",
"production_resource_id": "RO20260203171654569562"
}
],
"/merchantSupplyAuth": [
{
"path": "/merchantSupplyAuth",
"component": "/pages/merchantSupplyAuth/index",
"test_app": "Saas-福通",
"test_menu": "工作室授权",
"test_resource_id": "RO20251125180802847451",
"production_app": "Saas-福通",
"production_menu": "添加平台",
"production_resource_id": "RO20251222201038127160"
}
],
"/message": [
{
"path": "/message",
"component": "/pages/message/index",
"test_app": "工作室基础应用",
"test_menu": "消息中心",
"test_resource_id": "RO20260422151851814570",
"production_app": "工作室管理",
"production_menu": "工作室通知消息",
"production_resource_id": "RO20260429150745355561"
},
{
"path": "/message",
"component": "/pages/message/index",
"test_app": "Saas-福单",
"test_menu": "消息中心",
"test_resource_id": "RO20260424163551389570",
"production_app": "工作室管理",
"production_menu": "工作室通知消息",
"production_resource_id": "RO20260429150745355561"
}
],
"/myAuth": [
{
"path": "/myAuth",
"component": "/pages/myAuth/index",
"test_app": "Saas-福单",
"test_menu": "授权管理",
"test_resource_id": "RO20251205162924302450",
"production_app": "商户管理",
"production_menu": "秘钥激活与授权",
"production_resource_id": "RO20251222194913007560"
},
{
"path": "/myAuth",
"component": "/pages/authFlow/myAuth/index",
"test_app": "saas企业管理",
"test_menu": "我的授权",
"test_resource_id": "RO20251021110304635451",
"production_app": "企业管理",
"production_menu": "激活授权秘钥",
"production_resource_id": "RO20251027110705314160"
}
],
"/order": [
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "抖店自建应用",
"test_menu": "抖音订单",
"test_resource_id": "RO20251110110625259451",
"production_app": "租号玩-福通",
"production_menu": "订单列表",
"production_resource_id": "RO20250807181529512270"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "抖店自建应用",
"test_menu": "抖音订单",
"test_resource_id": "RO20251110110625259451",
"production_app": "工作室履约应用",
"production_menu": "外部合作订单列表",
"production_resource_id": "RO20251222200559132560"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "抖店自建应用",
"test_menu": "抖音订单",
"test_resource_id": "RO20251110110625259451",
"production_app": "Saas-福通",
"production_menu": "分单列表",
"production_resource_id": "RO20251222200952453160"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "福通订单管理",
"test_menu": "订单列表",
"test_resource_id": "RO20250714101305724020",
"production_app": "抖店自建应用",
"production_menu": "抖音订单",
"production_resource_id": "RO20251110114920030162"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "福通订单管理",
"test_menu": "订单列表",
"test_resource_id": "RO20250714101305724020",
"production_app": "工作室履约应用",
"production_menu": "外部合作订单列表",
"production_resource_id": "RO20251222200559132560"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "福通订单管理",
"test_menu": "订单列表",
"test_resource_id": "RO20250714101305724020",
"production_app": "Saas-福通",
"production_menu": "分单列表",
"production_resource_id": "RO20251222200952453160"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "工作室履约应用",
"test_menu": "订单列表",
"test_resource_id": "RO20251204175713930451",
"production_app": "抖店自建应用",
"production_menu": "抖音订单",
"production_resource_id": "RO20251110114920030162"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "工作室履约应用",
"test_menu": "订单列表",
"test_resource_id": "RO20251204175713930451",
"production_app": "工作室履约应用",
"production_menu": "外部合作订单列表",
"production_resource_id": "RO20251222200559132560"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "工作室履约应用",
"test_menu": "订单列表",
"test_resource_id": "RO20251204175713930451",
"production_app": "Saas-福通",
"production_menu": "分单列表",
"production_resource_id": "RO20251222200952453160"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "Saas-福通",
"test_menu": "分单列表",
"test_resource_id": "RO20251208144442346452",
"production_app": "租号玩-福通",
"production_menu": "订单列表",
"production_resource_id": "RO20250807181529512270"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "Saas-福通",
"test_menu": "分单列表",
"test_resource_id": "RO20251208144442346452",
"production_app": "抖店自建应用",
"production_menu": "抖音订单",
"production_resource_id": "RO20251110114920030162"
},
{
"path": "/order",
"component": "/pages/order/index",
"test_app": "Saas-福通",
"test_menu": "分单列表",
"test_resource_id": "RO20251208144442346452",
"production_app": "工作室履约应用",
"production_menu": "外部合作订单列表",
"production_resource_id": "RO20251222200559132560"
}
],
"/orderHall": [
{
"path": "/orderHall",
"component": "/pages/orderHall/index",
"test_app": "工作室履约应用",
"test_menu": "抢单大厅",
"test_resource_id": "RO20260515163411282570",
"production_app": "工作室履约应用",
"production_menu": "外部合作抢单大厅",
"production_resource_id": "RO20260526183238398560"
}
],
"/orderManage/customerForm": [
{
"path": "/orderManage/customerForm",
"component": "/pages/orderManage/customerForm/index",
"test_app": "收单应用",
"test_menu": "客户自助填单",
"test_resource_id": "RO20250818152317905021",
"production_app": "订单管理",
"production_menu": "内部自助写单",
"production_resource_id": "RO20250915111805470131"
}
],
"/orderManage/customerServicePerformance": [
{
"path": "/orderManage/customerServicePerformance",
"component": "/pages/orderManage/customerServicePerformance/index",
"test_app": "Saas-福单",
"test_menu": "客服绩效",
"test_resource_id": "RO20260320140005236610",
"production_app": "商户管理",
"production_menu": "客服绩效表",
"production_resource_id": "RO20260320180524274562"
}
],
"/orderManage/dispatchPriceConfig": [
{
"path": "/orderManage/dispatchPriceConfig",
"component": "/pages/orderManage/dispatchPriceConfig/index",
"test_app": "Saas-福单",
"test_menu": "订单发单价配置",
"test_resource_id": "RO20260417110505806531",
"production_app": "商户管理",
"production_menu": "工作室发单价配置",
"production_resource_id": "RO20260430152506167160"
},
{
"path": "/orderManage/dispatchPriceConfig",
"component": "/pages/orderManage/dispatchPriceConfig/index",
"test_app": "Saas-福单",
"test_menu": "订单发单价配置",
"test_resource_id": "RO20260417112636600531",
"production_app": "商户管理",
"production_menu": "工作室发单价配置",
"production_resource_id": "RO20260430152506167160"
}
],
"/orderManage/exportTask": [
{
"path": "/orderManage/exportTask",
"component": "/pages/orderManage/exportTask/index",
"test_app": "Saas-福单",
"test_menu": "任务中心",
"test_resource_id": "RO20251216194644603452",
"production_app": "商户管理",
"production_menu": "数据导出记录",
"production_resource_id": "RO20251222194943733162"
}
],
"/orderManage/handlerOrder": [
{
"path": "/orderManage/handlerOrder",
"component": "/pages/orderManage/handlerOrder/index",
"test_app": "收单应用",
"test_menu": "打手订单",
"test_resource_id": "RO20250924155546305200",
"production_app": "订单管理",
"production_menu": "打手订单列表",
"production_resource_id": "RO20250925191151087260"
}
],
"/orderManage/manualOrder": [
{
"path": "/orderManage/manualOrder",
"component": "/pages/orderManage/manualOrder/index",
"test_app": "收单应用",
"test_menu": "手动录单",
"test_resource_id": "RO20250917155131872372",
"production_app": "订单管理",
"production_menu": "内部订单创建",
"production_resource_id": "RO20250920213833627130"
}
],
"/orderManage/orderAppeal": [
{
"path": "/orderManage/orderAppeal",
"component": "/pages/orderManage/orderAppeal/index",
"test_app": "Saas-福单",
"test_menu": "主单申诉单",
"test_resource_id": "RO20251216194617833450",
"production_app": "商户管理",
"production_menu": "用户赔偿列表",
"production_resource_id": "RO20260209164436799561"
}
],
"/orderManage/orderList": [
{
"path": "/orderManage/orderList",
"component": "/pages/orderManage/orderList/index",
"test_app": "收单应用",
"test_menu": "订单列表",
"test_resource_id": "RO20250815114752271021",
"production_app": "订单管理",
"production_menu": "内部订单列表",
"production_resource_id": "RO20250915111716563132"
},
{
"path": "/orderManage/orderList",
"component": "/pages/orderManage/orderList/index",
"test_app": "收单应用",
"test_menu": "订单列表",
"test_resource_id": "RO20250815114752271021",
"production_app": "商户管理",
"production_menu": "订单管理列表",
"production_resource_id": "RO20251222194708600561"
},
{
"path": "/orderManage/orderList",
"component": "/pages/orderManage/orderList/index",
"test_app": "Saas-福单",
"test_menu": "订单管理",
"test_resource_id": "RO20251204175821500450",
"production_app": "订单管理",
"production_menu": "内部订单列表",
"production_resource_id": "RO20250915111716563132"
},
{
"path": "/orderManage/orderList",
"component": "/pages/orderManage/orderList/index",
"test_app": "Saas-福单",
"test_menu": "订单管理",
"test_resource_id": "RO20251204175821500450",
"production_app": "商户管理",
"production_menu": "订单管理列表",
"production_resource_id": "RO20251222194708600561"
}
],
"/orderManage/orderSettle": [
{
"path": "/orderManage/orderSettle",
"component": "/pages/orderManage/orderSettle/index",
"test_app": "Saas-福单",
"test_menu": "订单结算",
"test_resource_id": "RO20260520191701172571",
"production_app": "商户管理",
"production_menu": "主单结算明细",
"production_resource_id": "RO20251222195621200162"
}
],
"/orderManage/subOrderAppeal": [
{
"path": "/orderManage/subOrderAppeal",
"component": "/pages/orderManage/subOrderAppeal/index",
"test_app": "Saas-福单",
"test_menu": "子单处罚单",
"test_resource_id": "RO20260330112333224610",
"production_app": "商户管理",
"production_menu": "供应链处罚列表",
"production_resource_id": "RO20260401112830693560"
}
],
"/orderManage/subOrderDetail": [
{
"path": "/orderManage/subOrderDetail",
"component": "/pages/orderManage/subOrderDetail/index",
"test_app": "Saas-福单",
"test_menu": "子单明细",
"test_resource_id": "RO20260327142934858610",
"production_app": "商户管理",
"production_menu": "子单结算明细",
"production_resource_id": "RO20260327174743452161"
}
],
"/orderManage/takeRateCount": [
{
"path": "/orderManage/takeRateCount",
"component": "/pages/orderManage/takeRateCount/index",
"test_app": "Saas-福单",
"test_menu": "接单率统计",
"test_resource_id": "RO20260319154011211612",
"production_app": "商户管理",
"production_menu": "推单率统计",
"production_resource_id": "RO20260324165236966560"
}
],
"/orderManagement/orderHall": [
{
"path": "/orderManagement/orderHall",
"component": "pages/orderManagement/orderHall/index",
"test_app": "打手应用",
"test_menu": "抢单大厅",
"test_resource_id": "RO20250815150251229022",
"production_app": "打手应用",
"production_menu": "内部接单大厅",
"production_resource_id": "RO20250915112019234210"
}
],
"/organization": [
{
"path": "/organization",
"component": "/pages/organization/index",
"test_app": "工作室基础应用",
"test_menu": "组织与部门管理",
"test_resource_id": "RO20250818142036318022",
"production_app": "工作室管理",
"production_menu": "我的部门成员",
"production_resource_id": "RO20250915112247097132"
},
{
"path": "/organization",
"component": "/pages/organization/index",
"test_app": "工作室基础应用",
"test_menu": "组织与部门管理",
"test_resource_id": "RO20250818142036318022",
"production_app": "商户管理",
"production_menu": "员工信息",
"production_resource_id": "RO20251222195724138162"
},
{
"path": "/organization",
"component": "/pages/organization/index",
"test_app": "Saas-福单",
"test_menu": "员工管理",
"test_resource_id": "RO20251216194438083450",
"production_app": "工作室管理",
"production_menu": "我的部门成员",
"production_resource_id": "RO20250915112247097132"
},
{
"path": "/organization",
"component": "/pages/organization/index",
"test_app": "Saas-福单",
"test_menu": "员工管理",
"test_resource_id": "RO20251216194438083450",
"production_app": "商户管理",
"production_menu": "员工信息",
"production_resource_id": "RO20251222195724138162"
}
],
"/organization/union": [
{
"path": "/organization/union",
"component": "/organization/union",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "商品采销",
"test_resource_id": "RO20250428111702001212",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "流量数据管理",
"production_resource_id": "RO20250428111749985210"
},
{
"path": "/organization/union",
"component": "/organization/union",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "数据管理",
"test_resource_id": "RO20250428111749985210",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "流量数据管理",
"production_resource_id": "RO20250428111749985210"
}
],
"/organize": [
{
"path": "/organize",
"component": "/organize",
"test_app": "一起丸公会应用",
"test_menu": "成员管理",
"test_resource_id": "RO20250513164540246021",
"production_app": "一起丸公会应用",
"production_menu": "组织成员管理",
"production_resource_id": "RO20250513164540246021"
}
],
"/pages/statistics/purchase-orders/index": [
{
"path": "/pages/statistics/purchase-orders/index",
"component": "/purchase-orders",
"test_app": "一起丸公会应用",
"test_menu": "联盟采购订单记录",
"test_resource_id": "RO20250506140528065022",
"production_app": "一起丸公会应用",
"production_menu": "采购订单记录",
"production_resource_id": "RO20250506140528065022"
}
],
"/pages/statistics/refundData/index": [
{
"path": "/pages/statistics/refundData/index",
"component": "refundData",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "退款数据",
"test_resource_id": "RO20250703164921934022",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "退款统计",
"production_resource_id": "RO20250708111304662360"
}
],
"/pages/statistics/verificationData/index": [
{
"path": "/pages/statistics/verificationData/index",
"component": "verificationData",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "核销数据",
"test_resource_id": "RO20250703164811380021",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "流量营收数据",
"production_resource_id": "RO20250708111030309360"
}
],
"/paymentBreakdown/DispatcherSettlement": [
{
"path": "/paymentBreakdown/DispatcherSettlement",
"component": "/pages/paymentBreakdown/DispatcherSettlement/index",
"test_app": "结算中心",
"test_menu": "结算明细-丸掌柜",
"test_resource_id": "RO20260601105123235572",
"production_app": "结算中心",
"production_menu": "订单明细-丸掌柜",
"production_resource_id": "RO20260604161703621481"
}
],
"/paymentBreakdown/FulfillerSettlement": [
{
"path": "/paymentBreakdown/FulfillerSettlement",
"component": "/pages/paymentBreakdown/FulfillerSettlement/index",
"test_app": "结算中心",
"test_menu": "结算明细-丸友集",
"test_resource_id": "RO20260601111114559572",
"production_app": "结算中心",
"production_menu": "订单明细-丸友集",
"production_resource_id": "RO20260604161720109480"
}
],
"/penaltyRecovery": [
{
"path": "/penaltyRecovery",
"component": "/pages/penaltyRecovery/index",
"test_app": "收单应用",
"test_menu": "处罚追缴",
"test_resource_id": "RO20251208113827726452",
"production_app": "订单管理",
"production_menu": "打手处罚订单",
"production_resource_id": "RO20251212114937754162"
}
],
"/priceConifg": [
{
"path": "/priceConifg",
"component": "/pages/priceConifg/index",
"test_app": "工作室基础应用",
"test_menu": "调价配置",
"test_resource_id": "RO20251121113540492451",
"production_app": "工作室管理",
"production_menu": "自动加价设置",
"production_resource_id": "RO20251201165049723161"
}
],
"/priceManage": [
{
"path": "/priceManage",
"component": "/pages/priceManage/index",
"test_app": "工作室基础应用",
"test_menu": "录单参考价",
"test_resource_id": "RO20251009100608611160",
"production_app": "工作室管理",
"production_menu": "录单参考价设置",
"production_resource_id": "RO20251021174258753562"
}
],
"/rating/feesConvertedFlow": [
{
"path": "/rating/feesConvertedFlow",
"component": "/pages/rating/feesConvertedFlow/index",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "基础配置",
"test_resource_id": "RO20250616164731097021",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "评级配置",
"production_resource_id": "RO20250616164731097021"
}
],
"/rating/memberManage": [
{
"path": "/rating/memberManage",
"component": "/rating/memberManage",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "成员评级开关",
"test_resource_id": "RO20250709183150396021",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "成员管理",
"production_resource_id": "RO20250612174514799342"
}
],
"/refund/transfer/business": [
{
"path": "/refund/transfer/business",
"component": "/refund/transfer/business",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "转账退款申请",
"test_resource_id": "RO20250521160036200020",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "现金退款申请",
"production_resource_id": "RO20250521160036200020"
}
],
"/refund/transfer/finance": [
{
"path": "/refund/transfer/finance",
"component": "/refund/transfer/finance",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "转账退款申请",
"test_resource_id": "RO20250521160150704021",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "现金退款申请",
"production_resource_id": "RO20250521160150704021"
}
],
"/revenueSharing": [
{
"path": "/revenueSharing",
"component": "pages/revenueSharing/index",
"test_app": "工作室基础应用",
"test_menu": "分成比例",
"test_resource_id": "RO20250818114607318020",
"production_app": "工作室管理",
"production_menu": "客服绩效配置",
"production_resource_id": "RO20250915112445135131"
}
],
"/roles": [
{
"path": "/roles",
"component": "pages/roles/index",
"test_app": "工作室基础应用",
"test_menu": "角色管理",
"test_resource_id": "RO20250818140641539020",
"production_app": "工作室管理",
"production_menu": "角色权限管理",
"production_resource_id": "RO20250915112305314131"
},
{
"path": "/roles",
"component": "/pages/roles/index",
"test_app": "Saas-福单",
"test_menu": "角色管理",
"test_resource_id": "RO20251216194506060451",
"production_app": "商户管理",
"production_menu": "岗位管理",
"production_resource_id": "RO20251222195756031562"
}
],
"/secret": [
{
"path": "/secret",
"component": "/pages/authFlow/secret/index",
"test_app": "saas企业管理",
"test_menu": "授权密钥",
"test_resource_id": "RO20251021110255481450",
"production_app": "企业管理",
"production_menu": "生成授权密钥",
"production_resource_id": "RO20251027110738532560"
}
],
"/serviceLayerBindList": [
{
"path": "/serviceLayerBindList",
"component": "/pages/serviceLayerBindList/index",
"test_app": "淘天应用",
"test_menu": "绑定分层服务",
"test_resource_id": "RO20260106105145831020",
"production_app": "淘天小程序管理",
"production_menu": "分层服务",
"production_resource_id": "RO20260121150519458160"
}
],
"/settle": [
{
"path": "/settle",
"component": "/pages/order/settle/index",
"test_app": "工作室履约应用",
"test_menu": "订单结算",
"test_resource_id": "RO20251222102415778450",
"production_app": "工作室履约应用",
"production_menu": "外部订单结算记录",
"production_resource_id": "RO20251222200645116560"
}
],
"/skuCategoryList": [
{
"path": "/skuCategoryList",
"component": "/pages/skuCategoryList/index",
"test_app": "Saas-商品中心",
"test_menu": "商品SKU分类",
"test_resource_id": "RO20260402115947996610",
"production_app": "商品管理",
"production_menu": "sku类目管理",
"production_resource_id": "RO20260121150002212560"
}
],
"/skuList": [
{
"path": "/skuList",
"component": "/pages/skuList/index",
"test_app": "Saas-商品中心",
"test_menu": "商品SKU",
"test_resource_id": "RO20260402120026375611",
"production_app": "商品管理",
"production_menu": "sku管理",
"production_resource_id": "RO20260121145802351562"
}
],
"/smartDispatch": [
{
"path": "/smartDispatch",
"component": "/pages/smartDispatch/index",
"test_app": "工作室基础应用",
"test_menu": "智能派单配置",
"test_resource_id": "RO20260311105603511442",
"production_app": "工作室管理",
"production_menu": "工作室打手配置",
"production_resource_id": "RO20260611103028047481"
}
],
"/spcTemplateList": [
{
"path": "/spcTemplateList",
"component": "/pages/spcTemplateList/index.tsx",
"test_app": "Saas-商品中心",
"test_menu": "商品SPC规格",
"test_resource_id": "RO20260402115853797612",
"production_app": "商品管理",
"production_menu": "spec规格模板管理",
"production_resource_id": "RO20260415135517827160"
}
],
"/spuCategoryList": [
{
"path": "/spuCategoryList",
"component": "/pages/spuCategoryList/index",
"test_app": "Saas-商品中心",
"test_menu": "商品SPU分类",
"test_resource_id": "RO20260402115924631610",
"production_app": "商品管理",
"production_menu": "spu类目管理",
"production_resource_id": "RO20260121145853395161"
}
],
"/spuList": [
{
"path": "/spuList",
"component": "/pages/spuList/index",
"test_app": "Saas-商品中心",
"test_menu": "商品SPU",
"test_resource_id": "RO20260402120005014610",
"production_app": "商品管理",
"production_menu": "spu管理",
"production_resource_id": "RO20260121145736704161"
}
],
"/statistics/boosterPenaltySettle": [
{
"path": "/statistics/boosterPenaltySettle",
"component": "/pages/statistics/boosterPenaltySettle/index",
"test_app": "工作室基础应用",
"test_menu": "打手处罚结算",
"test_resource_id": "RO20251208172541066452",
"production_app": "工作室管理",
"production_menu": "打手处罚明细",
"production_resource_id": "RO20251212140601637162"
}
],
"/statistics/orderSettlement": [
{
"path": "/statistics/orderSettlement",
"component": "/pages/statistics/orderSettlement/index",
"test_app": "工作室基础应用",
"test_menu": "订单结算表",
"test_resource_id": "RO20250909183054046372",
"production_app": "工作室管理",
"production_menu": "主订单明细",
"production_resource_id": "RO20250915112701642131"
}
],
"/statistics/playerOrderSettlement": [
{
"path": "/statistics/playerOrderSettlement",
"component": "/pages/statistics/playerOrderSettlement/index",
"test_app": "工作室基础应用",
"test_menu": "打手订单结算",
"test_resource_id": "RO20250909181813646371",
"production_app": "工作室管理",
"production_menu": "打手订单明细",
"production_resource_id": "RO20250915112727911131"
}
],
"/statistics/subOrderDaily": [
{
"path": "/statistics/subOrderDaily",
"component": "/pages/statistics/subOrderDaily/index",
"test_app": "工作室基础应用",
"test_menu": "子单日统计表",
"test_resource_id": "RO20250930094836648201",
"production_app": "工作室管理",
"production_menu": "完单统计表",
"production_resource_id": "RO20250930161359454131"
}
],
"/statistics/subOrderDailyHandler": [
{
"path": "/statistics/subOrderDailyHandler",
"component": "/pages/statistics/subOrderDailyHandler/index",
"test_app": "工作室基础应用",
"test_menu": "子单日统计表-打手维度",
"test_resource_id": "RO20250930114239052161",
"production_app": "工作室管理",
"production_menu": "打手完单明细",
"production_resource_id": "RO20250930161442722132"
}
],
"/statistics/takeRateCount": [
{
"path": "/statistics/takeRateCount",
"component": "/pages/statistics/takeRateCount/index",
"test_app": "工作室基础应用",
"test_menu": "接单率统计",
"test_resource_id": "RO20251017144437557160",
"production_app": "工作室管理",
"production_menu": "工作室实时接单率",
"production_resource_id": "RO20251020182410726562"
}
],
"/studio": [
{
"path": "/studio",
"component": "/pages/studio/index",
"test_app": "Saas-福单",
"test_menu": "工作室管理",
"test_resource_id": "RO20260126162444651280",
"production_app": "商户管理",
"production_menu": "工作室账户余额",
"production_resource_id": "RO20260203172036953160"
}
],
"/sysLog": [
{
"path": "/sysLog",
"component": "/pages/sysLog/index",
"test_app": "工作室基础应用",
"test_menu": "操作日志",
"test_resource_id": "RO20260127100656472280",
"production_app": "工作室管理",
"production_menu": "员工操作日志",
"production_resource_id": "RO20260203172212300162"
}
],
"/ticket": [
{
"path": "/ticket",
"component": "/pages/ticket/index",
"test_app": "Saas-福单",
"test_menu": "客服工单",
"test_resource_id": "RO20260415151112221530",
"production_app": "商户管理",
"production_menu": "客服工单列表",
"production_resource_id": "RO20260421172420767160"
}
],
"/trafficMemberFlowDetail": [
{
"path": "/trafficMemberFlowDetail",
"component": "/pages/statistics/trafficMemberFlowDetail/index",
"test_app": "代练工会解决方案后台(内部)",
"test_menu": "员工流量明细",
"test_resource_id": "RO20250604144604751022",
"production_app": "代练工会解决方案后台(内部)",
"production_menu": "成员流量变动记录",
"production_resource_id": "RO20250604144604751022"
}
],
"/orderManagement/myOrder": [
{
"path": "/orderManagement/myOrder",
"component": "/pages/orderManagement/myOrder/index",
"test_app": "打手应用",
"test_menu": "我的订单",
"test_resource_id": "",
"production_app": "打手应用",
"production_menu": "我的订单",
"production_resource_id": ""
}
]
}
}
......@@ -15,6 +15,17 @@ STORE_PATH = Path("runtime/e2e_accounts.json")
EXCEL_EXPORT_SCRIPT = Path("scripts/export_e2e_accounts_excel.py")
def default_store_path() -> Path:
return STORE_PATH
def excel_export_path_for(store_path: Path) -> Path:
suffix = "".join(store_path.suffixes)
if suffix.endswith(".json"):
return store_path.with_suffix(".xlsx")
return store_path.parent / f"{store_path.name}.xlsx"
@dataclass
class E2EAccount:
phone: str
......@@ -63,8 +74,8 @@ class E2EAccount:
class AccountStore:
def __init__(self, path: Path = STORE_PATH):
self.path = path
def __init__(self, path: Path | None = None):
self.path = path or STORE_PATH
def load(self) -> list[E2EAccount]:
if not self.path.exists():
......@@ -123,7 +134,11 @@ class AccountStore:
accounts = self.load()
now = datetime.now().isoformat(timespec="seconds")
for index, existing in enumerate(accounts):
if existing.phone == account.phone:
if (
existing.phone == account.phone
and existing.account_type == account.account_type
and existing.parent_phone == account.parent_phone
):
merged = existing
for key, value in asdict(account).items():
if value not in ("", "unknown", "not_opened", "not_bound"):
......@@ -216,6 +231,20 @@ class AccountStore:
return account
return None
def find_main(self, phone: str) -> E2EAccount | None:
return self.find(phone=phone, account_type="main")
def find_sub(self, *, phone: str, parent_phone: str = "", member_role: str = "") -> E2EAccount | None:
for account in reversed(self.load()):
if account.account_type != "sub" or account.phone != phone:
continue
if parent_phone and account.parent_phone != parent_phone:
continue
if member_role and account.member_role != member_role:
continue
return account
return None
def find_certified_without_wanyouji(self) -> E2EAccount | None:
return self.find(certification_status="approved", wanyouji_status="not_opened")
......@@ -230,6 +259,7 @@ class AccountStore:
def find_opened_without_binding(self) -> E2EAccount | None:
return self.find(
account_type="main",
certification_status="approved",
wanyouji_status="opened",
wanzhanggui_binding_status="not_bound",
......@@ -237,6 +267,8 @@ class AccountStore:
def find_wanyouji_without_wanzhanggui_binding(self, wanzhanggui_phone: str) -> E2EAccount | None:
for account in reversed(self.load()):
if account.account_type != "main":
continue
if account.certification_status != "approved" or account.wanyouji_status != "opened":
continue
if any(item.get("phone") == wanzhanggui_phone for item in account.wanzhanggui_bindings):
......@@ -248,6 +280,8 @@ class AccountStore:
def find_bound_wanyouji_without_workshop_authorization(self, wanzhanggui_phone: str) -> E2EAccount | None:
for account in reversed(self.load()):
if account.account_type != "main":
continue
if account.certification_status != "approved" or account.wanyouji_status != "opened":
continue
bound = account.bound_wanzhanggui_enterprise == wanzhanggui_phone or any(
......@@ -543,6 +577,7 @@ class AccountStore:
def find_permission_pending_binding(self) -> E2EAccount | None:
return self.find(
account_type="main",
certification_status="approved",
wanyouji_status="opened",
wanzhanggui_binding_status="permission_pending",
......
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
MAPPING_PATH = Path(__file__).resolve().parents[1] / "resources" / "menu_path_mapping.json"
@lru_cache(maxsize=1)
def menu_path_mapping() -> dict:
if not MAPPING_PATH.exists():
return {"paths": {}}
return json.loads(MAPPING_PATH.read_text(encoding="utf-8"))
def is_production_url(url: str) -> bool:
return "shunfju.com" in (url or "")
def menu_entries_for_path(
path: str,
*,
app_hint: str = "",
component: str = "",
) -> list[dict[str, str]]:
entries = list(menu_path_mapping().get("paths", {}).get(path, []))
if component:
component_matches = [entry for entry in entries if entry.get("component") == component]
if component_matches:
entries = component_matches
if app_hint:
app_matches = [
entry
for entry in entries
if app_hint in {entry.get("test_app", ""), entry.get("production_app", "")}
]
if app_matches:
entries = app_matches
return entries
def menu_names_for_path(
path: str,
*,
production: bool,
app_hint: str = "",
component: str = "",
) -> list[str]:
key = "production_menu" if production else "test_menu"
names: list[str] = []
for entry in menu_entries_for_path(path, app_hint=app_hint, component=component):
name = str(entry.get(key) or "").strip()
if name and name not in names:
names.append(name)
return names
......@@ -10,15 +10,37 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from e2e.utils.account_store import default_store_path
from e2e.utils.env_config import load_e2e_env
PROJECT_ROOT = Path(__file__).resolve().parents[1]
TESTS_DIR = PROJECT_ROOT / "e2e" / "tests"
RUNTIME_ACCOUNTS = PROJECT_ROOT / "runtime" / "e2e_accounts.json"
BUGS_DIR = PROJECT_ROOT / "bugs"
ALLURE_RESULTS = PROJECT_ROOT / "reports" / "allure-results"
ALLURE_REPORT = PROJECT_ROOT / "reports" / "allure-report"
load_e2e_env(PROJECT_ROOT)
RUNTIME_ACCOUNTS = PROJECT_ROOT / default_store_path()
def load_dotenv_value(key: str) -> str:
env_path = PROJECT_ROOT / ".env"
if not env_path.exists():
return ""
for line in _read_text(env_path).splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
current_key, value = stripped.split("=", 1)
if current_key == key:
return value.strip().strip("'\"")
return ""
@dataclass(frozen=True)
class TestCase:
path: str
......@@ -140,9 +162,18 @@ def allure_summary() -> dict[str, Any]:
def choose_targets(profile: str) -> list[str]:
profile = profile.strip()
no_registration_value = os.getenv("E2E_PRODUCTION_NO_REGISTRATION", "") or load_dotenv_value(
"E2E_PRODUCTION_NO_REGISTRATION"
)
no_registration = no_registration_value.lower() in {"1", "true", "yes", "on"}
registration_profiles = {"seed", "saas-001", "main"}
if no_registration and profile in registration_profiles:
raise ValueError(f"Production environment does not run registration automation: {profile}")
profile_map = {
"seed": ["-m", "e2e_seed"],
"standalone": ["-m", "e2e_standalone"],
"standalone": ["-m", "e2e_standalone and not e2e_seed and not e2e_saas_001"],
"login": ["-m", "e2e_login_only"],
"chain": ["-m", "e2e_chain"],
"main": ["e2e/tests/组合大流程/test_e2e_main_001_full_closed_loop.py"],
"main-002": ["e2e/tests/组合大流程/test_e2e_main_002_order_to_player_settlement.py"],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment