Skip to content

Commit

Permalink
fix: add message if type/paste function is called with an invalid ele…
Browse files Browse the repository at this point in the history
…ment (#375)

* test: add failing test

* fix: add an explicit error on type function

If the user tries to call type function with an invalid element, an explicit error will be thrown.
This error should be better then 'TypeError: Cannot read property 'length' of undefined'

* refactor: replace error with typerror

* test: add fail test for paste

* fix: throw TypeError for paste and type functions
  • Loading branch information
marcosvega91 committed Jun 21, 2020
1 parent 2e55359 commit 0dd387e
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -552,6 +552,7 @@ Thanks goes to these people ([emoji key][emojis]):

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors][all-contributors] specification.
Expand Down
9 changes: 9 additions & 0 deletions src/__tests__/paste.js
Expand Up @@ -94,3 +94,12 @@ test('should replace selected text all at once', () => {
userEvent.paste(element, 'friend')
expect(element).toHaveValue('hello friend')
})

test('should give error if we are trying to call paste on an invalid element', () => {
const {element} = setup('<div />')
expect(() =>
userEvent.paste(element, "I'm only a div :("),
).toThrowErrorMatchingInlineSnapshot(
`"the current element is of type DIV and doesn't have a valid value"`,
)
})
9 changes: 9 additions & 0 deletions src/__tests__/type.js
Expand Up @@ -706,3 +706,12 @@ test('typing an invalid input value', () => {
// but the badInput should actually be "true" if the user types "3-3"
expect(element.validity.badInput).toBe(false)
})

test('should give error if we are trying to call type on an invalid element', async () => {
const {element} = setup('<div />')
await expect(() =>
userEvent.type(element, "I'm only a div :("),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"the current element is of type BODY and doesn't have a valid value"`,
)
})
12 changes: 10 additions & 2 deletions src/paste.js
Expand Up @@ -8,7 +8,11 @@ function paste(
{initialSelectionStart, initialSelectionEnd} = {},
) {
if (element.disabled) return

if (typeof element.value === 'undefined') {
throw new TypeError(
`the current element is of type ${element.tagName} and doesn't have a valid value`,
)
}
element.focus()

// by default, a new element has it's selection start and end at 0
Expand All @@ -30,7 +34,11 @@ function paste(
fireEvent.paste(element, init)

if (!element.readOnly) {
const {newValue, newSelectionStart} = calculateNewValue(text, element)
const {newValue, newSelectionStart} = calculateNewValue(
text,
element,
element.value,
)
fireEvent.input(element, {
inputType: 'insertFromPaste',
target: {value: newValue},
Expand Down
30 changes: 21 additions & 9 deletions src/type.js
Expand Up @@ -53,7 +53,16 @@ async function typeImpl(
// and not just the element if the active element could change while the function
// is being run (for example, functions that are and/or fire events).
const currentElement = () => getActiveElement(element.ownerDocument)
const currentValue = () => currentElement().value
const currentValue = () => {
const activeElement = currentElement()
const value = activeElement.value
if (typeof value === 'undefined') {
throw new TypeError(
`the current element is of type ${activeElement.tagName} and doesn't have a valid value`,
)
}
return value
}
const setSelectionRange = ({newValue, newSelectionStart}) => {
// if we *can* change the selection start, then we will if the new value
// is the same as the current value (so it wasn't programatically changed
Expand Down Expand Up @@ -98,6 +107,7 @@ async function typeImpl(

const eventCallbackMap = getEventCallbackMap({
currentElement,
currentValue,
fireInputEventIfNeeded,
setSelectionRange,
})
Expand Down Expand Up @@ -212,7 +222,7 @@ async function typeImpl(
}

const inputEvent = fireInputEventIfNeeded({
...calculateNewValue(newEntry, currentElement()),
...calculateNewValue(newEntry, currentElement(), currentValue()),
eventOverrides: {
data: key,
inputType: 'insertText',
Expand Down Expand Up @@ -262,8 +272,8 @@ async function typeImpl(
// and you may be tempted to create a shared abstraction.
// If you, brave soul, decide to so endevor, please increment this count
// when you inevitably fail: 1
function calculateNewBackspaceValue(element) {
const {selectionStart, selectionEnd, value} = element
function calculateNewBackspaceValue(element, value) {
const {selectionStart, selectionEnd} = element
let newValue, newSelectionStart

if (selectionStart === null) {
Expand Down Expand Up @@ -295,8 +305,8 @@ function calculateNewBackspaceValue(element) {
return {newValue, newSelectionStart}
}

function calculateNewDeleteValue(element) {
const {selectionStart, selectionEnd, value} = element
function calculateNewDeleteValue(element, value) {
const {selectionStart, selectionEnd} = element
let newValue

if (selectionStart === null) {
Expand Down Expand Up @@ -325,6 +335,7 @@ function calculateNewDeleteValue(element) {

function getEventCallbackMap({
currentElement,
currentValue,
fireInputEventIfNeeded,
setSelectionRange,
}) {
Expand Down Expand Up @@ -383,6 +394,7 @@ function getEventCallbackMap({
const {newValue, newSelectionStart} = calculateNewValue(
'\n',
currentElement(),
currentValue(),
)
fireEvent.input(currentElement(), {
target: {value: newValue},
Expand Down Expand Up @@ -432,7 +444,7 @@ function getEventCallbackMap({

if (keyPressDefaultNotPrevented) {
fireInputEventIfNeeded({
...calculateNewDeleteValue(currentElement()),
...calculateNewDeleteValue(currentElement(), currentValue()),
eventOverrides: {
inputType: 'deleteContentForward',
...eventOverrides,
Expand Down Expand Up @@ -460,7 +472,7 @@ function getEventCallbackMap({

if (keyPressDefaultNotPrevented) {
fireInputEventIfNeeded({
...calculateNewBackspaceValue(currentElement()),
...calculateNewBackspaceValue(currentElement(), currentValue()),
eventOverrides: {
inputType: 'deleteContentBackward',
...eventOverrides,
Expand All @@ -478,7 +490,7 @@ function getEventCallbackMap({
// the user can actually select in several different ways
// we're not going to choose, so we'll *only* set the selection range
'{selectall}': () => {
currentElement().setSelectionRange(0, currentElement().value.length)
currentElement().setSelectionRange(0, currentValue().length)
},
}

Expand Down
1 change: 1 addition & 0 deletions src/utils.js
Expand Up @@ -94,6 +94,7 @@ function getActiveElement(document) {

function calculateNewValue(newEntry, element) {
const {selectionStart, selectionEnd, value} = element

// can't use .maxLength property because of a jsdom bug:
// https://github.com/jsdom/jsdom/issues/2927
const maxLength = Number(element.getAttribute('maxlength') ?? -1)
Expand Down

0 comments on commit 0dd387e

Please sign in to comment.