MarkText 引擎迁移功能对等记分牌(Parity Scoreboard)深度解析:从 muyajs 到 @muyajs/core 的 15 个回归缺口与修复方法论

发布时间:2026/9/18 3:35:45
MarkText 引擎迁移功能对等记分牌(Parity Scoreboard)深度解析:从 muyajs 到 @muyajs/core 的 15 个回归缺口与修复方法论 MarkText 引擎迁移功能对等记分牌Parity Scoreboard深度解析从 muyajs 到 muyajs/core 的 15 个回归缺口与修复方法论【免费下载链接】marktextA simple and elegant markdown editor, available for Linux, macOS and Windows.项目地址: https://gitcode.com/gh_mirrors/ma/marktext本文基于仓库内 packages/desktop/test/PARITY_SCOREBOARD.md 展开。MarkText 桌面端在 PR #4406 中把编辑器内核从遗留的packages/muyajs迁移到重写后的muyajs/corepackages/muya迁移后暴露出15 个已确认的功能对等parity缺口。仓库用一张失败测试记分牌把每个缺口编码成一条回归测试让缺口以预期失败的形式存活于绿色测试套件中再逐条翻转回真实断言。读完本文你将掌握这套以测试形式冻结回归缺口的工程方法论、15 个缺口的完整清单与修复状态以及如何为未来的缺口编写、定位、翻转这类 parity 测试。背景为什么需要一张失败测试记分牌内核迁移muyajs→muyajs/core是 MarkText 历史上一次破坏性重写新引擎重写了块树、选择系统、剪贴板、导出等核心路径。迁移完成后遗留引擎的许多行为在新引擎中悄然丢失——菜单状态失效、光标不恢复、图片粘贴不工作、导出 HTML 无样式……这些不是编译错误而是功能对等性functional parity问题新引擎能跑但行为与旧引擎不一致。直接修掉全部缺口再合并迁移会无限期阻塞主干。记分牌方案的核心思想是先把缺口变成可执行、可复现的测试——每条测试在develop上失败从而证明缺口真实存在把失败标记为预期失败expected failure——测试套件保持 GREEN迁移不被阻塞随着修复 PR 逐条落地摘掉失败标记测试转为真实断言永久守护该行为。截至记分牌最后一次更新15 个缺口已有14 个被修复七个 Wave-1 引擎 PR #4408–#4414 Wave-2 桌面端消费方接线唯一的例外是PG14source 模式交接处的撤销边界见其所在行的延期分析。从 git 提交历史看这套体系有清晰的落点#4407 建立记分牌#4408–#4414 是 Wave-1 引擎修复#4415 是桌面端 wave-2 接线菜单状态、copyAsRich、heading-link、source 光标、保存指示器。记分牌的工作机制How it works记分牌按三层测试策略工作每层对应一种失败标记语法1. 引擎单元测试vitestit.fails(...)位于packages/muya/src/**/__tests__/parity*.spec.ts。断言描述的是迁移前正确的行为修复前会失败——vitest 将失败计为通过。当修复落地、行为变正确时it.fails反而会报错从而强制修复者删除.fails。所有引擎 parity 测试现在都已改用普通it并通过。2. 桌面端 e2e 测试Playwrighttest.fail()位于packages/desktop/test/e2e/parity-*.spec.ts。测试在 headless 环境运行修复前失败会被 Playwright 计为通过修复落地后移除test.fail()。目前只有 PG14 仍携带该标记注仓库后续历史中引擎侧已落地replaceContent修复见下文 PG14 小节。3. 手动 QA 清单PARITY_QA.md位于 packages/desktop/test/PARITY_QA.md。覆盖无法 headless 驱动的缺口真实操作系统剪贴板位图、拖放手势等。命名约定PGn:前缀每条测试名都以缺口 id 开头如PG3: …因此修复 PR 只需一条命令即可定位其全部测试grep -rn PG3:这一约定把缺口 → 测试 → 修复 PR三者用字符串直接串了起来。如何翻转一个缺口Flipping a gap to green对修复 PR 的作者记分牌给出四步标准操作实现修复用grep -rn PGn:定位测试移除失败标记muya 侧it.fails→itdesktop e2e 侧删除test.fail()或执行并勾选手动 QA 条目确认测试现在真实通过并把记分牌Status列更新为 ✅。这套流程保证修复者提交 PR 时对应回归测试恰好从预期失败翻转为真实断言缺口是否闭合一目了然。15 个缺口全景Scoreboard剩余缺口1 / 15PG14accept-defer。其余 14 个已闭合七个 Wave-1 引擎 PR#4408–#4414落地了引擎侧修复Wave-2 桌面端 PR 完成了消费方接线PG1 affiliation 适配器、PG2setCursorByOffset、PG8 pdf.ts slugger、PG9copyAsRich映射、PG11heading-copy-link订阅、PG15 稳定 saved-id。PG4 的本地文件拖放持久化与 PG5 的操作系统剪贴板位图投递仍有 PARITY_QA.md 手动条目无法 headless 驱动但其代码路径已修复并有单元测试覆盖。PG14source 模式交接处的单步撤销边界被延期——见其所在行。Gap严重性丢失的行为测试位置机制状态PG1majorselection-change缺少块归属affiliation/祖先类型 → 原生 Paragraph 与 Format 菜单状态失效packages/muya/src/selection/tests/paritySelectionChange.spec.tsPG1:×2· packages/desktop/test/e2e/parity-pg1-menu-state.spec.tsPG1:通过的it 通过的test✅ 已修复引擎 #4410 · 桌面 wave 2PG2majorsource 模式 → WYSIWYG 光标未恢复handleFileChange丢弃muyaIndexCursorpackages/muya/src/tests/setCursorByOffset.spec.tsPG2:×5· packages/desktop/test/e2e/parity-source-undo-saved.spec.tsPG2:通过的it 通过的test✅ 已修复引擎setCursorByOffset 桌面 wave 2PG3majorautoCheck偏好未被消费任务列表勾选级联丢失packages/muya/src/block/gfm/taskListCheckbox/tests/parityAutoCheck.spec.tsPG3:×2通过的it✅ 引擎已修复#4409PG4major拖放图片插入本地文件 网页链接缺失packages/muya/src/editor/tests/dragDropImage.spec.tsPG4 ×7· packages/desktop/test/PARITY_QA.md § PG4单元合成DataTransfer 手动 QA✅ 引擎已修复#4413PG5major二进制/位图剪贴板图片粘贴丢失截图、浏览器复制图片packages/muya/src/clipboard/tests/parityImagePaste.spec.tsPG5:· packages/desktop/test/PARITY_QA.md § PG5通过的it 手动 QA✅ 引擎已修复 #4411OS 剪贴板手动 QA 仍保留PG6major粘贴的图片文件绕过imageAction复制到 assets / 上传偏好被忽略packages/muya/src/clipboard/tests/parityImagePaste.spec.tsPG6:×2通过的it✅ 引擎已修复#4411PG7major导出从 CDN 加载核心 CSS 而非内联离线时无样式packages/muya/src/state/tests/parityExportHtml.spec.tsPG7:×2通过的it✅ 引擎已修复#4412PG8major导出的标题没有idTOC /[TOC]锚点失效packages/muya/src/state/tests/parityExportHtml.spec.tsPG8:×2通过的it✅ 已修复引擎 #4412 · 桌面 pdf.ts slugger wave 2PG9major复制为富文本粘贴的是 HTML源码而非富文本无copyAsRich路径packages/muya/src/clipboard/tests/parityCopyAsRich.spec.tsPG9:×2通过的it✅ 已修复引擎 #4411 · 桌面copyAsRich映射 wave 2PG10minorpreview-image从未触发——选中图片 Space 全屏预览丢失packages/muya/src/selection/tests/parityPreviewImage.spec.tsPG10:×2通过的it✅ 引擎已修复 #4414桌面订阅本就存在PG11minorheading-copy-link从未触发——悬停复制锚点功能消失packages/muya/src/tests/parityHeadingCopyLink.spec.tsPG11:×2通过的it✅ 已修复引擎 #4414 · 桌面订阅 wave 2PG12minorhideLinkPopup偏好未被消费——链接悬停弹层未受控packages/muya/src/editor/tests/parityHideLinkPopup.spec.tsPG12:通过的it 对照组✅ 引擎已修复#4409PG13minor嵌套结构中insertParagraph锚定到最外层而非直接块packages/muya/src/tests/parityInsertParagraphNested.spec.tsPG13:×2通过的it✅ 引擎已修复#4408PG14minorsource 模式后的第一次撤销不能把编辑一次性回退packages/desktop/test/e2e/parity-source-undo-saved.spec.tsPG14:test.fail()❌ xfailaccept-deferPG15minor撤销回磁盘内容后未恢复 saved/clean 指示器packages/desktop/test/e2e/parity-source-undo-saved.spec.tsPG15:通过的test✅ 桌面端已修复wave 2严重性统计majorPG1–PG9 共 9 个——全部已修复minorPG10–PG15 共 6 个——除PG14外全部已修复值得注意9 个 major 缺口集中在输入输出边界——菜单状态PG1、光标恢复PG2、图片的粘贴与拖放PG4/PG5/PG6、导出PG7/PG8/PG9。这印证了内核重写最容易在引擎与宿主桌面壳的契约面上丢行为。重点缺口源码级拆解以下选取几个代表性缺口结合引擎与 e2e 测试源码看它们丢失了什么、如何断言、如何被修复。PG1selection-change 的块归属链菜单状态遗留muyajs的selectionChange携带祖先 PARAGRAPH 型块的affiliation链以及每块的.typemarkdown 块类型h1、p、pre…与.functionTypecodeContent、cellContent…。桌面端 store 的createApplicationMenuState靠它点亮 Paragraph 菜单的勾选、Loose/Task-list 开关、表格/代码围栏检测并在代码块内禁用 Format 菜单。新引擎的selection-change载荷只暴露扁平光标/选区信息anchor、focus、anchorBlock、isCollapsed、type等没有 affiliation 祖先链且type是选区种类Caret | Range而非块类型——原生菜单状态因此死掉。参见 paritySelectionChange.spec.ts 的注释。引擎修复#4410恢复 affiliation 链后桌面端 e2e parity-pg1-menu-state.spec.ts 读取真实应用菜单的checked状态把光标点进各类型块断言 Paragraph 子菜单中恰好对应的项被勾选。测试通过Menu.getApplicationMenu()getMenuItemById(paragraphMenuEntry)直接检查 Electron 原生菜单是对菜单状态恢复最直接的用户可见验证。PG2source 模式 → WYSIWYG 光标恢复source 模式CodeMirror切回所见即所得时交接只携带 CodeMirror 的{ line, ch }索引光标handleFileChange若丢弃muyaIndexCursorWYSIWYG 光标就落在错误位置。引擎侧修复是setCursorByOffset把索引光标重新映射到块键光标。其实现采用哨兵注入 树遍历见 setCursorByOffset.spec.ts 与selection/offsetCursor.ts的injectSentinels/resolveSentinelCursor——在文档中注入mUyAcUrSoR哨兵、定位到块、再按 offset 恢复选区随后断言文档中无哨兵残留。桌面端 e2e parity-source-undo-saved.spec.ts 从用户视角验证进入 source 模式、把 CodeMirror 光标放到第 4 行third para here、退出 source 模式然后向上遍历getSelection()的容器节点断言光标落回了包含 third para 的块。PG4拖放图片插入本地文件 网页链接新引擎重写后完全没有拖放图片处理器。attachDragDropImageHandlersdragDropImage.spec.ts通过在编辑器容器上绑定dragover/drop恢复该功能并把拖入的图片作为新的![](src)段落插入。单元测试用 happy-dom 提供的完整DataTransferitems.add/getAsString/files同步触发getAsString驱动真实处理器端到端本地图片文件经getPathForFile(file)解析路径后先插入loading-id占位符再调用imageAction({ src, alt, title })按嵌入方偏好持久化L122-L151若未提供imageAction则直接插入干净的shot.pngL153-L168网页链接图片通过text/uri-listtext/htmlimg签名识别——带扩展名或 HTML 证明的 URL 插入![](url)会剥掉 uri-list 的 CRLF 结尾纯超链接拖拽uri-list text/plain、无 html则放行给浏览器默认行为L208-L289守卫text/plain纯文本拖拽不preventDefault保住原生文本拖拽L302-L320拖到非内容块上不插入L322-L340。PG5 / PG6剪贴板图片粘贴与imageAction路由PG5位图剪贴板遗留pasteImage()有二进制分支——无文件路径时通过clipboardData.items[i].getAsFile()FileReader.readAsDataURL读入内存图片并经imageAction持久化。新引擎pasteHandler完全没有getAsFile/FileReader/clipboardData.files路径导致截图、复制图片粘贴后什么都不插入。PG6图片文件绕过偏好遗留引擎把粘贴的图片文件路由到imageAction(imagePath, id)让复制到 assets / 上传 / 保留原路径偏好生效新引擎的路径粘贴分支直接写![](rawPath)从不调用options.imageAction文档不可移植。parityImagePaste.spec.ts 的注释完整记录了这两个缺口的应有行为。修复后测试断言clipboardFilePath解析出路径后imageAction被调用且入参为解析路径L100-L126持久化后插入的是assets 相对路径而非磁盘原始路径L128-L146位图剪贴板无路径、仅有内存 PNGFile同样经imageAction持久化并插入L149-L174。PG7 / PG8导出 HTML 的自包含与标题锚点PG7遗留ExportHtml.generate通过?inline导入把 github-markdown-css、prism 主题、katex CSS 内联成style块导出的 HTML 完全自包含、可离线渲染新引擎改为从外部 CDNlink relstylesheet引用三套核心样式——离线 / CSP 环境 / 内网下导出无样式。PG8遗留导出把每个标题渲染为hN id{slug}与getHtmlToc的a href#{slug}对应文档内 [TOC]/目录链接可用新引擎用原生marked渲染、无 heading-id renderer导出的h1..h6没有 id所有 TOC 锚点失效。parityExportHtml.spec.ts 对导出产物做了四类断言HTML 内联.markdown-body样式且无任何https://外部样式表链接PG7标题携带 slug id 且与 marktext slug 规则一致Getting Started→getting-started重复/链式冲突标题获得唯一 id——对# heading、## heading、## heading-1断言输出heading、heading-1、heading-1-1即遗留 Slugger 的链式去重语义以及.toc-container目录样式链接继承正文色 点状引导线被内联对应 issue 229。PG11标题悬停复制锚点遗留muyajs在每个标题上渲染悬停装置i.icon.ag-copy-header-link并派发heading-copy-link { key }桌面端据此把标题的 GitHub slug/锚点复制到剪贴板copyGithubSlug。新引擎不渲染该装置、不触发事件copyGithubSlug成为不可达的死代码。parityHeadingCopyLink.spec.ts 断言修复后的完整行为标题渲染出复制链接装置选择器兼容ag-与mu-前缀点击装置触发heading-copy-link并携带块键装置是无障碍、可键盘聚焦的按钮rolebutton、tabindex0、有aria-label、装饰图标alt按 Enter 或空格同样触发事件。key是标题的稳定 slug——与getTOC()暴露的ITocItem.slug同值宿主可据此解析。PG15保存/干净指示器的内容键控历史PG15 涉及撤销回磁盘内容后恢复已保存指示器。其修复引入单调、永不复用、以实时文档内容为键的合成历史 idsyntheticHistory.ts。关键反回归测试 G6 记录在 parity-source-undo-saved.spec.ts旧方案以撤销深度作为 id撤销 分叉重编辑回到已保存深度时 id 会与保存 id碰撞脏标签被误读为干净关闭不保存有丢数据风险新方案用内容键控的单调 id使分叉后的文档保持脏状态。PG14为什么被延期accept-deferPG14 是唯一保持 xfail 的缺口source 模式下的第一次撤销无法把整段编辑作为一步回退。记分牌给出了完整的延期论证退出 source 模式时handleFileChange通过setContent重建文档会history.clear()再恢复 source 前的操作栈因此 bulk 的 source 模式变更不会作为单个引擎撤销操作被记录。要把它变成一个撤销边界需要计算通用的整文档ot-json1差异source 前状态 → source 后状态并经Editor.updateContents的 pick/drop 遍历器应用。但该遍历器只处理固定形状的操作按索引插入块、文本编辑、checked/meta任意 diff删除、移动、嵌套替换可能错误应用并损坏文档。风险大于收益——跨边界的首步撤销粒度是狭窄的边界场景撤销本身仍可用且 source 前的操作栈完好——所以 PG14 保留test.fail()而不是交付脆弱修复。文档同时给出了干净复活的方向需要一个专用的引擎 API把一次状态替换记录为单个撤销操作。从仓库后续历史看这个方向确实被跟进提交5d3d2819 fix(muya): record source-mode bulk edit as a single undo boundary (PG14) (#4420)引入了Muya.replaceContent——一个完全可逆的整文档 ot-json1 操作通过整树重建ScrollPage.updateState应用从不走增量 pick/drop 遍历器。其单元测试 packages/muya/src/tests/replaceContent.spec.ts 断言bulk 替换后的第一次undo()一步回退全部变更、redo()一步重放且重建 DOM 与 ground-truth同目标setContent的新树逐结构相等以捕获遍历器静默失同步的场景。也就是说记分牌中 PG14 的 xfail 状态是文档最后一次更新#4415时的记录引擎侧的修复方案此后已按文档预期的方向落地。若需了解当前最终状态以记分牌所在分支的最新 Status 列与 e2e 源码为准。手动 QA 清单无法 headless 的部分PARITY_QA.md 记录了无法在 headless/xvfb CI 中可靠驱动的缺口以精确手动检查清单形式跟踪每条对应记分牌一行。所有条目当前在develop上 FAIL缺口存在修复落地后执行步骤、观察到Expected (after fix)即通过。PG4 手动部分引擎拖放处理器已单元测试覆盖剩余 OS 集成部分本地图片文件打开文档最好是已保存的.md从系统文件管理器拖一个.png/.jpg到编辑器段落内。预期出现加载占位符 → 内联图片渲染当Preferences → Image → insert action copy to folder时文件被复制进文档 assets 文件夹链接指向那里而非原始绝对路径。注意这要求桌面 wave-2 接线——imageAction/getPathForFile选项必须在editor.vue构造 Muya 时传入否则拖放会原样插入原始路径、忽略插入偏好。网页链接图片从浏览器拖一张图或其 URL进编辑器。预期插入![](url)并渲染。此路径无需桌面接线。PG5 手动部分引擎二进制分支已实现并通过剩余 OS 剪贴板部分浏览器复制图片右键图片 → Copy Image剪贴板上是位图而非文件路径聚焦编辑器粘贴。预期位图以内联图片插入并按键入偏好持久化当前缺口行为是什么都不插入。macOS 截图集成触发应用内截图运行screencapture -i -c的功能/菜单框选区域后自动粘贴。预期截图以内联图片插入当前缺口行为是静默失效。运行测试套件# muya 引擎 parity 测试全部通过PG2 光标映射另由 # src/__tests__/setCursorByOffset.spec.ts 覆盖 pnpm -C packages/muya test # 单个缺口的引擎测试 pnpm -C packages/muya exec vitest run src/state/__tests__/parityExportHtml.spec.ts # 桌面端 parity e2e需先 pnpm run build:unpackPG14 保持 xfail pnpm -C packages/desktop exec playwright test \ test/e2e/parity-pg1-menu-state.spec.ts \ test/e2e/parity-source-undo-saved.spec.ts \ --config test/e2e/playwright.config.tsProvenance缺口清单从哪来缺口清单源于对 PR #4406 的对抗性审查adversarially-verifiedd2-parity-review产出PG01..PG16PG-COPYRICH。去重后有 15 个独立缺口——遗留的Space 预览与insert-paragraph 锚定缺口各出现两次copyAsRich计入 15 个之一。记分牌上的PGn编号是规范化的 1–15 列表而非原始审查 id。总结这套方法论的可复用价值PARITY_SCOREBOARD.md提供了一种在大规模内核重写中管理回归的成熟模式用预期失败的测试冻结每一个已知缺口让主干保持绿色、迁移不被阻塞用PGn:命名约定把缺口、测试、修复 PR 以字符串级可检索的方式绑定用三层测试策略vitestit.fails/ Playwrighttest.fail/ 手动 QA 清单按可自动化程度分层覆盖再用四步翻转流程让每个修复 PR 自带证明缺口闭合的回归测试。对于任何计划做破坏性重写的编辑器/富文本项目这套失败测试记分牌都是一份可直接复用的工程模板。【免费下载链接】marktextA simple and elegant markdown editor, available for Linux, macOS and Windows.项目地址: https://gitcode.com/gh_mirrors/ma/marktext创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考