如何使用Jest和react-testing-library测试react-dropzone?

18

我想在React组件中使用react-dropzone库的onDrop方法进行测试。我正在使用Jest和React Testing Library。我创建了一个模拟文件并尝试将这些文件放入输入框中,但在控制台中,文件仍然等于一个空数组。你有任何想法吗?

package.json

"typescript": "^3.9.7",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.0.4",
"@types/jest": "^26.0.13",
"jest": "^26.4.2",
"ts-jest": "^26.3.0",
"react-router-dom": "^5.1.2",
"react-dropzone": "^10.1.10",
"@types/react-dropzone": "4.2.0",

ModalImportFile.tsx

import React, { FC, useState } from "react";
import { Box, Button, Dialog, DialogContent, DialogTitle, Grid } from "@material-ui/core";
import { useDropzone } from "react-dropzone";
import AttachFileIcon from "@material-ui/icons/AttachFile";
import DeleteIcon from "@material-ui/icons/Delete";

interface Props {
    isOpen: boolean;
}

interface Events {
    onClose: () => void;
}

const ModalImportFile: FC<Props & Events> = props => {
    const { isOpen } = props as Props;
    const { onClose } = props as Events;

    const [files, setFiles] = useState<Array<File>>([]);

    const { getRootProps, getInputProps, open } = useDropzone({
        onDrop: (acceptedFiles: []) => {
            setFiles(
                acceptedFiles.map((file: File) =>
                    Object.assign(file, {
                        preview: URL.createObjectURL(file),
                    }),
                ),
            );
        },
        noClick: true,
        noKeyboard: true,
    });

    const getDragZoneContent = () => {
        if (files && files.length > 0)
            return (
                <Box border={1} borderRadius={5} borderColor={"#cecece"} p={2} mb={2}>
                    <Grid container alignItems="center" justify="space-between">
                        <Box color="text.primary">{files[0].name}</Box>
                        <Box ml={1} color="text.secondary">
                            <Button
                                startIcon={<DeleteIcon color="error" />}
                                onClick={() => {
                                    setFiles([]);
                                }}
                            />
                        </Box>
                    </Grid>
                </Box>
            );
        return (
            <Box border={1} borderRadius={5} borderColor={"#cecece"} p={2} mb={2} style={{ borderStyle: "dashed" }}>
                <Grid container alignItems="center">
                    <Box mr={1} color="text.secondary">
                        <AttachFileIcon />
                    </Box>
                    <Box color="text.secondary">
                        <Box onClick={open} component="span" marginLeft="5px">
                            Download
                        </Box>
                    </Box>
                </Grid>
            </Box>
        );
    };

    const closeHandler = () => {
        onClose();
        setFiles([]);
    };

    return (
        <Dialog open={isOpen} onClose={closeHandler}>
            <Box width={520}>
                <DialogTitle>Import</DialogTitle>
                <DialogContent>
                    <div data-testid="container" className="container">
                        <div data-testid="dropzone" {...getRootProps({ className: "dropzone" })}>
                            <input data-testid="drop-input" {...getInputProps()} />
                            {getDragZoneContent()}
                        </div>
                    </div>
                </DialogContent>
            </Box>
        </Dialog>
    );
};

export default ModalImportFile;

ModalImportFile.test.tsx

import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import ModalImportFile from "../../components/task/elements/ModalImportFile";

const props = {
    isOpen: true,
    onClose: jest.fn(),
};

beforeEach(() => jest.clearAllMocks());

