UI 浏览器测试
Overview
用 puppeteer-core 启动本机 Chrome 无头浏览器,真实打开页面复现 UI 问题。不依赖用户描述猜测根因,而是捕获真实请求/响应/控制台/DOM 数据后定位问题,修复后同一脚本复测验证。
目录
关键前提
- 需要 puppeteer-core(在 node_modules 中)和本机 Chrome:
- Chrome 路径:优先用环境变量
CHROME_PATH指定;未设置时按平台默认查找 (WindowsC:/Program Files/Google/Chrome/Application/chrome.exe或Program Files (x86)下同名路径; macOS/Applications/Google Chrome.app/Contents/MacOS/Google Chrome;Linux/usr/bin/google-chrome) - 运行需设置:
NODE_PATH=<node_modules路径>
- Chrome 路径:优先用环境变量
- 本地开发服务需已启动(如 uvicorn)。
- 页面有登录时需提供测试账号。
工作流程
Step 1: 用模板脚本快速复现
scripts/ui_test.js 是通用模板,覆盖核心捕获能力(console/pageerror、API 请求体与响应、DOM 快照、截图):
# 基础: 打开页面, 等待 5s, 截图(<url> 换成你的本地服务地址)
NODE_PATH=.../node_modules node ui_test.js <url> --wait-ms 5000
# 带登录(凭据用测试账号,可通过参数传入)
node ui_test.js <url> --login-user <user@example.com> --login-pass <password>
# 登录后点击某个按钮, 捕获指定 API 的请求/响应, 等待 8s
# (--click-selector / --watch-api 按目标系统实际元素与接口替换)
node ui_test.js <url> \
--login-user <user@example.com> --login-pass <password> \
--click-selector "<打开弹窗的按钮选择器>" \
--watch-api "<要监听的接口路径片段>" \
--wait-ms 8000
模板会输出:[REQ-BODY](重点看 postData 是否为空!)、[RESP] 状态码、DOM 快照(tables/modals/是否含 [object Object])、截图路径、控制台错误。
Step 2: 分析捕获数据定位根因
对照 references/bug-patterns.md 常见问题速查表:
| 现象 | 排查方向 |
|---|---|
| [object Object] | 看 [RESP] 状态码;若 422 → 抓 [REQ-BODY] 是否空 → 对象展开丢键 / new Error(数组) |
| 排版错乱/TBLSEP | 用 Node 单独跑渲染函数 + 真实 LLM 输出断言 |
| 流式闪现 | 检查 SSE 事件粒度,thinking 只显示动画 |
| 部署未生效 | md5 对比 + HTTP 抓取 + 服务重启(Python 必须重启进程)|
| 数据偶尔丢失/消失 | 持久化类 bug(§12):grep localStorage 全部读写点 → 判定单例还是实体集合 → 序列推演(A存→B存→A读)→ 无 clear/removeItem 即非"被清掉",而是单键互斥覆盖 |
Step 3: 修复 + 同一脚本复测
- 修复代码后重跑同一测试脚本,确认:
[REQ-BODY]非空- DOM 快照断言通过(无
[object Object]、table 数量正确) - 无 console/pageerror
- 截图人工确认视觉效果(保存于
ui-shots/) - 修复后必做举一反三(§11):同类问题是否在代码其他位置/场景同样存在(同函数不同分支、同模式不同位置、同数据不同入口、同 UI 不同视图),一并修复或列待办
Step 4: 强断言检查清单(防假阳性)
测试通过 ≠ 功能正确。三类陷阱(详见 references/bug-patterns.md §8、§9、§10):
第一类(元素层):
- className ≠ 布局:断言
offsetWidth/Height真实尺寸,不只断言 className - innerText ≠ DOM 结构:断言
querySelectorAll("table").length >= 1,不只断言"innerText 包含" - 测试场景 ≠ 真实路径:用"实际传递给被测代码"的输入(如 slice(0,8) 截断后的文本),不只测完整数据
第二类(体验层,更隐蔽):
4. 只测最终态漏测过程态:流式/异步/动画功能必须多次采样断言渐进变化(15s × 6),不能只等 100s 测最终
5. 元素存在 ≠ 用户看得到:断言 scrollHeight <= clientHeight(无 overflow 裁剪),关键结论在视口内
6. 弹性布局只测单点:用长/短两种内容量各测一遍,断言折叠/展开行为
7. 前后端契约不同步:改后端提示词后必须用真实输出全链路验证,断言新小节映射生效
第三类(滚动/交互层):
8. 嵌套滚动容器错位:监听、赋值、测试必须都指向真正可滚动的元素(CSS 有 overflow-y:auto 且内容溢出);外层 overflow:hidden 的 scrollTop 恒为 0,赋值无效
9. 测试操作了错误的元素:若被测元素 scrollTop 恒 0,任何"保持 0"断言都是假阳性——断言前先验证 scrollHeight > clientHeight(元素确实可滚动、内容确实溢出)
10. 滚动三态:自动跟随 / 手动滚动后不拉回 / 滚回底部恢复跟随,三态都要测
通用铁律:断言目标必须是用户实际感知的体验(看、点、读、确认结果),而非"代码做了什么"。
修复后必须逐项检查:
| 类型 | 弱断言陷阱 | 强断言方式 |
|---|---|---|
| 布局/尺寸 | className.includes("...") | offsetWidth/Height ≈ window.innerWidth/Height |
| 结构渲染 | innerText.includes("...") | querySelectorAll("table").length >= N |
| 截断边界 | 用完整输入测 | 用真实截断数据测(slice/cutoff) |
| 过程态 | 只测最终态 | 流式/异步多次采样断言渐进增长 |
| 可见性 | 元素存在 | scrollHeight <= clientHeight 无裁剪 |
| 弹性布局 | 单点内容量 | 长/短内容各测,断言折叠/展开 |
| 前后端契约 | 固定 mock 数据 | 真实后端输出全链路,断言新映射生效 |
| 嵌套滚动 | 操作外层容器 | 监听/赋值/测试指向同一可滚动元素,先验证 scrollHeight > clientHeight |
| 滚动三态 | 只测"保持 0" | 自动跟随 / 手动不拉回 / 回底恢复 三态全测 |
| 图表初始化时机 | 只查 DOM 存在 | 渲染元素实际宽度 = 容器宽度(防隐藏容器内 init 退化为默认尺寸,如 ECharts 100px) |
| 可点击 | el 存在 | getBoundingClientRect() 在视口内 |
| 网络成功 | resp.ok | 同时检查 Content-Type 和可解析 body |
专项测试场景
场景 A: 弹窗/模态框
node ui_test.js <url> --login-user U --login-pass P \
--click-selector "#btn-open-modal" --wait-ms 3000
# 断言: modals >= 1, 弹窗内无 [object Object], 截图
场景 B: SSE 流式渲染(AI 对话等)
node ui_test.js <url> --login-user U --login-pass P \
--click-selector "<触发流式的按钮>" --watch-api "<流式接口路径片段>" --wait-ms 100000
# 断言: REQ-BODY 非空(关键!); 完成后表格数>0; 无 TBLSEP/###/[object Object]
# 注: 流式接口响应体无法用 resp.text() 读取(已消费), 依赖截图+DOM 验证
场景 C: 页面功能回归(列表/表单/跳转)
node ui_test.js <url> --click-selector "nav a" --click-selector "#search-input" --wait-ms 3000
自定义测试脚本
模板不满足时(如需要多步交互、断言特定 DOM),直接写一次性 puppeteer 脚本(参考 scripts/ui_test.js 结构):
page.on('pageerror')+page.on('console')收集错误page.on('request'/'response')按 URL 片段过滤捕获 APIpage.evaluate()执行交互与 DOM 断言page.screenshot()保存证据- 输出结构化结果(JSON 格式便于解析)
关键 API 速记:
page.type(sel, text)输入page.click(sel)/page.evaluate(s => document.querySelector(s).click())点击page.$eval(sel, el => el.outerHTML)抓 HTMLpage.evaluate(() => document.body.innerText)抓文本page.screenshot({path})截图
Resources
scripts/ui_test.js— 通用测试模板(登录/点击/API 捕获/DOM 断言/截图,命令行参数化)references/streaming-ui.md— SSE 流式输出前端呈现与验证(三层呈现架构 + 打字机 / 智能滚动三态 / 关闭弹窗不中断与重开恢复(DOM 解耦)/ 图表容器可见后初始化 / 真实浏览器渲染验证铁律 / 验证清单;本文件只覆盖"流到了前端怎么渲染、怎么验证",接口/协议层(事件类型、会话 id、请求体构造)见你所对接平台的集成技能或接口文档;已按去项目化规则泛化,供对接任意 SSE 流式接口复用)references/bug-patterns.md— UI 常见问题根因速查 + 反思沉淀(§1-7 速查:[object Object]/TBLSEP/###/流式闪现/部署未生效/排查命令/方法论核心;§8-12 反思:三类假阳性、四类隐蔽假阳性、嵌套滚动容器错位、举一反三同类排查、持久化类 bug 单键互斥覆盖;§13 图表隐藏容器内 init 退化为默认尺寸(与 streaming-ui.md §4 同源))references/90-qc-ground-truth.md— 【质控专用,非使用教程】:领域事实清单/复查清单(15 条核心结论 + 已发现问题记录),本技能逻辑复查的领域锚点;质控方法论详见独立技能skill-qc(L0~L4),质控时加载 skill-qc 执行;正常使用本技能无需阅读,仅在排查/修订/复查时对照使用
Scan to join WeChat group