Rich Text Editor Testing Guide
How to Test Tiptap Editor with Playwright: Complete 2026 Guide
A scenario-by-scenario walkthrough of testing Tiptap rich text editors with Playwright. contenteditable divs, ProseMirror transactions, formatting marks and nodes, selection and cursor positioning, slash commands, mention dropdowns, collaborative editing with Yjs, and clipboard paste handling.
“Tiptap powers over 30,000 applications worldwide, from Notion-style editors to CMS platforms, making it the most popular ProseMirror-based editor framework on npm.”
Tiptap Input to DOM Update Flow
1. Why Testing Tiptap Is Harder Than It Looks
Tiptap is built on top of ProseMirror, the document editing framework that powers many of the web's most sophisticated editors. Unlike a simple <textarea> or <input>, Tiptap renders its content inside a contenteditable div. That means there is no .fill()method that works the way you expect. Playwright's page.fill() targets form elements, but a contenteditable div is not a form element. Your first instinct will fail, and that is the beginning of a chain of surprises.
The document model is the second obstacle. Tiptap maintains a ProseMirror document tree that is separate from the DOM. When you type text, ProseMirror creates a transaction, applies schema rules and extension logic, then patches the DOM with the minimum diff needed. If you assert against the DOM directly, you may see intermediate states that do not reflect the actual document. If you try to manipulate the DOM directly (via page.evaluate setting innerHTML), ProseMirror will either ignore or overwrite your changes on the next transaction.
There are six structural reasons that Tiptap editors are difficult to test reliably. First, contenteditable divs do not respond to standard form fill methods. Second, the ProseMirror document model means the DOM is a projection, not the source of truth. Third, formatting marks (bold, italic, code, links) are applied via keyboard shortcuts or toolbar buttons that produce transactions, not direct DOM mutations. Fourth, selection and cursor position matter because many operations depend on what text is selected. Fifth, custom extensions like slash commands, mentions, and drag handles introduce popup menus and complex interaction patterns. Sixth, collaborative editing with Yjs adds WebSocket synchronization that produces asynchronous document updates from other peers.
Tiptap Editor Architecture
User Input
Keystroke, click, or paste
contenteditable
DOM input event captured
ProseMirror
Transaction created
Extensions
Rules and plugins applied
New State
Document tree updated
DOM Patch
Minimal diff applied
2. Setting Up Your Tiptap Test Environment
Before writing any test scenarios, you need a working Playwright project pointed at an application that uses Tiptap. The Tiptap editor renders inside a div with the class .tiptap (or .ProseMirror if using ProseMirror directly). Your test setup needs to locate this element reliably and ensure the editor is fully initialized before interacting with it.
The Editor Locator Helper
Since you will reference the Tiptap editor div in every test, create a shared helper that waits for the editor to be ready. The key is waiting for the .tiptap div to have the contenteditable="true" attribute, which confirms ProseMirror has fully initialized.
Test Environment Setup Flow
Install Deps
Playwright + Tiptap packages
Configure
playwright.config.ts with webServer
Create Helpers
Editor locator + clear utility
Verify
npx playwright test --ui
Basic Text Input and Paragraph Creation
Straightforward3. Scenario: Basic Text Input and Paragraph Creation
The most fundamental test verifies that typing text into the Tiptap editor actually produces content. This sounds trivial, but the implementation matters. You cannot use page.fill() because contenteditable elements are not form inputs. Instead, you click the editor to focus it, then use page.keyboard.type() or page.keyboard.press() to simulate real keystrokes. Each keystroke triggers a ProseMirror input rule pipeline, and the resulting DOM update may not appear instantly.
For paragraph creation, you press Enter. Tiptap creates a new <p> node in the ProseMirror document. Your assertion should verify both the visible text and the document structure.
Applying Formatting Marks (Bold, Italic, Links)
Moderate4. Scenario: Applying Formatting Marks (Bold, Italic, Links)
Formatting in Tiptap works through marks in the ProseMirror schema. When you press Ctrl+B (or Meta+B on macOS), Tiptap dispatches a transaction that toggles the bold mark on the current selection. The DOM reflects this as a <strong> element wrapping the selected text. For italic, it produces <em>. For inline code, it wraps in <code>.
The tricky part is selection. To apply a mark to existing text, you must first select it. Playwright provides page.keyboard.press('Shift+End') for selecting to end of line, and you can use triple-click to select an entire paragraph. For precise character-level selection, use Shift+ArrowRight repeated the desired number of times, or use Meta+A to select all content.
Bold Formatting Test
test('bold formatting', async ({ page }) => {
await page.goto('/editor');
const editor = page.locator('.tiptap[contenteditable="true"]');
await editor.waitFor({ state: 'visible' });
await editor.click();
await page.keyboard.type('Hello ');
await page.keyboard.press('Meta+B');
await page.keyboard.type('bold');
await page.keyboard.press('Meta+B');
const strong = editor.locator('strong');
await expect(strong).toHaveText('bold');
});Slash Commands and Mention Dropdowns
Complex5. Scenario: Slash Commands and Mention Dropdowns
Many Tiptap implementations include slash commands (typing / to open a command palette) and mention dropdowns (typing @to tag a user). These features use Tiptap's suggestion plugin, which renders a floating dropdown that exists outside the editor's contenteditable div. The dropdown is typically a separate React component or vanilla DOM element positioned absolutely near the cursor.
The main testing challenge is timing. After typing the trigger character, the suggestion plugin needs to process the input, query its data source (which may be async), and render the dropdown. You must wait for the dropdown to appear before interacting with it. Using a hard-coded delay is fragile. Instead, use waitFor on the dropdown container element.
Slash Command Test
test('insert heading via slash command', async ({ page }) => {
await page.goto('/editor');
const editor = page.locator('.tiptap[contenteditable="true"]');
await editor.waitFor({ state: 'visible' });
await editor.click();
await page.keyboard.type('/');
const dropdown = page.locator('[data-tippy-root]');
await dropdown.waitFor({ state: 'visible' });
await page.keyboard.type('heading');
await dropdown.locator('text=Heading 1').first().click();
await expect(editor.locator('h1')).toBeVisible();
});Image Upload and Media Embedding
Moderate6. Scenario: Image Upload and Media Embedding
Tiptap's Image extension renders <img>nodes inside the editor. Testing image insertion involves either triggering a file upload dialog, pasting an image from the clipboard, or using the editor's command API to insert an image by URL. For file uploads, many Tiptap setups use a hidden <input type="file"> element that the image extension triggers when the user clicks an upload button or uses a slash command.
The challenge is that Playwright needs to intercept the file chooser dialog or set files on the hidden input element before the upload begins. For drag-and-drop image insertion, you need to dispatch custom events because Tiptap's drop handler listens for DataTransfer objects on the contenteditable element.
Clipboard Paste Handling (HTML, Markdown, Plain Text)
Complex7. Scenario: Clipboard Paste Handling (HTML, Markdown, Plain Text)
When users paste content into a Tiptap editor, the editor handles the clipboard data through ProseMirror's handlePastehook. HTML content is parsed into ProseMirror nodes according to the schema's parse rules. Plain text is wrapped in paragraph nodes. Markdown (if the Markdown extension is enabled) is converted to ProseMirror nodes before insertion. Testing paste behavior requires simulating clipboard events with specific MIME types and content.
Playwright does not have a built-in method for pasting arbitrary clipboard content with specific MIME types. You need to use page.evaluate to dispatch a synthetic paste event with a constructed DataTransfer object. This is one of the most complex patterns in Tiptap testing, but it is essential for verifying that pasted content from external sources (Google Docs, Notion, email clients) produces the expected document structure.
Collaborative Editing with Yjs
Complex8. Scenario: Collaborative Editing with Yjs
Tiptap's Collaboration extension uses Yjs (a CRDT library) to synchronize document state across multiple clients in real time. Testing collaborative editing requires opening two browser contexts connected to the same document, making edits in one, and verifying they appear in the other. This tests the WebSocket connection, the Yjs sync protocol, and Tiptap's integration with the Yjs document.
Playwright makes this possible by creating multiple browser contexts within the same test. Each context represents an independent browser session. You open the same editor URL in both contexts and verify that edits from one context propagate to the other. The key assertion timing challenge is that Yjs synchronization happens asynchronously over WebSocket, so you need to poll or wait for the content to appear rather than asserting immediately after typing.
9. Common Pitfalls That Break Tiptap Test Suites
The following pitfalls come from real Tiptap and ProseMirror issue trackers and community forums. Each one has broken production test suites, and understanding them upfront will save you hours of debugging.
Pitfalls to Avoid
- Using page.fill() on contenteditable: This method targets <input> and <textarea> elements. On a contenteditable div, it either throws an error or silently does nothing. Use page.keyboard.type() after clicking the editor.
- Asserting DOM immediately after typing: ProseMirror processes keystrokes through a transaction pipeline. The DOM update may lag by one or two animation frames. Always use expect().toContainText() with Playwright's auto-retry rather than snapshot assertions.
- Setting innerHTML directly via page.evaluate: ProseMirror maintains its own document state. Modifying innerHTML bypasses that state, and the next transaction will overwrite your changes. Use ProseMirror commands or Tiptap's chain() API instead.
- Forgetting to wait for editor initialization: The .tiptap div may exist in the DOM before ProseMirror attaches its EditorView. Always wait for contenteditable='true' before interacting.
- Ignoring selection state before formatting: Applying a mark (bold, italic) only works when text is selected. If your test types text and then immediately presses Ctrl+B without selecting, the mark toggles for future input, not existing text.
- Hardcoding dropdown selectors for slash commands: Tiptap suggestion plugins often use Tippy.js for positioning, and the dropdown renders outside the editor's DOM tree. Use a flexible locator like [data-tippy-root] or a data attribute you control.
- Not accounting for Yjs sync latency: Collaborative editing tests that assert immediately after typing will be flaky. Yjs sync depends on WebSocket round-trip time. Use Playwright's toContainText with a generous timeout.
- Testing with a minimal schema that differs from production: If your test app uses StarterKit but production adds custom extensions (tables, task lists, code blocks with syntax highlighting), your tests will pass locally but miss real failures.
Pre-Flight Checklist Before Running Tiptap Tests
- The test app's Tiptap schema matches production (same extensions, same configuration)
- Editor helper waits for contenteditable='true' before returning
- All keyboard shortcuts use Meta (macOS) or Control (Linux/Windows) appropriately for CI environment
- Slash command and mention dropdown selectors use data attributes, not fragile CSS classes
- Collaborative editing tests use separate browser contexts, not separate pages in the same context
- Image upload tests include fixture files in the test directory
10. Writing These Scenarios in Plain English with Assrt
Every scenario in this guide requires careful attention to contenteditable quirks, ProseMirror transaction timing, and selector fragility. With Assrt, you describe what you want to test in plain English. Assrt compiles your scenarios into the same Playwright TypeScript shown throughout this guide, committed to your repository as real tests you can read, run, and modify.
Here is the slash command scenario from Section 5, rewritten as an Assrt file. Notice that you describe the user intent, not the implementation details. Assrt handles the contenteditable interaction, the dropdown wait logic, and the assertion against the ProseMirror document structure.
Full Tiptap Test Suite
// 5 test files, ~280 lines of Playwright TypeScript
// tiptap-basic.spec.ts (3 tests, 45 lines)
// tiptap-formatting.spec.ts (3 tests, 65 lines)
// tiptap-slash-commands.spec.ts (3 tests, 55 lines)
// tiptap-clipboard.spec.ts (3 tests, 70 lines)
// tiptap-collab.spec.ts (1 test, 45 lines)
// Total: 13 tests across 5 files, ~280 lines
// Requires: contenteditable helpers, DataTransfer
// mocking, ProseMirror view access, multi-context
// setup for collaborative editing testsAssrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections. The compiled tests are committed to your repository as real, readable test files. When Tiptap updates its DOM structure or a custom extension changes its rendering, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locators. Your scenario files stay untouched.
Start with the basic text input scenario. Once it is green in your CI, add formatting marks, then slash commands, then clipboard paste, then collaborative editing. In a single afternoon you can have complete Tiptap editor coverage that most applications never achieve by hand.
Related Guides
How to Test CodeMirror
A practical, scenario-by-scenario guide to testing CodeMirror 6 editors with Playwright....
How to Test Lexical Editor
A practical, scenario-by-scenario guide to testing Lexical rich text editors with...
How to Test Monaco Editor
A practical, scenario-by-scenario guide to testing Monaco Editor with Playwright. Covers...
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.