README.md 53 KB

@testing-library/user-event

dog

Fire events the same way the user does


Build Status Code Coverage version downloads MIT License All Contributors PRs Welcome Code of Conduct

The problem

From testing-library/dom-testing-library#107:

[...] it is becoming apparent the need to express user actions on a web page using a higher-level abstraction than fireEvent

The solution

user-event tries to simulate the real events that would happen in the browser as the user interacts with it. For example userEvent.click(checkbox) would change the state of the checkbox.

The library is still a work in progress and any help is appreciated.

Table of Contents

Installation

With NPM:

npm install @testing-library/user-event @testing-library/dom --save-dev

With Yarn:

yarn add @testing-library/user-event @testing-library/dom --dev

Now simply import it in your tests:

import userEvent from '@testing-library/user-event'

// or

const {default: userEvent} = require('@testing-library/user-event')

API

Note: All userEvent methods are synchronous with one exception: when delay with userEvent.type as described below). We also discourage using userEvent inside before/after blocks at all, for important reasons described in "Avoid Nesting When You're Testing".

click(element, eventInit, options)

Clicks element, depending on what element is it can have different side effects.

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

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()
})

You can also ctrlClick / shiftClick etc with

userEvent.click(elem, {ctrlKey: true, shiftKey: true})

See the MouseEvent constructor documentation for more options.

Note that click will trigger hover events before clicking. To disable this, set the skipHover option to true.

dblClick(element, eventInit, options)

Clicks element twice, depending on what element is it can have different side effects.

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

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(element, text, [options])

Writes text inside an <input> or a <textarea>.

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('type', () => {
  render(<textarea />)

  userEvent.type(screen.getByRole('textbox'), 'Hello,{enter}World!')
  expect(screen.getByRole('textbox')).toHaveValue('Hello,\nWorld!')
})

options.delay is the number of milliseconds that pass between two characters are typed. By default it's 0. You can use this option if your component has a different behavior for fast or slow users. If you do this, you need to make sure to await!

To be clear, userEvent.type always returns a promise, but you only need to await the promise it returns if you're using the delay option. Otherwise everything runs synchronously and you can ignore the promise.

type will click the element before typing. To disable this, set the skipClick option to true.

Special characters

The following special character strings are supported:

Text string Key Modifier Notes
{enter} Enter N/A Will insert a newline character (<textarea /> only).
{space} ' ' N/A
{esc} Escape N/A
{backspace} Backspace N/A Will delete the previous character (or the characters within the selectedRange, see example below).
{del} Delete N/A Will delete the next character (or the characters within the selectedRange, see example below)
{selectall} N/A N/A Selects all the text of the element. Note that this will only work for elements that support selection ranges (so, not email, password, number, among others)
{arrowleft} ArrowLeft N/A
{arrowright} ArrowRight N/A
{arrowup} ArrowUp N/A
{arrowdown} ArrowDown N/A
{home} Home N/A
{end} End N/A
{shift} Shift shiftKey Does not capitalize following characters.
{ctrl} Control ctrlKey
{alt} Alt altKey
{meta} OS metaKey
{capslock} CapsLock modifierCapsLock Fires both keydown and keyup when used (simulates a user clicking their "Caps Lock" button to enable caps lock).

A note about modifiers: Modifier keys ({shift}, {ctrl}, {alt}, {meta}) will activate their corresponding event modifiers for the duration of type command or until they are closed (via {/shift}, {/ctrl}, etc.). If they are not closed explicitly, then events will be fired to close them automatically (to disable this, set the skipAutoClose option to true).

We take the same stance as Cypress in that we do not simulate the behavior that happens with modifier key combinations as different operating systems function differently in this regard.

An example of an usage with a selection range:

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

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')
})

support

The following is an example of usage of this library with <input type="time" />

import React from 'react
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

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')
})

upload(element, file, [{ clickInit, changeInit }], [options])

Uploads file to an <input>. For uploading multiple files use <input> with multiple attribute and the second upload argument must be array then. Also it's possible to initialize click or change event with using third argument.

If options.applyAccept is set to true and there is an accept attribute on the element, files that don't match will be discarded.

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

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(element)

