Skip to main content

How to Test Rich Text and WYSIWYG Editors

Short answer

WYSIWYG editors fail on contenteditable input quirks, toolbar state desync, and unsafe pasted HTML—not on whether bold looks bold in the iframe. Assert sanitized HTML via server probe after save, use keyboard.type or evaluate for contenteditable, and test paste XSS paths explicitly in CMS and email-builder flows.

Part of Testing Guides by UI patterns.

Who this is for

Teams shipping CMS bodies, email builders, comment boxes, policy editors, or in-app messaging with TipTap, ProseMirror, Quill, Slate, CKEditor, or Draft.js. Any contenteditable surface with toolbar buttons and paste from Word/Google Docs.

Why testing rich text editors matters

Editor bugs become security and brand incidents:

  • XSS via paste<script> or onerror attributes stored and rendered on public pages.
  • Data loss — toolbar shows bold but saved HTML is plain text; marketing publishes broken templates.
  • Compliance — legal disclaimers stripped on paste; published policy missing mandatory clauses.
  • Email rendering — inline styles stripped differently in preview vs sent message.
  • Collaboration conflicts — last save wins over concurrent edits; audit trail wrong.

Probe stored HTML or JSON document model—editor DOM is not the source of truth.

Complexity map

ScenarioEdge caseWhy tests breakApproach
fill() on contenteditableClears formattingEmpty saveclick + keyboard.type
Toolbar toggleState not appliedFalse boldAssert probe <strong>
Paste from WordBloated markupSanitizer strips allFixture .docx HTML paste
XSS paste<img onerror>Stored scriptProbe sanitized output
Link insertionjavascript: URLsOpen redirectProbe href protocol
Mention/autocompletePopup intercepts keysIncomplete mentionType @ + select option
Image uploadBase64 vs CDN URLHuge DB rowssetInputFiles + probe URL
Undo/redoStack desyncWrong content savedSave after undo; probe
Empty document<p><br></p>Validation failProbe allows empty vs not
Read-only modeToolbar still activeEdits persistProbe 403 on save
Markdown modeDual representationHTML/md mismatchProbe canonical format
i18n RTLCaret positionGarbled insertExploreChimp RTL pass

Typing in contenteditable

const editor = page.locator('[contenteditable=true]').first();
await editor.click();
await page.keyboard.type('Hello **world**'); // or use toolbar
await page.getByRole('button', { name: 'Bold' }).click();
await page.keyboard.type(' important');
await page.getByRole('button', { name: 'Save' }).click();

const doc = await request.get(`/api/test/documents/${docId}`).then(r => r.json());
expect(doc.html).toContain('<strong>');
expect(doc.html).not.toMatch(/<script/i);

For ProseMirror/TipTap, some teams set content in test env only:

await page.evaluate(({ html }) => {
window.__TEST_EDITOR__.commands.setContent(html);
}, { html: '<p>Fixture</p>' });

Paste sanitization (security-critical)

const malicious = '<p>Hi</p><img src=x onerror=alert(1)>';
await editor.click();
await page.evaluate(({ html }) => {
const dt = new DataTransfer();
dt.setData('text/html', html);
document.querySelector('[contenteditable]').dispatchEvent(
new ClipboardEvent('paste', { clipboardData: dt, bubbles: true })
);
}, { html: malicious });
await page.getByRole('button', { name: 'Save' }).click();
const saved = await request.get(`/api/test/documents/${docId}`).then(r => r.json());
expect(saved.html).not.toMatch(/onerror/i);
expect(saved.html).not.toMatch(/<script/i);

Always probe server-side sanitizer—client-only DOMPurify can differ from API.

Toolbar and format persistence

ActionUI check (optional)Probe assert
Boldaria-pressed=true on button<strong> or font-weight in HTML
HeadingBlock tag in editor<h2> in stored doc
LinkDialog URL fieldhref normalized https
ListOL/UL visible<ol><li> structure

CI checklist

  1. One XSS paste negative per public-facing editor
  2. Save → probe HTML for every critical toolbar action
  3. Avoid fill() on contenteditable unless app explicitly supports it
  4. Image uploads via setInputFiles on editor upload hook
  5. CMS publish flow cross-check with CMS publishing guide
  6. ExploreChimp on RTL locales for layout-breaking toolbars

Anti-patterns

Anti-patternWhy it failsBetter approach
Assert editor.innerHTML onlyUnsaved stateProbe after save
Skip paste testsXSS in prodClipboardEvent fixture
Screenshot toolbarTheme flakeProbe HTML tags
fill() long contentBreaks mentionskeyboard.type / setContent
Trust client sanitizer aloneAPI differsServer probe
Test every toolbar iconLow ROIRisk-ranked formats

Example scenario

Situation: Marketing pastes formatted content from Word into CMS body and publishes.

Expected outcome: Public page shows headings and lists; scripts stripped; CDN serves published HTML only.

Why UI-only automation breaks: Editor preview looks fine; public page missing lists because sanitizer removed tags.

  1. Arrange: Seed draft doc id; use Word-export HTML fixture in repo.
  2. Act: Paste fixture, save draft, publish workflow.
  3. Assert: Probe draft HTML retains lists; public probe has no script; live URL matches published slug.

TestChimp workflow: Instrument cms_save with content_source=paste|typed; prioritize paste scenarios if prod paste share is high.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

How do I type in contenteditable in Playwright?

Click the editor focus target, use page.keyboard.type for short text, or page.evaluate to call editor setContent in test env. Avoid fill() unless your app supports it on contenteditable.

Should I assert editor DOM or saved HTML?

Saved HTML or JSON document model via probe after Save. Editor DOM can look correct before autosave or sanitizer runs.

How do I test paste from Word safely?

Store representative bloated HTML fixtures in repo, dispatch ClipboardEvent paste, save, probe server output for retained structure and no script handlers.

How do I test image upload in editors?

Use setInputFiles on the editor file input or toolbar upload button, save, probe that stored content references CDN URL not giant base64 blob.

How do mentions and slash commands work in E2E?

Type trigger character, wait for popup, click option with getByRole. Probe stored mention node id—not just visible chip.

How does CMS publishing overlap with WYSIWYG tests?

WYSIWYG tests prove content integrity; CMS guide covers draft/preview/public separation—combine probe HTML with public URL cache checks.

Can ExploreChimp find editor UX issues?

Yes—especially RTL overflow and toolbar overlap on narrow viewports; convert sessions to SmartTests with paste fixtures.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo