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
```
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
- `seed`: data/account creation tests.
......
......@@ -4,18 +4,19 @@ import os
from dataclasses import dataclass
import pytest
from dotenv import load_dotenv
from playwright.sync_api import Page
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)
class E2ESettings:
e2e_env: str
tenant_base_url: str
tenant_home_url: str
admin_url: str
......@@ -48,9 +49,30 @@ class E2ESettings:
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")
def settings() -> E2ESettings:
return E2ESettings(
e2e_env=os.getenv("E2E_ENV", "test"),
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"),
admin_url=os.getenv("ADMIN_URL", "https://test-saas-adminui.lianlianwz.com/pages/home/index"),
......@@ -73,8 +95,8 @@ def settings() -> E2ESettings:
feishu_poll_interval_seconds=int(os.getenv("FEISHU_POLL_INTERVAL_SECONDS", "3")),
certified_tenant_phone=os.getenv("E2E_CERTIFIED_TENANT_PHONE", "19361091624"),
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_merchant_id=os.getenv("E2E_CERTIFIED_TENANT_MERCHANT_ID", "99"),
certified_tenant_enterprise_name=os.getenv("E2E_CERTIFIED_TENANT_ENTERPRISE_NAME", "E2E已认证企业"),
certified_tenant_merchant_id=os.getenv("E2E_CERTIFIED_TENANT_MERCHANT_ID", ""),
wanyouji_product_id=os.getenv("E2E_WANYOUJI_PRODUCT_ID", "12"),
wanzhanggui_product_id=os.getenv("E2E_WANZHANGGUI_PRODUCT_ID", "15"),
wanzhanggui_tenant_phone=os.getenv("E2E_WANZHANGGUI_TENANT_PHONE", "19147509308"),
......@@ -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
def bug_reporter(page: Page, request) -> BugReporter:
marker_to_case = {
......
This diff is collapsed.
......@@ -293,6 +293,7 @@ def test_自动接单用例_普通单发单和分支校验(
page: Page,
settings,
bug_reporter,
dual_account_pages,
case: AutoAcceptanceCase,
) -> None:
store = account_store()
......@@ -307,8 +308,9 @@ def test_自动接单用例_普通单发单和分支校验(
f"丸友集账号 {case.wanyouji_phone} 未开通丸友集,当前状态:{wanyouji_account.wanyouji_status}"
)
order_page = WanzhangguiOrderPage(page, settings.tenant_home_url, bug_reporter)
fulfillment_page = WanyoujiFulfillmentPage(page, settings.tenant_home_url, bug_reporter)
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)
fulfillment_page = WanyoujiFulfillmentPage(wanyouji_page, settings.tenant_home_url, bug_reporter)
try:
if case.workshop_name:
order_page.login_main_account(case.wanzhanggui_phone, wanzhanggui_account.password)
......@@ -464,6 +466,7 @@ def test_自动接单用例_担保单发单和担保交易校验(
page: Page,
settings,
bug_reporter,
dual_account_pages,
case: GuaranteeAutoAcceptanceCase,
) -> None:
store = account_store()
......@@ -503,10 +506,11 @@ def test_自动接单用例_担保单发单和担保交易校验(
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:
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.ensure_auto_accept_assignment(case.merchant_name, enabled=True)
bug_reporter.record_if_errors(
......@@ -562,7 +566,7 @@ def test_自动接单用例_担保单发单和担保交易校验(
location=f"{case.case_id} 丸掌柜担保交易金额校验 {fulfillment_order_no}",
)
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)
bug_reporter.record_if_errors(
account=case.wanyouji_phone,
......
......@@ -30,12 +30,12 @@ def role_for_phone(phone: str) -> str:
wanzhanggui_phone = (
os.getenv("E2E_DUAL_BROWSER_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:
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:
role = account.member_role or account.contact_name or "sub"
if "打手" in role or role.lower() == "player":
......@@ -44,7 +44,7 @@ def role_for_phone(phone: str) -> str:
return f"wanyouji_customer_service_{phone}"
return f"wanyouji_member_{phone}"
if account and account.wanyouji_status == "opened":
return "wanyouji"
return f"wanyouji_{phone}"
if account and account.parent_phone:
role = account.member_role or account.contact_name or "sub"
if "鎵撴墜" in role or "打手" in role or role.lower() == "player":
......@@ -180,6 +180,25 @@ class DualBrowserManager:
setattr(slot.page, "_e2e_current_main_account", phone)
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(
self,
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 =
e2e_push_003: Wanzhanggui creates dispatch strategy
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_auto_acceptance: Wanzhanggui/Wanyouji smart auto-acceptance flows
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_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