Table of contents
1.
Introduction
2.
Difference Between user-event and fireEvent
3.
Installation and Setup
4.
API
4.1.
click
4.2.
dblClick
4.3.
type
4.4.
keyboard
4.5.
upload
4.6.
clear
4.7.
selectOptions
4.8.
deselectOptions
4.9.
tab
4.10.
hover and unhover
4.11.
paste
5.
FAQs
6.
Key Takeaways
Last Updated: Mar 27, 2024
Easy

User Event API Version 13

Author Toohina Barua
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

user-event is a Testing Library companion library that simulates user interactions by dispatching events that would occur if the interaction occurred in a browser. While most user-event examples are for React, the library can be used with any framework with a DOM. From this article, we will learn about user-event v13.

Difference Between user-event and fireEvent

The built-in fireEvent utility makes dispatching events a lot easier. It sends out precisely the events you tell it to, and only those - even if those exact events have never been sent out in a real browser interaction.
On the other hand, user-event dispatches the events as if a user had interacted with the document. That may result in the same events you previously sent via fireEvent directly, but it also has the potential to catch bugs that prevent users from triggering said events.
This is why you should use user-event to test component interaction.

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

  1. 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.
  2. The click will trigger hover events before clicking. How do we disable this?
    Set the skipHover option to true to disable this.
  3. How to install user-event API v13 using Yarn?
    Write the following Yarn command:
    yarn add --dev @testing-library/user-event @testing-library/dom
  4. 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.
  5. 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!

Live masterclass