react-dropzone
Version:
Simple HTML5 drag-drop zone with React.js
1,671 lines (1,381 loc) • 83.6 kB
JavaScript
/* eslint react/prop-types: 0, jsx-a11y/label-has-for: 0 */
import React, { createRef } from 'react'
import { 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>
)
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)
})
test('<Dropzone> exposes and sets the ref if using a ref object', () => {
const dropzoneRef = createRef()
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, 'click')
const { rerender } = render(
<Dropzone ref={dropzoneRef}>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
)
expect(dropzoneRef.current).not.toBeNull()
expect(typeof dropzoneRef.current.open).toEqual('function')
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 { rerender } = render(
<Dropzone ref={setRef}>
{({ getRootProps, getInputProps, isFocused }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isFocused && <div id="focus" />}
</div>
)}
</Dropzone>
)
expect(dropzoneRef).not.toBeNull()
expect(typeof dropzoneRef.open).toEqual('function')
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 } = render(ui)
const dropzone = container.querySelector('#dropzone')
const fn = async () => {
fireDrop(dropzone, data)
await flushPromises(ui, container)
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')
const event = new Event('click', { bubbles: true, cancelable: true })
fireEvent(dropzone, event)
const ref = activeRef.current
expect(ref).not.toBeNull()
expect(dropzone).toContainElement(ref)
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 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).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(1) // TODO: Figure out who calls the preventDefault()
})
})
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('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 ui = (
<Dropzone {...parentProps}>
{({ getRootProps, getInputProps }) => (
<div id="outer-dropzone" {...getRootProps()}>
<input {...getInputProps()} />
<InnerDropzone />
</div>
)}
</Dropzone>
)
const { container } = render(ui)
const outerDropzone = container.querySelector('#outer-dropzone')
const innerDropzone = container.querySelector('#inner-dropzone')
// Sets drag targets on the outer dropzone
fireDragEnter(outerDropzone, data)
await flushPromises(ui, container)
fireDragEnter(innerDropzone, data)
await flushPromises(ui, container)
expect(innerProps.onDragEnter).toHaveBeenCalled()
expect(parentProps.onDragEnter).toHaveBeenCalledTimes(1)
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()
})
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()
})
})
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', () => {
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 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>
)
const dropzone = container.querySelector('div')
fireEvent.click(dropzone)
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>
)
const dropzone = container.querySelector('div')
const btn = container.querySelector('button')
fireEvent.click(dropzone)
expect(inputClickSpy).not.toHaveBeenCalled()
fireEvent.click(btn)
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>
)
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 if {noKeyboard} is true', () => {
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, 'click')
const { container } = render(
<Dropzone noKeyboard>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
)
const dropzone = container.querySelector('div')
fireEvent.keyDown(dropzone, {
keyCode: 32
})
expect(onClickSpy).not.toHaveBeenCalled()
})
it('restores the keydown behavior when {noKeyboard} is set back to false', () => {
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, 'click')
const { container, rerender } = render(
<Dropzone noKeyboard>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
)
const dropzone = container.querySelector('div')
fireEvent.keyDown(dropzone, {
keyCode: 32
})
expect(onClickSpy).not.toHaveBeenCalled()
rerender(
<Dropzone noKeyboard={false}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
)
fireEvent.keyDown(dropzone, {
keyCode: 32
})
expect(onClickSpy).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('does not invoke callbacks if {noDrag} is true', async () => {
const data = createDtWithFiles(files)
const props = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn(),
onDropAccepted: jest.fn(),
onDropRejected: jest.fn()
}
const ui = (
<Dropzone {...props} noDrag>
{({ 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('restores drag behavior if {noDrag} is set back to false', async () => {
const data = createDtWithFiles(files)
const props = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn()
}
const noDragUi = (
<Dropzone {...props} noDrag>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
)
const { container, rerender } = render(noDragUi)
const dropzone = container.querySelector('div')
fireDragEnter(dropzone, data)
await flushPromises(noDragUi, 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(noDragUi, container)
expect(props.onDrop).not.toHaveBeenCalled()
const ui = (
<Dropzone {...props} noDrag={false}>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
)
rerender(ui)
fireDragEnter(dropzone, data)
await flushPromises(ui, container)
expect(props.onDragEnter).toHaveBeenCalled()
fireDragOver(dropzone, data)
expect(props.onDragOver).toHaveBeenCalled()
fireDragLeave(dropzone,