diff --git a/src/__tests__/type-modifiers.js b/src/__tests__/type-modifiers.js
index f398ea99..be1b3dee 100644
--- a/src/__tests__/type-modifiers.js
+++ b/src/__tests__/type-modifiers.js
@@ -117,6 +117,14 @@ test('{backspace} deletes the selected range', async () => {
`)
})
+test('{backspace} on an input type that does not support selection ranges', async () => {
+ const {element} = setup()
+ // note: you cannot even call setSelectionRange on these kinds of elements...
+ await userEvent.type(element, '{backspace}')
+ // removed "m"
+ expect(element).toHaveValue('yo@example.co')
+})
+
test('{alt}a{/alt}', async () => {
const {element: input, getEventCalls} = setup()
diff --git a/src/__tests__/type.js b/src/__tests__/type.js
index 8735dfee..8b57efe5 100644
--- a/src/__tests__/type.js
+++ b/src/__tests__/type.js
@@ -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()
+
+ 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()
+ 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()
+ const email = 'yo@example.com'
+ await userEvent.type(element, email)
+ expect(element).toHaveValue(email)
+})
diff --git a/src/type.js b/src/type.js
index 1281bf19..6f014cea 100644
--- a/src/type.js
+++ b/src/type.js
@@ -26,7 +26,12 @@ 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()
@@ -34,24 +39,46 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
// 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 = {
@@ -116,7 +143,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
inputType: 'insertLineBreak',
...eventOverrides,
})
- setSelectionRange(newSelectionStart)
+ setSelectionRange({newValue, newSelectionStart})
}
await tick()
@@ -222,7 +249,7 @@ async function typeImpl(element, text, {allAtOnce = false, delay} = {}) {
...eventOverrides,
})
- setSelectionRange(newSelectionStart)
+ setSelectionRange({newValue, newSelectionStart})
}
}
@@ -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
@@ -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
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 735d4f84..4d7151cb 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -1,7 +1,9 @@
// Definitions by: Wu Haotian
-export interface IUserOptions {
+export interface ITypeOpts {
allAtOnce?: boolean
delay?: number
+ initialSelectionStart?: number
+ initialSelectionEnd?: number
}
export interface ITabUserOptions {
@@ -40,7 +42,7 @@ declare const userEvent: {
type: (
element: TargetElement,
text: string,
- userOpts?: IUserOptions,
+ userOpts?: ITypeOpts,
) => Promise
tab: (userOpts?: ITabUserOptions) => void
}