news 2026/10/16 6:07:38

MooTool Next-Compose 实战:JSONPath 选择弹层 `JsonPathPickerDialog` 的提取与「使用」交互单测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MooTool Next-Compose 实战:JSONPath 选择弹层 `JsonPathPickerDialog` 的提取与「使用」交互单测
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

导读

本文围绕 MooTool 下一代跨平台版本(next-compose)中 JSON 工具模块的JsonPathPickerDialog弹层重构展开:为兼顾可测试性与「使用此路径」仅选路径、不自动查询的交互语义(DIFF-319),项目将原本内联在JsonScreen中的路径选择对话框提取为独立 Compose 组件,并通过语义化contentDescription、会话状态封装与桌面 Compose UI 测试,把「使用」与「双击选路径」两条行为链路固化为可回归的自动化用例。读完本文,你将掌握该组件从提取、状态驱动、无障碍对齐到交互单测与场景帧证据的完整实现思路,可直接迁移到自身 Compose 桌面项目的弹层测试与行为封装中。

一、背景:为什么要把PathPickerDialog从JsonScreen中提取出来

在重构之前,JSONPath 选择弹层(PathPickerDialog)是直接内联在JsonScreen里的。随着功能演进,它暴露出两个问题:

  1. 体量过大不利于单测:弹层既承担路径列表加载、选中态维护,又要处理预览与确认/取消逻辑,内联在 2200+ 行的JsonScreen中(见 JsonScreen.kt)很难被 Compose 测试独立挂载和驱动;
  2. 交互语义需要回归保护: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)

三个入参各有明确职责:

参数类型职责
containerAppContainer提供 i18n 翻译(container.t(key, params))与全局能力,弹层内所有文案统一走翻译键
sessionJsonSession承载编辑区文本、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 个显式断言:

  1. 弹层关闭(pathPickerOpen == false);
  2. 路径写入(jsonPath == "$.store.books[0].title");
  3. 查询结果被清空(pathResult == "",即"不自动查询"的直接证据——如果弹层「使用」触发了查询,预置的["stale"]会被新结果覆盖而非清空);
  4. 提示为「已应用 JSON Path」;
  5. 测试通过后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 --offline

DIFF-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

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:DDrawCompat终极指南:让经典DirectX游戏在现代Windows上完美运行
下一篇:一键解决Windows DLL缺失问题:Visual C++运行库完整安装指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 6:07:34

一键网站下载:5分钟把整站备份到本地

一键网站下载&#xff1a;5分钟把整站备份到本地 【免费下载链接】Website-downloader &#x1f4a1; Download the complete source code of any website (including all assets). [ Javascripts, Stylesheets, Images ] using Node.js 项目地址: https://gitcode.com/GitH…

作者头像 李华
网站建设 2026/10/16 6:06:40

ES查询入门:URI Search与Query DSL对比解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:06:26

QQ邮箱网页版下载无法改保存路径?根因与浏览器设置有关

你在网页版邮箱里收到一份重要文档&#xff0c;点下载&#xff0c;文件“嗖”一下进了某个文件夹&#xff0c;找半天才翻出来。等你打开设置想换个默认保存位置&#xff0c;却发现整个邮箱页面翻遍了也没有这个选项。这个问题我在不同场合被问过很多次&#xff1a;QQ邮箱网页版…

作者头像 李华
网站建设 2026/10/16 6:06:00

AI编程革命:从写代码到做决策,开发者如何抓住新机会

1. 这场变革为什么来得比想象中更猛两年前&#xff0c;圈子里开始流行"AI取代程序员"的论调&#xff0c;当时大多数人当段子听。直到某天我接手一个遗留系统&#xff0c;面对两千多个无人维护的接口方法&#xff0c;第一次认真思考&#xff1a;这个活如果是五年前&am…

作者头像 李华
网站建设 2026/10/16 6:05:01

现场总线实战指南:物理层、协议栈与工程落地关键

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:04:33

uniapp 换行问题全解析:从 textarea 到 CSS 渲染的跨端实践

做 uniapp 项目的时候&#xff0c;几乎每个阶段都会遇到跟换行相关的问题&#xff1a;textarea 里明明按了回车&#xff0c;提交后存到库里再看却只剩一行&#xff1b;后端返回的字符串里明明带着\n&#xff0c;页面上偏偏不换行&#xff0c;英文和数字挤成一大坨&#xff1b;小…

作者头像 李华