Skip to content

Commit

Permalink
feat(upload): accept filter option (#562)
Browse files Browse the repository at this point in the history
* fix(upload): apply accept attribute (#558)

* fix(upload): make accept filter opt-in

* docs(upload): add options.applyAccept

* fix(typing): add options.applyAccept

Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
  • Loading branch information
ph-fritsche and LauraBeatris committed Feb 28, 2021
1 parent 5a4b1b7 commit ede2df1
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 6 deletions.
5 changes: 4 additions & 1 deletion README.md
Expand Up @@ -271,12 +271,15 @@ test('types into the input', () => {
})
```
### `upload(element, file, [{ clickInit, changeInit }])`
### `upload(element, file, [{ clickInit, changeInit }], [options])`
Uploads file to an `<input>`. For uploading multiple files use `<input>` with
`multiple` attribute and the second `upload` argument must be array then. Also
it's possible to initialize click or change event with using third argument.

If `options.applyAccept` is set to `true` and there is an `accept` attribute on
the element, files that don't match will be discarded.
```jsx
import React from 'react'
import {render, screen} from '@testing-library/react'
Expand Down
36 changes: 36 additions & 0 deletions src/__tests__/upload.js
Expand Up @@ -163,3 +163,39 @@ test('should call onChange/input bubbling up the event when a file is selected',
expect(onInputInput).toHaveBeenCalledTimes(1)
expect(onInputForm).toHaveBeenCalledTimes(1)
})

test.each([
[true, 'video/*,audio/*', 2],
[true, '.png', 1],
[true, 'text/csv', 1],
[true, '', 4],
[false, 'video/*', 4],
])(
'should filter according to accept attribute applyAccept=%s, acceptAttribute=%s',
(applyAccept, acceptAttribute, expectedLength) => {
const files = [
new File(['hello'], 'hello.png', {type: 'image/png'}),
new File(['there'], 'there.jpg', {type: 'audio/mp3'}),
new File(['there'], 'there.csv', {type: 'text/csv'}),
new File(['there'], 'there.jpg', {type: 'video/mp4'}),
]
const {element} = setup(`
<input
type="file"
accept="${acceptAttribute}" multiple
/>
`)

userEvent.upload(element, files, undefined, {applyAccept})

expect(element.files).toHaveLength(expectedLength)
},
)

test('should not trigger input event for empty list', () => {
const {element, eventWasFired} = setup('<input type="file"/>')
userEvent.upload(element, [])

expect(element.files).toHaveLength(0)
expect(eventWasFired('input')).toBe(false)
})
32 changes: 27 additions & 5 deletions src/upload.js
Expand Up @@ -3,23 +3,27 @@ import {click} from './click'
import {blur} from './blur'
import {focus} from './focus'

function upload(element, fileOrFiles, init) {
function upload(element, fileOrFiles, init, {applyAccept = false} = {}) {
if (element.disabled) return

click(element, init)

const input = element.tagName === 'LABEL' ? element.control : element

const files = (Array.isArray(fileOrFiles)
? fileOrFiles
: [fileOrFiles]
).slice(0, input.multiple ? undefined : 1)
const files = (Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles])
.filter(file => !applyAccept || isAcceptableFile(file, element.accept))
.slice(0, input.multiple ? undefined : 1)

// blur fires when the file selector pops up
blur(element, init)
// focus fires when they make their selection
focus(element, init)

// treat empty array as if the user just closed the file upload dialog
if (files.length === 0) {
return
}

// the event fired in the browser isn't actually an "input" or "change" event
// but a new Event with a type set to "input" and "change"
// Kinda odd...
Expand All @@ -46,4 +50,22 @@ function upload(element, fileOrFiles, init) {
})
}

function isAcceptableFile(file, accept) {
if (!accept) {
return true
}

const wildcards = ['audio/*', 'image/*', 'video/*']

return accept.split(',').some(acceptToken => {
if (acceptToken[0] === '.') {
// tokens starting with a dot represent a file extension
return file.name.endsWith(acceptToken)
} else if (wildcards.includes(acceptToken)) {
return file.type.startsWith(acceptToken.substr(0, acceptToken.length - 1))
}
return file.type === acceptToken
})
}

export {upload}
5 changes: 5 additions & 0 deletions typings/index.d.ts
Expand Up @@ -26,6 +26,10 @@ export interface IClickOptions {
clickCount?: number
}

export interface IUploadOptions {
applyAccept?: boolean
}

declare const userEvent: {
clear: (element: TargetElement) => void
click: (
Expand All @@ -52,6 +56,7 @@ declare const userEvent: {
element: TargetElement,
files: FilesArgument,
init?: UploadInitArgument,
options?: IUploadOptions,
) => void
type: <T extends ITypeOpts>(
element: TargetElement,
Expand Down

0 comments on commit ede2df1

Please sign in to comment.