packages/muya/e2e/BACKLOG.md
| Phase | Theme | Tests | CI delta | Status |
|---|---|---|---|---|
| 1 | P0 smoke + key interaction skeleton (infra) | 28 (1 fixme) | ~3-4 min | ✅ landed |
| 2 | Cross-browser matrix + drag/IME | 180 (11 skipped) | +4-6 min | ✅ landed |
| 3 | Render depth + remaining blocks + security | +23 → 78 (1 fixme) | +2-3 min | ✅ landed |
| 4 | Stability / performance / a11y guardrails | +28 → 106 | +3-5 min | ✅ landed |
Phase 1 baseline (PR landing snapshot):
tests/editing/clipboard.spec.ts — test.fixme waiting for Phase 2 CDP clipboard wiring)Phase 2 landing snapshot:
editing/search-replace.spec.ts (#all) — toolbar
driver fires synchronously and the engines swallow mid-flight
selection changesinline/format-toolbar.spec.ts +
inline/shortcuts.spec.ts — Phase 1 specs rely on Chromium's
triple-click select-paragraph behaviourclipboardData on
new ClipboardEvent('paste', { clipboardData })Unlocks Firefox + WebKit, and the input/drag flows that don't survive cross-engine differences.
firefox + webkit projects in playwright.config.ts.--project=chromium filter from pnpm e2e; add pnpm e2e:firefox / pnpm e2e:webkit aliases for targeted runs.ci-e2e.yml: install all three browsers (playwright install --with-deps), bump runner concurrency.compositionstart → multiple input events with isComposing=true → compositionend. Assert that the committed text lands AFTER compositionend (mid-burst, state stays at pre-composition text).getMarkdown() returns the columns in swapped order. NOTE: the bar is a reorder tool, not a resize tool — cells don't carry a width meta, so the original BACKLOG framing ("assert column meta width changed") was based on a misreading.getMarkdown order swapped.lint:types script to e2e/package.json (currently absent because the imported @muyajs/core source pulls in __MUYA_BLOCK__ / module-augmentation globals that aren't re-declared in e2e/types.d.ts). Carried to Phase 3.e2e/types.d.ts, or include packages/core/src/types/global.d.ts from the e2e tsconfig.test.fixme in tests/editing/clipboard.spec.ts with a real paste via synthetic ClipboardEvent + populated DataTransfer. Chromium-only — Firefox nulls clipboardData on synthetic ClipboardEvents (bug 1456493) and WebKit denies the permission in headless mode. The keyboard Cmd/Ctrl+V path doesn't work either: headless Chromium has an empty OS clipboard so the keystroke fires clipboardData: null. The synthetic-DataTransfer path mirrors what the editor sees from a real paste end-to-end (same pasteHandler code path), at the cost of Chromium-only coverage. Tracked alongside Firefox/WebKit clipboard parity in Phase 3.<b> → **…**, paste <a href> → […](url), paste <table> → GFM table, paste plain text fallback.editing/search-replace.spec.ts rewrite (Firefox + WebKit both gated). Root cause: the host toolbar fires replace() synchronously and both engines swallow mid-flight DOM selection changes.inline/format-toolbar.spec.ts + inline/shortcuts.spec.ts rewrite — both rely on Chromium's triple-click select-paragraph behaviour.packages/core/src/block/base/__tests__/autoPair.spec.ts cover the composeHandler branches.Landed: 23 new tests across tests/diagrams/, tests/blocks/, tests/security/.
Local runtime ~+2s on top of Phase 1 baseline.
setContent → wait for .mu-diagram-preview svg → count path|rect mark elements to verify the chart actually rendered. (tests/diagrams/vega-lite.spec.ts, 2 tests)@startuml…@enduml round-trips through setContent + getMarkdown. plantuml.com/** is mocked via page.route for hermeticity; the spec asserts the encoded URL shape and getMarkdown preserves source. (tests/diagrams/plantuml.spec.ts, 2 tests);;; / json {}). All four delimiter styles round-trip through setContent + getMarkdown. (tests/blocks/frontmatter.spec.ts, 4 tests)<u>, <mark>, <sup>, <sub> each round-trip via the generic htmlTag renderer; <ruby> is split out because it routes through the dedicated htmlRuby renderer (mounts span.mu-ruby not *.mu-raw-html). (tests/blocks/html-inline.spec.ts, 5 tests)example.test/** to make loadImage resolve. (tests/blocks/reference-link-image.spec.ts, 4 tests)[^a] refs sharing a definition, definition appearing before vs after the first ref, and the deliberate "no auto-cleanup of orphan defs" current contract. (tests/blocks/footnote-scenarios.spec.ts, 3 tests)<script>(window).__pwned = true</script> via setContent → assert window.__pwned never set. Canary declared on Window in e2e/types.d.ts.<a href="javascript:alert(1)">x</a> → assert anchor's rendered href is either dropped or no longer contains javascript:.onerror attribute is stripped + canary not set.new MarkdownToHtml(md).generate() against same payloads → assert sanitized HTML output. Deferred to Phase 4 — current host doesn't expose MarkdownToHtml on window, and reaching for page.evaluate(() => new (await import('@muyajs/core')).MarkdownToHtml(...)) would require new host plumbing. Phase 4 can wire it onto window.__e2e and assert the static path.setContent / locale() / destroy() + new Muya() 50× → assert EventCenter listener count stays bounded. → e2e/tests/stability/listener-leak.spec.ts. Asserts on eventCenter.events.length (DOM listener array, NOT a Map as the original brief described) and eventCenter.listeners (custom pub/sub) — both stay within ±5 across 49 rebuild cycles.MutationObserver count and listeners on domNode. → deferred. muya doesn't currently expose any MutationObserver registration through the public API; would require an internal hook.setContent. → e2e/tests/stability/perf.spec.ts. Budget is currently 60 s (not the 5 s target the brief asked for) — local Chromium against the Vite dev server lands in ~20 s; the muya render path runs synchronous per-block. Phase 5 should tighten this against a production bundle.@axe-core/playwright devDep.critical violation. → .exclude(['.tools']) keeps test-harness toolbar markup out of the scan (it's unlabeled <select>/<button> only used by tests; not part of muya's a11y surface).autoPairBracket / autoPairMarkdownSyntax / autoPairQuote — full on/off matrix × representative input sequences. → e2e/tests/options/autopair.spec.ts.focusMode: true — option round-trips. → e2e/tests/options/focus-mode.spec.ts. Caveat: focusMode is currently a no-op in the implementation: the option flag and MU_FOCUS_MODE class name exist, but no render path applies the class. The spec asserts the option survives the constructor and the editor still functions; once a render path lands, tighten the spec to assert the marker class.spellcheckEnabled — assert spellcheck attribute reflects. → e2e/tests/options/spellcheck.spec.ts.disableHtml — assert raw HTML stays unrendered. → e2e/tests/options/disable-html.spec.ts.setContent('')) — cursor placement, no crash.<script> survives generate(), mounting the output into the DOM does not execute the injected script).landmark-one-main (moderate), region (moderate), scrollable-region-focusable (serious), page-has-heading-one (moderate), color-contrast (serious, slash menu). The Phase 4 a11y bar is critical-only; Phase 5 should tighten to serious+.e2e/host/index.html's .tools block has unlabeled form controls (<select id="language-select"> etc.) — excluded from axe scans in Phase 4. Add labels so we can drop the exclusion.setContent budget is 60s; against a production bundle the target should be the brief's original 5s. Wire a vite build + preview server option to e2e/ for a dedicated @perf lane.MU_FOCUS_MODE class on the editor root when focusMode: true, then tighten e2e/tests/options/focus-mode.spec.ts to assert the visual marker.MutationObserver registrations once muya exposes that surface.schedule: cron running the full matrix; PR runs a @smoke-tagged subset onlyexpect(page).toHaveScreenshot(...) for key float-positioning scenariosjson-change but ships no transport)