Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add message if type/paste function is called with an invalid element #375

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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