From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
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
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting

Unit Testing Plate

PreviousNext

Learn how to unit test Plate editor and plugins.

This guide outlines best practices for unit testing Plate plugins and components using @platejs/test-utils.

Installation

pnpm add @platejs/test-utils
pnpm add @platejs/test-utils

Setting Up Tests

Add the JSX pragma at the top of your test file:

/** @jsx



DebuggingBrowser

On This Page

InstallationSetting Up TestsCreating Test CasesEditor State RepresentationTesting TransformsTesting SelectionTesting Key EventsTesting Complex ScenariosTesting Plugin StateMocking vs. Real Transforms
Build your editor
Production-ready AI template and reusable components.
Get all-access
jsx */
import { jsx } from '@platejs/test-utils';
jsx; // so Biome doesn't remove unused imports
/** @jsx jsx */
 
import { jsx } from '@platejs/test-utils';
 
jsx; // so Biome doesn't remove unused imports

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

Creating Test Cases

Editor State Representation

Use JSX to represent editor states:

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

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 createPlateEditor 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 any as PlateEditor;
 
  const output = (
    <editor>
      <hp>
        Hello <htext bold>world</htext>
      </hp>
    </editor>
  ) as any as PlateEditor;
 
  const editor = createPlateEditor({
    plugins: [BoldPlugin],
    initialValue: input.children,
    selection: input.selection,
  });
 
  editor.update((tx) => {
    tx.marks.toggle('bold');
  });
 
  expect(editor.children).toEqual(output.children);
});
it('should apply bold formatting', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />
        world
        <focus />
      </hp>
    </editor>
  ) as any as PlateEditor;
 
  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 any as PlateEditor;
 
  const output = (
    <editor>
      <hp>
        He<cursor />ld
      </hp>
    </editor>
  ) as any as PlateEditor;
 
  const editor = createPlateEditor({
    initialValue: input.children,
    selection: input.selection,
  });
 
  editor.update((tx) => {
    tx.text.deleteBackward({ unit: 'character' });
  });
 
  expect(editor.children).toEqual(output.children);
  expect(editor.selection).toEqual(output.selection);
});
it('should collapse selection on backspace', () => {
  const input = (
    <editor>
      <hp>
        He<anchor />llo wor<focus />ld
      </hp>
    </editor>
  ) as any as PlateEditor;
 
  const output = (
    <editor>
      <hp>
        He<cursor />ld
      </hp>
    </editor>













Testing Key Events

When you need to test keyboard handlers directly:

import { createPlateEditor, definePlatePlugin } from 'platejs/react';
 
it('should call the keyDown handler', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />world<focus />
      </hp>
    </editor>
  ) as any as PlateEditor;
 
  // Create a mock handler to verify it's called
  const keyDownMock = jest.fn();
 
  const TestPlugin = definePlatePlugin('test', {
    on: {
      keyDown: keyDownMock,
    },
  });
 
  const editor = createPlateEditor({
    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 { createPlateEditor, definePlatePlugin } from 'platejs/react';
 
it('should call the keyDown handler', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />world<focus />
      </hp>
    </editor>
  ) as any as PlateEditor;
 
  // Create a mock handler to verify it's called
  const keyDownMock = jest.fn();
 
  const TestPlugin = definePlatePlugin('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 any as PlateEditor;
 
    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 any as PlateEditor;
 
    const editor = createPlateEditor({
      initialValue: input.children,
      selection: input.selection,
      plugins: [TablePlugin],
    });
 
    editor.update((tx) => {
      tx.insert.table({ colCount: 2, rowCount: 2 });
    });
 
    expect(editor.children).toEqual(output.children);
    expect(editor.selection).toEqual(output.selection);
  });
});
describe('Table plugin', () => {
  it('should insert a table', () => {
    const input = (
      <editor>
        <hp>
          Test<cursor />
        </hp>
      </editor>
    ) as any as PlateEditor;
 
    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 = (
      <fragment>
        <hp>
          start <anchor />
          of regular text
          <focus />
        </hp>
      </fragment>
    ) as any;
 
    const output = (
      <fragment>
        <hp>
          start <ha url="https://google.com">https://google.com</ha>
          <htext />
        </hp>
      </fragment>
    ) as any;
 
    const editor = createPlateEditor({
      plugins: [
        LinkPlugin.configure({
          initialState: {
            keepSelectedTextOnPaste: false,
          },
        }),
      ],
      initialValue: input,
    });
 
    editor.api.dom.clipboard.insertData({
      getData: (type: string) => (type === 'text/plain' ? 'https://google.com' : ''),
    } as any);
 
    expect(input.children).toEqual(output.children);
  });
});
describe('when keepSelectedTextOnPaste is disabled', () => {
  it('replaces the selected text with the pasted url', () => {
    const input = (
      <fragment>
        <hp>
          start <anchor />
          of regular text
          <focus />
        </hp>
      </fragment>
    ) as any;
 
    const output = (
      <fragment>
        <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.

</editor>
) as any as PlateEditor;
const editor = createPlateEditor({
plugins: [BoldPlugin],
initialValue: input.children,
selection: input.selection,
});
editor.update((tx) => {
tx.marks.toggle('bold');
});
expect(editor.children).toEqual(output.children);
});
) as any as PlateEditor;
const editor = createPlateEditor({
initialValue: input.children,
selection: input.selection,
});
editor.update((tx) => {
tx.text.deleteBackward({ unit: 'character' });
});
expect(editor.children).toEqual(output.children);
expect(editor.selection).toEqual(output.selection);
});
on: {
keyDown: keyDownMock,
},
});
const editor = createPlateEditor({
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 any as PlateEditor;
const editor = createPlateEditor({
initialValue: input.children,
selection: input.selection,
plugins: [TablePlugin],
});
editor.update((tx) => {
tx.insert.table({ colCount: 2, rowCount: 2 });
});
expect(editor.children).toEqual(output.children);
expect(editor.selection).toEqual(output.selection);
});
});
ha url
=
"https://google.com"
>
https
:
//google.com</ha>
<htext />
</hp>
</fragment>
) as any;
const editor = createPlateEditor({
plugins: [
LinkPlugin.configure({
initialState: {
keepSelectedTextOnPaste: false,
},
}),
],
initialValue: input,
});
editor.api.dom.clipboard.insertData({
getData: (type: string) => (type === 'text/plain' ? 'https://google.com' : ''),
} as any);
expect(input.children).toEqual(output.children);
});
});