This repository was archived by the owner on May 29, 2026. It is now read-only.
test(e2e): Phase 3 β render depth + remaining blocks + sanitize - #237
Merged
Conversation
Lands the Phase 3 chunk of the e2e roadmap (BACKLOG row 3): render-depth
coverage for the two remaining diagram types, the four leftover block
types, and a sanitize/XSS guardrail.
23 new tests across three new directories:
tests/diagrams/
- vega-lite.spec.ts (2 tests) β SVG mounts + path/rect mark count;
`getMarkdown` round-trips the JSON spec inside a ```vega-lite fence.
- plantuml.spec.ts (2 tests) β `page.route('**/plantuml.com/**')`
hermetic stub returns an inline SVG so the spec asserts the encoded
URL shape + getMarkdown round-trip without external network.
tests/blocks/
- frontmatter.spec.ts (4 tests) β YAML (---), TOML (+++),
JSON (;;;), JSON ({β¦}) styles each render + round-trip with the
correct delimiter.
- html-inline.spec.ts (5 tests) β <u>, <mark>, <sup>, <sub>
route through `htmlTag.ts` and mount with `.mu-raw-html`. <ruby>
is split out: it uses the dedicated `htmlRuby.ts` renderer that
mounts `span.mu-ruby` wrapping the actual `<ruby>` via
`htmlToVNode(raw)`.
- reference-link-image.spec.ts (4 tests) β PR-16 defense-in-depth.
`[label][ref]` resolves the href from the labels Map; case-
insensitive label normalization holds; reference images mock
`example.test/**` so `loadImage` resolves and the renderer's
success branch mounts the real `<img>`.
- footnote-scenarios.spec.ts (3 tests) β multiple refs sharing
one definition, definition appearing before vs after the first
reference, and the deliberate "orphan def survives ref deletion"
contract.
tests/security/
- sanitize.spec.ts (3 tests) β DOMPurify guardrails on the
`setContent` β html-block render path: `<script>` payloads never
execute (canary `window.__pwned` stays undefined), `javascript:`
hrefs are stripped or rewritten, `onerror=` attributes are
dropped from `<img>`.
Infrastructure touched (Phase 3-scoped only):
- `e2e/tests/helpers/selectors.ts` β adds frontmatter, referenceLink,
referenceImage, rawHtml, diagramContainer locators.
- `e2e/types.d.ts` β declares `window.__pwned?: boolean` canary used
by the sanitize spec, alongside the existing `window.__e2e` and
`window.muya` augmentations.
- `e2e/BACKLOG.md` β Phase 3 row marked β
landed; checklist items
flipped to checked with file references. Phase 2 and Phase 4 rows
untouched per the parallel-agent agreement.
Deferred (with reasons logged in BACKLOG):
- Static-export sanitize via `new MarkdownToHtml(md).generate()` β
the host doesn't expose `MarkdownToHtml` on `window`, and reaching
for it via dynamic `import()` inside `page.evaluate` would require
new host plumbing. Punted to Phase 4 where the host can be extended.
Validation:
- `pnpm e2e` 78 passed + 1 skipped (was 55 + 1; +23)
- `pnpm test` 386 unit tests pass (unchanged)
- `pnpm exec eslint e2e` β only the pre-existing `host/main.ts` error
that lives on master (host/** is out of Phase 3 scope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds Phase 3 of the muya E2E roadmap by expanding Playwright coverage for diagram rendering depth, remaining block types, and sanitization/XSS guardrails in the live editor setContent β render path.
Changes:
- Added new E2E suites for Vega-Lite + PlantUML diagrams and round-trip via
getMarkdown. - Added E2E coverage for frontmatter delimiter styles, inline HTML tags, reference link/image behavior, and footnote scenarios.
- Added initial XSS/sanitization E2E guardrails plus a
Window.__pwnedcanary type and updated roadmap status.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| e2e/types.d.ts | Adds Window.__pwned canary type for XSS tests. |
| e2e/tests/security/sanitize.spec.ts | New sanitization/XSS E2E tests for html-block rendering. |
| e2e/tests/helpers/selectors.ts | Extends shared selector constants for new block/diagram coverage. |
| e2e/tests/diagrams/vega-lite.spec.ts | Adds Vega-Lite render + markdown round-trip tests. |
| e2e/tests/diagrams/plantuml.spec.ts | Adds PlantUML render tests with hermetic network stubbing + round-trip. |
| e2e/tests/blocks/reference-link-image.spec.ts | Adds reference link/image resolution + round-trip tests (with image route stubbing). |
| e2e/tests/blocks/html-inline.spec.ts | Adds inline HTML tag rendering + round-trip tests (incl. ruby path). |
| e2e/tests/blocks/frontmatter.spec.ts | Adds frontmatter delimiter-style round-trip tests via state. |
| e2e/tests/blocks/footnote-scenarios.spec.ts | Adds multi-ref, ordering, and orphan-definition footnote scenarios. |
| e2e/BACKLOG.md | Marks Phase 3 as landed and documents what was covered/deferred. |
π‘ Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+28
to
+45
| test('<script> in an html-block does not execute', async ({ page }) => { | ||
| const payload = '<script>(window).__pwned = true;</script>'; | ||
| await page.evaluate((text) => { | ||
| const state: TState[] = [{ name: 'html-block', text }]; | ||
| window.muya!.setContent(state); | ||
| }, payload); | ||
|
|
||
| // Sync barrier: html-block mounts an `.mu-html-block` and (eventually) | ||
| // an `.mu-html-preview` child. The script should never run. | ||
| await expect(page.locator(editor.htmlBlock).first()).toBeVisible(); | ||
|
|
||
| // Give the event loop a tick β DOMPurify strips inert nodes, but | ||
| // we want to be sure nothing async fires later. | ||
| await page.waitForTimeout(100); | ||
|
|
||
| const pwned = await page.evaluate(() => (window as Window & { __pwned?: boolean }).__pwned); | ||
| expect(pwned).toBeUndefined(); | ||
| }); |
Comment on lines
+39
to
+44
| // Give the event loop a tick β DOMPurify strips inert nodes, but | ||
| // we want to be sure nothing async fires later. | ||
| await page.waitForTimeout(100); | ||
|
|
||
| const pwned = await page.evaluate(() => (window as Window & { __pwned?: boolean }).__pwned); | ||
| expect(pwned).toBeUndefined(); |
| return 0; | ||
| return root.querySelectorAll('path, rect').length; | ||
| }); | ||
| expect(markCount).toBeGreaterThan(0); |
4 tasks
Jocs
added a commit
that referenced
this pull request
May 21, 2026
Adds 28 new Playwright specs across five buckets, landing the full Phase 4 BACKLOG scope on the muya-e2e suite. Total test count grows from 54 passed / 1 skipped to 83 passed / 1 skipped. Stability - stability/listener-leak.spec.ts: direct regression for PR-17 (commit 39852a6). 50Γ setContent/locale/destroy/rebuild loop; asserts EventCenter `events` (DOM) and `listeners` (pub/sub) arrays stay within Β±5 across rebuild cycles. - stability/perf.spec.ts: 10k-paragraph setContent timed via performance.now(); 60s budget against the unbundled Vite dev server (~20s observed locally). Tagged @Perf for future PR-time / nightly split. Accessibility - a11y/host-scan.spec.ts: @axe-core/playwright scan of the clean host plus IFT, slash menu, link tools, image tools, and table tools open states. Fails on `critical` only; non-critical violations logged for Phase 5 triage. Excludes `.tools` (host test-harness toolbar, not muya's a11y surface). Option matrix - options/autopair.spec.ts: full on/off matrix for autoPairBracket / autoPairMarkdownSyntax / autoPairQuote plus all-off combo. - options/focus-mode.spec.ts: option round-trips through the constructor (no visual marker assertion β focusMode is currently a no-op in the render path; tightened spec deferred to Phase 5). - options/spellcheck.spec.ts: asserts `spellcheck` attribute on `.mu-editor` root for both states. - options/disable-html.spec.ts: asserts `.mu-disable-html-render` class and that raw HTML stays as escaped source vs. live DOM. Edges - edges/empty-and-tiny.spec.ts: setContent(''), setContent('a'), 10Γ rapid setContent without awaits. Cursor placement and final-state correctness. Static export - export/markdown-to-html.spec.ts: heading/list/code-block/KaTeX shape; mermaid container; script-injection sanitised away (Phase 3 deferred this β landed here). Includes the payload-execution sentinel that Phase 3 PR #237 marked for Phase 4. Infrastructure - e2e/host/main.ts: exposes window.MarkdownToHtml and window.__e2e.rebuildMuya(opts) helper for option-matrix specs. - e2e/types.d.ts: typed augmentation of Window for the new globals. - e2e/package.json: adds @axe-core/playwright devDep. - e2e/tests/helpers/selectors.ts: adds htmlDisabled and htmlPreview selectors; documents the .mu-disable-html-render flag class. Phase 5 follow-ups captured in e2e/BACKLOG.md: triage non-critical axe violations, label the host toolbar so the scan exclusion can drop, tighten the focusMode spec once the render path applies the marker class, add a MutationObserver leak guard, and wire a perf lane against a production bundle. Validation: - pnpm e2e β 83 passed, 1 skipped (Phase 2 clipboard fixme). - pnpm test β 386 passed (unit baseline untouched). - pnpm exec eslint e2e β clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Jocs
added a commit
that referenced
this pull request
May 21, 2026
Adds 28 new Playwright specs across five buckets, landing the full Phase 4 BACKLOG scope on the muya-e2e suite. Total test count grows from 54 passed / 1 skipped to 83 passed / 1 skipped. Stability - stability/listener-leak.spec.ts: direct regression for PR-17 (commit 39852a6). 50Γ setContent/locale/destroy/rebuild loop; asserts EventCenter `events` (DOM) and `listeners` (pub/sub) arrays stay within Β±5 across rebuild cycles. - stability/perf.spec.ts: 10k-paragraph setContent timed via performance.now(); 60s budget against the unbundled Vite dev server (~20s observed locally). Tagged @Perf for future PR-time / nightly split. Accessibility - a11y/host-scan.spec.ts: @axe-core/playwright scan of the clean host plus IFT, slash menu, link tools, image tools, and table tools open states. Fails on `critical` only; non-critical violations logged for Phase 5 triage. Excludes `.tools` (host test-harness toolbar, not muya's a11y surface). Option matrix - options/autopair.spec.ts: full on/off matrix for autoPairBracket / autoPairMarkdownSyntax / autoPairQuote plus all-off combo. - options/focus-mode.spec.ts: option round-trips through the constructor (no visual marker assertion β focusMode is currently a no-op in the render path; tightened spec deferred to Phase 5). - options/spellcheck.spec.ts: asserts `spellcheck` attribute on `.mu-editor` root for both states. - options/disable-html.spec.ts: asserts `.mu-disable-html-render` class and that raw HTML stays as escaped source vs. live DOM. Edges - edges/empty-and-tiny.spec.ts: setContent(''), setContent('a'), 10Γ rapid setContent without awaits. Cursor placement and final-state correctness. Static export - export/markdown-to-html.spec.ts: heading/list/code-block/KaTeX shape; mermaid container; script-injection sanitised away (Phase 3 deferred this β landed here). Includes the payload-execution sentinel that Phase 3 PR #237 marked for Phase 4. Infrastructure - e2e/host/main.ts: exposes window.MarkdownToHtml and window.__e2e.rebuildMuya(opts) helper for option-matrix specs. - e2e/types.d.ts: typed augmentation of Window for the new globals. - e2e/package.json: adds @axe-core/playwright devDep. - e2e/tests/helpers/selectors.ts: adds htmlDisabled and htmlPreview selectors; documents the .mu-disable-html-render flag class. Phase 5 follow-ups captured in e2e/BACKLOG.md: triage non-critical axe violations, label the host toolbar so the scan exclusion can drop, tighten the focusMode spec once the render path applies the marker class, add a MutationObserver leak guard, and wire a perf lane against a production bundle. Validation: - pnpm e2e β 83 passed, 1 skipped (Phase 2 clipboard fixme). - pnpm test β 386 passed (unit baseline untouched). - pnpm exec eslint e2e β clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Jocs
added a commit
that referenced
this pull request
May 21, 2026
* test(e2e): Phase 4 β stability / perf / a11y guardrails Adds 28 new Playwright specs across five buckets, landing the full Phase 4 BACKLOG scope on the muya-e2e suite. Total test count grows from 54 passed / 1 skipped to 83 passed / 1 skipped. Stability - stability/listener-leak.spec.ts: direct regression for PR-17 (commit 39852a6). 50Γ setContent/locale/destroy/rebuild loop; asserts EventCenter `events` (DOM) and `listeners` (pub/sub) arrays stay within Β±5 across rebuild cycles. - stability/perf.spec.ts: 10k-paragraph setContent timed via performance.now(); 60s budget against the unbundled Vite dev server (~20s observed locally). Tagged @Perf for future PR-time / nightly split. Accessibility - a11y/host-scan.spec.ts: @axe-core/playwright scan of the clean host plus IFT, slash menu, link tools, image tools, and table tools open states. Fails on `critical` only; non-critical violations logged for Phase 5 triage. Excludes `.tools` (host test-harness toolbar, not muya's a11y surface). Option matrix - options/autopair.spec.ts: full on/off matrix for autoPairBracket / autoPairMarkdownSyntax / autoPairQuote plus all-off combo. - options/focus-mode.spec.ts: option round-trips through the constructor (no visual marker assertion β focusMode is currently a no-op in the render path; tightened spec deferred to Phase 5). - options/spellcheck.spec.ts: asserts `spellcheck` attribute on `.mu-editor` root for both states. - options/disable-html.spec.ts: asserts `.mu-disable-html-render` class and that raw HTML stays as escaped source vs. live DOM. Edges - edges/empty-and-tiny.spec.ts: setContent(''), setContent('a'), 10Γ rapid setContent without awaits. Cursor placement and final-state correctness. Static export - export/markdown-to-html.spec.ts: heading/list/code-block/KaTeX shape; mermaid container; script-injection sanitised away (Phase 3 deferred this β landed here). Includes the payload-execution sentinel that Phase 3 PR #237 marked for Phase 4. Infrastructure - e2e/host/main.ts: exposes window.MarkdownToHtml and window.__e2e.rebuildMuya(opts) helper for option-matrix specs. - e2e/types.d.ts: typed augmentation of Window for the new globals. - e2e/package.json: adds @axe-core/playwright devDep. - e2e/tests/helpers/selectors.ts: adds htmlDisabled and htmlPreview selectors; documents the .mu-disable-html-render flag class. Phase 5 follow-ups captured in e2e/BACKLOG.md: triage non-critical axe violations, label the host toolbar so the scan exclusion can drop, tighten the focusMode spec once the render path applies the marker class, add a MutationObserver leak guard, and wire a perf lane against a production bundle. Validation: - pnpm e2e β 83 passed, 1 skipped (Phase 2 clipboard fixme). - pnpm test β 386 passed (unit baseline untouched). - pnpm exec eslint e2e β clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(e2e): clarify perf-spec timeout budgets (Copilot review) The header said "Budget = 60_000ms (= the per-test timeout in playwright.config)" β wrong on both counts: the config default is 30_000ms, the per-spec override is 120_000ms, and the 60_000ms is the setContent assertion budget (not the test timeout). Rewrote the header to list the three numbers separately and explain why they differ, plus expanded the inline comment above setTimeout to note it's a 4Γ ceiling above the assertion budget so regressions surface as a meaningful expect failure, not a stack-trace timeout. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(e2e): use muya.focus() instead of click+wait for autopair tests CI's bundled Chromium-for-Testing fails 7/7 autopair tests because the sync barrier β click empty paragraph then waitForFunction activeContentBlock != null β never resolves. Root cause: clicking an *empty* `.mu-paragraph` lands the browser selection anchor on the paragraph element itself rather than on a text node inside it. muya/editor/index.ts dispatchEvents then sees no anchorBlock from selection.getSelection() and explicitly sets activeContentBlock = null before returning, so the wait times out at 30s. Replaced the click+wait pattern with: 1. window.muya.focus() β uses muya's API to setCursor(0, 0, β¦) on the first leaf block, establishing a real Range with a text-node anchor. 2. window.muya.domNode.focus() β ensures DOM activeElement is the contenteditable so subsequent page.keyboard.type() lands there. No behavior change locally (7/7 still pass via system Chrome). Fixes CI failures on bundled Chromium. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 of the 4-phase muya E2E roadmap (
e2e/BACKLOG.md). Adds 23 tests across three new directories, taking the suite from 55 passed + 1 skipped β 78 passed + 1 skipped.page.route('**/plantuml.com/**')for hermeticity; Vega-Lite renders client-side and we count SVGpath|rectmarks.<u>,<mark>,<sup>,<sub>,<ruby>), Reference link/image round-trip (PR-16 defense-in-depth incl. case-insensitive labels), and footnote scenarios (multiple refs, definition before/after, orphan-def survival).setContentβ html-block render path:<script>payloads never execute (canarywindow.__pwnedstaysundefined),javascript:hrefs are neutralized,onerror=attributes are dropped.What landed
Notable engineering choices
plantumlEncoderforwards towww.plantuml.com/plantuml. Real network would be flaky on CI and would leak telemetry;page.route('**/plantuml.com/**')returns a stubbed SVG. We still verify the encoded URL shape so the integration contract is asserted.page.routetoo.loadImageAsynconly mounts a real<img>after the load resolves.example.test/**is a fake TLD β without a route stub the load rejects, the image is removed, and the spec hangs. We serve a 1Γ1 PNG to keep the success branch live.<ruby>is split out. It routes through the dedicatedhtmlRuby.tsrenderer that mountsspan.mu-ruby(with the actual<ruby>DOM injected viahtmlToVNode(raw)), unlike the generichtmlTag.tspath that mounts<u>.mu-raw-htmletc.@axe-core/playwrightbelongs to Phase 4 per the agent-split agreement; not added here.e2e/host/**is out of Phase 3 scope. The lone pre-existinghost/main.tseslint error from master is unchanged.Deferred
new MarkdownToHtml(md).generate()against the same payloads). Reaching it from a spec needs new host plumbing β punted to Phase 4 alongside the rest of the static-export coverage. Tracked inBACKLOG.mdunder Phase 3 Β§ Sanitize / XSS.Test plan
pnpm e2eβ 78 passed, 1 skipped (clipboard, Phase 2)pnpm testβ 386 unit tests pass (unchanged)pnpm exec eslint e2eβ only the pre-existinghost/main.tserror remains (out of scope)π€ Generated with Claude Code