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
相关产品推荐
相关产品推荐

