主题
单元测试
本文引用的源码文件
本文引用的文件
- vitest.config.js
- package.json
- test/setup/chrome-mock.js
- test/setup/jsdom-globals.js
- test/unit/activity-store-write-coalescing.unit.test.js
- test/unit/chat-streaming-autoscroll.unit.test.js
- test/unit/sw-heartbeat.unit.test.js
- test/unit/manifest.unit.test.js
- test/unit/config-io-import.unit.test.js
- test/unit/theme.unit.test.js
- test/unit/subtask-timeout-budget.unit.test.js
- test/unit/subtask-progress-wiring.unit.test.js
- test/unit/subtask-partial-outcome.unit.test.js
- src/background/react-loop.js
- .github/workflows/release.yml
- .github/workflows/test-install.yml
更新摘要
变更内容
- 新增子任务超时预算测试模块,涵盖预算计算、时间格式化、进度回灌等核心功能
- 强化正则断言策略,从简单的存在性检查升级为精确的接线守卫
- 改进夹具语义,确保测试数据与生产代码字段完全对齐
- 增强边界条件测试,特别是子任务超时预算的并发控制和资源分配
目录
引言
本文件面向 AI Helper Chrome 扩展(Manifest V3,版本 2.3.1)的单元测试体系,聚焦 Vitest 测试框架的配置、Chrome API Mock 策略、异步与时间控制处理、测试用例组织方式、Mock 设计模式以及持续集成中的质量门禁。文档同时给出可复用的测试编写范式,覆盖工具函数、UI 行为、状态持久化、Service Worker 保活以及子任务超时预算管理等关键场景。
项目结构
测试代码采用"按功能模块划分 + 统一命名规范"的组织方式:
- 根级配置:Vitest 默认环境为 Node,通过 setupFiles 注入全局 Mock;JS-DOM 相关用例通过文件头注释切换为 jsdom 环境。
- 测试入口:npm scripts 提供 test:unit、test:e2e 等命令,统一由 package.json 管理。
- 测试目录:
- test/unit:Vitest 单元测试,匹配规则为 test/unit/**/*.test.js。
- test/e2e:Playwright 端到端测试(不在本文重点)。
- test/setup:共享测试基础设施,包括 Chrome API 最小桩和 jsdom 缺失 API 补丁。
graph TB A["package.json<br/>脚本入口"] --> B["vitest.config.js<br/>测试配置"] B --> C["test/setup/chrome-mock.js<br/>Chrome API 最小桩"] B --> D["test/setup/jsdom-globals.js<br/>jsdom 缺失 API 补丁"] B --> E["test/unit/**.test.js<br/>单元测试集合"] E --> F["src/background/*<br/>后台逻辑"] E --> G["src/side_panel/*<br/>侧边栏 UI"] E --> H["src/shared/*<br/>共享逻辑"]
图示来源
章节来源
核心组件
本节梳理测试体系的关键基础设施与常用模式。
Vitest 配置
- 全局 API:启用 globals,便于在测试中直接使用 describe、it、expect 等。
- 默认环境:node,适合纯后端逻辑或无 DOM 的工具函数。
- 初始化文件:chrome-mock.js 与 jsdom-globals.js 作为 setupFiles 注入。
- 包含规则:仅运行 test/unit/**/*.test.js。
Chrome API 最小桩
- 目标:避免被测模块意外访问 chrome.* 时抛出 ReferenceError。
- 策略:为 storage、runtime、tabs、scripting、bookmarks、history、cookies、downloads、notifications、offscreen 等提供 no-op 实现。
- 适用:纯函数测试、不直接依赖浏览器行为的逻辑。
jsdom 缺失 API 补丁
- 目标:让基于 DOM 的逻辑在 jsdom 下可运行。
- 内容:补齐 Element.scrollIntoView / scrollTo、NodeList.prototype.some/map/filter 等。
- 触发:仅在 window 与 Element 存在时生效,避免污染 node 环境。
测试数据与隔离
- 内存存储夹具:通过 makeFakeStorage 模拟 chrome.storage.local 的序列化语义,支持注入失败、记录 get/set 调用次数。
- 模块级重置:vi.resetModules 配合 freshStore 注入新的 chrome 实例,确保每个用例独立。
- 时间控制:vi.useFakeTimers/vi.advanceTimersByTimeAsync 精确控制防抖、心跳、rAF 等时序。
章节来源
- vitest.config.js#L5-L11
- test/setup/chrome-mock.js#L1-L32
- test/setup/jsdom-globals.js#L1-L24
- test/unit/activity-store-write-coalescing.unit.test.js#L23-L60
架构总览
下图展示单元测试如何围绕被测源码进行 Mock、断言与回归保护。
graph TB subgraph "测试层" T1["Vitest 配置<br/>vitest.config.js"] T2["Chrome 最小桩<br/>chrome-mock.js"] T3["jsdom 补丁<br/>jsdom-globals.js"] T4["用例集合<br/>test/unit/*.test.js"] end subgraph "被测源码" S1["background 逻辑<br/>src/background/*"] S2["side_panel 逻辑<br/>src/side_panel/*"] S3["shared 逻辑<br/>src/shared/*"] end T1 --> T4 T2 --> T4 T3 --> T4 T4 --> S1 T4 --> S2 T4 --> S3
图示来源
详细组件分析
活动存储写合并与竞态守卫测试
该用例验证 activity-store 的审计日志与工具统计写入路径:
- 形状不变量:条目字段与旧版一致,最新在前。
- 上限约束:最多保留 100 条。
- 冷启动合并:不清空存量,新条目插到最前。
- 防抖合并:窗口内多次变更只落盘一次,且多键同批写入。
- 关键审计即时落盘:敏感操作不等防抖。
- 失败重试:set 失败保留脏标,下次合并带出。
- 并发竞态对照:旧读-改-写会丢更新,新实现一条不丢。
- 接线守卫:tool-executor 改用 activity-store,热路径零逐次 IPC。
flowchart TD
Start(["开始:appendAuditLog/recordToolStats"]) --> CheckType{"是否关键审计?"}
CheckType --> |是| ImmediateFlush["立即落盘"]
CheckType --> |否| Debounce["进入防抖窗口"]
ImmediateFlush --> Persist["chrome.storage.local.set"]
Debounce --> Merge["合并待写数据"]
Merge --> FlushTimer["定时器到期后 flush"]
FlushTimer --> Persist
Persist --> ErrorCheck{"set 是否失败?"}
ErrorCheck --> |是| MarkDirty["标记脏数据并保留"]
ErrorCheck --> |否| Done(["结束"])
MarkDirty --> NextFlush["下一次 flush 携带脏数据"]
NextFlush --> Done
图示来源
- test/unit/activity-store-write-coalescing.unit.test.js#L23-L60
- test/unit/activity-store-write-coalescing.unit.test.js#L76-L170
- test/unit/activity-store-write-coalescing.unit.test.js#L172-L223
章节来源
流式滚动到底部补滚测试
该用例验证 chat-streaming 的智能滚动:
- 首帧 scrollHeight 偏小,下一帧才稳定增大。
- autoScrollToBottom 必须补滚一帧,否则最新内容被遮挡。
- scrollStreamingToBottom 对已连接元素立即补滚,对未连接元素不滚动。
- 使用手动 rAF 队列完全接管渲染时序,避免 jsdom 差异。
sequenceDiagram participant Test as "测试用例" participant DOM as "DOM 容器" participant Scroll as "scrollStreamingToBottom" participant RAF as "requestAnimationFrame" Test->>DOM : 构建容器并设置初始 scrollHeight Test->>Scroll : 调用滚动到底部 Scroll->>RAF : 入队补滚回调 Note over DOM,RAF : 下一帧内容高度稳定 RAF-->>Scroll : 执行补滚回调 Scroll->>DOM : 读取最终 scrollHeight 并滚动到底 Test->>Test : 断言 scrollTop 等于最终底部
图示来源
- test/unit/chat-streaming-autoscroll.unit.test.js#L50-L90
- test/unit/chat-streaming-autoscroll.unit.test.js#L92-L147
章节来源
Service Worker 心跳保活测试
该用例验证 MV3 Service Worker 任务级保活心跳:
- 心跳间隔小于 30 秒闲置阈值,并留出余量。
- start 立即跳一拍,之后每间隔跳一拍;stop 停止。
- 引用计数:嵌套 start/stop 成对出现,只有最外层 stop 真正停跳。
- 多余 stop 不出负数、不崩溃;API 抛异常静默容错。
- react-loop 导入共享心跳模块,并以包装器形式保证 finally 停止。
stateDiagram-v2 [*] --> 空闲 空闲 --> 跳动中 : "startSwHeartbeat()" 跳动中 --> 跳动中 : "startSwHeartbeat() 嵌套" 跳动中 --> 空闲 : "stopSwHeartbeat() 最外层" 跳动中 --> 空闲 : "定时器到期 → 调用轻量 API" 空闲 --> 空闲 : "多余 stop 不崩溃"
图示来源
- test/unit/sw-heartbeat.unit.test.js#L20-L40
- test/unit/sw-heartbeat.unit.test.js#L42-L99
- test/unit/sw-heartbeat.unit.test.js#L101-L131
章节来源
Manifest 权限与帧注入守卫测试
该用例校验 manifest.json 的阶段三帧支持与权限:
- permissions 包含 webNavigation。
- content_scripts 开启 all_frames、match_about_blank、match_origin_as_fallback,并匹配 <all_urls>。
章节来源
配置导入合并与安全性测试
该用例覆盖 config-io 的导入流程:
- merge 模式:新 ID 追加、同 ID 较新覆盖、较旧忽略,并重投影扁平键。
- replace 模式:整体替换厂商配置。
- 剥离下线字段:移除 toolReflection 等旧配置。
- 安全约定:networkAccessEnabled 与 allowedNetworkHosts 不参与导入,防止配置文件越权打开内网访问。
章节来源
主题系统测试
该用例覆盖 theme.js 的行为:
- 默认 light,写入 data-theme。
- setTheme(dark) 落盘 appTheme 并翻转属性。
- system 偏好按 prefers-color-scheme 解析并订阅变化。
- storage.onChanged 跨页同步翻转。
- 顶层立即应用 localStorage 镜像(防闪烁)。
- 非法偏好值回退 light,setTheme 拒绝非法值。
- apply 仅在生效值变化时通知订阅者(吞同值回声)。
- storage.get 报 lastError 时保留镜像主题,不回落 light。
章节来源
子任务超时预算与进度管理测试
新增 该测试模块验证子任务超时预算管理的核心功能:
- 预算计算逻辑:取消硬封顶,改为配置驱动,同时不透支父任务预算
- 并发控制:N 个并发子任务不得各享满额父剩余,需按比例分摊
- 边界条件:父剩余为零/为负时预算归零,pendingCount 极大时启用 60s 下限
- 时间格式化:整分钟不带小数,非整分钟保留一位小数,不足一分钟按秒显示
- 进度回灌:已完成节点摘要回灌给重跑的子任务,避免重复工作
- 接线守卫:精确的正则匹配确保关键函数调用点不被删除
flowchart TD
A["computeSubtaskLoopTimeoutMs"] --> B["计算配置上限 cap"]
B --> C{"是否有父预算?"}
C --> |否| D["返回配置值本身"]
C --> |是| E["计算预留 reserve = 5% 父剩余"]
E --> F["分摊 perChild = (hardCap - reserve) / pendingCount"]
F --> G["应用下限 MIN_BUDGET"]
G --> H["取 min(cap, budget)"]
H --> I["返回最终预算"]
图示来源
章节来源
子任务进度接线与字段契约测试
新增 该测试模块验证子任务进度管理的接线正确性和字段契约:
- 字段契约:生产代码将产出正文写在 observation 字段而非 result 字段
- 进度写入:createSubtaskPartialProgressWriter 负责滚动写入进度表
- 现场合并:跨尝试的进度有界合并,避免重复堆积
- 安全包裹:buildSubtaskResumeContext 对不可信内容进行 HTML 实体转义
- 接线守卫:精确计数确保关键函数调用点存在且参数正确
sequenceDiagram participant Writer as "进度写入器" participant Progress as "进度 Map" participant Context as "继承上下文" participant Prompt as "系统提示词" Note over Writer : 第一趟执行 Writer->>Progress : 写入工具执行结果 Note over Progress : 保存 partialSummary Note over Writer : 第二趟重试 Writer->>Progress : 追加新的执行结果 Progress->>Context : 生成继承上下文 Context->>Prompt : 注入「不要重做」段落
图示来源
- test/unit/subtask-progress-wiring.unit.test.js#L78-L164
- test/unit/subtask-progress-wiring.unit.test.js#L166-L239
章节来源
子任务部分完成语义测试
新增 该测试模块验证子任务部分完成的分类逻辑和失败上报:
- 分类逻辑:failed 且节点成功率 ≥80% 时显示「部分完成」而非「失败」
- 失败上报:失败 Result 附加已完成节点的产出摘要,避免父模型盲续
- 字段对齐:夹具严格对齐生产代码的 tool_exec 节点结构
- 样式守卫:CSS 定义 partial 橙色样式区别于红色 failed
章节来源
依赖关系分析
测试与源码之间的依赖关系如下:
- vitest.config.js 定义测试环境与初始化文件,决定哪些测试会被执行。
- chrome-mock.js 与 jsdom-globals.js 为所有测试提供运行时基础能力。
- 各 unit.test.js 通过 import 引入被测模块,并通过 vi.mock、vi.stubGlobal、readFileSync 等方式进行依赖隔离与静态守卫。
graph LR V["vitest.config.js"] --> U["test/unit/*.test.js"] M["chrome-mock.js"] --> U J["jsdom-globals.js"] --> U U --> B["src/background/*"] U --> S["src/side_panel/*"] U --> H["src/shared/*"]
图示来源
章节来源
性能与覆盖率
- 性能测试方法
- 使用 vi.useFakeTimers 与 vi.advanceTimersByTimeAsync 精确控制防抖、节流与心跳,避免真实时间不确定性。
- 手动接管 requestAnimationFrame,构造可控的渲染帧,验证布局延后稳定后的滚动行为。
- 并发竞态对照:用 Promise.all 并行触发旧实现与新实现,断言旧实现丢更新、新实现不丢。
- 覆盖率报告生成
- 当前仓库未显式配置 Vitest 覆盖率输出;如需生成覆盖率,可在 vitest.config.js 中添加 coverage 配置并在 npm scripts 中增加对应命令。
- 持续集成与质量门禁
- release.yml:负责构建扩展包、上传 GitHub Releases 与同步 Gitee,不包含单元测试步骤。
- test-install.yml:针对安装脚本的全生命周期验证,也不包含单元测试。
- 建议在 CI 中新增 job 执行 npm run test:unit,并将失败视为质量门禁阻断条件。
章节来源
- test/unit/activity-store-write-coalescing.unit.test.js#L172-L201
- test/unit/chat-streaming-autoscroll.unit.test.js#L54-L68
- .github/workflows/release.yml#L43-L130
- .github/workflows/test-install.yml#L1-L155
故障排查指南
- 常见问题
- 测试环境缺少 chrome.*:检查 setupFiles 是否正确加载 chrome-mock.js,或在用例中按需 vi.stubGlobal('chrome', ...)。
- jsdom 环境下 DOM API 缺失:确认 jsdom-globals.js 已加载,必要时在用例中补充 polyfill。
- 异步时序不稳定:优先使用 vi.useFakeTimers 与手动 rAF 队列,避免依赖真实浏览器时序。
- 模块单例污染:使用 vi.resetModules 与 freshStore 注入新的 chrome 实例,确保用例隔离。
- 心跳与保活:确保 start/stop 成对出现,引用计数正确;API 异常需静默容错。
- 子任务超时预算:检查 computeSubtaskLoopTimeoutMs 的参数传递,确保父剩余、配置值和 pendingCount 正确传入。
- 进度回灌失效:确认 fixture 字段与生产代码对齐(observation vs result),检查接线守卫是否通过。
- 调试建议
- 打印 fake storage 的 getCalls/setCalls,核对读写次数与合并批次。
- 对关键审计类操作,断言即时落盘;对普通操作,断言防抖窗口内不落盘。
- 对 UI 滚动行为,先断言首帧滚动结果,再断言下一帧补滚后的最终位置。
- 对子任务超时预算,验证预算计算公式:min(配置, max(MIN_BUDGET, (父剩余 - 预留) / 并发数), 父剩余)。
- 对进度回灌,检查 partialSummary 是否包含预期的工具执行结果,确认不应重做的语义正确。
章节来源
- test/unit/activity-store-write-coalescing.unit.test.js#L23-L60
- test/unit/sw-heartbeat.unit.test.js#L20-L40
- test/unit/chat-streaming-autoscroll.unit.test.js#L54-L90
- test/unit/subtask-timeout-budget.unit.test.js#L40-L126
- test/unit/subtask-progress-wiring.unit.test.js#L78-L164
结论
本项目单元测试体系以 Vitest 为核心,结合 Chrome API 最小桩与 jsdom 补丁,覆盖了后台逻辑、侧边栏 UI 与共享模块的关键行为。通过时间控制、并发竞态对照与静态接线守卫,测试不仅验证功能正确性,还承担回归保护职责。
最新更新 引入了子任务超时预算管理和进度回灌的完整测试套件,包括:
- 精确的正则断言策略,从简单的存在性检查升级为接线守卫
- 与生产代码字段完全对齐的夹具语义
- 全面的边界条件测试,特别是并发控制和资源分配
- 闭环测试确保进度回灌链路不断链
建议在后续迭代中补充覆盖率配置与 CI 质量门禁,进一步提升测试可见性与发布可靠性。