test(e2e): Phase 4 — stability / perf / a11y - #238
Conversation
There was a problem hiding this comment.
Pull request overview
Adds Phase 4 coverage to the muya-e2e Playwright suite, expanding stability/perf/a11y guardrails, option-matrix coverage, edge-input regressions, and static MarkdownToHtml export validation. This extends the repo’s automated end-to-end verification surface while adding minimal host plumbing to support the new scenarios.
Changes:
- Add
@axe-core/playwrightand introduce critical-only axe scans for the host + key floating UI states. - Add new e2e specs for stability (listener leak, perf smoke), options (auto-pair matrix, focusMode/spellcheck/disableHtml), edge inputs, and
MarkdownToHtmlexport + XSS sanitization. - Extend the e2e host with
window.MarkdownToHtmlandwindow.__e2e.rebuildMuya(opts)to support option-matrix and export tests; update BACKLOG status.
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks @axe-core/playwright and its transitive axe-core dependency. |
| e2e/package.json | Adds @axe-core/playwright dev dependency for accessibility scans. |
| e2e/types.d.ts | Extends Window typings for MarkdownToHtml and __e2e.rebuildMuya used by new specs. |
| e2e/host/main.ts | Exposes window.MarkdownToHtml and adds a rebuild helper to recreate Muya with different options. |
| e2e/tests/stability/perf.spec.ts | Adds a 10k-paragraph setContent perf smoke + scroll reachability check (@Perf). |
| e2e/tests/stability/listener-leak.spec.ts | Adds rebuild-loop regression test to ensure EventCenter listeners/dom events stay bounded. |
| e2e/tests/options/autopair.spec.ts | Adds on/off matrix tests for auto-pair options using rebuild helper. |
| e2e/tests/options/focus-mode.spec.ts | Validates focusMode option round-trip and editor usability after rebuild. |
| e2e/tests/options/spellcheck.spec.ts | Asserts editor root spellcheck attribute reflects option. |
| e2e/tests/options/disable-html.spec.ts | Asserts disableHtml prevents live HTML rendering in html-block preview. |
| e2e/tests/helpers/selectors.ts | Adds selector for the disable-html wrapper class. |
| e2e/tests/export/markdown-to-html.spec.ts | Adds export pipeline shape assertions and script-injection sanitization verification. |
| e2e/tests/edges/empty-and-tiny.spec.ts | Adds edge-case setContent tests (empty, 1-char, rapid setContent loop). |
| e2e/tests/a11y/host-scan.spec.ts | Adds axe scans for clean host and various UI states; fails on critical only. |
| e2e/BACKLOG.md | Marks Phase 4 items landed and documents deferred follow-ups. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * Budget = 60_000ms (= the per-test timeout in playwright.config). The | ||
| * goal is to catch a 5-10× regression on this path, not to pin a number. | ||
| * A future Phase-5 nightly job can tighten this against a production | ||
| * build. | ||
| * | ||
| * Tagged @perf so a future Phase-2 CI config can `--grep-invert "@perf"` | ||
| * for the PR-time runs and keep this in a nightly schedule. | ||
| */ | ||
| test.describe('stability / perf smoke @perf', () => { | ||
| // The setContent of 10k paragraphs alone routinely takes 15-25s on the | ||
| // Vite dev server, which exceeds the default 30s suite timeout. Bump | ||
| // the per-test timeout for this spec only. |
There was a problem hiding this comment.
Fair point — the comment conflated three different numbers. Fixed in 360cf35: rewrote the header to list each timeout separately and explain why they differ:
- playwright.config default
timeout: 30_000— used by every other spec test.setTimeout(120_000)(this spec only) — wide ceiling so regressions surface as a meaningfulexpectfailure rather than a Playwright stack-trace timeoutexpect(result.ms).toBeLessThan(60_000)— the actual setContent assertion budget; this is the one that catches a 5-10× regression
Also expanded the inline comment above setTimeout to call out that 120s is intentionally 4× the assertion budget.
aceb6aa to
360cf35
Compare
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>
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>
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>
ef255a3 to
26f235f
Compare
* test(e2e): skip 10k-paragraph perf budget on webkit and firefox The 60s budget added in #238 was calibrated against chromium (~20s observed locally) before the cross-browser matrix landed in #239. WebKit on ubuntu-latest against the Vite dev server consistently runs 108-120s; firefox 62-68s. Both blow the budget on every run, and the 3-attempt retry cycle has been pushing the e2e job past its 15min cap (#240 was cancelled by the GHA timeout for this reason). Skip the perf assertion on non-chromium browsers as a short-term fix so PR CI can stay green. The longer-term plan, already documented in the file header, is to move @Perf tests to a nightly schedule against a production bundle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): exclude @Perf tests from PR-time e2e job The per-browser skip added in the first iteration of this PR turned out to be incomplete: chromium on GHA ubuntu-latest also overshoots the 60s budget (~70s observed) once running against the Vite dev server, not just the webkit ~115s and firefox ~65s seen in the prior runs. All three browsers blow the budget; only the local M-class macOS Chromium baseline (~20s) actually fits. Switch to the project's already-documented plan (see file header): exclude @perf-tagged tests from PR-time CI via --grep-invert. Future @Perf tests automatically pick up the same treatment, and the test file stays free of skip logic. The follow-up to move @Perf onto a nightly schedule against a production bundle becomes a small workflow addition rather than a rewrite. Revert the per-browser test.skip in perf.spec.ts so the local `pnpm e2e` invocation still runs the regression guard on all three browsers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Lands the full Phase 4 BACKLOG of the muya-e2e suite: 28 new Playwright specs across stability, perf, accessibility, option matrix, edge inputs, and the static
MarkdownToHtmlexport pipeline. Total test count grows from 54 passed / 1 skipped → 83 passed / 1 skipped.setContentperf smoke (tagged@perf).@axe-core/playwrightscans of the clean host plus IFT, slash menu, link tools, image tools, and table tools open states. Fails oncriticalonly; non-critical violations logged for Phase 5 triage.autoPairBracket/autoPairMarkdownSyntax/autoPairQuote, plusfocusMode,spellcheckEnabled, anddisableHtml.setContentwithout awaits.window.MarkdownToHtmlpresence, generate shape (heading / list / code-block / KaTeX / mermaid), and the script-injection sanitization test that Phase 3 PR test(e2e): Phase 3 — render depth + remaining blocks + sanitize #237 deferred — DOMPurify strips<script>and mounting the output in the DOM does not execute the payload.window.MarkdownToHtmlandwindow.__e2e.rebuildMuya(opts)so option-matrix specs can rebuild Muya with differentIMuyaOptions. Types added toe2e/types.d.ts— no(window as any)anywhere.Caveats / known deferrals (captured in
e2e/BACKLOG.md)vite build+ preview lane for@perf.focusMode: option round-trips through the constructor, but the spec doesn't assert a visual marker —MU_FOCUS_MODEclass is declared in core but no render path applies it today. Spec will tighten once the render path lands.criticalonly for Phase 4. Non-critical findings (landmark-one-main,region,scrollable-region-focusable,page-has-heading-one,color-contrast) are surfaced viaconsole.logfor Phase 5 triage.MutationObserverregistration through the public API yet.Test plan
pnpm e2e— 83 passed, 1 skipped (Phase 2 clipboardtest.fixme).pnpm test— 386 passed (unit suite untouched).pnpm exec eslint e2e— clean.🎭 CI E2Eworkflow green on the PR.🤖 Generated with Claude Code