本指南概述了使用 @platejs/test-utils 对 Plate 插件和组件进行单元测试的最佳实践。
安装
pnpm add @platejs/test-utilspnpm add @platejs/test-utils设置测试
在测试文件顶部添加 JSX pragma:
/** @jsx 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这允许你使用 JSX 语法创建编辑器值。
创建测试用例
编辑器状态表示
使用 JSX 表示编辑器状态:
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;节点元素如 <hp />、<hul />、<hli /> 表示不同类型的节点。
特殊元素如 <cursor />、<anchor /> 和 <focus /> 表示选区状态。
测试转换
- 创建输入状态
- 定义预期输出状态
- 使用
createPlateEditor设置编辑器 - 直接应用转换
- 断言编辑器的新状态
测试粗体格式的示例:
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],
value: input.children,
selection: input.selection,
});
// Apply transform directly
editor.tf.toggleMark('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>
</editor>
) as any as PlateEditor;
const editor = createPlateEditor({
plugins: [BoldPlugin],
value: input.children,
selection: input.selection,
});
// Apply transform directly
editor.tf.toggleMark('bold');
expect(editor.children).toEqual(output.children);
});测试选区
测试操作如何影响编辑器的选区:
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({
value: input.children,
selection: input.selection,
});
editor.tf.deleteBackward();
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>
) as any as PlateEditor;
const editor = createPlateEditor({
value: input.children,
selection: input.selection,
});
editor.tf.deleteBackward();
expect(editor.children).toEqual(output.children);
expect(editor.selection).toEqual(output.selection);
});测试键盘事件
当你需要直接测试键盘处理程序时:
it('should call the onKeyDown 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 onKeyDownMock = jest.fn();
const editor = createPlateEditor({
value: input.children,
selection: input.selection,
plugins: [
{
key: 'test',
handlers: {
onKeyDown: onKeyDownMock,
},
},
],
});
// Create the keyboard event
const event = new KeyboardEvent('keydown', {
key: 'Enter',
}) as any;
// Call the handler directly
editor.plugins.test.handlers.onKeyDown({
...getEditorPlugin(editor, { key: 'test' }),
event,
});
// Verify the handler was called
expect(onKeyDownMock).toHaveBeenCalled();
});it('should call the onKeyDown 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 onKeyDownMock = jest.fn();
const editor = createPlateEditor({
value: input.children,
selection: input.selection,
plugins: [
{
key: 'test',
handlers: {
onKeyDown: onKeyDownMock,
},
},
],
});
// Create the keyboard event
const event = new KeyboardEvent('keydown', {
key: 'Enter',
}) as any;
// Call the handler directly
editor.plugins.test.handlers.onKeyDown({
...getEditorPlugin(editor, { key: 'test' }),
event,
});
// Verify the handler was called
expect(onKeyDownMock).toHaveBeenCalled();
});测试复杂场景
对于像表格这样的复杂插件,通过直接应用转换来测试各种场景:
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({
value: input.children,
selection: input.selection,
plugins: [TablePlugin],
});
// Call transform directly
editor.tf.insertTable({ rows: 2, columns: 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>
<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({
value: input.children,
selection: input.selection,
plugins: [TablePlugin],
});
// Call transform directly
editor.tf.insertTable({ rows: 2, columns: 2 });
expect(editor.children).toEqual(output.children);
expect(editor.selection).toEqual(output.selection);
});
});测试带选项的插件
测试不同插件选项如何影响行为:
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({
options: {
keepSelectedTextOnPaste: false,
},
}),
],
value: input,
});
editor.tf.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 <ha url="https://google.com">https://google.com</ha>
<htext />
</hp>
</fragment>
) as any;
const editor = createPlateEditor({
plugins: [
LinkPlugin.configure({
options: {
keepSelectedTextOnPaste: false,
},
}),
],
value: input,
});
editor.tf.insertData({
getData: (type: string) => (type === 'text/plain' ? 'https://google.com' : ''),
} as any);
expect(input.children).toEqual(output.children);
});
});Mock 与真实转换
虽然 mock 对于隔离特定行为很有用,但 Plate 测试通常会在转换后评估实际的编辑器 children 和选区。这种方法确保插件能与整个编辑器状态正确协作。