How to test react-dropzone with Jest and react-testing-library?

Viewed 8056

I want to test onDrop method from react-dropzone library in React component. I am using Jest, React Testing Library. I'm creating mock file and I'm trying to drop this files in input, but in console.log files are still equal to an empty array. Do you have any ideas?

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);
    });
}
3 Answers

With the rokki`s answer (https://stackoverflow.com/a/64643985/9405587), I rewrote the test component for easier understanding.

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();
}

How about changing fireEvent(node, event); to fireEvent.drop(node, event);.

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

requireActual

Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not.


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);
});
Related