如何正确模拟React Beautiful DnD拖拽事件通过单元测试
React Beautiful DnD 拖拽单元测试失效修复方案
问题背景
测试目标为模拟拖拽放置交互,验证表格行顺序是否按预期变更,在检索大量React Beautiful DnD测试示例后仍无法通过对应单元测试:
- 组件基于
DragDropContext、Droppable实现可拖拽表格,核心实现代码如下:
<DragDropContext onDragEnd={handleDragEnd}> <Table sx={{ minWidth: 650, borderCollapse: "separate", borderSpacing: "0px 1rem" }} aria-label="simple table"> <TableHead> <TableRow> <TableCell sx={{ color: "white", border: "0px solid" }}>ORDER</TableCell> <TableCell sx={{ color: "white", border: "0px solid" }} align="center">TYPE</TableCell> <TableCell sx={{ color: "white", border: "0px solid" }} align="left">CONTENT</TableCell> <TableCell sx={{ color: "white", border: "0px solid" }} align="right">DELETE</TableCell> </TableRow> </TableHead> <Droppable droppableId="droppable-1"> {(provider) => ( <TableBody ref={provider.innerRef} {...provider.droppableProps}> {tableData()} {provider.placeholder} </TableBody> )} </Droppable> </Table> </DragDropContext>
- 拖拽结束回调
handleDragEnd逻辑如下:
const handleDragEnd = (e) => { if (!e.destination) return; let tempData = Array.from(informationData); let [source_data] = tempData.splice(e.source.index, 1); tempData.splice(e.destination.index, 0, source_data); props.dispatch(setGeneralItem("informationData", tempData)); };
- 原有测试用例尝试通过
fireEvent触发dragStart和drop事件模拟拖拽流程,但测试失败:拖拽完成后派发的状态仍是初始列表,而非预期重排后的列表,onDragEnd回调未正常执行。原测试代码如下:
test("Drag and drop", ()=>{ jest.mock('react-beautiful-dnd', () => ({ Droppable: ({ children }) => children({ draggableProps: { key:{}, draggableId:{}, index:{}, style: {}, onDragEnd: jest.fn(), }, innerRef: jest.fn(), }, {}), Draggable: ({ children }) => children({ draggableProps: { style: {}, }, innerRef: jest.fn(), }, {}), DragDropContext: ({ children }) => children, })); jest.spyOn(actions, "setGeneralItem"); let info = [ { order: 1, dataType: "Text", contentUrl: "some text" }, { order: 2, dataType: "Image", contentUrl: "someimageurl" }, { order: 3, dataType: "Video", contentUrl: "somevideourl" }, ] let tempData = [ { order: 1, dataType: "Image", contentUrl: "someimageurl" }, { order: 2, dataType: "Video", contentUrl: "somevideourl" }, { order: 3, dataType: "Text", contentUrl: "some text" }, ] store.dispatch(actions.setGeneralItem("informationData", info)) let mockDispatch = (val) => { store.dispatch(val); } const { container } = render( renderWithStore(store, <Information {...props} dispatch={mockDispatch}/>)); let dragStart = container.querySelectorAll('tr')[1].querySelectorAll('td')[1] let dropPosition = container.querySelectorAll('tr')[3].querySelectorAll('td')[1] fireEvent.dragStart(dragStart) fireEvent.drop(dropPosition) expect(actions.setGeneralItem).toHaveBeenCalled(); expect(actions.setGeneralItem).toHaveBeenCalledTimes(1); expect(actions.setGeneralItem).toHaveBeenCalledWith("informationData", tempData); })
问题根因
react-beautiful-dndmock逻辑错误:当前mock将DragDropContext直接处理为返回子元素的空组件,既没有绑定onDragEnd回调,也没有实现拖拽事件识别逻辑,原生fireEvent触发的drag类事件无法被mock层识别,不会执行拖拽结束回调。- Droppable、Draggable组件mock传参不符合要求:没有返回正确的拖拽属性、占位元素配置,回调执行时无法拿到合法的
source、destination参数,会直接命中if (!e.destination) return的提前退出逻辑。 - 事件触发逻辑错误:拖拽事件绑定在
Draggable包裹的行根元素而非内部td单元格,且仅触发dragStart和drop事件无法覆盖react-beautiful-dnd的完整事件流,mock环境下无需模拟全量原生事件,直接触发拖拽生命周期回调即可。
修复方案
- 修正
react-beautiful-dnd的Jest mock配置,将mock代码放在测试文件顶部、import语句之后,不要写在测试用例内部:
jest.mock('react-beautiful-dnd', () => { const actual = jest.requireActual('react-beautiful-dnd'); return { ...actual, DragDropContext: ({ children, onDragEnd }) => { // 将拖拽结束回调挂到全局,测试中可直接调用 window.__rbdOnDragEnd = onDragEnd; return <>{children}</>; }, Droppable: ({ children }) => children({ innerRef: jest.fn(), droppableProps: { 'data-rbd-droppable-id': 'droppable-1' }, placeholder: null }, { isDraggingOver: false }), Draggable: ({ children, draggableId, index }) => children({ innerRef: jest.fn(), draggableProps: { 'data-rbd-draggable-id': draggableId, 'data-rbd-draggable-index': index, style: {} }, dragHandleProps: { 'data-testid': `drag-handle-${index}` } }, { isDragging: false }) } });
- 修改测试用例逻辑,无需手动触发原生drag事件,直接调用暴露的
onDragEnd回调,传入正确的source、destination参数即可,这也是React Beautiful DnD官方推荐的单元测试写法:
test("Drag and drop", ()=>{ jest.spyOn(actions, "setGeneralItem"); const info = [ { order: 1, dataType: "Text", contentUrl: "some text" }, { order: 2, dataType: "Image", contentUrl: "someimageurl" }, { order: 3, dataType: "Video", contentUrl: "somevideourl" }, ] // 索引0的项移动到索引2位置后的正确预期结果 const expectedData = [ { order: 2, dataType: "Image", contentUrl: "someimageurl" }, { order: 3, dataType: "Video", contentUrl: "somevideourl" }, { order: 1, dataType: "Text", contentUrl: "some text" }, ] store.dispatch(actions.setGeneralItem("informationData", info)) const mockDispatch = (val) => { store.dispatch(val); } render(renderWithStore(store, <Information {...props} dispatch={mockDispatch}/>)); // 模拟拖拽完成:将droppable-1下索引0的项拖到索引2位置 window.__rbdOnDragEnd({ source: { index: 0, droppableId: "droppable-1" }, destination: { index: 2, droppableId: "droppable-1" } }); expect(actions.setGeneralItem).toHaveBeenCalled(); expect(actions.setGeneralItem).toHaveBeenCalledTimes(1); expect(actions.setGeneralItem).toHaveBeenCalledWith("informationData", expectedData); })
注意:如果需要模拟真实用户的拖拽操作流而非直接调用回调,可以使用
@testing-library/user-event的拖拽API,配合官方测试预设mock即可,无需自行实现全量mock逻辑。如果order字段为数据存储字段而非渲染时按索引生成,需要在handleDragEnd中同步更新所有项的order值,否则后续拖拽索引会出现错乱。
内容的提问来源于stack exchange,提问作者Florentina Ilie
相关产品推荐
相关产品推荐

