Skip to content

Commit ede2df1

Browse files
feat(upload): accept filter option (#562)
* 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 <[email protected]>
1 parent 5a4b1b7 commit ede2df1

File tree

4 files changed

+72
-6
lines changed

4 files changed

+72
-6
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,15 @@ test('types into the input', () => {
271271
})
272272
```
273273
274-
### `upload(element, file, [{ clickInit, changeInit }])`
274+
### `upload(element, file, [{ clickInit, changeInit }], [options])`
275275
276276
Uploads file to an `<input>`. For uploading multiple files use `<input>` with
277277
`multiple` attribute and the second `upload` argument must be array then. Also
278278
it's possible to initialize click or change event with using third argument.
279279

280+
If `options.applyAccept` is set to `true` and there is an `accept` attribute on
281+
the element, files that don't match will be discarded.
282+
280283
```jsx
281284
import React from 'react'
282285
import {render, screen} from '@testing-library/react'

src/__tests__/upload.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,39 @@ test('should call onChange/input bubbling up the event when a file is selected',
163163
expect(onInputInput).toHaveBeenCalledTimes(1)
164164
expect(onInputForm).toHaveBeenCalledTimes(1)
165165
})
166+
167+
test.each([
168+
[true, 'video/*,audio/*', 2],
169+
[true, '.png', 1],
170+
[true, 'text/csv', 1],
171+
[true, '', 4],
172+
[false, 'video/*', 4],
173+
])(
174+
'should filter according to accept attribute applyAccept=%s, acceptAttribute=%s',
175+
(applyAccept, acceptAttribute, expectedLength) => {
176+
const files = [
177+
new File(['hello'], 'hello.png', {type: 'image/png'}),
178+
new File(['there'], 'there.jpg', {type: 'audio/mp3'}),
179+
new File(['there'], 'there.csv', {type: 'text/csv'}),
180+
new File(['there'], 'there.jpg', {type: 'video/mp4'}),
181+
]
182+
const {element} = setup(`
183+
<input
184+
type="file"
185+
accept="${acceptAttribute}" multiple
186+
/>
187+
`)
188+
189+
userEvent.upload(element, files, undefined, {applyAccept})
190+
191+
expect(element.files).toHaveLength(expectedLength)
192+
},
193+
)
194+
195+
test('should not trigger input event for empty list', () => {
196+
const {element, eventWasFired} = setup('<input type="file"/>')
197+
userEvent.upload(element, [])
198+
199+
expect(element.files).toHaveLength(0)
200+
expect(eventWasFired('input')).toBe(false)
201+
})

src/upload.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ import {click} from './click'
33
import {blur} from './blur'
44
import {focus} from './focus'
55

6-
function upload(element, fileOrFiles, init) {
6+
function upload(element, fileOrFiles, init, {applyAccept = false} = {}) {
77
if (element.disabled) return
88

99
click(element, init)
1010

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

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

1817
// blur fires when the file selector pops up
1918
blur(element, init)
2019
// focus fires when they make their selection
2120
focus(element, init)
2221

22+
// treat empty array as if the user just closed the file upload dialog
23+
if (files.length === 0) {
24+
return
25+
}
26+
2327
// the event fired in the browser isn't actually an "input" or "change" event
2428
// but a new Event with a type set to "input" and "change"
2529
// Kinda odd...
@@ -46,4 +50,22 @@ function upload(element, fileOrFiles, init) {
4650
})
4751
}
4852

53+
function isAcceptableFile(file, accept) {
54+
if (!accept) {
55+
return true
56+
}
57+
58+
const wildcards = ['audio/*', 'image/*', 'video/*']
59+
60+
return accept.split(',').some(acceptToken => {
61+
if (acceptToken[0] === '.') {
62+
// tokens starting with a dot represent a file extension
63+
return file.name.endsWith(acceptToken)
64+
} else if (wildcards.includes(acceptToken)) {
65+
return file.type.startsWith(acceptToken.substr(0, acceptToken.length - 1))
66+
}
67+
return file.type === acceptToken
68+
})
69+
}
70+
4971
export {upload}

typings/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export interface IClickOptions {
2626
clickCount?: number
2727
}
2828

29+
export interface IUploadOptions {
30+
applyAccept?: boolean
31+
}
32+
2933
declare const userEvent: {
3034
clear: (element: TargetElement) => void
3135
click: (
@@ -52,6 +56,7 @@ declare const userEvent: {
5256
element: TargetElement,
5357
files: FilesArgument,
5458
init?: UploadInitArgument,
59+
options?: IUploadOptions,
5560
) => void
5661
type: <T extends ITypeOpts>(
5762
element: TargetElement,

0 commit comments

Comments
 (0)