测试
界面测试大部分不需要开窗口:ui/internal/uitest 测试工具通过 Gio 真实的输入路由送入点击、按键、文字,组件走的代码和真实窗口完全一样。go test -race ./... 在本机跑完大约 10 秒。
交互测试
ui/ 下的模块都可以引用它(它在 internal 里,外部项目引用不到):
func TestInputSubmit(t *testing.T) {
var got string
f := Input("").OnSubmit(func(s string) { got = s }) // 在 package kit 里
h := uitest.New(el.Root(f)) // 布局一帧
h.Click(20, 10) // 点击获得焦点
h.Type("你好") // 输入文字
h.Key("⏎", 0) // 回车
if got != "你好" {
t.Fatalf("OnSubmit saw %q", got)
}
}
| 函数 | 作用 |
|---|---|
uitest.New(w) |
把组件放在左上角 (0,0) 布局一帧 |
uitest.NewFunc(fn) |
用任意帧函数,比如 window 包的测试用 w.layout 带上根视图和快捷键 |
h.Click(x, y) |
在 (x, y) 按下并松开左键,然后画一帧 |
h.Key(name, mods) |
按下并松开一个键,然后画一帧。name 是 Gio 的键名:字母大写 "A",回车 "⏎",也可以用 key.NameReturn 等常量 |
h.Type(s) |
向有焦点的输入框插入文字,然后画一帧 |
h.Frame() |
执行 core.Update 队列并画一帧 |
坐标规则:测试视口是 400×300,1dp = 1 像素。uitest.New 不带根视图,组件从 (0,0) 开始;用窗口布局测试时,内容从 (24,24) 开始。
每个 h.Click 等操作后面自带一帧,但回调修改了状态、要看重绘结果时,多调一次 h.Frame()。
模块边界测试
internal/deps 检查每个模块只引用了允许的包(见架构 · 模块):
- 下层模块引用了上层(比如
el引用kit),同层互相引用,或者native/*引用了ui、Gio,测试失败; - 新增了模块目录却没在允许表里登记,测试失败。
它随 go test ./... 一起运行。
什么该测
- 回调触发次数:点一次触发一次,禁用时不触发。
- 回调参数:
OnChange收到的是新值。 - 程序调用和用户操作的区别:
SetValue不触发OnChange,打字会触发。 - 解析类函数(快捷键字符串):合法输入和每一种非法输入。
native包:参数校验路径(不需要权限,也不会真的动鼠标)。
Agent 端到端测试
cmd/keel-mcp 让 Agent 通过 MCP 驱动应用:读元素、点击、输入、滚动、截图。应用在内存里渲染,不弹窗口。用法见 Agent 端到端测试。
仓库里有两层相关测试,都随 go test ./... 运行:
ui/window/automation_test.go:进程内测试自动化模式本身,包括滚动、Tab 移动焦点、回调里关闭窗口、禁用和勾选状态的报告。cmd/keel-mcp/main_test.go:以 MCP 客户端身份启动keel-mcp,把 multiwindow 示例完整走一遍,并测试连接用户自己启动的应用(attach)。改了协议、工具或组件的语义信息后,它会第一个失败。
截图对比
原生窗口生命周期和初始位置用桌面测试验证:
KEEL_DESKTOP=1 go test ./ui/window -run TestRealWindows -count=1 -v
2026-10-03 在 macOS 实际运行通过:多窗口置前/关闭、立即关闭后重开,以及普通/无边框窗口按屏幕可用区域居中。位置测试还移动窗口并请求后续帧,确认不会被重新居中。此结果未覆盖多显示器切换、Windows/Linux 的窗口管理器及系统偏好通知。
改了主题、间距、字体后,渲染前后截图,确认只有预期的地方变了:
go run ./examples/hello -screenshot /tmp/before.png
# 修改代码
go run ./examples/hello -screenshot /tmp/after.png
cmp /tmp/before.png /tmp/after.png && echo 完全一致
纯重构应该输出"完全一致"。截图由 GPU 离屏渲染,同一台机器上结果稳定;不同机器、不同系统字体下像素可能不同,不要把截图提交成跨机器的基准文件。
真实窗口测试
有些 bug 只在真实窗口里出现,比如多窗口之间的死锁。ui/window/desktop_test.go 在子进程里运行 ui/window/testdata/raise:打开两个窗口,在界面代码里(持有帧锁)调用 Raise、Close,15 秒内没走完就判定为死锁。
KEEL_DESKTOP=1 go test -run RealWindows ./ui/window
默认跳过,因为它会在屏幕上弹出窗口,而且需要图形界面环境。改动 ui/window、ui/internal/loop 里和窗口、锁相关的代码后必须跑一次。新增窗口方法时,把它加进 testdata/raise 的步骤里。
需要手动验证的部分
无界面测试覆盖不到这些,改动相关代码后手动跑一遍:
| 场景 | 怎么验证 |
|---|---|
| 真实窗口的打开、关闭、置前 | go run ./examples/multiwindow,点"打开设置窗口"两次,应该只有一个设置窗口 |
| 最后一个窗口关闭后退出 | 关掉所有窗口,终端里进程应结束 |
| 窗口快捷键 | 在主窗口按 ⌘+, |
| 全局快捷键 | go run ./examples/hotkey,切到别的应用按 ⌘⇧K,计数增加 |
后台 core.Update |
hotkey 示例里的时钟每秒走 |
| 权限、截图、合成输入 | 需要授权,按 原生能力 的说明手动测 |
| 中文输入法 | 在输入框里用拼音输入,候选框位置正确,上屏后内容正确 |
全组件截图矩阵
go run ./examples/components -matrix /tmp/keel-component-matrix
# 单个组件:窄窗口、1×、深色
go run ./examples/components -section button -width 320 -scale 1 -theme dark -screenshot /tmp/button.png
矩阵按已注册的每个组件生成浅 / 深色、320 / 680dp 宽、1× / 2× 共八张首帧截图,并输出可浏览的 index.html。每个案例重新构造组件,避免上一个案例的布局缓存影响结果。大图通过索引中的图片链接打开;截图只包含当前视口,滚动后的内容和浮层仍需交互测试。
window.Screenshot 保持默认 2×;需要其他缩放时用 ScreenshotAtScale。尺寸以 dp 表示,PNG 尺寸按缩放四舍五入到物理像素。非法尺寸或缩放返回错误。
组件示例中的固定展示宽度必须同时设置 MaxW(el.Full);否则测试到的是被裁掉的宽画布,而非组件的窄布局。并列操作用 Wrap,保留需要验证横向滚动的内容宽度。Sidebar 示例在 600dp 以下初始折叠,仍可手动展开;Dock 的多列工作区需要足够宽度,不把 320dp 截图当成手机布局承诺。
cmd/keel-mcp 的示例级回归包括:订单筛选、详情和删除、新建订单纯键盘流程;聊天流式输出、代码/表格和中断;设置页主题、搜索和语言切换;Dock 移动面板、切换后保留输入、菜单 Esc 与关闭面板。TestOrdersKeyboardOnly 不点击控件,从 Mod+N 开始用 Tab、方向键、Space、Enter 完成保存。