Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(type): handle selectionRange properly #313

Merged
merged 1 commit into from Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -57,7 +57,8 @@
"rules": {
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/tabindex-no-positive": "off",
"no-return-assign": "off"
"no-return-assign": "off",
"react/prop-types": "off"
},
"overrides": [
{
Expand Down
7 changes: 4 additions & 3 deletions src/__tests__/helpers/utils.js
Expand Up @@ -43,8 +43,8 @@ function setup(ui) {
} = render(ui)
element.previousTestData = getTestData(element)

const getEventCalls = addListeners(element)
return {element, getEventCalls}
const {getEventCalls, clearEventCalls} = addListeners(element)
return {element, getEventCalls, clearEventCalls}
}

function addListeners(element) {
Expand Down Expand Up @@ -102,7 +102,8 @@ function addListeners(element) {
})
.join('\n')
}
return getEventCalls
const clearEventCalls = () => generalListener.mockClear()
return {getEventCalls, clearEventCalls}
}

// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/toggleselectoptions.js
Expand Up @@ -46,7 +46,7 @@ test('should fire the correct events for multiple select when focus is in other

const $otherBtn = screen.getByRole('button')

const getButtonEvents = addListeners($otherBtn)
const {getEventCalls: getButtonEvents} = addListeners($otherBtn)

$otherBtn.focus()

Expand Down
58 changes: 51 additions & 7 deletions src/__tests__/type-modifiers.js
Expand Up @@ -28,9 +28,37 @@ test('{esc} triggers typing the escape character', async () => {
`)
})

test('a{backspace}', async () => {
const {element, getEventCalls} = setup(<input />)
await userEvent.type(element, 'a{backspace}')
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: a (97)
keypress: a (97)
input: "{CURSOR}" -> "a"
keyup: a (97)
keydown: Backspace (8)
input: "a{CURSOR}" -> ""
keyup: Backspace (8)
`)
})

test('{backspace}a', async () => {
const {element, getEventCalls} = setup(<input />)
await userEvent.type(element, '{backspace}a')
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: Backspace (8)
keyup: Backspace (8)
keydown: a (97)
keypress: a (97)
input: "{CURSOR}" -> "a"
keyup: a (97)
`)
})

test('{backspace} triggers typing the backspace character and deletes the character behind the cursor', async () => {
const {element: input, getEventCalls} = setup(<input />)
input.value = 'yo'
const {element: input, getEventCalls} = setup(<input defaultValue="yo" />)
input.setSelectionRange(1, 1)

await userEvent.type(input, '{backspace}')
Expand All @@ -44,9 +72,24 @@ test('{backspace} triggers typing the backspace character and deletes the charac
})

test('{backspace} on a readOnly input', async () => {
const {element: input, getEventCalls} = setup(<input />)
input.readOnly = true
input.value = 'yo'
const {element: input, getEventCalls} = setup(
<input readOnly defaultValue="yo" />,
)
input.setSelectionRange(1, 1)

await userEvent.type(input, '{backspace}')

expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: Backspace (8)
keyup: Backspace (8)
`)
})

test('{backspace} does not fire input if keydown prevents default', async () => {
const {element: input, getEventCalls} = setup(
<input defaultValue="yo" onKeyDown={e => e.preventDefault()} />,
)
input.setSelectionRange(1, 1)

await userEvent.type(input, '{backspace}')
Expand All @@ -59,8 +102,9 @@ test('{backspace} on a readOnly input', async () => {
})

test('{backspace} deletes the selected range', async () => {
const {element: input, getEventCalls} = setup(<input />)
input.value = 'Hi there'
const {element: input, getEventCalls} = setup(
<input defaultValue="Hi there" />,
)
input.setSelectionRange(1, 5)

await userEvent.type(input, '{backspace}')
Expand Down
99 changes: 99 additions & 0 deletions src/__tests__/type.js
Expand Up @@ -52,6 +52,31 @@ test('should append text all at once', async () => {
`)
})

test('does not fire input event when keypress calls prevent default', async () => {
const {element, getEventCalls} = setup(
<input onKeyPress={e => e.preventDefault()} />,
)
await userEvent.type(element, 'a')
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: a (97)
keypress: a (97)
keyup: a (97)
`)
})

test('does not fire keypress or input events when keydown calls prevent default', async () => {
const {element, getEventCalls} = setup(
<input onKeyDown={e => e.preventDefault()} />,
)
await userEvent.type(element, 'a')
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: a (97)
keyup: a (97)
`)
})

// TODO: Let's migrate these tests to use the setup util
test('should not type when event.preventDefault() is called', async () => {
const onChange = jest.fn()
Expand Down Expand Up @@ -396,3 +421,77 @@ test('does not continue firing events when disabled during typing', async () =>
await userEvent.type(input, 'hi there')
expect(input).toHaveValue('h')
})

function DollarInput({initialValue = ''}) {
const [value, setValue] = React.useState(initialValue)
function handleChange(event) {
const val = event.target.value
const withoutDollar = val.replace(/\$/g, '')
const num = Number(withoutDollar)
if (Number.isNaN(num)) return
setValue(`$${withoutDollar}`)
}
return <input value={value} type="text" onChange={handleChange} />
}

test('typing into a controlled input works', async () => {
const {element, getEventCalls} = setup(<DollarInput />)
await userEvent.type(element, '23')
expect(element.value).toBe('$23')
expect(getEventCalls()).toMatchInlineSnapshot(`
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)
`)
})

test('typing in the middle of a controlled input works', async () => {
const {element, getEventCalls} = setup(<DollarInput initialValue="$23" />)
element.setSelectionRange(2, 2)

await userEvent.type(element, '1')

expect(element.value).toBe('$213')
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: 1 (49)
keypress: 1 (49)
input: "$2{CURSOR}3" -> "$213"
keyup: 1 (49)
`)
})

test('ignored {backspace} in controlled input', async () => {
const {element, getEventCalls} = setup(<DollarInput initialValue="$23" />)
element.setSelectionRange(1, 1)

await 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
// before. When the value is set programmatically to something different
// from what was expected based on the input event, the browser sets
// 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')

expect(element.value).toBe('$234')
// the backslash in the inline snapshot is to escape the $ before {CURSOR}
expect(getEventCalls()).toMatchInlineSnapshot(`
focus
keydown: Backspace (8)
input: "\${CURSOR}23" -> "23"
keyup: Backspace (8)
keydown: 4 (52)
keypress: 4 (52)
input: "$23{CURSOR}" -> "$234"
keyup: 4 (52)
`)
})