Repository navigation
test(parity): failing-test scoreboard for #4406 muyajs→@muyajs/core gaps - #4407
Conversation
…10,11,12,13 Encode 10 of the 15 confirmed muyajs→@muyajs/core functional-parity gaps as vitest `it.fails` regression tests in the engine. Each asserts the desired pre-migration behaviour and fails on develop today (counted as a pass by `it.fails`); when a fix lands the test passes and `it.fails` errors, forcing the fixer to drop the marker. Every test name is prefixed with its gap id (`PGn:`) so fix PRs can grep + flip it. - PG1 selection-change lacks block affiliation / ancestor type - PG3 autoCheck preference not consumed (task-list cascade lost) - PG5 binary/bitmap clipboard image paste not persisted via imageAction - PG6 pasted image FILE bypasses imageAction (insert preference ignored) - PG7 export links core CSS from CDN instead of inlining it - PG8 exported headings carry no slug id (dead TOC anchors) - PG9 copyAsRich writes HTML source not rich text (no copyAsRich path) - PG10 preview-image not emitted (Space on a selected image) - PG11 heading-copy-link not emitted (hover-to-copy affordance gone) - PG12 hideLinkPopup not consumed (link hover popover not gated) - PG13 insertParagraph anchors to outermost not immediate nested block Allow the uppercase `PG` test-title prefix through antfu's `test/prefer-lowercase-title` rule (scoped to spec files) so the greppable gap-id naming coexists with the lint rule; all other test titles still must start lowercase. Suite stays GREEN: 512 pass + 20 expected-fail. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add Playwright `test.fail()` regression specs for the desktop-side parity gaps.
Each runs headless and currently fails (the gap), which `test.fail()` counts as
a pass; removing `test.fail()` flips the entry green once fixed. Test names are
prefixed with the gap id (`PGn:`) for grep-and-flip.
- PG1 Paragraph menu does not check the current block type (affiliation gap;
caret placed directly in an H1 content span — the engine-unit half lives
in paritySelectionChange.spec.ts)
- PG2 WYSIWYG caret not restored after a source-mode edit (handleFileChange
drops muyaIndexCursor; no index→path cursor conversion in the engine)
- PG14 first undo after exiting source mode does not revert the bulk source-mode
change as one step
- PG15 undo back to on-disk content leaves the tab marked unsaved (synthetic
history id never re-matches the saved id)
Undo is driven through the same `mt::editor-edit-action` IPC the Edit › Undo
menu uses. Suite stays GREEN (4 expected-fail).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add the visible "how many gaps remain" board (PARITY_SCOREBOARD.md): a table of all 15 muyajs→@muyajs/core parity gaps → severity → test location(s) → status (all xfail now), plus the flip-to-green workflow for fix PRs. Add PARITY_QA.md: precise manual-QA checklists (exact steps + expected vs current result) for the two gaps that cannot be driven headless — PG4 drag-drop image insertion and the OS-clipboard/screenshot half of PG5 binary image paste. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR introduces a “failing-test scoreboard” to track functional parity gaps from the packages/muyajs → @muyajs/core migration (PR #4406), encoding each confirmed gap as an expected-failure test (Vitest it.fails(...) for engine-unit tests, Playwright test.fail() for desktop e2e) plus manual-QA checklists for gaps that can’t be driven headlessly.
Changes:
- Adds parity regression specs in
packages/muyathat assert legacy/desired behavior and are marked as expected failures. - Adds desktop Playwright parity specs (expected failures) and manual QA checklist + a central scoreboard document.
- Tweaks
packages/muyaESLint config to allowPGn:-prefixed test titles while keeping the lowercase-title rule for all other tests.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/muya/src/state/tests/parityExportHtml.spec.ts | Adds xfail tests for export HTML CSS inlining + heading id parity (PG7/PG8). |
| packages/muya/src/selection/tests/paritySelectionChange.spec.ts | Adds xfail tests for selection-change payload parity (affiliation + block type) (PG1). |
| packages/muya/src/selection/tests/parityPreviewImage.spec.ts | Adds xfail tests for Space-to-preview-image event emission (PG10). |
| packages/muya/src/editor/tests/parityHideLinkPopup.spec.ts | Adds control + xfail test to assert hideLinkPopup suppresses link hover popover (PG12). |
| packages/muya/src/clipboard/tests/parityImagePaste.spec.ts | Adds xfail tests for bitmap paste + imageAction routing for pasted image files (PG5/PG6). |
| packages/muya/src/clipboard/tests/parityCopyAsRich.spec.ts | Adds xfail tests for “Copy as Rich Text” behavior / copyAsRich support (PG9). |
| packages/muya/src/block/gfm/taskListCheckbox/tests/parityAutoCheck.spec.ts | Adds xfail tests for autoCheck cascade behavior on task lists (PG3). |
| packages/muya/src/tests/parityInsertParagraphNested.spec.ts | Adds xfail tests for nested insertParagraph anchor behavior (PG13). |
| packages/muya/src/tests/parityHeadingCopyLink.spec.ts | Adds xfail tests for heading hover copy-link affordance + event emission (PG11). |
| packages/muya/eslint.config.mjs | Allows PG prefix for test titles under test/prefer-lowercase-title (spec/test files). |
| packages/desktop/test/PARITY_SCOREBOARD.md | Adds the parity scoreboard doc listing gaps, tests, and flip-to-green workflow. |
| packages/desktop/test/PARITY_QA.md | Adds manual QA checklists for gaps not reliably testable headlessly (PG4/PG5). |
| packages/desktop/test/e2e/parity-source-undo-saved.spec.ts | Adds Playwright xfail coverage for source-mode caret/undo/saved-indicator gaps (PG2/PG14/PG15). |
| packages/desktop/test/e2e/parity-pg1-menu-state.spec.ts | Adds Playwright xfail coverage for Paragraph menu checkmarks driven by selection affiliation gap (PG1). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| afterEach(() => { | ||
| while (bootedHosts.length) { | ||
| const host = bootedHosts.pop()!; | ||
| host.remove(); | ||
| } | ||
| if (hadVersion) | ||
| window.MUYA_VERSION = originalVersion as string; | ||
| else | ||
| delete (window as Partial<Window>).MUYA_VERSION; | ||
| }); | ||
|
|
||
| function bootMuya(markdown: string): Muya { | ||
| const host = document.createElement('div'); | ||
| document.body.appendChild(host); | ||
| const muya = new Muya(host, { markdown } as ConstructorParameters<typeof Muya>[1]); | ||
| muya.init(); | ||
| bootedHosts.push(muya.domNode); | ||
| return muya; | ||
| } |
There was a problem hiding this comment.
Good catch — fixed in 824deb2. The cleanup now tracks each booted Muya and calls muya.destroy() in afterEach, which runs eventCenter.detachAllDomEvents() (removing the document-level keydown/click listeners selection registers) and removes the host node, so listeners no longer leak across tests.
| afterEach(() => { | ||
| while (bootedHosts.length) { | ||
| const host = bootedHosts.pop()!; | ||
| host.remove(); | ||
| } | ||
| if (hadVersion) | ||
| window.MUYA_VERSION = originalVersion as string; | ||
| else | ||
| delete (window as Partial<Window>).MUYA_VERSION; | ||
| }); | ||
|
|
||
| function bootMuya(markdown: string): Muya { | ||
| const host = document.createElement('div'); | ||
| document.body.appendChild(host); | ||
| const muya = new Muya(host, { markdown } as ConstructorParameters<typeof Muya>[1]); | ||
| muya.init(); | ||
| bootedHosts.push(muya.domNode); | ||
| return muya; | ||
| } |
There was a problem hiding this comment.
Good catch — fixed in 824deb2. The cleanup now tracks each booted Muya and calls muya.destroy() in afterEach, which runs eventCenter.detachAllDomEvents() (removing the document-level keydown/click listeners selection registers) and removes the host node, so listeners no longer leak across tests.
| afterEach(() => { | ||
| while (bootedHosts.length) { | ||
| const host = bootedHosts.pop()!; | ||
| host.remove(); | ||
| } | ||
| if (hadVersion) | ||
| window.MUYA_VERSION = originalVersion as string; | ||
| else | ||
| delete (window as Partial<Window>).MUYA_VERSION; | ||
| }); | ||
|
|
||
| function bootMuya(markdown: string, options: Record<string, unknown> = {}): Muya { | ||
| const host = document.createElement('div'); | ||
| document.body.appendChild(host); | ||
| const muya = new Muya(host, { | ||
| markdown, | ||
| ...options, | ||
| } as ConstructorParameters<typeof Muya>[1]); | ||
| muya.init(); | ||
| bootedHosts.push(muya.domNode); | ||
| return muya; | ||
| } |
There was a problem hiding this comment.
Good catch — fixed in 824deb2. The cleanup now tracks each booted Muya and calls muya.destroy() in afterEach, which runs eventCenter.detachAllDomEvents() (removing the document-level keydown/click listeners selection registers) and removes the host node, so listeners no longer leak across tests.
| afterEach(() => { | ||
| while (bootedHosts.length) { | ||
| const host = bootedHosts.pop()!; | ||
| host.remove(); | ||
| } | ||
| if (hadVersion) | ||
| window.MUYA_VERSION = originalVersion as string; | ||
| else | ||
| delete (window as Partial<Window>).MUYA_VERSION; | ||
| }); | ||
|
|
||
| function bootMuya(markdown: string, options: Record<string, unknown> = {}): Muya { | ||
| const host = document.createElement('div'); | ||
| document.body.appendChild(host); | ||
| const muya = new Muya(host, { | ||
| markdown, | ||
| ...options, | ||
| } as ConstructorParameters<typeof Muya>[1]); | ||
| muya.init(); | ||
| bootedHosts.push(muya.domNode); | ||
| return muya; | ||
| } |
There was a problem hiding this comment.
Good catch — fixed in 824deb2. The cleanup now tracks each booted Muya and calls muya.destroy() in afterEach, which runs eventCenter.detachAllDomEvents() (removing the document-level keydown/click listeners selection registers) and removes the host node, so listeners no longer leak across tests.
| afterEach(() => { | ||
| while (bootedHosts.length) { | ||
| const host = bootedHosts.pop()!; | ||
| host.remove(); | ||
| } | ||
| if (hadVersion) | ||
| window.MUYA_VERSION = originalVersion as string; | ||
| else | ||
| delete (window as Partial<Window>).MUYA_VERSION; | ||
| }); | ||
|
|
||
| function bootMuya(markdown: string): Muya { | ||
| const host = document.createElement('div'); | ||
| document.body.appendChild(host); | ||
| const muya = new Muya(host, { markdown } as ConstructorParameters<typeof Muya>[1]); | ||
| muya.init(); | ||
| bootedHosts.push(muya.domNode); | ||
| return muya; | ||
| } |
There was a problem hiding this comment.
Good catch — fixed in 824deb2. The cleanup now tracks each booted Muya and calls muya.destroy() in afterEach, which runs eventCenter.detachAllDomEvents() (removing the document-level keydown/click listeners selection registers) and removes the host node, so listeners no longer leak across tests.
| afterEach(() => { | ||
| while (bootedHosts.length) { | ||
| const host = bootedHosts.pop()!; | ||
| host.remove(); | ||
| } | ||
| if (hadVersion) | ||
| window.MUYA_VERSION = originalVersion as string; | ||
| else | ||
| delete (window as Partial<Window>).MUYA_VERSION; | ||
| }); | ||
|
|
||
| function bootMuya(markdown: string): Muya { | ||
| const host = document.createElement('div'); | ||
| document.body.appendChild(host); | ||
| const muya = new Muya(host, { markdown } as ConstructorParameters<typeof Muya>[1]); | ||
| muya.init(); | ||
| bootedHosts.push(muya.domNode); | ||
| return muya; | ||
| } |
There was a problem hiding this comment.
Good catch — fixed in 824deb2. The cleanup now tracks each booted Muya and calls muya.destroy() in afterEach, which runs eventCenter.detachAllDomEvents() (removing the document-level keydown/click listeners selection registers) and removes the host node, so listeners no longer leak across tests.
|
Build artifacts for PR #4407: Run: https://github.com/marktext/marktext/actions/runs/27154157934
|
…teners Address Copilot review: the parity engine specs' cleanup only removed the host DOM node, never calling `muya.destroy()`, so the `document`-level keydown/click listeners selection registers during init leaked across tests — a flake / order- dependence risk as the suite grows. Track booted Muya instances and `destroy()` each in afterEach (detaches all DOM events + removes the node). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
What
Foundation PR for the desktop migration to
@muyajs/core(#4406). It encodes all 15 confirmed functional-parity gaps as regression tests that fail ondeveloptoday (proving each gap), marked as expected-failures so both suites stay GREEN. Later fix PRs flip each entry to passing by removing its marker.This PR is tests + docs only — no behavior change.
Mechanism
packages/muya/src/**/__tests__/parity*.spec.ts): vitestit.fails(...)— asserts the correct (pre-migration) behavior, fails today (counts as pass). When a fix lands the test passes andit.failsthen errors, forcing the fixer to delete.fails.packages/desktop/test/e2e/parity-*.spec.ts): Playwrighttest.fail()— runs headless and currently fails (counts as pass). Removetest.fail()to flip green.packages/desktop/test/PARITY_QA.md): precise checklists for gaps not drivable headless (real OS clipboard bitmaps, drag-drop gestures).PGn:) so fix PRs cangrep -rn "PGn:"and flip it.packages/desktop/test/PARITY_SCOREBOARD.md.The 15 gaps and their test locations
packages/muya/src/selection/__tests__/paritySelectionChange.spec.tsit.fails·packages/desktop/test/e2e/parity-pg1-menu-state.spec.tstest.fail()packages/desktop/test/e2e/parity-source-undo-saved.spec.tsPG2:test.fail()packages/muya/src/block/gfm/taskListCheckbox/__tests__/parityAutoCheck.spec.tsit.failspackages/desktop/test/PARITY_QA.md§ PG4 (manual-QA)packages/muya/src/clipboard/__tests__/parityImagePaste.spec.tsPG5:it.fails+PARITY_QA.md§ PG5packages/muya/src/clipboard/__tests__/parityImagePaste.spec.tsPG6:it.failspackages/muya/src/state/__tests__/parityExportHtml.spec.tsPG7:it.failspackages/muya/src/state/__tests__/parityExportHtml.spec.tsPG8:it.failspackages/muya/src/clipboard/__tests__/parityCopyAsRich.spec.tsit.failspackages/muya/src/selection/__tests__/parityPreviewImage.spec.tsit.failspackages/muya/src/__tests__/parityHeadingCopyLink.spec.tsit.failspackages/muya/src/editor/__tests__/parityHideLinkPopup.spec.tsit.fails(+ positive control)packages/muya/src/__tests__/parityInsertParagraphNested.spec.tsit.failspackages/desktop/test/e2e/parity-source-undo-saved.spec.tsPG14:test.fail()packages/desktop/test/e2e/parity-source-undo-saved.spec.tsPG15:test.fail()How fix PRs flip a gap green
grep -rn "PGn:"to find the test(s).it.fails→it(muya) or deletetest.fail()(desktop e2e), or run + tick the manual-QA entry.PARITY_SCOREBOARD.md.Verification
lint(0 err) ·lint:types·check-circular·test(512 pass + 20 xfail) ·test:spec(1347 pass) — all GREEN.build:unpack·typecheck·lint(0 err) ·test(560 pass) · the 4 parity e2e (all xfail/GREEN) — all GREEN.it.fails/test.failwas confirmed to fail for the right reason (the asserted gap) by temporarily removing the marker.A small scoped eslint tweak lets the greppable
PGtest-title prefix through antfu'sprefer-lowercase-titlerule (spec files only); no safety rule relaxed.Refs #4406.
🤖 Generated with Claude Code