Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Authored Changes
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting
  • Locations
  • Transactions
  • Serializing
  • Roots
  • Document Meta
  • Clipboard and Paste
  • Decorations, annotations, and widgets
  • Schema
  • History
  • Pagination
  • Annotations
  • DOM Coverage
  • External Text Views
  • Virtualized Rendering

Unit Testing Plate

PreviousNext

Learn how to unit test Plate editor and plugins.

@platejs/test keeps Plate fixtures and test harnesses in one package without mixing their runtimes. Its root works in plain Node. Import React, DOM, and Playwright helpers only from their explicit subpaths.

Installation

pnpm add -D platejs @platejs/test
pnpm add -D platejs @platejs/test

Install React test peers only when a test imports @platejs/test/react:

pnpm add -D react react-dom @testing-library/react
pnpm add -D react react-dom @testing-library/react
ImportUse for
DebuggingBrowser

On This Page

InstallationSetting up testsTesting React componentsCreating test casesEditor state representationTesting transformsTesting selectionTesting key eventsTesting complex scenariosTesting plugin stateMocking vs. real transformsCustom fixture vocabulary
Build your editor
Production-ready AI template and reusable components.
Get all-access
@platejs/testNode-safe JSX fixtures, editor builders, selection projection, and DataTransfer fixtures
@platejs/test/reactEditorTest and Testing Library interaction helpers
@platejs/test/browserBrowser DOM and selection inspection
@platejs/test/playwrightPlaywright editor harnesses
@platejs/test/proofPure proof contracts, classifiers, replay validation, and mobile receipts

Setting up tests

Use a .tsx file and select the classic JSX runtime with the fixture factory:

/** @jsxRuntime classic */
/** @jsx jsx */
 
import {
  jsx,
  projectTestSelectionRange,
  type TestEditorFixture,
} from '@platejs/test';
 
jsx; // so Biome doesn't remove unused imports
/** @jsxRuntime classic */
/** @jsx jsx */
 
import {
  jsx,
  projectTestSelectionRange,
  type TestEditorFixture,
} from '@platejs/test';
 
jsx; // so Biome doesn't remove unused imports

This allows you to use JSX syntax for creating editor values.

Testing React components

Use the React entrypoint when the test mounts EditorContent or interacts with the rendered editor:

import { createTestEditor } from '@platejs/test/react';
import { ParagraphPlugin } from 'platejs/react';
 
const [editor, actions] = await createTestEditor({
  initialValue: [
    { children: [{ text: '' }], type: 'paragraph' },
  ],
  plugins: [ParagraphPlugin],
});
 
await actions.type('Hello');
expect(editor.read.children()).toEqual([
  { children: [{ text: 'Hello' }], type: 'paragraph' },
]);
import { createTestEditor } from '@platejs/test/react';
import { ParagraphPlugin } from 'platejs/react';
 
const [editor, actions] = await createTestEditor({
  initialValue: [
    { children: [{ text: '' }], type: 'paragraph' },
  ],
  plugins: [ParagraphPlugin],
});
 
await actions.type('Hello');
expect(editor.read.children()).toEqual([
  { children: [{ text: 'Hello' }], type: 'paragraph' },
]);

Creating test cases

Editor state representation

Use JSX to represent editor states:

const input = (
  <editor>
    <hp>
      Hello<cursor /> world
    </hp>
  </editor>
) as TestEditorFixture;
const input = (
  <editor>
    <hp>
      Hello<cursor /> world
    </hp>
  </editor>
) as TestEditorFixture;

Node elements like <hp />, <hul />, <hli /> represent different types of nodes.

Special elements like <cursor />, <anchor />, and <focus /> represent selection states.

Testing transforms

  1. Create an input state
  2. Define the expected output state
  3. Use createEditor to set up the editor
  4. Apply the transform(s) directly
  5. Assert the editor's new state

Example testing bold formatting:

