You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

contentEditable div光标操作库:moveCursorLeft方法Node环境测试问题

解决Jest测试环境下选区操作方法无法执行的问题

核心问题分析

你的moveCursorLeft方法直接依赖window.getSelection()获取选区,但Jest运行在Node环境中,默认没有浏览器的window和document对象,导致this.selection始终为null,方法直接返回,无法测试核心逻辑。

解决方案:依赖注入+Mock模拟

通过将选区获取逻辑抽象为可注入的依赖,让测试时可以传入模拟的选区对象,从而脱离浏览器环境测试核心逻辑。

步骤1:重构Selection类,注入选区获取器

修改类的初始化逻辑,允许传入自定义的选区获取函数,默认使用window.getSelection():

class Selection {
  private editor: HTMLElement;
  private getSelection: () => Selection | null;

  constructor(editor: string | HTMLElement, getSelection?: () => Selection | null) {
    // 处理编辑器元素初始化
    if (typeof editor === 'string') {
      const elem = document.getElementById(editor);
      if (!elem) throw new Error('Editor element not found');
      this.editor = elem;
    } else {
      this.editor = editor;
    }
    // 注入选区获取逻辑,默认使用浏览器原生方法
    this.getSelection = getSelection || (() => window.getSelection());
  }

  moveCursorLeft() {
    // 每次调用时通过注入的方法获取选区
    const selection = this.getSelection();
    if (!selection || !selection.anchorNode) return;
    
    const { anchorNode, anchorOffset } = selection;
    if (anchorOffset > 0) {
      selection.collapse(anchorNode, anchorOffset - 1);
    } else {
      const childrenArray = Array.from(this.editor.children);
      const index = childrenArray.findIndex(
        (child) => child.firstChild === anchorNode
      );

      const prevNode = childrenArray[index - 1];

      if (
        prevNode &&
        prevNode.firstChild &&
        prevNode.firstChild.nodeValue?.length
      ) {
        selection.collapse(
          prevNode.firstChild,
          prevNode.firstChild.nodeValue.length - 1
        );
      }
    }
  }
}

步骤2:编写测试用例,Mock选区对象

在测试中创建模拟的Selection对象和DOM节点,传入自定义的getSelection函数,覆盖不同场景测试逻辑:

import { render } from '@testing-library/react';

describe('Selection.moveCursorLeft', () => {
  let mockSelection: Partial<Selection>;
  let mockAnchorNode: Text;
  let mockEditor: HTMLElement;

  beforeEach(() => {
    // 构建测试用DOM结构
    mockAnchorNode = document.createTextNode('test content');
    const childNode1 = document.createElement('div');
    childNode1.appendChild(mockAnchorNode);
    const childNode2 = document.createElement('div');
    childNode2.appendChild(document.createTextNode('previous content'));
    
    mockEditor = document.createElement('div');
    mockEditor.appendChild(childNode2);
    mockEditor.appendChild(childNode1);

    // 初始化模拟选区
    mockSelection = {
      anchorNode: mockAnchorNode,
      anchorOffset: 2,
      collapse: jest.fn()
    };
  });

  test('光标在文本中间时,左移一位', () => {
    const selectionInstance = new Selection(mockEditor, () => mockSelection as Selection);
    selectionInstance.moveCursorLeft();
    
    expect(mockSelection.collapse).toHaveBeenCalledWith(mockAnchorNode, 1);
  });

  test('光标在文本开头时,跳转至前一子节点末尾', () => {
    mockSelection.anchorOffset = 0;
    const selectionInstance = new Selection(mockEditor, () => mockSelection as Selection);
    selectionInstance.moveCursorLeft();
    
    const prevTextNode = mockEditor.children[0].firstChild as Text;
    expect(mockSelection.collapse).toHaveBeenCalledWith(prevTextNode, prevTextNode.nodeValue.length - 1);
  });

  test('无有效选区时,方法直接返回', () => {
    const selectionInstance = new Selection(mockEditor, () => null);
    selectionInstance.moveCursorLeft();
    
    expect(mockSelection.collapse).not.toHaveBeenCalled();
  });
});

辅助配置:启用jsdom模拟浏览器环境

确保你的jest.config.js中配置了testEnvironment: 'jsdom',模拟基本DOM和window对象,方便创建测试用节点:

module.exports = {
  testEnvironment: 'jsdom',
  // 其他Jest配置项
};

额外优化建议

  • 抽离浏览器Selection/Range的类型接口,让Mock逻辑更清晰
  • 将DOM节点判断逻辑(如prevNode.firstChild.nodeValue)拆分为独立小函数,进一步提升可测试性

内容的提问来源于stack exchange,提问作者Tarun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 03:45:53