Skip to content

Commit

Permalink
fix(type): ensure the selectionStart/End are consistent with browsers
Browse files Browse the repository at this point in the history
Closes: #321
Closes: #318
Closes: #316
  • Loading branch information
kentcdodds committed Jun 8, 2020
1 parent b62dc68 commit 0b6ffb8
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 14 deletions.
50 changes: 50 additions & 0 deletions src/__tests__/type.js
Expand Up @@ -521,3 +521,53 @@ test('ignored {backspace} in controlled input', async () => {
keyup: 4 (52)
`)
})

// https://github.com/testing-library/user-event/issues/321
test('typing in a textarea with existing text', async () => {
const {element, getEventCalls} = setup(<textarea defaultValue="Hello, " />)

await userEvent.type(element, '12')
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: 1 (49)
keypress: 1 (49)
input: "Hello, {CURSOR}" -> "Hello, 1"
keyup: 1 (49)
keydown: 2 (50)
keypress: 2 (50)
input: "Hello, 1{CURSOR}" -> "Hello, 12"
keyup: 2 (50)
`)
expect(element).toHaveValue('Hello, 12')
})

// https://github.com/testing-library/user-event/issues/321
test('accepts an initialSelectionStart and initialSelectionEnd', async () => {
const {element, getEventCalls} = setup(<textarea defaultValue="Hello, " />)
element.setSelectionRange(0, 0)

await userEvent.type(element, '12', {
initialSelectionStart: element.selectionStart,
initialSelectionEnd: element.selectionEnd,
})
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: 1 (49)
keypress: 1 (49)
input: "{CURSOR}Hello, " -> "1Hello, "
keyup: 1 (49)
keydown: 2 (50)
keypress: 2 (50)
input: "1{CURSOR}Hello, " -> "12Hello, "
keyup: 2 (50)
`)
expect(element).toHaveValue('12Hello, ')
})

// https://github.com/testing-library/user-event/issues/316#issuecomment-640199908
test('can type into an input with type `email`', async () => {
const {element} = setup(<input type="email" />)
const email = 'yo@example.com'
await userEvent.type(element, email)
expect(element).toHaveValue(email)
})
64 changes: 50 additions & 14 deletions src/type.js
Expand Up @@ -26,32 +26,59 @@ const getActiveElement = document => {
}
}

async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
// eslint-disable-next-line complexity
async function typeImpl(
element,
text,
{allAtOnce = false, delay, initialSelectionStart, initialSelectionEnd} = {},
) {
if (element.disabled) return

element.focus()

// The focused element could change between each event, so get the currently active element each time
const currentElement = () => getActiveElement(element.ownerDocument)
const currentValue = () => currentElement().value
const setSelectionRange = newSelectionStart => {
// if the actual selection start is different from the one we expected
// then we set it to the end of the input
if (currentElement().selectionStart !== newSelectionStart) {
currentElement().setSelectionRange?.(
currentValue().length,
currentValue().length,
)
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
// when the fireEvent.input was triggered).
// The reason we have to do this at all is because it actually *is*
// programmatically changed by fireEvent.input, so we have to simulate the
// browser's default behavior
if (
currentElement().selectionStart !== null &&
currentValue() === newValue
) {
currentElement().setSelectionRange?.(newSelectionStart, newSelectionStart)
}
}

// by default, a new element has it's selection start and end at 0
// but most of the time when people call "type", they expect it to type
// at the end of the current input value. So, if the selection start
// and end are both the default of 0, then we'll go ahead and change
// them to the length of the current value.
// the only time it would make sense to pass the initialSelectionStart or
// initialSelectionEnd is if you have an input with a value and want to
// explicitely start typing with the cursor at 0. Not super common.
if (
currentElement().selectionStart === 0 &&
currentElement().selectionEnd === 0
) {
currentElement().setSelectionRange(
initialSelectionStart ?? currentValue()?.length ?? 0,
initialSelectionEnd ?? currentValue()?.length ?? 0,
)
}

if (allAtOnce) {
if (!element.readOnly) {
const {newValue, newSelectionStart} = calculateNewValue(text)
fireEvent.input(element, {
target: {value: newValue},
})
setSelectionRange(newSelectionStart)
setSelectionRange({newValue, newSelectionStart})
}
} else {
const eventCallbackMap = {
Expand Down Expand Up @@ -116,7 +143,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
inputType: 'insertLineBreak',
...eventOverrides,
})
setSelectionRange(newSelectionStart)
setSelectionRange({newValue, newSelectionStart})
}

await tick()
Expand Down Expand Up @@ -222,7 +249,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
...eventOverrides,
})

setSelectionRange(newSelectionStart)
setSelectionRange({newValue, newSelectionStart})
}
}

Expand All @@ -235,7 +262,12 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
const value = currentValue()
let newValue, newSelectionStart

if (selectionStart === selectionEnd) {
if (selectionStart === null) {
// at the end of an input type that does not support selection ranges
// https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
newValue = value.slice(0, value.length - 1)
newSelectionStart = selectionStart - 1
} else if (selectionStart === selectionEnd) {
if (selectionStart === 0) {
// at the beginning of the input
newValue = value
Expand Down Expand Up @@ -267,7 +299,11 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
const value = currentValue()
let newValue, newSelectionStart

if (selectionStart === selectionEnd) {
if (selectionStart === null) {
// at the end of an input type that does not support selection ranges
// https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
newValue = value + newEntry
} else if (selectionStart === selectionEnd) {
if (selectionStart === 0) {
// at the beginning of the input
newValue = newEntry + value
Expand Down

0 comments on commit 0b6ffb8

Please sign in to comment.