- 新增 router/access.ts 统一准入与落地页(工作台→业务数据→回款), 守卫/登录回跳/菜单/面包屑复用;普通用户不再被无限重定向 - 待办按角色映射在途审批节点(财务/领导/admin),admin 不代办但可查全部 - 预警排除已终止业务;WarningBlock.total 为全量命中数;回款趋势改日期范围 - 工作台加载请求序号防竞态;KPI 口径说明补经理分成/调整单 - 新增 role_home_flow e2e,更新 system_management 普通用户断言 Co-Authored-By: Claude Code <noreply@anthropic.com>
142 lines
5.1 KiB
Python
142 lines
5.1 KiB
Python
"""浏览器端到端验证:普通用户落地页与路由守卫回退。
|
||
|
||
工作台收紧为 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())
|