#!/usr/bin/env python3 """CDP E2E 综合测试 v4 — 通过 UI 登录并测试全流程""" import json, time, urllib.request, base64 from websocket import create_connection FRONTEND_URL = "http://localhost:5199" CDP_URL = "http://127.0.0.1:9225" CREDENTIALS = {"username": "yeij0942", "password": "Jiang1143218570"} passed = 0 failed = 0 def check(name, condition, detail=""): global passed, failed if condition: passed += 1 print(f" ✅ {name}") else: failed += 1 print(f" ❌ {name} {detail}") print("=" * 60) print("CDP E2E 测试 v4 — UI 登录 + 全流程验证") print("=" * 60) # Step 1: Create fresh page print("\n--- Step 1: 创建新页面 ---") req = urllib.request.Request(f"{CDP_URL}/json/new?url=about:blank", method="PUT") resp = urllib.request.urlopen(req, timeout=10) page = json.loads(resp.read()) ws_url = page["webSocketDebuggerUrl"] print(f" Page: {page.get('id')}") ws = create_connection(ws_url, timeout=15) msg_id = [0] def cdp(method, params=None): msg_id[0] += 1 mid = msg_id[0] ws.send(json.dumps({"id": mid, "method": method, "params": params or {}})) return mid def recv_all(timeout=3): ws.settimeout(timeout) msgs = [] try: while True: msgs.append(ws.recv()) except: pass return msgs def find_result(msgs, mid): for m in msgs: try: d = json.loads(m) if d.get("id") == mid: return d.get("result", {}) except: pass return None # Enable domains cdp("Page.enable") cdp("Network.enable") cdp("Runtime.enable") cdp("Log.enable") time.sleep(0.5) recv_all(0.5) # Step 2: Navigate to frontend print("\n--- Step 2: 导航到前端 ---") mid_nav = cdp("Page.navigate", {"url": FRONTEND_URL + "/"}) time.sleep(4) msgs = recv_all(3) # Check for JS errors has_js_error = False for m in msgs: d = json.loads(m) if d.get("method") == "Runtime.exceptionThrown": has_js_error = True exc = d.get("params", {}).get("exceptionDetails", {}) print(f" [JS ERROR] {exc.get('text', '')}") check("页面无 JS 异常", not has_js_error) # Step 3: Check localStorage (should NOT have test-token-cyrene) print("\n--- Step 3: localStorage 检查 ---") mid_ls = cdp("Runtime.evaluate", { "expression": "JSON.stringify({token: localStorage.getItem('token'), user_id: localStorage.getItem('user_id'), allKeys: Object.keys(localStorage)})" }) time.sleep(0.5) result = find_result(recv_all(1), mid_ls) ls_val = "" if result: ls_val = result.get("result", {}).get("value", "") print(f" localStorage: {ls_val}") check("无硬编码 test-token-cyrene", "test-token-cyrene" not in ls_val) # Step 4: Fill login form and click login print("\n--- Step 4: 通过 UI 登录 ---") # Find username input and fill mid_user = cdp("Runtime.evaluate", { "expression": f""" (() => {{ const inputs = document.querySelectorAll('input'); let usernameInput = null; let passwordInput = null; let loginBtn = null; for (const inp of inputs) {{ const placeholder = (inp.placeholder || '').toLowerCase(); const type = (inp.type || '').toLowerCase(); if (placeholder.includes('用户') || placeholder.includes('账号') || placeholder.includes('username')) {{ usernameInput = inp; }} else if (type === 'password' || placeholder.includes('密码') || placeholder.includes('password')) {{ passwordInput = inp; }} }} if (usernameInput) {{ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeInputValueSetter.call(usernameInput, '{CREDENTIALS["username"]}'); usernameInput.dispatchEvent(new Event('input', {{ bubbles: true }})); }} if (passwordInput) {{ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeInputValueSetter.call(passwordInput, '{CREDENTIALS["password"]}'); passwordInput.dispatchEvent(new Event('input', {{ bubbles: true }})); }} // Find login SUBMIT button (text is "进入昔涟的世界 ♪", NOT the mode switcher "登录") const buttons = document.querySelectorAll('button'); for (const btn of buttons) {{ const text = btn.textContent.trim(); if (text.includes('进入') || text.includes('昔涟')) {{ loginBtn = btn; break; }} }} return JSON.stringify({{ foundUser: !!usernameInput, foundPass: !!passwordInput, foundBtn: !!loginBtn, btnText: loginBtn ? loginBtn.textContent.trim() : 'N/A', inputCount: inputs.length, bodySnippet: document.body.innerText.substring(0, 200) }}); }})() """ }) time.sleep(0.5) result = find_result(recv_all(1), mid_user) form_info = {} if result: try: form_info = json.loads(result.get("result", {}).get("value", "{}")) print(f" 表单状态: {json.dumps(form_info, indent=2, ensure_ascii=False)}") except: print(f" Form result: {result.get('result', {}).get('value', '')[:200]}") check("找到用户名输入框", form_info.get("foundUser", False)) check("找到密码输入框", form_info.get("foundPass", False)) check("找到登录按钮", form_info.get("foundBtn", False)) # Click login if form_info.get("foundBtn"): mid_click = cdp("Runtime.evaluate", { "expression": """ (() => { const buttons = document.querySelectorAll('button'); for (const btn of buttons) { const text = btn.textContent.trim(); if (text.includes('进入') || text.includes('昔涟')) { btn.click(); return 'clicked'; } } return 'not_found'; })() """ }) time.sleep(4) msgs = recv_all(4) # Check API calls after login api_calls = [] console_errors = [] for m in msgs: d = json.loads(m) method = d.get("method", "") params = d.get("params", {}) if method == "Network.requestWillBeSent": req = params.get("request", {}) url = req.get("url", "") headers = req.get("headers", {}) auth = headers.get("Authorization", "") or headers.get("authorization", "") if "/api/" in url: api_calls.append(f"{req.get('method','?')} {url[:100]}") elif method == "Runtime.consoleAPICalled": args = params.get("args", []) if params.get("type") == "error": texts = [str(a.get("value", "") or a.get("description", ""))[:150] for a in args] console_errors.append(" ".join(texts)) elif method == "Runtime.exceptionThrown": exc = params.get("exceptionDetails", {}) console_errors.append(f"JS: {exc.get('text', '')}") print(f" API 调用: {len(api_calls)}") for c in api_calls[:8]: print(f" {c}") auth_errors = [e for e in console_errors if "401" in e or "认证" in e or "无效" in e] check("无认证错误", len(auth_errors) == 0, str(auth_errors[:2]) if auth_errors else "") # Step 5: Check post-login UI print("\n--- Step 5: 登录后 UI 检查 ---") mid_ui = cdp("Runtime.evaluate", { "expression": """ (() => { const hasSidebar = !!(document.querySelector('aside, nav[class*="side"], [class*="sidebar"], [class*="Sidebar"]')); const hasChatArea = !!(document.querySelector('[class*="chat"], [class*="Chat"], [class*="message"]')); const token = localStorage.getItem('token'); const userId = localStorage.getItem('user_id'); const bodyText = document.body.innerText.substring(0, 300); return JSON.stringify({hasSidebar, hasChatArea, hasToken: !!token, userId, bodyText}); })() """ }) time.sleep(0.5) result = find_result(recv_all(1), mid_ui) ui_state = {} if result: try: ui_state = json.loads(result.get("result", {}).get("value", "{}")) print(f" UI: {json.dumps(ui_state, indent=2, ensure_ascii=False)[:500]}") except: pass check("localStorage 有 token", ui_state.get("hasToken", False)) check("侧边栏或聊天区域存在", ui_state.get("hasSidebar", False) or ui_state.get("hasChatArea", False)) # Step 6: Screenshot mid_ss = cdp("Page.captureScreenshot", {"format": "png"}) time.sleep(1) result = find_result(recv_all(2), mid_ss) if result: img_data = result.get("data", "") if img_data: with open("/tmp/cdp_screenshot_final.png", "wb") as f: f.write(base64.b64decode(img_data)) print(" 截图: /tmp/cdp_screenshot_final.png") ws.close() # ============================================================ # Part B: WebSocket + Backend Tests # ============================================================ print("\n" + "=" * 60) print("Part B: Backend API 深度测试") print("=" * 60) # Login to get token login_req = urllib.request.Request( "http://localhost:8080/api/v1/auth/login", data=json.dumps(CREDENTIALS).encode(), headers={"Content-Type": "application/json"}, method="POST" ) resp = urllib.request.urlopen(login_req, timeout=10) login_data = json.loads(resp.read()) token = login_data.get("token", "") user_id = login_data.get("user_id", "") print(f" Token: {token[:30]}..., User: {user_id}") AUTH = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} BASE = "http://localhost:8080/api/v1" # B1: Sessions print("\n--- B1: 会话管理 ---") req = urllib.request.Request(f"{BASE}/sessions?user_id={user_id}", headers=AUTH) resp = urllib.request.urlopen(req, timeout=10) sessions = json.loads(resp.read()) sessions_list = sessions.get("sessions", sessions.get("data", [])) check("会话列表", isinstance(sessions_list, list), f"count={len(sessions_list) if isinstance(sessions_list, list) else 'N/A'}") # Create session req = urllib.request.Request( f"{BASE}/sessions", data=json.dumps({"user_id": user_id, "title": "E2E Test Session"}).encode(), headers=AUTH, method="POST" ) resp = urllib.request.urlopen(req, timeout=10) session_data = json.loads(resp.read()) session_id = session_data.get("session_id", session_data.get("id", "")) check("创建新会话", bool(session_id)) # B2: Chat History if session_id: print("\n--- B2: 聊天历史 ---") req = urllib.request.Request( f"{BASE}/sessions/{session_id}/messages?limit=10", headers=AUTH ) resp = urllib.request.urlopen(req, timeout=10) msgs = json.loads(resp.read()) check("获取消息历史", resp.status == 200) # B3: Memory print("\n--- B3: 记忆系统 ---") req = urllib.request.Request(f"{BASE}/memory", headers=AUTH) try: resp = urllib.request.urlopen(req, timeout=10) mem_list = json.loads(resp.read()) check("记忆列表", resp.status in [200, 404]) except Exception as e: check("记忆列表", False, str(e)[:80]) # B4: IoT via WebSocket print("\n--- B4: IoT 设备(通过 WebSocket)---") import websocket as ws_lib ws_url = f"ws://localhost:8080/ws/chat?token={token}&session_id={session_id}" try: sock = ws_lib.create_connection(ws_url, timeout=10) print(f" WebSocket 已连接") check("WebSocket 连接", True) # Send IoT query iot_query = json.dumps({ "type": "message", "content": "列出所有IoT设备", "session_id": session_id, "timestamp": int(time.time() * 1000) }) sock.send(iot_query) print(f" 已发送 IoT 查询") # Read responses sock.settimeout(8) responses = [] start = time.time() while time.time() - start < 8: try: msg = sock.recv() data = json.loads(msg) msg_type = data.get("type", "?") content = str(data.get("content", ""))[:80] responses.append(f"[{msg_type}] {content}") if msg_type in ("response", "stream_end"): break except: break print(f" 收到 {len(responses)} 条消息:") for r in responses[:10]: print(f" {r}") has_response = any("response" in r or "device" in r.lower() or "iot" in r.lower() for r in responses) check("收到 IoT 响应", len(responses) > 0) sock.close() except Exception as e: check("WebSocket/IoT 测试", False, str(e)[:80]) # Summary print("\n" + "=" * 60) print(f"测试完成: {passed} 通过, {failed} 失败, 总计 {passed+failed}") print("=" * 60)