diff --git a/src/__tests__/paste.js b/src/__tests__/paste.js index e1665727..7427c165 100644 --- a/src/__tests__/paste.js +++ b/src/__tests__/paste.js @@ -100,9 +100,9 @@ test('should replace selected text all at once', () => { test('should give error if we are trying to call paste on an invalid element', () => { const {element} = setup('
') - expect(() => - userEvent.paste(element, "I'm only a div :("), - ).toThrowErrorMatchingInlineSnapshot( - `"the current element is of type DIV and doesn't have a valid value"`, - ) + expect(() => userEvent.paste(element, "I'm only a div :(")) + .toThrowErrorMatchingInlineSnapshot(` + "The given DIV element is currently unsupported. + A PR extending this implementation would be very much welcome at https://github.com/testing-library/user-event" + `) }) diff --git a/src/__tests__/upload.js b/src/__tests__/upload.js index 39cea8ca..f57c116b 100644 --- a/src/__tests__/upload.js +++ b/src/__tests__/upload.js @@ -180,9 +180,9 @@ test.each([ new File(['there'], 'there.jpg', {type: 'video/mp4'}), ] const {element} = setup(` - `) @@ -235,3 +235,19 @@ test('input.files implements iterable', () => { expect(Array.from(eventTargetFiles)).toEqual(files) }) + +test('throw error if trying to use upload on an invalid element', () => { + const {elements} = setup('
') + + expect(() => + userEvent.upload(elements[0], "I'm only a div :("), + ).toThrowErrorMatchingInlineSnapshot( + `"The given DIV element does not accept file uploads"`, + ) + + expect(() => + userEvent.upload(elements[1], "I'm a checkbox :("), + ).toThrowErrorMatchingInlineSnapshot( + `"The associated INPUT element does not accept file uploads"`, + ) +}) diff --git a/src/paste.ts b/src/paste.ts index 4bd4ae4e..2945662f 100644 --- a/src/paste.ts +++ b/src/paste.ts @@ -5,6 +5,8 @@ import { calculateNewValue, eventWrapper, isDisabled, + isElementType, + editableInputTypes, } from './utils' interface pasteOptions { @@ -12,22 +14,36 @@ interface pasteOptions { initialSelectionEnd?: number } +function isSupportedElement( + element: HTMLElement, +): element is + | HTMLTextAreaElement + | (HTMLInputElement & {type: editableInputTypes}) { + return ( + (isElementType(element, 'input') && + Boolean(editableInputTypes[element.type as editableInputTypes])) || + isElementType(element, 'textarea') + ) +} + function paste( - element: HTMLInputElement | HTMLTextAreaElement, + element: HTMLElement, text: string, init?: ClipboardEventInit, {initialSelectionStart, initialSelectionEnd}: pasteOptions = {}, ) { - if (isDisabled(element)) { - return - } - // TODO: implement for contenteditable - if (typeof element.value === 'undefined') { + if (!isSupportedElement(element)) { throw new TypeError( - `the current element is of type ${element.tagName} and doesn't have a valid value`, + `The given ${element.tagName} element is currently unsupported. + A PR extending this implementation would be very much welcome at https://github.com/testing-library/user-event`, ) } + + if (isDisabled(element)) { + return + } + eventWrapper(() => element.focus()) // by default, a new element has it's selection start and end at 0 diff --git a/src/upload.ts b/src/upload.ts index 8615b379..04a79bb7 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -14,19 +14,24 @@ interface uploadOptions { } function upload( - element: HTMLInputElement | HTMLLabelElement, + element: HTMLElement, fileOrFiles: File | File[], init?: uploadInit, {applyAccept = false}: uploadOptions = {}, ) { + const input = isElementType(element, 'label') ? element.control : element + + if (!input || !isElementType(input, 'input', {type: 'file'})) { + throw new TypeError( + `The ${input === element ? 'given' : 'associated'} ${ + input?.tagName + } element does not accept file uploads`, + ) + } if (isDisabled(element)) return click(element, init?.clickInit) - const input = isElementType(element, 'label') - ? (element.control as HTMLInputElement) - : element - const files = (Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles]) .filter(file => !applyAccept || isAcceptableFile(file, input.accept)) .slice(0, input.multiple ? undefined : 1) diff --git a/src/utils/edit/isEditable.ts b/src/utils/edit/isEditable.ts index ac53db06..08733e6b 100644 --- a/src/utils/edit/isEditable.ts +++ b/src/utils/edit/isEditable.ts @@ -1,42 +1,45 @@ -import { isElementType } from "../misc/isElementType"; -import { isContentEditable } from './isContentEditable' +import {isElementType} from '../misc/isElementType' +import {isContentEditable} from './isContentEditable' // eslint-disable-next-line @typescript-eslint/no-explicit-any -type GuardedType = T extends (x: any) => x is (infer R) ? R : never +type GuardedType = T extends (x: any) => x is infer R ? R : never export function isEditable( - element: Element + element: Element, ): element is - GuardedType - | GuardedType - | HTMLTextAreaElement & {readOnly: false} -{ - return isEditableInput(element) - || isElementType(element, 'textarea', {readOnly: false}) - || isContentEditable(element) + | GuardedType + | GuardedType + | (HTMLTextAreaElement & {readOnly: false}) { + return ( + isEditableInput(element) || + isElementType(element, 'textarea', {readOnly: false}) || + isContentEditable(element) + ) } -enum editableInputTypes { - 'text' = 'text', - 'date' = 'date', - 'datetime-local' = 'datetime-local', - 'email' = 'email', - 'month' = 'month', - 'number' = 'number', - 'password' = 'password', - 'search' = 'search', - 'tel' = 'tel', - 'time' = 'time', - 'url' = 'url', - 'week' = 'week', +export enum editableInputTypes { + 'text' = 'text', + 'date' = 'date', + 'datetime-local' = 'datetime-local', + 'email' = 'email', + 'month' = 'month', + 'number' = 'number', + 'password' = 'password', + 'search' = 'search', + 'tel' = 'tel', + 'time' = 'time', + 'url' = 'url', + 'week' = 'week', } export function isEditableInput( - element: Element + element: Element, ): element is HTMLInputElement & { - readOnly: false, - type: editableInputTypes + readOnly: false + type: editableInputTypes } { - return isElementType(element, 'input', {readOnly: false}) - && Boolean(editableInputTypes[element.type as editableInputTypes]) + return ( + isElementType(element, 'input', {readOnly: false}) && + Boolean(editableInputTypes[element.type as editableInputTypes]) + ) }