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

4

21

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);
    });
}
Culpa answered 29/10, 2020 at 8:40 Comment(2)
Seems the official test example provided on dropzone's side is so bad and confusing and some parts not working in action. do you find any way to test components contains this component and drop event?Apothem
@PouyaJabbarisani I rewrote the test component, please see my answerCulpa
P
5

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

Papyrus answered 2/11, 2020 at 10:41 Comment(3)
Welcome to Stack Overflow. While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. How to AnswerEcheverria
It didn't help me. Files are still equal to an empty arrayCulpa
I changed fireEvent(node, event); to fireEvent.drop(node, event); and I added await waitFor(() => expect(screen.getByText("ping.json")).toBeInTheDocument()); to the last line and the test passed.Papyrus
C
14

With the rokki`s answer (https://mcmap.net/q/604716/-how-to-test-react-dropzone-with-jest-and-react-testing-library), 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();
}
Culpa answered 5/11, 2020 at 10:37 Comment(1)
didn't work for me, not sure why notEntremets
E
6

Although the accepted answer does trigger the event onDrop, that wasn't enough for me to test with useDropzone() because the hook's states, like acceptedFiles, weren't updated.

I found this code snippet that uses userEvent.upload(<input>, <files>) to upload files to the nested <input>. I'm gonna paste the relevant code here in case the link is gone.

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

Notice that the element set as data-testid="dropzone" is the <input>, and not the <div>. That's required so userEvent.upload can adequately perform the upload.

Ethiopia answered 25/2, 2023 at 0:57 Comment(2)
This is the only solution that worked in my case. userEvent being up-to date compated to fireEvent makes this a latest soltion.Outcurve
import userEvent from "@testing-library/user-event";Anybody
P
5

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

Papyrus answered 2/11, 2020 at 10:41 Comment(3)
Welcome to Stack Overflow. While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. How to AnswerEcheverria
It didn't help me. Files are still equal to an empty arrayCulpa
I changed fireEvent(node, event); to fireEvent.drop(node, event); and I added await waitFor(() => expect(screen.getByText("ping.json")).toBeInTheDocument()); to the last line and the test passed.Papyrus
R
0

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);
});
Reactor answered 3/7, 2022 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.