本菜鸟记录一下项目的机房判分测试工具如有不完善的地方欢迎指正基于 CDP 协议的 WebView2 自动化测试工具用于批量验证学生端考试流程。目录1. 项目概述2. 技术架构3. 配置文件4. 打包部署5. CDP 调试协议6. WebSocket 客户端7. CDP 命令封装8. 进程管理9. 主测试流程10. 异常处理与容错11. 常见问题排查1. 项目概述1.1 背景在智能测评系统的机房部署场景中需要在100 台客户机上验证学生端ExamStudent.exe的完整考试流程是否正常运行。人工逐台操作效率极低因此开发了本自动化测试工具。1.2 功能定位本工具通过CDPChrome DevTools Protocol协议控制基于 WebView2 渲染的学生端桌面应用模拟真实用户的完整考试操作流程启动 → 登录 → 选择任务 → 进入考试 → 交卷 → 输入验证码 → 确认提交1.3 核心特性特性说明零依赖纯 Python 标准库实现无需 pip install 任何第三方包免安装运行通过 PyInstaller 打包为单文件 exe双击即可运行自动发现自动查找 ExamStudent.exe支持常见路径搜索 全盘搜索 GUI 选择CDP 自动化手写 WebSocket 客户端通过 CDP 协议控制 WebView2 页面配置持久化首次运行自动生成 config.json后续直接读取测试报告每次运行生成 JSON 格式的测试报告1.4 文件结构判分测试/├── exam_test.py # 主脚本约 1235 行├── config.json # 运行配置文件├── build.bat # PyInstaller 打包脚本├── 判分测试工具.exe # 打包产物dist 目录下└── test_report_*.json # 测试报告自动生成2. 技术架构2.1 技术栈Python 3.9纯标准库├── socket / struct / base64 → 手写 WebSocket 客户端├── urllib.request → HTTP 请求CDP 端口探测├── json → 配置读写 / CDP 消息序列化├── subprocess → 进程管理启动/杀死 ExamStudent.exe├── os / sys / time → 系统交互 / 路径处理 / 流程控制├── tkinter → 文件选择对话框GUI 降级方案└── PyInstaller → 打包为单文件 exe2.2 架构图┌─────────────────────┐│ 判分测试工具.exe ││ ││ ┌───────────────┐ │ env: WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS│ │ setup_cdp_env │──┼──────────────────────────────────┐│ └───────────────┘ │ ││ ┌───────────────┐ │ ▼│ │ launch_with │──┼──► subprocess.Popen ──► ExamStudent.exe│ │ _cdp() │ │ │ │ └───────────────┘ │ ▼│ ┌───────────────┐ │ WebView2 渲染引擎│ │ wait_cdp_ready│──┼──► HTTP :9222/json/list│ │ └───────────────┘ │ ▼│ ┌───────────────┐ │ CDP 端口 9222 开放│ │ connect_cdp() │──┼──► WebSocket 连接│ └───────────────┘ │ ││ ┌───────────────┐ │ ▼│ │ test_student()│──┼──► DOM / Runtime / Input 命令│ └───────────────┘ ││ ┌───────────────┐ ││ │ main() │──┼──► config.json / test_report.json│ └───────────────┘ │└─────────────────────┘2.3 核心模块关系exam_test.py ├── find_exe() # 自动查找学生端可执行文件├── load_config() # 配置加载与持久化├── WebSocketClient # 纯标准库 WebSocket 客户端│ ├── _handshake() # HTTP Upgrade 握手│ ├── send_json() # 发送 JSON 消息带 WebSocket 帧封装│ └── recv_json() # 接收并解析 JSON 消息├── CDP # Chrome DevTools Protocol 封装│ ├── send() # 发送 CDP 命令并等待响应│ ├── enable_domains() # 启用 DOM / Runtime / Page 域│ ├── query_selector() # CSS 选择器查询│ ├── set_value() # 设置输入框值兼容 Vue/React 响应式│ ├── eval_js() # 执行 JavaScript 表达式│ ├── wait_selector() # 等待元素出现│ └── dump_dom() # 导出 DOM 树调试用├── setup_cdp_env() # 设置 CDP 环境变量os.environ setx├── launch_with_cdp() # 启动学生端并注入 CDP 环境变量├── wait_cdp_ready() # 轮询等待 CDP 端口就绪├── connect_cdp() # 建立 WebSocket 连接├── test_student() # 单个学生的完整测试流程19 步└── main() # 入口函数3. 配置文件3.1 config.json 结构{exe_path:C:/Program Files/智能测评系统/ExamStudent.exe,password:123456,wait_seconds:5,cdp_port:9222,students:[cs001,cs002,cs003]}3.2 配置字段说明字段类型默认值说明exe_pathstring自动查找学生端可执行文件的绝对路径passwordstring123456登录密码所有账号统一密码wait_secondsint5进入考试后的等待秒数模拟答题时间cdp_portint9222WebView2 CDP 调试端口studentsarray[cs002]待测试的学生账号列表3.3 自动查找策略find_exe()按以下优先级查找 ExamStudent.exe常见安装路径7 个预设路径覆盖 C/D 盘的 Program Files全盘搜索限制深度 4 层避免遍历过深GUI 文件选择tkinter 文件对话框作为最终降级方案查找结果会自动保存到config.json下次运行直接读取。3.4 PyInstaller 路径处理ifgetattr(sys,frozen,False):SCRIPT_DIRos.path.dirname(sys.executable)# 打包后exe 所在目录else:SCRIPT_DIRos.path.dirname(os.path.abspath(file))# 开发时脚本所在目录CONFIG_FILEos.path.join(SCRIPT_DIR,config.json)注意打包后__file__指向_MEI临时目录必须用sys.executable获取 exe 所在路径。4. 打包部署4.1 打包脚本 build.batecho off chcp 65001 nul cd /d %~dp0 set PYTHON_EXED:\py\venv\Scripts\python.exe :: 清理旧构建 if exist build rd /s /q build if exist dist rd /s /q dist if exist 判分测试工具.spec del /q 判分测试工具.spec :: 安装 PyInstaller 并打包 %PYTHON_EXE% -m pip install pyinstaller -q %PYTHON_EXE% -m PyInstaller --onefile --console --name 判分测试工具 --clean ^ --hidden-import tkinter ^ --hidden-import tkinter.filedialog ^ exam_test.py :: 复制配置文件 copy /Y config.json dist\config.json nul 214.2 打包要点要点说明必须cd /d %~dp0双击 bat 时工作目录可能是 system32不切换会导致 PyInstaller 报错必须清理旧构建build/dist/.spec文件不清理可能产生缓存问题--hidden-import tkinterPyInstaller 无法自动检测 tkinter 的动态导入需显式声明--onefile --console单文件 控制台模式方便查看运行日志Python 路径当前配置为D:\py\venv\Scripts\python.exe需根据实际环境修改4.3 部署方式打包完成后将dist/目录下的文件复制到目标机器dist/├── 判分测试工具.exe # 主程序└── config.json # 配置文件运行方式直接双击判分测试工具.exe即可不需要管理员权限。5. CDP 调试协议5.1 原理ExamStudent.exe 是基于 Tauri 框架打包的桌面应用内部使用WebView2Chromium 内核渲染 UI。WebView2 支持通过环境变量开启 Chrome DevTools ProtocolCDP调试端口。5.2 环境变量设置defsetup_cdp_env(cdp_port):dbg_arg--remote-debugging-portstr(cdp_port)os.environ[WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS]dbg_arg os.environ[WEBVIEW2_BROWSER_EXECUTABLE_ARGS]dbg_arg os.system(setx WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS dbg_arg nul 21)os.system(setx WEBVIEW2_BROWSER_EXECUTABLE_ARGS dbg_arg nul 21)方法作用范围持久性说明os.environ当前进程及子进程进程结束即失效普通运行时有效setx写入用户注册表永久生效确保管理员模式等场景也能读取5.3 启动流程setup_cdp_env(9222) ← 先写环境变量os.environ setx 注册表↓kill_existing() ← 杀掉旧的 ExamStudent.exe↓launch_with_cdp(exe_path) ← subprocess.Popen 启动env 继承环境变量↓ExamStudent.exe 启动↓WebView2 读取 WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS↓开启 --remote-debugging-port9222↓wait_cdp_ready(9222) ← 轮询 HTTP http://127.0.0.1:9222/json/list↓ connect_cdp(pages) ← 解析 webSocketDebuggerUrl建立 WebSocket 连接5.4 重要限制⚠️ 不要以管理员身份运行管理员模式下WebView2 的子进程msedgewebview2.exe可能以降低的完整性级别运行导致环境变量传递断裂CDP 端口无法开启。普通用户模式下所有进程在同一完整性级别中等环境变量正常传递。6. WebSocket 客户端6.1 设计说明由于项目要求零第三方依赖WebSocket 客户端完全使用 Python 标准库socket、struct、base64手写实现。6.2 核心类 WebSocketClientclassWebSocketClient:definit(self,host,port,path):self.socksocket.socket(socket.AF_INET,socket.SOCK_STREAM)self.sock.settimeout(30)self.sock.connect((host,port))self._handshake(host,port,path)# RFC 6455 握手self._msg_id06.3 握手流程遵循 RFC 6455 规范客户端 → 服务端:GET /devtools/page/xxx HTTP/1.1Host: 127.0.0.1:9222Upgrade: websocketConnection: UpgradeSec-WebSocket-Key: base64随机16字节Sec-WebSocket-Version: 13服务端 → 客户端: HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: SHA1(key magic)6.4 帧协议操作实现发送构造 WebSocket 帧0x81文本帧FIN 0x80接收解析帧头 → 读取长度7bit / 16bit / 64bit→ 处理掩码 → 按 opcode 分发Ping/Pong收到opcode0x9Ping自动回复 Pong0x8A关闭发送opcode0x8Close帧 关闭 socket6.5 消息格式CDP 消息遵循 JSON-RPC 风格// 请求 {id: 1, method: DOM.getDocument}// 响应 {id: 1, result: {root: {nodeId: 1, ...}}}// 事件推送 {method: Page.loadEventFired, params: {...}}7. CDP 命令封装7.1 CDP 类class CDP: def init(self, ws): self.ws ws def send(self, method, paramsNone, timeout15): 发送 CDP 命令等待匹配的 id 响应 msg_id self.ws.send_json(method, params) while True: msg self.ws.recv_json(timeout) if msg and msg.get(id) msg_id: return msg if msg and method in msg: # 跳过事件推送 continue7.2 核心方法一览方法CDP 命令功能enable_domains()DOM.enableRuntime.enablePage.enable启用必要的 CDP 域query_selector(sel)DOM.getDocumentDOM.querySelectorCSS 选择器查询返回 nodeIdset_value(sel, val)DOM.focusRuntime.evaluate设置输入框值兼容 Vue/Reacteval_js(expr)Runtime.evaluate执行 JS 表达式返回值click(sel)Runtime.evaluate点击元素wait_selector(sel, t)轮询query_selector等待元素出现超时 t 秒dump_dom(depth)Runtime.evaluate导出 DOM 树结构调试用7.3 Vue/React 输入框兼容WebView2 页面使用 Vue/Element UI直接修改input.value不会触发响应式更新。需要使用原生settervarnativeSetObject.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,value).set;nativeSet.call(el,新值);el.dispatchEvent(newEvent(input,{bubbles:true}));el.dispatchEvent(newEvent(change,{bubbles:true}));7.4 鼠标事件双重保障对于关键操作如双击任务行同时使用两种机制确保触发JS 模拟element.dispatchEvent(new MouseEvent(dblclick, ...))CDP InputInput.dispatchMouseEvent底层渲染级别的鼠标事件第一层JS 模拟 cdp.eval_js(element.dispatchEvent(new MouseEvent(dblclick, ...)))第二层CDP Input更底层绕过 JS 事件系统 cdp.send(Input.dispatchMouseEvent,{type:mousePressed,x:x,y:y,...})cdp.send(Input.dispatchMouseEvent,{type:mouseReleased,x:x,y:y,...})8. 进程管理8.1 清理旧进程defkill_existing():os.system(taskkill /F /IM ExamStudent.exe nul 21)time.sleep(2)使用nul 21抑制输出避免进程不存在时报错。等待 2 秒确保进程完全退出、端口释放。8.2 启动学生端deflaunch_with_cdp(exe_path,cdp_port):envos.environ.copy()procsubprocess.Popen([exe_path],envenv)returnproc关键点subprocess.Popen必须显式传递envenv否则子进程无法继承环境变量。8.3 等待 CDP 就绪def wait_cdp_ready(cdp_port, timeout30): while time.time() end: try: resp urllib.request.urlopen( http://127.0.0.1: str(cdp_port) /json/list, timeout3 ) data json.loads(resp.read().decode(utf-8)) if data: return data # 返回页面列表 except Exception: pass time.sleep(1) return None每秒轮询一次最多等待 30 秒。返回的页面列表包含webSocketDebuggerUrl用于建立 WebSocket 连接。8.4 连接 CDPdefconnect_cdp(pages):forpageinpages:ifpage.get(type)page:ws_urlpage.get(webSocketDebuggerUrl,)breakparsedurlparse(ws_url)wsWebSocketClient(parsed.hostname,parsed.port,parsed.path)returnCDP(ws)9. 主测试流程9.1 流程总览test_student()函数包含19 个步骤模拟完整的考试操作步骤 0: 设置 CDP 环境变量步骤 1: 清理旧进程步骤 2: 启动学生端带 CDP步骤 3: 等待 CDP就绪 步骤 4: 连接 WebSocket步骤 5: 等待登录页加载步骤 6: 输入账号密码步骤 7: 点击登录步骤 7.5: 处理未完成考试弹窗步骤 8: 双击练习图标 步骤9: 等待任务列表 步骤10: 双击编号为 1 的任务行步骤 11: 点击信息确认步骤 12: 点击开始考试步骤 13-14: 等待考试进行步骤 15: 点击交卷步骤 16: Enter 确认交卷步骤 17: 读取验证码步骤 18: 输入验证码步骤 19: 点击确定完成交卷9.2 登录流程步骤 5-7等待登录页 cdp.wait_selector(input,20)多选择器尝试账号输入框 input_selectors[input[typetext],input[placeholder*账号],input[placeholder*用户名],input[nameusername],input[nameaccount],input]多选择器尝试密码输入框 pwd_selectors[input[typepassword],input[placeholder*密码],input[namepassword]]使用原生 setter 设置值兼容 Vue 响应式 cdp.set_value(selector,username)cdp.set_value(selector,password)文本匹配点击登录按钮 cdp.eval_js( (function() { var btns document.querySelectorAll(button, .el-button, ...); for (var i 0; i btns.length; i) { var t (btns[i].innerText || btns[i].textContent || ).trim(); if (t 登录 || t 登 录) { btns[i].click(); return clicked; } } return not_found; })() )9.3 弹窗处理步骤 7.5登录后可能遇到未完成考试弹窗自动检测并处理检测弹窗 unfinishedcdp.eval_js( (function() { var all document.querySelectorAll(*); for (var i 0; i all.length; i) { var t (all[i].innerText || ).trim(); if (t.indexOf(未完成) 0 || t.indexOf(未完成的考试) 0) return detected; } return none; })() )点击结束考试按钮ifstr(unfinished)detected:cdp.eval_js(... 查找并点击 结束考试 按钮 ...)9.4 任务选择步骤 8-10双击练习图标 cdp.eval_js( var all document.querySelectorAll(*); for (var i 0; i all.length; i) { var t (el.innerText || ).trim(); if (t 练习) { el.dispatchEvent(new MouseEvent(dblclick, {bubbles: true})); return practice_dblclicked; } } )等待表格数据加载轮询 td 有内容的数量 td_countcdp.eval_js( var cells document.querySelectorAll(td); var count 0; for (var i 0; i cells.length; i) { if ((cells[i].textContent || ).trim().length 0) count; } return count; )双击编号为1的任务行JS模拟CDP Input 双重保障9.5 交卷流程步骤 15-19点击交卷 → Enter确认 → 读取验证码 → 输入验证码 → 点击确定验证码读取策略四轮降级轮次策略说明第 1 轮红色/橙色背景的 4 位数字匹配backgroundColor中的 red/orange/rgb(2xx…)第 2 轮白色文字的 4 位数字匹配color中的 white/#fff第 3 轮内联样式含 background/color匹配style属性第 4 轮任何叶子节点的 4 位数字兜底排除 input 和无子元素的节点验证码输入策略三级降级级别方法说明第 1 级Input.insertTextCDP 底层直接插入文本最可靠第 2 级Input.dispatchKeyEvent逐字符模拟键盘输入第 3 级JSnativeSet赋值直接操作 DOM 的 value 属性10. 异常处理与容错10.1 异常捕获结构deftest_student(config,username,index,total):try:# ... 19 步测试流程 ...return{username:username,status:success,msg:交卷成功,log:log}exceptExceptionase:return{username:username,status:fail,msg:str(e),log:log}finally:# 关闭 WebSocket 连接ifcdp:cdp.ws.close()# 清理环境变量os.environ.pop(WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS,None)os.environ.pop(WEBVIEW2_BROWSER_EXECUTABLE_ARGS,None)time.sleep(2)10.2 多选择器降级对于每个关键 UI 元素都提供多个 CSS 选择器尝试python账号输入框6个选择器 input_selectors[input[typetext],input[placeholder*账号],input[placeholder*用户名],input[nameusername],input[nameaccount],input# 最终兜底]任务列表11个选择器 task_selectors[table,.el-table,.task-list,[class*task],.el-card,.el-table__body,tr,.list-item,[class*paper],[class*exam],.ant-tablepy]10.3 重试机制场景重试策略双击练习图标最多重试 30 次每次间隔 3 秒等待表格数据最多轮询 20 次每次间隔 0.5 秒读取验证码最多重试 10 次每次间隔 0.5 秒等待 CDP 就绪最多等待 30 秒每秒轮询等待元素出现wait_selector超时 20-30 秒10.4 日志记录每个步骤的操作和结果都记录到log数组最终输出到控制台和测试报告log[]log.append(清理旧进程...)log.append(启动 ExamStudent.exe (CDP 端口: 9222)...)log.append(CDP 已就绪 (2 个页面))log.append(已连接 WebSocket)log.append(输入账号: cs002)log.append( 账号输入框: input[typetext])...11. 常见问题排查11.1 CDP 端口未就绪现象等待 CDP 端口就绪一直超时报WinError 10061排查步骤powershell1.确认进程是否启动 tasklist|findstrExamStudent2.确认端口状态 netstat-ano|findstr92223.确认环境变量setWEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS常见原因原因解决方案以管理员身份运行不要用管理员运行直接双击即可端口被其他进程占用netstat -ano | findstr 9222查看占用进程WebView2 运行时未安装安装 WebView2 Runtime环境变量未生效检查setx是否执行成功重启终端后再试11.2 打包后运行报错错误原因解决配置文件不存在: C:\...\_MEIxxx\config.json使用了__file__而非sys.executable确保sys.frozen判断逻辑正确pip is not recognizedPython 不在 PATH 中build.bat 中使用完整路径Do not run pyinstaller from system32bat 未切换目录确保cd /d %~dp0打包后功能没更新运行的是旧 exe重新运行 build.bat清理 build/dist11.3 UI 操作失败现象原因解决未找到账号输入框页面未加载完成增加wait_selector超时时间登录按钮未找到按钮文本不匹配检查是否有空格如登 录任务行双击无效Tauri 不响应 JS 事件已使用 CDP Input 双重保障验证码读取失败页面渲染延迟已实现 10 次重试 4 轮降级策略验证码输入失败WebView2 输入框特殊处理已实现 3 级降级insertText → 键盘 → JS赋值11.4 测试报告每次运行自动生成test_report_YYYYMMDD_HHMMSS.json{time:2026-07-15 14:30:00,total:1,success:1,fail:0,results:[{username:cs002,status:success,msg:交卷成功,log:[清理旧进程...,启动 ExamStudent.exe (CDP 端口: 9222)...,CDP 已就绪 (2 个页面),已连接 WebSocket,...]}]}