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.

30K+

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.

0Editor scenarios covered
0 <textarea>sIn a Tiptap editor
0+Built-in mark types
0%Fewer lines with Assrt

Tiptap Input to DOM Update Flow

UserBrowser DOMProseMirror EditorViewTransaction PipelineTiptap ExtensionsKeystroke / pasteInput event on contenteditableCreate TransactionApply extension rulesModified transactiondispatch(tr) updates stateDOM patch (minimal diff)

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.

Project Setup
playwright.config.ts

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.

tests/helpers/editor.ts

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

1

Basic Text Input and Paragraph Creation

Straightforward

3. 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.

tests/tiptap-basic.spec.ts
Test Run: Basic Input
2

Applying Formatting Marks (Bold, Italic, Links)

Moderate

4. 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.

tests/tiptap-formatting.spec.ts

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');
});
18% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started
3

Slash Commands and Mention Dropdowns

Complex

5. 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.

tests/tiptap-slash-commands.spec.ts

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();
});
27% fewer lines
4

Image Upload and Media Embedding

Moderate

6. 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.

tests/tiptap-images.spec.ts
5

Clipboard Paste Handling (HTML, Markdown, Plain Text)

Complex

7. 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.

tests/tiptap-clipboard.spec.ts
6

Collaborative Editing with Yjs

Complex

8. 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.

tests/tiptap-collab.spec.ts
Test Run: Collaborative Editing

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.

tests/tiptap-editor.assrt

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 tests
84% fewer lines

Assrt 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

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk