- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
导读
本文围绕 MooTool 下一代跨平台版本(next-compose)中 JSON 工具模块的JsonPathPickerDialog弹层重构展开:为兼顾可测试性与「使用此路径」仅选路径、不自动查询的交互语义(DIFF-319),项目将原本内联在JsonScreen中的路径选择对话框提取为独立 Compose 组件,并通过语义化contentDescription、会话状态封装与桌面 Compose UI 测试,把「使用」与「双击选路径」两条行为链路固化为可回归的自动化用例。读完本文,你将掌握该组件从提取、状态驱动、无障碍对齐到交互单测与场景帧证据的完整实现思路,可直接迁移到自身 Compose 桌面项目的弹层测试与行为封装中。
一、背景:为什么要把PathPickerDialog从JsonScreen中提取出来
在重构之前,JSONPath 选择弹层(PathPickerDialog)是直接内联在JsonScreen里的。随着功能演进,它暴露出两个问题:
- 体量过大不利于单测:弹层既承担路径列表加载、选中态维护,又要处理预览与确认/取消逻辑,内联在 2200+ 行的
JsonScreen中(见 JsonScreen.kt)很难被 Compose 测试独立挂载和驱动; - 交互语义需要回归保护:Electron 版
JsonPathPicker的「使用」与双击共用onChoose(path)——只把路径写入输入框并关闭弹层,不触发 JSONPath 查询、不把节点预览写进结果区。Compose 端若在此处行为漂移,会导致 DIFF-319「仅选路径、不自动查询」的回归。
因此 DIFF-457 的核心动作是把弹层提取为独立组件,让回归验证落到可重复执行的单测上。
二、组件提取:JsonPathPickerDialog的结构与状态驱动
2.1 新增独立组件文件
新增 JsonPathPickerDialog.kt,对外暴露一个internal的 Composable:
@Composable internal fun JsonPathPickerDialog(container: AppContainer, session: JsonSession, onChanged: () -> Unit)三个入参各有明确职责:
| 参数 | 类型 | 职责 |
|---|---|---|
container | AppContainer | 提供 i18n 翻译(container.t(key, params))与全局能力,弹层内所有文案统一走翻译键 |
session | JsonSession | 承载编辑区文本、jsonPath、pathPickerOpen、pathPickerSelection、pathResult、notice等会话状态,是弹层的"唯一数据源" |
onChanged | () -> Unit | 状态变更后的通知回调,由JsonScreen传入{ refresh() }驱动界面刷新 |
调用侧由JsonScreen在pathPickerOpen为 true 时挂载(见 JsonScreen.kt):
if (session.pathPickerOpen) { JsonPathPickerDialog(container, session) { refresh() } }2.2 路径列表的懒加载与兜底
弹层组合时并不会预先持有完整路径树,而是基于当前编辑器文本即时计算:
val entries = runCatching { JsonEngine.listPaths(session.editor.text, translator) }.getOrDefault(emptyList()) val entryPaths = entries.map { it.path }- 解析失败的场景(编辑器里不是合法 JSON)通过
runCatching兜底为空列表,弹层表现为无条目可选,不会崩溃; - 路径枚举逻辑在 JsonEngine.kt 的
listPaths中实现,核心是collectPaths的递归遍历(JsonEngine.kt):- 数组节点生成
$path[index]形式,label 为[index]; - 对象节点键名若匹配
^[a-zA-Z_$][\w$]*$则生成$path.key,否则生成$path["键名"](带 JSON 引号); - 每个节点产出
JsonPathEntry(path, label, preview(node), depth)四元组(模型定义见 JsonModels.kt)。
- 数组节点生成
2.3 打开弹层时自动对齐当前选中路径
弹层通过LaunchedEffect在打开时把选中项对齐到会话当前jsonPath,避免旧pathPickerSelection残留:
LaunchedEffect(session.pathPickerOpen, session.jsonPath, entryPaths) { if (!session.pathPickerOpen) return@LaunchedEffect val initial = jsonPathPickerSelectionForOpen(session.jsonPath, entryPaths) if (session.pathPickerSelection != initial) { session.pathPickerSelection = initial onChanged() } }对齐规则实现在 JsonInspectorPathUi.kt 与 JsonInspectorResult.kt:
internal fun jsonPathPickerInitialSelection(jsonPath: String, entryPaths: List<String>): String = entryPaths.find { it == jsonPath } ?: entryPaths.firstOrNull() ?: jsonPath优先级:当前jsonPath存在于路径树 → 选中它;不存在则选中第一个路径条目;连条目都没有则原样保留jsonPath。注意此时只改选中态,不写入、不查询。
2.4 布局与预览区
弹层尺寸固定为 760×480 dp,主题化mooDialogSurface圆角卡片,内部为左右两栏(见 JsonPathPickerDialog.kt):
- 左栏(
weight(0.9f)):LazyColumn渲染JsonPathListRow条目,单击仅更新pathPickerSelection,双击走applyPathPickerChoice(双击语义见下文 DIFF-458 部分); - 中缝(1 dp 分隔线):右侧预览区用 1 dp 边框线隔离;
- 右栏(
weight(1.1f)):展示选中路径(等宽字体、11 sp)与节点预览(等宽字体、11 sp),预览文本通过jsonPathNodePreview懒计算,并随editor.revision与预览路径变化而重建:
val previewText = remember(session.editor.revision, previewPath) { jsonPathNodePreview(session.editor.text, previewPath, translator, current?.preview.orEmpty()) }预览格式化由 JsonEngine.kt 的pathPickerPreview提供,对齐 ElectronJsonPathPicker的formatPreview:字符串节点返回原文(不带 JSON 引号),对象/数组节点走 pretty-print;路径失效或缺失时返回空字符串。
三、「使用」按钮:语义化contentDescription与「仅选路径」行为
3.1 无障碍语义对齐
「使用」按钮通过Modifier.semantics显式声明contentDescription(JsonPathPickerDialog.kt):
modifier = Modifier.semantics { contentDescription = container.t("json.pathPicker.use") }文案键json.pathPicker.use在 Translator.kt 中登记为中文「使用该路径」。这一设计同时服务两类消费者:
- Compose UI 测试:测试可用
onNodeWithContentDescription(zh.t("json.pathPicker.use"))精确点击按钮; - 无障碍:与同窗口其他可点击控件的语义保持一致,屏幕阅读器可朗读出按钮含义。
按钮可用性由canUse = current != null决定——只有选中项确实存在于路径树中时「使用」才可点击,有效拦截"选中了一个不存在路径"的非法提交。相关弹层内 i18n 键还有json.pathPicker.title(选择 JSONPath)、json.pathPicker.path(路径)、json.pathPicker.preview(预览)。
3.2 点击后的状态变换:applyPathPickerChoice
「使用」按钮点击后调用会话级的统一行为封装(JsonInspectorResult.kt):
internal fun JsonSession.applyPathPickerChoice(path: String, pathAppliedNotice: String) { jsonPath = path clearJsonPathQueryResult() notice = pathAppliedNotice pathPickerOpen = false }这一步完成了 DIFF-319 强调的 Electron 对齐语义:
| 状态字段 | 变更结果 | 含义 |
|---|---|---|
jsonPath | 写入选中路径 | 路径输入框反映新选择 |
pathResult | 清空 | 不触发 JSONPath 查询,也不把节点预览塞进结果区 |
notice | 置为json.notice.pathApplied(中文「已应用 JSON Path」) | 仅以提示形式告知用户路径已应用 |
pathPickerOpen | 置为 false | 弹层随即关闭 |
与之形成对照的是检查器内联路径树的单机预览行为:applyInlinePathTreePreview(JsonInspectorResult.kt)会写入jsonPath并把预览写入pathResult。也就是说,弹层「使用」走的是"纯净选择"语义,内联树单击走的是"预览语义",两者刻意区分,由 DIFF-319 与 DIFF-457 共同固化。
四、交互单测:useButtonAppliesPathWithoutQuery的完整链路
4.1 测试骨架
新增 JsonPathPickerDialogInteractionTest.kt,以runDesktopComposeUiTest(width = 800, height = 520)挂载真实MooTheme与对话框:
- 用
createTempDirectory构造临时产品目录,走AppPaths.resolve(...).ensureCreated(),创建SettingsRepository、AppDatabase、HistoryRepository、LegacyMigrationRowRepository、SessionStore,最终组装出与生产等价的AppContainer; session直接以JsonSession()构造并预置状态:session.editor.setText("""{"store":{"books":[{"title":"One"}]}}""")固定测试 JSON;session.pathPickerOpen = true打开弹层;session.pathPickerSelection = "$.store.books[0].title"预置选中路径;session.pathResult = """["stale"]"""预置一条过期查询结果,用于验证"使用后必须被清空"。
4.2 断言矩阵
waitForIdle() onNodeWithContentDescription(zh.t("json.pathPicker.use")).performClick() assertFalse(session.pathPickerOpen) assertEquals("$.store.books[0].title", session.jsonPath) assertEquals("", session.pathResult) assertEquals(zh.t("json.notice.pathApplied"), session.notice)这条测试把 DIFF-319 的行为契约翻译成 5 个显式断言:
- 弹层关闭(
pathPickerOpen == false); - 路径写入(
jsonPath == "$.store.books[0].title"); - 查询结果被清空(
pathResult == "",即"不自动查询"的直接证据——如果弹层「使用」触发了查询,预置的["stale"]会被新结果覆盖而非清空); - 提示为「已应用 JSON Path」;
- 测试通过后
deleteRecursively()清理临时目录。
从源码调用链看,测试点击按钮后实际执行的是 JsonPathPickerDialog.kt 的onClick,其中val entry = current ?: return@MooButton的守卫与canUse共同保证非法状态不可提交。
4.3 双击路径行的补充回归(DIFF-458)
DIFF-458 在弹层单测中追加了双击链路:doubleTapRowAppliesPathWithoutQuery通过onNodeWithContentDescription("$.store.books[0].title").performTouchInput { doubleClick() }触发,断言与「使用」完全一致(关闭弹层、写入路径、清空pathResult、仅设置pathApplied提示)。其实现基础是 JsonPathListUi.kt 中JsonPathListRow的:
.semantics(mergeDescendants = true) { contentDescription = entry.path } .pointerInput(entry.path) { detectTapGestures(onTap = { onTap() }, onDoubleTap = { onDoubleTap() }) }mergeDescendants = true保证整行路径作为单一语义节点暴露给测试与辅助功能,detectTapGestures同时承载单击(仅改选中)与双击(应用路径)两种手势。至此弹层的「使用」与「双击」两条入口共用同一applyPathPickerChoice语义,与 ElectrononChoose(path)完全对齐。
五、场景帧捕获:capturePathPickerUseButtonFocusRing
除行为断言外,DIFF-457 还新增了 JsonPathPickerCaptureTest.kt 用于产出 Compose 场景帧证据:
- 同样以
runDesktopComposeUiTest(width = 800, height = 520)挂载弹层({"a":1,"b":2}简单 JSON),waitForIdle()后onRoot().captureToImage().toAwtImage()抓取整帧; - 写盘路径为
docs/evidence/2026-09-15-inspector-screencapture/windows/148-compose-json-path-picker-use-tab-focus.png,先mkdirs()再ImageIO.write; - 断言
file.length() > 3_000,防止写出空白/损坏帧。
该证据图片同时记录了「使用」按钮在 Tab 焦点下的焦点环渲染,属于对非产品主窗的独立 Compose 场景帧,供 UI 评审与回归对照:
六、验证命令与回归结果
在next-compose目录下执行桌面测试全量验证:
./gradlew :composeApp:desktopTest --offlineDIFF-457 落地时全量桌面测试通过(615/615),随后 DIFF-458 追加双击用例后扩展至 616/616。--offline表示不访问远程依赖仓库、纯本地离线构建,适合在 CI 受限或网络隔离环境下稳定复现。
七、设计要点小结
- 弹层组件化是可测试性的前提:
JsonPathPickerDialog(container, session, onChanged)的签名让测试能以真实AppContainer+ 手工构造的JsonSession独立挂载弹层,而不必启动整个JsonScreen; - 状态封装收敛行为契约:
applyPathPickerChoice把"仅选路径、不查询、关闭弹层、置提示"四项语义收敛为单一函数,使「使用」与「双击」天然复用同一契约,杜绝分支漂移; - 语义化贯穿 UI 与测试:
contentDescription同时服务onNodeWithContentDescription定位与无障碍朗读,是 Compose 桌面端"可测性即可访问性"的典型实践; - 证据帧沉淀 UI 回归:场景帧捕获测试把无法用断言表达的视觉细节(焦点环、布局)落盘为证据文件,供跨平台对照与评审追溯。
如需继续深入,可进一步阅读同目录下的相关 DIFF 记录:DIFF-319「仅选路径、不自动查询」语义、DIFF-458 双击路径行回归,以及弹层依赖的核心实现 JsonEngine.listPaths 与 JsonInspectorResult。
- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
相关推荐
MooTool Compose 桌面版 JSONPath 选择器弹层对齐 Electron 布局:规格解析与实现验证
MooTool Compose 桌面版 JSONPath 选择器弹层对齐 Electron 布局:规格解析与实现验证 MooTool 在 Electron 前端
桌面应用开发工具Penrose 词云示例实战:用 Style 约束与目标函数排版文本标签
Penrose 词云示例实战:用 Style 约束与目标函数排版文本标签 本指南以仓库中 word cloud 示例 https://link.gitcode.
桌面应用开发工具MooTool Compose 版 JSONPath 弹层双击执行查询的设计演变:从 DIFF-285 到 DIFF-319 的交互收敛
MooTool Compose 版 JSONPath 弹层双击执行查询的设计演变:从 DIFF 285 到 DIFF 319 的交互收敛 导读 本文围绕 Moo
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考