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

如何正确模拟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-dnd mock逻辑错误:当前mock将DragDropContext直接处理为返回子元素的空组件,既没有绑定onDragEnd回调,也没有实现拖拽事件识别逻辑,原生fireEvent触发的drag类事件无法被mock层识别,不会执行拖拽结束回调。
  • Droppable、Draggable组件mock传参不符合要求:没有返回正确的拖拽属性、占位元素配置,回调执行时无法拿到合法的source、destination参数,会直接命中if (!e.destination) return的提前退出逻辑。
  • 事件触发逻辑错误:拖拽事件绑定在Draggable包裹的行根元素而非内部td单元格,且仅触发dragStart和drop事件无法覆盖react-beautiful-dnd的完整事件流,mock环境下无需模拟全量原生事件,直接触发拖拽生命周期回调即可。

修复方案

  1. 修正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 })
  }
});
  1. 修改测试用例逻辑,无需手动触发原生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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 05:27:20