"""浏览器端到端验证:普通用户落地页与路由守卫回退。 工作台收紧为 admin/finance/leader 后,登录默认页不能硬编码 `/dashboard` (普通用户会被无限重定向)。本脚本验证: - 普通用户登录后落业务数据页,不出现「无权访问」提示 - 侧边菜单不出现首页/系统管理/报表统计 - 刷新后仍保持登录态与落地页 - 直连 `/dashboard` 或报表页时回退业务数据页并给出权限提示 运行前置:容器、后端 8000、前端 5173 均在运行;设置 ``E2E_ADMIN_PASSWORD``。 运行:: cd frontend && E2E_ADMIN_PASSWORD=<密码> python e2e/role_home_flow.py """ import json import os import time import urllib.error import urllib.request import uuid from playwright.sync_api import Page, sync_playwright from common import BASE_URL, E2EFailure, current_path, fail, login, toast_text ADMIN = "admin" USERNAME = f"e2e_role_{int(time.time())}_{uuid.uuid4().hex[:4]}" PASSWORD = "E2eRole@2026" HOME_PATH = "/business/businesses" def _api(method: str, path: str, token: str, body: dict | None = None) -> dict: """带令牌的轻量 API 调用(urllib,避免额外依赖)。""" request = urllib.request.Request( f"{BASE_URL}{path}", method=method, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, data=json.dumps(body).encode() if body is not None else None, ) try: with urllib.request.urlopen(request, timeout=20) as response: payload = json.loads(response.read().decode()) except urllib.error.HTTPError as exc: detail = exc.read().decode()[:200] raise E2EFailure(f"API {method} {path} HTTP {exc.code}:{detail}") from exc if payload.get("code") != 200: raise E2EFailure(f"API {method} {path} 业务失败:{payload.get('message')}") return payload["data"] def _token(page: Page) -> str: token = page.evaluate("localStorage.getItem('ys_token')") if not token: fail("API", "未取到登录令牌") return token def _create_user(page: Page) -> dict: return _api( "POST", "/api/v1/system/users", _token(page), { "username": USERNAME, "password": PASSWORD, "real_name": "E2E普通用户", "roles": ["user"], }, ) def run(page: Page) -> None: admin_password = os.environ.get("E2E_ADMIN_PASSWORD", "") if not admin_password: fail("环境变量", "未设置 E2E_ADMIN_PASSWORD(开发库 admin 密码)") login(page, ADMIN, admin_password) user = _create_user(page) try: page.evaluate("localStorage.clear()") login(page, USERNAME, PASSWORD, expected_path=HOME_PATH) page.wait_for_timeout(500) print("✓ 1. 普通用户登录 落地业务数据页") menu_text = page.locator(".app-menu").inner_text() for forbidden in ("首页", "系统管理", "报表统计"): if forbidden in menu_text: fail("菜单权限", f"普通用户看到了「{forbidden}」菜单") print("✓ 2. 菜单权限 首页/系统管理/报表统计均不可见") page.reload(wait_until="networkidle") page.wait_for_timeout(600) if current_path(page) != HOME_PATH: fail("刷新恢复", f"刷新后未保持落地页,当前 {page.url}") print("✓ 3. 刷新恢复 登录态与落地页保持") for path in ("/dashboard", "/reports/personal-detail"): page.goto(f"{BASE_URL}{path}", wait_until="networkidle") page.wait_for_timeout(800) if current_path(page) != HOME_PATH: fail("路由守卫", f"直连 {path} 未回退业务数据页,当前 {page.url}") if "无权访问该页面" not in toast_text(page): fail("路由守卫", f"直连 {path} 未给出权限提示") print("✓ 4. 路由守卫 受限页均回退业务数据页并提示") finally: # 无论用例是否失败都清理测试账号(重新以 admin 登录取令牌) page.evaluate("localStorage.clear()") login(page, ADMIN, admin_password) _api("DELETE", f"/api/v1/system/users/{user['id']}", _token(page)) def main() -> int: with sync_playwright() as playwright: browser = playwright.chromium.launch(headless=True) page = browser.new_page(viewport={"width": 1440, "height": 900}) page.set_default_timeout(15000) try: run(page) except E2EFailure as exc: page.screenshot(path="e2e/failure.png", full_page=True) print(f"\n ✗ {exc}") print(" 已保存截图 e2e/failure.png") return 1 except Exception as exc: # noqa: BLE001 — 测试脚本需打印完整失败原因 page.screenshot(path="e2e/failure.png", full_page=True) print(f"\n ✗ 执行中断:{type(exc).__name__}: {exc}") return 1 finally: browser.close() print("\n全部通过。") return 0 if __name__ == "__main__": raise SystemExit(main())