UNPKG

react-dropzone

Version:
1,572 lines (1,320 loc) 60.1 kB
/* eslint react/prop-types: 0 */ import React, { createRef } from 'react' import { cleanup, fireEvent, render } from 'react-testing-library' import { renderHook } from 'react-hooks-testing-library' 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> ) const input = container.querySelector('input') expect(input).toHaveAttribute('accept', accept) }) 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> ) const input = container.querySelector('input') expect(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> ) const input = container.querySelector('input') expect(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> ) const dropzone = container.querySelector('div') expect(dropzone).toHaveAttribute('aria-label', ariaLabel) }) it('runs the custom callback handlers provided to the root props getter', () => { 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 ui = ( <Dropzone> {({ getRootProps, getInputProps }) => ( <div {...getRootProps(rootProps)}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) 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() fireEvent.dragEnter(dropzone, event) expect(rootProps.onDragEnter).toHaveBeenCalled() fireEvent.dragOver(dropzone, event) expect(rootProps.onDragOver).toHaveBeenCalled() fireEvent.dragLeave(dropzone, event) expect(rootProps.onDragLeave).toHaveBeenCalled() fireEvent.drop(dropzone, event) expect(rootProps.onDrop).toHaveBeenCalled() }) it('runs the custom callback handlers provided to the input props getter', () => { const inputProps = { onClick: jest.fn(), onChange: jest.fn() } const ui = ( <Dropzone> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps(inputProps)} /> </div> )} </Dropzone> ) const { container } = render(ui) const input = container.querySelector('input') fireEvent.click(input) expect(inputProps.onClick).toHaveBeenCalled() fireEvent.change(input) expect(inputProps.onChange).toHaveBeenCalled() }) it('runs no callback handlers if {disabled} is true', () => { 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() 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() fireEvent.drop(dropzone, event) expect(rootProps.onDrop).not.toHaveBeenCalled() const input = container.querySelector('input') fireEvent.click(input) expect(inputProps.onClick).not.toHaveBeenCalled() 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) }) 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 -1 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')).toHaveAttribute('tabindex', '-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).toHaveBeenCalledTimes(1) const dropEvt = new Event('drop', { bubbles: true }) const dropEvtPreventDefaultSpy = jest.spyOn(dropEvt, 'preventDefault') fireEvent(document.body, dropEvt) expect(dropEvtPreventDefaultSpy).toHaveBeenCalledTimes(1) }) it('permits drags and drops on elements inside our dropzone', () => { const { container } = render( <Dropzone> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const dropzone = container.querySelector('div') const dropEvt = new Event('drop', { bubbles: true }) const dropEvtPreventDefaultSpy = jest.spyOn(dropEvt, 'preventDefault') fireEvent(dropzone, dropEvt) // A call is from the onDrop handler for the dropzone, // but there should be no more than 1 expect(dropEvtPreventDefaultSpy).toHaveBeenCalledTimes(1) }) 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).not.toHaveBeenCalledTimes(1) }) }) 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 ui = ( <Dropzone {...parentProps}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> <InnerDropzone /> </div> )} </Dropzone> ) const { container } = render(ui) const innerDropzone = container.querySelector('#inner-dropzone') fireDragEnter(innerDropzone, data) await flushPromises(ui, container) expect(innerProps.onDragEnter).toHaveBeenCalled() expect(parentProps.onDragEnter).toHaveBeenCalled() fireDragOver(innerDropzone, data) expect(innerProps.onDragOver).toHaveBeenCalled() expect(parentProps.onDragOver).toHaveBeenCalled() fireDragLeave(innerDropzone, data) expect(innerProps.onDragLeave).toHaveBeenCalled() expect(parentProps.onDragLeave).toHaveBeenCalled() fireDrop(innerDropzone, data) await flushPromises(ui, container) 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 ui = ( <Dropzone {...parentProps}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> <InnerDropzone /> </div> )} </Dropzone> ) const { container } = render(ui) const innerDropzone = container.querySelector('#inner-dropzone') fireDragEnter(innerDropzone, data) await flushPromises(ui, container) expect(innerProps.onDragEnter).toHaveBeenCalled() expect(parentProps.onDragEnter).not.toHaveBeenCalled() fireDragOver(innerDropzone, data) expect(innerProps.onDragOver).toHaveBeenCalled() expect(parentProps.onDragOver).not.toHaveBeenCalled() fireDragLeave(innerDropzone, data) expect(innerProps.onDragLeave).toHaveBeenCalled() expect(parentProps.onDragLeave).not.toHaveBeenCalled() fireDrop(innerDropzone, data) await flushPromises(ui, container) 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 ui = ( <Dropzone onDragLeave={parentDragLeave}> {({ getRootProps, getInputProps }) => ( <div id="parent-dropzone" {...getRootProps()}> <input {...getInputProps()} /> <InnerDropzone /> </div> )} </Dropzone> ) const { container } = render(ui) const parentDropzone = container.querySelector('#parent-dropzone') fireDragEnter(parentDropzone, data) await flushPromises(ui, container) const innerDropzone = container.querySelector('#inner-dropzone') fireDragEnter(innerDropzone, data) await flushPromises(ui, container) fireDragLeave(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 ui = ( <Dropzone {...props}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(props.onDragEnter).toHaveBeenCalled() fireDragOver(dropzone, data) expect(props.onDragOver).toHaveBeenCalled() fireDragLeave(dropzone, data) expect(props.onDragLeave).toHaveBeenCalled() fireDrop(dropzone, data) await flushPromises(ui, container) expect(props.onDrop).toHaveBeenCalled() expect(props.getFilesFromEvent).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() }) }) 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() }) }) describe('onClick', () => { 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) const ref = activeRef.current expect(ref).not.toBeNull() expect(dropzone).toContainElement(ref) 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> ) const dropzone = container.querySelector('div') fireEvent.click(dropzone) expect(onClickSpy).not.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> ) const dropzone = container.querySelector('div') fireEvent.click(dropzone) jest.runAllTimers() expect(onClickSpy).toHaveBeenCalled() jest.useRealTimers() isIeOrEdgeSpy.mockClear() }) }) describe('onKeyDown', () => { it('triggers the click event on the input if the SPACE/ENTER keys are pressed', () => { 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.keyDown(dropzone, { keyCode: 32 }) fireEvent.keyDown(dropzone, { keyCode: 13 }) const ref = activeRef.current expect(ref).not.toBeNull() expect(dropzone).toContainElement(ref) expect(onClickSpy).toHaveBeenCalledTimes(2) }) it('does not trigger the click event on the input if the dropzone is not in focus', () => { const onClickSpy = jest.spyOn(HTMLInputElement.prototype, 'click') const { container } = render( <Dropzone> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const input = container.querySelector('input') fireEvent.keyDown(input, { keyCode: 32 }) expect(onClickSpy).not.toHaveBeenCalled() }) it('does not trigger the click event on the input if event propagation was stopped', () => { const onClickSpy = jest.spyOn(HTMLInputElement.prototype, 'click') const { container } = render( <Dropzone> {({ getRootProps, getInputProps }) => ( <div {...getRootProps({ onKeyDown: event => event.stopPropagation() })}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const dropzone = container.querySelector('div') fireEvent.keyDown(dropzone, { keyCode: 32 }) expect(onClickSpy).not.toHaveBeenCalled() }) it('does not trigger the click event on the input for other keys', () => { const onClickSpy = jest.spyOn(HTMLInputElement.prototype, 'click') const { container } = render( <Dropzone> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const dropzone = container.querySelector('div') fireEvent.keyDown(dropzone, { keyCode: 97 }) expect(onClickSpy).not.toHaveBeenCalled() }) }) describe('onDrag*', () => { it('invokes callbacks for the appropriate events', async () => { const data = createDtWithFiles(files) const props = { onDragEnter: jest.fn(), onDragOver: jest.fn(), onDragLeave: jest.fn(), onDrop: jest.fn() } const ui = ( <Dropzone {...props}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(props.onDragEnter).toHaveBeenCalled() fireDragOver(dropzone, data) expect(props.onDragOver).toHaveBeenCalled() fireDragLeave(dropzone, data) expect(props.onDragLeave).toHaveBeenCalled() fireDrop(dropzone, data) await flushPromises(ui, container) expect(props.onDrop).toHaveBeenCalled() }) it('invokes callbacks for the appropriate events even if it cannot access the data', async () => { const emptyData = createDtWithFiles([]) const props = { onDragEnter: jest.fn(), onDragOver: jest.fn(), onDragLeave: jest.fn(), onDrop: jest.fn(), onDropAccepted: jest.fn(), onDropRejected: jest.fn() } const ui = ( <Dropzone {...props}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDragEnter(dropzone, emptyData) await flushPromises(ui, container) expect(props.onDragEnter).toHaveBeenCalled() fireDragOver(dropzone, emptyData) expect(props.onDragOver).toHaveBeenCalled() fireDragLeave(dropzone, emptyData) expect(props.onDragLeave).toHaveBeenCalled() const data = createDtWithFiles(files) fireDrop(dropzone, data) await flushPromises(ui, container) expect(props.onDrop).toHaveBeenCalled() expect(props.onDropAccepted).toHaveBeenCalledWith(files, expect.any(Object)) expect(props.onDropRejected).not.toHaveBeenCalled() }) it('does not invoke callbacks if no files are detected', async () => { const data = { dataTransfer: { items: [], types: ['text/html', 'text/plain'] } } const props = { onDragEnter: jest.fn(), onDragOver: jest.fn(), onDragLeave: jest.fn(), onDrop: jest.fn(), onDropAccepted: jest.fn(), onDropRejected: jest.fn() } const ui = ( <Dropzone {...props}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(props.onDragEnter).not.toHaveBeenCalled() fireDragOver(dropzone, data) expect(props.onDragOver).not.toHaveBeenCalled() fireDragLeave(dropzone, data) expect(props.onDragLeave).not.toHaveBeenCalled() fireDrop(dropzone, data) await flushPromises(ui, container) expect(props.onDrop).not.toHaveBeenCalled() expect(props.onDropAccepted).not.toHaveBeenCalled() expect(props.onDropRejected).not.toHaveBeenCalled() }) it('sets {isDragActive} and {isDragAccept} if some files are accepted on dragented', async () => { const ui = ( <Dropzone> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const data = createDtWithFiles(files) fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(dropzone).toHaveTextContent('dragActive') expect(dropzone).toHaveTextContent('dragAccept') expect(dropzone).not.toHaveTextContent('dragReject') }) it('sets {isDragActive} and {isDragReject} of some files are not accepted on dragenter', async () => { const ui = ( <Dropzone accept="image/*"> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const data = createDtWithFiles([...files, ...images]) fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(dropzone).toHaveTextContent('dragActive') expect(dropzone).not.toHaveTextContent('dragAccept') expect(dropzone).toHaveTextContent('dragReject') }) it('sets {isDragActive, isDragAccept, isDragReject} if all files are accepted and {multiple} is false on dragenter', async () => { const ui = ( <Dropzone accept="image/*" multiple={false}> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const data = createDtWithFiles(images) fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(dropzone).toHaveTextContent('dragActive') expect(dropzone).toHaveTextContent('dragAccept') expect(dropzone).toHaveTextContent('dragReject') }) it('keeps {isDragActive} if dragleave is triggered for some arbitrary node', async () => { const { container: overlayContainer } = render(<div />) const ui = ( <Dropzone> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const data = createDtWithFiles(files) fireDragEnter(dropzone, data) await flushPromises(ui, container) const event = new Event('dragleave', { bubbles: true }) Object.defineProperty(event, 'target', { value: overlayContainer.querySelector('div'), writable: false }) fireEvent(dropzone, event) expect(dropzone).toHaveTextContent('dragActive') }) it('updates {isDragActive} if {accept} changes mid-drag', async () => { const ui = ( <Dropzone accept="image/*"> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} </div> )} </Dropzone> ) const { container, rerender } = render(ui) const dropzone = container.querySelector('div') const data = createDtWithFiles(images) fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(dropzone).toHaveTextContent('dragActive') expect(dropzone).toHaveTextContent('dragAccept') expect(dropzone).not.toHaveTextContent('dragReject') rerender( <Dropzone accept="text/*"> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} </div> )} </Dropzone> ) expect(dropzone).toHaveTextContent('dragActive') expect(dropzone).not.toHaveTextContent('dragAccept') expect(dropzone).toHaveTextContent('dragReject') }) it('resets {isDragActive, isDragAccept, isDragReject} on dragleave', async () => { const ui = ( <Dropzone accept="image/*"> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} {!isDragActive && ( <span id="child" data-accept={isDragAccept} data-reject={isDragReject} /> )} </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const data = createDtWithFiles(images) fireDragEnter(container.querySelector('#child'), data) fireDragEnter(dropzone, data) await flushPromises(ui, container) fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(dropzone).toHaveTextContent('dragActive') expect(dropzone).toHaveTextContent('dragAccept') expect(dropzone).not.toHaveTextContent('dragReject') fireDragLeave(dropzone, data) await flushPromises(ui, container) expect(dropzone).not.toHaveTextContent('dragActive') expect(dropzone).not.toHaveTextContent('dragAccept') expect(dropzone).not.toHaveTextContent('dragReject') const child = container.querySelector('#child') expect(child).toHaveAttribute('data-accept', 'false') expect(child).toHaveAttribute('data-reject', 'false') }) }) describe('onDrop', () => { test('callback is invoked when <input> change event occurs', async () => { const onDropSpy = jest.fn() const ui = ( <Dropzone onDrop={onDropSpy}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const input = container.querySelector('input') Object.defineProperty(input, 'files', { value: files }) dispatchEvt(input, 'change') await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith(files, [], expect.anything()) }) it('sets {acceptedFiles, rejectedFiles}', async () => { const FileList = (props = { files: [] }) => ( <ul> {props.files.map(file => ( <li key={file.name} data-type={props.type}> {file.name} </li> ))} </ul> ) const getAcceptedFiles = node => node.querySelectorAll(`[data-type="accepted"]`) const getRejectedFiles = node => node.querySelectorAll(`[data-type="rejected"]`) const matchToFiles = (fileList, files) => Array.from(fileList).every(item => !!files.find(file => file.name === item.textContent)) const ui = ( <Dropzone accept="image/*"> {({ getRootProps, getInputProps, acceptedFiles, rejectedFiles }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> <FileList files={acceptedFiles} type="accepted" /> <FileList files={rejectedFiles} type="rejected" /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDrop(dropzone, createDtWithFiles(images)) await flushPromises(ui, container) const acceptedFileList = getAcceptedFiles(dropzone) expect(acceptedFileList).toHaveLength(images.length) expect(matchToFiles(acceptedFileList, images)).toBe(true) fireDrop(dropzone, createDtWithFiles(files)) await flushPromises(ui, container) const rejectedFileList = getRejectedFiles(dropzone) expect(rejectedFileList).toHaveLength(files.length) expect(matchToFiles(rejectedFileList, files)).toBe(true) }) it('resets {isDragActive, isDragAccept, isDragReject}', async () => { const ui = ( <Dropzone> {({ getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isDragActive && 'dragActive'} {isDragAccept && 'dragAccept'} {isDragReject && 'dragReject'} </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const data = createDtWithFiles(files) fireDragEnter(dropzone, data) await flushPromises(ui, container) expect(dropzone).toHaveTextContent('dragActive') expect(dropzone).toHaveTextContent('dragAccept') expect(dropzone).not.toHaveTextContent('dragReject') fireDrop(dropzone, data) await flushPromises(ui, container) expect(dropzone).not.toHaveTextContent('dragActive') expect(dropzone).not.toHaveTextContent('dragAccept') expect(dropzone).not.toHaveTextContent('dragReject') }) it('rejects all files if {multiple} is false and {accept} criteria is not met', async () => { const onDropSpy = jest.fn() const ui = ( <Dropzone accept="image/*" onDrop={onDropSpy} multiple={false}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDrop(dropzone, createDtWithFiles(files)) await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith([], files, expect.anything()) }) it('rejects all files if {multiple} is false and {accept} criteria is met', async () => { const onDropSpy = jest.fn() const ui = ( <Dropzone accept="image/*" onDrop={onDropSpy} multiple={false}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDrop(dropzone, createDtWithFiles(images)) await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith([], images, expect.anything()) }) it('accepts a single files if {multiple} is false and {accept} criteria is met', async () => { const onDropSpy = jest.fn() const ui = ( <Dropzone accept="image/*" onDrop={onDropSpy} multiple={false}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const [image] = images fireDrop(dropzone, createDtWithFiles([image])) await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith([image], [], expect.anything()) }) it('accepts all files if {multiple} is true', async () => { const onDropSpy = jest.fn() const ui = ( <Dropzone onDrop={onDropSpy}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDrop(dropzone, createDtWithFiles(files)) await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith(files, [], expect.anything()) }) it('resets {isFileDialogActive} state', async () => { const onDropSpy = jest.fn() const activeRef = createRef() const active = <span ref={activeRef}>I am active</span> const ui = ( <Dropzone onDrop={onDropSpy}> {({ getRootProps, getInputProps, isFileDialogActive, open }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> {isFileDialogActive && active} <button type="button" onClick={open}> Open </button> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') const btn = container.querySelector('button') btn.click() const ref = activeRef.current expect(dropzone).toContainElement(ref) fireDrop(dropzone, createDtWithFiles(files)) await flushPromises(ui, container) expect(dropzone).not.toContainElement(ref) }) it('gets invoked with both accepted/rejected files', async () => { const onDropSpy = jest.fn() const ui = ( <Dropzone accept="image/*" onDrop={onDropSpy}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDrop(dropzone, createDtWithFiles(files)) await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith([], files, expect.anything()) onDropSpy.mockClear() fireDrop(dropzone, createDtWithFiles(images)) await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith(images, [], expect.anything()) onDropSpy.mockClear() fireDrop(dropzone, createDtWithFiles([...files, ...images])) await flushPromises(ui, container) expect(onDropSpy).toHaveBeenCalledWith(images, files, expect.anything()) }) test('onDropAccepted callback is invoked if some files are accepted', async () => { const onDropAcceptedSpy = jest.fn() const ui = ( <Dropzone accept="image/*" onDropAccepted={onDropAcceptedSpy}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDrop(dropzone, createDtWithFiles(files)) await flushPromises(ui, container) expect(onDropAcceptedSpy).not.toHaveBeenCalled() onDropAcceptedSpy.mockClear() fireDrop(dropzone, createDtWithFiles(images)) await flushPromises(ui, container) expect(onDropAcceptedSpy).toHaveBeenCalledWith(images, expect.anything()) onDropAcceptedSpy.mockClear() fireDrop(dropzone, createDtWithFiles([...files, ...images])) await flushPromises(ui, container) expect(onDropAcceptedSpy).toHaveBeenCalledWith(images, expect.anything()) }) test('onDropRejected callback is invoked if some files are rejected', async () => { const onDropRejectedSpy = jest.fn() const ui = ( <Dropzone accept="image/*" onDropRejected={onDropRejectedSpy}> {({ getRootProps, getInputProps }) => ( <div {...getRootProps()}> <input {...getInputProps()} /> </div> )} </Dropzone> ) const { container } = render(ui) const dropzone = container.querySelector('div') fireDrop(dropzone, createDtWithFiles(files)) await flushPromises(ui, container) expect(onDropRejectedSpy).toHaveBeenCalledWith(files, expect.anything()) onDropRejectedSpy.mockClear() fireDrop(dropzone, createDtWithFiles(images)) await flushPromises(ui, container) expect(onDropRejectedSpy).not.toHaveBeenCalled() onDropRejectedSpy.mockClear() fireDrop(dropzone, c