ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GPUI Kit 集成测试实战:用 `[gpui_kit::test]` 驱动无头窗口验证真实 UI 交互

GPUI Kit 集成测试实战:用 `[gpui_kit::test]` 驱动无头窗口验证真实 UI 交互 GPUI Kit 集成测试实战用#[gpui_kit::test]驱动无头窗口验证真实 UI 交互【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit本文基于 GPUI KitRust 桌面 GUI 框架官方测试文档系统讲解如何编写 UI 集成测试在无头窗口中渲染真实组件、派发点击/键盘/滚动事件再断言状态、焦点、布局与业务回调。你将学会选择正确的测试层级、配置test-support依赖、使用TestWindowExt查询与交互 API、处理帧与异步任务并理解ElementSnapshot断言边界最终能够为应用的生产视图编写可运行、可维护的集成测试。UI 集成测试定义与价值GPUI Kit 的 UI 集成测试UI integration testing在无头窗口headless window中渲染真实组件派发真实的点击、键盘输入与滚动事件然后检查状态、焦点、布局和拥有者的回调。它的典型场景是为 Checkbox 编写一个 UI 集成测试证明点击会切换拥有者的值而禁用状态下控件拒绝该交互。使用术语 component interaction coverage组件交互覆盖率来描述此类覆盖范围。测试入口是#[gpui_kit::test]宏——它运行测试并提供 GPUI 上下文而gpui_kit::test模块负责操作并检视其 UI。这一定位在 crates/kit/src/test.rs 的模块文档中有明确说明Render real components in a headless window, simulate clicks, keyboard input and scrolling, then verify state, focus, layout and application callbacks且该模块不检查渲染出的像素——像素级验证需要 GPUI 的独立渲染器见下文断言边界一节。选择正确的测试层级不是所有测试都需要 UI 集成测试。根据被测对象选择层级层级工具适用场景纯逻辑普通 Rust#[test]不涉及窗口、视图、订阅的纯函数逻辑实体/订阅/动作/异步任务#[gpui_kit::test]TestAppContextentities、subscriptions、actions、async tasks它也能创建无头窗口现有 GPUI 窗口辅助VisualTestContext使用既有 GPUI window helpers 的场景应用 UI 流程gpui_kit::test::TestWindowExt作用于真实Window断言由原生事件产生的行为导入规范与#[test]遮蔽陷阱显式导入用到的 Kit 类型并编写#[gpui_kit::test]。不要在测试模块中使用use gpui_kit::*;启用test-support后这个 glob 会带入 GPUI 的test宏可能遮蔽 Rust 内置的#[test]。这一点在 crates/kit/src/lib.rs 的源码注释中有明确警告Test modules should import their Kit types explicitly to avoid shadowing Rusts #[test]。将test-support加入应用gpui-kit的 dev-dependency并且必须与普通依赖使用完全相同的源码与版本。辅助 API 需要包含它们的 Kit 版本在假设某个旧发布版支持这些 API 之前先检查已安装的 API。不需要额外的测试 crate也不需要打 GPUI 补丁。独立测试包配置示例对于紧挨着 Kit 检出目录的独立测试包standalone test package[package] name ui-tests version 0.1.0 edition 2024 publish false [dev-dependencies] gpui-kit { path ../gpui-kit/crates/kit, features [test-support] }无头测试仍然需要平台的原生构建依赖headless tests still need the platforms native build dependencies。首次创建包后运行一次cargo generate-lockfile然后执行cargo test --test ui --locked在 Kit 仓库内部等价命令是cargo test -p gpui-kit --features test-support --test ui --locked该命令的配置依据位于 crates/kit/Cargo.toml[[test]] name ui声明了required-features [test-support, component]。test-supportfeature 的定义见 crates/kit/Cargo.toml它传递启用了gpui/test-support、gpui_platform/test-support、gpui-base/test-support与gpui-component?/test-support。UI 集成测试工作流一个完整的 UI 集成测试遵循以下六步导入生产视图与构造函数它们来自应用库application library。状态实体与订阅由该视图持有。仅在自包含示例或刻意构造 fixture 时才在测试内部定义视图。初始化与挂载用cx.update(gpui_kit::init)初始化以显式尺寸打开无头窗口将组件应用包裹在Root中。当流程使用到 overlay 时挂载Root::render_dialog_layer、render_sheet_layer与render_notification_layer子节点——仅构造Root不会自动挂载它们。渲染一帧并定位已有控件 ID。自定义原生元素通过TestSupportExt用.id(status).test_support()选择参与观察该调用必须放在.track_focus(handle)之前自定义包装器必须把track_focus转发给内部元素。注册不增加任何布局容器Registration adds no layout container。通过TestWindowExt派发点击、输入与命名按键。务必走真实的 handler 路径——直接调用回调或直接设置控件状态会绕过被测行为。断言初始状态、每个有意义动作后的可见结果、以及应用结果。包含一个相关的反面用例negative case例如禁用控件拒绝交互、或非法输入阻止保存。运行受影响的测试目标并报告结果。未编译的示例或从不检查动作结果的测试都不能算作完成证据。完整示例通过 UI 保存 Profile把下面的代码复制到tests/ui.rs。它就是 crates/kit/tests/ui.rs 中的 Kit 集成测试示例内联视图使这份参考在无仓库检出时也可用。在真实应用中请把该视图定义替换为对生产视图的导入。use gpui_kit::test::{TestSupportExt, TestWindowExt}; use gpui_kit::{ AppContext, Context, Entity, SharedString, TestAppContext, Window, component::{ Root, button::Button, input::{Input, InputState}, }, div, prelude::*, px, size, }; struct Profile { name: EntityInputState, submitted: OptionSharedString, } impl Render for Profile { fn render(mut self, _: mut Window, cx: mut ContextSelf) - impl IntoElement { let status self.submitted.as_ref().map_or_else( || SharedString::from(Not saved), |name| SharedString::from(format!(Saved: {name})), ); div() .size_full() .flex() .flex_col() .p_4() .gap_4() .child(Input::new(self.name).id(name).w(px(240.))) .child( Button::new(save) .label(Save) .on_click(cx.listener(|this, _, _, cx| { this.submitted Some(this.name.read(cx).value()); cx.notify(); })), ) .child( div() .id(status) .role(gpui_kit::Role::Status) .test_support() .aria_label(status.clone()) .child(status), ) } } #[gpui_kit::test] fn saves_a_profile_through_the_ui(cx: mut TestAppContext) { cx.update(gpui_kit::init); let mut profile None; let handle cx.open_window(size(px(640.), px(480.)), |window, cx| { let view cx.new(|cx| Profile { name: cx.new(|cx| InputState::new(window, cx)), submitted: None, }); profile Some(view.clone()); Root::new(view, window, cx) }); let profile profile.unwrap(); cx.update_window(handle.into(), |_, window, cx| { window.render_frame(cx); assert_eq!(window.find(status).label(), Some(Not saved)); window.click(name, cx); window.input(Ada 中文, cx); let name window.find(name); assert_eq!(name.focused(), Some(true)); assert_eq!(name.value(), Some(Ada 中文)); assert!(name.bounds().size.width px(0.)); // Named keys share the same Window API and refresh the resulting frame. window.press(backspace, cx); assert_eq!(window.find(name).value(), Some(Ada 中)); window.click(save, cx); let status window.find(status); assert!(status.visible()); assert_eq!(status.label(), Some(Saved: Ada 中)); assert!(status.bounds().top() window.find(save).bounds().bottom()); }) .unwrap(); // Verify the application result as well as the native properties. cx.update(|cx| { assert_eq!(profile.read(cx).submitted.as_deref(), Some(Ada 中)); }); }这个示例体现了三个关键实践中文/Unicode 输入window.input(Ada 中文, cx)证明input按字符char逐个派发 Unicode 字符见 crates/kit/src/test.rs 的input_text实现——每个字符被解析为 GPUI keystroke 并逐个dispatch_keystroke。命名按键window.press(backspace, cx)通过Keystroke::parse解析命名键后派发并在前后各渲染一帧刷新结果见 crates/kit/src/test.rs。原生属性 应用结果双重断言既断言label()、focused()、value()等原生无障碍属性又回到TestAppContext层断言profile.submitted的最终值——这呼应了原生属性也可能出错的边界原则。查询与交互 API从gpui_kit::test导入TestWindowExt自定义注册场景再导入TestSupportExt。使用普通 Rust 的assert!/assert_eq!配合快照snapshots即可。API行为window.find(id)要求唯一的已观察目标出错时列出已注册路径。window.try_find(id)目标不存在时返回NoneID 存在歧义时仍然 panic。window.within(id)解析一个原生 GPUI 身份作用域identity scope包括未被观察的祖先。click、right_click、double_click、hover在目标中心派发真实指针事件。click_at(id, offset, cx)使用相对目标 bounds 左上角的偏移量。scroll(id, delta, cx)派发 GPUIScrollDelta滚轮事件。drag_to(from_id, to_id, cx)在当前作用域内于两个目标中心之间拖拽。window.drag(from, to, cx)在窗口局部坐标点之间拖拽跨作用域或精确几何用bounds()。press(key, cx)发送命名 GPUI 按键如backspace、escape或secondary-a。input(text, cx)在当前焦点处输入 Unicode 字符先点击输入框。这些 API 的签名定义见 crates/kit/src/test.rs 的TestWindowExttrait。底层实现要点从 crates/kit/src/test.rs 的源码可以看出真实事件意味着click系列通过MouseDownEvent/MouseUpEvent构造平台输入to_platform_input()派发点击次数click_count从 1 递增到目标次数——double_click就是派发两次完整的 down/up 序列。每次事件派发后都会调用window.render_frame(cx)保证快照与事件结果同步。drag在按下左键后分 8 步插值移动指针from.x (to.x - from.x) * step / 8.再在终点松开模拟真实的拖拽轨迹。press与input都通过Keystroke::parse解析input还会把key_char设置为该字符的字符串形式。作用域查询Scoped queriesGPUI 的 ID 只需要在原生作用域native scope内唯一。对重复的局部 ID使用window.within(dialog).find(name)。作用域窗口ScopedWindow同样提供指针操作、press与input键盘辅助函数要求该作用域内存在被观察的焦点并在每个字符之间重新检查焦点。它们不会自动聚焦目标也不会替换其值。对于包含另一个popup-menuID 的子菜单在打开子菜单前先保留已解析的外层作用域再查询其后代。作用域机制的实现在 crates/kit/src/test.rs 的ScopedWindow中它只持有一组ElementId路径前缀scope不引入新元素或布局容器。within通过 crates/base/src/test_support.rs 的scope函数解析——即使只有后代被观察也能反推出祖先路径Resolves a scope even when only its descendants are observed。真实的测试用例 crates/kit/tests/interactions.rs 验证了这一行为window.within(toolbar).find(save)与window.within(dialog).within(footer).click(save, cx)证明作用域查询遵循既有 GPUI 路径且不需要观察容器本身。帧与异步工作快照Snapshots是上一已完成帧的所有权事实owned facts不是实时元素。每次交互后必须重新获取快照。交互辅助函数会自动刷新帧但在外部实体更新、焦点变化或窗口尺寸调整之后查询前需要调用window.render_frame(cx)。其实现见 crates/kit/src/test.rsrender_frame先refresh()再draw(cx).clear(cx)完成一帧。对于延迟动作deferred actions、订阅或弹窗更新需要退出update_window让 GPUI 处理其副作用。然后用cx.run_until_parked()处理排队的工作或导入TestAppContextExt等待一个有界条件cx.wait_for(handle.into(), std::time::Duration::from_secs(1), |window, _| { window.try_find(dialog).is_none() }).await;wait_for在借用窗口更新之外运行并使用 GPUI 测试时钟。其实现见 crates/kit/src/test.rs每 10msGPUI 测试时间刷新一帧并求值一次谓词超时则 panic 并附带已注册路径。关键陷阱在改变某个动作要校验的输入之前先处理排队中的该动作否则它可能读到的是后续的输入而不是预期测试步骤那一刻的值。对于定时器timers在截止时间之前和到期之后都要检查 UI。GPUI 的非同步Animation使用墙钟时间wall-clock time——推进测试时钟并不会让它完成。当受支持时使用 reduced motion或在断言最终几何前为生产动画采用合适的有界等待bounded wait。断言边界哪些属性可信任、哪些不能ElementSnapshot的全部字段定义见 crates/base/src/test_support.rs访问器则集中在该文件 crates/base/src/test_support.rs。以下是各断言的语义边界checked()、selected()、expanded()、value()与label()来自原生无障碍属性native accessibility properties。None表示不可用掩码输入masked input会刻意省略其值。不要为此再提供一套仅测试用的状态。disabled()目前只报告Some(true)或None。要证明控件可用请执行交互并断言结果——缺少标志并不等于控件接受输入。focused()度量被观察的原生焦点作用域。当原生元素声明Action::Focus但绑定缺失时会被诊断出来源码中对应的断言是focus binding was not observed ... use .test_support().track_focus(handle)见 crates/base/src/test_support.rs没有该 action 的控件仍可能返回None。请注册真实的绑定。bounds()支持包含containment、相对位置、重叠、滚动与拖拽断言。visible()检查几何、裁剪与观察到的样式它不能证明目标未被遮挡——真实命中测试hit testing才决定一次点击是否到达控件。原生属性本身也可能出错。将它们与真实交互、几何和应用结果配对使用。标签不是渲染出的文本正确的无障碍值也不等于正确的像素。这些测试在进程内运行。像素比较需要 GPUI 的独立渲染器打包应用行为与完整 OS IME 合成需要其他覆盖。Kit 的显式 macOS 渲染测试目标是cargo test -p gpui-kit --features test-support --test rendering --locked关于渲染测试的性质见 crates/kit/tests/rendering.rs 的说明这些测试需要 GPUI 的 Metal 渲染器其他平台仍运行原生事件/状态套件不会为缺失的 GPU 支持替换假渲染器。渲染测试本身演示了原生属性无法发现渲染缺陷即使checked()返回Some(true)一个缺少 check-mark 路径的 SVG 资产也会被像素比较捕获crates/kit/tests/rendering.rs同理value()正确但文字被设置为透明色的缺陷也只能靠像素发现crates/kit/tests/rendering.rs。观察机制test_support()做了什么自定义控件要参与断言需要调用.test_support()它来自gpui_kit::test::TestSupportExt在 crates/kit/src/lib.rs 以pub use gpui_base::TestSupportExt;重新导出。其底层是 crates/base/src/test_support.rs 的ObservedE透明转发元素test_support()要求元素已有 ElementIdtest_support requires an existing ElementId。在prepaint阶段它读取原生 accessibility 节点gpui::accesskit::Node的toggled、is_selected、is_expanded、value、is_disabled、label与supports_action组装成ElementSnapshot。注册表按窗口隔离以ArcWindowTextSystem指针为键用弱引用存放注册项随 GPUI 元素状态的生命周期自动清理。在paint阶段更新visible由 visibility、opacity 与裁剪后尺寸决定与focused通过FocusHandle::contains_focused。track_focus会被转发给内部元素见 crates/base/src/test_support.rs这正是文档要求自定义包装器必须转发该方法的原因。这套观察完全构建在公开的 GPUI API 之上Opt-in headless observation built exclusively on public GPUI APIs因此不依赖任何测试专用状态注入。测试组织Kit 仓库中的真实布局在 Kit 仓库内部UI 测试按主题拆分为多个[[test]]目标均声明required-features [test-support]必要时加component见 crates/kit/Cargo.tomlui、controls、interactions、disclosure、overlays、menu、collections、date_picker、dock、search、window、lifecycle、components、input等。rendering目标则特殊声明test false; harness false以自定义 main 运行crates/kit/Cargo.toml。这套测试清单与文档中的测试分层完全对应普通#[test]负责纯逻辑#[gpui_kit::test]负责实体与异步任务TestWindowExt负责应用级 UI 流程。深入阅读GPUI test examples——实体与上下文模式。GPUI test reference——重入re-entrancy与属性测试。Kit 测试模块源码——TestWindowExt、ScopedWindow、TestAppContextExt的完整实现。观察机制源码——ElementSnapshot、Observed与注册表。Kit 集成测试目标、交互测试、渲染测试——可直接对照的真实用例。Kit Cargo.toml——test-supportfeature 与各测试目标的依赖声明。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表