react-dropzone-async-validation
Version:
Simple HTML5 drag-drop zone with React.js
1,639 lines (1,351 loc) • 108 kB
JavaScript
/* eslint react/prop-types: 0, jsx-a11y/label-has-for: 0 */
import React, { createRef } from "react";
import { act, cleanup, fireEvent, render } from "@testing-library/react";
import { renderHook } from "@testing-library/react-hooks";
import { fromEvent } from "file-selector";
import * as utils from "./utils";
import Dropzone, { useDropzone } from "./index";
describe("useDropzone() hook", () => {
let files;
let images;
beforeEach(() => {
files = [createFile("file1.pdf", 1111, "application/pdf")];
images = [
createFile("cats.gif", 1234, "image/gif"),
createFile("dogs.gif", 2345, "image/jpeg"),
];
});
afterEach(cleanup);
describe("behavior", () => {
it("renders the root and input nodes with the necessary props", () => {
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.innerHTML).toMatchSnapshot();
});
it("sets {accept} prop on the <input>", () => {
const accept = {
"image/jpeg": [],
};
const { container } = render(
<Dropzone accept={accept}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("input")).toHaveAttribute(
"accept",
"image/jpeg"
);
});
it("updates {multiple} prop on the <input> when it changes", () => {
const { container, rerender } = render(
<Dropzone
accept={{
"image/jpeg": [],
}}
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("input")).toHaveAttribute(
"accept",
"image/jpeg"
);
rerender(
<Dropzone
accept={{
"image/png": [],
}}
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("input")).toHaveAttribute(
"accept",
"image/png"
);
});
it("sets {multiple} prop on the <input>", () => {
const { container } = render(
<Dropzone multiple>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("input")).toHaveAttribute("multiple");
});
it("updates {multiple} prop on the <input> when it changes", () => {
const { container, rerender } = render(
<Dropzone multiple={false}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("input")).not.toHaveAttribute("multiple");
rerender(
<Dropzone multiple>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("input")).toHaveAttribute("multiple");
});
it("sets any props passed to the input props getter on the <input>", () => {
const name = "dropzone-input";
const { container } = render(
<Dropzone multiple>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps({ name })} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("input")).toHaveAttribute("name", name);
});
it("sets any props passed to the root props getter on the root node", () => {
const ariaLabel = "Dropzone area";
const { container } = render(
<Dropzone multiple>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps({ "aria-label": ariaLabel })}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("div")).toHaveAttribute(
"aria-label",
ariaLabel
);
});
it("runs the custom callback handlers provided to the root props getter", async () => {
const event = createDtWithFiles(files);
const rootProps = {
onClick: jest.fn(),
onKeyDown: jest.fn(),
onFocus: jest.fn(),
onBlur: jest.fn(),
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps(rootProps)}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(rootProps.onClick).toHaveBeenCalled();
fireEvent.focus(dropzone);
fireEvent.keyDown(dropzone);
expect(rootProps.onFocus).toHaveBeenCalled();
expect(rootProps.onKeyDown).toHaveBeenCalled();
fireEvent.blur(dropzone);
expect(rootProps.onBlur).toHaveBeenCalled();
await act(() => fireEvent.dragEnter(dropzone, event));
expect(rootProps.onDragEnter).toHaveBeenCalled();
fireEvent.dragOver(dropzone, event);
expect(rootProps.onDragOver).toHaveBeenCalled();
fireEvent.dragLeave(dropzone, event);
expect(rootProps.onDragLeave).toHaveBeenCalled();
await act(() => fireEvent.drop(dropzone, event));
expect(rootProps.onDrop).toHaveBeenCalled();
});
it("runs the custom callback handlers provided to the input props getter", async () => {
const inputProps = {
onClick: jest.fn(),
onChange: jest.fn(),
};
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps(inputProps)} />
</div>
)}
</Dropzone>
);
const input = container.querySelector("input");
fireEvent.click(input);
expect(inputProps.onClick).toHaveBeenCalled();
await act(async () => fireEvent.change(input, { target: { files: [] } }));
expect(inputProps.onChange).toHaveBeenCalled();
});
it("runs no callback handlers if {disabled} is true", async () => {
const event = createDtWithFiles(files);
const rootProps = {
onClick: jest.fn(),
onKeyDown: jest.fn(),
onFocus: jest.fn(),
onBlur: jest.fn(),
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const inputProps = {
onClick: jest.fn(),
onChange: jest.fn(),
};
const { container } = render(
<Dropzone disabled>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps(rootProps)}>
<input {...getInputProps(inputProps)} />
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(rootProps.onClick).not.toHaveBeenCalled();
fireEvent.focus(dropzone);
fireEvent.keyDown(dropzone);
expect(rootProps.onFocus).not.toHaveBeenCalled();
expect(rootProps.onKeyDown).not.toHaveBeenCalled();
fireEvent.blur(dropzone);
expect(rootProps.onBlur).not.toHaveBeenCalled();
await act(() => fireEvent.dragEnter(dropzone, event));
expect(rootProps.onDragEnter).not.toHaveBeenCalled();
fireEvent.dragOver(dropzone, event);
expect(rootProps.onDragOver).not.toHaveBeenCalled();
fireEvent.dragLeave(dropzone, event);
expect(rootProps.onDragLeave).not.toHaveBeenCalled();
await act(() => fireEvent.drop(dropzone, event));
expect(rootProps.onDrop).not.toHaveBeenCalled();
const input = container.querySelector("input");
fireEvent.click(input);
expect(inputProps.onClick).not.toHaveBeenCalled();
await act(() => fireEvent.change(input));
expect(inputProps.onChange).not.toHaveBeenCalled();
});
test("{rootRef, inputRef} are exposed", () => {
const { result } = renderHook(() => useDropzone());
const { rootRef, inputRef, getRootProps, getInputProps } = result.current;
const { container } = render(
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
);
expect(container.querySelector("div")).toEqual(rootRef.current);
expect(container.querySelector("input")).toEqual(inputRef.current);
});
test("<Dropzone> exposes and sets the ref if using a ref object", () => {
const dropzoneRef = createRef();
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const ui = (
<Dropzone ref={dropzoneRef}>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const { rerender } = render(ui);
expect(dropzoneRef.current).not.toBeNull();
expect(typeof dropzoneRef.current.open).toEqual("function");
act(() => dropzoneRef.current.open());
expect(onClickSpy).toHaveBeenCalled();
rerender(null);
expect(dropzoneRef.current).toBeNull();
});
test("<Dropzone> exposes and sets the ref if using a ref fn", () => {
let dropzoneRef;
const setRef = (ref) => (dropzoneRef = ref);
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const ui = (
<Dropzone ref={setRef}>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const { rerender } = render(ui);
expect(dropzoneRef).not.toBeNull();
expect(typeof dropzoneRef.open).toEqual("function");
act(() => dropzoneRef.open());
expect(onClickSpy).toHaveBeenCalled();
rerender(null);
expect(dropzoneRef).toBeNull();
});
test("<Dropzone> doesn't invoke the ref fn if it hasn't changed", () => {
const setRef = jest.fn();
const { rerender } = render(
<Dropzone ref={setRef}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
rerender(
<Dropzone ref={setRef}>
{({ getRootProps }) => <div {...getRootProps()} />}
</Dropzone>
);
expect(setRef).toHaveBeenCalledTimes(1);
});
it("sets {isFocused} to false if {disabled} is true", () => {
const { container, rerender } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
rerender(
<Dropzone disabled>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
expect(dropzone.querySelector("#focus")).toBeNull();
});
test("{tabindex} is 0 if {disabled} is false", () => {
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("div")).toHaveAttribute("tabindex", "0");
});
test("{tabindex} is not set if {disabled} is true", () => {
const { container, rerender } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("div")).toHaveAttribute("tabindex", "0");
rerender(
<Dropzone disabled>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("div")).not.toHaveAttribute("tabindex");
});
test("{tabindex} is not set if {noKeyboard} is true", () => {
const { container, rerender } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("div")).toHaveAttribute("tabindex", "0");
rerender(
<Dropzone noKeyboard>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(container.querySelector("div")).not.toHaveAttribute("tabindex");
});
test("refs are set when {refKey} is set to a different value", (done) => {
const data = createDtWithFiles(files);
class MyView extends React.Component {
render() {
const { children, innerRef, ...rest } = this.props;
return (
<div id="dropzone" ref={innerRef} {...rest}>
<div>{children}</div>
</div>
);
}
}
const ui = (
<Dropzone>
{({ getRootProps }) => (
<MyView {...getRootProps({ refKey: "innerRef" })}>
<span>Drop some files here ...</span>
</MyView>
)}
</Dropzone>
);
const { container, rerender } = render(ui);
const dropzone = container.querySelector("#dropzone");
const fn = async () => {
await act(() => fireEvent.drop(dropzone, data));
rerender(ui);
done();
};
expect(fn).not.toThrow();
});
test("click events originating from <label> should not trigger file dialog open twice", () => {
const activeRef = createRef();
const active = <span ref={activeRef}>I am active</span>;
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFileDialogActive }) => (
<label {...getRootProps()}>
<input {...getInputProps()} />
{isFileDialogActive && active}
</label>
)}
</Dropzone>
);
const dropzone = container.querySelector("label");
fireEvent.click(dropzone, { bubbles: true, cancelable: true });
expect(activeRef.current).not.toBeNull();
expect(dropzone).toContainElement(activeRef.current);
expect(onClickSpy).toHaveBeenCalledTimes(1);
});
});
describe("document drop protection", () => {
const addEventListenerSpy = jest.spyOn(document, "addEventListener");
const removeEventListenerSpy = jest.spyOn(document, "removeEventListener");
// Collect the list of addEventListener/removeEventListener spy calls into an object keyed by event name
const collectEventListenerCalls = (spy) =>
spy.mock.calls.reduce(
(acc, [eventName, ...rest]) => ({
...acc,
[eventName]: rest,
}),
{}
);
it("installs hooks to prevent stray drops from taking over the browser window", () => {
render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
expect(addEventListenerSpy).toHaveBeenCalledTimes(2);
const addEventCalls = collectEventListenerCalls(addEventListenerSpy);
const events = Object.keys(addEventCalls);
expect(events).toContain("dragover");
expect(events).toContain("drop");
events.forEach((eventName) => {
const [fn, options] = addEventCalls[eventName];
expect(fn).toBeDefined();
expect(options).toBe(false);
});
});
it("removes document hooks when unmounted", () => {
const { unmount } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledTimes(2);
const addEventCalls = collectEventListenerCalls(addEventListenerSpy);
const removeEventCalls = collectEventListenerCalls(
removeEventListenerSpy
);
const events = Object.keys(removeEventCalls);
expect(events).toContain("dragover");
expect(events).toContain("drop");
events.forEach((eventName) => {
const [a] = addEventCalls[eventName];
const [b] = removeEventCalls[eventName];
expect(a).toEqual(b);
});
});
it("terminates drags and drops on elements outside our dropzone", () => {
render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const dragEvt = new Event("dragover", { bubbles: true });
const dragEvtPreventDefaultSpy = jest.spyOn(dragEvt, "preventDefault");
fireEvent(document.body, dragEvt);
expect(dragEvtPreventDefaultSpy).toHaveBeenCalled();
const dropEvt = new Event("drop", { bubbles: true });
const dropEvtPreventDefaultSpy = jest.spyOn(dropEvt, "preventDefault");
fireEvent(document.body, dropEvt);
expect(dropEvtPreventDefaultSpy).toHaveBeenCalled();
});
it("permits drags and drops on elements inside our dropzone", () => {
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const dropEvt = new Event("drop", { bubbles: true });
const dropEvtPreventDefaultSpy = jest.spyOn(dropEvt, "preventDefault");
fireEvent(container.querySelector("div"), dropEvt);
// A call is from the onDrop handler for the dropzone,
// but there should be no more than 1
expect(dropEvtPreventDefaultSpy).toHaveBeenCalled();
});
it("does not prevent stray drops when {preventDropOnDocument} is false", () => {
render(
<Dropzone preventDropOnDocument={false}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const dropEvt = new Event("drop", { bubbles: true });
const dropEvtPreventDefaultSpy = jest.spyOn(dropEvt, "preventDefault");
fireEvent(document.body, dropEvt);
expect(dropEvtPreventDefaultSpy).toHaveBeenCalledTimes(0);
});
});
describe("event propagation", () => {
const data = createDtWithFiles(files);
test("drag events propagate from the inner dropzone to parents", async () => {
const innerProps = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const InnerDropzone = () => (
<Dropzone {...innerProps}>
{({ getRootProps, getInputProps }) => (
<div id="inner-dropzone" {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const parentProps = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const { container } = render(
<Dropzone {...parentProps}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
<InnerDropzone />
</div>
)}
</Dropzone>
);
const innerDropzone = container.querySelector("#inner-dropzone");
await act(() => fireEvent.dragEnter(innerDropzone, data));
expect(innerProps.onDragEnter).toHaveBeenCalled();
expect(parentProps.onDragEnter).toHaveBeenCalled();
fireEvent.dragOver(innerDropzone, data);
expect(innerProps.onDragOver).toHaveBeenCalled();
expect(parentProps.onDragOver).toHaveBeenCalled();
fireEvent.dragLeave(innerDropzone, data);
expect(innerProps.onDragLeave).toHaveBeenCalled();
expect(parentProps.onDragLeave).toHaveBeenCalled();
await act(() => fireEvent.drop(innerDropzone, data));
expect(innerProps.onDrop).toHaveBeenCalled();
expect(parentProps.onDrop).toHaveBeenCalled();
});
test("drag events do not propagate from the inner dropzone to parent dropzone if user invoked stopPropagation() on the events", async () => {
const innerProps = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
Object.keys(innerProps).forEach((prop) =>
innerProps[prop].mockImplementation((...args) => {
const event = prop === "onDrop" ? args.pop() : args.shift();
event.stopPropagation();
})
);
const InnerDropzone = () => (
<Dropzone {...innerProps}>
{({ getRootProps, getInputProps }) => (
<div id="inner-dropzone" {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const parentProps = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const { container } = render(
<Dropzone {...parentProps}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
<InnerDropzone />
</div>
)}
</Dropzone>
);
const innerDropzone = container.querySelector("#inner-dropzone");
await act(() => fireEvent.dragEnter(innerDropzone, data));
expect(innerProps.onDragEnter).toHaveBeenCalled();
expect(parentProps.onDragEnter).not.toHaveBeenCalled();
fireEvent.dragOver(innerDropzone, data);
expect(innerProps.onDragOver).toHaveBeenCalled();
expect(parentProps.onDragOver).not.toHaveBeenCalled();
fireEvent.dragLeave(innerDropzone, data);
expect(innerProps.onDragLeave).toHaveBeenCalled();
expect(parentProps.onDragLeave).not.toHaveBeenCalled();
await act(() => fireEvent.drop(innerDropzone, data));
expect(innerProps.onDrop).toHaveBeenCalled();
expect(parentProps.onDrop).not.toHaveBeenCalled();
});
test("drag events do not propagate from the inner dropzone to parent dropzone if {noDragEventsBubbling} is true", async () => {
const innerProps = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const InnerDropzone = () => (
<Dropzone {...innerProps} noDragEventsBubbling>
{({ getRootProps, getInputProps }) => (
<div id="inner-dropzone" {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const parentProps = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const { container } = render(
<Dropzone {...parentProps}>
{({ getRootProps, getInputProps }) => (
<div id="outer-dropzone" {...getRootProps()}>
<input {...getInputProps()} />
<InnerDropzone />
</div>
)}
</Dropzone>
);
const outerDropzone = container.querySelector("#outer-dropzone");
const innerDropzone = container.querySelector("#inner-dropzone");
// Sets drag targets on the outer dropzone
await act(() => fireEvent.dragEnter(outerDropzone, data));
await act(() => fireEvent.dragEnter(innerDropzone, data));
expect(innerProps.onDragEnter).toHaveBeenCalled();
expect(parentProps.onDragEnter).toHaveBeenCalledTimes(1);
fireEvent.dragOver(innerDropzone, data);
expect(innerProps.onDragOver).toHaveBeenCalled();
expect(parentProps.onDragOver).not.toHaveBeenCalled();
fireEvent.dragLeave(innerDropzone, data);
expect(innerProps.onDragLeave).toHaveBeenCalled();
expect(parentProps.onDragLeave).not.toHaveBeenCalled();
await act(() => fireEvent.drop(innerDropzone, data));
expect(innerProps.onDrop).toHaveBeenCalled();
expect(parentProps.onDrop).not.toHaveBeenCalled();
});
test("onDragLeave is not invoked for the parent dropzone if it was invoked for an inner dropzone", async () => {
const innerDragLeave = jest.fn();
const InnerDropzone = () => (
<Dropzone onDragLeave={innerDragLeave}>
{({ getRootProps, getInputProps }) => (
<div id="inner-dropzone" {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const parentDragLeave = jest.fn();
const { container } = render(
<Dropzone onDragLeave={parentDragLeave}>
{({ getRootProps, getInputProps }) => (
<div id="parent-dropzone" {...getRootProps()}>
<input {...getInputProps()} />
<InnerDropzone />
</div>
)}
</Dropzone>
);
const parentDropzone = container.querySelector("#parent-dropzone");
await act(() => fireEvent.dragEnter(parentDropzone, data));
const innerDropzone = container.querySelector("#inner-dropzone");
await act(() => fireEvent.dragEnter(innerDropzone, data));
fireEvent.dragLeave(innerDropzone, data);
expect(innerDragLeave).toHaveBeenCalled();
expect(parentDragLeave).not.toHaveBeenCalled();
});
});
describe("plugin integration", () => {
it("uses provided getFilesFromEvent()", async () => {
const data = createDtWithFiles(files);
const props = {
getFilesFromEvent: jest
.fn()
.mockImplementation((event) => fromEvent(event)),
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
};
const { container } = render(
<Dropzone {...props}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
await act(() => fireEvent.dragEnter(dropzone, data));
expect(props.onDragEnter).toHaveBeenCalled();
fireEvent.dragOver(dropzone, data);
expect(props.onDragOver).toHaveBeenCalled();
fireEvent.dragLeave(dropzone, data);
expect(props.onDragLeave).toHaveBeenCalled();
await act(() => fireEvent.drop(dropzone, data));
expect(props.onDrop).toHaveBeenCalled();
expect(props.getFilesFromEvent).toHaveBeenCalledTimes(2);
});
it("calls {onError} when getFilesFromEvent() rejects", async () => {
const data = createDtWithFiles(files);
const props = {
getFilesFromEvent: jest
.fn()
.mockImplementation(() => Promise.reject("oops :(")),
onDragEnter: jest.fn(),
onDrop: jest.fn(),
onError: jest.fn(),
};
const ui = (
<Dropzone {...props}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const { container } = render(ui);
const dropzone = container.querySelector("div");
await act(() => fireEvent.dragEnter(dropzone, data));
expect(props.onDragEnter).not.toHaveBeenCalled();
await act(() => fireEvent.drop(dropzone, data));
expect(props.onDrop).not.toHaveBeenCalled();
expect(props.getFilesFromEvent).toHaveBeenCalledTimes(2);
expect(props.onError).toHaveBeenCalledTimes(2);
});
});
describe("onFocus", () => {
it("sets focus state", () => {
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
});
it("does not set focus state if user stopped event propagation", () => {
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div
{...getRootProps({ onFocus: (event) => event.stopPropagation() })}
>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).toBeNull();
});
it("does not set focus state if {noKeyboard} is true", () => {
const { container } = render(
<Dropzone noKeyboard>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).toBeNull();
});
it("restores focus behavior if {noKeyboard} is set back to false", () => {
const { container, rerender } = render(
<Dropzone noKeyboard>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).toBeNull();
rerender(
<Dropzone noKeyboard={false}>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
});
it("{autoFocus} sets the focus state on render", () => {
const { container, rerender } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
expect(dropzone.querySelector("#focus")).toBeNull();
rerender(
/* eslint-disable-next-line jsx-a11y/no-autofocus */
<Dropzone autoFocus>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
expect(dropzone.querySelector("#focus")).not.toBeNull();
rerender(
/* eslint-disable-next-line jsx-a11y/no-autofocus */
<Dropzone autoFocus disabled>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
expect(dropzone.querySelector("#focus")).toBeNull();
});
});
describe("onBlur", () => {
it("unsets focus state", () => {
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
fireEvent.blur(dropzone);
expect(dropzone.querySelector("#focus")).toBeNull();
});
it("does not unset focus state if user stopped event propagation", () => {
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div
{...getRootProps({ onBlur: (event) => event.stopPropagation() })}
>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
fireEvent.blur(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
});
it("does not unset focus state if {noKeyboard} is true", () => {
const { container, rerender } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
rerender(
<Dropzone noKeyboard>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
fireEvent.blur(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
});
it("restores blur behavior if {noKeyboard} is set back to false", () => {
const { container, rerender } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.focus(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
rerender(
<Dropzone noKeyboard>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
fireEvent.blur(dropzone);
expect(dropzone.querySelector("#focus")).not.toBeNull();
rerender(
<Dropzone noKeyboard={false}>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
);
fireEvent.blur(dropzone);
expect(dropzone.querySelector("#focus")).toBeNull();
});
});
describe("onClick", () => {
let currentShowOpenFilePicker;
beforeEach(() => {
currentShowOpenFilePicker = window.showOpenFilePicker;
});
afterEach(() => {
if (currentShowOpenFilePicker) {
window.showOpenFilePicker = currentShowOpenFilePicker;
} else {
delete window.showOpenFilePicker;
}
});
it("should proxy the click event to the input", () => {
const activeRef = createRef();
const active = <span ref={activeRef}>I am active</span>;
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps, isFileDialogActive }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFileDialogActive && active}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(activeRef.current).not.toBeNull();
expect(dropzone).toContainElement(activeRef.current);
expect(onClickSpy).toHaveBeenCalled();
});
it("should not not proxy the click event to the input if event propagation was stopped", () => {
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div
{...getRootProps({ onClick: (event) => event.stopPropagation() })}
>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
fireEvent.click(container.querySelector("div"));
expect(onClickSpy).not.toHaveBeenCalled();
});
it("should not not proxy the click event to the input if {noClick} is true", () => {
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const { container } = render(
<Dropzone noClick>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
fireEvent.click(container.querySelector("div"));
expect(onClickSpy).not.toHaveBeenCalled();
});
it("restores click behavior if {noClick} is set back to false", () => {
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const { container, rerender } = render(
<Dropzone noClick>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(onClickSpy).not.toHaveBeenCalled();
rerender(
<Dropzone noClick={false}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
fireEvent.click(dropzone);
expect(onClickSpy).toHaveBeenCalled();
});
// https://github.com/react-dropzone/react-dropzone/issues/783
it("should continue event propagation if {noClick} is true", () => {
const btnClickSpy = jest.fn();
const inputClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const { container } = render(
<Dropzone noClick>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
<button onClick={btnClickSpy} />
</div>
)}
</Dropzone>
);
fireEvent.click(container.querySelector("div"));
expect(inputClickSpy).not.toHaveBeenCalled();
fireEvent.click(container.querySelector("button"));
expect(btnClickSpy).toHaveBeenCalled();
});
it("should schedule input click on next tick in Edge", () => {
jest.useFakeTimers();
const isIeOrEdgeSpy = jest
.spyOn(utils, "isIeOrEdge")
.mockReturnValueOnce(true);
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const { container } = render(
<Dropzone>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
fireEvent.click(container.querySelector("div"));
drainPendingTimers();
expect(onClickSpy).toHaveBeenCalled();
jest.useRealTimers();
isIeOrEdgeSpy.mockClear();
});
it("should not use showOpenFilePicker() if supported and {useFsAccessApi} is not true", () => {
jest.useFakeTimers();
const activeRef = createRef();
const active = <span ref={activeRef}>I am active</span>;
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const showOpenFilePickerMock = jest.fn();
window.showOpenFilePicker = showOpenFilePickerMock;
const onDropSpy = jest.fn();
const onFileDialogOpenSpy = jest.fn();
const { container } = render(
<Dropzone
onDrop={onDropSpy}
onFileDialogOpen={onFileDialogOpenSpy}
accept={{
"application/pdf": [],
}}
multiple
useFsAccessApi={false}
>
{({ getRootProps, getInputProps, isFileDialogActive }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFileDialogActive && active}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(showOpenFilePickerMock).not.toHaveBeenCalled();
expect(onClickSpy).toHaveBeenCalled();
expect(onFileDialogOpenSpy).toHaveBeenCalled();
expect(activeRef.current).not.toBeNull();
expect(dropzone).toContainElement(activeRef.current);
focusWindow();
drainPendingTimers();
expect(activeRef.current).toBeNull();
expect(dropzone).not.toContainElement(activeRef.current);
expect(onDropSpy).not.toHaveBeenCalled();
jest.useRealTimers();
});
it("should not use showOpenFilePicker() if supported and {isSecureContext} is not true", () => {
jest.useFakeTimers();
const activeRef = createRef();
const active = <span ref={activeRef}>I am active</span>;
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const showOpenFilePickerMock = jest.fn();
window.showOpenFilePicker = showOpenFilePickerMock;
window.isSecureContext = false;
const onDropSpy = jest.fn();
const onFileDialogOpenSpy = jest.fn();
const { container } = render(
<Dropzone
onDrop={onDropSpy}
onFileDialogOpen={onFileDialogOpenSpy}
accept={{
"application/pdf": [],
}}
multiple
>
{({ getRootProps, getInputProps, isFileDialogActive }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFileDialogActive && active}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(showOpenFilePickerMock).not.toHaveBeenCalled();
expect(onClickSpy).toHaveBeenCalled();
expect(onFileDialogOpenSpy).toHaveBeenCalled();
expect(activeRef.current).not.toBeNull();
expect(dropzone).toContainElement(activeRef.current);
focusWindow();
drainPendingTimers();
expect(activeRef.current).toBeNull();
expect(dropzone).not.toContainElement(activeRef.current);
expect(onDropSpy).not.toHaveBeenCalled();
jest.useRealTimers();
window.isSecureContext = true;
});
it("should use showOpenFilePicker() if supported and {useFsAccessApi} is true, and not trigger click on input", async () => {
const activeRef = createRef();
const active = <span ref={activeRef}>I am active</span>;
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, "click");
const handlers = files.map((f) => createFileSystemFileHandle(f));
const thenable = createThenable();
const showOpenFilePickerMock = jest
.fn()
.mockReturnValue(thenable.promise);
window.showOpenFilePicker = showOpenFilePickerMock;
const onDropSpy = jest.fn();
const onFileDialogOpenSpy = jest.fn();
const { container } = render(
<Dropzone
onDrop={onDropSpy}
onFileDialogOpen={onFileDialogOpenSpy}
accept={{
"application/pdf": [],
}}
multiple
useFsAccessApi
>
{({ getRootProps, getInputProps, isFileDialogActive }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFileDialogActive && active}
</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(showOpenFilePickerMock).toHaveBeenCalledWith({
multiple: true,
types: [
{
description: "Files",
accept: { "application/pdf": [] },
},
],
});
expect(onClickSpy).not.toHaveBeenCalled();
expect(onFileDialogOpenSpy).toHaveBeenCalled();
expect(activeRef.current).not.toBeNull();
expect(dropzone).toContainElement(activeRef.current);
await act(() => thenable.done(handlers));
expect(activeRef.current).toBeNull();
expect(dropzone).not.toContainElement(activeRef.current);
expect(onDropSpy).toHaveBeenCalledWith(files, [], null);
});
test("if showOpenFilePicker() is supported and {useFsAccessApi} is true, it should work without the <input>", async () => {
const activeRef = createRef();
const active = <span ref={activeRef}>I am active</span>;
const handlers = files.map((f) => createFileSystemFileHandle(f));
const thenable = createThenable();
const showOpenFilePickerMock = jest
.fn()
.mockReturnValue(thenable.promise);
window.showOpenFilePicker = showOpenFilePickerMock;
const onDropSpy = jest.fn();
const onFileDialogOpenSpy = jest.fn();
const { container } = render(
<Dropzone
onDrop={onDropSpy}
onFileDialogOpen={onFileDialogOpenSpy}
useFsAccessApi
>
{({ getRootProps, isFileDialogActive }) => (
<div {...getRootProps()}>{isFileDialogActive && active}</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(showOpenFilePickerMock).toHaveBeenCalled();
expect(onFileDialogOpenSpy).toHaveBeenCalled();
await act(() => thenable.done(handlers));
expect(activeRef.current).toBeNull();
expect(dropzone).not.toContainElement(activeRef.current);
expect(onDropSpy).toHaveBeenCalledWith(files, [], null);
});
test("if showOpenFilePicker() is supported and {useFsAccessApi} is true, and the user cancels it should call onFileDialogCancel", async () => {
const activeRef = createRef();
const active = <span ref={activeRef}>I am active</span>;
const thenable = createThenable();
const showOpenFilePickerMock = jest
.fn()
.mockReturnValue(thenable.promise);
window.showOpenFilePicker = showOpenFilePickerMock;
const onDropSpy = jest.fn();
const onFileDialogCancelSpy = jest.fn();
const { container } = render(
<Dropzone
onDrop={onDropSpy}
onFileDialogCancel={onFileDialogCancelSpy}
useFsAccessApi
>
{({ getRootProps, isFileDialogActive }) => (
<div {...getRootProps()}>{isFileDialogActive && active}</div>
)}
</Dropzone>
);
const dropzone = container.querySelector("div");
fireEvent.click(dropzone);
expect(showOpenFilePickerMock).toHaveBeenCalled();
await act(() =>
thenable.cancel(new DOMException("user aborted request", "AbortError"))
);
expect(activeRef.current).toBeNull();
expect(dropzone).not.toContainElement(activeRef.current);
expect