单元测试 Plate

学习如何对 Plate 编辑器及插件进行单元测试。

本指南概述了使用 @platejs/test-utils 对 Plate 插件和组件进行单元测试的最佳实践。

安装

pnpm add @platejs/test-utils

测试设置

在测试文件顶部添加 JSX 编译指示:

/** @jsx jsx */
 
import { jsx } from '@platejs/test-utils';
 
jsx; // 避免 ESLint 报错

这允许你使用 JSX 语法来创建编辑器值。

创建测试用例

编辑器状态表示

使用 JSX 表示编辑器状态:

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

节点元素如 <hp /><hul /><hli /> 表示不同类型的节点。

特殊元素如 <cursor /><anchor /><focus /> 表示选择状态。

测试转换操作

  1. 创建输入状态
  2. 定义预期输出状态
  3. 使用 createPlateEditor 设置编辑器
  4. 直接应用转换操作
  5. 断言编辑器的新状态

测试加粗格式化的示例:

it('应应用加粗格式化', () => {
  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,
  });
 
  // 直接应用转换
  editor.tf.toggleMark('bold');
 
  expect(editor.children).toEqual(output.children);
});

测试选择操作

测试操作如何影响编辑器的选择:

it('应在退格时折叠选择', () => {
  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('应调用 onKeyDown 处理程序', () => {
  const input = (
    <editor>
      <hp>
        Hello <anchor />world<focus />
      </hp>
    </editor>
  ) as any as PlateEditor;
 
  // 创建模拟处理程序以验证调用
  const onKeyDownMock = jest.fn();
 
  const editor = createPlateEditor({
    value: input.children,
    selection: input.selection,
    plugins: [
      {
        key: 'test',
        handlers: {
          onKeyDown: onKeyDownMock,
        },
      },
    ],
  });
 
  // 创建键盘事件
  const event = new KeyboardEvent('keydown', {
    key: 'Enter',
  }) as any;
 
  // 直接调用处理程序
  editor.plugins.test.handlers.onKeyDown({
    ...getEditorPlugin(editor, { key: 'test' }),
    event,
  });
 
  // 验证处理程序被调用
  expect(onKeyDownMock).toHaveBeenCalled();
});

测试复杂场景

对于表格等复杂插件,通过直接应用转换来测试各种场景:

describe('表格插件', () => {
  it('应插入表格', () => {
    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],
    });
 
    // 直接调用转换
    editor.tf.insertTable({ rows: 2, columns: 2 });
 
    expect(editor.children).toEqual(output.children);
    expect(editor.selection).toEqual(output.selection);
  });
});

测试带选项的插件

测试不同插件选项如何影响行为:

describe('当启用撤销时', () => {
  it('应在删除时撤销文本格式', () => {
    const input = (
      <fragment>
        <hp>
          1/<cursor />
        </hp>
      </fragment>
    ) as any;
 
    const output = (
      <fragment>
        <hp>
          1/4<cursor />
        </hp>
      </fragment>
    ) as any;
 
    const editor = createPlateEditor({
      plugins: [
        AutoformatPlugin.configure({
          options: {
            enableUndoOnDelete: true,
            rules: [
              {
                format: '¼',
                match: '1/4',
                mode: 'text',
              },
            ],
          },
        }),
      ],
      value: input,
    });
 
    // 触发自动格式化
    editor.tf.insertText('4');
 
    // 模拟退格键
    const event = new KeyboardEvent('keydown', {
      key: 'backspace',
    }) as any;
 
    // 调用处理程序
    editor.getPlugin({key: KEYS.autoformat}).handlers.onKeyDown({
      ...getEditorPlugin(editor, AutoformatPlugin),
      event,
    });
 
    // 当 enableUndoOnDelete: true 时,按退格键应恢复原始文本
    expect(input.children).toEqual(output.children);
  });
});

模拟与真实转换

虽然模拟可用于隔离特定行为,但 Plate 测试通常会在转换后评估实际的编辑器子节点和选择。这种方法确保插件能与整个编辑器状态正确协同工作。