- 桌面应用
【免费下载链接】ZoneDeck
The Ultimate Workspace Manager, Switch between work and life, seamlessly生活工作无缝切换,专业的桌面工作区管理助手
ZoneDeck(专业桌面工作区管理助手)的代码库由 Rust 后端(zonedeck-common与zonedeck-core)与 Svelte 5 前端配置界面组成,其测试体系覆盖从纯逻辑单元测试到真实 Windows 系统行为集成测试的多层结构。本文以仓库官方测试策略文档(中文版 / 英文版)为骨架,结合crates/与apps/config/ui/下的真实测试用例与源码实现,完整讲解如何运行全套测试、各层测试覆盖了什么、以及项目约定的测试编写规范,帮助你快速上手为 ZoneDeck 贡献或深度定制。
一、测试体系全景:三层结构
ZoneDeck 的测试分三个层次,CI 会在每次 PR 与推送时运行整套测试套件:
zonedeck-common(纯逻辑单元测试):模型、配置解析、匹配算法、IPC 协议等可脱离系统环境验证的纯逻辑,连同配置文件读写的集成测试;zonedeck-core(系统级集成测试):窗口隐藏/恢复、热键拦截、命名管道、进程冻结、能效统计、静音、媒体暂停、崩溃恢复等真实系统行为,多数用例会创建真实资源并做「往返」验证;- 前端
vitest单元测试:配置界面ui/src/lib/中的纯逻辑(热键解析、鼠标连击、规则分组、主题、i18n 等)。
这种分层把「可以纯函数化验证的逻辑」下沉到可单测的模块,把「必须触碰 Windows API 的行为」留在集成测试中用真实资源验证,兼顾测试速度与置信度。
二、运行测试:两条命令与一个关键警告
2.1 运行全部 Rust 测试
cargo test --workspace工作区成员在根 Cargo.toml 中声明为crates/common、crates/core、apps/config/src-tauri三个包(当前工作区版本号为3.2.0)。该命令会依次执行:
zonedeck-common的内联单元测试(如 ipc.rs 里mod tests中的Command/Response往返序列化用例、config.rs 中的配置解析用例)与tests/集成测试(config_file_io.rs、data_dir_resolution.rs、legacy_dir_migration.rs);zonedeck-core的tests/集成测试(agent_ipc.rs、agent_recovery.rs 等 10 个文件)。
2.2 运行前端单元测试
文档给出了两种等价的写法,依所用包管理器而定:
# 使用 pnpm(仓库自带 pnpm-lock.yaml,packageManager 为 pnpm@10.33.4) pnpm --dir apps/config/ui test # 使用 npm npm --prefix apps/config/ui test二者的落点都是 apps/config/ui/package.json 中声明的脚本:"test": "vitest run"(另有"test:watch": "vitest"用于监听模式)。测试文件与源码一一对应,位于ui/src/lib/下,文件名形如*.test.js。
2.3 重要警告:COM 相关的核心测试必须单线程
zonedeck-core的部分测试会初始化 COM(最典型的是静音链路,走 Core Audio 的 COM 接口),在多线程并行运行时可能崩溃。遇到此类问题,请改用单线程执行:
cargo test -p zonedeck-core -- --test-threads=1这条命令等价于「只跑核心包、限制测试线程数为 1」。从源码看,toast_without_tray.rs 等用例中通过CoInitializeEx初始化 COM 公寓,确实存在跨线程共用的敏感状态,因此官方在测试策略文档中把这条警告放在显著位置。
三、zonedeck-common:纯逻辑与配置 I/O 的守卫
zonedeck-common的测试分两类:模型 / 配置 / 匹配 / 协议的纯逻辑单元测试,以及配置文件读写的集成测试。官方文档列出的核心覆盖点包括:
- 配置解析与默认值、旧配置迁移:扁平绑定 → 窗口规则、旧鼠标开关 → 连击触发(
Hotkey::normalize在 config.rs 中通过*hook |= intercept完成「走钩子蕴含吞键」的幂等归一); PID大写字段兼容、正则规则往返序列化;- 连击次数 / 连击窗口的范围钳制:常量定义在 config.rs ——
MAX_CLICKS = 3(最多三连击)、MIN_MULTI_CLICK_MS = 150、MAX_MULTI_CLICK_MS = 1000、默认DEFAULT_MULTI_CLICK_MS = 350;MouseButton::normalize用clamp(1, MAX_CLICKS)钳制次数; - 协议
Command/Response的 round-trip 与 snake_case 标签:Command以#[serde(tag = "cmd", rename_all = "snake_case")]序列化,如ReloadConfig序列化为{"cmd":"reload_config"}; - 语言标签解析与偏好归一化(
zh-Hant→zh-TW、无翻译的语言回落auto等); - 数据目录定位与迁移;
- 配置写入的原子性。
3.1 配置文件 I/O 的真实用例(config_file_io.rs)
该文件是目前最厚的 common 集成测试,完整刻画了配置读写的边界行为:
| 测试行为 | 用例要点 |
|---|---|
| 保存/加载往返 | 写入含窗口规则、进程正则规则的配置到真实临时文件,Config::load后整体相等 |
| 缺失文件 | 加载不存在的文件返回默认值(不是报错) |
| 损坏文件 | 解析失败回退默认值,且load无副作用:不移动原文件、不产生.bad备份 |
| 错误上报 | load_reporting在回退默认值的同时把解析错误返回给调用方,让「用户规则丢失」可被日志记录 |
| 损坏文件备份 | 备份为config.json.bad,须保留完整原文;rename直接顶替旧备份 |
| 数值字段类型错误 | 单个数值字段类型不符(如浮点写进整数字段)会让整份配置解析失败并回退,用户规则必须能从备份找回 |
| 备份失败仍回退 | 备份被同名目录占位导致 rename 失败时,仍须回退默认值保证核心能启动,并报告「备份原文件失败」 |
| 保存不留临时文件 | 写入后目录里只应有config.json一个文件(tmp 改名成功) |
| 写失败不破坏旧文件 | 用目录占住config.json.tmp模拟写入中途失败(磁盘满、杀软拦截),原配置必须完好 |
| 错误带路径 | I/O 错误信息必须包含实际失败路径 |
| 自动建目录 | 保存时自动创建缺失的父目录 |
| 通用 JSON 可读性 | 写出的文件须能被serde_json::Value这类通用解析器读取,且包含setting/hotkey/window_rules/process_rules/notifications五个顶层键 |
| 新 schema 降级保护 | 更高版本配置(如version: "v9.9.9.9")照常加载、认得字段一个不少,同时复制留底config.json.v9.9.9.9.bak并报告NewerSchema |
| 版本号安全 | 无法解析的版本号(""、latest、-rc1后缀等)绝不当作更高版本;恶意的9.9.9.9/../../../evil路径不得逃逸配置目录写出任何文件 |
| schema 版本戳 | 保存时一律盖当前程序 schema 版本(APP_CONFIG_VERSION,当前为v3.0.0.0) |
| 废弃运行时字段 | history、frozen_pids这类运行时字段只读不回写 |
3.2 数据目录定位与迁移(data_dir_resolution.rs、legacy_dir_migration.rs)
这两组用例验证了 ZoneDeck 的数据目录策略:
- 便携版就地:压缩包解压版的数据目录就是程序目录,拷贝整个文件夹即带走全部设置,且不在用户目录下留空文件夹;
- 安装版走用户目录:装在 Program Files 下的核心即使提权后写得进程序目录,也必须用
%APPDATA%\ZoneDeck,避免普通权限的配置程序写不进去、两边各读一份; - 不可写回退:便携目录写不进去时回退用户目录,
DataDirKind::PortableFallback让界面能提示用户「这是权限问题」并点名失败目录; - 安装版识别:通过标记文件(
INSTALLED_MARKER)识别,标记被误删时退而识别卸载程序unins001.exe(序号递增,不能只认unins000);uninstall-notes.txt、unins000.exe.bak这类长得像的文件不会被误判; - 迁移语义:安装版接管程序目录里的旧配置时,内容必须搬过去、搬走后旧位置不留副本;原文件删不掉(Windows 上文件被打开)时迁移结果不受影响;目标已有配置时只补空缺、绝不覆盖;迁移幂等;源与目标相同时不得把配置搬没;
- 品牌改名迁移(legacy_dir_migration.rs):旧
%APPDATA%\BossKey自动迁往%APPDATA%\ZoneDeck,新目录存在则不碰旧目录,重命名被占用(文件以仅共享读方式打开)时退回复制关键文件; - 可写性探针:
dir_writable的探针文件必须用后即删。
四、zonedeck-core:系统级集成测试矩阵
核心包的集成测试「多为创建真实资源往返验证」。官方文档给出了完整的领域覆盖表,逐项说明如下:
| 领域 | 测试内容 | 对应实现/用例 |
|---|---|---|
| 窗口枚举 / 隐藏 / 显示 | 创建真实窗口做隐藏 → 显示往返 | HideController配合真实窗口 |
| 热键解析 | 字符串 →RegisterHotKey参数 | hotkey.rs解析逻辑 |
| 热键拦截 | 键盘钩子判定的纯逻辑:修饰键完全吻合、长按只触发一次、按下被吞则抬起也吞 | 键盘钩子决策分支 |
| 单实例互斥 | 命名互斥体 | single_instance.rs |
| 命名管道 | 服务端收发;连开连关的重连竞态(客户端抢在ConnectNamedPipe之前连上仍须正常应答) | ipc_server.rs 服务端 |
| 进程冻结 | 真实子进程挂起 / 恢复 | freeze.rs(普通冻结 + pssuspend 增强冻结) |
| 能效统计 | 真实子进程走一遍冻结 / 效率模式,核对落盘的成绩单;经 IPC 重置清零 | power_stats.rs、agent_power_stats.rs |
| 静音 | Core Audio COM 链路;会话匹配与记账判定 | audio.rs |
| 媒体暂停 | 匹配 SMTC 的 AUMID 与映像路径 | media.rs |
| HideController | mock 注入验证静音 / 冻结 / 暂停媒体编排 | Effects/WindowManagertrait 的 mock 实现 |
| 开机自启 | 普通注册表键逻辑 + 真实schtasks接受任务 XML | autostart.rs |
| 崩溃日志 | 写入 / 轮转 / panic 钩子 | logging.rs |
| 崩溃恢复 | 快照落盘往返 + mock 恢复编排 | agent_recovery.rs |
| 图标编码 | CRC32 / Adler-32 / base64 已知向量 + PNG 结构解析 + 真实 explorer.exe 提取 | icon.rs |
| 端到端 IPC | 经PipeClient驱动真实 agent 的 IPC / 崩溃恢复测试 | agent_ipc.rs 等 |
| 文案 catalog | 三种语言逐条非空、英文与中文不重复、占位符跨语言一致 | i18n.rs |
4.1 端到端:用PipeClient驱动真实 agent
zonedeck-core/tests/下共有 10 个文件,其中agent_*系列(agent_ipc.rs、agent_power_stats.rs、agent_recovery.rs、agent_tray_clicks.rs、agent_tray_toggle.rs、agent_window_tracking.rs、agent_tool_alignment.rs)是最有代表性的端到端测试:
- 套路:在独立线程上以
AgentOptions { pipe_name, enable_tray, auto_quit_ms, .. }启动真实 agent(agent::run),再以PipeClient::new(pipe)连接命名管道,用真实 IPC 命令驱动整个核心; - 验证点:
GetState/GetStatus(一次往返取回隐藏态、权限与自动隐藏开关)/ReloadConfig/Toggle/Quit全程应答,且 agent 收到Quit后线程能在超时内干净退出; - 重连竞态:文档特别提到「客户端抢在
ConnectNamedPipe之前连上仍须正常应答」。客户端侧 ipc.rs 的PipeClient默认重试 25 次、间隔 40ms,还提供fast()快速失败模式(只连一次); - 托盘点击(agent_tray_clicks.rs):直接向代理窗口投递
WM_APP_TRAY+WM_LBUTTONUP/WM_LBUTTONDBLCLK消息序列,覆盖真人点不出来的边角——尤其是双击尾部那次抬起必须被吞掉,否则会在双击判定过后再翻转一次隐藏状态; - 托盘开关动态挂载(agent_tray_toggle.rs):撤下 → 挂回 → 再撤下,每一步都走通托盘图标的创建与销毁两条路径,且核心全程应答。
4.2 崩溃恢复与窗口追踪的真实窗口测试
- agent_recovery.rs:创建带消息循环的真实窗口并预先隐藏,预置
recovery.json快照后启动 agent——核心接手后窗口须继续隐藏(不得弹出),接管后的记录重新落盘以防再次异常退出;正常退出时窗口须还回来。另一个用例验证跨重启的过期快照(boot_time_ms比当前开机时刻早一天)会被直接丢弃、不做任何恢复动作、文件被清除; - agent_window_tracking.rs:被隐藏的窗口自行销毁后,事件钩子须实时移除隐藏记录并同步删除恢复文件;
- agent_tool_alignment.rs:恢复工具经
AdoptWindows/ReleaseWindows隐藏/释放窗口时,核心记录与恢复文件须保持同步。
快照结构见 recovery.rs:Snapshot带schema(当前为 1)、boot_time_ms、hidden、frozen、muted、efficiency等字段;ProcRecord带进程创建时刻用于身份校验(PID 会被系统回收复用),写入走 tmp + rename 原子替换。
4.3 真实副作用的「量出来」测试
- power_stats.rs:用真实子进程(
cmd /c ping -n 30 127.0.0.1 >nul)走完set_efficiency → suspend → trim_working_set → resume → clear_efficiency,核对落盘的PowerStatsStore:冻结/效率模式各记一笔、时长在解冻/撤销时结算、清空工作集量出实际换出的内存(清空前后的差值);同时验证副作用施加失败的进程不计账(对不存在的 PID 调用挂起不产生任何统计); - efficiency_real_roundtrip.rs:接真实的
WinEffects(而非 mock),通过GetProcessInformation(ProcessPowerThrottling)探测子进程三个时点(基线 / 隐藏后 / 恢复后)的 EcoQoS 状态掩码与优先级:隐藏后进程带上PROCESS_POWER_THROTTLING_EXECUTION_SPEED、优先级压到IDLE_PRIORITY_CLASS;恢复后 EcoQoS 撤销、优先级抬回普通、电源策略交还系统托管(ControlMask留着会让进程从此不再被自动放进 EcoQoS)。两个用例分别验证「只开效率模式」与「冻结+效率模式同开(默认配置)时的撤销顺序」; - agent_power_stats.rs:预置非空成绩单启动 agent,经
Command::ResetPowerStats清零后立即落盘(而非等到退出),并确认退出时的 flush 不会把清零前的旧值又写回去——这正是 ipc.rs 注释「核心在运行时必须由它自己清,否则内存里的旧值会覆盖回文件」所防范的问题; - toast_without_tray.rs:Toast 通知在没有托盘图标的情况下也能走完发送流程(托盘气泡做不到:图标不在时
Shell_NotifyIcon直接失败)。断言落在「开始菜单里有没有带程序 AUMID 的快捷方式」而非「通知平台认不认」(后者由 shell 异步索引、时机不定);用例会触碰真实开始菜单,结束时按跑之前的状态还原;不发通知则绝不写开始菜单。
4.4 受限环境说明
文档特别注明:schtasks相关的开机自启集成测试在受限 / 无权限的 CI 或沙盒环境中可能失败,这属于环境限制而非功能回归,本地具备权限时应能通过。
五、前端测试(vitest):把复杂度下沉到lib/
前端的设计原则是:UI 组件(components/)保持薄逻辑,主要复杂度全部下沉到可测试的lib/。ui/src/lib/下每份纯逻辑模块都有同名*.test.js配对:
| 测试文件 | 覆盖内容 | 典型断言 |
|---|---|---|
| hotkey.test.js | 热键解析 / 格式化 | modifiersFromEvent按固定顺序拼Ctrl / Alt / Shift / Win;keyName字母数字统一大写、F1–F24、小键盘按code认(不受 NumLock 影响)、OEM 符号映射为位置名(Semicolon → OEM_1);requiresHook判断纯修饰键 / 多主键组合只能走低级键盘钩子 |
| pointer.test.js | 鼠标连击 / 连击窗口 | describeTrigger把 1/2/3 次连击说成单击/双击/三击、越界夹回1..3;buildTimeline生成角落隐藏的动画时间轴,验证「甩过去 → 隐藏 → 再甩一次 → 恢复」的可见性序列与「仅快速移动」标记 |
| grouping.test.js | 窗口 / 进程规则增删与过滤 | groupByProcess按进程聚合;filterWindows标题模糊匹配不区分大小写;splitByVisibility按可见性分组(缺省视为可见,兼容旧数据);规则去重按匹配依据(同路径忽略大小写、正则规则不参与去重种子);正则种子containsPattern("微信") → ".*微信.*";traceWindowRule判定live / reacquired / missing / regex |
| theme.test.js | 主题偏好逻辑 | nextTheme按auto → light → dark → auto循环,未知值回到auto;resolveTheme显式偏好压过系统配色,auto跟随系统 |
| i18n.test.js | 语言解析 + 三份 catalog 对齐 | fromTag把zh/zh-Hans/zh_CN等写法归一为zh-CN、zh-Hant→zh-TW;normalizePref非法值回落auto;逐键校验三份 catalog 键集完全一致、没有空文案、占位符{...}跨语言一致(漏译会静默回落到简体中文、界面变中英混排) |
| analytics.test.js | 遥测事件白名单对齐 | mock 掉ipc.js后收集analytics_track调用;读取 Rust 侧analytics.rs里EVENTS白名单,与界面实际track("…")调用做双向比对,防止前后端事件名漂移 |
六、编写测试的约定
官方测试策略文档给出三条核心约定:
- 纯逻辑优先单测:任何可以抽成纯函数的逻辑,都应放入
zonedeck-common或前端ui/src/lib/,配单元测试。这保证大部分逻辑可以在秒级跑完,不依赖 Windows 系统资源; - 系统行为用集成测试:涉及窗口 / COM / 管道等系统资源的,创建真实资源做往返验证;必要时用 mock(
Effects/WindowManagertrait)隔离副作用。HideController的编排逻辑正是通过 mock 注入验证「静音 / 冻结 / 暂停媒体」的组合调用顺序; - 提交前本地必过:提交前确保本地测试通过。详细流程见贡献指南(英文)与贡献指南(中文)的「提交前检查清单」。
补充一点来自贡献指南的联动约束:任何新增的用户可见字符串必须同步补进三种语言,否则测试会失败;跨文件的漏译由 scripts/i18n-check.ps1 兜底检查。
七、实践建议:如何在本地把整套测试跑起来
按官方流程落地到日常开发:
# 1. 先跑纯逻辑(快、无系统依赖) cargo test -p zonedeck-common pnpm --dir apps/config/ui test # 2. 再跑核心集成测试(需要 Windows + 相应权限) cargo test -p zonedeck-core # 3. 若 COM 相关用例崩溃,退守单线程 cargo test -p zonedeck-core -- --test-threads=1需要留意的是:zonedeck-core的集成测试创建真实窗口、真实子进程、真实命名管道,甚至触碰开始菜单与注册表/计划任务,因此应在具备足够权限的 Windows 本地环境运行;在受限 CI 或沙盒中,schtasks自启用例可能失败,这并非功能回归。而 common 与前端两层的测试完全平台无关,可作为每次改动的快速回归基线。理解了这套「纯逻辑单测 + 真实系统集成测试 + 前端 vitest」的分层体系,就能准确判断每个新功能应该落在哪一层、用什么方式验证——这正是 ZoneDeck 得以在窗口管理这类高系统耦合场景下保持稳定与可维护性的关键。
- 桌面应用
【免费下载链接】ZoneDeck
The Ultimate Workspace Manager, Switch between work and life, seamlessly生活工作无缝切换,专业的桌面工作区管理助手
相关推荐
Kedro测试策略:从单元测试到集成测试的完整实践
Kedro测试策略:从单元测试到集成测试的完整实践 引言:为什么数据科学项目需要严谨的测试策略? 你是否曾遇到过这些问题:数据管道在本地运行正常,部署后却频繁崩
数据工程工作流自动化如何快速在Atlas 800T A2服务器上部署Qwen3-30B-A3B-Thinking-2507模型:完整指南
如何快速在Atlas 800T A2服务器上部署Qwen3 30B A3B Thinking 2507模型:完整指南 想要在Atlas 800T A2服务器上高
PayloadsAllTheThings:Web安全payload去哪里找、怎么用
PayloadsAllTheThings:Web安全payload去哪里找、怎么用 WAF把你的测试请求拦下来时,问题往往不在你不懂漏洞,而在手边没有一个能用的
网络安全应用安全渗透测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考