@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.
Install React test peers only when a test imports @platejs/test/react:
| Import | Use for |
|---|
@platejs/test | Node-safe JSX fixtures, editor builders, selection projection, and DataTransfer fixtures |
@platejs/test/react | EditorTest and Testing Library interaction helpers |
@platejs/test/browser | Browser DOM and selection inspection |
@platejs/test/playwright | Playwright editor harnesses |
@platejs/test/proof | Pure proof contracts, classifiers, replay validation, and mobile receipts |
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 importsThis allows you to use JSX syntax for creating editor values.
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' },
]);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.
createEditor to set up the editorExample 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>
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>
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', {
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>
<
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
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.
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.