egui_kittest:基于 AccessKit 的 egui 即时模式 UI 自动化测试与图像快照实践指南
【免费下载链接】eguiegui: an easy-to-use immediate mode GUI in Rust that runs on both web and native项目地址: https://gitcode.com/GitHub_Trending/eg/egui
导读
egui_kittest是 egui 官方仓库中随附的 UI 测试库(位于 crates/egui_kittest),它基于 kittest 为主线,结合仓库内 lib.rs、config.rs、snapshot.rs 等源码与 tests/tests.rs 测试用例,系统讲解如何用它在无窗口环境下驱动 egui、断言控件状态、配置kittest.toml、开展快照测试并解决跨平台渲染差异。
一、为什么需要 egui_kittest
egui 是即时模式(immediate mode)GUI:每一帧 UI 都由应用代码重新绘制,没有传统保留模式 GUI 那样的控件对象与事件回调。直接测试这种 UI 很麻烦——你不能「拿到按钮对象再调用 onClick」,因为按钮只在绘制那一帧存在。
egui_kittest的解法是:在测试进程中用一个 Harness 模拟 egui 的整个运行循环(输入事件、帧推进、AccessKit 无障碍树更新),然后通过无障碍树查询控件、模拟点击/按键/滚动,最后断言控件状态;配合wgpu特性甚至可以离屏渲染出像素图,与预先提交的快照 PNG 逐像素比对。它同时是 egui 自身(含 egui_demo_lib 的 40 余张 demo 快照)与 egui_kittest 自带测试 所使用的回归测试基础设施。
二、最小示例:从「查询控件」到「断言状态」
README 给出了一个完整的 checkbox 交互测试。这里按源码结构拆解它的每一环节:
use egui::accesskit::Toggled; use egui_kittest::{Harness, kittest::{Queryable, NodeT}}; let mut checked = false; let app = |ui: &mut egui::Ui| { ui.checkbox(&mut checked, "Check me!"); }; let mut harness = Harness::new_ui(app); let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::False)); checkbox.click(); harness.run(); let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.accesskit_node().toggled(), Some(Toggled::True)); // 把窗口收缩到刚好包住内容的最小尺寸 harness.fit_contents(); // 甚至可以渲染 UI 并做图像快照测试 #[cfg(all(feature = "wgpu", feature = "snapshot"))] harness.snapshot("readme_example");关键环节在源码中均有对应实现:
- 初始化即渲染首帧:
Harness::new_ui内部调用Self::builder().build_ui(app)(lib.rs),而Harness::from_builder会先ctx.run_ui跑一帧「让 AccessKit 状态初始化,用户可以立刻查询控件」,随后harness.run_ok()把界面推进到稳定态(lib.rs)。 - 无障碍树查询:
Harness实现了kittest::Queryabletrait(lib.rs),因此get_by_label、get_by_role等查询方法直接可用,返回 Node 包装节点。 - 模拟点击:
Node::click()会在节点中心依次注入按下、抬起两个PointerButton事件(node.rs);事件被写入EventQueue,下一次harness.run()/step()时逐条送入 egui(lib.rs)。 - 推进到稳定:
run()会一直step()直到「所有动画结束、不再请求重绘」,超出max_steps则 panic(lib.rs)。 - 收缩窗口:
fit_contents()执行一次 sizing pass,把 popup、tooltip 等所有层级的可见区域合并计算后设置窗口尺寸(lib.rs),快照前常调用它,避免图像里留下大片空白。
带状态(State)的 Harness
如果需要在测试里读取/修改应用状态,用new_ui_state:
use egui_kittest::{Harness, kittest::Queryable}; let mut checked = false; let mut harness = Harness::new_ui_state(|ui, checked| { ui.checkbox(checked, "Check me!"); }, checked); harness.get_by_label("Check me!").click(); harness.run(); assert_eq!(*harness.state(), true);Harness<'a, State>自带泛型状态,通过state()/state_mut()/into_state()访问(lib.rs);源码注释也指出:多数情况下状态直接闭包捕获即可,状态参数主要服务于「创建 Harness 之后仍需访问状态」的场景。
三、用 HarnessBuilder 定制测试环境
Harness::builder()返回HarnessBuilder,其默认值与可配置项在 builder.rs 中一目了然:
| 配置项 | 默认值 | 说明 |
|---|---|---|
| 窗口尺寸 | 800.0 × 600.0 | 通过with_size设置 |
pixels_per_point | 1.0 | 通过with_pixels_per_point设置 |
| 主题 | Theme::Dark | with_theme可切浅色 |
| 模拟 OS | OperatingSystem::Nix | with_os影响快捷键等显示,快照测试建议固定,避免换机器就变 |
max_steps | 4 | run()最多推进的帧数,配合默认step_dt约合 1 秒模拟 |
step_dt | 1.0 / 4.0 | 单步模拟时间增量,默认偏低以免空等动画 |
wait_for_pending_images | true | 等待异步图片加载完成(has_pending_images) |
| 渲染器 | LazyRenderer | 未启用 wgpu 时惰性初始化;启用则默认WgpuTestRenderer |
用法示例(来自 tests.rs):
let mut harness = Harness::builder() .with_size(Vec2::new(60.0, 120.0)) .build_ui(|ui| { egui_extras::install_image_loaders(ui.ctx()); ui.add_sized(Vec2::splat(30.0), egui::Image::new("https://.../icon.png")); }); harness.snapshot("should_wait_for_images");常用 Builder 方法还包括:with_render_options(默认egui_wgpu::RendererOptions::PREDICTABLE,追求可预测渲染)、wgpu()/wgpu_setup(...)、renderer(...)(自定义TestRenderer)、with_options(设置快照默认参数)与build_eframe(在eframe特性下测试完整的eframe::App)。
测试 eframe::App
启用eframe特性后(Cargo.toml),可以用Harness::new_eframe或builder().build_eframe(...)直接驱动实现了eframe::App的完整应用(lib.rs),这覆盖了logic与ui两个生命周期回调(app_kind.rs)。
四、模拟交互 API 全集
Harness与Node提供了丰富的输入注入手段,均以「排队 egui 事件,下次step/run时消费」的方式工作(事件队列实现在 node.rs)。
键盘(lib.rs):
key_down(key)/key_up(key):单独的按下/抬起事件key_press(key):按下 + 抬起key_combination(&[Key]):按顺序按下全部键再逆序抬起key_press_modifiers(Modifiers::COMMAND, key)/key_combination_modifiers(...):自动完成「按下修饰键 → 按键 → 释放修饰键」序列
鼠标指针(lib.rs):
hover_at(pos):移动鼠标到坐标drag_at(pos)/drop_at(pos):按下/释放主键,模拟拖拽remove_cursor():触发PointerGone。点击按钮后立刻快照,按钮会以 hover 态出现;不想看到悬浮态就调用它(对应测试 test_remove_cursor)
节点级交互(node.rs):
click()/click_secondary()/click_modifiers(Modifiers):在节点中心点击,支持修饰键click_accesskit():直接发accesskit::Action::Click,对不可见控件也有效(区别于像素级click())focus():发 Focus 动作;type_text(&str):注入文本输入scroll_to_me()/scroll_down()/scroll_up()/scroll_left()/scroll_right():滚动包含该节点的ScrollArea(对应测试 test_scroll_to_me 验证了先滚动后点击隐藏按钮)
测试 test_modifiers 展示了完整的修饰键组合场景(Cmd+点击、Cmd+Z、Cmd+Y)。
定时与步进控制
step():跑一帧(消费已排队事件);run_steps(n):跑 n 帧run()/try_run():跑到稳定;try_run()在超出max_steps时返回ExceededMaxStepsError(带steps_to_settle与repaint_causes诊断信息,lib.rs)try_run_realtime():每步真实 sleepstep_dt,适合等待异步操作(如网络图片加载)run_ok():超出上限只返回None不 panic- 注意:构造 Harness 时会禁用光标闪烁与滚动动画(lib.rs),保证快照稳定
五、kittest.toml:全局测试配置
在 workspace 根目录放置kittest.toml即可配置测试参数。配置加载逻辑见 config.rs:从当前工作目录(测试运行时通常是 crate 根)逐级向上查找kittest.toml,找到即解析,未找到则使用默认值,且以进程内单例(LazyLock)缓存。
README 给出的完整配置及默认值:
# 快照输出目录 output_path = "tests/snapshots" # 两个对应像素之间允许的最大加权平方 YIQ 颜色距离 # (逐像素的颜色容差,作用于每一对像素本身) threshold = 0.6 # 允许超过 threshold 的像素个数,超过则测试失败 # (绝对像素数,不是图像面积的百分比) max_failed_pixels = 0 # 超过 max_steps 后 Harness::run 还会继续步进多少步, # 用于报告 UI 本应需要多少步才能稳定 diagnostic_max_steps = 100 [windows] threshold = 0.6 max_failed_pixels = 0 [macos] threshold = 0.6 max_failed_pixels = 0 [linux] threshold = 0.6 max_failed_pixels = 0细节说明(均有源码依据):
- 顶层节名实际为
windows、mac、linux(config.rs),README 示例中[macos]是文档写法,仓库根目录的真实 kittest.toml 用的是[mac];若两个节同时出现,以源码解析为准。 - 各 OS 节的
threshold/max_failed_pixels为Option,未配置时回退到顶层值(config.rs)。 - 旧键名
failed_pixel_count_threshold仍然兼容但会打印弃用警告(config.rs),新代码请使用max_failed_pixels。 - 解析失败会直接
panic!("Failed to parse ..."),未知键因deny_unknown_fields也会报错(config.rs)。
仓库自身的 kittest.toml 是一个实战参考:它把 macOS 的threshold定为 0.6(CI 事实基准),其他 OS 放宽到 2.0,让本地开发机也能跑快照而不被微小的渲染差异误伤。
max_failed_pixels 的使用红线
README 特别警告:max_failed_pixels只应极谨慎地调高——超过约 10 就足以掩盖真实变化,例如分隔线被移动、单像素边框发生偏移、小图标渲染错误。应优先通过threshold控制灵敏度,并始终选用「恰好能让测试通过的最小值」;每次更新快照后都要重新评估该值。
六、快照测试:从渲染像素到版本化图像
开启方式与文件产物
启用snapshot与wgpu两个特性后(Cargo.toml),即可调用Harness::snapshot(name):渲染当前帧并保存到tests/snapshots目录。渲染器默认是LazyRenderer,首次调用render()时才真正初始化 wgpu(renderer.rs)。
一次快照比较产生的文件(snapshot.rs):
{name}.png:提交到版本库的基准快照{name}.new.png:本次测试渲染出的新图像{name}.diff.png:比对失败时生成的差异图{name}.old.png:更新快照时被替换掉的旧图备份
仓库根目录的 .gitignore 已经包含了 README 建议的三条规则:
**/tests/snapshots/**/*.diff.png **/tests/snapshots/**/*.new.png **/tests/snapshots/**/*.old.pngUPDATE_SNAPSHOTS 环境变量
| 取值 | 行为 |
|---|---|
未设置 /false/0/no/off | 正常比对,失败则测试失败 |
true/1/yes/on | 只更新失败的测试,且让这些测试通过(便于批量更新而不被第一个失败 crate 打断) |
force | 更新全部快照,连误差容忍范围内的也一并覆盖(此时比较阈值强制为 0.0,snapshot.rs) |
用法:UPDATE_SNAPSHOTS=true cargo test。模式解析实现在 snapshot.rs。若快照缺失(新测试),UPDATE_SNAPSHOTS开启时会直接写基准文件,否则报Missing snapshot并提示更新命令(snapshot.rs)。
SnapshotOptions 与 OsThreshold:逐测试/逐 OS 调参
SnapshotOptions包含threshold、max_failed_pixels、output_path三个字段,默认值全部来自kittest.toml(snapshot.rs)。它支持链式调用:
use egui_kittest::{OsThreshold, SnapshotOptions}; let options = SnapshotOptions::new() .threshold(OsThreshold::new(0.0).windows(10.0)) // Windows 放宽阈值 .max_failed_pixels(OsThreshold::new(0).windows(10).macos(53));OsThreshold<T>的threshold()依据当前编译目标选择对应值,非 Windows/macOS/Linux 走fallback(snapshot.rs)。
egui 仓库内的真实用法(demo_app_windows.rs):为「Bézier Curve」demo 单独设置 Linux 阈值 2.1,其余保持默认:
let mut options = SnapshotOptions::default(); if name == "Bézier Curve" { options = options.threshold(OsThreshold::new(0.0_f32).linux(2.1)); } results.add(harness.try_snapshot_options(format!("demos/{name}"), &options));多个快照收集为 SnapshotResults
同一个测试里有多张快照时,用SnapshotResults统一收集,保证能一次性全部更新(否则多个未被处理的SnapshotResults会被 Drop 检查直接 panic,snapshot.rs):
let mut results = SnapshotResults::new(); results.add(harness.try_snapshot("test_scroll_initial")); // ...交互... results.add(harness.try_snapshot("test_scroll_scrolled")); // 离开作用域时若存在错误会自动 panic还可以通过extend/extend_harness合并多个 Harness 的结果,或用into_result()/into_inner()转为普通Result/Vec<SnapshotError>自行处理。
其他快照辅助手段
Harness::mask(rect):用品红色矩形盖住不稳定区域(如时间戳),在run()之后、snapshot()之前调用(lib.rs;测试见 test_masking)Harness::render():不比较、直接返回当前帧的RgbaImage,可用于自定义断言(同一帧多次调用会命中last_render缓存,避免重复执行 paint callback,lib.rs)debug_open_snapshot()(已废弃,仅调试用):把快照写到临时文件并用系统看图软件打开spawn_eframe_app()(已废弃,仅调试用):弹出一个真实窗口手动排查失败用例;macOS 上必须主线程调用,需要把测试 target 设为harness = false并自写main(lib.rs)
七、编写快照测试的官方准则
README 给出三条明确建议,值得作为团队规范:
- 能用普通 Rust 测试或
insta快照测试就别用图像比对,因为图像测试:- 相对较慢;
- 脆弱——无关副作用(比如一次配色调整)也会导致失败;
- 图像文件占用仓库体积。
- 图像本身的要求:
- 应提交进仓库或以其他方式可获取(egui 用 git LFS 存放此类文件);
- 只应精确呈现被测内容,不要夹带无关元素;
- 保持低分辨率,控制仓库体积增长;
- 保持低比对阈值,防止「虽然不想要差异却照样通过」(默认阈值对大多数用例已经足够)。
- 调参要克制:优先调
threshold,max_failed_pixels仅作为最后手段。
八、跨平台渲染差异:成因与排查
默认容差对绝大多数 GUI 比对测试是够用的。但使用自定义渲染时,不同机器可能产出不同图像。排查顺序:
- 先确认差异是否源于渲染特性集合不同(硬件/软件渲染器的能力差异)。应当尽量在所有测试运行中强制同一套特性集,但意外仍可能发生。
- 确认差异微小且难以避免后,才考虑谨慎调整该测试的
SnapshotOptions::threshold,最后才是max_failed_pixels(参见 egui 仓库 issue #5683)。 - ⚠️ 容差调太高可能掩盖真实失败。调整后应手动验证测试在正确的失败条件下仍然会失败。
差异的技术根源
差异可能来自 GPU、操作系统、渲染后端(Metal/Vulkan/DX12 等)甚至同一驱动的不同版本。README 列举的常见成因包括:
- 多重采样抗锯齿(MSAA):采样点布局与 resolve 步骤是实现定义的;alpha-to-coverage 的算法/模式在不同实现间差异很大
- 纹理过滤:即使是简单的线性纹理过滤,不同实现也可能应用不同优化
- 越界纹理访问(
textureLoad):实现可以自由返回不确定值而不是 clamp - 浮点求值(详见 WGSL 规范 §15.7):舍入模式可能不一致;浮点运算可能被「优化」到改变结果;非规格化浮点数(denormal)可能被刷成零;本应产生
NaN/Inf的结果可能得到不确定值;内置函数(三角函数等)的精度与错误处理各异 - 偏导数(
dpdx/dpdy):实现可以自由选择dpdxFine或dpdxCoarse
由此得出几条实践建议(是否采用取决于你的渲染设置是否允许):
- 除非明确在测试 MSAA,否则不要启用多重采样抗锯齿
- 不要依赖
NaN、Inf和非规格化浮点值 - 为纹理采样场景考虑专用测试路径
- 优先使用显式偏导数函数
九、在项目中使用 egui_kittest
在Cargo.toml中加入:
[dev-dependencies] egui_kittest = { version = "0", features = ["snapshot", "wgpu"] } # 需要测试完整 eframe::App 时再启用: # features = ["snapshot", "wgpu", "eframe"]特性说明(Cargo.toml):wgpu提供基于 wgpu 的测试渲染器;snapshot提供基于 dify 的图像快照工具;eframe允许测试eframe::App;x11仅用于 Linux 下--all-features编译通过。快照 PNG 建议用 git LFS 管理,并在.gitignore中加入*.diff.png/*.new.png/*.old.png。
仓库中的 egui_kittest 测试集 与 egui_demo_lib 的 demo 快照测试(对应tests/snapshots/demos下 40 张 PNG)是现成的最佳实践范本:从控件交互、修饰键、滚动、IME 组合文本到多快照收集与逐 OS 阈值覆盖,均可在这些文件中找到可直接迁移的代码模式。
总结
egui_kittest把「即时模式 UI 不可直接操作」的痛点转化为一套基于 AccessKit 无障碍树的查询/交互 API,配合 wgpu 离屏渲染实现像素级快照回归。本文覆盖了从最小用例、Builder 定制、事件模拟、kittest.toml配置到快照更新工作流与跨平台差异治理的完整链路。为你的 egui 项目引入它时,记住三条主线:能用逻辑断言就用逻辑断言,快照只测视觉、图像只含被测内容,容差参数宁小勿大——这能让测试既稳定又敏感。
【免费下载链接】eguiegui: an easy-to-use immediate mode GUI in Rust that runs on both web and native项目地址: https://gitcode.com/GitHub_Trending/eg/egui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考