# DiffLens 测试策略与方案 > 规范化 DiffLens 的测试体系:分层、选型、覆盖重点、运行与质量门禁。 > 配套文档:《应用开发与版本迭代规范》 --- ## 1. 目标与原则 - **保障正确性**:核心 diff 算法与报告生成必须被单元测试覆盖,防止回归 - **可回归**:每次迭代(`0.y.z`)在前都能稳定跑通测试 - **简单优先**:不做为测而测,优先覆盖纯逻辑与高风险模块 - **门禁前置**:`npm test` 纳入提交/推送前的强制检查 --- ## 2. 测试分层 | 层级 | 覆盖对象 | 工具 | | --- | --- | --- | | 单元测试(UT) | 纯逻辑:diff 引擎、报告生成器、解码探测、文件夹扫描 | Vitest | | 组件测试 | React 组件:应用空态、工具栏交互、文件夹视图 | Vitest + Testing Library | | E2E(0.6.0 起) | 构建产物真实 Electron:主进程解码/写盘/文件夹扫描/偏好重启 | Playwright | > 单元 + 组件覆盖核心逻辑与交互;E2E 覆盖 jsdom 无法触达的主进程真实链路(mock `window.api` 测不到的部分),发布前必跑。 --- ## 3. 技术栈 | 用途 | 选型 | | --- | --- | | 框架 | Vitest(与 Vite 生态一致,快) | | 环境 | jsdom | | React 测试 | @testing-library/react | | 断言扩展 | @testing-library/jest-dom | | E2E | Playwright(`_electron` 模式,复用项目自带 Electron,无需下载浏览器) | --- ## 4. 目录结构 ``` vitest.config.ts # Vitest 配置(jsdom + 测试范围含 shared + 覆盖率 80% 硬门禁) playwright.config.ts # Playwright E2E 配置(对 out/ 产物启动真实 Electron,发布前必跑) e2e/ ├── helpers.ts # E2E 辅助(启动应用/对话框 stub/临时 fixture/粘贴入口/合成拖放) └── app.spec.ts # E2E 用例(启动/粘贴/编码链路/选项含字符粒度统计徽章/导航/导出写盘含 Patch 落盘与判等禁用/文件夹对比含单行头部与快照归组下拉/增量重扫/忽略规则含高级 glob/扫描进度/快照含内容级对照与大快照专用通道与截断快照拦截/关于弹框/拖拽导入/偏好重启) src/shared/ ├── ignoreRules.ts # 忽略规则匹配器纯逻辑(gitignore 简化子集含高级 glob 与段内字符通配——? 单字符/[abc] 字符类;主进程扫描剪枝与渲染端无效提示共用,段模式预编译 + DP 段序列匹配 + 否定仲裁) ├── ignoreRules.test.ts # 忽略规则单元测试(名字/目录/通配回归 + 多段/锚定/跨段/段内通配/否定仲裁/无效清单/上限/IPC 校验 + ? 与字符类专项) ├── textExtensions.ts # 文本扩展名清单(0.10.3 收敛:主进程对话框筛选与语义判等文本判定、渲染端拖拽入口校验共用同一源文件,双向同步负担清零) ├── pathNorm.ts # 路径归一化纯逻辑(0.10.3 收敛:主进程 scanCacheStore 目录对键、渲染端 snapshotDiff 目录对匹配与 App 同目录防呆共用) └── pathNorm.test.ts # 路径归一化单元测试(分隔符归一/盘符小写/尾斜杠/非路径透传) src/main/ ├── index.ts # 主进程入口(依赖 electron 运行时,覆盖率排除,真实链路由 E2E 覆盖;含 folder:read-batch 批量读取与 folder:scan 进度推送;扫描缓存读盘与偏好写盘均异步 fs 不阻塞事件循环) ├── decode.ts # 解码与编码探测纯逻辑(Uint8Array 入参,同步与 worker 路径共用) ├── decode.test.ts # 解码单元测试(BOM 三种/严格 UTF-8/GBK 回退/二进制启发式) ├── decodeWorker.ts # 解码 worker_threads 入口(覆盖率排除,逻辑由 decode 测试覆盖) ├── folderScan.ts # 文件夹对比纯逻辑(递归枚举/对齐/字节判定/采样近似/受控并发比对/语义判等/单点失败降级/忽略规则剪枝含高级 glob 与字符通配/比对进度回调) ├── folderScan.test.ts # 文件夹扫描单元测试(真实临时目录:状态判定/递归/symlink/截断/近似/语义判等/目录降级/unreadable/忽略规则含 glob·否定·?与字符类/进度回调) ├── scanCacheStore.ts # 扫描缓存跨会话持久化纯数据(目录对键归一化复用 shared/pathNorm/LRU/防御性解析) ├── scanCacheStore.test.ts # 缓存存储单元测试(解析回退/条目过滤/键归一化/LRU 淘汰/序列化往返) ├── scanIpc.ts # folder:scan IPC 入参校验与缓存源选择纯逻辑(无 electron 依赖;异步签名——loadStored 同步/异步实现均兼容,惰性读盘语义不变) ├── scanIpc.test.ts # IPC 校验单元测试(previous 形态校验/缓存源选择与惰性读盘含异步 stub 兼容/忽略规则校验) ├── windowState.ts # 应用状态持久化纯逻辑(bounds 校验/钳制,无 electron 依赖) └── windowState.test.ts # 应用状态单元测试(解析回退/工作区钳制/最小尺寸) src/renderer/src/ ├── test/ │ └── setup.ts # 全局 setup:jest-dom 匹配器 + 自动 cleanup ├── diff/ │ ├── diffEngine.test.ts # diff 引擎单元测试(行级/词级/字符级分块锚定与降级/换位不变量) │ ├── diffWorker.ts # diff 计算 Web Worker(内联打包,覆盖率排除,逻辑由 diffEngine 覆盖) │ ├── createDiffWorker.test.ts # worker 工厂(环境回退/实例化) │ ├── createDiffWorker.ts # worker 工厂:环境不支持时返回 null 由调用方回退 │ ├── report.test.ts # 单文件报告生成测试(含 0.11.0:Patch 构建器/canBuildPatch 判定/TXT·MD 分块省略/元信息版本号与对照说明) │ ├── folderReport.ts # 文件夹报告生成器(HTML/TXT/MD 三格式,快照对照口径附注;对照模式条目表替换为变迁对照表;fmtSize 大小格式化含 GB·TB 档,列表与报告共用;0.11.0:忽略规则说明行 + 版本号署名) │ ├── folderReport.test.ts # 文件夹报告测试(统计/结论/三格式/截断警示/unreadable/快照变迁对照表/fmtSize 各档与 GB·TB 修复锚/忽略规则与版本号元信息) │ ├── snapshotDiff.ts # 快照基线纯逻辑(v1/v2 结构/解析校验含 contents/目录对归一化匹配(normDir 复用 shared/pathNorm)/六类变迁对照/共享变迁标签映射/内容保存上限常量) │ ├── snapshotDiff.test.ts # 快照单元测试(解析防御/v1 v2 兼容/contents 形态校验/变迁判定/双侧空集/unreadable 语义/目录对归一化匹配) │ └── textUtils.test.ts # 文本扩展名与行数统计测试 ├── hooks/ │ ├── useDiff.test.ts # worker 化计算 hook(快路径/去抖/过期丢弃/回退/卸载清理/字符级大字符量重路径/选项变化保留旧结果防滚动塌陷) │ ├── useDiff.ts │ └── useDismiss.ts ├── components/ │ ├── ContextMenu.test.tsx # 右键菜单(含边缘防溢出;↑/↓ 键盘导航跳过禁用项首尾循环、Enter 触发高亮项、悬停同步高亮) │ ├── DiffView.test.tsx # 对比视图(渲染/滚动同步/右键/拖放/搜索命中行内高亮) │ ├── ErrorBoundary.tsx # 顶层渲染错误边界(错误面板 + 一键重载,防白屏死机) │ ├── ErrorBoundary.test.tsx # 渲染错误兜底测试(抛错子组件 fallback/正常子组件透传) │ ├── AboutModal.tsx # 「关于」弹框(应用介绍/版本/作者/邮箱/组织/开源地址/许可证;邮箱与地址点击复制) │ ├── AboutModal.test.tsx # 关于弹框测试(元信息渲染/版本占位/复制回调/三种关闭方式/卸载监听清理) │ ├── LogoIcon.tsx # 应用 Logo 图标(App 头部与关于弹框共用单一实现) │ ├── folderTree.ts # 文件夹条目树纯逻辑(建树/状态聚合/扁平化/过滤含扩展名/子树收集) │ ├── folderTree.test.ts # 条目树单元测试(聚合排序/折叠/过滤/搜索/扩展名过滤/relExt) │ ├── FolderView.test.tsx # 文件夹视图(树形渲染/折叠/统计徽章点击过滤/搜索/扩展名下拉/忽略规则下拉含无效规则提示与进度遮罩/快照对照模式含变迁过滤归零复位与内容对照 title/目录双击/重新扫描与重新选择/快照保存中 saving 禁用/双击/导出菜单/虚拟滚动/resize 重算) │ ├── TextInputModal.test.tsx # 粘贴文本弹窗 │ └── Toolbar.test.tsx # 工具栏(选项/导航/导出菜单向上弹出/计算中禁用/Patch 菜单项可用与判等禁用+title 解释(0.11.0)/字符级开关与空白选项联动灰显勾选+已包含徽标+点击解释+已降级与部分降级标记/搜索框计数与 Enter·Shift+Enter·Esc 键盘交互/ref 转发) └── __tests__/ ├── App.test.tsx # 应用集成级测试(加载/拖拽/粘贴/导出/容错/toast 队列/清空确认/状态栏行数/大输入回退/单文件视图搜索(含重算后计数与高亮一致·命中归零清空)/Ctrl+F/文件夹模式进出与双击(含同目录异写归一化防呆)/语义判等链路/差异文件导航(含游标不在序列时导航隐藏)/扫描竞态丢弃过期结果/增量重扫携带缓存/目录聚合导航/字符级部分降级/忽略规则链路与持久化/快照保存加载对照与拦截(加载走 snapshot:read 专用通道·快照自身截断拦截·保存中退出静默终止)/快照内容级对照(v2 保存含内容与读取进度·超限降级·双击基线vs当前·v1 兼容·导出报告含对照说明与版本号(0.11.0))/扫描进度显示与过期丢弃/关于弹框(打开·元信息·复制·双模式可见·Esc 关闭)) └── main.test.ts # 入口挂载冒烟测试 ``` > 约定:测试文件与被测文件同目录(`*.test.ts(x)`),或集中放各模块下 `__tests__`。 --- ## 5. 覆盖重点与用例 ### 5.1 diff 引擎(`diffEngine.ts`)— 最高优先级 - 空文本 / 空对非空(全新增) - 完全一致 → 全部 `unchanged`,无差异 - 行级:新增、删除(含左/右行号、空槽 lineNo 为 `null`) - 词级:同一删除/新增配对为 `modified` 并产生左右 `segs` - 忽略选项:`ignoreCase`、`trimWhitespace`、`ignoreBlankLines`、`ignoreAllWhitespace` 生效与关闭;忽略所有空白覆盖行首尾空白、换行结构差异不受其影响、空行数量差异需配合忽略空行、与大小写忽略组合、展示文本保持原文 - 字符级对比(`charMode`):判等组(跨行重组一致 / 空行数量差异 / 行内空白与全角空格 / 与忽略大小写组合 / 一致但行数不等短侧空槽仍 unchanged / 展示原文);定位组(行内替换 modified 与 segs 结构 / 行内删除与新增的对侧行回拉(含行首行尾流界跨界)/ 整行增删不误配对 / 多块修改穿插 / 删除段空白继承 / 跨行变更行号单调);换位专项组(换位场景 jsdiff 对齐任意,断言只锚定不变量:segs 拼接无损 / 高亮不越侧 / 行号单调 / 同行 ne-eq-ne 多块变更仅输出一个 modified 不重复行);降级组(归一化流超 3 万字符自动降级行级并带 `charModeDowngraded` 标志 / 编辑距离超限快速降级(内部参数 charDiffMaxEdit 注入小阈值毫秒级构造)/ 降级结果与等效行级选项一致 / 恰好阈值不降级 / 未开启不携带标志 / 全部组超限整体降级不出现 partial);逐组预算制部分降级组(0.8.0):组总流超预算时预算内组字符级、预算外组行级配对(`charModePartial` 置位、segs 拼接无损、超长行触发词级防护 segs 为 null、行号单调)/ 单组流超上限仅该组行级其余组字符级精确定位 / 单组编辑距离超限仅该组行级(charDiffMaxEdit 注入)/ 行级降级组内行数不等按序配对多余侧 added;预算自适应组(0.9.0):charGroupBudget 函数边界(小输入取下限 / 大输入按总组流一半向上取整 / 上限封顶 100 万 / 非有限输入回退下限)/ 大输入跨行重组 400 组总流 48 万 → 预算 24 万 → 200 组字符级(固定预算仅 83 组,行为放宽固化)/ 总流未超下限两倍时与历史固定预算行为完全一致(回归锚) - 超长行防护:任一侧超过阈值跳过词级高亮(segs 为 null),行仍为 modified;边界长度仍产生 segs - 字符粒度增删统计(0.10.5):charsAdded/charsDeleted 码点计数——行级 added/removed 整行计数 / modified 按 segs 增删段计数(common 不计)/ 全角与 emoji 按码点(代理对不拆分)/ 字符级模式回映射 segs 同口径 / 超长防护行(segs null)不计 / 完全一致为零 / 行级与字符级路径统计口径一致 - 行切分:结尾换行不产生多余空行 ### 5.2 报告生成(`report.ts`) - HTML:输出合法文档;Hero 区含人话结论(verdict)、文件卡片、统计卡与图例;差异分块(hunk-head)与块内行渲染;多块时输出目录锚点(toc/href="#seg-N"),单块不输出;相同内容段折叠(details/summary)且折叠段内保留全量行数据;一致场景 verdict ok;两侧空内容提示 - 纯文本:含增删改标记(`+ -`)、行号按最大位数右对齐、头部人话结论 - Markdown:表格化、管道符转义保持表格结构、引用块含人话结论 - 差异分块(`buildSegments`):空行集 / 无变更单段全展示 / 上下文 3 行与边界收敛 / 相邻区间合并 / 远距分两段省略数正确 / 自定义 context - 人话总结(`plainSummary`):无差异一致结论 / 为 0 项省略 / 三类齐全完整罗列 - 比较选项描述(`plainOptions`)与头部说明行:无选项严格对比文案 / 生效项罗列 / 忽略所有空白遮蔽行首尾空白;TXT / Markdown / HTML 三格式头部含“比较选项”行 - 字符粒度统计行(0.10.5):字符级模式(ctx.options.charMode)时统计行追加「| 字符 +A / −D」、HTML 追加「字符增删」统计卡;行级模式不追加 - TXT/Markdown 分块省略(0.11.0):段间相同行以「⋯ 省略 N 行相同内容 ⋯」提示行替代(Markdown 每段独立成表重复表头);段内上下文行保留、省略段内行不输出;完全一致场景保持全量(留档语义);单段场景无省略(回归锚) - Patch 标准差异(0.11.0,`buildPatchReport`):文件头 `--- a/` `+++ b/`(优先完整路径);hunk 头 `@@ -l,c +l,c @@` 双侧行号与计数正确;modified 行展开 -旧/+新、removed/added 单行、unchanged 上下文行;多段多 hunk 行号各自锚定;尾部无换行 `\ No newline at end of file` 标记(双侧/单侧/标记紧跟对应行且计数正确);无变更仅输出文件头(空 diff) - Patch 可导出判定(0.11.0,`canBuildPatch`):严格对比恒可导出 / 忽略大小写判等污染不可导出 / 选项开启但未触发判等差异仍可导出 / 字符级跨行重组不可导出 / 空变更行集可导出 - 元信息增强(0.11.0):版本号进三格式尾部署名(未提供时保持历史格式)/ 对照说明(compareNote)进三格式头部 / HTML 文件卡行数优先原始文本口径(ctx.leftLines) - 格式分发与扩展名映射(`REPORT_EXT`,含 patch) ### 5.3 文本工具(`textUtils.ts`) - 文本扩展名白名单放行 / 拒绝 / 大小写不敏感 - `countLines` 与 diff 引擎切分规则一致(结尾换行、CRLF) - `displayCols` 半角 1 列 / 全角 2 列 / 中英混排叠加 / 传 cap 时达上限提前返回 ### 5.4 对比视图(`DiffView.tsx`) - 渲染:行内容、行号、词级高亮段、行尾空白标注 - 搜索命中高亮(0.9.3):searchQuery 非空时命中子串包裹高亮 span(大小写不敏感、多处命中全部拆分、左右两侧各自渲染);词级 segs 行在 seg 文本内拆分;无命中或未传 query 不渲染 - 虚拟滚动:大行数下仅渲染可见窗口(DOM 行数远小于总量);滚动后窗口移动;容器总高度按全部行数撑满 - 内容最小宽度:探针测得字符宽后按最大列宽换算 minWidth(半角/全角列宽,超长行封顶);探针未测得时不设置 - 折叠提示行:默认文案与自定义文案(仅看差异视图) - 交互:左右/横向滚动同步、右键菜单回调、拖放回调、导航定位走受控 `scrollTo` ### 5.5 应用冒烟与集成(`App.tsx`) - 渲染应用名 `DiffLens`;未加载文件时展示左右空态面板、不显示导出入口 - 加载文件后的对比视图、导出、右键菜单、拖拽、粘贴、容错提示 - 仅看差异视图:未更改行折叠、折叠提示出现、关闭后恢复 - F7 / Shift+F7 导航快捷键;粘贴弹窗打开时快捷键不抢占 - 交换左右侧后面板文件互换 - toast 队列:连续多条提示同时可见不互顶 / 各自独立计时消失 / 超过上限顶掉最旧 / 动作按钮点击后该条立即关闭 / 二进制预警与行数预警共存 - 清空二次确认:有内容时第一次点击仅提示不清空 / 确认窗口内再点执行清空 / 超时自动复位(再点仍先提示)/ 空态点击不弹确认 - 状态栏行数:单侧/双侧加载显示对应行数(切分与引擎一致)/ 清空后随空态消失 - 偏好持久化(比较选项 + 仅看差异开关):启动时恢复上次保存的状态(选项勾选与视图折叠生效)/ 损坏 JSON 回退默认且不崩溃 / 非布尔字段防御性忽略 / 切换选项后写回存储(用例间 beforeEach 清理 localStorage 防串扰) - 字符级对比:跨行重组开启后判为完全一致;开启时三个空白选项灰显勾选并带"已包含"徽标,点击弹出解释提示,关闭后恢复用户原勾选状态;内容超限自动降级并 toast 提示(开关旁常显"已降级"标记);导出报告携带字符级说明;降级后报告按实际生效的行级语义描述选项 - 忽略所有空白:仅行内空白不同的两侧文本开启后判为完全一致;导出报告携带比较选项说明 - 单文件视图搜索(0.9.3):输入关键字即时命中计数与首个命中行 active 定位 · Enter/Shift+Enter 命中行间推进回退(行号随动)· 首尾循环 · 大小写不敏感 · 无命中 0/0 且 Enter 不崩溃不推进 · Esc 清空复位 · Ctrl+F 聚焦搜索框 · 命中行被“仅看差异”折叠时计数仍统计全量行、跳转 toast 提示且不推进 · 切选项/换文件重算后命中计数与 active 高亮行自动钳制重定位保持一致(0.10.4,rows 重建行 id 重新编号不再脱节;命中归零清空高亮) - 文件夹模式搜索与换目录状态(0.10.5):Ctrl+F 聚焦文件名搜索框并 Esc 清空(跨模式一致)· 重新选择新目录对后旧搜索词不残留(视图状态重置生效) - 大输入(超快路径阈值)在无 Worker 环境去抖到期后回退同步计算,完成后无计算中遮罩 - 文件夹模式:依次选择两侧目录进入(路径卡片/统计徽章/状态栏切换/默认仅看差异过滤 same) · 任一次选择取消不进入 · 两次选择同一目录防呆提示且不进入(含 Windows 异写路径归一化判定,0.10.2) · 扫描失败 toast 且不进入 · 双击差异条目加载两侧进入单文件对比并可返回文件夹(返回清空两侧面板) · 双击单侧条目加载存在侧并清空另一侧 · 双击条目任一侧读取失败(error 返回 / IPC 异常)清空失败侧显示未选择(不残留旧对比内容与旧文件名) · 「返回文件对比」清空文件夹状态回单文件空态(先有文件对比再进文件夹的场景同样清空,不残留旧对比) · 扫描竞态:慢的旧扫描后完成被会话序号丢弃(不覆盖新扫描的目录与条目、不残留扫描遮罩);扫描进行中退出文件夹对比后迟到的结果不把界面拉回文件夹模式 - 快照基线(0.9.0,0.9.1 收尾,0.9.2 对称防呆,0.10.0 内容级对照):保存快照落盘内容为合法快照 JSON(含当前结果与目录对) · 截断结果保存快照被拦截提示且不落盘(0.9.1,防对照误报) · 当前扫描结果截断时加载快照同样被拦截(0.9.2,不弹文件选择——截断缺失文件会被误报已移除) · 对照模式中重扫触发截断自动退出对照并提示(0.9.2) · 快照语义判等口径与当前开关不一致时加载提示含口径差异、一致时无提示(0.9.2) · 加载快照对照模式渲染变迁统计与条目标注 · 目录对不一致 / 快照文件无效 / 读取失败拦截提示 · 对照模式导出报告携带快照口径说明与变迁对照明细(0.9.1) · 退出对照恢复常规视图 · 对照随重扫自动刷新 · 快照内容级对照(0.10.0):保存时差异条目双侧内容入 contents(批量读取按路径返回、same 条目不存、单文件超限侧不读该侧 null)/ 内容总量超限整体降级为纯状态快照并提示(vi.mock 压低上限构造)/ 对照模式双击含基线内容条目进入"快照基线 vs 当前"内容对比(左面板快照基线标注、右面板当前文件、有差异徽章)/ v1 快照双击走常规当前文件对比(兼容回归锚) · 保存读取分批进度遮罩"已读取 N / M"(0.10.1,第二批挂起构造断言,结束复位) · 双击 same 条目 / 对照模式双击 fixed 条目进入对比后差异文件导航隐藏(0.10.1,游标不在差异序列;双击差异条目导航恢复) · 加载走 snapshot:read 专用读取通道(0.10.2,mock 随通道迁移;保存/加载上限不对称修复的通道基础) · 快照自身截断(手工构造形态 truncated: true)同样被拦截且不进入对照(0.10.4,与保存侧/当前侧对称的三侧防呆收官) · 保存批量读取期间退出文件夹对比:在途保存静默终止、saveReport 不被调用、遮罩即时消失(0.10.4,保存世代号作废;正常保存路径回归锚由读取进度用例覆盖) - 关于弹框(0.10.2):点击 header ⓘ 按钮打开(版本/作者/双邮箱/组织/开源地址/许可证全量断言,版本号来自 app:get-version) · Esc 关闭 · 点击邮箱触发剪贴板复制与 toast 反馈 · 文件夹模式下入口同样可见 - 导出报告增强(0.11.0):快照内容对照模式导出 HTML 报告——头部含对照说明(左侧为快照基线 + 保存时间、右侧为当前文件)与生成器版本号署名(app:get-version 链路) ### 5.6 diff 计算 worker 化(`useDiff.ts` / `createDiffWorker.ts`) - 快路径:两侧总行数不超过 2000 时同步计算,不创建 worker,结果与 computeDiff 一致 - 重路径:进入 computing 且 diff 置空;200ms 去抖到期后才创建 worker 并派发;响应按 jobId 匹配,错误/过期 jobId 被丢弃 - 字符级大字符量重路径:charMode 开启且两侧字符总量超过阈值(少行大文件,行数维度防不住 diffChars 耗时)同样进入重路径;未超阈值/charMode 关闭不受影响;恰好阈值不触发 - 输入变更:已派发的旧 worker 终止重建(等价取消);去抖期内变更则旧任务未创建即取消(零创建与计算开销) - worker 不可用(环境缺失/工厂返回 null):去抖到期后主线程同步回退 - 卸载:去抖期内卸载则 worker 从未创建;去抖到期后卸载则终止 worker,去抖定时器均被清理 - worker 运行崩溃(onerror 兜底):终止实例并回退主线程同步计算(computing 不再永久卡死);迟到错误(effect 已取消 / jobId 已推进)只终止不回填,新任务结果正常生效 - 工厂:无 Worker 环境返回 null;可用环境返回实例 ### 5.7 应用状态持久化(`src/main/windowState.ts`) - parseState:空输入/非法 JSON 回退空状态;合法 bounds 与 lastDir 保留;lastReportDir 合法字符串保留、非字符串/空串忽略(0.11.0 导出目录记忆);字段类型异常(非矩形/非字符串/空串/非有限数值)防御性忽略 - clampBounds:工作区内合法 bounds 原样保留;小于最小尺寸钳到最小值;超出工作区封顶;移出屏幕四向拉回(至少 60px 进入工作区);多显示器偏移工作区同样钳制;非整数取整;工作区小于最小尺寸时尺寸仍保持最小值 - 磁盘读写与窗口事件绑定在主进程入口(覆盖率排除,真实链路由 E2E 覆盖) ### 5.8 解码与编码探测(`src/main/decode.ts`) - looksBinary:空输入 / NUL 立即判定 / 纯可见 ASCII / 控制字符占比超 5% / 常见空白不计入 - decodeText:UTF-8 BOM(剥离 BOM + 标注)/ UTF-16 LE 与 BE BOM / 仅 BOM 无内容 / 短于 BOM 长度走严格 UTF-8 路径 / 无 BOM 合法 UTF-8 与纯 ASCII / GBK 中文回退解码正确 / 空输入 / 含 NUL 非法 UTF-8 的 GBK 回退 + 二进制判定 / 替换符占比超 5% 判二进制 - worker 调度(阈值分流/崩溃回退)在主进程入口(覆盖率排除,大文件 worker 链路由 E2E 覆盖) ### 5.9 文件夹扫描(`src/main/folderScan.ts`,真实临时目录) - 状态判定:内容一致 same / 同大小内容不同 different / 大小不同 different / 单侧缺失 only(另一侧大小 null) - 语义判等(0.7.0):semanticEquals 判等口径(空白/换行/空行/跨行重排判等,实质差异与大小写不判等);仅空白差异文本文件判 semantic-same(同大小与不同大小均覆盖);实质/大小写差异仍 different;非文本扩展名与超大小上限不参与(保持 different);未开启时行为与 0.6.2 一致;字节一致文件不触发解码 - 失败降级(0.7.0):单文件 IO 失败(模拟被锁)标记 unreadable 不影响其余条目;目录不可读跳过该目录(该侧按无文件处理,另一侧条目为 only),不再整体抛异常 - 递归子目录按相对路径对齐(统一 / 分隔);二进制内容字节级判定;symlink 跳过;条目字典序排序;total 取两侧较大值 - 边界与选项:两个空目录 / maxFiles 截断(truncated + total + 条目数受限;截断后不再深入子目录,total 为已遍历下限) / 超 maxContentBytes 同大小文件头部采样一致判 same 带 approximate / 采样头部不同直接 different - 受控并发:跨多个并发批次(> 2×SCAN_CONCURRENCY)的混合状态全部判定正确、条目排序与并发执行顺序无关 - 增量重扫(0.8.0,previous 缓存):指纹一致(size+mtime 未变)零 IO 沿用判定(decode 计数零增长)/ 语义开关翻转按 semSame 纯内存组合(different ↔ semantic-same 零 IO,开→关→开三轮)/ 上次未参与语义(semSame null)而本次开启读内容补判 / 指纹变化(内容修改)重新判定且文件增删正确 / 单侧缺失形状变化不误沿用 / 本次截断忽略 previous 且 cache 产出为空 / unreadable 不进缓存(故障恢复后重扫正确判定) - 忽略规则(0.9.0,0.10.0 高级 glob,0.10.1 字符通配):目录规则整树剪枝(深层子树不枚举、不计数、不进结果与缓存)/ 扩展名通配与名字匹配跳过文件(大小写不敏感)/ 被忽略文件不占用 maxFiles(原本截断的场景忽略后完整扫描)/ 规则变化后重扫缓存未命中的重新纳入条目走全量判定 / 空规则数组与全非法规则不产生任何剪枝 / 多段路径从根锚定剪枝(其他分支同名目录不受影响)/ ** 跨段剪枝与否定救回(父目录未被剪枝时子路径可被反否定)/ 父目录被剪枝后否定规则救不回子项(gitignore 同款语义)/ 段内 ? 单字符与 [abc] 字符类跳过文件(*.lo? 与 *.[ch] 真实目录集成,0.10.1) - 比对进度回调(0.10.0):需读内容的条目按批推进进度(total 恒为需读内容条目数、done 单调递增至 total)/ 全部条目零 IO(单侧缺失)时不触发回调 ### 5.10 文件夹视图(`FolderView.tsx`)与条目树(`folderTree.ts`) - 条目树纯逻辑:按路径分段聚合目录/文件(同层字典序);目录聚合子树状态(different > unreadable > only > 语义同 > 相同)与文件计数;折叠目录子树隐藏;过滤(onlyDiff / statusFilter / query 子串不区分大小写 / exts 扩展名集合);空目录剪枝由“过滤后建树”天然保证;relExt 取文件名段扩展名(小写、无扩展名空串、不误取目录名中的点) - 渲染:目录路径卡片 / 重新扫描、重新选择、导出报告与返回按钮 / 统计徽章(相同/语义同/不同/仅左/仅右/无法读取/总数;语义关闭隐藏语义同、无失败隐藏无法读取) - 树形交互:目录行点击折叠展开(默认全展开);搜索命中强制展开命中路径、清空恢复用户折叠;无命中提示 - 扩展名过滤(0.8.0):下拉展示全量条目 ext→count(字典序、无扩展名单列);点选过滤与其他条件取交集、计数切 N / 总数;多选并集、再点取消、清除按钮复位;与状态过滤双激活空态提示;点击外部关闭且过滤状态保留 - 忽略规则(0.9.0,0.10.0 高级 glob 与无效提示,0.10.1 字符通配文案):已有规则时入口标注数量并高亮;下拉打开以当前规则初始化草稿,逐行解析去空白去空行(截断至 50 条)后应用回调并关闭;清空规则回调空数组;点击外部关闭不触发回调;无效规则实时提示"将被忽略的无效规则"(修正/清空后提示消失,不再静默丢弃);提示文案与 title 含 ? 单字符与 [abc] 字符类新语法 - 快照对照模式(0.9.0):变迁统计徽章(已修复/新增差异/新文件/已移除/仍有差异/保持一致)与基线时间替换常规统计行;对照条目平铺全路径渲染、行尾变迁徽章、gone 回退快照数据;零计数变迁不占位;默认隐藏 stable、点击徽章单选过滤再点取消;双击当前条目进单文件对比、已移除条目双击无效;退出对照恢复常规视图;全 stable 空态提示;重扫刷新后过滤的变迁类型计数归零自动复位(0.9.3,残留过滤不再误显空态);对照条目含基线内容时 title 提示"双击查看快照基线内容对照"(0.10.0) - 快照归组下拉(0.10.5 布局重构):「快照」入口打开下拉含保存/加载菜单项,菜单项触发回调并关闭;点击外部关闭不触发回调;scanning/saving 时入口禁用(不打开下拉)、其余操作(重扫/重选/导出/返回)不受影响 - 换目录视图状态重置(0.10.5):目录对变化重置搜索词/状态过滤/扩展名过滤/折叠集合/变迁过滤(同目录重扫保留——区分「重扫」与「换目录」) - 搜索框(0.10.5):Esc 清空并失焦(与单文件搜索框同款)、有内容时显示 × 清空按钮(点击清空并聚焦回输入框)、searchInputRef 转发(Ctrl+F 聚焦用) - 扫描进度遮罩(0.10.0):progress props 非空时遮罩显示"已比对 N / M",null 时仅基础文案 - 过滤:仅看差异隐藏 same 与 semantic-same;徽章点击状态精确过滤(激活态高亮、与仅看差异互斥联动) - 扫描中交互防抖(0.9.2):扫描进行中语义判等开关与忽略规则入口禁用(触发重扫的入口,点击不触发回调/不打开下拉);仅看差异开关与搜索仍可用(纯视图过滤不触发重扫);「重新选择」扫描中保持可用(0.9.3 固化:发起新会话作废在途扫描,与「返回文件对比」同类) - 快照保存中防抖(0.10.2):saving=true 时保存/加载快照按钮禁用(点击不触发回调);重新扫描/重新选择/导出/返回不受影响(仅快照链路防抖) - 目录行双击(0.8.0):触发 onOpenDir 携带目录相对路径;双击前折叠态自动展开(toggle 残留消除);重新扫描按钮触发 onRescan 且扫描中禁用 - 双击文件回调携带原始条目;大小格式化(B/KB 与缺失占位);approximate 渲染 ≈ 标注;语义同条目双击提示;截断提示;空态提示(无文件 / 全部一致 / 过滤无结果) - 虚拟滚动:大列表仅渲染可见窗口、容器总高度按树形扁平化行数撑满 - 窗口尺寸变化:resize 后按新视口高度补齐渲染窗口(stub clientHeight 模拟拉高,DOM 行数随之增加) ### 5.11 E2E 端到端(`e2e/app.spec.ts`,对 build 产物启动真实 Electron) - 启动冒烟:标题与双空态面板、空态无导出入口 - 粘贴两侧:差异渲染 + 统计徽章(主进程剪贴板链路) - 编码链路:GBK 与 UTF-8 BOM 文件打开(编码标注 + 内容渲染)、大体积 GBK 文件经解码 worker 后台解码完整渲染(状态栏总行数断言) - 比较选项:忽略大小写与忽略所有空白组合、字符级跨行重组判一致、字符级模式字符粒度统计徽章出现(0.10.5,+0 字 / −0 字) - 滚动稳定性回归锚(0.7.0):真实文件(行数不等)→ 开字符级进 worker 重路径 → 滚到中部 → 切选项二次重算 → 断言 scrollTop 保持不归零、滚到底内容连续(0.6.3 回退缺陷的固化防线) - 导航与视图:F7 / Shift+F7、仅看差异折叠、交换左右后面板互换 - 单文件视图搜索(0.9.3):粘贴两侧后 Ctrl+F 聚焦搜索框(真实键盘事件)→ 输入关键字即时计数与行内命中高亮 → Enter 跳转下一命中行(active 行号定位断言)→ Esc 清空复位计数 - 报告导出:stub 保存对话框 → 单文件 HTML 报告真实落盘且含结论与统计;Patch 标准差异真实落盘(unified diff 文件头/hunk 头/上下文与增删行断言,0.11.0);忽略大小写判等污染下 Patch 菜单项禁用(0.11.0) - 文件夹对比:真实临时目录扫描(状态判定 / 默认过滤 / 树形显示)→ 双击进入单文件对比 → 返回文件夹(扫描结果保留) - 文件夹新功能(0.7.0):语义判等徽章与过滤(CRLF/LF 差异计语义同)→ 树形折叠展开 → 搜索 → 文件夹报告 TXT 真实落盘(判定口径 + 结论 + 全量条目) - 增量重扫(0.8.0):真实目录修改文件后点「重新扫描」状态正确更新(相同→不同→改回相同,真实 mtime 变化走指纹判定) - 拖拽导入(0.8.0):dropFiles 合成 DataTransfer 拖放——空面板拖入渲染(arrayBuffer → decodeBuffer IPC 全链路)、双侧拖入对比、非文本扩展名拒绝、多文件提示取首个 - 忽略规则(0.9.0,0.10.0 高级 glob):真实目录含 node_modules 子树——无规则时差异参与对比,输入规则应用重扫后整树剪枝(不同归零、空态一致提示、被忽略条目消失);高级 glob——build 下深层 temp 目录经 build/**/temp 跨段剪枝、*.log 忽略 + !keep.log 否定救回(差异计数精确收敛)、无效规则提示出现 - 扫描进度反馈(0.10.0,0.10.3 采样修稳):4000 条目目录扫描期间遮罩"已比对 N / 4000"轮询抓取(瞬态断言)——evaluate 直读 DOM(避免遮罩消失后 locator 自动重试阻塞采样)+ intervals 前密采样,快机上数百毫秒的瞬态窗口稳定可抓;完成后全量判定与遮罩消失 - 增量缓存跨会话持久化(0.9.0):第一实例扫描后 userData/scan-cache.json 落盘(等待异步写盘,断言含条目与合法结构)→ 同 userData 第二实例重扫同目录结果正确(主进程注入盘上缓存) - 快照基线(0.9.0,0.9.1 增强,0.10.0 内容级对照,0.10.5 快照归组下拉交互):保存快照真实落盘(JSON 含版本与条目;保存/加载经「快照」下拉两步触发)→ 修复差异 + 新增文件重扫 → 加载快照对照(已修复/新文件变迁统计与条目标注)→ 对照模式导出报告真实写盘断言变迁明细(已修复/新文件/保持一致,0.9.1)→ 退出对照恢复;内容级对照——保存 v2 快照(JSON 断言含差异条目双侧内容)→ 修改左侧文件重扫 → 加载快照双击变更条目进入"快照基线 vs 当前"内容对比(左面板"快照 · 文件名"标注、基线旧词与当前新词词级高亮、有差异徽章);截断快照拦截(0.10.4)——手工构造 truncated: true 快照加载被拦截提示"条目不完整无法对照",不进入对照模式 - 大文件性能基准(0.9.0):5 万行双文件 20 处差异——打开右侧到「有差异」出现耗时 < 15s;300KB GBK 15000 行——打开到状态栏行数渲染 < 10s(万级实测秒级,上限按慢机 4 倍余量防 flaky,断言失败即性能回归) - 关于弹框(0.10.2):点击 header ⓘ 打开弹框——版本号(app.getVersion 真实链路,0.x 形态正则)/作者/双邮箱/组织/开源地址/许可证断言;点击邮箱走主进程剪贴板并 toast;Esc 关闭 - 大快照加载(0.10.2):手工构造 11MB+ 快照 JSON(contents 塞大字符串)→ snapshot:read 专用通道加载成功("已加载快照"出现且无 10MB 拦截提示)——固化保存/加载上限不对称修复 - 偏好持久化:跨实例重启恢复比较选项与视图开关(同一 userData) - 隔离:每用例启动清空 localStorage + 重载,避免偏好跨用例污染(偏好用例验证实例显式跳过清理) ### 5.12 folder:scan IPC 校验(`src/main/scanIpc.ts`,0.9.1,0.10.3 异步化) - isScanCacheEntries 形态校验:合法缓存数组(rel/status 为字符串)通过;非数组 / 含非对象 / rel 或 status 非字符串拒绝 - resolveScanArgs 缓存源选择(异步签名,loadStored 同步/异步实现均兼容):渲染端携带合法内存缓存原样传递且惰性不读盘 / 内存缓存为空数组(合法形态)同样优先(阻止注入可能过期的盘上缓存)/ 未携带与形态非法回退盘上缓存(惰性读盘恰好一次)/ 盘上缓存为 null 或空数组不注入(全量扫描)/ 异步 loadStored stub(生产 fs.promises 读盘形态)同样正确回退 - 忽略规则校验:复用 isIgnoreRulesArray(合法字符串数组与空数组传递;非数组 / 含非字符串 / 超条数整体忽略) - 组合场景:内存缓存非法 + 忽略规则合法 + 盘上缓存有效时两者分别正确解析 - 主进程 index.ts 的 handler 壳(IPC 注册与真实读写盘)由 E2E 文件夹用例覆盖 --- ## 6. 运行命令 ```bash npm test # 运行全部单元 + 组件测试(CI / 提交前) npm run test:watch # 监听模式,开发中用 npm run test:e2e # E2E 验收(先 build 再对产物启动真实 Electron;发布前必跑) ``` --- ## 7. 质量门禁 提交与推送前 **必须全部通过**: ```bash npm run typecheck npm test npm run build ``` 任一失败即禁止提交;修复通过后再提交发布。 --- ## 8. 覆盖率目标(演进基线) | 模块 | 目标 | | --- | --- | | 逻辑层(diff / report / textUtils) | 行覆盖 ≥ 80% | | 组件层(components / App) | 覆盖主流程空态与关键交互 | > 80% 四项阈值(statements / branches / functions / lines)已写入 `vitest.config.ts` 的 `coverage.thresholds`,作为 CI 硬门禁强制执行,未达标即测试失败。 --- ## 9. 演进方向(后续迭代) - 快照体验:regressed 场景基线内容保存策略(当前仅保存差异条目,基线一致的条目内容不存——需全量保存的体积代价权衡;快照内差异内容的报告导出已于 0.11.0 以"内容对照导出附对照说明"落地) - 忽略规则:gitignore 更完整子集评估(`\` 转义、前导 `!` 组合语义等,按真实需求驱动;`?` 单字符与 `[abc]` 字符类已于 0.10.1 落地) - 主进程 IPC 并发场景集成测试(入参校验与缓存注入逻辑已于 0.9.1 单测化:scanIpc;缓存落盘串行化于 0.9.3 修复) - mac 构建实际验证(entitlements 配置就绪但从未打包跑通) --- © 2026 MetonaTeam · thzxx · DiffLens