')
userEvent.click(element)
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: div
- mouseover: Left (0)
- mousemove: Left (0)
- mousedown: Left (0)
- mouseup: Left (0)
- click: Left (0)
+ div - pointerover
+ div - pointerenter
+ div - mouseover: Left (0)
+ div - mouseenter: Left (0)
+ div - pointermove
+ div - mousemove: Left (0)
+ div - pointerdown
+ div - mousedown: Left (0)
+ div - pointerup
+ div - mouseup: Left (0)
+ div - click: Left (0)
`)
})
test('toggles the focus', () => {
- const {element} = setup(`
`)
const a = element.children[0]
const b = element.children[1]
@@ -127,44 +167,75 @@ test('toggles the focus', () => {
})
test('should blur the previous element', () => {
- const {element} = setup(`
`,
+ )
const a = element.children[0]
const b = element.children[1]
- const {getEventCalls, clearEventCalls} = addListeners(a)
+ const aListeners = addListeners(a)
+ const bListeners = addListeners(b)
userEvent.click(a)
clearEventCalls()
userEvent.click(b)
- expect(getEventCalls()).toMatchInlineSnapshot(`
- Events fired on: input[value=""]
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: div
- mousemove: Left (0) (bubbled from input[value=""])
- mouseleave: Left (0)
- blur
+ input[name="b"][value=""] - pointerover
+ input[name="b"][value=""] - mouseover: Left (0)
+ input[name="b"][value=""] - pointermove
+ input[name="b"][value=""] - mousemove: Left (0)
+ input[name="b"][value=""] - pointerdown
+ input[name="b"][value=""] - mousedown: Left (0)
+ input[name="a"][value=""] - focusout
+ input[name="b"][value=""] - focusin
+ input[name="b"][value=""] - pointerup
+ input[name="b"][value=""] - mouseup: Left (0)
+ input[name="b"][value=""] - click: Left (0)
`)
+ // focus/blur events don't bubble (but the focusout/focusin do!)
+ // we just want to make sure the blur was fired on a
+ // and the focus was fired on b
+ expect(aListeners.eventWasFired('blur')).toBe(true)
+ expect(bListeners.eventWasFired('focus')).toBe(true)
})
test('should not blur the previous element when mousedown prevents default', () => {
- const {element} = setup(`
`,
+ )
const a = element.children[0]
const b = element.children[1]
- addEventListener(b, 'mousedown', e => e.preventDefault())
-
- const {getEventCalls, clearEventCalls} = addListeners(a)
+ const aListeners = addListeners(a)
+ const bListeners = addListeners(b, {
+ eventHandlers: {mouseDown: e => e.preventDefault()},
+ })
userEvent.click(a)
clearEventCalls()
userEvent.click(b)
- expect(getEventCalls()).toMatchInlineSnapshot(`
- Events fired on: input[value=""]
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: div
- mousemove: Left (0) (bubbled from input[value=""])
- mouseleave: Left (0)
+ input[name="b"][value=""] - pointerover
+ input[name="b"][value=""] - mouseover: Left (0)
+ input[name="b"][value=""] - pointermove
+ input[name="b"][value=""] - mousemove: Left (0)
+ input[name="b"][value=""] - pointerdown
+ input[name="b"][value=""] - mousedown: Left (0)
+ input[name="b"][value=""] - pointerup
+ input[name="b"][value=""] - mouseup: Left (0)
+ input[name="b"][value=""] - click: Left (0)
`)
+ // focus/blur events don't bubble (but the focusout do!)
+ // we just want to make sure the blur was fired on a
+ // and the focus was fired on b
+ expect(aListeners.eventWasFired('blur')).toBe(false)
+ expect(bListeners.eventWasFired('focus')).toBe(false)
})
test('does not lose focus when click updates focus', () => {
@@ -212,6 +283,19 @@ test('gives focus to the form control when clicking within a label', () => {
expect(input).toHaveFocus()
})
+test('fires no events when clicking a label with a nested control that is disabled', () => {
+ const {element, getEventSnapshot} = setup(`
`)
+ userEvent.click(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
+ `No events were fired on: label`,
+ )
+})
+
+test('does not crash if the label has no control', () => {
+ const {element} = setup(`
`)
+ userEvent.click(element)
+})
+
test('clicking a label checks the checkbox', () => {
const {element} = setup(`
+ `)
const [checkbox, radio, number] = document.querySelectorAll(
'[data-testid="element"]',
@@ -85,7 +167,7 @@ test('should respect tabindex, regardless of dom position', () => {
expect(radio).toHaveFocus()
})
-test('should respect dom order when tabindex are all the same', () => {
+test('should respect tab index order, then DOM order', () => {
setup(`
@@ -244,10 +326,10 @@ test('should support unstable sorting environments like node 10', () => {
expect.assertions(26)
- letters.split('').forEach(letter => {
+ for (const letter of letters.split('')) {
userEvent.tab()
expect(document.querySelector(`[data-testid="${letter}"]`)).toHaveFocus()
- })
+ }
})
test('should not focus disabled elements', () => {
diff --git a/src/__tests__/toggleselectoptions.js b/src/__tests__/toggleselectoptions.js
deleted file mode 100644
index c57a0dd7..00000000
--- a/src/__tests__/toggleselectoptions.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import userEvent from '..'
-import {addListeners, setupSelect, setup} from './helpers/utils'
-
-test('should fire the correct events for multiple select', () => {
- const {form, select, getEventCalls} = setupSelect({multiple: true})
-
- userEvent.toggleSelectOptions(select, '1')
-
- expect(getEventCalls()).toMatchInlineSnapshot(`
- Events fired on: select[name="select"][value=["1"]]
-
- mouseover: Left (0)
- mousemove: Left (0)
- mousedown: Left (0)
- focus
- mouseup: Left (0)
- click: Left (0)
- mouseover: Left (0) (bubbled from option[value="1"])
- mousemove: Left (0) (bubbled from option[value="1"])
- mousedown: Left (0) (bubbled from option[value="1"])
- mouseup: Left (0) (bubbled from option[value="1"])
- click: Left (0) (bubbled from option[value="1"])
- change
- `)
-
- expect(form).toHaveFormValues({select: ['1']})
-})
-
-test('should fire the correct events for multiple select when focus is in other element', () => {
- const {select} = setupSelect({multiple: true})
- const button = document.createElement('button')
- document.body.append(button)
-
- const {getEventCalls: getSelectEventCalls} = addListeners(select)
- const {getEventCalls: getButtonEventCalls} = addListeners(button)
-
- button.focus()
-
- userEvent.toggleSelectOptions(select, '1')
-
- expect(getButtonEventCalls()).toMatchInlineSnapshot(`
- Events fired on: button
-
- focus
- mousemove: Left (0)
- mouseleave: Left (0)
- blur
- `)
- expect(getSelectEventCalls()).toMatchInlineSnapshot(`
- Events fired on: select[name="select"][value=["1"]]
-
- mouseover: Left (0)
- mousemove: Left (0)
- mousedown: Left (0)
- focus
- mouseup: Left (0)
- click: Left (0)
- mouseover: Left (0) (bubbled from option[value="1"])
- mousemove: Left (0) (bubbled from option[value="1"])
- mousedown: Left (0) (bubbled from option[value="1"])
- mouseup: Left (0) (bubbled from option[value="1"])
- click: Left (0) (bubbled from option[value="1"])
- change
- `)
-})
-
-test('toggle options as expected', () => {
- const {element} = setup(`
-
- `)
-
- const select = element.querySelector('select')
-
- // select one
- userEvent.toggleSelectOptions(select, ['1'])
- expect(element).toHaveFormValues({select: ['1']})
-
- // unselect one and select two
- userEvent.toggleSelectOptions(select, ['1', '2'])
- expect(element).toHaveFormValues({select: ['2']})
-
- // // select one
- userEvent.toggleSelectOptions(select, ['1'])
- expect(element).toHaveFormValues({select: ['1', '2']})
-})
-
-it('throws error when provided element is not a multiple select', () => {
- const {element} = setup(`
`)
-
- expect(() => {
- userEvent.toggleSelectOptions(element)
- }).toThrowErrorMatchingInlineSnapshot(
- `Unable to toggleSelectOptions - please provide a select element with multiple=true`,
- )
-})
diff --git a/src/__tests__/type-modifiers.js b/src/__tests__/type-modifiers.js
index 1ccb12d4..09a7d404 100644
--- a/src/__tests__/type-modifiers.js
+++ b/src/__tests__/type-modifiers.js
@@ -1,4 +1,4 @@
-import userEvent from '..'
+import userEvent from '../'
import {setup} from './helpers/utils'
// Note, use the setup function at the bottom of the file...
@@ -15,303 +15,781 @@ import {setup} from './helpers/utils'
// This also means that '{shift}a' will fire an input event with the shiftKey,
// but will not capitalize "a".
-test('{esc} triggers typing the escape character', async () => {
- const {element, getEventCalls} = setup('
')
+test('{esc} triggers typing the escape character', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{esc}')
+ userEvent.type(element, '{esc}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]
- focus
- keydown: Escape (27)
- keyup: Escape (27)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Escape (27)
+ input[value=""] - keyup: Escape (27)
`)
})
-test('a{backspace}', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, 'a{backspace}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+test('a{backspace}', () => {
+ const {element, getEventSnapshot} = setup('
')
+ userEvent.type(element, 'a{backspace}')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]
- focus
- keydown: a (97)
- keypress: a (97)
- input: "{CURSOR}" -> "a"
- keyup: a (97)
- keydown: Backspace (8)
- input: "a{CURSOR}" -> ""
- keyup: Backspace (8)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: a (97)
+ input[value=""] - keypress: a (97)
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97)
+ input[value="a"] - keydown: Backspace (8)
+ input[value=""] - input
+ "a{CURSOR}" -> "{CURSOR}"
+ input[value=""] - keyup: Backspace (8)
`)
})
-test('{backspace}a', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, '{backspace}a')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+test('{backspace}a', () => {
+ const {element, getEventSnapshot} = setup('
')
+ userEvent.type(element, '{backspace}a')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
- focus
- keydown: Backspace (8)
- keyup: Backspace (8)
- keydown: a (97)
- keypress: a (97)
- input: "{CURSOR}" -> "a"
- keyup: a (97)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Backspace (8)
+ input[value=""] - keyup: Backspace (8)
+ input[value=""] - keydown: a (97)
+ input[value=""] - keypress: a (97)
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97)
`)
})
-test('{backspace} triggers typing the backspace character and deletes the character behind the cursor', async () => {
- const {element, getEventCalls} = setup('
')
+test('{backspace} triggers typing the backspace character and deletes the character behind the cursor', () => {
+ const {element, getEventSnapshot} = setup('
')
element.setSelectionRange(1, 1)
- await userEvent.type(element, '{backspace}')
+ userEvent.type(element, '{backspace}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="o"]
- focus
- keydown: Backspace (8)
- input: "y{CURSOR}o" -> "o"
- keyup: Backspace (8)
+ input[value="yo"] - select
+ input[value="yo"] - pointerover
+ input[value="yo"] - pointerenter
+ input[value="yo"] - mouseover: Left (0)
+ input[value="yo"] - mouseenter: Left (0)
+ input[value="yo"] - pointermove
+ input[value="yo"] - mousemove: Left (0)
+ input[value="yo"] - pointerdown
+ input[value="yo"] - mousedown: Left (0)
+ input[value="yo"] - focus
+ input[value="yo"] - focusin
+ input[value="yo"] - pointerup
+ input[value="yo"] - mouseup: Left (0)
+ input[value="yo"] - click: Left (0)
+ input[value="yo"] - keydown: Backspace (8)
+ input[value="o"] - input
+ "y{CURSOR}o" -> "o{CURSOR}"
+ input[value="o"] - select
+ input[value="o"] - keyup: Backspace (8)
`)
})
-test('{backspace} on a readOnly input', async () => {
- const {element, getEventCalls} = setup('
')
+test('{backspace} on a readOnly input', () => {
+ const {element, getEventSnapshot} = setup('
')
element.setSelectionRange(1, 1)
- await userEvent.type(element, '{backspace}')
+ userEvent.type(element, '{backspace}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="yo"]
- focus
- keydown: Backspace (8)
- keyup: Backspace (8)
+ input[value="yo"] - select
+ input[value="yo"] - pointerover
+ input[value="yo"] - pointerenter
+ input[value="yo"] - mouseover: Left (0)
+ input[value="yo"] - mouseenter: Left (0)
+ input[value="yo"] - pointermove
+ input[value="yo"] - mousemove: Left (0)
+ input[value="yo"] - pointerdown
+ input[value="yo"] - mousedown: Left (0)
+ input[value="yo"] - focus
+ input[value="yo"] - focusin
+ input[value="yo"] - pointerup
+ input[value="yo"] - mouseup: Left (0)
+ input[value="yo"] - click: Left (0)
+ input[value="yo"] - keydown: Backspace (8)
+ input[value="yo"] - keyup: Backspace (8)
`)
})
-test('{backspace} does not fire input if keydown prevents default', async () => {
- const {element, getEventCalls} = setup('
', {
+test('{backspace} does not fire input if keydown prevents default', () => {
+ const {element, getEventSnapshot} = setup('
', {
eventHandlers: {keyDown: e => e.preventDefault()},
})
element.setSelectionRange(1, 1)
- await userEvent.type(element, '{backspace}')
+ userEvent.type(element, '{backspace}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="yo"]
- focus
- keydown: Backspace (8)
- keyup: Backspace (8)
+ input[value="yo"] - select
+ input[value="yo"] - pointerover
+ input[value="yo"] - pointerenter
+ input[value="yo"] - mouseover: Left (0)
+ input[value="yo"] - mouseenter: Left (0)
+ input[value="yo"] - pointermove
+ input[value="yo"] - mousemove: Left (0)
+ input[value="yo"] - pointerdown
+ input[value="yo"] - mousedown: Left (0)
+ input[value="yo"] - focus
+ input[value="yo"] - focusin
+ input[value="yo"] - pointerup
+ input[value="yo"] - mouseup: Left (0)
+ input[value="yo"] - click: Left (0)
+ input[value="yo"] - keydown: Backspace (8)
+ input[value="yo"] - keyup: Backspace (8)
`)
})
-test('{backspace} deletes the selected range', async () => {
- const {element, getEventCalls} = setup('
')
+test('{backspace} deletes the selected range', () => {
+ const {element, getEventSnapshot} = setup('
')
element.setSelectionRange(1, 5)
- await userEvent.type(element, '{backspace}')
+ userEvent.type(element, '{backspace}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="Here"]
- focus
- keydown: Backspace (8)
- input: "H{SELECTION}i th{/SELECTION}ere" -> "Here"
- keyup: Backspace (8)
+ input[value="Hi there"] - select
+ input[value="Hi there"] - pointerover
+ input[value="Hi there"] - pointerenter
+ input[value="Hi there"] - mouseover: Left (0)
+ input[value="Hi there"] - mouseenter: Left (0)
+ input[value="Hi there"] - pointermove
+ input[value="Hi there"] - mousemove: Left (0)
+ input[value="Hi there"] - pointerdown
+ input[value="Hi there"] - mousedown: Left (0)
+ input[value="Hi there"] - focus
+ input[value="Hi there"] - focusin
+ input[value="Hi there"] - pointerup
+ input[value="Hi there"] - mouseup: Left (0)
+ input[value="Hi there"] - click: Left (0)
+ input[value="Hi there"] - keydown: Backspace (8)
+ input[value="Here"] - input
+ "H{SELECTION}i th{/SELECTION}ere" -> "Here{CURSOR}"
+ input[value="Here"] - select
+ input[value="Here"] - keyup: Backspace (8)
`)
})
-test('{backspace} on an input type that does not support selection ranges', async () => {
+test('{backspace} on an input type that does not support selection ranges', () => {
const {element} = setup('
')
// note: you cannot even call setSelectionRange on these kinds of elements...
- await userEvent.type(element, '{backspace}{backspace}a')
+ userEvent.type(element, '{backspace}{backspace}a')
// removed "m" then "o" then add "a"
expect(element).toHaveValue('yo@example.ca')
})
-test('{alt}a{/alt}', async () => {
- const {element, getEventCalls} = setup('
')
+test('{alt}a{/alt}', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{alt}a{/alt}')
+ userEvent.type(element, '{alt}a{/alt}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
- focus
- keydown: Alt (18) {alt}
- keydown: a (97) {alt}
- keypress: a (97) {alt}
- input: "{CURSOR}" -> "a"
- keyup: a (97) {alt}
- keyup: Alt (18)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Alt (18) {alt}
+ input[value=""] - keydown: a (97) {alt}
+ input[value=""] - keypress: a (97) {alt}
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97) {alt}
+ input[value="a"] - keyup: Alt (18)
`)
})
-test('{meta}a{/meta}', async () => {
- const {element, getEventCalls} = setup('
')
+test('{meta}a{/meta}', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{meta}a{/meta}')
+ userEvent.type(element, '{meta}a{/meta}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
- focus
- keydown: Meta (93) {meta}
- keydown: a (97) {meta}
- keypress: a (97) {meta}
- input: "{CURSOR}" -> "a"
- keyup: a (97) {meta}
- keyup: Meta (93)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Meta (93) {meta}
+ input[value=""] - keydown: a (97) {meta}
+ input[value=""] - keypress: a (97) {meta}
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97) {meta}
+ input[value="a"] - keyup: Meta (93)
`)
})
-test('{ctrl}a{/ctrl}', async () => {
- const {element, getEventCalls} = setup('
')
+test('{ctrl}a{/ctrl}', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{ctrl}a{/ctrl}')
+ userEvent.type(element, '{ctrl}a{/ctrl}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
- focus
- keydown: Control (17) {ctrl}
- keydown: a (97) {ctrl}
- keypress: a (97) {ctrl}
- input: "{CURSOR}" -> "a"
- keyup: a (97) {ctrl}
- keyup: Control (17)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Control (17) {ctrl}
+ input[value=""] - keydown: a (97) {ctrl}
+ input[value=""] - keypress: a (97) {ctrl}
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97) {ctrl}
+ input[value="a"] - keyup: Control (17)
`)
})
-test('{shift}a{/shift}', async () => {
- const {element, getEventCalls} = setup('
')
+test('{shift}a{/shift}', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{shift}a{/shift}')
+ userEvent.type(element, '{shift}a{/shift}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
- focus
- keydown: Shift (16) {shift}
- keydown: a (97) {shift}
- keypress: a (97) {shift}
- input: "{CURSOR}" -> "a"
- keyup: a (97) {shift}
- keyup: Shift (16)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Shift (16) {shift}
+ input[value=""] - keydown: a (97) {shift}
+ input[value=""] - keypress: a (97) {shift}
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97) {shift}
+ input[value="a"] - keyup: Shift (16)
`)
})
-test('a{enter}', async () => {
- const {element, getEventCalls} = setup('
')
+test('a{enter}', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, 'a{enter}')
+ userEvent.type(element, 'a{enter}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
- focus
- keydown: a (97)
- keypress: a (97)
- input: "{CURSOR}" -> "a"
- keyup: a (97)
- keydown: Enter (13)
- keypress: Enter (13)
- keyup: Enter (13)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: a (97)
+ input[value=""] - keypress: a (97)
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97)
+ input[value="a"] - keydown: Enter (13)
+ input[value="a"] - keypress: Enter (13)
+ input[value="a"] - keyup: Enter (13)
`)
})
-test('{enter} with preventDefault keydown', async () => {
- const {element, getEventCalls} = setup('
', {
+test('{enter} with preventDefault keydown', () => {
+ const {element, getEventSnapshot} = setup('
', {
eventHandlers: {
keyDown: e => e.preventDefault(),
},
})
- await userEvent.type(element, '{enter}')
+ userEvent.type(element, '{enter}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]
- focus
- keydown: Enter (13)
- keyup: Enter (13)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Enter (13)
+ input[value=""] - keyup: Enter (13)
`)
})
-test('{enter} on a button', async () => {
- const {element, getEventCalls} = setup('
')
+test('{enter} on a button', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{enter}')
+ userEvent.type(element, '{enter}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: button
- focus
- keydown: Enter (13)
- keypress: Enter (13)
- click: Left (0)
- keyup: Enter (13)
+ button - pointerover
+ button - pointerenter
+ button - mouseover: Left (0)
+ button - mouseenter: Left (0)
+ button - pointermove
+ button - mousemove: Left (0)
+ button - pointerdown
+ button - mousedown: Left (0)
+ button - focus
+ button - focusin
+ button - pointerup
+ button - mouseup: Left (0)
+ button - click: Left (0)
+ button - keydown: Enter (13)
+ button - keypress: Enter (13)
+ button - click: Left (0)
+ button - keyup: Enter (13)
`)
})
-test('{enter} on a textarea', async () => {
- const {element, getEventCalls} = setup('
')
+test('{enter} on a textarea', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{enter}')
+ userEvent.type(element, '{enter}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: textarea[value="\\n"]
- focus
- keydown: Enter (13)
- keypress: Enter (13)
- input: "{CURSOR}" -> "
- "
- keyup: Enter (13)
+ textarea[value=""] - pointerover
+ textarea[value=""] - pointerenter
+ textarea[value=""] - mouseover: Left (0)
+ textarea[value=""] - mouseenter: Left (0)
+ textarea[value=""] - pointermove
+ textarea[value=""] - mousemove: Left (0)
+ textarea[value=""] - pointerdown
+ textarea[value=""] - mousedown: Left (0)
+ textarea[value=""] - focus
+ textarea[value=""] - focusin
+ textarea[value=""] - pointerup
+ textarea[value=""] - mouseup: Left (0)
+ textarea[value=""] - click: Left (0)
+ textarea[value=""] - keydown: Enter (13)
+ textarea[value=""] - keypress: Enter (13)
+ textarea[value="\\n"] - input
+ "{CURSOR}" -> "\\n{CURSOR}"
+ textarea[value="\\n"] - keyup: Enter (13)
`)
})
-test('{meta}{enter}{/meta} on a button', async () => {
- const {element, getEventCalls} = setup('
')
+test('{meta}{enter}{/meta} on a button', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{meta}{enter}{/meta}')
+ userEvent.type(element, '{meta}{enter}{/meta}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: button
- focus
- keydown: Meta (93) {meta}
- keydown: Enter (13) {meta}
- keypress: Enter (13) {meta}
- click: Left (0) {meta}
- keyup: Enter (13) {meta}
- keyup: Meta (93)
+ button - pointerover
+ button - pointerenter
+ button - mouseover: Left (0)
+ button - mouseenter: Left (0)
+ button - pointermove
+ button - mousemove: Left (0)
+ button - pointerdown
+ button - mousedown: Left (0)
+ button - focus
+ button - focusin
+ button - pointerup
+ button - mouseup: Left (0)
+ button - click: Left (0)
+ button - keydown: Meta (93) {meta}
+ button - keydown: Enter (13) {meta}
+ button - keypress: Enter (13) {meta}
+ button - click: Left (0) {meta}
+ button - keyup: Enter (13) {meta}
+ button - keyup: Meta (93)
`)
})
-test('{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}', async () => {
- const {element, getEventCalls} = setup('
')
+test('{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}')
+ userEvent.type(element, '{meta}{alt}{ctrl}a{/ctrl}{/alt}{/meta}')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
- focus
- keydown: Meta (93) {meta}
- keydown: Alt (18) {alt}{meta}
- keydown: Control (17) {alt}{meta}{ctrl}
- keydown: a (97) {alt}{meta}{ctrl}
- keypress: a (97) {alt}{meta}{ctrl}
- input: "{CURSOR}" -> "a"
- keyup: a (97) {alt}{meta}{ctrl}
- keyup: Control (17) {alt}{meta}
- keyup: Alt (18) {meta}
- keyup: Meta (93)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Meta (93) {meta}
+ input[value=""] - keydown: Alt (18) {alt}{meta}
+ input[value=""] - keydown: Control (17) {alt}{meta}{ctrl}
+ input[value=""] - keydown: a (97) {alt}{meta}{ctrl}
+ input[value=""] - keypress: a (97) {alt}{meta}{ctrl}
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97) {alt}{meta}{ctrl}
+ input[value="a"] - keyup: Control (17) {alt}{meta}
+ input[value="a"] - keyup: Alt (18) {meta}
+ input[value="a"] - keyup: Meta (93)
+ `)
+})
+
+test('{selectall} selects all the text', () => {
+ const value = 'abcdefg'
+ const {element, clearEventCalls, getEventSnapshot} = setup(
+ `
`,
+ )
+ element.setSelectionRange(2, 6)
+
+ clearEventCalls()
+
+ userEvent.type(element, '{selectall}')
+
+ expect(element.selectionStart).toBe(0)
+ expect(element.selectionEnd).toBe(value.length)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="abcdefg"]
+
+ input[value="abcdefg"] - pointerover
+ input[value="abcdefg"] - pointerenter
+ input[value="abcdefg"] - mouseover: Left (0)
+ input[value="abcdefg"] - mouseenter: Left (0)
+ input[value="abcdefg"] - pointermove
+ input[value="abcdefg"] - mousemove: Left (0)
+ input[value="abcdefg"] - pointerdown
+ input[value="abcdefg"] - mousedown: Left (0)
+ input[value="abcdefg"] - focus
+ input[value="abcdefg"] - focusin
+ input[value="abcdefg"] - pointerup
+ input[value="abcdefg"] - mouseup: Left (0)
+ input[value="abcdefg"] - click: Left (0)
+ input[value="abcdefg"] - select
+ `)
+})
+
+test('{del} at the start of the input', () => {
+ const {element, getEventSnapshot} = setup(`
`)
+
+ userEvent.type(element, '{del}', {
+ initialSelectionStart: 0,
+ initialSelectionEnd: 0,
+ })
+
+ expect(element.selectionStart).toBe(0)
+ expect(element.selectionEnd).toBe(0)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="ello"]
+
+ input[value="hello"] - pointerover
+ input[value="hello"] - pointerenter
+ input[value="hello"] - mouseover: Left (0)
+ input[value="hello"] - mouseenter: Left (0)
+ input[value="hello"] - pointermove
+ input[value="hello"] - mousemove: Left (0)
+ input[value="hello"] - pointerdown
+ input[value="hello"] - mousedown: Left (0)
+ input[value="hello"] - focus
+ input[value="hello"] - focusin
+ input[value="hello"] - pointerup
+ input[value="hello"] - mouseup: Left (0)
+ input[value="hello"] - click: Left (0)
+ input[value="hello"] - keydown: Delete (46)
+ input[value="ello"] - input
+ "{CURSOR}hello" -> "ello{CURSOR}"
+ input[value="ello"] - select
+ input[value="ello"] - keyup: Delete (46)
+ `)
+})
+
+test('{del} at end of the input', () => {
+ const {element, getEventSnapshot} = setup(`
`)
+
+ userEvent.type(element, '{del}')
+
+ expect(element.selectionStart).toBe(element.value.length)
+ expect(element.selectionEnd).toBe(element.value.length)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="hello"]
+
+ input[value="hello"] - pointerover
+ input[value="hello"] - pointerenter
+ input[value="hello"] - mouseover: Left (0)
+ input[value="hello"] - mouseenter: Left (0)
+ input[value="hello"] - pointermove
+ input[value="hello"] - mousemove: Left (0)
+ input[value="hello"] - pointerdown
+ input[value="hello"] - mousedown: Left (0)
+ input[value="hello"] - focus
+ input[value="hello"] - focusin
+ input[value="hello"] - pointerup
+ input[value="hello"] - mouseup: Left (0)
+ input[value="hello"] - click: Left (0)
+ input[value="hello"] - select
+ input[value="hello"] - keydown: Delete (46)
+ input[value="hello"] - keyup: Delete (46)
+ `)
+})
+
+test('{del} in the middle of the input', () => {
+ const {element, getEventSnapshot} = setup(`
`)
+
+ userEvent.type(element, '{del}', {
+ initialSelectionStart: 2,
+ initialSelectionEnd: 2,
+ })
+
+ expect(element.selectionStart).toBe(2)
+ expect(element.selectionEnd).toBe(2)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="helo"]
+
+ input[value="hello"] - pointerover
+ input[value="hello"] - pointerenter
+ input[value="hello"] - mouseover: Left (0)
+ input[value="hello"] - mouseenter: Left (0)
+ input[value="hello"] - pointermove
+ input[value="hello"] - mousemove: Left (0)
+ input[value="hello"] - pointerdown
+ input[value="hello"] - mousedown: Left (0)
+ input[value="hello"] - focus
+ input[value="hello"] - focusin
+ input[value="hello"] - pointerup
+ input[value="hello"] - mouseup: Left (0)
+ input[value="hello"] - click: Left (0)
+ input[value="hello"] - select
+ input[value="hello"] - keydown: Delete (46)
+ input[value="helo"] - input
+ "he{CURSOR}llo" -> "helo{CURSOR}"
+ input[value="helo"] - select
+ input[value="helo"] - keyup: Delete (46)
+ `)
+})
+
+test('{del} with a selection range', () => {
+ const {element, getEventSnapshot} = setup(`
`)
+
+ userEvent.type(element, '{del}', {
+ initialSelectionStart: 1,
+ initialSelectionEnd: 3,
+ })
+
+ expect(element.selectionStart).toBe(1)
+ expect(element.selectionEnd).toBe(1)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="hlo"]
+
+ input[value="hello"] - pointerover
+ input[value="hello"] - pointerenter
+ input[value="hello"] - mouseover: Left (0)
+ input[value="hello"] - mouseenter: Left (0)
+ input[value="hello"] - pointermove
+ input[value="hello"] - mousemove: Left (0)
+ input[value="hello"] - pointerdown
+ input[value="hello"] - mousedown: Left (0)
+ input[value="hello"] - focus
+ input[value="hello"] - focusin
+ input[value="hello"] - pointerup
+ input[value="hello"] - mouseup: Left (0)
+ input[value="hello"] - click: Left (0)
+ input[value="hello"] - select
+ input[value="hello"] - keydown: Delete (46)
+ input[value="hlo"] - input
+ "h{SELECTION}el{/SELECTION}lo" -> "hlo{CURSOR}"
+ input[value="hlo"] - select
+ input[value="hlo"] - keyup: Delete (46)
+ `)
+})
+
+// TODO: eventually we'll want to support this, but currently we cannot
+// because selection ranges are (intentially) unsupported in certain input types
+// per the spec.
+test('{del} on an input that does not support selection range does not change the value', () => {
+ const {element, eventWasFired} = setup(`
`)
+
+ userEvent.type(element, '{del}')
+ expect(element).toHaveValue('a@b.c')
+ expect(eventWasFired('input')).not.toBe(true)
+})
+
+test('{del} does not delete if keydown is prevented', () => {
+ const {element, eventWasFired} = setup(`
`, {
+ eventHandlers: {keyDown: e => e.preventDefault()},
+ })
+
+ userEvent.type(element, '{del}', {
+ initialSelectionStart: 2,
+ initialSelectionEnd: 2,
+ })
+ expect(element).toHaveValue('hello')
+ expect(element.selectionStart).toBe(2)
+ expect(element.selectionEnd).toBe(2)
+ expect(eventWasFired('input')).not.toBe(true)
+})
+
+test('any remaining type modifiers are automatically released at the end', () => {
+ const {element, getEventSnapshot} = setup('
')
+
+ userEvent.type(element, '{meta}{alt}{ctrl}a{/alt}')
+
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="a"]
+
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Meta (93) {meta}
+ input[value=""] - keydown: Alt (18) {alt}{meta}
+ input[value=""] - keydown: Control (17) {alt}{meta}{ctrl}
+ input[value=""] - keydown: a (97) {alt}{meta}{ctrl}
+ input[value=""] - keypress: a (97) {alt}{meta}{ctrl}
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97) {alt}{meta}{ctrl}
+ input[value="a"] - keyup: Alt (18) {meta}{ctrl}
+ input[value="a"] - keyup: Meta (93) {ctrl}
+ input[value="a"] - keyup: Control (17)
+ `)
+})
+
+test('modifiers will not be closed if skipAutoClose is enabled', () => {
+ const {element, getEventSnapshot} = setup('
')
+
+ userEvent.type(element, '{meta}a', {skipAutoClose: true})
+
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: input[value="a"]
+
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: Meta (93) {meta}
+ input[value=""] - keydown: a (97) {meta}
+ input[value=""] - keypress: a (97) {meta}
+ input[value="a"] - input
+ "{CURSOR}" -> "a{CURSOR}"
+ input[value="a"] - keyup: a (97) {meta}
`)
})
diff --git a/src/__tests__/type.js b/src/__tests__/type.js
index 34c1d206..2faefabf 100644
--- a/src/__tests__/type.js
+++ b/src/__tests__/type.js
@@ -1,173 +1,238 @@
-import userEvent from '..'
+import userEvent from '../'
import {setup, addListeners} from './helpers/utils'
-import './helpers/customElement'
+import './helpers/custom-element'
-test('types text in input', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, 'Sup')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+test('types text in input', () => {
+ const {element, getEventSnapshot} = setup('
')
+ userEvent.type(element, 'Sup')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="Sup"]
- focus
- keydown: S (83)
- keypress: S (83)
- input: "{CURSOR}" -> "S"
- keyup: S (83)
- keydown: u (117)
- keypress: u (117)
- input: "S{CURSOR}" -> "Su"
- keyup: u (117)
- keydown: p (112)
- keypress: p (112)
- input: "Su{CURSOR}" -> "Sup"
- keyup: p (112)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: S (83)
+ input[value=""] - keypress: S (83)
+ input[value="S"] - input
+ "{CURSOR}" -> "S{CURSOR}"
+ input[value="S"] - keyup: S (83)
+ input[value="S"] - keydown: u (117)
+ input[value="S"] - keypress: u (117)
+ input[value="Su"] - input
+ "S{CURSOR}" -> "Su{CURSOR}"
+ input[value="Su"] - keyup: u (117)
+ input[value="Su"] - keydown: p (112)
+ input[value="Su"] - keypress: p (112)
+ input[value="Sup"] - input
+ "Su{CURSOR}" -> "Sup{CURSOR}"
+ input[value="Sup"] - keyup: p (112)
`)
})
-test('types text in input with allAtOnce', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, 'Sup', {allAtOnce: true})
- expect(getEventCalls()).toMatchInlineSnapshot(`
+test('can skip the initial click', () => {
+ const {element, getEventSnapshot, clearEventCalls} = setup('
')
+ element.focus() // users MUST focus themselves if they wish to skip the click
+ clearEventCalls()
+ userEvent.type(element, 'Sup', {skipClick: true})
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="Sup"]
- focus
- input: "{CURSOR}" -> "Sup"
+ input[value=""] - keydown: S (83)
+ input[value=""] - keypress: S (83)
+ input[value="S"] - input
+ "{CURSOR}" -> "S{CURSOR}"
+ input[value="S"] - keyup: S (83)
+ input[value="S"] - keydown: u (117)
+ input[value="S"] - keypress: u (117)
+ input[value="Su"] - input
+ "S{CURSOR}" -> "Su{CURSOR}"
+ input[value="Su"] - keyup: u (117)
+ input[value="Su"] - keydown: p (112)
+ input[value="Su"] - keypress: p (112)
+ input[value="Sup"] - input
+ "Su{CURSOR}" -> "Sup{CURSOR}"
+ input[value="Sup"] - keyup: p (112)
`)
})
-test('types text inside custom element', async () => {
+test('types text inside custom element', () => {
const element = document.createElement('custom-el')
document.body.append(element)
const inputEl = element.shadowRoot.querySelector('input')
- const {getEventCalls} = addListeners(inputEl)
+ const {getEventSnapshot} = addListeners(inputEl)
- await userEvent.type(inputEl, 'Sup')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ userEvent.type(inputEl, 'Sup')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="Sup"]
- focus
- keydown: S (83)
- keypress: S (83)
- input: "{CURSOR}" -> "S"
- keyup: S (83)
- keydown: u (117)
- keypress: u (117)
- input: "S{CURSOR}" -> "Su"
- keyup: u (117)
- keydown: p (112)
- keypress: p (112)
- input: "Su{CURSOR}" -> "Sup"
- keyup: p (112)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: S (83)
+ input[value=""] - keypress: S (83)
+ input[value="S"] - input
+ "{CURSOR}" -> "S{CURSOR}"
+ input[value="S"] - keyup: S (83)
+ input[value="S"] - keydown: u (117)
+ input[value="S"] - keypress: u (117)
+ input[value="Su"] - input
+ "S{CURSOR}" -> "Su{CURSOR}"
+ input[value="Su"] - keyup: u (117)
+ input[value="Su"] - keydown: p (112)
+ input[value="Su"] - keypress: p (112)
+ input[value="Sup"] - input
+ "Su{CURSOR}" -> "Sup{CURSOR}"
+ input[value="Sup"] - keyup: p (112)
`)
})
-test('types text in textarea', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, 'Sup')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+test('types text in textarea', () => {
+ const {element, getEventSnapshot} = setup('
')
+ userEvent.type(element, 'Sup')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: textarea[value="Sup"]
- focus
- keydown: S (83)
- keypress: S (83)
- input: "{CURSOR}" -> "S"
- keyup: S (83)
- keydown: u (117)
- keypress: u (117)
- input: "S{CURSOR}" -> "Su"
- keyup: u (117)
- keydown: p (112)
- keypress: p (112)
- input: "Su{CURSOR}" -> "Sup"
- keyup: p (112)
+ textarea[value=""] - pointerover
+ textarea[value=""] - pointerenter
+ textarea[value=""] - mouseover: Left (0)
+ textarea[value=""] - mouseenter: Left (0)
+ textarea[value=""] - pointermove
+ textarea[value=""] - mousemove: Left (0)
+ textarea[value=""] - pointerdown
+ textarea[value=""] - mousedown: Left (0)
+ textarea[value=""] - focus
+ textarea[value=""] - focusin
+ textarea[value=""] - pointerup
+ textarea[value=""] - mouseup: Left (0)
+ textarea[value=""] - click: Left (0)
+ textarea[value=""] - keydown: S (83)
+ textarea[value=""] - keypress: S (83)
+ textarea[value="S"] - input
+ "{CURSOR}" -> "S{CURSOR}"
+ textarea[value="S"] - keyup: S (83)
+ textarea[value="S"] - keydown: u (117)
+ textarea[value="S"] - keypress: u (117)
+ textarea[value="Su"] - input
+ "S{CURSOR}" -> "Su{CURSOR}"
+ textarea[value="Su"] - keyup: u (117)
+ textarea[value="Su"] - keydown: p (112)
+ textarea[value="Su"] - keypress: p (112)
+ textarea[value="Sup"] - input
+ "Su{CURSOR}" -> "Sup{CURSOR}"
+ textarea[value="Sup"] - keyup: p (112)
`)
})
-test('should append text all at once', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, 'Sup', {allAtOnce: true})
- expect(getEventCalls()).toMatchInlineSnapshot(`
- Events fired on: input[value="Sup"]
-
- focus
- input: "{CURSOR}" -> "Sup"
- `)
-})
-
-test('does not fire input event when keypress calls prevent default', async () => {
- const {element, getEventCalls} = setup('
', {
+test('does not fire input event when keypress calls prevent default', () => {
+ const {element, getEventSnapshot} = setup('
', {
eventHandlers: {keyPress: e => e.preventDefault()},
})
- await userEvent.type(element, 'a')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ userEvent.type(element, 'a')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]
- focus
- keydown: a (97)
- keypress: a (97)
- keyup: a (97)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: a (97)
+ input[value=""] - keypress: a (97)
+ input[value=""] - keyup: a (97)
`)
})
-test('does not fire keypress or input events when keydown calls prevent default', async () => {
- const {element, getEventCalls} = setup('
', {
+test('does not fire keypress or input events when keydown calls prevent default', () => {
+ const {element, getEventSnapshot} = setup('
', {
eventHandlers: {keyDown: e => e.preventDefault()},
})
- await userEvent.type(element, 'a')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ userEvent.type(element, 'a')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]
- focus
- keydown: a (97)
- keyup: a (97)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: a (97)
+ input[value=""] - keyup: a (97)
`)
})
-test('does not fire events when disabled', async () => {
- const {element, getEventCalls} = setup('
')
+test('does not fire events when disabled', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, 'a')
- expect(getEventCalls()).toMatchInlineSnapshot(
+ userEvent.type(element, 'a')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: input[value=""]`,
)
})
-test('does not fire input when readonly', async () => {
- const {element, getEventCalls} = setup('
')
-
- await userEvent.type(element, 'a')
- expect(getEventCalls()).toMatchInlineSnapshot(`
- Events fired on: input[value=""]
-
- focus
- keydown: a (97)
- keypress: a (97)
- keyup: a (97)
- `)
-})
-
-test('does not fire input when readonly (with allAtOnce)', async () => {
- const {element, getEventCalls} = setup('
')
+test('does not fire input when readonly', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, 'a', {allAtOnce: true})
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ userEvent.type(element, 'a')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]
- focus
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: a (97)
+ input[value=""] - keypress: a (97)
+ input[value=""] - keyup: a (97)
`)
})
-test('does not fire any events when disabled (with allAtOnce)', async () => {
- const {element, getEventCalls} = setup('
')
-
- await userEvent.type(element, 'a', {allAtOnce: true})
- expect(getEventCalls()).toMatchInlineSnapshot(
- `No events were fired on: input[value=""]`,
- )
-})
-
test('should delay the typing when opts.delay is not 0', async () => {
const inputValues = [{timestamp: Date.now(), value: ''}]
const onInput = jest.fn(event => {
@@ -190,45 +255,74 @@ test('should delay the typing when opts.delay is not 0', async () => {
}
})
-test('honors maxlength', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, '123')
+test('honors maxlength', () => {
+ const {element, getEventSnapshot} = setup('
')
+ userEvent.type(element, '123')
// NOTE: no input event when typing "3"
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="12"]
- focus
- keydown: 1 (49)
- keypress: 1 (49)
- input: "{CURSOR}" -> "1"
- keyup: 1 (49)
- keydown: 2 (50)
- keypress: 2 (50)
- input: "1{CURSOR}" -> "12"
- keyup: 2 (50)
- keydown: 3 (51)
- keypress: 3 (51)
- keyup: 3 (51)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: 1 (49)
+ input[value=""] - keypress: 1 (49)
+ input[value="1"] - input
+ "{CURSOR}" -> "1{CURSOR}"
+ input[value="1"] - keyup: 1 (49)
+ input[value="1"] - keydown: 2 (50)
+ input[value="1"] - keypress: 2 (50)
+ input[value="12"] - input
+ "1{CURSOR}" -> "12{CURSOR}"
+ input[value="12"] - keyup: 2 (50)
+ input[value="12"] - keydown: 3 (51)
+ input[value="12"] - keypress: 3 (51)
+ input[value="12"] - keyup: 3 (51)
`)
})
-test('honors maxlength with existing text', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, '3')
+test('honors maxlength with existing text', () => {
+ const {element, getEventSnapshot} = setup(
+ '
',
+ )
+ userEvent.type(element, '3')
// NOTE: no input event when typing "3"
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="12"]
- focus
- keydown: 3 (51)
- keypress: 3 (51)
- keyup: 3 (51)
+ input[value="12"] - pointerover
+ input[value="12"] - pointerenter
+ input[value="12"] - mouseover: Left (0)
+ input[value="12"] - mouseenter: Left (0)
+ input[value="12"] - pointermove
+ input[value="12"] - mousemove: Left (0)
+ input[value="12"] - pointerdown
+ input[value="12"] - mousedown: Left (0)
+ input[value="12"] - focus
+ input[value="12"] - focusin
+ input[value="12"] - pointerup
+ input[value="12"] - mouseup: Left (0)
+ input[value="12"] - click: Left (0)
+ input[value="12"] - select
+ input[value="12"] - keydown: 3 (51)
+ input[value="12"] - keypress: 3 (51)
+ input[value="12"] - keyup: 3 (51)
`)
})
-test('should fire events on the currently focused element', async () => {
+test('should fire events on the currently focused element', () => {
const {element} = setup(`
`, {
eventHandlers: {keyDown: handleKeyDown},
})
@@ -244,27 +338,27 @@ test('should fire events on the currently focused element', async () => {
}
}
- await userEvent.type(input1, text)
+ userEvent.type(input1, text)
expect(input1).toHaveValue(text.slice(0, changeFocusLimit))
expect(input2).toHaveValue(text.slice(changeFocusLimit))
expect(input2).toHaveFocus()
})
-test('should replace selected text', async () => {
+test('should replace selected text', () => {
const {element} = setup('
')
const selectionStart = 'hello world'.search('world')
const selectionEnd = selectionStart + 'world'.length
element.setSelectionRange(selectionStart, selectionEnd)
- await userEvent.type(element, 'friend')
+ userEvent.type(element, 'friend')
expect(element).toHaveValue('hello friend')
})
-test('does not continue firing events when disabled during typing', async () => {
+test('does not continue firing events when disabled during typing', () => {
const {element} = setup('
', {
eventHandlers: {input: e => (e.target.disabled = true)},
})
- await userEvent.type(element, 'hi')
+ userEvent.type(element, 'hi')
expect(element).toHaveValue('h')
})
@@ -287,50 +381,79 @@ function setupDollarInput({initialValue = ''} = {}) {
return returnValue
}
-test('typing into a controlled input works', async () => {
- const {element, getEventCalls} = setupDollarInput()
+test('typing into a controlled input works', () => {
+ const {element, getEventSnapshot} = setupDollarInput()
- await userEvent.type(element, '23')
+ userEvent.type(element, '23')
expect(element.value).toBe('$23')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="$23"]
- focus
- keydown: 2 (50)
- keypress: 2 (50)
- input: "{CURSOR}" -> "$2"
- keyup: 2 (50)
- keydown: 3 (51)
- keypress: 3 (51)
- input: "$2{CURSOR}" -> "$23"
- keyup: 3 (51)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: 2 (50)
+ input[value=""] - keypress: 2 (50)
+ input[value="2"] - input
+ "{CURSOR}" -> "$2{CURSOR}"
+ input[value="$2"] - keyup: 2 (50)
+ input[value="$2"] - keydown: 3 (51)
+ input[value="$2"] - keypress: 3 (51)
+ input[value="$23"] - input
+ "$2{CURSOR}" -> "$23{CURSOR}"
+ input[value="$23"] - keyup: 3 (51)
`)
})
-test('typing in the middle of a controlled input works', async () => {
- const {element, getEventCalls} = setupDollarInput({initialValue: '$23'})
+test('typing in the middle of a controlled input works', () => {
+ const {element, getEventSnapshot} = setupDollarInput({initialValue: '$23'})
element.setSelectionRange(2, 2)
- await userEvent.type(element, '1')
+ userEvent.type(element, '1')
expect(element.value).toBe('$213')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="$213"]
- focus
- keydown: 1 (49)
- keypress: 1 (49)
- input: "$2{CURSOR}3" -> "$213"
- keyup: 1 (49)
+ input[value="$23"] - select
+ input[value="$23"] - pointerover
+ input[value="$23"] - pointerenter
+ input[value="$23"] - mouseover: Left (0)
+ input[value="$23"] - mouseenter: Left (0)
+ input[value="$23"] - pointermove
+ input[value="$23"] - mousemove: Left (0)
+ input[value="$23"] - pointerdown
+ input[value="$23"] - mousedown: Left (0)
+ input[value="$23"] - focus
+ input[value="$23"] - focusin
+ input[value="$23"] - pointerup
+ input[value="$23"] - mouseup: Left (0)
+ input[value="$23"] - click: Left (0)
+ input[value="$23"] - keydown: 1 (49)
+ input[value="$23"] - keypress: 1 (49)
+ input[value="$213"] - input
+ "$2{CURSOR}3" -> "$213{CURSOR}"
+ input[value="$213"] - select
+ input[value="$213"] - keyup: 1 (49)
`)
})
-test('ignored {backspace} in controlled input', async () => {
- const {element, getEventCalls} = setupDollarInput({initialValue: '$23'})
+test('ignored {backspace} in controlled input', () => {
+ const {element, getEventSnapshot} = setupDollarInput({initialValue: '$23'})
element.setSelectionRange(1, 1)
- await userEvent.type(element, '{backspace}')
+ userEvent.type(element, '{backspace}')
// this is the same behavior in the browser.
// in our case, when you try to backspace the "$", our event handler
// will ignore that change and React resets the value to what it was
@@ -339,142 +462,239 @@ test('ignored {backspace} in controlled input', async () => {
// the selection start and end to the end of the input
expect(element.selectionStart).toBe(element.value.length)
expect(element.selectionEnd).toBe(element.value.length)
- await userEvent.type(element, '4')
+ userEvent.type(element, '4')
expect(element.value).toBe('$234')
// the backslash in the inline snapshot is to escape the $ before {CURSOR}
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="$234"]
- focus
- keydown: Backspace (8)
- input: "\${CURSOR}23" -> "$23"
- keyup: Backspace (8)
- keydown: 4 (52)
- keypress: 4 (52)
- input: "$23{CURSOR}" -> "$234"
- keyup: 4 (52)
+ input[value="$23"] - select
+ input[value="$23"] - pointerover
+ input[value="$23"] - pointerenter
+ input[value="$23"] - mouseover: Left (0)
+ input[value="$23"] - mouseenter: Left (0)
+ input[value="$23"] - pointermove
+ input[value="$23"] - mousemove: Left (0)
+ input[value="$23"] - pointerdown
+ input[value="$23"] - mousedown: Left (0)
+ input[value="$23"] - focus
+ input[value="$23"] - focusin
+ input[value="$23"] - pointerup
+ input[value="$23"] - mouseup: Left (0)
+ input[value="$23"] - click: Left (0)
+ input[value="$23"] - keydown: Backspace (8)
+ input[value="23"] - input
+ "\${CURSOR}23" -> "$23{CURSOR}"
+ input[value="$23"] - keyup: Backspace (8)
+ input[value="$23"] - pointerover
+ input[value="$23"] - pointerenter
+ input[value="$23"] - mouseover: Left (0)
+ input[value="$23"] - mouseenter: Left (0)
+ input[value="$23"] - pointermove
+ input[value="$23"] - mousemove: Left (0)
+ input[value="$23"] - pointerdown
+ input[value="$23"] - mousedown: Left (0)
+ input[value="$23"] - pointerup
+ input[value="$23"] - mouseup: Left (0)
+ input[value="$23"] - click: Left (0)
+ input[value="$23"] - keydown: 4 (52)
+ input[value="$23"] - keypress: 4 (52)
+ input[value="$234"] - input
+ "$23{CURSOR}" -> "$234{CURSOR}"
+ input[value="$234"] - keyup: 4 (52)
`)
})
+// https://github.com/testing-library/user-event/issues/346
+test('typing in an empty textarea', () => {
+ const {element} = setup('
')
+
+ userEvent.type(element, '1234')
+ expect(element).toHaveValue('1234')
+})
+
// https://github.com/testing-library/user-event/issues/321
-test('typing in a textarea with existing text', async () => {
- const {element, getEventCalls} = setup('
')
+test('typing in a textarea with existing text', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.type(element, '12')
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ userEvent.type(element, '12')
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: textarea[value="Hello, 12"]
- 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)
+ textarea[value="Hello, "] - pointerover
+ textarea[value="Hello, "] - pointerenter
+ textarea[value="Hello, "] - mouseover: Left (0)
+ textarea[value="Hello, "] - mouseenter: Left (0)
+ textarea[value="Hello, "] - pointermove
+ textarea[value="Hello, "] - mousemove: Left (0)
+ textarea[value="Hello, "] - pointerdown
+ textarea[value="Hello, "] - mousedown: Left (0)
+ textarea[value="Hello, "] - focus
+ textarea[value="Hello, "] - focusin
+ textarea[value="Hello, "] - pointerup
+ textarea[value="Hello, "] - mouseup: Left (0)
+ textarea[value="Hello, "] - click: Left (0)
+ textarea[value="Hello, "] - select
+ textarea[value="Hello, "] - keydown: 1 (49)
+ textarea[value="Hello, "] - keypress: 1 (49)
+ textarea[value="Hello, 1"] - input
+ "Hello, {CURSOR}" -> "Hello, 1{CURSOR}"
+ textarea[value="Hello, 1"] - keyup: 1 (49)
+ textarea[value="Hello, 1"] - keydown: 2 (50)
+ textarea[value="Hello, 1"] - keypress: 2 (50)
+ textarea[value="Hello, 12"] - input
+ "Hello, 1{CURSOR}" -> "Hello, 12{CURSOR}"
+ textarea[value="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('
')
+test('accepts an initialSelectionStart and initialSelectionEnd', () => {
+ const {element, getEventSnapshot} = setup('
')
element.setSelectionRange(0, 0)
- await userEvent.type(element, '12', {
+ userEvent.type(element, '12', {
initialSelectionStart: element.selectionStart,
initialSelectionEnd: element.selectionEnd,
})
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: textarea[value="12Hello, "]
- 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)
+ textarea[value="Hello, "] - select
+ textarea[value="Hello, "] - pointerover
+ textarea[value="Hello, "] - pointerenter
+ textarea[value="Hello, "] - mouseover: Left (0)
+ textarea[value="Hello, "] - mouseenter: Left (0)
+ textarea[value="Hello, "] - pointermove
+ textarea[value="Hello, "] - mousemove: Left (0)
+ textarea[value="Hello, "] - pointerdown
+ textarea[value="Hello, "] - mousedown: Left (0)
+ textarea[value="Hello, "] - focus
+ textarea[value="Hello, "] - focusin
+ textarea[value="Hello, "] - pointerup
+ textarea[value="Hello, "] - mouseup: Left (0)
+ textarea[value="Hello, "] - click: Left (0)
+ textarea[value="Hello, "] - keydown: 1 (49)
+ textarea[value="Hello, "] - keypress: 1 (49)
+ textarea[value="1Hello, "] - input
+ "{CURSOR}Hello, " -> "1Hello, {CURSOR}"
+ textarea[value="1Hello, "] - select
+ textarea[value="1Hello, "] - keyup: 1 (49)
+ textarea[value="1Hello, "] - keydown: 2 (50)
+ textarea[value="1Hello, "] - keypress: 2 (50)
+ textarea[value="12Hello, "] - input
+ "1{CURSOR}Hello, " -> "12Hello, {CURSOR}"
+ textarea[value="12Hello, "] - select
+ textarea[value="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 () => {
+test('can type into an input with type `email`', () => {
const {element} = setup('
')
const email = 'yo@example.com'
- await userEvent.type(element, email)
+ userEvent.type(element, email)
expect(element).toHaveValue(email)
})
// https://github.com/testing-library/user-event/issues/336
-test('can type "-" into number inputs', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, '-3')
+test('can type "-" into number inputs', () => {
+ const {element, getEventSnapshot} = setup('
')
+ const negativeNumber = '-3'
+ userEvent.type(element, negativeNumber)
expect(element).toHaveValue(-3)
// NOTE: the input event here does not actually change the value thanks to
// weirdness with browsers. Then the second input event inserts both the
// - and the 3. /me rolls eyes
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="-3"]
- focus
- keydown: - (45)
- keypress: - (45)
- input: "{CURSOR}" -> ""
- keyup: - (45)
- keydown: 3 (51)
- keypress: 3 (51)
- input: "{CURSOR}" -> "-3"
- keyup: 3 (51)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: - (45)
+ input[value=""] - keypress: - (45)
+ input[value=""] - input
+ input[value=""] - keyup: - (45)
+ input[value=""] - keydown: 3 (51)
+ input[value=""] - keypress: 3 (51)
+ input[value="-3"] - input
+ "{CURSOR}" -> "{CURSOR}-3"
+ input[value="-3"] - keyup: 3 (51)
`)
})
// https://github.com/testing-library/user-event/issues/336
-test('can type "." into number inputs', async () => {
- const {element, getEventCalls} = setup('
')
- await userEvent.type(element, '0.3')
+test('can type "." into number inputs', () => {
+ const {element, getEventSnapshot} = setup('
')
+ userEvent.type(element, '0.3')
expect(element).toHaveValue(0.3)
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=".3"]
- focus
- keydown: 0 (48)
- keypress: 0 (48)
- input: "{CURSOR}" -> "0"
- keyup: 0 (48)
- keydown: . (46)
- keypress: . (46)
- input: "{CURSOR}0" -> ""
- keyup: . (46)
- keydown: 3 (51)
- keypress: 3 (51)
- input: "{CURSOR}" -> ".3"
- keyup: 3 (51)
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - keydown: 0 (48)
+ input[value=""] - keypress: 0 (48)
+ input[value="0"] - input
+ "{CURSOR}" -> "{CURSOR}0"
+ input[value="0"] - keyup: 0 (48)
+ input[value="0"] - keydown: . (46)
+ input[value="0"] - keypress: . (46)
+ input[value=""] - input
+ "{CURSOR}0" -> "{CURSOR}"
+ input[value=""] - keyup: . (46)
+ input[value=""] - keydown: 3 (51)
+ input[value=""] - keypress: 3 (51)
+ input[value=".3"] - input
+ "{CURSOR}" -> "{CURSOR}.3"
+ input[value=".3"] - keyup: 3 (51)
`)
})
-test('-{backspace}3', async () => {
+test('-{backspace}3', () => {
const {element} = setup('
')
- await userEvent.type(element, '-{backspace}3')
+ const negativeNumber = '-{backspace}3'
+ userEvent.type(element, negativeNumber)
expect(element).toHaveValue(3)
})
-test('-a3', async () => {
+test('-a3', () => {
const {element} = setup('
')
- await userEvent.type(element, '-a3')
+ const negativeNumber = '-a3'
+ userEvent.type(element, negativeNumber)
expect(element).toHaveValue(-3)
})
-test('typing an invalid input value', async () => {
+test('typing an invalid input value', () => {
const {element} = setup('
')
- await userEvent.type(element, '3-3')
+ userEvent.type(element, '3-3')
// TODO: fix this bug
// THIS IS A BUG! It should be expect(element.value).toBe('')
diff --git a/src/__tests__/unhover.js b/src/__tests__/unhover.js
index 283ba0f5..09b3dee8 100644
--- a/src/__tests__/unhover.js
+++ b/src/__tests__/unhover.js
@@ -1,14 +1,40 @@
-import userEvent from '..'
+import userEvent from '../'
import {setup} from './helpers/utils'
-test('unhover', async () => {
- const {element, getEventCalls} = setup('
')
+test('unhover', () => {
+ const {element, getEventSnapshot} = setup('
')
- await userEvent.unhover(element)
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ userEvent.unhover(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: button
- mousemove: Left (0)
- mouseleave: Left (0)
+ button - pointermove
+ button - mousemove: Left (0)
+ button - pointerout
+ button - pointerleave
+ button - mouseout: Left (0)
+ button - mouseleave: Left (0)
`)
})
+
+test('unhover on disabled element', () => {
+ const {element, getEventSnapshot} = setup('
')
+
+ userEvent.unhover(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: button
+
+ button - pointermove
+ button - pointerout
+ button - pointerleave
+ `)
+})
+
+test('no events fired on labels that contain disabled controls', () => {
+ const {element, getEventSnapshot} = setup('
')
+
+ userEvent.unhover(element)
+ expect(getEventSnapshot()).toMatchInlineSnapshot(
+ `No events were fired on: label`,
+ )
+})
diff --git a/src/__tests__/upload.js b/src/__tests__/upload.js
index ef2d3899..223a6219 100644
--- a/src/__tests__/upload.js
+++ b/src/__tests__/upload.js
@@ -1,56 +1,66 @@
-import userEvent from '..'
-import {setup, addListeners} from './helpers/utils'
+import userEvent from '../'
+import {setup} from './helpers/utils'
test('should fire the correct events for input', () => {
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
- const {element, getEventCalls} = setup('
')
+ const {element, getEventSnapshot} = setup('
')
userEvent.upload(element, file)
- expect(getEventCalls()).toMatchInlineSnapshot(`
+ // NOTE: A known limitation is that it's impossible to set the
+ // value of the input programmatically. The value in the browser
+ // set by a user would be: `C:\\fakepath\\${file.name}`
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]
- mouseover: Left (0)
- mousemove: Left (0)
- mousedown: Left (0)
- focus
- mouseup: Left (0)
- click: Left (0)
- change
+ input[value=""] - pointerover
+ input[value=""] - pointerenter
+ input[value=""] - mouseover: Left (0)
+ input[value=""] - mouseenter: Left (0)
+ input[value=""] - pointermove
+ input[value=""] - mousemove: Left (0)
+ input[value=""] - pointerdown
+ input[value=""] - mousedown: Left (0)
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - pointerup
+ input[value=""] - mouseup: Left (0)
+ input[value=""] - click: Left (0)
+ input[value=""] - blur
+ input[value=""] - focusout
+ input[value=""] - focus
+ input[value=""] - focusin
+ input[value=""] - input
+ input[value=""] - change
`)
})
test('should fire the correct events with label', () => {
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
- const container = document.createElement('div')
- container.innerHTML = `
-
-
- `
-
- const label = container.children[0]
- const input = container.children[1]
- const {getEventCalls: getLabelEventCalls} = addListeners(label)
- const {getEventCalls: getInputEventCalls} = addListeners(input)
-
- userEvent.upload(label, file)
-
- expect(getLabelEventCalls()).toMatchInlineSnapshot(`
- Events fired on: label[for="element"]
-
- mouseover: Left (0)
- mousemove: Left (0)
- mousedown: Left (0)
- mouseup: Left (0)
- click: Left (0)
- change
+ const {element, getEventSnapshot} = setup(`
+
`)
- expect(getInputEventCalls()).toMatchInlineSnapshot(`
- Events fired on: input#element[value=""]
- click: Left (0)
- focus
+ userEvent.upload(element.querySelector('label'), file)
+
+ expect(getEventSnapshot()).toMatchInlineSnapshot(`
+ Events fired on: form
+
+ label[for="element"] - pointerover
+ label[for="element"] - mouseover: Left (0)
+ label[for="element"] - pointermove
+ label[for="element"] - mousemove: Left (0)
+ label[for="element"] - pointerdown
+ label[for="element"] - mousedown: Left (0)
+ label[for="element"] - pointerup
+ label[for="element"] - mouseup: Left (0)
+ label[for="element"] - click: Left (0)
+ input#element[value=""] - click: Left (0)
+ input#element[value=""] - focusin
`)
})
@@ -81,6 +91,30 @@ test('should upload multiple files', () => {
expect(element.files).toHaveLength(2)
})
+test('should upload multiple files when firing on the label', () => {
+ const files = [
+ new File(['hello'], 'hello.png', {type: 'image/png'}),
+ new File(['there'], 'there.png', {type: 'image/png'}),
+ ]
+ const {element} = setup(`
+
+
+
+
+ `)
+
+ const label = element.children[0]
+ const input = element.children[1]
+
+ userEvent.upload(label, files)
+
+ expect(input.files[0]).toStrictEqual(files[0])
+ expect(input.files.item(0)).toStrictEqual(files[0])
+ expect(input.files[1]).toStrictEqual(files[1])
+ expect(input.files.item(1)).toStrictEqual(files[1])
+ expect(input.files).toHaveLength(2)
+})
+
test('should not upload when is disabled', () => {
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
const {element} = setup('
')
diff --git a/src/blur.js b/src/blur.js
new file mode 100644
index 00000000..549e0454
--- /dev/null
+++ b/src/blur.js
@@ -0,0 +1,14 @@
+import {fireEvent} from '@testing-library/dom'
+import {getActiveElement, isFocusable} from './utils'
+
+function blur(element, init) {
+ if (!isFocusable(element)) return
+
+ const wasActive = getActiveElement(element.ownerDocument) === element
+ if (!wasActive) return
+
+ element.blur()
+ fireEvent.focusOut(element, init)
+}
+
+export {blur}
diff --git a/src/clear.js b/src/clear.js
new file mode 100644
index 00000000..965d1a6c
--- /dev/null
+++ b/src/clear.js
@@ -0,0 +1,30 @@
+import {type} from './type'
+
+function clear(element) {
+ if (element.tagName !== 'INPUT' && element.tagName !== 'TEXTAREA') {
+ // TODO: support contenteditable
+ throw new Error(
+ 'clear currently only supports input and textarea elements.',
+ )
+ }
+
+ if (element.disabled) return
+ // TODO: track the selection range ourselves so we don't have to do this input "type" trickery
+ // just like cypress does: https://github.com/cypress-io/cypress/blob/8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683/packages/driver/src/dom/selection.ts#L16-L37
+ const elementType = element.type
+ // type is a readonly property on textarea, so check if element is an input before trying to modify it
+ if (element.tagName === 'INPUT') {
+ // setSelectionRange is not supported on certain types of inputs, e.g. "number" or "email"
+ element.type = 'text'
+ }
+ type(element, '{selectall}{del}', {
+ delay: 0,
+ initialSelectionStart: element.selectionStart,
+ initialSelectionEnd: element.selectionEnd,
+ })
+ if (element.tagName === 'INPUT') {
+ element.type = elementType
+ }
+}
+
+export {clear}
diff --git a/src/click.js b/src/click.js
new file mode 100644
index 00000000..ee5fc6ec
--- /dev/null
+++ b/src/click.js
@@ -0,0 +1,106 @@
+import {fireEvent} from '@testing-library/dom'
+import {
+ getMouseEventOptions,
+ isLabelWithInternallyDisabledControl,
+} from './utils'
+import {hover} from './hover'
+import {blur} from './blur'
+import {focus} from './focus'
+
+function getPreviouslyFocusedElement(element) {
+ const focusedElement = element.ownerDocument.activeElement
+ const wasAnotherElementFocused =
+ focusedElement &&
+ focusedElement !== element.ownerDocument.body &&
+ focusedElement !== element
+ return wasAnotherElementFocused ? focusedElement : null
+}
+
+function clickLabel(label, init, {clickCount}) {
+ if (isLabelWithInternallyDisabledControl(label)) return
+
+ fireEvent.pointerDown(label, init)
+ fireEvent.mouseDown(
+ label,
+ getMouseEventOptions('mousedown', init, clickCount),
+ )
+ fireEvent.pointerUp(label, init)
+ fireEvent.mouseUp(label, getMouseEventOptions('mouseup', init, clickCount))
+ fireEvent.click(label, getMouseEventOptions('click', init, clickCount))
+ // clicking the label will trigger a click of the label.control
+ // however, it will not focus the label.control so we have to do it
+ // ourselves.
+ if (label.control) focus(label.control)
+}
+
+function clickBooleanElement(element, init, clickCount) {
+ fireEvent.pointerDown(element, init)
+ if (!element.disabled) {
+ fireEvent.mouseDown(
+ element,
+ getMouseEventOptions('mousedown', init, clickCount),
+ )
+ }
+ focus(element, init)
+ fireEvent.pointerUp(element, init)
+ if (!element.disabled) {
+ fireEvent.mouseUp(
+ element,
+ getMouseEventOptions('mouseup', init, clickCount),
+ )
+ fireEvent.click(element, getMouseEventOptions('click', init, clickCount))
+ }
+}
+
+function clickElement(element, init, {clickCount}) {
+ const previousElement = getPreviouslyFocusedElement(element)
+ fireEvent.pointerDown(element, init)
+ if (!element.disabled) {
+ const continueDefaultHandling = fireEvent.mouseDown(
+ element,
+ getMouseEventOptions('mousedown', init, clickCount),
+ )
+ const shouldFocus = element.ownerDocument.activeElement !== element
+ if (continueDefaultHandling) {
+ if (previousElement) blur(previousElement, init)
+ if (shouldFocus) focus(element, init)
+ }
+ }
+ fireEvent.pointerUp(element, init)
+ if (!element.disabled) {
+ fireEvent.mouseUp(
+ element,
+ getMouseEventOptions('mouseup', init, clickCount),
+ )
+ fireEvent.click(element, getMouseEventOptions('click', init, clickCount))
+ const parentLabel = element.closest('label')
+ if (parentLabel?.control) focus(parentLabel.control, init)
+ }
+}
+
+function click(element, init, {skipHover = false, clickCount = 0} = {}) {
+ if (!skipHover) hover(element, init)
+ switch (element.tagName) {
+ case 'LABEL':
+ clickLabel(element, init, {clickCount})
+ break
+ case 'INPUT':
+ if (element.type === 'checkbox' || element.type === 'radio') {
+ clickBooleanElement(element, init, {clickCount})
+ } else {
+ clickElement(element, init, {clickCount})
+ }
+ break
+ default:
+ clickElement(element, init, {clickCount})
+ }
+}
+
+function dblClick(element, init) {
+ hover(element, init)
+ click(element, init, {skipHover: true, clickCount: 0})
+ click(element, init, {skipHover: true, clickCount: 1})
+ fireEvent.dblClick(element, getMouseEventOptions('dblclick', init, 2))
+}
+
+export {click, dblClick}
diff --git a/src/focus.js b/src/focus.js
new file mode 100644
index 00000000..52644ae6
--- /dev/null
+++ b/src/focus.js
@@ -0,0 +1,14 @@
+import {fireEvent} from '@testing-library/dom'
+import {getActiveElement, isFocusable} from './utils'
+
+function focus(element, init) {
+ if (!isFocusable(element)) return
+
+ const isAlreadyActive = getActiveElement(element.ownerDocument) === element
+ if (isAlreadyActive) return
+
+ element.focus()
+ fireEvent.focusIn(element, init)
+}
+
+export {focus}
diff --git a/src/hover.js b/src/hover.js
new file mode 100644
index 00000000..e2a3bb80
--- /dev/null
+++ b/src/hover.js
@@ -0,0 +1,37 @@
+import {fireEvent} from '@testing-library/dom'
+import {
+ isLabelWithInternallyDisabledControl,
+ getMouseEventOptions,
+} from './utils'
+
+function hover(element, init) {
+ if (isLabelWithInternallyDisabledControl(element)) return
+
+ fireEvent.pointerOver(element, init)
+ fireEvent.pointerEnter(element, init)
+ if (!element.disabled) {
+ fireEvent.mouseOver(element, getMouseEventOptions('mouseover', init))
+ fireEvent.mouseEnter(element, getMouseEventOptions('mouseenter', init))
+ }
+ fireEvent.pointerMove(element, init)
+ if (!element.disabled) {
+ fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init))
+ }
+}
+
+function unhover(element, init) {
+ if (isLabelWithInternallyDisabledControl(element)) return
+
+ fireEvent.pointerMove(element, init)
+ if (!element.disabled) {
+ fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init))
+ }
+ fireEvent.pointerOut(element, init)
+ fireEvent.pointerLeave(element, init)
+ if (!element.disabled) {
+ fireEvent.mouseOut(element, getMouseEventOptions('mouseout', init))
+ fireEvent.mouseLeave(element, getMouseEventOptions('mouseleave', init))
+ }
+}
+
+export {hover, unhover}
diff --git a/src/index.js b/src/index.js
index 4a7af1ff..5b0925fd 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,491 +1,24 @@
-import {fireEvent} from '@testing-library/dom'
+import {click, dblClick} from './click'
import {type} from './type'
-import {tick} from './tick'
-
-function isMousePressEvent(event) {
- return (
- event === 'mousedown' ||
- event === 'mouseup' ||
- event === 'click' ||
- event === 'dblclick'
- )
-}
-
-function invert(map) {
- const res = {}
- for (const key of Object.keys(map)) {
- res[map[key]] = key
- }
-
- return res
-}
-
-// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
-const BUTTONS_TO_NAMES = {
- 0: 'none',
- 1: 'primary',
- 2: 'secondary',
- 4: 'auxiliary',
-}
-const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES)
-
-// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
-const BUTTON_TO_NAMES = {
- 0: 'primary',
- 1: 'auxiliary',
- 2: 'secondary',
-}
-
-const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES)
-
-function convertMouseButtons(event, init, property, mapping) {
- if (!isMousePressEvent(event)) {
- return 0
- }
-
- if (init[property] != null) {
- return init[property]
- }
-
- if (init.buttons != null) {
- return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0
- }
-
- if (init.button != null) {
- return mapping[BUTTON_TO_NAMES[init.button]] || 0
- }
-
- return property != 'button' && isMousePressEvent(event) ? 1 : 0
-}
-
-function getMouseEventOptions(event, init, clickCount = 0) {
- init = init || {}
- return {
- ...init,
- // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
- detail:
- event === 'mousedown' || event === 'mouseup'
- ? 1 + clickCount
- : clickCount,
- buttons: convertMouseButtons(event, init, 'buttons', NAMES_TO_BUTTONS),
- button: convertMouseButtons(event, init, 'button', NAMES_TO_BUTTON),
- }
-}
-
-function clickLabel(label, init) {
- fireEvent.mouseOver(label, getMouseEventOptions('mouseover', init))
- fireEvent.mouseMove(label, getMouseEventOptions('mousemove', init))
- fireEvent.mouseDown(label, getMouseEventOptions('mousedown', init))
- fireEvent.mouseUp(label, getMouseEventOptions('mouseup', init))
- fireEvent.click(label, getMouseEventOptions('click', init))
-
- // clicking the label will trigger a click of the label.control
- // however, it will not focus the label.control so we have to do it
- // ourselves.
- if (label.control) label.control.focus()
-}
-
-function clickBooleanElement(element, init) {
- if (element.disabled) return
-
- fireEvent.mouseOver(element, getMouseEventOptions('mouseover', init))
- fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init))
- fireEvent.mouseDown(element, getMouseEventOptions('mousedown', init))
- fireEvent.focus(element)
- fireEvent.mouseUp(element, getMouseEventOptions('mouseup', init))
- fireEvent.click(element, getMouseEventOptions('click', init))
-}
-
-function clickElement(element, previousElement, init) {
- fireEvent.mouseOver(element, getMouseEventOptions('mouseover', init))
- fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init))
- const continueDefaultHandling = fireEvent.mouseDown(
- element,
- getMouseEventOptions('mousedown', init),
- )
- const shouldFocus = element.ownerDocument.activeElement !== element
- if (continueDefaultHandling) {
- if (previousElement) previousElement.blur()
- if (shouldFocus) element.focus()
- }
- fireEvent.mouseUp(element, getMouseEventOptions('mouseup', init))
- fireEvent.click(element, getMouseEventOptions('click', init, 1))
- const parentLabel = element.closest('label')
- if (parentLabel?.control) parentLabel?.control.focus?.()
-}
-
-function dblClickElement(element, previousElement, init) {
- fireEvent.mouseOver(element, getMouseEventOptions('mouseover', init))
- fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init))
- const continueDefaultHandling = fireEvent.mouseDown(
- element,
- getMouseEventOptions('mousedown', init),
- )
- const shouldFocus = element.ownerDocument.activeElement !== element
- if (continueDefaultHandling) {
- if (previousElement) previousElement.blur()
- if (shouldFocus) element.focus()
- }
- fireEvent.mouseUp(element, getMouseEventOptions('mouseup', init))
- fireEvent.click(element, getMouseEventOptions('click', init, 1))
- const parentLabel = element.closest('label')
- if (parentLabel?.control) parentLabel?.control.focus?.()
-
- fireEvent.mouseDown(element, getMouseEventOptions('mousedown', init, 1))
- fireEvent.mouseUp(element, getMouseEventOptions('mouseup', init, 1))
- fireEvent.click(element, getMouseEventOptions('click', init, 2))
- fireEvent.dblClick(element, getMouseEventOptions('dblclick', init, 2))
-}
-
-function dblClickCheckbox(checkbox, init) {
- fireEvent.mouseOver(checkbox, getMouseEventOptions('mouseover', init))
- fireEvent.mouseMove(checkbox, getMouseEventOptions('mousemove', init))
- fireEvent.mouseDown(checkbox, getMouseEventOptions('mousedown', init))
- fireEvent.focus(checkbox)
- fireEvent.mouseUp(checkbox, getMouseEventOptions('mouseup', init))
- fireEvent.click(checkbox, getMouseEventOptions('click', init, 1))
- fireEvent.mouseDown(checkbox, getMouseEventOptions('mousedown', init, 1))
- fireEvent.mouseUp(checkbox, getMouseEventOptions('mouseup', init, 1))
- fireEvent.click(checkbox, getMouseEventOptions('click', init, 2))
-}
-
-function selectOption(select, option, init) {
- fireEvent.mouseOver(option, getMouseEventOptions('mouseover', init))
- fireEvent.mouseMove(option, getMouseEventOptions('mousemove', init))
- fireEvent.mouseDown(option, getMouseEventOptions('mousedown', init))
- fireEvent.focus(option)
- fireEvent.mouseUp(option, getMouseEventOptions('mouseup', init))
- fireEvent.click(option, getMouseEventOptions('click', init, 1))
-
- option.selected = true
-
- fireEvent.change(select)
-}
-
-function toggleSelectOption(select, option, init) {
- fireEvent.mouseOver(option, getMouseEventOptions('mouseover', init))
- fireEvent.mouseMove(option, getMouseEventOptions('mousemove', init))
- fireEvent.mouseDown(option, getMouseEventOptions('mousedown', init))
- fireEvent.focus(option)
- fireEvent.mouseUp(option, getMouseEventOptions('mouseup', init))
- fireEvent.click(option, getMouseEventOptions('click', init, 1))
-
- option.selected = !option.selected
-
- fireEvent.change(select)
-}
-
-const Keys = {
- Backspace: {keyCode: 8, code: 'Backspace', key: 'Backspace'},
-}
-
-function backspace(element) {
- const keyboardEventOptions = {
- key: Keys.Backspace.key,
- keyCode: Keys.Backspace.keyCode,
- which: Keys.Backspace.keyCode,
- }
- fireEvent.keyDown(element, keyboardEventOptions)
- fireEvent.keyUp(element, keyboardEventOptions)
-
- if (!element.readOnly) {
- fireEvent.input(element, {
- inputType: 'deleteContentBackward',
- })
-
- // We need to call `fireEvent.change` _before_ we change `element.value`
- // because `fireEvent.change` will use the element's native value setter
- // (meaning it will avoid prototype overrides implemented by React). If we
- // call `input.value = ""` first, React will swallow the change event (this
- // is checked in the tests). `fireEvent.change` will only call the native
- // value setter method if the event options include `{ target: { value }}`
- // (https://github.com/testing-library/dom-testing-library/blob/8846eaf20972f8e41ed11f278948ac38a692c3f1/src/events.js#L29-L32).
- //
- // Also, we still must call `element.value = ""` after calling
- // `fireEvent.change` because `fireEvent.change` will _only_ call the native
- // `value` setter and not the prototype override defined by React, causing
- // React's internal represetation of this state to get out of sync with the
- // value set on `input.value`; calling `element.value` after will also call
- // React's setter, keeping everything in sync.
- //
- // Comment either of these out or re-order them and see what parts of the
- // tests fail for more context.
- fireEvent.change(element, {target: {value: ''}})
- element.value = ''
- }
-}
-
-function selectAll(element) {
- dblClick(element) // simulate events (will not actually select)
- const elementType = element.type
- // type is a readonly property on textarea, so check if element is an input before trying to modify it
- if (isInputElement(element)) {
- // setSelectionRange is not supported on certain types of inputs, e.g. "number" or "email"
- element.type = 'text'
- }
- element.setSelectionRange(0, element.value.length)
- if (isInputElement(element)) {
- element.type = elementType
- }
-}
-
-function isInputElement(element) {
- return element.tagName.toLowerCase() === 'input'
-}
-
-function getPreviouslyFocusedElement(element) {
- const focusedElement = element.ownerDocument.activeElement
- const wasAnotherElementFocused =
- focusedElement &&
- focusedElement !== element.ownerDocument.body &&
- focusedElement !== element
- return wasAnotherElementFocused ? focusedElement : null
-}
-
-function click(element, init) {
- const previouslyFocusedElement = getPreviouslyFocusedElement(element)
- if (previouslyFocusedElement) {
- fireEvent.mouseMove(
- previouslyFocusedElement,
- getMouseEventOptions('mousemove', init),
- )
- fireEvent.mouseLeave(
- previouslyFocusedElement,
- getMouseEventOptions('mouseleave', init),
- )
- }
-
- switch (element.tagName) {
- case 'LABEL':
- clickLabel(element, init)
- break
- case 'INPUT':
- if (element.type === 'checkbox' || element.type === 'radio') {
- clickBooleanElement(element, init)
- break
- }
- // eslint-disable-next-line no-fallthrough
- default:
- clickElement(element, previouslyFocusedElement, init)
- }
-}
-
-function dblClick(element, init) {
- const previouslyFocusedElement = getPreviouslyFocusedElement(element)
- if (previouslyFocusedElement) {
- fireEvent.mouseMove(
- previouslyFocusedElement,
- getMouseEventOptions('mousemove', init),
- )
- fireEvent.mouseLeave(
- previouslyFocusedElement,
- getMouseEventOptions('mouseleave', init),
- )
- }
-
- switch (element.tagName) {
- case 'INPUT':
- if (element.type === 'checkbox') {
- dblClickCheckbox(element, previouslyFocusedElement, init)
- break
- }
- // eslint-disable-next-line no-fallthrough
- default:
- dblClickElement(element, previouslyFocusedElement, init)
- }
-}
-
-function selectOptions(element, values, init) {
- const previouslyFocusedElement = getPreviouslyFocusedElement(element)
- if (previouslyFocusedElement) {
- fireEvent.mouseMove(
- previouslyFocusedElement,
- getMouseEventOptions('mousemove', init),
- )
- fireEvent.mouseLeave(
- previouslyFocusedElement,
- getMouseEventOptions('mouseleave', init),
- )
- }
-
- clickElement(element, previouslyFocusedElement, init)
-
- const valArray = Array.isArray(values) ? values : [values]
- const selectedOptions = Array.from(element.querySelectorAll('option')).filter(
- opt => valArray.includes(opt.value) || valArray.includes(opt),
- )
-
- if (selectedOptions.length > 0) {
- if (element.multiple) {
- selectedOptions.forEach(option => selectOption(element, option))
- } else {
- selectOption(element, selectedOptions[0])
- }
- }
-}
-
-function toggleSelectOptions(element, values, init) {
- if (!element || element.tagName !== 'SELECT' || !element.multiple) {
- throw new Error(
- `Unable to toggleSelectOptions - please provide a select element with multiple=true`,
- )
- }
-
- const previouslyFocusedElement = getPreviouslyFocusedElement(element)
- if (previouslyFocusedElement) {
- fireEvent.mouseMove(
- previouslyFocusedElement,
- getMouseEventOptions('mousemove', init),
- )
- fireEvent.mouseLeave(
- previouslyFocusedElement,
- getMouseEventOptions('mouseleave', init),
- )
- }
-
- clickElement(element, previouslyFocusedElement, init)
-
- const valArray = Array.isArray(values) ? values : [values]
- const selectedOptions = Array.from(element.querySelectorAll('option')).filter(
- opt => valArray.includes(opt.value) || valArray.includes(opt),
- )
-
- if (selectedOptions.length > 0) {
- selectedOptions.forEach(option => toggleSelectOption(element, option, init))
- }
-}
-
-function clear(element) {
- if (element.disabled) return
-
- selectAll(element)
- backspace(element)
-}
-
-function upload(element, fileOrFiles, {clickInit, changeInit} = {}) {
- if (element.disabled) return
- const focusedElement = element.ownerDocument.activeElement
-
- let files
-
- if (element.tagName === 'LABEL') {
- clickLabel(element)
- files = element.control.multiple ? fileOrFiles : [fileOrFiles]
- } else {
- files = element.multiple ? fileOrFiles : [fileOrFiles]
- clickElement(element, focusedElement, clickInit)
- }
-
- fireEvent.change(element, {
- target: {
- files: {
- length: files.length,
- item: index => files[index] || null,
- ...files,
- },
- },
- ...changeInit,
- })
-}
-
-function tab({shift = false, focusTrap = document} = {}) {
- const focusableElements = focusTrap.querySelectorAll(
- 'input, button, select, textarea, a[href], [tabindex]',
- )
-
- const enabledElements = [...focusableElements].filter(
- el => el.getAttribute('tabindex') !== '-1' && !el.disabled,
- )
-
- if (enabledElements.length === 0) return
-
- const orderedElements = enabledElements
- .map((el, idx) => ({el, idx}))
- .sort((a, b) => {
- const tabIndexA = a.el.getAttribute('tabindex')
- const tabIndexB = b.el.getAttribute('tabindex')
-
- const diff = tabIndexA - tabIndexB
-
- return diff === 0 ? a.idx - b.idx : diff
- })
- .map(({el}) => el)
-
- if (shift) orderedElements.reverse()
-
- // keep only the checked or first element in each radio group
- const prunedElements = []
- for (const el of orderedElements) {
- if (el.type === 'radio' && el.name) {
- const replacedIndex = prunedElements.findIndex(
- ({name}) => name === el.name,
- )
-
- if (replacedIndex === -1) {
- prunedElements.push(el)
- } else if (el.checked) {
- prunedElements.splice(replacedIndex, 1)
- prunedElements.push(el)
- }
- } else {
- prunedElements.push(el)
- }
- }
-
- if (shift) prunedElements.reverse()
-
- const index = prunedElements.findIndex(
- el => el === el.ownerDocument.activeElement,
- )
-
- const nextIndex = shift ? index - 1 : index + 1
- const defaultIndex = shift ? prunedElements.length - 1 : 0
-
- const next = prunedElements[nextIndex] || prunedElements[defaultIndex]
-
- if (next.getAttribute('tabindex') === null) {
- next.setAttribute('tabindex', '0') // jsdom requires tabIndex=0 for an item to become 'document.activeElement'
- next.focus()
- next.removeAttribute('tabindex') // leave no trace. :)
- } else {
- next.focus()
- }
-}
-
-async function hover(element, init) {
- await tick()
- fireEvent.mouseOver(element, getMouseEventOptions('mouseover', init))
- await tick()
- fireEvent.mouseEnter(element, getMouseEventOptions('mouseenter', init))
- await tick()
- fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init))
-}
-
-async function unhover(element, init) {
- await tick()
- fireEvent.mouseMove(element, getMouseEventOptions('mousemove', init))
- await tick()
- fireEvent.mouseOut(element, getMouseEventOptions('mouseout', init))
- await tick()
- fireEvent.mouseLeave(element, getMouseEventOptions('mouseleave', init))
-}
+import {clear} from './clear'
+import {tab} from './tab'
+import {hover, unhover} from './hover'
+import {upload} from './upload'
+import {selectOptions, deselectOptions} from './select-options'
+import {paste} from './paste'
const userEvent = {
click,
dblClick,
- selectOptions,
- toggleSelectOptions,
- clear,
type,
- upload,
+ clear,
tab,
hover,
unhover,
+ upload,
+ selectOptions,
+ deselectOptions,
+ paste,
}
export default userEvent
-
-/*
-eslint
- max-depth: ["error", 6],
-*/
diff --git a/src/paste.js b/src/paste.js
new file mode 100644
index 00000000..3306eb50
--- /dev/null
+++ b/src/paste.js
@@ -0,0 +1,45 @@
+import {fireEvent} from '@testing-library/dom'
+import {setSelectionRangeIfNecessary, calculateNewValue} from './utils'
+
+function paste(
+ element,
+ text,
+ init,
+ {initialSelectionStart, initialSelectionEnd} = {},
+) {
+ if (element.disabled) return
+
+ element.focus()
+
+ // by default, a new element has it's selection start and end at 0
+ // but most of the time when people call "paste", they expect it to paste
+ // 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 (element.selectionStart === 0 && element.selectionEnd === 0) {
+ setSelectionRangeIfNecessary(
+ element,
+ initialSelectionStart ?? element.value.length,
+ initialSelectionEnd ?? element.value.length,
+ )
+ }
+
+ fireEvent.paste(element, init)
+
+ if (!element.readOnly) {
+ const {newValue, newSelectionStart} = calculateNewValue(text, element)
+ fireEvent.input(element, {
+ inputType: 'insertFromPaste',
+ target: {value: newValue},
+ })
+ setSelectionRangeIfNecessary(element, {
+ newSelectionStart,
+ newSelectionEnd: newSelectionStart,
+ })
+ }
+}
+
+export {paste}
diff --git a/src/select-options.js b/src/select-options.js
new file mode 100644
index 00000000..4c55144b
--- /dev/null
+++ b/src/select-options.js
@@ -0,0 +1,71 @@
+import {createEvent, getConfig, fireEvent} from '@testing-library/dom'
+import {click} from './click'
+import {focus} from './focus'
+
+function selectOptionsBase(newValue, select, values, init) {
+ if (!newValue && !select.multiple) {
+ throw getConfig().getElementError(
+ `Unable to deselect an option in a non-multiple select. Use selectOptions to change the selection instead.`,
+ select,
+ )
+ }
+ const valArray = Array.isArray(values) ? values : [values]
+ const allOptions = Array.from(select.querySelectorAll('option'))
+ const selectedOptions = valArray
+ .map(val => {
+ if (allOptions.includes(val)) {
+ return val
+ } else {
+ const matchingOption = allOptions.find(o => o.value === val)
+ if (matchingOption) {
+ return matchingOption
+ } else {
+ throw getConfig().getElementError(
+ `Value "${val}" not found in options`,
+ select,
+ )
+ }
+ }
+ })
+ .filter(option => !option.disabled)
+
+ if (select.disabled || !selectedOptions.length) return
+
+ if (select.multiple) {
+ for (const option of selectedOptions) {
+ // events fired for multiple select are weird. Can't use hover...
+ fireEvent.pointerOver(option, init)
+ fireEvent.pointerEnter(select, init)
+ fireEvent.mouseOver(option)
+ fireEvent.mouseEnter(select)
+ fireEvent.pointerMove(option, init)
+ fireEvent.mouseMove(option, init)
+ fireEvent.pointerDown(option, init)
+ fireEvent.mouseDown(option, init)
+ focus(select, init)
+ fireEvent.pointerUp(option, init)
+ fireEvent.mouseUp(option, init)
+ selectOption(option)
+ fireEvent.click(option, init)
+ }
+ } else if (selectedOptions.length === 1) {
+ click(select, init)
+ selectOption(selectedOptions[0])
+ } else {
+ throw getConfig().getElementError(
+ `Cannot select multiple options on a non-multiple select`,
+ select,
+ )
+ }
+
+ function selectOption(option) {
+ option.selected = newValue
+ fireEvent(select, createEvent('input', select, init))
+ fireEvent(select, createEvent('change', select, init))
+ }
+}
+
+const selectOptions = selectOptionsBase.bind(null, true)
+const deselectOptions = selectOptionsBase.bind(null, false)
+
+export {selectOptions, deselectOptions}
diff --git a/src/tab.js b/src/tab.js
new file mode 100644
index 00000000..964dbffe
--- /dev/null
+++ b/src/tab.js
@@ -0,0 +1,118 @@
+import {fireEvent} from '@testing-library/dom'
+import {getActiveElement, FOCUSABLE_SELECTOR} from './utils'
+import {focus} from './focus'
+import {blur} from './blur'
+
+function tab({shift = false, focusTrap} = {}) {
+ const previousElement = getActiveElement(focusTrap?.ownerDocument ?? document)
+
+ if (!focusTrap) {
+ focusTrap = document
+ }
+
+ const focusableElements = focusTrap.querySelectorAll(FOCUSABLE_SELECTOR)
+
+ const enabledElements = [...focusableElements].filter(
+ el => el.getAttribute('tabindex') !== '-1' && !el.disabled,
+ )
+
+ if (enabledElements.length === 0) return
+
+ const orderedElements = enabledElements
+ .map((el, idx) => ({el, idx}))
+ .sort((a, b) => {
+ const tabIndexA = a.el.getAttribute('tabindex')
+ const tabIndexB = b.el.getAttribute('tabindex')
+
+ const diff = tabIndexA - tabIndexB
+
+ return diff === 0 ? a.idx - b.idx : diff
+ })
+ .map(({el}) => el)
+
+ if (shift) orderedElements.reverse()
+
+ // keep only the checked or first element in each radio group
+ const prunedElements = []
+ for (const el of orderedElements) {
+ if (el.type === 'radio' && el.name) {
+ const replacedIndex = prunedElements.findIndex(
+ ({name}) => name === el.name,
+ )
+
+ if (replacedIndex === -1) {
+ prunedElements.push(el)
+ } else if (el.checked) {
+ prunedElements.splice(replacedIndex, 1)
+ prunedElements.push(el)
+ }
+ } else {
+ prunedElements.push(el)
+ }
+ }
+
+ if (shift) prunedElements.reverse()
+
+ const index = prunedElements.findIndex(
+ el => el === el.ownerDocument.activeElement,
+ )
+
+ const nextIndex = shift ? index - 1 : index + 1
+ const defaultIndex = shift ? prunedElements.length - 1 : 0
+
+ const nextElement = prunedElements[nextIndex] || prunedElements[defaultIndex]
+
+ const shiftKeyInit = {
+ key: 'Shift',
+ keyCode: 16,
+ shiftKey: true,
+ }
+ const tabKeyInit = {
+ key: 'Tab',
+ keyCode: 9,
+ shiftKey: shift,
+ }
+
+ let continueToTab = true
+
+ // not sure how to make it so there's no previous element...
+ // istanbul ignore else
+ if (previousElement) {
+ // preventDefault on the shift key makes no difference
+ if (shift) fireEvent.keyDown(previousElement, {...shiftKeyInit})
+ continueToTab = fireEvent.keyDown(previousElement, {...tabKeyInit})
+ if (continueToTab) {
+ blur(previousElement)
+ }
+ }
+
+ const keyUpTarget =
+ !continueToTab && previousElement ? previousElement : nextElement
+
+ if (continueToTab) {
+ const hasTabIndex = nextElement.getAttribute('tabindex') !== null
+ if (!hasTabIndex) {
+ nextElement.setAttribute('tabindex', '0') // jsdom requires tabIndex=0 for an item to become 'document.activeElement'
+ }
+
+ focus(nextElement)
+
+ if (!hasTabIndex) {
+ nextElement.removeAttribute('tabindex') // leave no trace. :)
+ }
+ }
+
+ fireEvent.keyUp(keyUpTarget, {...tabKeyInit})
+
+ if (shift) {
+ fireEvent.keyUp(keyUpTarget, {...shiftKeyInit, shiftKey: false})
+ }
+}
+
+export {tab}
+
+/*
+eslint
+ complexity: "off",
+ max-statements: "off",
+*/
diff --git a/src/tick.js b/src/tick.js
deleted file mode 100644
index 126e39bc..00000000
--- a/src/tick.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/* istanbul ignore file */
-// the part of this file that we need tested is definitely being run
-// and the part that is not cannot easily have useful tests written
-// anyway. So we're just going to ignore coverage for this file
-// we're using this to ensure events are not fired synchronously one
-// after the other because that's not how things work when a user
-// fires them...
-/**
- * copied from React's enqueueTask.js
- */
-
-let didWarnAboutMessageChannel = false
-let enqueueTask
-try {
- // read require off the module object to get around the bundlers.
- // we don't want them to detect a require and bundle a Node polyfill.
- const requireString = `require${Math.random()}`.slice(0, 7)
- const nodeRequire = module && module[requireString]
- // assuming we're in node, let's try to get node's
- // version of setImmediate, bypassing fake timers if any.
- enqueueTask = nodeRequire.call(module, 'timers').setImmediate
-} catch (_err) {
- // we're in a browser
- // we can't use regular timers because they may still be faked
- // so we try MessageChannel+postMessage instead
- enqueueTask = callback => {
- const supportsMessageChannel = typeof MessageChannel === 'function'
- if (supportsMessageChannel) {
- const channel = new MessageChannel()
- channel.port1.onmessage = callback
- channel.port2.postMessage(undefined)
- } else if (didWarnAboutMessageChannel === false) {
- didWarnAboutMessageChannel = true
-
- // eslint-disable-next-line no-console
- console.error(
- 'This browser does not have a MessageChannel implementation, ' +
- 'so enqueuing tasks via await act(async () => ...) will fail. ' +
- 'Please file an issue at https://github.com/testing-library/user-event/issues ' +
- 'if you encounter this warning.',
- )
- }
- }
-}
-
-function tick() {
- return {
- then(resolve) {
- enqueueTask(resolve)
- },
- }
-}
-
-export {tick}
diff --git a/src/type.js b/src/type.js
index 0acadc3b..fbf26f98 100644
--- a/src/type.js
+++ b/src/type.js
@@ -1,42 +1,57 @@
+// TODO: wrap in asyncWrapper
import {
- getConfig as getDOMTestingLibraryConfig,
fireEvent,
+ getConfig as getDOMTestingLibraryConfig,
} from '@testing-library/dom'
-import {tick} from './tick'
+import {
+ getActiveElement,
+ calculateNewValue,
+ setSelectionRangeIfNecessary,
+} from './utils'
+import {click} from './click'
function wait(time) {
return new Promise(resolve => setTimeout(() => resolve(), time))
}
// this needs to be wrapped in the asyncWrapper for React's act and angular's change detection
-async function type(...args) {
- let result
- await getDOMTestingLibraryConfig().asyncWrapper(async () => {
- result = await typeImpl(...args)
- })
- return result
-}
-
-const getActiveElement = document => {
- const activeElement = document.activeElement
- if (activeElement.shadowRoot) {
- return getActiveElement(activeElement.shadowRoot) || activeElement
+// but only if it's actually going to be async.
+async function type(element, text, {delay = 0, ...options} = {}) {
+ // we do not want to wrap in the asyncWrapper if we're not
+ // going to actually be doing anything async, so we only wrap
+ // if the delay is greater than 0
+ if (delay > 0) {
+ let result
+ await getDOMTestingLibraryConfig().asyncWrapper(async () => {
+ result = await typeImpl(element, text, {delay, ...options})
+ })
+ return result
} else {
- return activeElement
+ return typeImpl(element, text, {delay, ...options})
}
}
-// eslint-disable-next-line complexity
async function typeImpl(
element,
text,
- {allAtOnce = false, delay, initialSelectionStart, initialSelectionEnd} = {},
+ {
+ delay,
+ skipClick = false,
+ skipAutoClose = false,
+ initialSelectionStart,
+ initialSelectionEnd,
+ },
) {
if (element.disabled) return
- element.focus()
+ if (!skipClick) click(element)
// The focused element could change between each event, so get the currently active element each time
+ // This is why most of the utilities are within the type function itself. If
+ // they weren't, then we'd have to pass the "currentElement" function to them
+ // as an argument, which would be fine, but make sure that you pass the function
+ // 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 setSelectionRange = ({newValue, newSelectionStart}) => {
@@ -46,11 +61,12 @@ async function typeImpl(
// 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)
+ if (currentValue() === newValue) {
+ setSelectionRangeIfNecessary(
+ currentElement(),
+ newSelectionStart,
+ newSelectionStart,
+ )
}
}
@@ -66,172 +82,61 @@ async function typeImpl(
currentElement().selectionStart === 0 &&
currentElement().selectionEnd === 0
) {
- currentElement().setSelectionRange(
- initialSelectionStart ?? currentValue()?.length ?? 0,
- initialSelectionEnd ?? currentValue()?.length ?? 0,
+ setSelectionRangeIfNecessary(
+ currentElement(),
+ initialSelectionStart ?? currentValue().length,
+ initialSelectionEnd ?? currentValue().length,
)
}
- if (allAtOnce) {
- if (!element.readOnly) {
- const {newValue, newSelectionStart} = calculateNewValue(text)
- fireEvent.input(element, {
- target: {value: newValue},
- })
- setSelectionRange({newValue, newSelectionStart})
- }
- } else {
- const eventCallbackMap = {
- ...modifier({
- name: 'shift',
- key: 'Shift',
- keyCode: 16,
- modifierProperty: 'shiftKey',
- }),
- ...modifier({
- name: 'ctrl',
- key: 'Control',
- keyCode: 17,
- modifierProperty: 'ctrlKey',
- }),
- ...modifier({
- name: 'alt',
- key: 'Alt',
- keyCode: 18,
- modifierProperty: 'altKey',
- }),
- ...modifier({
- name: 'meta',
- key: 'Meta',
- keyCode: 93,
- modifierProperty: 'metaKey',
- }),
- '{enter}': async ({eventOverrides}) => {
- const key = 'Enter'
- const keyCode = 13
-
- const keyDownDefaultNotPrevented = fireEvent.keyDown(currentElement(), {
- key,
- keyCode,
- which: keyCode,
- ...eventOverrides,
- })
-
- if (keyDownDefaultNotPrevented) {
- await tick()
-
- fireEvent.keyPress(currentElement(), {
- key,
- keyCode,
- charCode: keyCode,
- ...eventOverrides,
- })
- }
-
- if (currentElement().tagName === 'BUTTON') {
- await tick()
- fireEvent.click(currentElement(), {
- ...eventOverrides,
- })
- }
-
- if (currentElement().tagName === 'TEXTAREA') {
- await tick()
- const {newValue, newSelectionStart} = calculateNewValue('\n')
- fireEvent.input(currentElement(), {
- target: {value: newValue},
- inputType: 'insertLineBreak',
- ...eventOverrides,
- })
- setSelectionRange({newValue, newSelectionStart})
- }
-
- await tick()
-
- fireEvent.keyUp(currentElement(), {
- key,
- keyCode,
- which: keyCode,
- ...eventOverrides,
- })
- },
- '{esc}': async ({eventOverrides}) => {
- const key = 'Escape'
- const keyCode = 27
-
- fireEvent.keyDown(currentElement(), {
- key,
- keyCode,
- which: keyCode,
- ...eventOverrides,
- })
-
- await tick()
-
- // NOTE: Browsers do not fire a keypress on meta key presses
-
- fireEvent.keyUp(currentElement(), {
- key,
- keyCode,
- which: keyCode,
- ...eventOverrides,
- })
- },
- '{backspace}': async ({eventOverrides}) => {
- const key = 'Backspace'
- const keyCode = 8
-
- const keyPressDefaultNotPrevented = fireEvent.keyDown(
- currentElement(),
- {
- key,
- keyCode,
- which: keyCode,
- ...eventOverrides,
- },
- )
-
- if (keyPressDefaultNotPrevented) {
- await fireInputEventIfNeeded({
- ...calculateNewBackspaceValue(),
- eventOverrides: {
- inputType: 'deleteContentBackward',
- ...eventOverrides,
- },
- })
- }
+ const eventCallbackMap = getEventCallbackMap({
+ currentElement,
+ fireInputEventIfNeeded,
+ setSelectionRange,
+ })
- await tick()
+ const eventCallbacks = queueCallbacks()
+ await runCallbacks(eventCallbacks)
- fireEvent.keyUp(currentElement(), {
- key,
- keyCode,
- which: keyCode,
- ...eventOverrides,
- })
- },
- }
- const eventCallbacks = []
+ function queueCallbacks() {
+ const callbacks = []
+ const modifierClosers = []
let remainingString = text
while (remainingString) {
const eventKey = Object.keys(eventCallbackMap).find(key =>
remainingString.startsWith(key),
)
if (eventKey) {
- eventCallbacks.push(eventCallbackMap[eventKey])
+ const modifierCallback = eventCallbackMap[eventKey]
+ callbacks.push(modifierCallback)
+
+ // if this modifier has an associated "close" callback and the developer
+ // doesn't close it themselves, then we close it for them automatically
+ // Effectively if they send in: '{alt}a' then we type: '{alt}a{/alt}'
+ if (
+ !skipAutoClose &&
+ modifierCallback.close &&
+ !remainingString.includes(modifierCallback.close.name)
+ ) {
+ modifierClosers.push(modifierCallback.close.fn)
+ }
remainingString = remainingString.slice(eventKey.length)
} else {
const character = remainingString[0]
- eventCallbacks.push((...args) => typeCharacter(character, ...args))
+ callbacks.push((...args) => typeCharacter(character, ...args))
remainingString = remainingString.slice(1)
}
}
+ return [...callbacks, ...modifierClosers]
+ }
+
+ async function runCallbacks(callbacks) {
const eventOverrides = {}
let prevWasMinus, prevWasPeriod
- for (const callback of eventCallbacks) {
+ for (const callback of callbacks) {
if (delay > 0) await wait(delay)
if (!currentElement().disabled) {
- const returnValue = await callback({
+ const returnValue = callback({
prevWasMinus,
prevWasPeriod,
eventOverrides,
@@ -243,15 +148,13 @@ async function typeImpl(
}
}
- async function fireInputEventIfNeeded({
+ function fireInputEventIfNeeded({
newValue,
newSelectionStart,
eventOverrides,
}) {
const prevValue = currentValue()
if (!currentElement().readOnly && newValue !== prevValue) {
- await tick()
-
fireEvent.input(currentElement(), {
target: {value: newValue},
...eventOverrides,
@@ -263,88 +166,7 @@ async function typeImpl(
return {prevValue}
}
- // yes, calculateNewBackspaceValue and calculateNewValue look extremely similar
- // 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() {
- const {selectionStart, selectionEnd} = currentElement()
- const value = currentValue()
- let newValue, newSelectionStart
-
- 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
- } else if (selectionStart === value.length) {
- // at the end of the input
- newValue = value.slice(0, value.length - 1)
- newSelectionStart = selectionStart - 1
- } else {
- // in the middle of the input
- newValue =
- value.slice(0, selectionStart - 1) + value.slice(selectionEnd)
- newSelectionStart = selectionStart - 1
- }
- } else {
- // we have something selected
- const firstPart = value.slice(0, selectionStart)
- newValue = firstPart + value.slice(selectionEnd)
- newSelectionStart = firstPart.length
- }
-
- return {newValue, newSelectionStart}
- }
-
- function calculateNewValue(newEntry) {
- const {selectionStart, selectionEnd} = currentElement()
- // can't use .maxLength property because of a jsdom bug:
- // https://github.com/jsdom/jsdom/issues/2927
- const maxLength = Number(currentElement().getAttribute('maxlength') ?? -1)
- const value = currentValue()
- let newValue, newSelectionStart
-
- 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
- } else if (selectionStart === value.length) {
- // at the end of the input
- newValue = value + newEntry
- } else {
- // in the middle of the input
- newValue =
- value.slice(0, selectionStart) + newEntry + value.slice(selectionEnd)
- }
- newSelectionStart = selectionStart + newEntry.length
- } else {
- // we have something selected
- const firstPart = value.slice(0, selectionStart) + newEntry
- newValue = firstPart + value.slice(selectionEnd)
- newSelectionStart = firstPart.length
- }
-
- if (maxLength < 0) {
- return {newValue, newSelectionStart}
- } else {
- return {
- newValue: newValue.slice(0, maxLength),
- newSelectionStart:
- newSelectionStart > maxLength ? maxLength : newSelectionStart,
- }
- }
- }
-
- async function typeCharacter(
+ function typeCharacter(
char,
{prevWasMinus = false, prevWasPeriod = false, eventOverrides},
) {
@@ -360,8 +182,6 @@ async function typeImpl(
})
if (keyDownDefaultNotPrevented) {
- await tick()
-
const keyPressDefaultNotPrevented = fireEvent.keyPress(currentElement(), {
key,
keyCode,
@@ -377,8 +197,8 @@ async function typeImpl(
newEntry = `.${char}`
}
- const {prevValue} = await fireInputEventIfNeeded({
- ...calculateNewValue(newEntry),
+ const {prevValue} = fireInputEventIfNeeded({
+ ...calculateNewValue(newEntry, currentElement()),
eventOverrides: {
data: key,
inputType: 'insertText',
@@ -391,6 +211,7 @@ async function typeImpl(
// `-${newEntry}`
// we also preserve the prevWasMinus when the value is unchanged due
// to typing an invalid character (typing "-a3" results in "-3")
+ // same applies for the decimal character.
if (currentElement().type === 'number') {
const newValue = currentValue()
if (newValue === prevValue && newEntry !== '-') {
@@ -407,8 +228,6 @@ async function typeImpl(
}
}
- await tick()
-
fireEvent.keyUp(currentElement(), {
key,
keyCode,
@@ -418,35 +237,263 @@ async function typeImpl(
return {prevWasMinus: nextPrevWasMinus, prevWasPeriod: nextPrevWasPeriod}
}
+}
- function modifier({name, key, keyCode, modifierProperty}) {
- return {
- [`{${name}}`]: ({eventOverrides}) => {
- const newEventOverrides = {[modifierProperty]: true}
+// yes, calculateNewBackspaceValue and calculateNewValue look extremely similar
+// 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
+ let newValue, newSelectionStart
+
+ 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
+ newSelectionStart = selectionStart
+ } else if (selectionStart === value.length) {
+ // at the end of the input
+ newValue = value.slice(0, value.length - 1)
+ newSelectionStart = selectionStart - 1
+ } else {
+ // in the middle of the input
+ newValue = value.slice(0, selectionStart - 1) + value.slice(selectionEnd)
+ newSelectionStart = selectionStart - 1
+ }
+ } else {
+ // we have something selected
+ const firstPart = value.slice(0, selectionStart)
+ newValue = firstPart + value.slice(selectionEnd)
+ newSelectionStart = firstPart.length
+ }
+
+ return {newValue, newSelectionStart}
+}
+
+function calculateNewDeleteValue(element) {
+ const {selectionStart, selectionEnd, value} = element
+ let newValue
+
+ 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
+ } else if (selectionStart === selectionEnd) {
+ if (selectionStart === 0) {
+ // at the beginning of the input
+ newValue = value.slice(1)
+ } else if (selectionStart === value.length) {
+ // at the end of the input
+ newValue = value
+ } else {
+ // in the middle of the input
+ newValue = value.slice(0, selectionStart) + value.slice(selectionEnd + 1)
+ }
+ } else {
+ // we have something selected
+ const firstPart = value.slice(0, selectionStart)
+ newValue = firstPart + value.slice(selectionEnd)
+ }
+
+ return {newValue, newSelectionStart: selectionStart}
+}
- fireEvent.keyDown(currentElement(), {
+function getEventCallbackMap({
+ currentElement,
+ fireInputEventIfNeeded,
+ setSelectionRange,
+}) {
+ return {
+ ...modifier({
+ name: 'shift',
+ key: 'Shift',
+ keyCode: 16,
+ modifierProperty: 'shiftKey',
+ }),
+ ...modifier({
+ name: 'ctrl',
+ key: 'Control',
+ keyCode: 17,
+ modifierProperty: 'ctrlKey',
+ }),
+ ...modifier({
+ name: 'alt',
+ key: 'Alt',
+ keyCode: 18,
+ modifierProperty: 'altKey',
+ }),
+ ...modifier({
+ name: 'meta',
+ key: 'Meta',
+ keyCode: 93,
+ modifierProperty: 'metaKey',
+ }),
+ '{enter}': ({eventOverrides}) => {
+ const key = 'Enter'
+ const keyCode = 13
+
+ const keyDownDefaultNotPrevented = fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ if (keyDownDefaultNotPrevented) {
+ fireEvent.keyPress(currentElement(), {
key,
keyCode,
- which: keyCode,
+ charCode: keyCode,
...eventOverrides,
- ...newEventOverrides,
})
+ }
- return {eventOverrides: newEventOverrides}
- },
- [`{/${name}}`]: ({eventOverrides}) => {
- const newEventOverrides = {[modifierProperty]: false}
+ if (currentElement().tagName === 'BUTTON') {
+ fireEvent.click(currentElement(), {
+ ...eventOverrides,
+ })
+ }
- fireEvent.keyUp(currentElement(), {
- key,
- keyCode,
- which: keyCode,
+ if (currentElement().tagName === 'TEXTAREA') {
+ const {newValue, newSelectionStart} = calculateNewValue(
+ '\n',
+ currentElement(),
+ )
+ fireEvent.input(currentElement(), {
+ target: {value: newValue},
+ inputType: 'insertLineBreak',
...eventOverrides,
- ...newEventOverrides,
})
+ setSelectionRange({newValue, newSelectionStart})
+ }
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ },
+ '{esc}': ({eventOverrides}) => {
+ const key = 'Escape'
+ const keyCode = 27
+
+ fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ // NOTE: Browsers do not fire a keypress on meta key presses
- return {eventOverrides: newEventOverrides}
- },
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ },
+ '{del}': ({eventOverrides}) => {
+ const key = 'Delete'
+ const keyCode = 46
+
+ const keyPressDefaultNotPrevented = fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ if (keyPressDefaultNotPrevented) {
+ fireInputEventIfNeeded({
+ ...calculateNewDeleteValue(currentElement()),
+ eventOverrides: {
+ inputType: 'deleteContentForward',
+ ...eventOverrides,
+ },
+ })
+ }
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ },
+ '{backspace}': ({eventOverrides}) => {
+ const key = 'Backspace'
+ const keyCode = 8
+
+ const keyPressDefaultNotPrevented = fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+
+ if (keyPressDefaultNotPrevented) {
+ fireInputEventIfNeeded({
+ ...calculateNewBackspaceValue(currentElement()),
+ eventOverrides: {
+ inputType: 'deleteContentBackward',
+ ...eventOverrides,
+ },
+ })
+ }
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ })
+ },
+ // 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)
+ },
+ }
+
+ function modifier({name, key, keyCode, modifierProperty}) {
+ function open({eventOverrides}) {
+ const newEventOverrides = {[modifierProperty]: true}
+
+ fireEvent.keyDown(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ ...newEventOverrides,
+ })
+
+ return {eventOverrides: newEventOverrides}
+ }
+ open.close = {name: [`{/${name}}`], fn: close}
+ function close({eventOverrides}) {
+ const newEventOverrides = {[modifierProperty]: false}
+
+ fireEvent.keyUp(currentElement(), {
+ key,
+ keyCode,
+ which: keyCode,
+ ...eventOverrides,
+ ...newEventOverrides,
+ })
+
+ return {eventOverrides: newEventOverrides}
+ }
+ return {
+ [`{${name}}`]: open,
+ [`{/${name}}`]: close,
}
}
}
@@ -455,7 +502,6 @@ export {type}
/*
eslint
- no-await-in-loop: "off",
no-loop-func: "off",
max-lines-per-function: "off",
*/
diff --git a/src/upload.js b/src/upload.js
new file mode 100644
index 00000000..4cecbba9
--- /dev/null
+++ b/src/upload.js
@@ -0,0 +1,51 @@
+import {fireEvent, createEvent} from '@testing-library/dom'
+import {click} from './click'
+import {blur} from './blur'
+import {focus} from './focus'
+
+function upload(element, fileOrFiles, init) {
+ if (element.disabled) return
+
+ let files
+ let input = element
+
+ click(element, init)
+ if (element.tagName === 'LABEL') {
+ files = element.control.multiple ? fileOrFiles : [fileOrFiles]
+ input = element.control
+ } else {
+ files = element.multiple ? fileOrFiles : [fileOrFiles]
+ }
+
+ // blur fires when the file selector pops up
+ blur(element, init)
+ // focus fires when they make their selection
+ focus(element, init)
+
+ // the event fired in the browser isn't actually an "input" or "change" event
+ // but a new Event with a type set to "input" and "change"
+ // Kinda odd...
+ const inputFiles = {
+ length: files.length,
+ item: index => files[index],
+ ...files,
+ }
+
+ fireEvent(
+ input,
+ createEvent('input', input, {
+ target: {files: inputFiles},
+ ...init,
+ }),
+ )
+
+ fireEvent(
+ input,
+ createEvent('change', input, {
+ target: {files: inputFiles},
+ ...init,
+ }),
+ )
+}
+
+export {upload}
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 00000000..d38745a8
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,179 @@
+function isMousePressEvent(event) {
+ return (
+ event === 'mousedown' ||
+ event === 'mouseup' ||
+ event === 'click' ||
+ event === 'dblclick'
+ )
+}
+
+function invert(map) {
+ const res = {}
+ for (const key of Object.keys(map)) {
+ res[map[key]] = key
+ }
+
+ return res
+}
+
+// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
+const BUTTONS_TO_NAMES = {
+ 0: 'none',
+ 1: 'primary',
+ 2: 'secondary',
+ 4: 'auxiliary',
+}
+const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES)
+
+// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+const BUTTON_TO_NAMES = {
+ 0: 'primary',
+ 1: 'auxiliary',
+ 2: 'secondary',
+}
+
+const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES)
+
+function convertMouseButtons(event, init, property, mapping) {
+ if (!isMousePressEvent(event)) {
+ return 0
+ }
+
+ if (init[property] != null) {
+ return init[property]
+ }
+
+ if (init.buttons != null) {
+ // not sure how to test this. Feel free to try and add a test if you want.
+ // istanbul ignore next
+ return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0
+ }
+
+ if (init.button != null) {
+ // not sure how to test this. Feel free to try and add a test if you want.
+ // istanbul ignore next
+ return mapping[BUTTON_TO_NAMES[init.button]] || 0
+ }
+
+ return property != 'button' && isMousePressEvent(event) ? 1 : 0
+}
+
+function getMouseEventOptions(event, init, clickCount = 0) {
+ init = init || {}
+ return {
+ ...init,
+ // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
+ detail:
+ event === 'mousedown' || event === 'mouseup' || event === 'click'
+ ? 1 + clickCount
+ : clickCount,
+ buttons: convertMouseButtons(event, init, 'buttons', NAMES_TO_BUTTONS),
+ button: convertMouseButtons(event, init, 'button', NAMES_TO_BUTTON),
+ }
+}
+
+// Absolutely NO events fire on label elements that contain their control
+// if that control is disabled. NUTS!
+// no joke. There are NO events for: