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