AI 应用 UI 设计与测试
Overview
本技能覆盖 AI 应用(智能体/数据分析问答/AI 对话)的前端 UI 设计规范与真实浏览器测试两个维度:
- 设计侧:智能体流式输出的前端呈现设计规范——思考面板三层呈现、内容净化、步骤化提炼 + 折叠展开、布局区分、智能滚动三态、打字机、多轮追问追加不覆盖(详见
references/streaming-ui.md)。 - 测试侧:用 puppeteer-core 启动本机 Chrome 无头浏览器,真实打开页面复现 UI 问题。不依赖用户描述猜测根因,而是捕获真实请求/响应/控制台/DOM 数据后定位问题,修复后同一脚本复测验证(详见 bug 根因速查
references/bug-patterns.md)。
目录
关键前提
- 需要 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 即非"被清掉",而是单键互斥覆盖 |
| 地图/图表配色不生效(一片蓝/默认色) | ECharts map+visualMap 配色陷阱(§14):数据项设了 visualMap:false 会令其 itemStyle.areaColor 失效、fallback 默认蓝 #5470c6 → 移除 visualMap:false,让 visualMap continuous 正常映射 + 动态 min/max;必须像素级采样(getImageData)验证实际渲染,不能只看 getOption 配置 |
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. 滚动三态(通用必测项):流式增量渲染区(思考区/日志区/聊天流/流式正文)只要做了自动跟随,实现必做三态——自动跟随 / 手动滚动后不拉回 / 滚回底部恢复跟随;只做自动跟随视为未完成,三态都要测(详见 streaming-ui.md §2)
通用铁律:断言目标必须是用户实际感知的体验(看、点、读、确认结果),而非"代码做了什么"。
修复后必须逐项检查:
| 类型 | 弱断言陷阱 | 强断言方式 |
|---|---|---|
| 布局/尺寸 | className.includes("...") | offsetWidth/Height ≈ window.innerWidth/Height |
| 结构渲染 | innerText.includes("...") | querySelectorAll("table").length >= N |
| 截断边界 | 用完整输入测 | 用真实截断数据测(slice/cutoff) |
| 过程态 | 只测最终态 | 流式/异步多次采样断言渐进增长 |
| 可见性(内容) | 元素存在 | scrollHeight <= clientHeight 无裁剪 |
| 可见性(显示/隐藏) | 只看 el.hidden 属性(假阳性,本项目漏检教训) | offsetParent === null 或 getComputedStyle().display === 'none'(.btn{display:inline-flex} 会覆盖 hidden,属性 true 但视觉仍显示;代码层用全局 [hidden]{display:none!important} 兜底) |
| Markdown 标题 | 渲染器支持 ### 就认为 OK | 解析必须逐行扫描(标题后紧跟内容无空行时按块判断会吞掉标题行 → 原样输出),单测覆盖"标题+无空行内容"输入 |
| 弹性布局 | 单点内容量 | 长/短内容各测,断言折叠/展开 |
| 前后端契约 | 固定 mock 数据 | 真实后端输出全链路,断言新映射生效 |
| 嵌套滚动 | 操作外层容器 | 监听/赋值/测试指向同一可滚动元素,先验证 scrollHeight > clientHeight |
| 滚动三态(通用必测) | 只做自动跟随 / 只测"保持 0" | 实现三态(跟随/不拉回/恢复)+ 三态全测,断言真实可滚动元素 |
| 图表初始化时机 | 只查 DOM 存在 | 渲染元素实际宽度 = 容器宽度(防隐藏容器内 init 退化为默认尺寸,如 ECharts 100px) |
| 字体/尺寸一致性(同页图表) | 只查某卡正常 | 断言 legend/axis/label fontSize 与同页其他图一致(如统一 11)、关键卡片 offsetHeight 一致(如统一 300,不因 sm 类残留 240) |
| 图表配色/视觉映射 | 只看 getOption() 配置(配置暖色 ≠ 渲染暖色,§14 教训) | canvas.getContext('2d').getImageData(x,y) 采样像素,断言是目标色而非默认蓝 #5470c6;hover 高亮用 dispatchAction({type:'highlight'}) 后采样验证 |
| 可点击 | 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
场景 D: 多轮追问/对话(追加不覆盖,防"问 A 答整份报告")
首轮完成后,在追问框发新问题,断言(详见 streaming-ui.md §8):
- 原报告卡片数不变(未被清空重建)
- 追问回复以新卡片追加,且非完整模板(卡片数/小节数/长度显著小于首轮)
- 用户气泡包含问题文本;本轮状态条结束态正常
// 追问前记录 → 追问后断言(示例,以你的实现为准)
const before = await page.$$eval('#cardsWrap .card', els => els.length); // 首轮卡片数
await page.type('#followInput', '你的追问');
await page.click('#followBtn');
// 等待本轮状态完成 → 断言 before 不变、新增卡片 < 首轮、气泡文本匹配
场景 E: 思考面板内容净化(用户 facing 界面,防工具独白泄漏)
触发流式后,mid_stream 阶段断言思考区(用户视图)DOM(详见 streaming-ui.md §1.1.1):
- 不泄漏工具调用独白:无工具名/参数原文(如
analyze_image(、image_urls_list=、S3 URL,以你对接的平台/工具为准) - 只显示预设干净步骤文案(如"正在分析…""正在检索…");未知片段显示通用兜底文案而非原文
- 若实现了开发者视图开关:切到原始视图应能看到 thinking 原文累积(对照确认净化仅在用户视图生效)
// 示例断言:思考区文本不含技术细节原文(以你的实现为准)
const thinkText = await page.$eval('#think-body', el => el.textContent);
if (thinkText.includes('analyze_image(') || thinkText.includes('image_urls_list=')) {
throw new Error('思考面板泄漏工具调用独白');
}
场景 F: 流式滚动三态(通用必测项,适用所有流式增量渲染区)
任何"内容持续增长、自动跟随底部"的滚动区(思考区/日志区/聊天消息流/流式正文)都必须实现并验证三态,缺一不可(实现骨架与断言详见 streaming-ui.md §2):
- 自动跟随:内容溢出后 scrollTop 接近底部(
scrollHeight > clientHeight且scrollTop >= scrollHeight - clientHeight - 阈值) - 手动不拉回:滚到顶部 + dispatch scroll → 后续内容增长后 scrollTop 仍≈0(不被拽回)
- 回底恢复:滚回底部 + dispatch scroll → 后续内容到达恢复跟随到底
// 三态断言骨架(以你的实现为准;先验证 scrollHeight > clientHeight,防"保持 0"假阳性)
await page.waitForFunction(() => {
const c = document.querySelector('#stream-area'); // 换成你的流式区容器
return c.scrollHeight > c.clientHeight + 10; // 元素确实可滚、内容确实溢出
});
// 态1 自动跟随 → 态2 手动不拉回 → 态3 回底恢复:逐态断言(断言式见 streaming-ui.md §2 表)
场景 G: 数据分析问答/AI 对话 重试兜底与图表渲染(覆盖"模型退化 final_answer"假阳性)
数据分析问答类流式功能,除场景 B 的"最终出图表"断言外,还须验证重试兜底不会无限循环且退化场景被兜底:
- 重试有上限(防无限循环铁律):连续制造"流结束无 final_answer 也无 error"的退化响应(mock 或弱模型触发),断言
askCalls <= 2(首次 + 至多 1 次重试);第 2 次仍失败即停止显示"未获得有效回答",不再发起第 3 次请求——调用方传入retryLeft逐次递减,retryLeft=0停,绝不递归 / while 无限重试 - 图表真实渲染:断言生成
<canvas>(ECharts/Chart.js)或<svg>(D3)存在 + 实际宽度 = 容器宽度(防隐藏容器 init 退化);散点/柱状/堆叠等图类型按数据正确出图 - legend 居中/明亮 + 字体一致:断言图例
left:"center"(居中)且色值非灰暗;图表 legend/axis/label 的fontSize与同页其他图表一致(如统一 11,避免某卡因sm类被压成 240px 高度、fontSize 10 显小) - 卡片尺寸一致性:断言关键卡片
offsetHeight与同页其他卡片一致(如统一 300px,不因历史sm类残留 240px)
// 伪代码:退化重试上限断言(以你的实现为准)
const calls = await page.evaluate(() => window.__askCalls);
if (calls > 2) throw new Error('重试超过上限,疑似无限循环');
自定义测试脚本
模板不满足时(如需要多步交互、断言特定 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 解耦)/ 图表容器可见后初始化 / 思考面板净化双视图(用户净化 + 开发者原始开关,§1.1) / 思考过程步骤化 + 折叠展开(提炼呈现:步骤卡片时间线 + 每步详情默认折叠、点击展开,§1.1.2) / error 事件不中断(§5.2:error 可能是过程性失败、智能体重试后继续,前端只记录不中断、以是否到达 final_answer 为成功标准,禁止一收 error 就中断——真实踩坑) / 多轮追问追加不覆盖(§8) / 真实浏览器渲染验证铁律 / 联调期前端资源缓存失效(?v= + no-cache,§5.1) / 验证清单;本文件只覆盖"流到了前端怎么渲染、怎么验证",接口/协议层(事件类型、会话 id、请求体构造)见你所对接平台的集成技能或接口文档;已按去项目化规则泛化,供对接任意 SSE 流式接口复用)references/bug-patterns.md— UI 常见问题根因速查 + 反思沉淀(§1-7 速查:[object Object]/TBLSEP/###(§3:标题后紧跟无空行内容时必须逐行扫描,块级判断会吞标题行)/流式闪现/部署未生效/排查命令/方法论核心;§8-12 反思:三类假阳性、四类隐蔽假阳性(§9.2.1 新增:hidden 属性≠视觉隐藏,须用 offsetParent/getComputedStyle 断言,禁止只看 el.hidden——真实漏检教训)、嵌套滚动容器错位、举一反三同类排查、持久化类 bug 单键互斥覆盖;§13 图表隐藏容器内 init 退化为默认尺寸(与 streaming-ui.md §4 同源);§14 ECharts map+visualMap 配色陷阱:数据项visualMap:false令 itemStyle.areaColor 失效、fallback 默认蓝#5470c6(配置≠渲染,必须像素级采样验证,不能只看 getOption);§15 ECharts map 交互禁用三层:silent 只禁事件、emphasis 高亮需单独 disabled、系列级会误伤须数据项级;§16 百分比尺寸图表:多图对齐依赖容器等宽(复用不对称 grid 类致图大小不一);§17 CSS transition 阻塞卡中间态:元素"空白/消失"假象)references/90-qc-ground-truth.md— 【质控专用,非使用教程】:领域事实清单/复查清单(15 条核心结论 + 已发现问题记录),本技能逻辑复查的领域锚点;质控方法论详见独立技能skill-qc(L0~L4),质控时加载 skill-qc 执行;正常使用本技能无需阅读,仅在排查/修订/复查时对照使用
Scan to join WeChat group