Paste is one of the easiest ways to break a rich text editor. It crosses boundaries that normal typing never touches, HTML structure, clipboard formats, browser security rules, keyboard shortcuts, and editor-specific cleanup logic. If you are trying to test paste handling in rich text editors, the real question is not “did text appear”. It is “did the editor preserve the right content, strip the wrong content, and keep undo/redo predictable across browsers?”

That makes clipboard sanitization testing a different problem from ordinary form input testing. A plain type() call exercises key events. Pasting exercises clipboard transfer, paste or beforeinput handling, DOM mutation, selection restoration, and often a sanitization pipeline that rewrites incoming HTML before it reaches the document.

The most useful tests here verify behavior at the editor boundary, not internal implementation details. You want confidence that the user can paste, edit, undo, and recover without unsafe markup or broken selection state.

What needs to be true after a paste

A rich text editor usually supports several distinct paste modes, even if the UI exposes only one paste shortcut.

1) Plain-text paste

This is the safest baseline. When a user pastes unformatted text, the editor should insert text only, preserve line breaks as expected, and avoid importing external styles, classes, or inline HTML.

2) HTML paste

If the clipboard contains HTML, the editor may allow some formatting, such as bold, italic, lists, and links. It should still reject unsafe or unsupported markup, such as scripts, event handlers, embedded objects, or pasted CSS that can affect layout unexpectedly.

3) Sanitized rich paste

Many editors allow a controlled subset of HTML. That means the result is not “raw HTML in, raw HTML out”. It is “input transformed through an allowlist.” Your tests should verify that transformation.

4) Undo and redo after paste

Undo behavior matters because paste is often followed by quick edits. The editor should treat paste as a meaningful unit of history, so Ctrl+Z or Cmd+Z reverts the insertion cleanly, and redo restores it without duplicating content or losing selection.

5) Cross-browser consistency

Clipboard behavior differs between Chromium, Firefox, and WebKit, especially when automation is involved. The browser may block direct clipboard access unless permissions, user activation, or a trusted gesture are present. See the Clipboard API and events and the HTML editing surface sections of the HTML Standard for the underlying model.

Test cases that actually catch regressions

A good suite does not need dozens of nearly identical scenarios. It needs a small set of cases that isolate the failure modes.

Scenario What to verify Common failure
Plain text paste Text is inserted, line breaks preserved as designed HTML tags leak into the editor, line breaks collapse
Allowed HTML paste Allowed tags and attributes survive sanitization Bold, lists, links, or line breaks are lost
Blocked markup Scripts, iframes, handlers, styles, or objects are stripped Unsafe attributes remain in DOM
Paste over selection Selected content is replaced, cursor lands correctly Paste inserts at wrong range or duplicates text
Undo after paste One undo step reverts the paste cleanly Undo removes too much or too little
Redo after undo Redo restores paste and selection Redo duplicates nodes or fails silently
Repeated pastes State remains stable after multiple insertions Selection drifts, history stack corrupts
Browser variants Same user-visible result in target browsers Safari or Firefox diverges from Chromium

How paste events are supposed to work

For automation and debugging, it helps to distinguish the browser events involved.

  • paste, a clipboard event that can be intercepted in the page
  • beforeinput, which may expose inputType such as insertFromPaste
  • input, which fires after the DOM changes
  • selection and range updates, which determine where content lands

The Input Events specification is useful here because many editors use beforeinput to inspect or veto paste before the browser mutates the document. That is important for sanitization, because preventing unsafe content early is usually simpler than cleaning up DOM after insertion.

Distinguish browser-page limits from automation limits

A browser page may not be allowed to read clipboard contents freely, but your automation framework can still trigger a paste gesture or inject content through a supported API. Those are not the same thing.

If your test merely sets element.value, it is not testing paste handling. If it dispatches a synthetic paste event without clipboard data, it is also not enough. You need either a true clipboard interaction or a framework-supported input path that reaches the same editor logic.