describe("<ModalImportFile/>", () => {
    it("should drop", async () => {
        render(<ModalImportFile {...props} />);

        const file = new File([JSON.stringify({ ping: true })], "ping.json", { type: "application/json" });
        const data = mockData([file]);

        function dispatchEvt(node: any, type: any, data: any) {
            const event = new Event(type, { bubbles: true });
            Object.assign(event, data);
            fireEvent(node, event);
        }

        function mockData(files: Array<File>) {
            return {
                dataTransfer: {
                    files,
                    items: files.map(file => ({
                        kind: "file",
                        type: file.type,
                        getAsFile: () => file,
                    })),
                    types: ["Files"],
                },
            };
        }
        const inputEl = screen.getByTestId("drop-input");
        dispatchEvt(inputEl, "dragenter", data);
    });
}

似乎Dropzone官方提供的测试示例非常糟糕和令人困惑,有些部分在操作中也无法正常工作。你是否找到了任何测试组件以包含此组件和拖放事件的方法? - Pouya Jabbarisani
@PouyaJabbarisani,我重写了测试组件,请查看我的答案。 - Evgeniy Valyaev
4个回答

13

借助rokk的回答(https://dev59.com/q1IG5IYBdhLWcg3wiApO#64643985),我对测试组件进行了重写以便更易于理解。

ModalImportFile.test.tsx

import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import ModalImportFile from "../../components/task/elements/ModalImportFile";

const props = {
    isOpen: true,
    onClose: jest.fn(),
};

beforeEach(() => jest.clearAllMocks());

describe("<ModalImportFile/>", () => {
    it("should drop", async () => {
        render(<ModalImportFile {...props} />);
        window.URL.createObjectURL = jest.fn().mockImplementation(() => "url");
        const inputEl = screen.getByTestId("drop-input");
        const file = new File(["file"], "ping.json", {
            type: "application/json",
        });
        Object.defineProperty(inputEl, "files", {
            value: [file],
        });
        fireEvent.drop(inputEl);
        expect(await screen.findByText("ping.json")).toBeInTheDocument();
}

没有对我有效,不确定原因。 - ChumiestBucket

4

fireEvent(node, event);更改为fireEvent.drop(node, event);,你觉得怎么样?


1
欢迎来到 Stack Overflow。虽然这段代码可能回答了问题,但提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。如何回答问题 - Elletlar
它没有帮助我。文件仍然等于一个空数组。 - Evgeniy Valyaev
1
我将 fireEvent(node, event); 改为了 fireEvent.drop(node, event);,并在最后一行添加了 await waitFor(() => expect(screen.getByText("ping.json")).toBeInTheDocument());,测试通过了。 - rokki

3

尽管被接受的答案会触发事件onDrop,但这对我来说不足以测试useDropzone(),因为钩子的状态,如acceptedFiles,没有更新。

我找到了this代码片段,它使用userEvent.upload(<input>, <files>)将文件上传到嵌套的<input>中。我会在这里粘贴相关的代码,以防链接失效。

App.test.tsx

test("upload multiple files", () => {
  const files = [
    new File(["hello"], "hello.geojson", { type: "application/json" }),
    new File(["there"], "hello2.geojson", { type: "application/json" })
  ];

  const { getByTestId } = render(<App />);
  const input = getByTestId("dropzone") as HTMLInputElement;
  userEvent.upload(input, files);

  expect(input.files).toHaveLength(2);
  expect(input.files[0]).toStrictEqual(files[0]);
  expect(input.files[1]).toStrictEqual(files[1]);
});

App.tsx

export default function App() {
  
  const {
    acceptedFiles,
    isDragActive,
    isDragAccept,
    isDragReject,
    getRootProps,
    getInputProps
  } = useDropzone({ accept: ".geojson, .geotiff, .tiff" });

  useEffect(() => console.log(acceptedFiles), [acceptedFiles]);

  return (
    <section>
      <div {...getRootProps()}>
        <input data-testid="dropzone" {...getInputProps()} />
        <p>Drag 'n' drop some files here, or click to select files</p>
      </div>
    </section>
  );
}

请注意,设置为 data-testid="dropzone" 的元素是 <input>,而不是 <div>。这是必需的,以便 userEvent.upload 能够充分执行上传操作。

0

References: https://jestjs.io/docs/jest-object#jestrequireactualmodulename

requireActual

返回实际的模块而不是模拟对象,绕过所有检查,无论该模块是否应该接收模拟实现。


let dropCallback = null;
let onDragEnterCallback = null;
let onDragLeaveCallback = null;

jest.mock('react-dropzone', () => ({
  ...jest.requireActual('react-dropzone'),
  useDropzone: options => {
    dropCallback = options.onDrop;
    onDragEnterCallback = options.onDragEnter;
    onDragLeaveCallback = options.onDragLeave;

    return {
      acceptedFiles: [{
          path: 'sample4.png'
        },
        {
          path: 'sample3.png'
        }
      ],
      fileRejections: [{
        file: {
          path: 'FileSelector.docx'
        },
        errors: [{
          code: 'file-invalid-type',
          message: 'File type must be image/*'
        }]
      }],
      getRootProps: jest.fn(),
      getInputProps: jest.fn(),
      open: jest.fn()
    };
  }
}));


it('Should get on drop Function with parameter', async() => {
  const accepted = [{
      path: 'sample4.png'
    },
    {
      path: 'sample3.png'
    },
    {
      path: 'sample2.png'
    }
  ];
  const rejected = [{
    file: {
      path: 'FileSelector.docx'
    },
    errors: [{
      code: 'file-invalid-type',
      message: 'File type must be image/*'
    }]
  }];

  const event = {
    bubbles: true,
    cancelable: false,
    currentTarget: null,
    defaultPrevented: true,
    eventPhase: 3,
    isDefaultPrevented: () => {},
    isPropagationStopped: () => {},
    isTrusted: true,
    target: {
      files: {
        '0': {
          path: 'FileSelector.docx'
        },
        '1': {
          path: 'sample4.png'
        },
        '2': {
          path: 'sample3.png'
        },
        '3': {
          path: 'sample2.png'
        }
      }
    },
    timeStamp: 1854316.299999997,
    type: 'change'
  };
  dropCallback(accepted, rejected, event);
  onDragEnterCallback();
  onDragLeaveCallback();
  expect(handleFiles).toHaveBeenCalledTimes(1);
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接