it('should apply bold formatting', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />
        world
        <focus />
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  const output = (
    <editor>
      <hp>
        Hello <htext bold>world</htext>
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  const editor = createEditor({
    plugins: [BoldPlugin],
    initialValue: input.children,
    selection: input.selection,
  });
 
  editor.update((tx) => {
    tx.marks.toggle('bold');
  });
 
  expect(editor.read.children()).toEqual(output.children);
});
it('should apply bold formatting', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />
        world
        <focus />
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  const output = (
    <editor>
      <hp>
        Hello <htext bold>world</htext>
      </hp>














Testing selection

Test how operations affect the editor's selection:

it('should collapse selection on backspace', () => {
  const input = (
    <editor>
      <hp>
        He<anchor />llo wor<focus />ld
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  const output = (
    <editor>
      <hp>
        He<cursor />ld
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  const editor = createEditor({
    initialValue: input.children,
    selection: input.selection,
  });
 
  editor.update((tx) => {
    tx.text.deleteBackward({ unit: 'character' });
  });
 
  expect(editor.read.children()).toEqual(output.children);
  expect(editor.read.selection()).toEqual(
    projectTestSelectionRange(output.selection)
  );
});
it('should collapse selection on backspace', () => {
  const input = (
    <editor>
      <hp>
        He<anchor />llo wor<focus />ld
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  const output = (
    <editor>
      <hp>
        He<cursor />ld
      </hp>
    </editor>















Testing key events

When you need to test keyboard handlers directly:

import { createEditor, definePlugin } from 'platejs/react';
 
it('should call the keyDown handler', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />world<focus />
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  // Create a mock handler to verify it's called
  const keyDownMock = jest.fn();
 
  const TestPlugin = definePlugin('test', {
    on: {
      keyDown: keyDownMock,
    },
  });
 
  const editor = createEditor({
    initialValue: input.children,
    selection: input.selection,
    plugins: [TestPlugin],
  });
 
  // Create the keyboard event
  const event = new KeyboardEvent('keydown', {
    key: 'Enter',
  }) as any;
 
  // Resolve the installed descriptor through its typed portal
  const testPlugin = editor.plugin(TestPlugin);
 
  testPlugin.on.keyDown?.({
    ...testPlugin,
    event,
  });
 
  // Verify the handler was called
  expect(keyDownMock).toHaveBeenCalled();
});
import { createEditor, definePlugin } from 'platejs/react';
 
it('should call the keyDown handler', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />world<focus />
      </hp>
    </editor>
  ) as TestEditorFixture;
 
  // Create a mock handler to verify it's called
  const keyDownMock = jest.fn();
 
  const TestPlugin = definePlugin('test', {


























Testing complex scenarios

For complex plugins like tables, test various scenarios by directly applying transforms:

describe('Table plugin', () => {
  it('should insert a table', () => {
    const input = (
      <editor>
        <hp>
          Test<cursor />
        </hp>
      </editor>
    ) as TestEditorFixture;
 
    const output = (
      <editor>
        <hp>Test</hp>
        <htable>
          <htr>
            <htd>
              <hp>
                <cursor />
              </hp>
            </htd>
            <htd>
              <hp></hp>
            </htd>
          </htr>
          <htr>
            <htd>
              <hp></hp>
            </htd>
            <htd>
              <hp></hp>
            </htd>
          </htr>
        </htable>
      </editor>
    ) as TestEditorFixture;
 
    const editor = createEditor({
      initialValue: input.children,
      selection: input.selection,
      plugins: [TablePlugin],
    });
 
    editor.update((tx) => {
      tx.insert.table({ colCount: 2, rowCount: 2 });
    });
 
    expect(editor.read.children()).toEqual(output.children);
    expect(editor.read.selection()).toEqual(
      projectTestSelectionRange(output.selection)
    );
  });
});
describe('Table plugin', () => {
  it('should insert a table', () => {
    const input = (
      <editor>
        <hp>
          Test<cursor />
        </hp>
      </editor>
    ) as TestEditorFixture;
 
    const output = (
      <editor>
        <hp>Test</hp>
        <htable>
          <htr>
            <



































Testing plugin state

Test how different initial state affects behavior:

describe('when keepSelectedTextOnPaste is disabled', () => {
  it('replaces the selected text with the pasted url', () => {
    const input = (
      <editor>
        <hp>
          start <anchor />
          of regular text
          <focus />
        </hp>
      </editor>
    ) as TestEditorFixture;
 
    const output = (
      <editor>
        <hp>
          start <ha url="https://google.com">https://google.com</ha>
          <htext />
        </hp>
      </editor>
    ) as TestEditorFixture;
 
    const editor = createEditor({
      plugins: [
        LinkPlugin.configure({
          initialState: {
            keepSelectedTextOnPaste: false,
          },
        }),
      ],
      initialValue: input.children,
      selection: input.selection,
    });
 
    editor.api.dom.clipboard.insertData({
      getData: (type: string) => (type === 'text/plain' ? 'https://google.com' : ''),
    } as any);
 
    expect(editor.read.children()).toEqual(output.children);
  });
});
describe('when keepSelectedTextOnPaste is disabled', () => {
  it('replaces the selected text with the pasted url', () => {
    const input = (
      <editor>
        <hp>
          start <anchor />
          of regular text
          <focus />
        </hp>
      </editor>
    ) as TestEditorFixture;
 
    const output = (
      <editor>
        <hp>
          start 























Mocking vs. real transforms

While mocking can be useful for isolating specific behaviors, Plate tests often assess actual editor children and selection after transforms. This approach ensures that plugins work correctly with the entire editor state.

Custom fixture vocabulary

Use createHyperscript from platejs/hyperscript for a small domain-specific fixture factory. It creates ordinary node values and selection markers; keep it in tests and fixtures.

import { createHyperscript } from 'platejs/hyperscript';
 
const h = createHyperscript({
  elements: { paragraph: { type: 'paragraph' } },
});
 
const paragraph = h('paragraph', {}, 'Hello');
import { createHyperscript } from 'platejs/hyperscript';
 
const h = createHyperscript({
  elements: { paragraph: { type: 'paragraph' } },
});
 
const paragraph = h('paragraph', {}, 'Hello');

The built-in fragment, element, and text tags construct nodes; cursor, anchor, and focus place selection endpoints. The selection tag constructs a range. Use @platejs/test when its Plate vocabulary and harnesses cover the fixture.

</
editor
>
) as TestEditorFixture;
const editor = createEditor({
plugins: [BoldPlugin],
initialValue: input.children,
selection: input.selection,
});
editor.update((tx) => {
tx.marks.toggle('bold');
});
expect(editor.read.children()).toEqual(output.children);
});
)
as
TestEditorFixture
;
const editor = createEditor({
initialValue: input.children,
selection: input.selection,
});
editor.update((tx) => {
tx.text.deleteBackward({ unit: 'character' });
});
expect(editor.read.children()).toEqual(output.children);
expect(editor.read.selection()).toEqual(
projectTestSelectionRange(output.selection)
);
});
on: {
keyDown: keyDownMock,
},
});
const editor = createEditor({
initialValue: input.children,
selection: input.selection,
plugins: [TestPlugin],
});
// Create the keyboard event
const event = new KeyboardEvent('keydown', {
key: 'Enter',
}) as any;
// Resolve the installed descriptor through its typed portal
const testPlugin = editor.plugin(TestPlugin);
testPlugin.on.keyDown?.({
...testPlugin,
event,
});
// Verify the handler was called
expect(keyDownMock).toHaveBeenCalled();
});
htd
>
<hp>
<cursor />
</hp>
</htd>
<htd>
<hp></hp>
</htd>
</htr>
<htr>
<htd>
<hp></hp>
</htd>
<htd>
<hp></hp>
</htd>
</htr>
</htable>
</editor>
) as TestEditorFixture;
const editor = createEditor({
initialValue: input.children,
selection: input.selection,
plugins: [TablePlugin],
});
editor.update((tx) => {
tx.insert.table({ colCount: 2, rowCount: 2 });
});
expect(editor.read.children()).toEqual(output.children);
expect(editor.read.selection()).toEqual(
projectTestSelectionRange(output.selection)
);
});
});
<
ha url
=
"https://google.com"
>
https
:
//google.com</ha>
<htext />
</hp>
</editor>
) as TestEditorFixture;
const editor = createEditor({
plugins: [
LinkPlugin.configure({
initialState: {
keepSelectedTextOnPaste: false,
},
}),
],
initialValue: input.children,
selection: input.selection,
});
editor.api.dom.clipboard.insertData({
getData: (type: string) => (type === 'text/plain' ? 'https://google.com' : ''),
} as any);
expect(editor.read.children()).toEqual(output.children);
});
});