diff --git a/README.md b/README.md index c723777c..d6d552f9 100644 --- a/README.md +++ b/README.md @@ -271,12 +271,15 @@ test('types into the input', () => { }) ``` -### `upload(element, file, [{ clickInit, changeInit }])` +### `upload(element, file, [{ clickInit, changeInit }], [options])` Uploads file to an ``. For uploading multiple files use `` 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' diff --git a/src/__tests__/upload.js b/src/__tests__/upload.js index f09a6ae2..d1cbbba7 100644 --- a/src/__tests__/upload.js +++ b/src/__tests__/upload.js @@ -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(` + + `) + + userEvent.upload(element, files, undefined, {applyAccept}) + + expect(element.files).toHaveLength(expectedLength) + }, +) + +test('should not trigger input event for empty list', () => { + const {element, eventWasFired} = setup('') + userEvent.upload(element, []) + + expect(element.files).toHaveLength(0) + expect(eventWasFired('input')).toBe(false) +}) diff --git a/src/upload.js b/src/upload.js index a252b438..db7eda9d 100644 --- a/src/upload.js +++ b/src/upload.js @@ -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... @@ -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} diff --git a/typings/index.d.ts b/typings/index.d.ts index 56211bfc..a603a026 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -26,6 +26,10 @@ export interface IClickOptions { clickCount?: number } +export interface IUploadOptions { + applyAccept?: boolean +} + declare const userEvent: { clear: (element: TargetElement) => void click: ( @@ -52,6 +56,7 @@ declare const userEvent: { element: TargetElement, files: FilesArgument, init?: UploadInitArgument, + options?: IUploadOptions, ) => void type: ( element: TargetElement,