Selects the text inside an <input> or <textarea> and deletes it.

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('clear', () => {
  render(<textarea value="Hello, World!" />)

  userEvent.clear(screen.getByRole('textbox', 'email'))
  expect(screen.getByRole('textbox', 'email')).toHaveAttribute('value', '')
})

selectOptions(element, values)

Selects the specified option(s) of a <select> or a <select multiple> element.

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('selectOptions', () => {
  render(
    <select multiple data-testid="select-multiple">
      <option data-testid="val1" value="1">
        A
      </option>
      <option data-testid="val2" value="2">
        B
      </option>
      <option data-testid="val3" value="3">
        C
      </option>
    </select>,
  )

  userEvent.selectOptions(screen.getByTestId('select-multiple'), ['1', '3'])

  expect(screen.getByTestId('val1').selected).toBe(true)
  expect(screen.getByTestId('val2').selected).toBe(false)
  expect(screen.getByTestId('val3').selected).toBe(true)
})

The values parameter can be either an array of values or a singular scalar value.

It also accepts option nodes:

userEvent.selectOptions(screen.getByTestId('select-multiple'), [
  screen.getByText('A'),
  screen.getByText('B'),
])

deselectOptions(element, values)

Remove the selection for the specified option(s) of a <select multiple> element.

import * as React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

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'])
})

The values parameter can be either an array of values or a singular scalar value.

tab({shift, focusTrap})

Fires a tab event changing the document.activeElement in the same way the browser does.

Options:

  • shift (default false) can be true or false to invert tab direction.
  • focusTrap (default document) a container element to restrict the tabbing within.

A note about tab: jsdom does not support tabbing, so this feature is a way to enable tests to verify tabbing from the end user's perspective. However, this limitation in jsdom will mean that components like focus-trap-react will not work with userEvent.tab() or jsdom. For that reason, the focusTrap option is available to let you ensure your user is restricted within a focus-trap.

import React from 'react'
import {render, screen} from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import userEvent from '@testing-library/user-event'

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(element)

Hovers over element.

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Tooltip from '../tooltip'

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()
})

unhover(element)

Unhovers out of element.

See above for an example

paste(element, text, eventInit, options)

Allows you to simulate the user pasting some text into an input.

test('should paste text in input', () => {
  render(<MyInput />)

  const text = 'Hello, world!'
  userEvent.paste(getByRole('textbox', {name: /paste your greeting/i}), text)
  expect(element).toHaveValue(text)
})

You can use the eventInit if what you're pasting should have clipboardData (like files).

specialChars

A handful set of special characters used in type method.

Key Character
arrowLeft {arrowleft}
arrowRight {arrowright}
arrowDown {arrowdown}
arrowUp {arrowup}
home {home}
end {end}
enter {enter}
escape {esc}
delete {del}
backspace {backspace}
selectAll {selectall}
space {space}
whitespace ' '

Usage example:

import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent, {specialChars} from '@testing-library/user-event'

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, `${specialChars.backspace}good`)

  expect(input).toHaveValue('This is a good example')
})

Issues

Looking to contribute? Look for the Good First Issue label.

๐Ÿ› Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

๐Ÿ’ก Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding a ๐Ÿ‘. This helps maintainers prioritize what to work on.

See Feature Requests

Contributors โœจ

Thanks goes to these people (emoji key):


Giorgio Polvara

๐Ÿ› ๐Ÿ’ป ๐Ÿ“– ๐Ÿค” ๐Ÿš‡ ๐Ÿ‘€ โš ๏ธ

Weyert de Boer

๐Ÿ’ป โš ๏ธ

Tim Whitbeck

๐Ÿ› ๐Ÿ’ป

Michaรซl De Boey

๐Ÿ“–

Michael Lasky

๐Ÿ’ป ๐Ÿ“– ๐Ÿค”

Ahmad Esmaeilzadeh

๐Ÿ“–

Caleb Eby

๐Ÿ’ป ๐Ÿ› ๐Ÿ‘€

Adriร  Fontcuberta

๐Ÿ› โš ๏ธ ๐Ÿ’ป

Sky Wickenden

๐Ÿ› ๐Ÿ’ป

Bodnar Bogdan

๐Ÿ› ๐Ÿ’ป

Zach Perrault

๐Ÿ“–

Ryan Stelly

๐Ÿ“–

Ben Monro

๐Ÿ’ป

