-
Notifications
You must be signed in to change notification settings - Fork 469
feat(userEvent): Add paste API (fixes #640) #645
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
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
990917a
feat(paste): Extract paste event from type event
nickserv 195cf53
fix(userEvent): make test more consistent
nickserv 05fa679
fix(userEvent): use fireEvent from utils module
nickserv 9b1ed3b
chore(userEvent): extract duplicate functions
nickserv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import {userEvent} from '../../' | ||
import {setup} from './helpers/utils' | ||
|
||
test('should paste text in input', async () => { | ||
const {element, getEventSnapshot} = setup('<input />') | ||
|
||
const text = 'Hello, world!' | ||
await userEvent.paste(element, text) | ||
expect(element).toHaveValue(text) | ||
expect(getEventSnapshot()).toMatchInlineSnapshot(` | ||
Events fired on: input[value="Hello, world!"] | ||
|
||
input[value=""] - focus | ||
input[value=""] - select | ||
input[value="Hello, world!"] - input | ||
"{CURSOR}" -> "Hello, world!{CURSOR}" | ||
input[value="Hello, world!"] - select | ||
`) | ||
}) | ||
|
||
test('should paste text in textarea', async () => { | ||
const {element, getEventSnapshot} = setup('<textarea />') | ||
|
||
const text = 'Hello, world!' | ||
await userEvent.paste(element, text) | ||
expect(element).toHaveValue(text) | ||
expect(getEventSnapshot()).toMatchInlineSnapshot(` | ||
Events fired on: textarea[value="Hello, world!"] | ||
|
||
textarea[value=""] - focus | ||
textarea[value=""] - select | ||
textarea[value="Hello, world!"] - input | ||
"{CURSOR}" -> "Hello, world!{CURSOR}" | ||
textarea[value="Hello, world!"] - select | ||
`) | ||
}) | ||
|
||
test('does not paste when readOnly', async () => { | ||
const {element, getEventSnapshot} = setup('<input readonly />') | ||
|
||
await userEvent.paste(element, 'hi') | ||
expect(getEventSnapshot()).toMatchInlineSnapshot(` | ||
Events fired on: input[value=""] | ||
|
||
input[value=""] - focus | ||
input[value=""] - select | ||
`) | ||
}) | ||
|
||
test('does not paste when disabled', async () => { | ||
const {element, getEventSnapshot} = setup('<input disabled />') | ||
|
||
await userEvent.paste(element, 'hi') | ||
expect(getEventSnapshot()).toMatchInlineSnapshot( | ||
`No events were fired on: input[value=""]`, | ||
) | ||
}) | ||
|
||
test.each(['input', 'textarea'])( | ||
'should paste text in <%s> up to maxLength if provided', | ||
async type => { | ||
const {element} = setup(`<${type} maxlength="10" />`) | ||
|
||
await userEvent.type(element, 'superlongtext') | ||
expect(element).toHaveValue('superlongt') | ||
|
||
element.value = '' | ||
await userEvent.paste(element, 'superlongtext') | ||
expect(element).toHaveValue('superlongt') | ||
}, | ||
) | ||
|
||
test.each(['input', 'textarea'])( | ||
'should append text in <%s> up to maxLength if provided', | ||
async type => { | ||
const {element} = setup(`<${type} maxlength="10" />`) | ||
|
||
await userEvent.type(element, 'superlong') | ||
await userEvent.type(element, 'text') | ||
expect(element).toHaveValue('superlongt') | ||
|
||
element.value = '' | ||
await userEvent.paste(element, 'superlongtext') | ||
expect(element).toHaveValue('superlongt') | ||
}, | ||
) | ||
|
||
test('should replace selected text all at once', async () => { | ||
const {element} = setup('<input value="hello world" />') | ||
|
||
const selectionStart = 'hello world'.search('world') | ||
const selectionEnd = selectionStart + 'world'.length | ||
element.setSelectionRange(selectionStart, selectionEnd) | ||
await userEvent.paste(element, 'friend') | ||
expect(element).toHaveValue('hello friend') | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import {getConfig as getDOMTestingLibraryConfig} from '../config' | ||
import {fireEvent, getActiveElement, calculateNewValue} from './utils' | ||
|
||
// this needs to be wrapped in the asyncWrapper for React's act and angular's change detection | ||
async function paste(...args) { | ||
let result | ||
await getDOMTestingLibraryConfig().asyncWrapper(async () => { | ||
result = await pasteImpl(...args) | ||
}) | ||
return result | ||
} | ||
nickserv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// eslint-disable-next-line complexity | ||
async function pasteImpl( | ||
element, | ||
text, | ||
{initialSelectionStart, initialSelectionEnd} = {}, | ||
) { | ||
if (element.disabled) return | ||
|
||
element.focus() | ||
|
||
// The focused element could change between each event, so get the currently active element each time | ||
const currentElement = () => getActiveElement(element.ownerDocument) | ||
const currentValue = () => currentElement().value | ||
const setSelectionRange = ({newValue, newSelectionStart}) => { | ||
// if we *can* change the selection start, then we will if the new value | ||
// is the same as the current value (so it wasn't programatically changed | ||
// when the fireEvent.input was triggered). | ||
// The reason we have to do this at all is because it actually *is* | ||
// programmatically changed by fireEvent.input, so we have to simulate the | ||
// browser's default behavior | ||
if ( | ||
currentElement().selectionStart !== null && | ||
currentValue() === newValue | ||
) { | ||
currentElement().setSelectionRange?.(newSelectionStart, newSelectionStart) | ||
} | ||
} | ||
|
||
// by default, a new element has it's selection start and end at 0 | ||
// but most of the time when people call "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 ( | ||
currentElement().selectionStart === 0 && | ||
currentElement().selectionEnd === 0 | ||
) { | ||
currentElement().setSelectionRange( | ||
initialSelectionStart ?? currentValue()?.length ?? 0, | ||
initialSelectionEnd ?? currentValue()?.length ?? 0, | ||
) | ||
} | ||
|
||
if (!element.readOnly) { | ||
const {newValue, newSelectionStart} = calculateNewValue( | ||
text, | ||
currentElement(), | ||
) | ||
await fireEvent.input(element, { | ||
target: {value: newValue}, | ||
}) | ||
setSelectionRange({newValue, newSelectionStart}) | ||
} | ||
} | ||
|
||
export {paste} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.