Skip to content
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
Jocs merged 1 commit into
masterfrom
test/e2e-phase-3-render-and-sanitize
May 21, 2026
Merged

test(e2e): Phase 3 β€” render depth + remaining blocks + sanitize#237
Jocs merged 1 commit into
masterfrom
test/e2e-phase-3-render-and-sanitize

Conversation

@Jocs

@Jocs Jocs commented May 21, 2026

Copy link
Copy Markdown
Member

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.

  • Render depth β€” Vega-Lite + PlantUML diagrams. PlantUML is mocked via page.route('**/plantuml.com/**') for hermeticity; Vega-Lite renders client-side and we count SVG path|rect marks.
  • Remaining block types β€” Frontmatter (all 4 delimiter styles), inline HTML tags (<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).
  • Sanitize / XSS β€” three DOMPurify guardrails on the setContent β†’ html-block render path: <script> payloads never execute (canary window.__pwned stays undefined), javascript: hrefs are neutralized, onerror= attributes are dropped.

What landed

e2e/tests/
  diagrams/
    vega-lite.spec.ts            (2 tests)
    plantuml.spec.ts             (2 tests)
  blocks/
    frontmatter.spec.ts          (4 tests)
    html-inline.spec.ts          (5 tests)
    reference-link-image.spec.ts (4 tests)
    footnote-scenarios.spec.ts   (3 tests)
  security/
    sanitize.spec.ts             (3 tests)
  helpers/selectors.ts           (extended)
e2e/types.d.ts                   (added __pwned canary)
e2e/BACKLOG.md                   (Phase 3 row + checklist marked done)

Notable engineering choices

  • PlantUML mocked, not real network. plantumlEncoder forwards to www.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.
  • Reference images need page.route too. loadImageAsync only 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 dedicated htmlRuby.ts renderer that mounts span.mu-ruby (with the actual <ruby> DOM injected via htmlToVNode(raw)), unlike the generic htmlTag.ts path that mounts <u>.mu-raw-html etc.
  • No new deps. @axe-core/playwright belongs to Phase 4 per the agent-split agreement; not added here.
  • No host touched. e2e/host/** is out of Phase 3 scope. The lone pre-existing host/main.ts eslint error from master is unchanged.

Deferred

  • Static-export sanitize (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 in BACKLOG.md under 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-existing host/main.ts error remains (out of scope)
  • No Phase 2 / Phase 4 files touched (verified against the parallel-agent file-scope contract)

πŸ€– Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings May 21, 2026 01:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.__pwned canary 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);
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
Jocs merged commit 231ad81 into master May 21, 2026
7 checks passed
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>
@Jocs
Jocs deleted the test/e2e-phase-3-render-and-sanitize branch May 21, 2026 04:37
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants