UNPKG

react-dropzone-async-validation

Version:
1,639 lines (1,351 loc) 108 kB
/* 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