How I Defeated ProseMirror: The Only Way to Programmatically Insert Text Into Rich Text Editors

Published: (March 14, 2026 at 03:23 AM EDT)
3 min read
Source: Dev.to

Source: Dev.to

Introduction

If you’ve ever tried to automate form filling on a modern web app, you’ve probably hit this wall: rich‑text editors that ignore your input. I spent hours trying to get Playwright to fill a ProseMirror editor on Gumroad. Here’s what I learned.

Why innerHTML and dispatchEvent Don’t Work

editor.innerHTML = '
My description
';
editor.dispatchEvent(new Event('input', { bubbles: true }));

ProseMirror maintains its own internal document model. When you change innerHTML, ProseMirror doesn’t know about it, so the next render discards your changes. Even dispatching input events doesn’t help—ProseMirror ignores DOM mutations it didn’t initiate.

Playwright’s fill() on contenteditable

await page.locator('[contenteditable]').fill('text')

fill() is designed for <input> and <textarea> elements; it doesn’t work on contenteditable elements.

Typing with the Keyboard

await editor.click()
await page.keyboard.type('My text')

This actually types into the editor and ProseMirror picks it up, but it’s painfully slow for long text (character‑by‑character) and focus can be lost mid‑typing.

Using document.execCommand('insertText')

execCommand('insertText') is a browser‑native command that ProseMirror (and TipTap, Slate, Quill, and most rich‑text editors) listens for. When the browser processes this command, it triggers the same internal event pipeline as a real keystroke—the editor’s transaction system picks it up, updates its document model, and renders correctly.

Why It Works

MethodWhat Happens
innerHTMLBypasses the editor entirely
dispatchEventProseMirror checks if events come from trusted sources
keyboard.type()Works but is slow (character‑by‑character)
execCommand('insertText')Triggers the editor’s internal transaction pipeline

execCommand is technically deprecated, but every browser still supports it, and it’s currently the only reliable way to programmatically input text into contenteditable editors.

Battle‑Tested Function for Filling ProseMirror/TipTap Editors

async def fill_prosemirror(page, text):
    # Find the editor (skip small contenteditable elements)
    editors = page.locator('[contenteditable="true"]')
    count = await editors.count()
    editor = None
    for i in range(count):
        el = editors.nth(i)
        box = await el.bounding_box()
        if box and box['height'] > 80:  # Skip URL slugs, etc.
            editor = el
            break

    if not editor:
        return False

    # Focus
    await editor.click(force=True)
    await page.wait_for_timeout(300)

    # Clear existing content
    await page.keyboard.press('Meta+a')  # Cmd+A on Mac
    await page.keyboard.press('Backspace')
    await page.wait_for_timeout(200)

    # Insert line by line
    lines = text.split('\n')
    for i, line in enumerate(lines):
        if line.strip():
            await page.evaluate(
                '(t) => document.execCommand("insertText", false, t)',
                line
            )

If the editor includes a toolbar, you likely need execCommand.

Triggering the Save Button

Rich‑text editors often intercept normal clicks. Bypass Playwright’s actionability checks with a direct JS click:

await page.evaluate(`() => {
    const buttons = document.querySelectorAll('button');
    for (const btn of buttons) {
        if (btn.textContent.includes('Save')) {
            btn.click();
            return true;
        }
    }
    return false;
}`);

Complementary Tool: SessionKeeper

I built SessionKeeper to handle the authentication side of web automation (CAPTCHAs, login walls). The ProseMirror trick above handles the form‑filling side. Together, they cover most of the “last mile” problems in browser automation.


Have you battled other rich‑text editors? Drop a comment with your war stories.

0 views
Back to Blog

Related posts

Read more »

Intro About Java Script

Introduction In today’s class I learned a short introduction to JavaScript, so I’ll share some facts about JavaScript in this blog. What Is JavaScript? JavaScr...