A practical test strategy

Start with three layers.

Layer 1: DOM-level unit tests for sanitizer logic

If your editor has a sanitizer function, test it directly with representative payloads.

javascript

const html = '<p><strong>Hello</strong> <img src=x onerror=alert(1) /></p>';
const sanitized = sanitizeClipboardHtml(html);
expect(sanitized).toContain('<strong>Hello</strong>');
expect(sanitized).not.toContain('onerror');
expect(sanitized).not.toContain('<img');

This does not replace end-to-end testing, but it catches regressions in allowlists, attribute filtering, and tag normalization faster than a browser run.

Layer 2: Browser automation for user-visible behavior

Use browser automation to verify the actual result in the editing surface. For rich text editor e2e tests, the important assertion is what the user sees in the content area and what the serialized document contains.

A Playwright example can paste through the clipboard when the environment supports it:

import { test, expect } from '@playwright/test';
test('pastes sanitized HTML into the editor', async ({ page, context }) => {
  await page.goto('/editor');
  await context.grantPermissions(['clipboard-read', 'clipboard-write']);

  await page.evaluate(async () => {
    await navigator.clipboard.write([
      new ClipboardItem({
        'text/html': new Blob(['<p><strong>Hi</strong><script>alert(1)</script></p>'], { type: 'text/html' }),
        'text/plain': new Blob(['Hi'], { type: 'text/plain' })
      })
    ]);
  });

  await page.locator('[contenteditable="true"]').click();
  await page.keyboard.press(process.platform === 'darwin' ? 'Meta+V' : 'Control+V');

  await expect(page.locator('[contenteditable="true"]')).toContainText('Hi');
  await expect(page.locator('[contenteditable="true"] script')).toHaveCount(0);
});

This works best when the application and browser automation environment both allow clipboard permissions. If they do not, prefer a framework-specific paste helper that exercises the editor’s paste path rather than raw DOM injection.

Layer 3: History behavior around the paste

Undo/redo should be part of the same suite, not a separate afterthought. Test the sequence that users actually perform:

  1. place the caret,
  2. paste content,
  3. type a character,
  4. undo once,
  5. undo again,
  6. redo.

That sequence checks whether paste forms a clean history boundary.

test('undo and redo after paste are stable', async ({ page }) => {
  await page.goto('/editor');
  const editor = page.locator('[contenteditable="true"]');

  await editor.click();
  await page.keyboard.insertText('Before ');
  await page.keyboard.press(process.platform === 'darwin' ? 'Meta+V' : 'Control+V');
  await page.keyboard.type('!');

  await page.keyboard.press(process.platform === 'darwin' ? 'Meta+Z' : 'Control+Z');
  await expect(editor).not.toContainText('!');

  await page.keyboard.press(process.platform === 'darwin' ? 'Meta+Z' : 'Control+Z');
  await expect(editor).toContainText('Before');
});

This example assumes the test environment has a known clipboard state. If you cannot guarantee that, set the clipboard first or use a custom paste helper.

What to assert for sanitization

Do not just check that the paste completed. Check the specific security and formatting expectations.

Good assertions

  • disallowed tags are removed, such as script, iframe, object, embed
  • dangerous attributes are stripped, such as onload, onclick, onerror
  • allowed formatting is preserved, such as strong, em, ul, ol, a[href]
  • links keep safe protocols only, such as https:, while javascript: is removed
  • pasted content does not bring unexpected inline styles unless styles are explicitly supported
  • the serialized HTML and the visible editor DOM match the product contract

Bad assertions

  • only checking that the editor still renders something
  • only checking text content when HTML preservation is required
  • asserting against internal sanitizer function calls instead of output
  • comparing exact HTML strings when the editor normalizes harmless attributes or whitespace

If your editor normalizes HTML, test the normalized contract. Do not make the suite brittle by freezing incidental DOM details.

Undo and redo: where teams get surprised

