Commit 44dd226a authored by wushanjing's avatar wushanjing

修正智能接单多账号和担保单流程

parent 072be617
...@@ -25,6 +25,16 @@ Use the repository's automation core instead of rediscovering test state manuall ...@@ -25,6 +25,16 @@ Use the repository's automation core instead of rediscovering test state manuall
``` ```
5. Patch Page Objects in `e2e/pages/` before patching tests when the problem is selector or navigation reuse. 5. Patch Page Objects in `e2e/pages/` before patching tests when the problem is selector or navigation reuse.
## Multi-Account Browser Rule
When creating or updating E2E cases that involve more than one logged-in tenant account, use the shared Chrome dual-account fixture instead of switching accounts in one page.
- Add `dual_account_pages` to the pytest function arguments.
- Call `wanzhanggui_page, wanyouji_page = dual_account_pages.pair(wanzhanggui_phone, wanyouji_phone)` before constructing Page Objects.
- Construct each Page Object with its own page, for example `WanzhangguiOrderPage(wanzhanggui_page, ...)` and `WanyoujiFulfillmentPage(wanyouji_page, ...)`.
- Do not manually launch browsers or create contexts in tests. The fixture routes accounts to one Chrome browser with isolated BrowserContexts/windows and asserts that the contexts are distinct.
- Keep `E2E_DUAL_BROWSER` enabled. It is enabled by default; only single-account tests may opt out.
## Profiles ## Profiles
- `seed`: data/account creation tests. - `seed`: data/account creation tests.
......
...@@ -4,18 +4,19 @@ import os ...@@ -4,18 +4,19 @@ import os
from dataclasses import dataclass from dataclasses import dataclass
import pytest import pytest
from dotenv import load_dotenv
from playwright.sync_api import Page from playwright.sync_api import Page
from e2e.utils.bug_reporter import BugReporter from e2e.utils.bug_reporter import BugReporter
from e2e.utils.dual_browser import dual_browser_enabled, dual_browser_manager from e2e.utils.dual_browser import assert_distinct_account_contexts, dual_browser_enabled, dual_browser_manager
from e2e.utils.env_config import load_e2e_env
load_dotenv() load_e2e_env()
@dataclass(frozen=True) @dataclass(frozen=True)
class E2ESettings: class E2ESettings:
e2e_env: str
tenant_base_url: str tenant_base_url: str
tenant_home_url: str tenant_home_url: str
admin_url: str admin_url: str
...@@ -48,9 +49,30 @@ class E2ESettings: ...@@ -48,9 +49,30 @@ class E2ESettings:
viewport_height: int viewport_height: int
@dataclass
class DualAccountPages:
seed_page: Page
viewport: dict[str, int]
def page_for(self, phone: str) -> Page:
assert dual_browser_enabled(), "多账号用例必须开启 E2E_DUAL_BROWSER,默认值为开启。"
return dual_browser_manager().page_for_account(
phone,
viewport=self.viewport,
seed_page=self.seed_page,
)
def pair(self, left_phone: str, right_phone: str) -> tuple[Page, Page]:
left_page = self.page_for(left_phone)
right_page = self.page_for(right_phone)
assert_distinct_account_contexts(left_page, left_phone, right_page, right_phone)
return left_page, right_page
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def settings() -> E2ESettings: def settings() -> E2ESettings:
return E2ESettings( return E2ESettings(
e2e_env=os.getenv("E2E_ENV", "test"),
tenant_base_url=os.getenv("TENANT_BASE_URL", "https://test-lianlianyun.lianlianwz.com"), tenant_base_url=os.getenv("TENANT_BASE_URL", "https://test-lianlianyun.lianlianwz.com"),
tenant_home_url=os.getenv("TENANT_HOME_URL", "https://test-lianlianyun.lianlianwz.com/home"), tenant_home_url=os.getenv("TENANT_HOME_URL", "https://test-lianlianyun.lianlianwz.com/home"),
admin_url=os.getenv("ADMIN_URL", "https://test-saas-adminui.lianlianwz.com/pages/home/index"), admin_url=os.getenv("ADMIN_URL", "https://test-saas-adminui.lianlianwz.com/pages/home/index"),
...@@ -73,8 +95,8 @@ def settings() -> E2ESettings: ...@@ -73,8 +95,8 @@ def settings() -> E2ESettings:
feishu_poll_interval_seconds=int(os.getenv("FEISHU_POLL_INTERVAL_SECONDS", "3")), feishu_poll_interval_seconds=int(os.getenv("FEISHU_POLL_INTERVAL_SECONDS", "3")),
certified_tenant_phone=os.getenv("E2E_CERTIFIED_TENANT_PHONE", "19361091624"), certified_tenant_phone=os.getenv("E2E_CERTIFIED_TENANT_PHONE", "19361091624"),
certified_tenant_password=os.getenv("E2E_CERTIFIED_TENANT_PASSWORD", "test123456"), certified_tenant_password=os.getenv("E2E_CERTIFIED_TENANT_PASSWORD", "test123456"),
certified_tenant_enterprise_name=os.getenv("E2E_CERTIFIED_TENANT_ENTERPRISE_NAME", "E2E测试企业0603174618802"), certified_tenant_enterprise_name=os.getenv("E2E_CERTIFIED_TENANT_ENTERPRISE_NAME", "E2E已认证企业"),
certified_tenant_merchant_id=os.getenv("E2E_CERTIFIED_TENANT_MERCHANT_ID", "99"), certified_tenant_merchant_id=os.getenv("E2E_CERTIFIED_TENANT_MERCHANT_ID", ""),
wanyouji_product_id=os.getenv("E2E_WANYOUJI_PRODUCT_ID", "12"), wanyouji_product_id=os.getenv("E2E_WANYOUJI_PRODUCT_ID", "12"),
wanzhanggui_product_id=os.getenv("E2E_WANZHANGGUI_PRODUCT_ID", "15"), wanzhanggui_product_id=os.getenv("E2E_WANZHANGGUI_PRODUCT_ID", "15"),
wanzhanggui_tenant_phone=os.getenv("E2E_WANZHANGGUI_TENANT_PHONE", "19147509308"), wanzhanggui_tenant_phone=os.getenv("E2E_WANZHANGGUI_TENANT_PHONE", "19147509308"),
...@@ -112,6 +134,17 @@ def browser_context_args(browser_context_args, settings: E2ESettings): ...@@ -112,6 +134,17 @@ def browser_context_args(browser_context_args, settings: E2ESettings):
} }
@pytest.fixture
def dual_account_pages(page: Page, settings: E2ESettings) -> DualAccountPages:
return DualAccountPages(
seed_page=page,
viewport={
"width": settings.viewport_width,
"height": settings.viewport_height,
},
)
@pytest.fixture @pytest.fixture
def bug_reporter(page: Page, request) -> BugReporter: def bug_reporter(page: Page, request) -> BugReporter:
marker_to_case = { marker_to_case = {
......
...@@ -11,7 +11,7 @@ from pathlib import Path ...@@ -11,7 +11,7 @@ from pathlib import Path
from playwright.sync_api import Frame, Page, expect from playwright.sync_api import Frame, Page, expect
from e2e.pages.tenant_auth_page import ENTERPRISE_CONSOLE_URL, TenantAuthPage from e2e.pages.tenant_auth_page import TenantAuthPage
from e2e.pages.wanzhanggui_push_page import WANZHANGGUI_CONSOLE_URL from e2e.pages.wanzhanggui_push_page import WANZHANGGUI_CONSOLE_URL
from e2e.utils.bug_reporter import BugReporter from e2e.utils.bug_reporter import BugReporter
from e2e.utils.data_factory import ( from e2e.utils.data_factory import (
...@@ -20,11 +20,14 @@ from e2e.utils.data_factory import ( ...@@ -20,11 +20,14 @@ from e2e.utils.data_factory import (
default_dispatch_price_percent, default_dispatch_price_percent,
) )
from e2e.utils.image_captcha import ImageCaptchaSolver, decode_data_image from e2e.utils.image_captcha import ImageCaptchaSolver, decode_data_image
from e2e.utils.env_config import assert_url_allowed_for_env
WANYOUJI_CONSOLE_URL = "https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20250814095939369041" WANYOUJI_CONSOLE_URL = "https://test-merchant-lianlianyun.lianlianwz.com/productConsole?productCode=P20250814095939369041"
WANYOUJI_WORKSHOP_ORDER_LIST_URL = "https://test-orbit.lianlianwz.com/order" WANYOUJI_WORKSHOP_ORDER_LIST_URL = "https://test-orbit.lianlianwz.com/order"
WANZHANGGUI_ORDER_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/orderList" WANZHANGGUI_ORDER_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/orderList"
PROD_WANZHANGGUI_CONSOLE_URL = "https://merchant-lianlianyun.shunfju.com/productConsole?productCode=P20251222184412861622"
PROD_WANZHANGGUI_ORDER_LIST_URL = "https://order-pc.shunfju.com/orderManage/orderList"
WANZHANGGUI_MAIN_APPEAL_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/orderAppeal" WANZHANGGUI_MAIN_APPEAL_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/orderAppeal"
WANZHANGGUI_SUB_ORDER_PENALTY_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/subOrderPunish" WANZHANGGUI_SUB_ORDER_PENALTY_LIST_URL = "https://test-order-pc.lianlianwz.com/orderManage/subOrderPunish"
WANZHANGGUI_SUPPLY_AUTH_URL = "https://test-futong-pc.lianlianwz.com/merchantSupplyAuth" WANZHANGGUI_SUPPLY_AUTH_URL = "https://test-futong-pc.lianlianwz.com/merchantSupplyAuth"
...@@ -46,7 +49,7 @@ class ManualOrderData: ...@@ -46,7 +49,7 @@ class ManualOrderData:
main_order_no: str = "" main_order_no: str = ""
receive_price: str = "80" receive_price: str = "80"
dispatch_price: str = "70" dispatch_price: str = "70"
dispatch_price_mode: str = "percent" dispatch_price_mode: str = ""
dispatch_price_percent: str = "" dispatch_price_percent: str = ""
sub_order_title: str = "" sub_order_title: str = ""
security_deposit: str = "0" security_deposit: str = "0"
...@@ -89,20 +92,29 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -89,20 +92,29 @@ class WanzhangguiOrderPage(TenantAuthPage):
super().__init__(page, tenant_home_url, bug_reporter) super().__init__(page, tenant_home_url, bug_reporter)
def open_order_management(self) -> Frame: def open_order_management(self) -> Frame:
self.goto(WANZHANGGUI_CONSOLE_URL) self.goto_wanzhanggui_console()
self.wait_for_enterprise_console_shell() self.wait_for_enterprise_console_shell()
self.page.wait_for_timeout(1200) self.page.wait_for_timeout(1200)
self.dismiss_transient_order_error() self.dismiss_transient_order_error()
if self.is_production():
self.click_sidebar_text("商家订单管理列表", right_edge=True)
self.page.wait_for_timeout(800)
self.click_sidebar_text("订单管理列表")
else:
self.click_sidebar_text("商家管理", right_edge=True) self.click_sidebar_text("商家管理", right_edge=True)
self.page.wait_for_timeout(800) self.page.wait_for_timeout(800)
self.click_sidebar_text("订单管理") self.click_sidebar_path(
"/orderManage/orderList",
app_hint="Saas-福单",
fallback_texts=["订单管理"],
)
self.page.wait_for_timeout(3500) self.page.wait_for_timeout(3500)
for attempt in range(3): for attempt in range(3):
self.dismiss_transient_order_error() self.dismiss_transient_order_error()
frame = self.order_frame() frame = self.order_frame()
if "/orderManage/orderList" not in frame.url: if "/orderManage/orderList" not in frame.url:
frame.evaluate("url => { window.location.href = url; }", WANZHANGGUI_ORDER_LIST_URL) frame.evaluate("url => { window.location.href = url; }", self.order_list_url())
self.page.wait_for_timeout(3000) self.page.wait_for_timeout(3000)
try: try:
return self.wait_for_order_list_ready() return self.wait_for_order_list_ready()
...@@ -110,10 +122,21 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -110,10 +122,21 @@ class WanzhangguiOrderPage(TenantAuthPage):
if attempt == 2: if attempt == 2:
raise raise
self.dismiss_transient_order_error() self.dismiss_transient_order_error()
self.click_sidebar_text("订单管理") self.click_sidebar_text("订单管理列表" if self.is_production() else "订单管理")
self.page.wait_for_timeout(3500) self.page.wait_for_timeout(3500)
return self.wait_for_order_list_ready() return self.wait_for_order_list_ready()
def is_production(self) -> bool:
return "shunfju.com" in self.tenant_home_url
def goto_wanzhanggui_console(self) -> None:
self.goto(PROD_WANZHANGGUI_CONSOLE_URL if self.is_production() else WANZHANGGUI_CONSOLE_URL)
self.page.wait_for_load_state("domcontentloaded")
self.page.wait_for_timeout(3000)
def order_list_url(self) -> str:
return PROD_WANZHANGGUI_ORDER_LIST_URL if self.is_production() else WANZHANGGUI_ORDER_LIST_URL
def wait_for_order_list_ready(self) -> Frame: def wait_for_order_list_ready(self) -> Frame:
last_text = "" last_text = ""
for attempt in range(8): for attempt in range(8):
...@@ -124,13 +147,13 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -124,13 +147,13 @@ class WanzhangguiOrderPage(TenantAuthPage):
self.dismiss_transient_order_error(frame) self.dismiss_transient_order_error(frame)
self.page.wait_for_timeout(1200) self.page.wait_for_timeout(1200)
continue continue
if "手工录单" in last_text and "订单编号" in last_text: if ("手工录单" in last_text or "抖音录单" in last_text) and "订单编号" in last_text:
return frame return frame
except Exception: except Exception:
pass pass
if attempt in {2, 5}: if attempt in {2, 5}:
try: try:
frame.evaluate("url => { window.location.href = url; }", WANZHANGGUI_ORDER_LIST_URL) frame.evaluate("url => { window.location.href = url; }", self.order_list_url())
except Exception: except Exception:
try: try:
self.page.reload(wait_until="domcontentloaded") self.page.reload(wait_until="domcontentloaded")
...@@ -140,7 +163,11 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -140,7 +163,11 @@ class WanzhangguiOrderPage(TenantAuthPage):
continue continue
if attempt == 4: if attempt == 4:
try: try:
self.click_sidebar_text("订单管理") self.click_sidebar_path(
"/orderManage/orderList",
app_hint="Saas-福单",
fallback_texts=["订单管理列表", "商家订单管理列表"] if self.is_production() else ["订单管理"],
)
except Exception: except Exception:
pass pass
self.page.wait_for_timeout(1000) self.page.wait_for_timeout(1000)
...@@ -154,7 +181,11 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -154,7 +181,11 @@ class WanzhangguiOrderPage(TenantAuthPage):
self.click_sidebar_text("商家管理", right_edge=True) self.click_sidebar_text("商家管理", right_edge=True)
self.page.wait_for_timeout(800) self.page.wait_for_timeout(800)
try: try:
self.click_sidebar_text("主单申诉单") self.click_sidebar_path(
"/orderManage/orderAppeal",
app_hint="Saas-福单",
fallback_texts=["主单申诉单"],
)
self.page.wait_for_timeout(2500) self.page.wait_for_timeout(2500)
except Exception: except Exception:
pass pass
...@@ -262,7 +293,7 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -262,7 +293,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
def order_frame(self) -> Frame: def order_frame(self) -> Frame:
for frame in self.page.frames: for frame in self.page.frames:
if "test-order-pc" in frame.url: if "test-order-pc" in frame.url or "order-pc.shunfju.com" in frame.url:
return frame return frame
urls = [frame.url for frame in self.page.frames] urls = [frame.url for frame in self.page.frames]
raise AssertionError(f"Wanzhanggui order iframe not found; frames={urls}") raise AssertionError(f"Wanzhanggui order iframe not found; frames={urls}")
...@@ -352,6 +383,7 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -352,6 +383,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
for frame in self.page.frames: for frame in self.page.frames:
if "test-futong-pc" in frame.url: if "test-futong-pc" in frame.url:
if path not in frame.url: if path not in frame.url:
assert_url_allowed_for_env(target_url)
frame.goto(target_url, wait_until="domcontentloaded") frame.goto(target_url, wait_until="domcontentloaded")
self.page.wait_for_timeout(1500) self.page.wait_for_timeout(1500)
return frame return frame
...@@ -542,7 +574,7 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -542,7 +574,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
return trade_record["row_text"] return trade_record["row_text"]
def open_guarantee_trade_detail_page(self) -> Page: def open_guarantee_trade_detail_page(self) -> Page:
self.goto(ENTERPRISE_CONSOLE_URL) self.goto_enterprise_console()
self.wait_for_enterprise_console_shell() self.wait_for_enterprise_console_shell()
try: try:
self.click_sidebar_text("企业概览") self.click_sidebar_text("企业概览")
...@@ -566,6 +598,7 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -566,6 +598,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
trade_page.wait_for_timeout(2500) trade_page.wait_for_timeout(2500)
if not self.guarantee_trade_scope_or_none(trade_page): if not self.guarantee_trade_scope_or_none(trade_page):
print("[E2E] 未捕获到担保平台新页,直接进入担保平台资金管理页") print("[E2E] 未捕获到担保平台新页,直接进入担保平台资金管理页")
assert_url_allowed_for_env(GUARANTEE_PLATFORM_FUND_URL)
trade_page.goto(GUARANTEE_PLATFORM_FUND_URL, wait_until="domcontentloaded") trade_page.goto(GUARANTEE_PLATFORM_FUND_URL, wait_until="domcontentloaded")
trade_page.wait_for_timeout(3000) trade_page.wait_for_timeout(3000)
return trade_page return trade_page
...@@ -4750,7 +4783,7 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -4750,7 +4783,7 @@ class WanzhangguiOrderPage(TenantAuthPage):
@staticmethod @staticmethod
def resolve_sub_order_price_mode(order: ManualOrderData) -> str: def resolve_sub_order_price_mode(order: ManualOrderData) -> str:
raw = os.getenv("E2E_S_FLOW_DISPATCH_PRICE_MODE", order.dispatch_price_mode or "percent").strip().lower() raw = (order.dispatch_price_mode or os.getenv("E2E_S_FLOW_DISPATCH_PRICE_MODE", "percent")).strip().lower()
if raw in {"percent", "percentage", "百分比", "百分比发单价"}: if raw in {"percent", "percentage", "百分比", "百分比发单价"}:
return "percent" return "percent"
if raw in {"fixed", "amount", "固定", "固定发单价"}: if raw in {"fixed", "amount", "固定", "固定发单价"}:
...@@ -4771,6 +4804,8 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -4771,6 +4804,8 @@ class WanzhangguiOrderPage(TenantAuthPage):
f"必须为 {DISPATCH_PRICE_PERCENT_MIN}-{DISPATCH_PRICE_PERCENT_MAX} 的整数;当前值:{value}" f"必须为 {DISPATCH_PRICE_PERCENT_MIN}-{DISPATCH_PRICE_PERCENT_MAX} 的整数;当前值:{value}"
) )
return value return value
if order.dispatch_price_mode:
return order.dispatch_price.strip() or os.getenv("E2E_S_FLOW_DISPATCH_PRICE", "70").strip() or "70"
return os.getenv("E2E_S_FLOW_DISPATCH_PRICE", order.dispatch_price).strip() or order.dispatch_price return os.getenv("E2E_S_FLOW_DISPATCH_PRICE", order.dispatch_price).strip() or order.dispatch_price
def select_sub_order_price_mode(self, frame: Frame, strategy_name: str, price_label: str) -> None: def select_sub_order_price_mode(self, frame: Frame, strategy_name: str, price_label: str) -> None:
...@@ -5019,7 +5054,13 @@ class WanzhangguiOrderPage(TenantAuthPage): ...@@ -5019,7 +5054,13 @@ class WanzhangguiOrderPage(TenantAuthPage):
return return
selector = selectors.first selector = selectors.first
current_text = selector.inner_text(timeout=5000).strip() current_text = selector.inner_text(timeout=5000).strip()
if current_text and "请选择" not in current_text and "璇烽€夋嫨" not in current_text and text not in current_text: if (
current_text
and not current_text.isdigit()
and "请选择" not in current_text
and "璇烽€夋嫨" not in current_text
and text not in current_text
):
return return
selector.click(force=True) selector.click(force=True)
self.page.wait_for_timeout(800) self.page.wait_for_timeout(800)
...@@ -9306,7 +9347,17 @@ class WanyoujiFulfillmentPage(TenantAuthPage): ...@@ -9306,7 +9347,17 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
self.goto(WANYOUJI_CONSOLE_URL) self.goto(WANYOUJI_CONSOLE_URL)
self.wait_for_enterprise_console_shell() self.wait_for_enterprise_console_shell()
self.expand_player_app_menu() self.expand_player_app_menu()
if self.click_sidebar_leaf_strict("我的订单"): clicked = False
try:
self.click_sidebar_path(
"/orderManagement/myOrder",
app_hint="打手应用",
fallback_texts=["我的订单"],
)
clicked = True
except Exception:
clicked = self.click_sidebar_leaf_strict("我的订单")
if clicked:
self.page.wait_for_timeout(4000) self.page.wait_for_timeout(4000)
frame = self.player_my_orders_frame_if_ready() frame = self.player_my_orders_frame_if_ready()
if frame is not None: if frame is not None:
...@@ -9623,6 +9674,12 @@ class WanyoujiFulfillmentPage(TenantAuthPage): ...@@ -9623,6 +9674,12 @@ class WanyoujiFulfillmentPage(TenantAuthPage):
text, text,
right_edge=right_edge, right_edge=right_edge,
) )
if not clicked:
clicked = (
self.click_sidebar_menu_item_exact(text)
or self.click_sidebar_leaf_precise(text)
or self.click_sidebar_text_scrolled(text, right_edge=right_edge)
)
if not clicked: if not clicked:
return False return False
self.page.wait_for_timeout(700) self.page.wait_for_timeout(700)
......
...@@ -293,6 +293,7 @@ def test_自动接单用例_普通单发单和分支校验( ...@@ -293,6 +293,7 @@ def test_自动接单用例_普通单发单和分支校验(
page: Page, page: Page,
settings, settings,
bug_reporter, bug_reporter,
dual_account_pages,
case: AutoAcceptanceCase, case: AutoAcceptanceCase,
) -> None: ) -> None:
store = account_store() store = account_store()
...@@ -307,8 +308,9 @@ def test_自动接单用例_普通单发单和分支校验( ...@@ -307,8 +308,9 @@ def test_自动接单用例_普通单发单和分支校验(
f"丸友集账号 {case.wanyouji_phone} 未开通丸友集,当前状态:{wanyouji_account.wanyouji_status}" f"丸友集账号 {case.wanyouji_phone} 未开通丸友集,当前状态:{wanyouji_account.wanyouji_status}"
) )
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter) wanzhanggui_page, wanyouji_page = dual_account_pages.pair(case.wanzhanggui_phone, case.wanyouji_phone)
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter) order_page = WanzhangguiOrderPage(wanzhanggui_page, settings.tenant_home_url, bug_reporter)
fulfillment_page = WanyoujiFulfillmentPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
try: try:
if case.workshop_name: if case.workshop_name:
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password) order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
...@@ -464,6 +466,7 @@ def test_自动接单用例_担保单发单和担保交易校验( ...@@ -464,6 +466,7 @@ def test_自动接单用例_担保单发单和担保交易校验(
page: Page, page: Page,
settings, settings,
bug_reporter, bug_reporter,
dual_account_pages,
case: GuaranteeAutoAcceptanceCase, case: GuaranteeAutoAcceptanceCase,
) -> None: ) -> None:
store = account_store() store = account_store()
...@@ -503,10 +506,11 @@ def test_自动接单用例_担保单发单和担保交易校验( ...@@ -503,10 +506,11 @@ def test_自动接单用例_担保单发单和担保交易校验(
role_name=f"AAG角色{timestamp[-4:]}", role_name=f"AAG角色{timestamp[-4:]}",
) )
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter) wanzhanggui_page, wanyouji_page = dual_account_pages.pair(case.wanzhanggui_phone, case.wanyouji_phone)
order_page = WanzhangguiOrderPage(wanzhanggui_page, settings.tenant_home_url, bug_reporter)
try: try:
if case.merchant_name: if case.merchant_name:
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter) fulfillment_page = WanyoujiFulfillmentPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password) fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
fulfillment_page.ensure_auto_accept_assignment(case.merchant_name, enabled=True) fulfillment_page.ensure_auto_accept_assignment(case.merchant_name, enabled=True)
bug_reporter.record_if_errors( bug_reporter.record_if_errors(
...@@ -562,7 +566,7 @@ def test_自动接单用例_担保单发单和担保交易校验( ...@@ -562,7 +566,7 @@ def test_自动接单用例_担保单发单和担保交易校验(
location=f"{case.case_id} 丸掌柜担保交易金额校验 {fulfillment_order_no}", location=f"{case.case_id} 丸掌柜担保交易金额校验 {fulfillment_order_no}",
) )
else: else:
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter) fulfillment_page = WanyoujiFulfillmentPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password) fulfillment_page.login_main_account(case.wanyouji_phone, wanyouji_account.password)
bug_reporter.record_if_errors( bug_reporter.record_if_errors(
account=case.wanyouji_phone, account=case.wanyouji_phone,
......
...@@ -30,12 +30,12 @@ def role_for_phone(phone: str) -> str: ...@@ -30,12 +30,12 @@ def role_for_phone(phone: str) -> str:
wanzhanggui_phone = ( wanzhanggui_phone = (
os.getenv("E2E_DUAL_BROWSER_WANZHANGGUI_PHONE", "").strip() os.getenv("E2E_DUAL_BROWSER_WANZHANGGUI_PHONE", "").strip()
or os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", "").strip() or os.getenv("E2E_ORDER_WANZHANGGUI_PHONE", "").strip()
or os.getenv("E2E_WANZHANGGUI_TENANT_PHONE", "19147509308").strip() or os.getenv("E2E_WANZHANGGUI_TENANT_PHONE", "17335564065").strip()
) )
if phone == wanzhanggui_phone: if phone == wanzhanggui_phone:
return "wanzhanggui" return "wanzhanggui"
account = account_store().find(phone=phone) account = account_store().find_main(phone) or account_store().find(phone=phone)
if account and account.parent_phone: if account and account.parent_phone:
role = account.member_role or account.contact_name or "sub" role = account.member_role or account.contact_name or "sub"
if "打手" in role or role.lower() == "player": if "打手" in role or role.lower() == "player":
...@@ -44,7 +44,7 @@ def role_for_phone(phone: str) -> str: ...@@ -44,7 +44,7 @@ def role_for_phone(phone: str) -> str:
return f"wanyouji_customer_service_{phone}" return f"wanyouji_customer_service_{phone}"
return f"wanyouji_member_{phone}" return f"wanyouji_member_{phone}"
if account and account.wanyouji_status == "opened": if account and account.wanyouji_status == "opened":
return "wanyouji" return f"wanyouji_{phone}"
if account and account.parent_phone: if account and account.parent_phone:
role = account.member_role or account.contact_name or "sub" role = account.member_role or account.contact_name or "sub"
if "鎵撴墜" in role or "打手" in role or role.lower() == "player": if "鎵撴墜" in role or "打手" in role or role.lower() == "player":
...@@ -180,6 +180,25 @@ class DualBrowserManager: ...@@ -180,6 +180,25 @@ class DualBrowserManager:
setattr(slot.page, "_e2e_current_main_account", phone) setattr(slot.page, "_e2e_current_main_account", phone)
self.set_active_page(slot.page) self.set_active_page(slot.page)
def register_page_for_account(self, phone: str, page: Page) -> None:
role = role_for_phone(phone)
self.register_page_for_role(role, page, current_phone=phone)
def register_page_for_role(self, role: str, page: Page, current_phone: str = "") -> None:
slot = self._slots.get(role)
if not slot:
return
setattr(page, "_e2e_dual_browser_role", role)
setattr(page, "_e2e_dual_browser_channel", slot.channel)
if current_phone:
setattr(page, "_e2e_current_main_account", current_phone)
slot.page = page
slot.context = page.context
slot.browser = page.context.browser or slot.browser
if current_phone:
slot.current_phone = current_phone
self.set_active_page(page)
def _launch_slot( def _launch_slot(
self, self,
role: str, role: str,
......
from __future__ import annotations
import os
from pathlib import Path
from urllib.parse import urlparse
from dotenv import dotenv_values, load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_E2E_ENV = "test"
VALID_E2E_ENVS = {"test", "prod"}
PROD_HOST_SUFFIXES = ("shunfju.com",)
TEST_HOST_SUFFIXES = ("lianlianwz.com",)
def e2e_env() -> str:
value = os.getenv("E2E_ENV", DEFAULT_E2E_ENV).strip().lower()
return value if value in VALID_E2E_ENVS else DEFAULT_E2E_ENV
def load_e2e_env(project_root: Path | None = None) -> str:
root = project_root or PROJECT_ROOT
shell_keys = set(os.environ)
base_env = root / ".env"
if base_env.exists():
load_dotenv(base_env, override=False)
selected_env = e2e_env()
env_file = root / f".env.{selected_env}"
if env_file.exists():
for key, value in dotenv_values(env_file).items():
if value is not None and key not in shell_keys:
os.environ[key] = value
os.environ.setdefault("E2E_ENV", selected_env)
return selected_env
def is_production_env() -> bool:
return e2e_env() == "prod"
def allow_env_url_mismatch() -> bool:
return os.getenv("E2E_ALLOW_ENV_URL_MISMATCH", "").strip().lower() in {"1", "true", "yes", "on"}
def _host_matches(hostname: str, suffixes: tuple[str, ...]) -> bool:
host = hostname.lower().rstrip(".")
return any(host == suffix or host.endswith(f".{suffix}") for suffix in suffixes)
def _is_test_host(hostname: str) -> bool:
host = hostname.lower().rstrip(".")
return _host_matches(host, TEST_HOST_SUFFIXES) and (host.startswith("test-") or ".test-" in host)
def assert_url_allowed_for_env(url: str) -> None:
if allow_env_url_mismatch():
return
parsed = urlparse(url)
if parsed.scheme in {"", "about", "data", "blob", "file"}:
return
hostname = parsed.hostname or ""
selected_env = e2e_env()
if selected_env == "test" and _host_matches(hostname, PROD_HOST_SUFFIXES):
raise AssertionError(
f"E2E_ENV=test forbids production URL: {url}. "
"Use test URLs/accounts, or set E2E_ALLOW_ENV_URL_MISMATCH=1 only for an explicit manual check."
)
if selected_env == "prod" and _is_test_host(hostname):
raise AssertionError(
f"E2E_ENV=prod forbids test URL: {url}. "
"Use production URLs/accounts, or set E2E_ALLOW_ENV_URL_MISMATCH=1 only for an explicit manual check."
)
...@@ -14,6 +14,7 @@ markers = ...@@ -14,6 +14,7 @@ markers =
e2e_push_003: Wanzhanggui creates dispatch strategy e2e_push_003: Wanzhanggui creates dispatch strategy
e2e_order_001: Wanzhanggui manual order dispatch and Wanyouji order hall verification e2e_order_001: Wanzhanggui manual order dispatch and Wanyouji order hall verification
e2e_order_002: Wanzhanggui revoke dispatch and Wanyouji order hall invisibility verification e2e_order_002: Wanzhanggui revoke dispatch and Wanyouji order hall invisibility verification
e2e_auto_acceptance: Wanzhanggui/Wanyouji smart auto-acceptance flows
data_config_001: Wanzhanggui data dictionary setup for work-order aftersale options data_config_001: Wanzhanggui data dictionary setup for work-order aftersale options
data_config_002: Wanzhanggui data dictionary setup for work-order urgency options data_config_002: Wanzhanggui data dictionary setup for work-order urgency options
data_config_003: Wanzhanggui data dictionary setup for work-order follow-up options data_config_003: Wanzhanggui data dictionary setup for work-order follow-up options
......
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