Christopher Martin

๐Ÿ’ป

Yuancheng Wu

๐Ÿ‘€

MJ

๐Ÿ“–

Jeff McRiffey

๐Ÿ’ป โš ๏ธ

Jaga Santagostino

๐Ÿ’ป โš ๏ธ

jordyvandomselaar

๐Ÿ’ป โš ๏ธ

Ilya Lyamkin

๐Ÿ’ป โš ๏ธ

Kenneth Lujรกn Rosas

๐Ÿ’ป โš ๏ธ

Joe Morgan

๐Ÿ’ป

David Hirtle

๐Ÿ’ป

whiteUnicorn

๐Ÿ’ป

Matej ล nuderl

๐Ÿ‘€

Rodrigo Pombo

๐Ÿ’ป

Jake Verbaten

๐Ÿ’ป

Spencer Miskoviak

๐Ÿ“–

Vadim Shvetsov

๐Ÿค” ๐Ÿ’ป โš ๏ธ

Greg Shtilman

๐Ÿ’ป โš ๏ธ ๐Ÿ›

Ricardo Busquet

๐Ÿ› ๐Ÿ’ป โš ๏ธ

Doug Bacelar

๐Ÿ’ป โš ๏ธ

Kayleigh Ridd

๐Ÿ› ๐Ÿ’ป โš ๏ธ

Malcolm Kee

๐Ÿ’ป ๐Ÿ“– โš ๏ธ

kelvinlzhang

๐Ÿ›

Krzysztof

๐Ÿ›

Pontus Lundin

๐Ÿ’ป โš ๏ธ

Aleks Hudochenkov

๐Ÿ›

Vijay Kumar Otti

๐Ÿ›

Tom Picton

๐Ÿ› ๐Ÿ’ป โš ๏ธ

Hung Viet Nguyen

๐Ÿ›

Nick McCurdy

๐Ÿ“† ๐Ÿ’ฌ ๐Ÿ’ป โš ๏ธ ๐Ÿ“–

Tim Deschryver

โš ๏ธ

Ben Dyer

๐Ÿ’ป โš ๏ธ

Dan Kirkham

๐Ÿ’ป

Johannesklint

๐Ÿ“–

Juan Carlos Medina

๐Ÿ’ป โš ๏ธ

Dade Cook

๐Ÿ’ป โš ๏ธ

Leandro Lourenci

๐Ÿ’ป โš ๏ธ

Marco Moretti

๐Ÿ’ป โš ๏ธ

ybentz

๐Ÿ’ป โš ๏ธ

Nasdan

๐Ÿ›

Javier Martรญnez

๐Ÿ“–

Jรถrg Bayreuther

๐Ÿ’ป โš ๏ธ ๐Ÿ“–

Lucas Bernalte

๐Ÿ“–

Maxwell Newlands

๐Ÿ’ป โš ๏ธ

ph-fritsche

๐Ÿ’ป โš ๏ธ

Rey Wright

๐Ÿ› ๐Ÿ’ป

Niklas Mischkulnig

๐Ÿ’ป โš ๏ธ

Pascal Duez

๐Ÿ’ป

Malachi Willey

๐Ÿ’ป โš ๏ธ

Clark Winters

๐Ÿ“–

lazytype

๐Ÿ’ป โš ๏ธ

Luรญs Takahashi

๐Ÿ’ป โš ๏ธ

Jesu Castillo

๐Ÿ’ป โš ๏ธ

Sarah Dayan

๐Ÿ“–

Mirone

๐Ÿ›

Amanda Pouget

๐Ÿ“–

Sonic12040

๐Ÿ’ป โš ๏ธ ๐Ÿ“–

Gonzalo D'Elia

๐Ÿ’ป โš ๏ธ ๐Ÿ“–

Vasilii Kovalev

๐Ÿ’ป ๐Ÿ“–

Dale Seo

๐Ÿ“–

Alex Boyce

๐Ÿ’ป

Ben Styles

๐Ÿ’ป โš ๏ธ

Laura Beatris

๐Ÿ’ป โš ๏ธ

Boris Serdiuk

๐Ÿ›

bozdoz

๐Ÿ“–

Jan Kattelans

๐Ÿ’ป

schoeneu

๐Ÿ›

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT