Installation and Setup
Run the following npm command in the terminal to install user-event:
npm install --save-dev @testing-library/user-event @testing-library/dom
Now simply import it into your tests:
import userEvent from '@testing-library/user-event'
Or
const {default: userEvent} = require('@testing-library/user-event')
Import the libraries that are needed:
import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
API
click
Calling click() on an element can have different side effects depending on which element is clicked.
test('click', () => {
render(
<div>
<label htmlFor="checkbox">Check</label>
<input id="checkbox" type="checkbox" />
</div>,
)
userEvent.click(screen.getByText('Check'))
expect(screen.getByLabelText('Check')).toBeChecked()
})
click() takes three arguments like so: click(element, eventInit, options).
You can also ctrlClick / shiftClick etc with
userEvent.click(elem, {ctrlKey: true, shiftKey: true})
Let’s look at the pointer event options. It will throw an error if you try to click an element with pointer-events set to "none" (i.e. unclickable). Set skipPointerEventsCheck to true if you want to disable this behavior:
userEvent.click(elem, undefined, {skipPointerEventsCheck: true})
dblClick
dblClick clicks on an element twice, with different side effects depending on which element it is. In dblClick(element, eventInit, options) options includes pointer events options.
test('double click', () => {
const onChange = jest.fn()
render(<input type="checkbox" onChange={onChange} />)
const checkbox = screen.getByRole('checkbox')
userEvent.dblClick(checkbox)
expect(onChange).toHaveBeenCalledTimes(2)
expect(checkbox).not.toBeChecked()
})
type
The type(element, text, [options]) inserts text into a textarea or an input.
test('type', () => {
render(<textarea />)
userEvent.type(screen.getByRole('textbox'), 'Hello,{enter}World!')
expect(screen.getByRole('textbox')).toHaveValue('Hello,\nWorld!')
})
{enter} will insert a newline character (<textarea /> only).
The number of milliseconds that pass between two typed characters is called options.delay. It's set to 0 by default. If your component has different behavior for fast and slow users, you can use this option. If you do this, make sure to be patient!
Before typing, the type will click the element. Set the skipClick option to true to disable this.
An example of an usage with a selection range:
test('delete characters within the selectedRange', () => {
render(
<div>
<label htmlFor="my-input">Example:</label>
<input id="my-input" type="text" value="This is a bad example" />
</div>,
)
const input = screen.getByLabelText(/example/i)
input.setSelectionRange(10, 13)
userEvent.type(input, '{backspace}good')
expect(input).toHaveValue('This is a good example')
{backspace} will delete the previous character (or the characters within the selectedRange).
Type, by default, adds to the existing text. Set the element's selection range to zero and provide the initialSelectionStart and initialSelectionEnd options to prepend text:
test('prepend text', () => {
render(<input defaultValue="World!"/>)
const element = screen.getByRole('textbox')
// Prepend text
element.setSelectionRange(0, 0)
userEvent.type(element, 'Hello, ', {
initialSelectionStart: 0,
initialSelectionEnd: 0,
})
expect(element).toHaveValue('Hello, World!')
})
<input type="time" /> support:
The following is an example of usage of this library with <input type="time" />
test('types into the input', () => {
render(
<>
<label for="time">Enter a time</label>
<input type="time" id="time" />
</>,
)
const input = screen.getByLabelText(/enter a time/i)
userEvent.type(input, '13:58')
expect(input.value).toBe('13:58')
})
keyboard
We can simulate the keyboard events described by text with keyboard(text, options). This works in the same way as userEvent.type(), but without the need to click or change the selection range.
If you just want to simulate pressing buttons on the keyboard, use userEvent.keyboard. If you just want to quickly insert text into an input field or textarea, userEvent.type is the way.
Keystrokes can be described:
Per printable character
userEvent.keyboard('foo') // translates to: f, o, o
The brackets { and [ are special characters that can be doubled to refer to them.
userEvent.keyboard('{{a[[') // translates to: {, a, [
Per KeyboardEvent.key (only supports alphanumeric values of key)
userEvent.keyboard('{Shift}{f}{o}{o}') // translates to: Shift, f, o, o
This prevents any key from being pressed. As a result, before pressing f, Shift will be lifted.
Per KeyboardEvent.code
userEvent.keyboard('[ShiftLeft][KeyF][KeyO][KeyO]') // translates to: Shift, f, o, o
userEvent.type modifier/specialChar is a legacy userEvent.type modifier/specialChar. Like before, modifiers like {shift} (note the lowercase) will be kept pressed by default. By appending a / to the end of the descriptor, you can stop it from happening.
userEvent.keyboard('{shift}{ctrl/}a{/shift}') // translates to: Shift(down), Control(down+up), a, Shift(up)
Adding a > to the end of the descriptor keeps the keys pressed, and adding a / to the beginning of the descriptor lifts them:
userEvent.keyboard('{Shift>}A{/Shift}') // translates to: Shift(down), A, Shift(up)
The keyboard state returned by userEvent.keyboard can be used to continue keyboard operations.
const keyboardState = userEvent.keyboard('[ControlLeft>]') // keydown [ControlLeft]
// ... inspect some changes ...
userEvent.keyboard('a', {keyboardState}) // press [KeyA] with active ctrlKey modifier
A default key map depicting a "default" US-keyboard performs the key-to-code mapping. As an option, you can provide your own local keyboard mapping.
userEvent.keyboard('?', {keyboardMap: myOwnLocaleKeyboardMap})
upload
upload(element, file, [clickInit, changeInit], [options]) is a function that uploads a file to an input. Use input> with the multiple attribute and the second upload argument as an array to upload multiple files. A third argument can also be used to initiate a click or change event.
Files that don't match will be discarded if options.applyAccept is set to true and the element has an accept attribute.
test('upload file', () => {
const file = new File(['hello'], 'hello.png', {type: 'image/png'})
render(
<div>
<label htmlFor="file-uploader">Upload file:</label>
<input id="file-uploader" type="file" />
</div>,
)
const input = screen.getByLabelText(/upload file/i)
userEvent.upload(input, file)
expect(input.files[0]).toStrictEqual(file)
expect(input.files.item(0)).toStrictEqual(file)
expect(input.files).toHaveLength(1)
})
test('upload multiple files', () => {
const files = [
new File(['hello'], 'hello.png', {type: 'image/png'}),
new File(['there'], 'there.png', {type: 'image/png'}),
]
render(
<div>
<label htmlFor="file-uploader">Upload file:</label>
<input id="file-uploader" type="file" multiple />
</div>,
)
const input = screen.getByLabelText(/upload file/i)
userEvent.upload(input, files)
expect(input.files).toHaveLength(2)
expect(input.files[0]).toStrictEqual(files[0])
expect(input.files[1]).toStrictEqual(files[1])
})
clear
clear(element) selects and deletes the text in an <input> or <textarea>.
test('clear', () => {
render(<textarea defaultValue="Hello, World!" />)
userEvent.clear(screen.getByRole('textbox'))
expect(screen.getByRole('textbox')).toHaveValue('')
})
selectOptions
selectOptions(element, values, options) selects a select or select multiple element's specified option(s).
test('selectOptions', () => {
render(
<select multiple>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>,
)
userEvent.selectOptions(screen.getByRole('listbox'), ['1', '3'])
expect(screen.getByRole('option', {name: 'A'}).selected).toBe(true)
expect(screen.getByRole('option', {name: 'B'}).selected).toBe(false)
expect(screen.getByRole('option', {name: 'C'}).selected).toBe(true)
})
An array of values or a single scalar value can be used as the values parameter.
Option nodes are also accepted:
userEvent.selectOptions(screen.getByTestId('select-multiple'), [
screen.getByText('A'),
screen.getByText('B'),
])
options includes Pointer events options.
deselectOptions
deselectOptions(element, values, options) removes a select multiple element's selection for the specified option(s).
test('deselectOptions', () => {
render(
<select multiple>
<option value="1">A</option>
<option value="2">B</option>
<option value="3">C</option>
</select>,
)
userEvent.selectOptions(screen.getByRole('listbox'), '2')
expect(screen.getByText('B').selected).toBe(true)
userEvent.deselectOptions(screen.getByRole('listbox'), '2')
expect(screen.getByText('B').selected).toBe(false)
// can do multiple at once as well:
// userEvent.deselectOptions(screen.getByRole('listbox'), ['1', '2'])
})
An array of values or a single scalar value can be used as the values parameter.
tab
Changes the document by triggering a tab event. In the same way that the browser does, activeElement is used.
The options are as follows:
- To invert tab direction, set shift (default false) to true or false.
- focusTrap (default document) is a container element that prevents tabbing within the document.
Import jest-dom like so:
import '@testing-library/jest-dom'
Following is an example of using tab:
it('should cycle elements in document tab order', () => {
render(
<div>
<input data-testid="element" type="checkbox" />
<input data-testid="element" type="radio" />
<input data-testid="element" type="number" />
</div>,
)
const [checkbox, radio, number] = screen.getAllByTestId('element')
expect(document.body).toHaveFocus()
userEvent.tab()
expect(checkbox).toHaveFocus()
userEvent.tab()
expect(radio).toHaveFocus()
userEvent.tab()
expect(number).toHaveFocus()
userEvent.tab()
// cycle goes back to the body element
expect(document.body).toHaveFocus()
userEvent.tab()
expect(checkbox).toHaveFocus()
})
hover and unhover
hover(element, options) hovers over the element. unhover(element, options) unhovers out of the element.
Import tooltip like so:
import Tooltip from '../tooltip'
Now when we hover over text then a tooltip will be displayed. The tooltip will disappear when we unhover the text. Write the following code for this:
test('hover', () => {
const messageText = 'Hello'
render(
<Tooltip messageText={messageText}>
<TrashIcon aria-label="Delete" />
</Tooltip>,
)
userEvent.hover(screen.getByLabelText(/delete/i))
expect(screen.getByText(messageText)).toBeInTheDocument()
userEvent.unhover(screen.getByLabelText(/delete/i))
expect(screen.queryByText(messageText)).not.toBeInTheDocument()
})
paste
You can simulate a user pasting text into an input using paste(element, text, eventInit, options).
test('should paste text in input', () => {
render(<MyInput />)
const text = 'Hello, world!'
const element = getByRole('textbox', {name: /paste your greeting/i})
userEvent.paste(element, text)
expect(element).toHaveValue(text)
})
If what you're pasting should have clipboardData, you can use eventInit (like files).
FAQs
-
What is the fireEvent method?
It sends out exactly the events you tell it to, and only those - even if those exact events have never been sent out in a real browser interaction.
-
The click will trigger hover events before clicking. How do we disable this?
Set the skipHover option to true to disable this.
-
How to install user-event API v13 using Yarn?
Write the following Yarn command:
yarn add --dev @testing-library/user-event @testing-library/dom
-
Why is focusTrap option available to us?
Because jsdom does not support tabbing, this feature allows tests to verify tabbing from the end user's perspective. Components like focus-trap-react, on the other hand, will not work with userEvent.tab() or jsdom due to this limitation in jsdom. As a result, the focusTrap option allows you to ensure that your user is trapped within a focus-trap.
-
When should use userEvent.keyboard instead of userEvent.type?
If you just want to simulate pressing buttons on the keyboard, use userEvent.keyboard. If you just want to quickly insert text into an input field or textarea, userEvent.type is the way to go.
Key Takeaways
In this article, we have extensively discussed the theoretical and practical implementation of user-event API version 13.
We hope that this blog has helped you enhance your knowledge regarding user-event API v13 and if you would like to learn more, check out our articles on Coding Ninjas Studio. Do upvote our blog to help other ninjas grow. Happy Coding!