Undo behavior is not just a browser feature. Rich text editors often maintain their own history stack, and that stack can differ from native browser undo semantics.

The tricky cases are:

  • pasting large fragments, then typing immediately after
  • replacing a selection, then undoing only the replacement
  • mixed input methods, such as IME composition plus paste
  • editor plugins that wrap or transform content after insertion
  • nested editable regions or custom selection models

If your editor intercepts beforeinput and applies a custom transaction, make sure undo groups are intentional. A paste should not collapse into unrelated typing unless the product spec says it should.

Cross-browser quirks worth covering

Browser differences are not theoretical here.

  • Chromium often provides the smoothest clipboard automation path.
  • Firefox can differ in clipboard permission handling and editable selection behavior.
  • WebKit and Safari frequently expose edge cases around selection restoration and keyboard shortcut routing.

For automation, this means one green Chromium run is not enough. At minimum, cover the browser family you officially support, and add a focused regression test whenever you see a browser-specific paste bug.

A debugging checklist for broken paste tests

When a paste test fails, avoid guessing. Check these in order:

  1. Is the clipboard actually populated with the expected MIME types?
  2. Did the editor receive a real user-like paste action, not just DOM text injection?
  3. Did beforeinput cancel the paste or rewrite it?
  4. Did sanitizer logic remove more than expected?
  5. Did the selection move before insertion?
  6. Did the history stack merge paste with another action?
  7. Is the failure browser-specific?

A quick way to inspect what the editor received is to log the clipboard payload in the app’s paste handler during a test build.

document.querySelector('[contenteditable="true"]').addEventListener('paste', (event) => {
  const html = event.clipboardData?.getData('text/html');
  const text = event.clipboardData?.getData('text/plain');
  console.debug({ html, text });
});

That is not production code, but it can tell you whether the failure begins at the clipboard boundary or later in sanitization.

A sane minimum suite

If you need the smallest useful set for a release pipeline, I would start with these four checks:

  1. paste plain text into an empty editor
  2. paste sanitized HTML with one allowed formatting example and one blocked element
  3. paste over a selected range and confirm replacement
  4. undo and redo after paste

That set does not cover every browser quirk, but it catches the failures most likely to frustrate users or leak unsafe markup.

When to add deeper coverage

Add more cases when the product has one of these constraints:

  • supports rich copy and paste from external documents, email, or word processors
  • allows user-generated links or embedded media
  • performs server-side rendering or HTML serialization for storage
  • uses custom selection, collaboration cursors, or track changes
  • must pass accessibility and security review for regulated environments

In those cases, paste handling is not just editor behavior. It becomes part of your trust boundary.

FAQ

How do I test paste handling in rich text editors without relying on flaky keyboard shortcuts?

Use a framework-supported clipboard setup when available, or invoke the editor’s paste path with a realistic clipboard payload. Avoid tests that only set DOM text directly, because that skips the paste pipeline.

Should I assert against HTML or visible text?

Both, if the editor is supposed to preserve formatting. Visible text proves the user sees content, while HTML assertions prove sanitization and allowed markup rules.

What is the difference between clipboard sanitization testing and paste event automation?

Clipboard sanitization testing checks what content survives the filtering rules. Paste event automation checks whether the editor receives and processes a paste action correctly. You usually need both.

How do I test undo redo browser behavior after paste?

Paste content, perform one or more follow-up edits, then issue undo and redo through the same keyboard shortcut the user would use. Verify the editor content and caret position after each step.

Why does paste work manually but fail in automation?

Automation often runs with tighter clipboard permissions, different focus timing, or synthetic input that does not trigger the same editor path as a real user gesture. Check clipboard permissions, active element focus, and whether the test reaches paste or beforeinput.

What is the most important single regression to catch?

Unsafe content surviving sanitization is the highest-risk failure, followed by broken undo behavior. Both can make an editor feel unreliable even when typing still works.