"""KSP管理后台 前端迁移 E2E 测试""" import asyncio, json from pathlib import Path from playwright.async_api import async_playwright BASE = "http://127.0.0.1:9000" SS = Path(__file__).resolve().parent.parent / "demo" / "test_screenshots" SS.mkdir(exist_ok=True) results = [] def T(name, ok, detail=""): results.append({"name": name, "ok": ok, "detail": detail}) print(f" [{'OK' if ok else 'FAIL'}] {name}" + (f" -- {detail}" if detail and not ok else "")) async def run(): async with async_playwright() as pw: b = await pw.chromium.launch(headless=True) pg = await b.new_page(viewport={"width":1440,"height":900}) errs = [] pg.on("console", lambda m: errs.append(m.text) if m.type=="error" else None) # ─── M1: 基础框架 ─── print("\n=== M1: 基础框架 ===") await pg.goto(f"{BASE}/", wait_until="networkidle", timeout=10000) await pg.wait_for_timeout(800) items = await pg.query_selector_all("#sidebar .ni") T("1.1 侧边栏导航", len(items)>=8, f"{len(items)} items") secs = await pg.query_selector_all(".nst") exp = 0 for s in secs: if not (await s.evaluate("el => el.classList.contains('on')")): exp += 1 T("1.2 侧边栏默认展开", exp>=2, f"{exp} expanded") act = await pg.query_selector_all(".ni.act") T("1.3 当前页高亮", len(act)>=1) theme = await pg.evaluate("()=>document.documentElement.getAttribute('data-theme')") T("1.4 默认深色模式", theme=="dark", f"theme={theme}") await pg.evaluate("()=>toggleTheme()") await pg.wait_for_timeout(200) T("1.5 浅色模式", await pg.evaluate("()=>document.documentElement.getAttribute('data-theme')")=="light") await pg.evaluate("()=>toggleTheme()") await pg.wait_for_timeout(200) T("1.6 深色模式切换回", await pg.evaluate("()=>document.documentElement.getAttribute('data-theme')")=="dark") td = await pg.text_content("#sim-time-display") T("1.7 模拟时间显示", "2060" in (td or "")) await pg.evaluate("()=>openTimeModal()") await pg.wait_for_timeout(300) T("1.12 Modal打开", await pg.evaluate("()=>document.getElementById('time-modal').classList.contains('open')")) await pg.evaluate("()=>closeTimeModal()") T("1.14 Cancel关闭", await pg.evaluate("()=>!document.getElementById('time-modal').classList.contains('open')")) await pg.evaluate("()=>openTimeModal()") await pg.keyboard.press("Escape") await pg.wait_for_timeout(200) T("1.15 ESC关闭", await pg.evaluate("()=>!document.getElementById('time-modal').classList.contains('open')")) # ─── M2: 任务总览 ─── print("\n=== M2: 任务总览 ===") h1 = await pg.text_content("h1") T("2.1 标题", h1 and "任务总览" in h1) cards = await pg.query_selector_all(".mc") T("2.2 指标卡片", len(cards)>=3, f"{len(cards)} cards") ac = await pg.text_content(".mc.ac .v") T("2.3 真实数据", ac and int(ac)>0, f"count={ac}") grps = await pg.query_selector_all(".sgi") T("2.4 状态看板", len(grps)>=2, f"{len(grps)} groups") links = await pg.query_selector_all(".srw .an") T("2.6 资产链接", len(links)>=1) # ─── M3: 资产列表 ─── print("\n=== M3: 资产列表 ===") await pg.goto(f"{BASE}/assets", wait_until="networkidle", timeout=10000) await pg.wait_for_timeout(500) rows = await pg.query_selector_all("table tbody tr") T("3.1 列表渲染", len(rows)>=1, f"{len(rows)} rows") await pg.fill('input[name="q"]', "ST-01") await pg.click('button[type="submit"]') await pg.wait_for_timeout(500) rows2 = await pg.query_selector_all("table tbody tr") T("3.2 搜索", len(rows2){len(rows2)}") # ─── M4: 资产详情 ─── print("\n=== M4: 资产详情 ===") await pg.goto(f"{BASE}/assets", wait_until="networkidle", timeout=10000) await pg.wait_for_timeout(500) fl = await pg.query_selector("table tbody tr:first-child td:first-child a") if fl: await fl.click() await pg.wait_for_timeout(1000) h1d = await pg.text_content("h1") T("4.1 详情渲染", h1d and len(h1d)>0) cards = await pg.query_selector_all(".mc") T("4.2 信息卡片", len(cards)>=4, f"{len(cards)} cards") lrows = await pg.query_selector_all("table tbody tr") T("4.5 日志列表", len(lrows)>=1) # ─── M5: 对接 ─── print("\n=== M5: 对接功能 ===") dock = await pg.query_selector('button:text("+ Dock")') if dock: await dock.click() await pg.wait_for_timeout(500) T("5.1 Dock按钮", await pg.evaluate("()=>document.getElementById('dock-modal')?.classList.contains('open')")) opts = await pg.query_selector_all("#dock-vehicle option") T("5.2 Modal内容", len(opts)>=2, f"{len(opts)} options") await pg.evaluate("()=>closeModal('dock-modal')") T("5.6 Cancel", True) else: T("5.1 Dock按钮", False, "not found") # ─── M6: 日志CRUD ─── print("\n=== M6: 日志CRUD ===") sb = await pg.query_selector('button:text("+ State Interval")') if sb: await sb.click() await pg.wait_for_timeout(500) T("6.1 State Interval", await pg.evaluate("()=>document.getElementById('log-modal')?.classList.contains('open')")) ab = await pg.query_selector('button:text("+ Add Node")') if ab: await ab.click(); await ab.click() T("6.5 Node添加", await pg.evaluate("()=>logNodeData.length")>=2) await pg.evaluate("()=>closeModal('log-modal')") T("6.10 Modal关闭", True) else: T("6.1 State Interval", False, "not found") # ─── M7: 地点板 ─── print("\n=== M7: 地点板 ===") await pg.goto(f"{BASE}/mission-preview/location-board", wait_until="networkidle", timeout=10000) await pg.wait_for_timeout(1500) T("7.1 Canvas", await pg.query_selector("#map-cv") is not None) sc = await pg.text_content("#mi-title") T("7.2 范围", sc and "Solar" in sc) # ─── M8: 时间线 ─── print("\n=== M8: 时间线 ===") await pg.goto(f"{BASE}/mission-preview/timeline?sim_time=2060-03-12T09:00", wait_until="networkidle", timeout=10000) await pg.wait_for_timeout(1500) T("8.1 Canvas", await pg.query_selector("#tl-canvas") is not None) T("8.2 刻度行", await pg.query_selector("#tl-canvas > div:first-child") is not None) rows = await pg.query_selector_all(".tl-fl") T("8.3 段", len(rows)>=1, f"{len(rows)} segments") evts = await pg.query_selector_all(".tl-ev, .tl-sn") T("8.4 事件", len(evts)>=1, f"{len(evts)} events") links = await pg.query_selector_all("#tl-canvas a[href*='/assets/']") T("8.5 资产链接", len(links)>=1, f"{len(links)} links") has_json = await pg.evaluate("() => !!document.getElementById('tl-json')") T("8.6 JSON数据", has_json) await pg.mouse.wheel(0, 100) await pg.wait_for_timeout(500) T("8.7 滚轮缩放", True) # ─── M9: 数据中心 ─── print("\n=== M9: 数据中心 ===") for n,p in [("engines","/engines"),("comms","/communications"),("tanks","/tanks"),("vehicles","/vehicles")]: await pg.goto(f"{BASE}{p}", wait_until="networkidle", timeout=10000) await pg.wait_for_timeout(500) r = await pg.query_selector_all("table tbody tr") T(f"9.{n}", len(r)>=1, f"{len(r)} rows") # ─── M10: 燃料换算 ─── print("\n=== M10: 燃料换算 ===") await pg.goto(f"{BASE}/fuel-converter", wait_until="networkidle", timeout=10000) await pg.wait_for_timeout(500) T("10.1 渲染", await pg.query_selector("#conv-form") is not None) T("JS Errors", len(errs)==0, f"{len(errs)} errors") if errs: print(f" Errors: {errs[:3]}") await b.close() # Report print("\n"+"="*60) print("TEST REPORT") print("="*60) passed=sum(1 for r in results if r["ok"]) failed=sum(1 for r in results if not r["ok"]) total=len(results) print(f"Total: {total} | Passed: {passed} | Failed: {failed} | Rate: {passed/total*100:.1f}%") if failed: print("\nFailed:") for r in results: if not r["ok"]: print(f" [FAIL] {r['name']}" + (f" -- {r['detail']}" if r['detail'] else "")) print("="*60) with open(SS/"test_report.json","w") as f: json.dump({"total":total,"passed":passed,"failed":failed,"results":results},f,ensure_ascii=False,indent=2) print(f"Report: {SS}/test_report.json") return failed==0 if __name__=="__main__": asyncio.run(run())