0.2.0 - Mid migration

This commit is contained in:
Daniel Mason 2022-04-25 14:47:15 +12:00
parent 139e6a915e
commit 7e38fdbd7d
42393 changed files with 5358157 additions and 62 deletions

View file

@ -0,0 +1,5 @@
# CHANGELOG
The changelog is automatically updated using
[semantic-release](https://github.com/semantic-release/semantic-release). You
can see it on the [releases page](../../releases).

20
web/node_modules/@testing-library/user-event/LICENSE generated vendored Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2020 Giorgio Polvara
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

737
web/node_modules/@testing-library/user-event/README.md generated vendored Normal file
View file

@ -0,0 +1,737 @@
<div align="center">
<h1>@testing-library/user-event</h1>
<a href="https://www.joypixels.com/profiles/emoji/1f415">
<img
height="80"
width="80"
alt="dog"
src="https://raw.githubusercontent.com/testing-library/user-event/master/other/dog.png"
/>
</a>
<p>Fire events the same way the user does</p>
</div>
---
<!-- prettier-ignore-start -->
[![Build Status][build-badge]][build]
[![Code Coverage][coverage-badge]][coverage]
[![version][version-badge]][package]
[![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]
[![All Contributors][all-contributors-badge]](#contributors)
[![PRs Welcome][prs-badge]][prs]
[![Code of Conduct][coc-badge]][coc]
<!-- prettier-ignore-end -->
## The problem
From
[testing-library/dom-testing-library#107](https://github.com/testing-library/dom-testing-library/issues/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
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Installation](#installation)
- [API](#api)
- [`click(element, eventInit, options)`](#clickelement-eventinit-options)
- [`dblClick(element, eventInit, options)`](#dblclickelement-eventinit-options)
- [`type(element, text, [options])`](#typeelement-text-options)
- [`upload(element, file, [{ clickInit, changeInit }])`](#uploadelement-file--clickinit-changeinit-)
- [`clear(element)`](#clearelement)
- [`selectOptions(element, values)`](#selectoptionselement-values)
- [`deselectOptions(element, values)`](#deselectoptionselement-values)
- [`tab({shift, focusTrap})`](#tabshift-focustrap)
- [`hover(element)`](#hoverelement)
- [`unhover(element)`](#unhoverelement)
- [`paste(element, text, eventInit, options)`](#pasteelement-text-eventinit-options)
- [`specialChars`](#specialchars)
- [Issues](#issues)
- [🐛 Bugs](#-bugs)
- [💡 Feature Requests](#-feature-requests)
- [Contributors ✨](#contributors-)
- [LICENSE](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Installation
With NPM:
```sh
npm install @testing-library/user-event @testing-library/dom --save-dev
```
With Yarn:
```sh
yarn add @testing-library/user-event @testing-library/dom --dev
```
Now simply import it in your tests:
```js
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"](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing).
### `click(element, eventInit, options)`
Clicks `element`, depending on what `element` is it can have different side
effects.
```jsx
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
```js
userEvent.click(elem, {ctrlKey: true, shiftKey: true})
```
See the
[`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/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.
```jsx
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>`.
```jsx
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`).
<!-- space out these notes -->
> We take the same
> [stance as Cypress](https://docs.cypress.io/api/commands/type.html#Modifiers)
> 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:
```jsx
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')
})
```
#### <input type="time" /> support
The following is an example of usage of this library with
`<input type="time" />`
```jsx
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.
```jsx
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.
```jsx
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.
```jsx
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:
```js
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.
```jsx
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](https://github.com/jsdom/jsdom/issues/2102),
> 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](https://github.com/davidtheclark/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.
```jsx
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`.
```jsx
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](#hoverelement) for an example
### `paste(element, text, eventInit, options)`
Allows you to simulate the user pasting some text into an input.
```javascript
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](#typeelement-text-options)
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:
```jsx
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][good-first-issue]
label._
### 🐛 Bugs
Please file an issue for bugs, missing documentation, or unexpected behavior.
[**See Bugs**][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**][requests]
## Contributors ✨
Thanks goes to these people ([emoji key][emojis]):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://twitter.com/Gpx"><img src="https://avatars0.githubusercontent.com/u/767959?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Giorgio Polvara</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3AGpx" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Documentation">📖</a> <a href="#ideas-Gpx" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-Gpx" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/testing-library/user-event/pulls?q=is%3Apr+reviewed-by%3AGpx" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/testing-library/user-event/commits?author=Gpx" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/weyert"><img src="https://avatars3.githubusercontent.com/u/7049?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Weyert de Boer</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=weyert" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=weyert" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/twhitbeck"><img src="https://avatars2.githubusercontent.com/u/762471?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tim Whitbeck</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Atwhitbeck" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=twhitbeck" title="Code">💻</a></td>
<td align="center"><a href="https://michaeldeboey.be"><img src="https://avatars3.githubusercontent.com/u/6643991?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michaël De Boey</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=MichaelDeBoey" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/michaellasky"><img src="https://avatars2.githubusercontent.com/u/6646599?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael Lasky</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=michaellasky" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=michaellasky" title="Documentation">📖</a> <a href="#ideas-michaellasky" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/shomalgan"><img src="https://avatars0.githubusercontent.com/u/2883620?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ahmad Esmaeilzadeh</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=shomalgan" title="Documentation">📖</a></td>
<td align="center"><a href="https://calebeby.ml"><img src="https://avatars1.githubusercontent.com/u/13206945?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Caleb Eby</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=calebeby" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/issues?q=author%3Acalebeby" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/pulls?q=is%3Apr+reviewed-by%3Acalebeby" title="Reviewed Pull Requests">👀</a></td>
</tr>
<tr>
<td align="center"><a href="https://afontcu.dev"><img src="https://avatars0.githubusercontent.com/u/9197791?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Adrià Fontcuberta</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Aafontcu" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=afontcu" title="Tests">⚠️</a> <a href="https://github.com/testing-library/user-event/commits?author=afontcu" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/skywickenden"><img src="https://avatars2.githubusercontent.com/u/4930551?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sky Wickenden</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Askywickenden" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=skywickenden" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bogdanbodnar"><img src="https://avatars2.githubusercontent.com/u/9034868?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bodnar Bogdan</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Abogdanbodnar" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=bogdanbodnar" title="Code">💻</a></td>
<td align="center"><a href="https://zach.website"><img src="https://avatars0.githubusercontent.com/u/1699281?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zach Perrault</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=zperrault" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/ryanastelly"><img src="https://avatars1.githubusercontent.com/u/4138357?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Stelly</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=FLGMwt" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/benmonro"><img src="https://avatars3.githubusercontent.com/u/399236?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben Monro</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=benmonro" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/GentlemanHal"><img src="https://avatars2.githubusercontent.com/u/415521?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Christopher Martin</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=GentlemanHal" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://fullgallop.me"><img src="https://avatars0.githubusercontent.com/u/32252769?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yuancheng Wu</b></sub></a><br /><a href="https://github.com/testing-library/user-event/pulls?q=is%3Apr+reviewed-by%3AYuanchengWu" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://github.com/maheshjag"><img src="https://avatars0.githubusercontent.com/u/1705603?v=4?s=100" width="100px;" alt=""/><br /><sub><b>MJ</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=maheshjag" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jmcriffey"><img src="https://avatars0.githubusercontent.com/u/2831294?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jeff McRiffey</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=jmcriffey" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=jmcriffey" title="Tests">⚠️</a></td>
<td align="center"><a href="http://jagascript.com"><img src="https://avatars0.githubusercontent.com/u/4562878?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jaga Santagostino</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=kandros" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=kandros" title="Tests">⚠️</a></td>
<td align="center"><a href="http://jordy.app"><img src="https://avatars3.githubusercontent.com/u/12712484?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jordyvandomselaar</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=jordyvandomselaar" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=jordyvandomselaar" title="Tests">⚠️</a></td>
<td align="center"><a href="https://lyamkin.com"><img src="https://avatars2.githubusercontent.com/u/3854930?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ilya Lyamkin</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=ilyamkin" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=ilyamkin" title="Tests">⚠️</a></td>
<td align="center"><a href="http://todofullstack.com"><img src="https://avatars2.githubusercontent.com/u/4474353?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kenneth Luján Rosas</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=klujanrosas" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=klujanrosas" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center"><a href="http://thejoemorgan.com"><img src="https://avatars1.githubusercontent.com/u/2388943?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joe Morgan</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=jsmapr1" title="Code">💻</a></td>
<td align="center"><a href="https://twitter.com/wachunga"><img src="https://avatars0.githubusercontent.com/u/438545?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Hirtle</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=wachunga" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/bdh1011"><img src="https://avatars2.githubusercontent.com/u/8446067?v=4?s=100" width="100px;" alt=""/><br /><sub><b>whiteUnicorn</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=bdh1011" title="Code">💻</a></td>
<td align="center"><a href="https://www.matej.snuderl.si/"><img src="https://avatars3.githubusercontent.com/u/8524109?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matej Šnuderl</b></sub></a><br /><a href="https://github.com/testing-library/user-event/pulls?q=is%3Apr+reviewed-by%3AMeemaw" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://pomb.us"><img src="https://avatars1.githubusercontent.com/u/1911623?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rodrigo Pombo</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=pomber" title="Code">💻</a></td>
<td align="center"><a href="http://github.com/Raynos"><img src="https://avatars3.githubusercontent.com/u/479538?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jake Verbaten</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=Raynos" title="Code">💻</a></td>
<td align="center"><a href="https://skovy.dev"><img src="https://avatars1.githubusercontent.com/u/5247455?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Spencer Miskoviak</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=skovy" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://proling.ru/"><img src="https://avatars2.githubusercontent.com/u/16336572?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vadim Shvetsov</b></sub></a><br /><a href="#ideas-vadimshvetsov" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/testing-library/user-event/commits?author=vadimshvetsov" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=vadimshvetsov" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/9still"><img src="https://avatars0.githubusercontent.com/u/4924760?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Greg Shtilman</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=9still" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=9still" title="Tests">⚠️</a> <a href="https://github.com/testing-library/user-event/issues?q=author%3A9still" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/rbusquet"><img src="https://avatars1.githubusercontent.com/u/7198302?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ricardo Busquet</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Arbusquet" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=rbusquet" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=rbusquet" title="Tests">⚠️</a></td>
<td align="center"><a href="https://www.linkedin.com/in/dougbacelar/en"><img src="https://avatars3.githubusercontent.com/u/9267678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Doug Bacelar</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=dougbacelar" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=dougbacelar" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kayleighridd"><img src="https://avatars3.githubusercontent.com/u/36446015?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kayleigh Ridd</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Akayleighridd" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=kayleighridd" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=kayleighridd" title="Tests">⚠️</a></td>
<td align="center"><a href="https://malcolmkee.com"><img src="https://avatars0.githubusercontent.com/u/24528512?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Malcolm Kee</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=malcolm-kee" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=malcolm-kee" title="Documentation">📖</a> <a href="https://github.com/testing-library/user-event/commits?author=malcolm-kee" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kelvinlzhang"><img src="https://avatars3.githubusercontent.com/u/8291294?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kelvinlzhang</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Akelvinlzhang" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/krzysztof-hellostudio"><img src="https://avatars3.githubusercontent.com/u/1942664?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krzysztof</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Akrzysztof-hellostudio" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/hontas"><img src="https://avatars2.githubusercontent.com/u/1521113?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pontus Lundin</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=hontas" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=hontas" title="Tests">⚠️</a></td>
<td align="center"><a href="https://hudochenkov.com/"><img src="https://avatars2.githubusercontent.com/u/654597?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Aleks Hudochenkov</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Ahudochenkov" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/nanivijay"><img src="https://avatars0.githubusercontent.com/u/5945591?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vijay Kumar Otti</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Ananivijay" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://tompicton.com"><img src="https://avatars2.githubusercontent.com/u/12588098?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tom Picton</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Atpict" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=tpict" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=tpict" title="Tests">⚠️</a></td>
<td align="center"><a href="https://hung.dev"><img src="https://avatars3.githubusercontent.com/u/8603085?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Hung Viet Nguyen</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Anvh95" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://nickmccurdy.com/"><img src="https://avatars0.githubusercontent.com/u/927220?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nick McCurdy</b></sub></a><br /><a href="#projectManagement-nickmccurdy" title="Project Management">📆</a> <a href="#question-nickmccurdy" title="Answering Questions">💬</a> <a href="https://github.com/testing-library/user-event/commits?author=nickmccurdy" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=nickmccurdy" title="Tests">⚠️</a> <a href="https://github.com/testing-library/user-event/commits?author=nickmccurdy" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://timdeschryver.dev"><img src="https://avatars1.githubusercontent.com/u/28659384?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tim Deschryver</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=timdeschryver" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/ben-dyer"><img src="https://avatars2.githubusercontent.com/u/43922444?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben Dyer</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=ben-dyer" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=ben-dyer" title="Tests">⚠️</a></td>
<td align="center"><a href="https://twitter.com/herecydev"><img src="https://avatars1.githubusercontent.com/u/11328618?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dan Kirkham</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=herecydev" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Johannesklint"><img src="https://avatars3.githubusercontent.com/u/16774845?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Johannesklint</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=Johannesklint" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/juanca"><img src="https://avatars0.githubusercontent.com/u/841084?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Juan Carlos Medina</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=juanca" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=juanca" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/WretchedDade"><img src="https://avatars0.githubusercontent.com/u/17183431?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dade Cook</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=WretchedDade" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=WretchedDade" title="Tests">⚠️</a></td>
<td align="center"><a href="https://blog.lourenci.com/"><img src="https://avatars3.githubusercontent.com/u/2339362?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Leandro Lourenci</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=lourenci" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=lourenci" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/marcosvega91"><img src="https://avatars2.githubusercontent.com/u/5365582?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Marco Moretti</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=marcosvega91" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=marcosvega91" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/ybentz"><img src="https://avatars3.githubusercontent.com/u/14811577?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ybentz</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=ybentz" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=ybentz" title="Tests">⚠️</a></td>
<td align="center"><a href="http://www.lemoncode.net/"><img src="https://avatars2.githubusercontent.com/u/4374977?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nasdan</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3ANasdan" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/JavierMartinz"><img src="https://avatars1.githubusercontent.com/u/1155507?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Javier Martínez</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=JavierMartinz" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.visualjerk.de"><img src="https://avatars0.githubusercontent.com/u/28823153?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jörg Bayreuther</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=visualjerk" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=visualjerk" title="Tests">⚠️</a> <a href="https://github.com/testing-library/user-event/commits?author=visualjerk" title="Documentation">📖</a></td>
<td align="center"><a href="https://ko-fi.com/thislucas"><img src="https://avatars0.githubusercontent.com/u/8645841?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Lucas Bernalte</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=lucbpz" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/maxnewlands"><img src="https://avatars3.githubusercontent.com/u/1304166?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maxwell Newlands</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=maxnewlands" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=maxnewlands" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ph-fritsche"><img src="https://avatars3.githubusercontent.com/u/39068198?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ph-fritsche</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=ph-fritsche" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=ph-fritsche" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/reywright"><img src="https://avatars3.githubusercontent.com/u/708820?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rey Wright</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Areywright" title="Bug reports">🐛</a> <a href="https://github.com/testing-library/user-event/commits?author=reywright" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/mischnic"><img src="https://avatars1.githubusercontent.com/u/4586894?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Niklas Mischkulnig</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=mischnic" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=mischnic" title="Tests">⚠️</a></td>
<td align="center"><a href="http://pascalduez.me"><img src="https://avatars3.githubusercontent.com/u/335467?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pascal Duez</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=pascalduez" title="Code">💻</a></td>
<td align="center"><a href="http://malachi.dev"><img src="https://avatars3.githubusercontent.com/u/10888943?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Malachi Willey</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=malwilley" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=malwilley" title="Tests">⚠️</a></td>
<td align="center"><a href="https://clarkwinters.com"><img src="https://avatars2.githubusercontent.com/u/40615752?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Clark Winters</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=cwinters8" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/lazytype"><img src="https://avatars1.githubusercontent.com/u/840985?v=4?s=100" width="100px;" alt=""/><br /><sub><b>lazytype</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=lazytype" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=lazytype" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center"><a href="https://www.linkedin.com/in/luis-takahashi/"><img src="https://avatars0.githubusercontent.com/u/19766035?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Luís Takahashi</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=luistak" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=luistak" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/jesujcastillom"><img src="https://avatars3.githubusercontent.com/u/7827281?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jesu Castillo</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=jesujcastillom" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=jesujcastillom" title="Tests">⚠️</a></td>
<td align="center"><a href="https://sarahdayan.dev"><img src="https://avatars1.githubusercontent.com/u/5370675?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sarah Dayan</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=sarahdayan" title="Documentation">📖</a></td>
<td align="center"><a href="http://saul-mirone.github.io/"><img src="https://avatars0.githubusercontent.com/u/10047788?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mirone</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3ASaul-Mirone" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/amandapouget"><img src="https://avatars3.githubusercontent.com/u/12855692?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Amanda Pouget</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=amandapouget" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Sonic12040"><img src="https://avatars3.githubusercontent.com/u/21055893?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sonic12040</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=Sonic12040" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=Sonic12040" title="Tests">⚠️</a> <a href="https://github.com/testing-library/user-event/commits?author=Sonic12040" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/gndelia"><img src="https://avatars1.githubusercontent.com/u/352474?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gonzalo D'Elia</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=gndelia" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=gndelia" title="Tests">⚠️</a> <a href="https://github.com/testing-library/user-event/commits?author=gndelia" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/vasilii-kovalev"><img src="https://avatars0.githubusercontent.com/u/10310491?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vasilii Kovalev</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=vasilii-kovalev" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=vasilii-kovalev" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.daleseo.com"><img src="https://avatars1.githubusercontent.com/u/5466341?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dale Seo</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=daleseo" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.alex-boyce.me/"><img src="https://avatars.githubusercontent.com/u/4050934?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Boyce</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=curiosity26" title="Code">💻</a></td>
<td align="center"><a href="https://benadamstyles.com"><img src="https://avatars.githubusercontent.com/u/4380655?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben Styles</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=benadamstyles" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=benadamstyles" title="Tests">⚠️</a></td>
<td align="center"><a href="http://laurabeatris.com"><img src="https://avatars.githubusercontent.com/u/48022589?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Laura Beatris</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=LauraBeatris" title="Code">💻</a> <a href="https://github.com/testing-library/user-event/commits?author=LauraBeatris" title="Tests">⚠️</a></td>
<td align="center"><a href="https://twitter.com/boriscoder"><img src="https://avatars.githubusercontent.com/u/812240?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Boris Serdiuk</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Ajust-boris" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://bozdoz.com"><img src="https://avatars.githubusercontent.com/u/1410985?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bozdoz</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=bozdoz" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jKatt"><img src="https://avatars.githubusercontent.com/u/5550790?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jan Kattelans</b></sub></a><br /><a href="https://github.com/testing-library/user-event/commits?author=jKatt" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/schoeneu"><img src="https://avatars.githubusercontent.com/u/3261341?v=4?s=100" width="100px;" alt=""/><br /><sub><b>schoeneu</b></sub></a><br /><a href="https://github.com/testing-library/user-event/issues?q=author%3Aschoeneu" title="Bug reports">🐛</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors][all-contributors] specification.
Contributions of any kind welcome!
## LICENSE
MIT
<!-- prettier-ignore-start -->
[npm]: https://www.npmjs.com
[node]: https://nodejs.org
[build-badge]: https://img.shields.io/github/workflow/status/testing-library/user-event/validate/master?logo=github&style=flat-square
[build]: https://github.com/testing-library/user-event/actions?query=workflow%3Avalidate
[coverage-badge]: https://img.shields.io/codecov/c/github/testing-library/user-event.svg?style=flat-square
[coverage]: https://codecov.io/github/testing-library/user-event
[version-badge]: https://img.shields.io/npm/v/@testing-library/user-event.svg?style=flat-square
[package]: https://www.npmjs.com/package/@testing-library/user-event
[downloads-badge]: https://img.shields.io/npm/dm/@testing-library/user-event.svg?style=flat-square
[npmtrends]: http://www.npmtrends.com/@testing-library/user-event
[license-badge]: https://img.shields.io/npm/l/@testing-library/user-event.svg?style=flat-square
[license]: https://github.com/testing-library/user-event/blob/master/LICENSE
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[prs]: http://makeapullrequest.com
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
[coc]: https://github.com/testing-library/user-event/blob/master/other/CODE_OF_CONDUCT.md
[emojis]: https://github.com/all-contributors/all-contributors#emoji-key
[all-contributors]: https://github.com/all-contributors/all-contributors
[all-contributors-badge]: https://img.shields.io/github/all-contributors/testing-library/user-event?color=orange&style=flat-square
[bugs]: https://github.com/testing-library/user-event/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug
[requests]: https://github.com/testing-library/user-event/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement
[good-first-issue]: https://github.com/testing-library/user-event/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22
<!-- prettier-ignore-end -->

View file

@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.blur = blur;
var _utils = require("./utils");
function blur(element) {
if (!(0, _utils.isFocusable)(element)) return;
const wasActive = (0, _utils.getActiveElement)(element.ownerDocument) === element;
if (!wasActive) return;
(0, _utils.eventWrapper)(() => element.blur());
}

View file

@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.clear = clear;
var _type = require("./type");
function clear(element) {
if (element.tagName !== 'INPUT' && element.tagName !== 'TEXTAREA') {
// TODO: support contenteditable
throw new Error('clear currently only supports input and textarea elements.');
}
if (element.disabled) return; // TODO: track the selection range ourselves so we don't have to do this input "type" trickery
// just like cypress does: https://github.com/cypress-io/cypress/blob/8d7f1a0bedc3c45a2ebf1ff50324b34129fdc683/packages/driver/src/dom/selection.ts#L16-L37
const elementType = element.type; // type is a readonly property on textarea, so check if element is an input before trying to modify it
if (element.tagName === 'INPUT') {
// setSelectionRange is not supported on certain types of inputs, e.g. "number" or "email"
element.type = 'text';
}
(0, _type.type)(element, '{selectall}{del}', {
delay: 0,
initialSelectionStart: element.selectionStart,
initialSelectionEnd: element.selectionEnd
});
if (element.tagName === 'INPUT') {
element.type = elementType;
}
}

View file

@ -0,0 +1,154 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.click = click;
exports.dblClick = dblClick;
var _dom = require("@testing-library/dom");
var _utils = require("./utils");
var _hover = require("./hover");
var _blur = require("./blur");
var _focus = require("./focus");
function getPreviouslyFocusedElement(element) {
const focusedElement = element.ownerDocument.activeElement;
const wasAnotherElementFocused = focusedElement && focusedElement !== element.ownerDocument.body && focusedElement !== element;
return wasAnotherElementFocused ? focusedElement : null;
}
function clickLabel(label, init, {
clickCount
}) {
if ((0, _utils.isLabelWithInternallyDisabledControl)(label)) return;
_dom.fireEvent.pointerDown(label, init);
_dom.fireEvent.mouseDown(label, (0, _utils.getMouseEventOptions)('mousedown', init, clickCount));
_dom.fireEvent.pointerUp(label, init);
_dom.fireEvent.mouseUp(label, (0, _utils.getMouseEventOptions)('mouseup', init, clickCount));
_dom.fireEvent.click(label, (0, _utils.getMouseEventOptions)('click', init, clickCount)); // clicking the label will trigger a click of the label.control
// however, it will not focus the label.control so we have to do it
// ourselves.
if (label.control) (0, _focus.focus)(label.control);
}
function clickBooleanElement(element, init, clickCount) {
_dom.fireEvent.pointerDown(element, init);
if (!element.disabled) {
_dom.fireEvent.mouseDown(element, (0, _utils.getMouseEventOptions)('mousedown', init, clickCount));
}
(0, _focus.focus)(element, init);
_dom.fireEvent.pointerUp(element, init);
if (!element.disabled) {
_dom.fireEvent.mouseUp(element, (0, _utils.getMouseEventOptions)('mouseup', init, clickCount));
_dom.fireEvent.click(element, (0, _utils.getMouseEventOptions)('click', init, clickCount));
}
}
function clickElement(element, init, {
clickCount
}) {
const previousElement = getPreviouslyFocusedElement(element);
_dom.fireEvent.pointerDown(element, init);
if (!element.disabled) {
const continueDefaultHandling = _dom.fireEvent.mouseDown(element, (0, _utils.getMouseEventOptions)('mousedown', init, clickCount));
if (continueDefaultHandling) {
const closestFocusable = findClosest(element, _utils.isFocusable);
if (previousElement && !closestFocusable) {
(0, _blur.blur)(previousElement, init);
} else if (closestFocusable) {
(0, _focus.focus)(closestFocusable, init);
}
}
}
_dom.fireEvent.pointerUp(element, init);
if (!element.disabled) {
_dom.fireEvent.mouseUp(element, (0, _utils.getMouseEventOptions)('mouseup', init, clickCount));
_dom.fireEvent.click(element, (0, _utils.getMouseEventOptions)('click', init, clickCount));
const parentLabel = element.closest('label');
if (parentLabel != null && parentLabel.control) (0, _focus.focus)(parentLabel.control, init);
}
}
function findClosest(el, callback) {
do {
if (callback(el)) {
return el;
}
el = el.parentElement;
} while (el && el !== document.body);
return undefined;
}
function click(element, init, {
skipHover = false,
clickCount = 0
} = {}) {
if (!skipHover) (0, _hover.hover)(element, init);
switch (element.tagName) {
case 'LABEL':
clickLabel(element, init, {
clickCount
});
break;
case 'INPUT':
if (element.type === 'checkbox' || element.type === 'radio') {
clickBooleanElement(element, init, {
clickCount
});
} else {
clickElement(element, init, {
clickCount
});
}
break;
default:
clickElement(element, init, {
clickCount
});
}
}
function dblClick(element, init) {
(0, _hover.hover)(element, init);
click(element, init, {
skipHover: true,
clickCount: 0
});
click(element, init, {
skipHover: true,
clickCount: 1
});
_dom.fireEvent.dblClick(element, (0, _utils.getMouseEventOptions)('dblclick', init, 2));
}

View file

@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.focus = focus;
var _utils = require("./utils");
function focus(element) {
if (!(0, _utils.isFocusable)(element)) return;
const isAlreadyActive = (0, _utils.getActiveElement)(element.ownerDocument) === element;
if (isAlreadyActive) return;
(0, _utils.eventWrapper)(() => element.focus());
}

View file

@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.hover = hover;
exports.unhover = unhover;
var _dom = require("@testing-library/dom");
var _utils = require("./utils");
// includes `element`
function getParentElements(element) {
const parentElements = [element];
let currentElement = element;
while ((currentElement = currentElement.parentElement) != null) {
parentElements.push(currentElement);
}
return parentElements;
}
function hover(element, init) {
if ((0, _utils.isLabelWithInternallyDisabledControl)(element)) return;
const parentElements = getParentElements(element).reverse();
_dom.fireEvent.pointerOver(element, init);
for (const el of parentElements) {
_dom.fireEvent.pointerEnter(el, init);
}
if (!element.disabled) {
_dom.fireEvent.mouseOver(element, (0, _utils.getMouseEventOptions)('mouseover', init));
for (const el of parentElements) {
_dom.fireEvent.mouseEnter(el, (0, _utils.getMouseEventOptions)('mouseenter', init));
}
}
_dom.fireEvent.pointerMove(element, init);
if (!element.disabled) {
_dom.fireEvent.mouseMove(element, (0, _utils.getMouseEventOptions)('mousemove', init));
}
}
function unhover(element, init) {
if ((0, _utils.isLabelWithInternallyDisabledControl)(element)) return;
const parentElements = getParentElements(element);
_dom.fireEvent.pointerMove(element, init);
if (!element.disabled) {
_dom.fireEvent.mouseMove(element, (0, _utils.getMouseEventOptions)('mousemove', init));
}
_dom.fireEvent.pointerOut(element, init);
for (const el of parentElements) {
_dom.fireEvent.pointerLeave(el, init);
}
if (!element.disabled) {
_dom.fireEvent.mouseOut(element, (0, _utils.getMouseEventOptions)('mouseout', init));
for (const el of parentElements) {
_dom.fireEvent.mouseLeave(el, (0, _utils.getMouseEventOptions)('mouseleave', init));
}
}
}

View file

@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "specialChars", {
enumerable: true,
get: function () {
return _type.specialCharMap;
}
});
exports.default = void 0;
var _click = require("./click");
var _type = require("./type");
var _clear = require("./clear");
var _tab = require("./tab");
var _hover = require("./hover");
var _upload = require("./upload");
var _selectOptions = require("./select-options");
var _paste = require("./paste");
const userEvent = {
click: _click.click,
dblClick: _click.dblClick,
type: _type.type,
clear: _clear.clear,
tab: _tab.tab,
hover: _hover.hover,
unhover: _hover.unhover,
upload: _upload.upload,
selectOptions: _selectOptions.selectOptions,
deselectOptions: _selectOptions.deselectOptions,
paste: _paste.paste
};
var _default = userEvent;
exports.default = _default;

View file

@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.navigationKey = navigationKey;
var _dom = require("@testing-library/dom");
var _utils = require("../utils");
const keys = {
Home: {
keyCode: 36
},
End: {
keyCode: 35
},
ArrowLeft: {
keyCode: 37
},
ArrowRight: {
keyCode: 39
}
};
function getSelectionRange(currentElement, key) {
const {
selectionStart,
selectionEnd
} = currentElement();
if (key === 'Home') {
return {
selectionStart: 0,
selectionEnd: 0
};
}
if (key === 'End') {
return {
selectionStart: selectionEnd + 1,
selectionEnd: selectionEnd + 1
};
}
const cursorChange = Number(key in keys) * (key === 'ArrowLeft' ? -1 : 1);
return {
selectionStart: selectionStart + cursorChange,
selectionEnd: selectionEnd + cursorChange
};
}
function navigationKey(key) {
const event = {
key,
keyCode: keys[key].keyCode,
which: keys[key].keyCode
};
return ({
currentElement,
eventOverrides
}) => {
_dom.fireEvent.keyDown(currentElement(), { ...event,
...eventOverrides
});
const range = getSelectionRange(currentElement, key);
(0, _utils.setSelectionRangeIfNecessary)(currentElement(), range.selectionStart, range.selectionEnd);
_dom.fireEvent.keyUp(currentElement(), { ...event,
...eventOverrides
});
};
}

View file

@ -0,0 +1,55 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.paste = paste;
var _dom = require("@testing-library/dom");
var _utils = require("./utils");
function paste(element, text, init, {
initialSelectionStart,
initialSelectionEnd
} = {}) {
if (element.disabled) return;
if (typeof element.value === 'undefined') {
throw new TypeError(`the current element is of type ${element.tagName} and doesn't have a valid value`);
}
(0, _utils.eventWrapper)(() => element.focus()); // 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 (element.selectionStart === 0 && element.selectionEnd === 0) {
(0, _utils.setSelectionRangeIfNecessary)(element, initialSelectionStart != null ? initialSelectionStart : element.value.length, initialSelectionEnd != null ? initialSelectionEnd : element.value.length);
}
_dom.fireEvent.paste(element, init);
if (!element.readOnly) {
const {
newValue,
newSelectionStart
} = (0, _utils.calculateNewValue)(text, element);
_dom.fireEvent.input(element, {
inputType: 'insertFromPaste',
target: {
value: newValue
}
});
(0, _utils.setSelectionRangeIfNecessary)(element, {
newSelectionStart,
newSelectionEnd: newSelectionStart
});
}
}

View file

@ -0,0 +1,118 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.deselectOptions = exports.selectOptions = void 0;
var _dom = require("@testing-library/dom");
var _utils = require("./utils");
var _click = require("./click");
var _focus = require("./focus");
var _hover = require("./hover");
function selectOptionsBase(newValue, select, values, init) {
if (!newValue && !select.multiple) {
throw (0, _dom.getConfig)().getElementError(`Unable to deselect an option in a non-multiple select. Use selectOptions to change the selection instead.`, select);
}
const valArray = Array.isArray(values) ? values : [values];
const allOptions = Array.from(select.querySelectorAll('option, [role="option"]'));
const selectedOptions = valArray.map(val => {
if (allOptions.includes(val)) {
return val;
} else {
const matchingOption = allOptions.find(o => o.value === val || o.innerHTML === val);
if (matchingOption) {
return matchingOption;
} else {
throw (0, _dom.getConfig)().getElementError(`Value "${val}" not found in options`, select);
}
}
}).filter(option => !option.disabled);
if (select.disabled || !selectedOptions.length) return;
if ((0, _utils.isInstanceOfElement)(select, 'HTMLSelectElement')) {
if (select.multiple) {
for (const option of selectedOptions) {
// events fired for multiple select are weird. Can't use hover...
_dom.fireEvent.pointerOver(option, init);
_dom.fireEvent.pointerEnter(select, init);
_dom.fireEvent.mouseOver(option);
_dom.fireEvent.mouseEnter(select);
_dom.fireEvent.pointerMove(option, init);
_dom.fireEvent.mouseMove(option, init);
_dom.fireEvent.pointerDown(option, init);
_dom.fireEvent.mouseDown(option, init);
(0, _focus.focus)(select, init);
_dom.fireEvent.pointerUp(option, init);
_dom.fireEvent.mouseUp(option, init);
selectOption(option);
_dom.fireEvent.click(option, init);
}
} else if (selectedOptions.length === 1) {
// the click to open the select options
(0, _click.click)(select, init);
selectOption(selectedOptions[0]); // the browser triggers another click event on the select for the click on the option
// this second click has no 'down' phase
_dom.fireEvent.pointerOver(select, init);
_dom.fireEvent.pointerEnter(select, init);
_dom.fireEvent.mouseOver(select);
_dom.fireEvent.mouseEnter(select);
_dom.fireEvent.pointerUp(select, init);
_dom.fireEvent.mouseUp(select, init);
_dom.fireEvent.click(select, init);
} else {
throw (0, _dom.getConfig)().getElementError(`Cannot select multiple options on a non-multiple select`, select);
}
} else if (select.getAttribute('role') === 'listbox') {
selectedOptions.forEach(option => {
(0, _hover.hover)(option, init);
(0, _click.click)(option, init);
(0, _hover.unhover)(option, init);
});
} else {
throw (0, _dom.getConfig)().getElementError(`Cannot select options on elements that are neither select nor listbox elements`, select);
}
function selectOption(option) {
option.selected = newValue;
(0, _dom.fireEvent)(select, (0, _dom.createEvent)('input', select, {
bubbles: true,
cancelable: false,
composed: true,
...init
}));
_dom.fireEvent.change(select, init);
}
}
const selectOptions = selectOptionsBase.bind(null, true);
exports.selectOptions = selectOptions;
const deselectOptions = selectOptionsBase.bind(null, false);
exports.deselectOptions = deselectOptions;

View file

@ -0,0 +1,138 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.tab = tab;
var _dom = require("@testing-library/dom");
var _utils = require("./utils");
var _focus = require("./focus");
var _blur = require("./blur");
function getNextElement(currentIndex, shift, elements, focusTrap) {
if (focusTrap === document && currentIndex === 0 && shift) {
return document.body;
} else if (focusTrap === document && currentIndex === elements.length - 1 && !shift) {
return document.body;
} else {
const nextIndex = shift ? currentIndex - 1 : currentIndex + 1;
const defaultIndex = shift ? elements.length - 1 : 0;
return elements[nextIndex] || elements[defaultIndex];
}
}
function tab({
shift = false,
focusTrap
} = {}) {
var _focusTrap$ownerDocum, _focusTrap;
const previousElement = (0, _utils.getActiveElement)((_focusTrap$ownerDocum = (_focusTrap = focusTrap) == null ? void 0 : _focusTrap.ownerDocument) != null ? _focusTrap$ownerDocum : document);
if (!focusTrap) {
focusTrap = document;
}
const focusableElements = focusTrap.querySelectorAll(_utils.FOCUSABLE_SELECTOR);
const enabledElements = [...focusableElements].filter(el => el === previousElement || el.getAttribute('tabindex') !== '-1' && !el.disabled && // Hidden elements are not tabable
(0, _utils.isVisible)(el));
if (enabledElements.length === 0) return;
const orderedElements = enabledElements.map((el, idx) => ({
el,
idx
})).sort((a, b) => {
// tabindex has no effect if the active element has tabindex="-1"
if (previousElement && previousElement.getAttribute('tabindex') === '-1') {
return a.idx - b.idx;
}
const tabIndexA = a.el.getAttribute('tabindex');
const tabIndexB = b.el.getAttribute('tabindex');
const diff = tabIndexA - tabIndexB;
return diff === 0 ? a.idx - b.idx : diff;
}).map(({
el
}) => el);
const checkedRadio = {};
let prunedElements = [];
orderedElements.forEach(el => {
// For radio groups keep only the active radio
// If there is no active radio, keep only the checked radio
// If there is no checked radio, treat like everything else
if (el.type === 'radio' && el.name) {
// If the active element is part of the group, add only that
if (previousElement && previousElement.type === el.type && previousElement.name === el.name) {
if (el === previousElement) {
prunedElements.push(el);
}
return;
} // If we stumble upon a checked radio, remove the others
if (el.checked) {
prunedElements = prunedElements.filter(e => e.type !== el.type || e.name !== el.name);
prunedElements.push(el);
checkedRadio[el.name] = el;
return;
} // If we already found the checked one, skip
if (checkedRadio[el.name]) {
return;
}
}
prunedElements.push(el);
});
const index = prunedElements.findIndex(el => el === previousElement);
const nextElement = getNextElement(index, shift, prunedElements, focusTrap);
const shiftKeyInit = {
key: 'Shift',
keyCode: 16,
shiftKey: true
};
const tabKeyInit = {
key: 'Tab',
keyCode: 9,
shiftKey: shift
};
let continueToTab = true; // not sure how to make it so there's no previous element...
// istanbul ignore else
if (previousElement) {
// preventDefault on the shift key makes no difference
if (shift) _dom.fireEvent.keyDown(previousElement, { ...shiftKeyInit
});
continueToTab = _dom.fireEvent.keyDown(previousElement, { ...tabKeyInit
});
}
const keyUpTarget = !continueToTab && previousElement ? previousElement : nextElement;
if (continueToTab) {
if (nextElement === document.body) {
(0, _blur.blur)(previousElement);
} else {
(0, _focus.focus)(nextElement);
}
}
_dom.fireEvent.keyUp(keyUpTarget, { ...tabKeyInit
});
if (shift) {
_dom.fireEvent.keyUp(keyUpTarget, { ...shiftKeyInit,
shiftKey: false
});
}
}
/*
eslint
complexity: "off",
max-statements: "off",
*/

View file

@ -0,0 +1,867 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.type = type;
exports.specialCharMap = void 0;
var _dom = require("@testing-library/dom");
var _utils = require("./utils");
var _click = require("./click");
var _navigationKey = require("./keys/navigation-key");
// TODO: wrap in asyncWrapper
const modifierCallbackMap = { ...createModifierCallbackEntries({
name: 'shift',
key: 'Shift',
keyCode: 16,
modifierProperty: 'shiftKey'
}),
...createModifierCallbackEntries({
name: 'ctrl',
key: 'Control',
keyCode: 17,
modifierProperty: 'ctrlKey'
}),
...createModifierCallbackEntries({
name: 'alt',
key: 'Alt',
keyCode: 18,
modifierProperty: 'altKey'
}),
...createModifierCallbackEntries({
name: 'meta',
key: 'Meta',
keyCode: 93,
modifierProperty: 'metaKey'
}),
// capslock is inline because of the need to fire both keydown and keyup on use, while preserving the modifier state.
'{capslock}': function capslockOn({
currentElement,
eventOverrides
}) {
const newEventOverrides = {
modifierCapsLock: true
};
_dom.fireEvent.keyDown(currentElement(), {
key: 'CapsLock',
keyCode: 20,
which: 20,
...eventOverrides,
...newEventOverrides
});
_dom.fireEvent.keyUp(currentElement(), {
key: 'CapsLock',
keyCode: 20,
which: 20,
...eventOverrides,
...newEventOverrides
});
return {
eventOverrides: newEventOverrides
};
},
'{/capslock}': function capslockOff({
currentElement,
eventOverrides
}) {
const newEventOverrides = {
modifierCapsLock: false
};
_dom.fireEvent.keyDown(currentElement(), {
key: 'CapsLock',
keyCode: 20,
which: 20,
...eventOverrides,
...newEventOverrides
});
_dom.fireEvent.keyUp(currentElement(), {
key: 'CapsLock',
keyCode: 20,
which: 20,
...eventOverrides,
...newEventOverrides
});
return {
eventOverrides: newEventOverrides
};
}
};
const specialCharMap = {
arrowLeft: '{arrowleft}',
arrowRight: '{arrowright}',
arrowDown: '{arrowdown}',
arrowUp: '{arrowup}',
enter: '{enter}',
escape: '{esc}',
delete: '{del}',
backspace: '{backspace}',
home: '{home}',
end: '{end}',
selectAll: '{selectall}',
space: '{space}',
whitespace: ' '
};
exports.specialCharMap = specialCharMap;
const specialCharCallbackMap = {
[specialCharMap.arrowLeft]: (0, _navigationKey.navigationKey)('ArrowLeft'),
[specialCharMap.arrowRight]: (0, _navigationKey.navigationKey)('ArrowRight'),
[specialCharMap.arrowDown]: handleArrowDown,
[specialCharMap.arrowUp]: handleArrowUp,
[specialCharMap.home]: (0, _navigationKey.navigationKey)('Home'),
[specialCharMap.end]: (0, _navigationKey.navigationKey)('End'),
[specialCharMap.enter]: handleEnter,
[specialCharMap.escape]: handleEsc,
[specialCharMap.delete]: handleDel,
[specialCharMap.backspace]: handleBackspace,
[specialCharMap.selectAll]: handleSelectall,
[specialCharMap.space]: handleSpace,
[specialCharMap.whitespace]: handleSpace
};
function wait(time) {
return new Promise(resolve => setTimeout(() => resolve(), time));
} // this needs to be wrapped in the event/asyncWrapper for React's act and angular's change detection
// depending on whether it will be async.
async function type(element, text, {
delay = 0,
...options
} = {}) {
// we do not want to wrap in the asyncWrapper if we're not
// going to actually be doing anything async, so we only wrap
// if the delay is greater than 0
let result;
if (delay > 0) {
await (0, _dom.getConfig)().asyncWrapper(async () => {
result = await typeImpl(element, text, {
delay,
...options
});
});
} else {
result = typeImpl(element, text, {
delay,
...options
});
}
return result;
}
async function typeImpl(element, text, {
delay,
skipClick = false,
skipAutoClose = false,
initialSelectionStart,
initialSelectionEnd
}) {
if (element.disabled) return;
if (!skipClick) (0, _click.click)(element);
if ((0, _utils.isContentEditable)(element) && document.getSelection().rangeCount === 0) {
const range = document.createRange();
range.setStart(element, 0);
range.setEnd(element, 0);
document.getSelection().addRange(range);
} // The focused element could change between each event, so get the currently active element each time
const currentElement = () => (0, _utils.getActiveElement)(element.ownerDocument); // by default, a new element has it's selection start and end at 0
// but most of the time when people call "type", they expect it to type
// 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.
const value = (0, _utils.getValue)(currentElement());
const {
selectionStart,
selectionEnd
} = (0, _utils.getSelectionRange)(element);
if (value != null && selectionStart === 0 && selectionEnd === 0) {
(0, _utils.setSelectionRangeIfNecessary)(currentElement(), initialSelectionStart != null ? initialSelectionStart : value.length, initialSelectionEnd != null ? initialSelectionEnd : value.length);
}
const eventCallbacks = queueCallbacks();
await runCallbacks(eventCallbacks);
function queueCallbacks() {
const callbacks = [];
let remainingString = text;
while (remainingString) {
const {
callback,
remainingString: newRemainingString
} = getNextCallback(remainingString, skipAutoClose);
callbacks.push(callback);
remainingString = newRemainingString;
}
return callbacks;
}
async function runCallbacks(callbacks) {
const eventOverrides = {};
let prevWasMinus, prevWasPeriod, prevValue, typedValue;
for (const callback of callbacks) {
if (delay > 0) await wait(delay);
if (!currentElement().disabled) {
const returnValue = callback({
currentElement,
prevWasMinus,
prevWasPeriod,
prevValue,
eventOverrides,
typedValue
});
Object.assign(eventOverrides, returnValue == null ? void 0 : returnValue.eventOverrides);
prevWasMinus = returnValue == null ? void 0 : returnValue.prevWasMinus;
prevWasPeriod = returnValue == null ? void 0 : returnValue.prevWasPeriod;
prevValue = returnValue == null ? void 0 : returnValue.prevValue;
typedValue = returnValue == null ? void 0 : returnValue.typedValue;
}
}
}
}
function getNextCallback(remainingString, skipAutoClose) {
const modifierCallback = getModifierCallback(remainingString, skipAutoClose);
if (modifierCallback) {
return modifierCallback;
}
const specialCharCallback = getSpecialCharCallback(remainingString);
if (specialCharCallback) {
return specialCharCallback;
}
return getTypeCallback(remainingString);
}
function getModifierCallback(remainingString, skipAutoClose) {
const modifierKey = Object.keys(modifierCallbackMap).find(key => remainingString.startsWith(key));
if (!modifierKey) {
return null;
}
const callback = modifierCallbackMap[modifierKey]; // if this modifier has an associated "close" callback and the developer
// doesn't close it themselves, then we close it for them automatically
// Effectively if they send in: '{alt}a' then we type: '{alt}a{/alt}'
if (!skipAutoClose && callback.closeName && !remainingString.includes(callback.closeName)) {
remainingString += callback.closeName;
}
remainingString = remainingString.slice(modifierKey.length);
return {
callback,
remainingString
};
}
function getSpecialCharCallback(remainingString) {
const specialChar = Object.keys(specialCharCallbackMap).find(key => remainingString.startsWith(key));
if (!specialChar) {
return null;
}
return {
callback: specialCharCallbackMap[specialChar],
remainingString: remainingString.slice(specialChar.length)
};
}
function getTypeCallback(remainingString) {
const character = remainingString[0];
const callback = context => typeCharacter(character, context);
return {
callback,
remainingString: remainingString.slice(1)
};
}
function setSelectionRange({
currentElement,
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
const value = (0, _utils.getValue)(currentElement());
if (value === newValue) {
(0, _utils.setSelectionRangeIfNecessary)(currentElement(), newSelectionStart, newSelectionStart);
} else {
// If the currentValue is different than the expected newValue and we *can*
// change the selection range, than we should set it to the length of the
// currentValue to ensure that the browser behavior is mimicked.
(0, _utils.setSelectionRangeIfNecessary)(currentElement(), value.length, value.length);
}
}
function fireInputEventIfNeeded({
currentElement,
newValue,
newSelectionStart,
eventOverrides
}) {
const prevValue = (0, _utils.getValue)(currentElement());
if (!currentElement().readOnly && !(0, _utils.isClickableInput)(currentElement()) && newValue !== prevValue) {
if ((0, _utils.isContentEditable)(currentElement())) {
_dom.fireEvent.input(currentElement(), {
target: {
textContent: newValue
},
...eventOverrides
});
} else {
_dom.fireEvent.input(currentElement(), {
target: {
value: newValue
},
...eventOverrides
});
}
setSelectionRange({
currentElement,
newValue,
newSelectionStart
});
}
return {
prevValue
};
}
function typeCharacter(char, {
currentElement,
prevWasMinus = false,
prevWasPeriod = false,
prevValue = '',
typedValue = '',
eventOverrides
}) {
const key = char; // TODO: check if this also valid for characters with diacritic markers e.g. úé etc
const keyCode = char.charCodeAt(0);
let nextPrevWasMinus, nextPrevWasPeriod;
const textToBeTyped = typedValue + char;
const keyDownDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
if (keyDownDefaultNotPrevented) {
const keyPressDefaultNotPrevented = _dom.fireEvent.keyPress(currentElement(), {
key,
keyCode,
charCode: keyCode,
...eventOverrides
});
if ((0, _utils.getValue)(currentElement()) != null && keyPressDefaultNotPrevented) {
let newEntry = char;
if (prevWasMinus) {
newEntry = `-${char}`;
} else if (prevWasPeriod) {
newEntry = `${prevValue}.${char}`;
}
if ((0, _utils.isValidDateValue)(currentElement(), textToBeTyped)) {
newEntry = textToBeTyped;
}
const timeNewEntry = (0, _utils.buildTimeValue)(textToBeTyped);
if ((0, _utils.isValidInputTimeValue)(currentElement(), timeNewEntry)) {
newEntry = timeNewEntry;
}
const inputEvent = fireInputEventIfNeeded({ ...(0, _utils.calculateNewValue)(newEntry, currentElement()),
eventOverrides: {
data: key,
inputType: 'insertText',
...eventOverrides
},
currentElement
});
prevValue = inputEvent.prevValue;
if ((0, _utils.isValidDateValue)(currentElement(), textToBeTyped)) {
_dom.fireEvent.change(currentElement(), {
target: {
value: textToBeTyped
}
});
}
fireChangeForInputTimeIfValid(currentElement, prevValue, timeNewEntry); // typing "-" into a number input will not actually update the value
// so for the next character we type, the value should be set to
// `-${newEntry}`
// we also preserve the prevWasMinus when the value is unchanged due
// to typing an invalid character (typing "-a3" results in "-3")
// same applies for the decimal character.
if (currentElement().type === 'number') {
const newValue = (0, _utils.getValue)(currentElement());
if (newValue === prevValue && newEntry !== '-') {
nextPrevWasMinus = prevWasMinus;
} else {
nextPrevWasMinus = newEntry === '-';
}
if (newValue === prevValue && newEntry !== '.') {
nextPrevWasPeriod = prevWasPeriod;
} else {
nextPrevWasPeriod = newEntry === '.';
}
}
}
}
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
return {
prevWasMinus: nextPrevWasMinus,
prevWasPeriod: nextPrevWasPeriod,
prevValue,
typedValue: textToBeTyped
};
}
function fireChangeForInputTimeIfValid(currentElement, prevValue, timeNewEntry) {
if ((0, _utils.isValidInputTimeValue)(currentElement(), timeNewEntry) && prevValue !== timeNewEntry) {
_dom.fireEvent.change(currentElement(), {
target: {
value: timeNewEntry
}
});
}
} // yes, calculateNewBackspaceValue and calculateNewValue look extremely similar
// and you may be tempted to create a shared abstraction.
// If you, brave soul, decide to so endevor, please increment this count
// when you inevitably fail: 1
function calculateNewBackspaceValue(element) {
const {
selectionStart,
selectionEnd
} = (0, _utils.getSelectionRange)(element);
const value = (0, _utils.getValue)(element);
let newValue, newSelectionStart;
if (selectionStart === null) {
// at the end of an input type that does not support selection ranges
// https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
newValue = value.slice(0, value.length - 1);
newSelectionStart = selectionStart - 1;
} else if (selectionStart === selectionEnd) {
if (selectionStart === 0) {
// at the beginning of the input
newValue = value;
newSelectionStart = selectionStart;
} else if (selectionStart === value.length) {
// at the end of the input
newValue = value.slice(0, value.length - 1);
newSelectionStart = selectionStart - 1;
} else {
// in the middle of the input
newValue = value.slice(0, selectionStart - 1) + value.slice(selectionEnd);
newSelectionStart = selectionStart - 1;
}
} else {
// we have something selected
const firstPart = value.slice(0, selectionStart);
newValue = firstPart + value.slice(selectionEnd);
newSelectionStart = firstPart.length;
}
return {
newValue,
newSelectionStart
};
}
function calculateNewDeleteValue(element) {
const {
selectionStart,
selectionEnd
} = (0, _utils.getSelectionRange)(element);
const value = (0, _utils.getValue)(element);
let newValue;
if (selectionStart === null) {
// at the end of an input type that does not support selection ranges
// https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
newValue = value;
} else if (selectionStart === selectionEnd) {
if (selectionStart === 0) {
// at the beginning of the input
newValue = value.slice(1);
} else if (selectionStart === value.length) {
// at the end of the input
newValue = value;
} else {
// in the middle of the input
newValue = value.slice(0, selectionStart) + value.slice(selectionEnd + 1);
}
} else {
// we have something selected
const firstPart = value.slice(0, selectionStart);
newValue = firstPart + value.slice(selectionEnd);
}
return {
newValue,
newSelectionStart: selectionStart
};
}
function createModifierCallbackEntries({
name,
key,
keyCode,
modifierProperty
}) {
const openName = `{${name}}`;
const closeName = `{/${name}}`;
function open({
currentElement,
eventOverrides
}) {
const newEventOverrides = {
[modifierProperty]: true
};
_dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides,
...newEventOverrides
});
return {
eventOverrides: newEventOverrides
};
}
open.closeName = closeName;
function close({
currentElement,
eventOverrides
}) {
const newEventOverrides = {
[modifierProperty]: false
};
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides,
...newEventOverrides
});
return {
eventOverrides: newEventOverrides
};
}
return {
[openName]: open,
[closeName]: close
};
}
function handleEnter({
currentElement,
eventOverrides
}) {
const key = 'Enter';
const keyCode = 13;
const keyDownDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
if (keyDownDefaultNotPrevented) {
const keyPressDefaultNotPrevented = _dom.fireEvent.keyPress(currentElement(), {
key,
keyCode,
charCode: keyCode,
...eventOverrides
});
if (keyPressDefaultNotPrevented) {
if ((0, _utils.isClickableInput)(currentElement()) || // Links with href defined should handle Enter the same as a click
(0, _utils.isInstanceOfElement)(currentElement(), 'HTMLAnchorElement') && currentElement().href) {
_dom.fireEvent.click(currentElement(), { ...eventOverrides
});
}
if (currentElement().tagName === 'TEXTAREA') {
const {
newValue,
newSelectionStart
} = (0, _utils.calculateNewValue)('\n', currentElement());
_dom.fireEvent.input(currentElement(), {
target: {
value: newValue
},
inputType: 'insertLineBreak',
...eventOverrides
});
setSelectionRange({
currentElement,
newValue,
newSelectionStart
});
}
if (currentElement().tagName === 'INPUT' && currentElement().form && (currentElement().form.querySelectorAll('input').length === 1 || currentElement().form.querySelector('input[type="submit"]') || currentElement().form.querySelector('button[type="submit"]'))) {
_dom.fireEvent.submit(currentElement().form);
}
}
}
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
}
function handleEsc({
currentElement,
eventOverrides
}) {
const key = 'Escape';
const keyCode = 27;
_dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
}); // NOTE: Browsers do not fire a keypress on meta key presses
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
}
function handleDel({
currentElement,
eventOverrides
}) {
const key = 'Delete';
const keyCode = 46;
const keyPressDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
if (keyPressDefaultNotPrevented) {
fireInputEventIfNeeded({ ...calculateNewDeleteValue(currentElement()),
eventOverrides: {
inputType: 'deleteContentForward',
...eventOverrides
},
currentElement
});
}
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
}
function handleBackspace({
currentElement,
eventOverrides
}) {
const key = 'Backspace';
const keyCode = 8;
const keyPressDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
if (keyPressDefaultNotPrevented) {
fireInputEventIfNeeded({ ...calculateNewBackspaceValue(currentElement()),
eventOverrides: {
inputType: 'deleteContentBackward',
...eventOverrides
},
currentElement
});
}
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
}
function handleSelectall({
currentElement
}) {
currentElement().setSelectionRange(0, (0, _utils.getValue)(currentElement()).length);
}
function handleSpace(context) {
if ((0, _utils.isClickableInput)(context.currentElement())) {
handleSpaceOnClickable(context);
return;
}
typeCharacter(' ', context);
}
function handleSpaceOnClickable({
currentElement,
eventOverrides
}) {
const key = ' ';
const keyCode = 32;
const keyDownDefaultNotPrevented = _dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
if (keyDownDefaultNotPrevented) {
_dom.fireEvent.keyPress(currentElement(), {
key,
keyCode,
charCode: keyCode,
...eventOverrides
});
}
const keyUpDefaultNotPrevented = _dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
if (keyDownDefaultNotPrevented && keyUpDefaultNotPrevented) {
_dom.fireEvent.click(currentElement(), { ...eventOverrides
});
}
}
function handleArrowDown({
currentElement,
eventOverrides
}) {
const key = 'ArrowDown';
const keyCode = 40;
_dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
}
function handleArrowUp({
currentElement,
eventOverrides
}) {
const key = 'ArrowUp';
const keyCode = 38;
_dom.fireEvent.keyDown(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
_dom.fireEvent.keyUp(currentElement(), {
key,
keyCode,
which: keyCode,
...eventOverrides
});
}

View file

@ -0,0 +1,74 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.upload = upload;
var _dom = require("@testing-library/dom");
var _click = require("./click");
var _blur = require("./blur");
var _focus = require("./focus");
function upload(element, fileOrFiles, init, {
applyAccept = false
} = {}) {
if (element.disabled) return;
(0, _click.click)(element, init);
const input = element.tagName === 'LABEL' ? element.control : element;
const files = (Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles]).filter(file => !applyAccept || isAcceptableFile(file, element.accept)).slice(0, input.multiple ? undefined : 1); // blur fires when the file selector pops up
(0, _blur.blur)(element, init); // focus fires when they make their selection
(0, _focus.focus)(element, init); // do not fire an input event if the file selection does not change
if (files.length === input.files.length && files.every((f, i) => f === input.files.item(i))) {
return;
} // the event fired in the browser isn't actually an "input" or "change" event
// but a new Event with a type set to "input" and "change"
// Kinda odd...
const inputFiles = {
length: files.length,
item: index => files[index],
...files
};
(0, _dom.fireEvent)(input, (0, _dom.createEvent)('input', input, {
target: {
files: inputFiles
},
bubbles: true,
cancelable: false,
composed: true,
...init
}));
_dom.fireEvent.change(input, {
target: {
files: inputFiles
},
...init
});
}
function isAcceptableFile(file, accept) {
if (!accept) {
return true;
}
const wildcards = ['audio/*', 'image/*', 'video/*'];
return accept.split(',').some(acceptToken => {
if (acceptToken[0] === '.') {
// tokens starting with a dot represent a file extension
return file.name.endsWith(acceptToken);
} else if (wildcards.includes(acceptToken)) {
return file.type.startsWith(acceptToken.substr(0, acceptToken.length - 1));
}
return file.type === acceptToken;
});
}

View file

@ -0,0 +1,354 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isFocusable = isFocusable;
exports.isClickableInput = isClickableInput;
exports.getMouseEventOptions = getMouseEventOptions;
exports.isLabelWithInternallyDisabledControl = isLabelWithInternallyDisabledControl;
exports.getActiveElement = getActiveElement;
exports.calculateNewValue = calculateNewValue;
exports.setSelectionRangeIfNecessary = setSelectionRangeIfNecessary;
exports.eventWrapper = eventWrapper;
exports.isValidDateValue = isValidDateValue;
exports.isValidInputTimeValue = isValidInputTimeValue;
exports.buildTimeValue = buildTimeValue;
exports.getValue = getValue;
exports.getSelectionRange = getSelectionRange;
exports.isContentEditable = isContentEditable;
exports.isInstanceOfElement = isInstanceOfElement;
exports.isVisible = isVisible;
exports.FOCUSABLE_SELECTOR = void 0;
var _dom = require("@testing-library/dom");
var _helpers = require("@testing-library/dom/dist/helpers");
// isInstanceOfElement can be removed once the peerDependency for @testing-library/dom is bumped to a version that includes https://github.com/testing-library/dom-testing-library/pull/885
/**
* Check if an element is of a given type.
*
* @param {Element} element The element to test
* @param {string} elementType Constructor name. E.g. 'HTMLSelectElement'
*/
function isInstanceOfElement(element, elementType) {
try {
const window = (0, _helpers.getWindowFromNode)(element); // Window usually has the element constructors as properties but is not required to do so per specs
if (typeof window[elementType] === 'function') {
return element instanceof window[elementType];
}
} catch (e) {// The document might not be associated with a window
} // Fall back to the constructor name as workaround for test environments that
// a) not associate the document with a window
// b) not provide the constructor as property of window
if (/^HTML(\w+)Element$/.test(element.constructor.name)) {
return element.constructor.name === elementType;
} // The user passed some node that is not created in a browser-like environment
throw new Error(`Unable to verify if element is instance of ${elementType}. Please file an issue describing your test environment: https://github.com/testing-library/dom-testing-library/issues/new`);
}
function isMousePressEvent(event) {
return event === 'mousedown' || event === 'mouseup' || event === 'click' || event === 'dblclick';
}
function invert(map) {
const res = {};
for (const key of Object.keys(map)) {
res[map[key]] = key;
}
return res;
} // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
const BUTTONS_TO_NAMES = {
0: 'none',
1: 'primary',
2: 'secondary',
4: 'auxiliary'
};
const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES); // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
const BUTTON_TO_NAMES = {
0: 'primary',
1: 'auxiliary',
2: 'secondary'
};
const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES);
function convertMouseButtons(event, init, property, mapping) {
if (!isMousePressEvent(event)) {
return 0;
}
if (init[property] != null) {
return init[property];
}
if (init.buttons != null) {
// not sure how to test this. Feel free to try and add a test if you want.
// istanbul ignore next
return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0;
}
if (init.button != null) {
// not sure how to test this. Feel free to try and add a test if you want.
// istanbul ignore next
return mapping[BUTTON_TO_NAMES[init.button]] || 0;
}
return property != 'button' && isMousePressEvent(event) ? 1 : 0;
}
function getMouseEventOptions(event, init, clickCount = 0) {
init = init || {};
return { ...init,
// https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
detail: event === 'mousedown' || event === 'mouseup' || event === 'click' ? 1 + clickCount : clickCount,
buttons: convertMouseButtons(event, init, 'buttons', NAMES_TO_BUTTONS),
button: convertMouseButtons(event, init, 'button', NAMES_TO_BUTTON)
};
} // Absolutely NO events fire on label elements that contain their control
// if that control is disabled. NUTS!
// no joke. There are NO events for: <label><input disabled /><label>
function isLabelWithInternallyDisabledControl(element) {
var _element$control;
return element.tagName === 'LABEL' && ((_element$control = element.control) == null ? void 0 : _element$control.disabled) && element.contains(element.control);
}
function getActiveElement(document) {
const activeElement = document.activeElement;
if (activeElement != null && activeElement.shadowRoot) {
return getActiveElement(activeElement.shadowRoot);
} else {
return activeElement;
}
}
function supportsMaxLength(element) {
if (element.tagName === 'TEXTAREA') return true;
if (element.tagName === 'INPUT') {
const type = element.getAttribute('type'); // Missing value default is "text"
if (!type) return true; // https://html.spec.whatwg.org/multipage/input.html#concept-input-apply
if (type.match(/email|password|search|telephone|text|url/)) return true;
}
return false;
}
function getSelectionRange(element) {
if (isContentEditable(element)) {
const range = element.ownerDocument.getSelection().getRangeAt(0);
return {
selectionStart: range.startOffset,
selectionEnd: range.endOffset
};
}
return {
selectionStart: element.selectionStart,
selectionEnd: element.selectionEnd
};
} //jsdom is not supporting isContentEditable
function isContentEditable(element) {
return element.hasAttribute('contenteditable') && (element.getAttribute('contenteditable') == 'true' || element.getAttribute('contenteditable') == '');
}
function getValue(element) {
if (isContentEditable(element)) {
return element.textContent;
}
return element.value;
}
function calculateNewValue(newEntry, element) {
var _element$getAttribute;
const {
selectionStart,
selectionEnd
} = getSelectionRange(element);
const value = getValue(element); // can't use .maxLength property because of a jsdom bug:
// https://github.com/jsdom/jsdom/issues/2927
const maxLength = Number((_element$getAttribute = element.getAttribute('maxlength')) != null ? _element$getAttribute : -1);
let newValue, newSelectionStart;
if (selectionStart === null) {
// at the end of an input type that does not support selection ranges
// https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
newValue = value + newEntry;
} else if (selectionStart === selectionEnd) {
if (selectionStart === 0) {
// at the beginning of the input
newValue = newEntry + value;
} else if (selectionStart === value.length) {
// at the end of the input
newValue = value + newEntry;
} else {
// in the middle of the input
newValue = value.slice(0, selectionStart) + newEntry + value.slice(selectionEnd);
}
newSelectionStart = selectionStart + newEntry.length;
} else {
// we have something selected
const firstPart = value.slice(0, selectionStart) + newEntry;
newValue = firstPart + value.slice(selectionEnd);
newSelectionStart = firstPart.length;
}
if (element.type === 'date' && !isValidDateValue(element, newValue)) {
newValue = value;
}
if (element.type === 'time' && !isValidInputTimeValue(element, newValue)) {
if (isValidInputTimeValue(element, newEntry)) {
newValue = newEntry;
} else {
newValue = value;
}
}
if (!supportsMaxLength(element) || maxLength < 0) {
return {
newValue,
newSelectionStart
};
} else {
return {
newValue: newValue.slice(0, maxLength),
newSelectionStart: newSelectionStart > maxLength ? maxLength : newSelectionStart
};
}
}
function setSelectionRangeIfNecessary(element, newSelectionStart, newSelectionEnd) {
const {
selectionStart,
selectionEnd
} = getSelectionRange(element);
if (!isContentEditable(element) && (!element.setSelectionRange || selectionStart === null)) {
// cannot set selection
return;
}
if (selectionStart !== newSelectionStart || selectionEnd !== newSelectionStart) {
if (isContentEditable(element)) {
const range = element.ownerDocument.createRange();
range.selectNodeContents(element);
range.setStart(element.firstChild, newSelectionStart);
range.setEnd(element.firstChild, newSelectionEnd);
element.ownerDocument.getSelection().removeAllRanges();
element.ownerDocument.getSelection().addRange(range);
} else {
element.setSelectionRange(newSelectionStart, newSelectionEnd);
}
}
}
const FOCUSABLE_SELECTOR = ['input:not([type=hidden]):not([disabled])', 'button:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[contenteditable=""]', '[contenteditable="true"]', 'a[href]', '[tabindex]:not([disabled])'].join(', ');
exports.FOCUSABLE_SELECTOR = FOCUSABLE_SELECTOR;
function isFocusable(element) {
return !isLabelWithInternallyDisabledControl(element) && (element == null ? void 0 : element.matches(FOCUSABLE_SELECTOR));
}
const CLICKABLE_INPUT_TYPES = ['button', 'color', 'file', 'image', 'reset', 'submit'];
function isClickableInput(element) {
return element.tagName === 'BUTTON' || isInstanceOfElement(element, 'HTMLInputElement') && CLICKABLE_INPUT_TYPES.includes(element.type);
}
function isVisible(element) {
const getComputedStyle = (0, _helpers.getWindowFromNode)(element).getComputedStyle;
for (; element && element.ownerDocument; element = element.parentNode) {
const display = getComputedStyle(element).display;
if (display === 'none') {
return false;
}
}
return true;
}
function eventWrapper(cb) {
let result;
(0, _dom.getConfig)().eventWrapper(() => {
result = cb();
});
return result;
}
function isValidDateValue(element, value) {
if (element.type !== 'date') return false;
const clone = element.cloneNode();
clone.value = value;
return clone.value === value;
}
function buildTimeValue(value) {
function build(onlyDigitsValue, index) {
const hours = onlyDigitsValue.slice(0, index);
const validHours = Math.min(parseInt(hours, 10), 23);
const minuteCharacters = onlyDigitsValue.slice(index);
const parsedMinutes = parseInt(minuteCharacters, 10);
const validMinutes = Math.min(parsedMinutes, 59);
return `${validHours.toString().padStart(2, '0')}:${validMinutes.toString().padStart(2, '0')}`;
}
const onlyDigitsValue = value.replace(/\D/g, '');
if (onlyDigitsValue.length < 2) {
return value;
}
const firstDigit = parseInt(onlyDigitsValue[0], 10);
const secondDigit = parseInt(onlyDigitsValue[1], 10);
if (firstDigit >= 3 || firstDigit === 2 && secondDigit >= 4) {
let index;
if (firstDigit >= 3) {
index = 1;
} else {
index = 2;
}
return build(onlyDigitsValue, index);
}
if (value.length === 2) {
return value;
}
return build(onlyDigitsValue, 2);
}
function isValidInputTimeValue(element, timeValue) {
if (element.type !== 'time') return false;
const clone = element.cloneNode();
clone.value = timeValue;
return clone.value === timeValue;
}

View file

@ -0,0 +1,82 @@
{
"name": "@testing-library/user-event",
"version": "12.8.3",
"description": "Fire events the same way the user does",
"main": "dist/index.js",
"typings": "typings/index.d.ts",
"keywords": [
"react-testing-library",
"dom-testing-library",
"react",
"testing"
],
"author": "Giorgio Polvara <polvara@gmail.com>",
"license": "MIT",
"engines": {
"node": ">=10",
"npm": ">=6"
},
"repository": {
"type": "git",
"url": "https://github.com/testing-library/user-event"
},
"bugs": {
"url": "https://github.com/testing-library/user-event/issues"
},
"homepage": "https://github.com/testing-library/user-event#readme",
"files": [
"dist",
"typings/index.d.ts"
],
"scripts": {
"build": "kcd-scripts build",
"lint": "kcd-scripts lint",
"setup": "npm install && npm run validate -s",
"test": "kcd-scripts test",
"test:debug": "kcd-scripts --inspect-brk test --runInBand",
"test:update": "npm test -- --updateSnapshot --coverage",
"validate": "kcd-scripts validate",
"typecheck": "kcd-scripts typecheck --build typings"
},
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"devDependencies": {
"@testing-library/dom": "^7.28.1",
"@testing-library/jest-dom": "^5.11.6",
"@types/estree": "0.0.45",
"is-ci": "^2.0.0",
"jest-serializer-ansi": "^1.0.3",
"kcd-scripts": "^7.5.1",
"typescript": "^4.1.2"
},
"peerDependencies": {
"@testing-library/dom": ">=7.21.4"
},
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
"rules": {
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/tabindex-no-positive": "off",
"no-func-assign": "off",
"no-return-assign": "off",
"react/prop-types": "off",
"testing-library/no-dom-import": "off"
},
"overrides": [
{
"files": [
"**/__tests__/**"
],
"rules": {
"no-console": "off"
}
}
]
},
"eslintIgnore": [
"node_modules",
"coverage",
"dist"
]
}

View file

@ -0,0 +1,97 @@
// Definitions by: Wu Haotian <https://github.com/whtsky>
export interface ITypeOpts {
skipClick?: boolean
skipAutoClose?: boolean
delay?: number
initialSelectionStart?: number
initialSelectionEnd?: number
}
export interface ITabUserOptions {
shift?: boolean
focusTrap?: Document | Element
}
export type TargetElement = Element | Window
export type FilesArgument = File | File[]
export type UploadInitArgument = {
clickInit?: MouseEventInit
changeInit?: Event
}
export interface IClickOptions {
skipHover?: boolean
clickCount?: number
}
export interface IUploadOptions {
applyAccept?: boolean
}
declare const userEvent: {
clear: (element: TargetElement) => void
click: (
element: TargetElement,
init?: MouseEventInit,
options?: IClickOptions,
) => void
dblClick: (
element: TargetElement,
init?: MouseEventInit,
options?: IClickOptions,
) => void
selectOptions: (
element: TargetElement,
values: string | string[] | HTMLElement | HTMLElement[],
init?: MouseEventInit,
) => void
deselectOptions: (
element: TargetElement,
values: string | string[] | HTMLElement | HTMLElement[],
init?: MouseEventInit,
) => void
upload: (
element: TargetElement,
files: FilesArgument,
init?: UploadInitArgument,
options?: IUploadOptions,
) => void
type: <T extends ITypeOpts>(
element: TargetElement,
text: string,
userOpts?: T,
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
) => T extends {delay: number} ? Promise<void> : void
tab: (userOpts?: ITabUserOptions) => void
paste: (
element: TargetElement,
text: string,
init?: MouseEventInit,
pasteOptions?: {
initialSelectionStart?: number
initialSelectionEnd?: number
},
) => void
hover: (element: TargetElement, init?: MouseEventInit) => void
unhover: (element: TargetElement, init?: MouseEventInit) => void
}
export default userEvent
export enum specialChars {
arrowLeft = '{arrowleft}',
arrowRight = '{arrowright}',
arrowDown = '{arrowdown}',
arrowUp = '{arrowup}',
enter = '{enter}',
escape = '{esc}',
delete = '{del}',
backspace = '{backspace}',
home = '{home}',
end = '{end}',
selectAll = '{selectall}',
space = '{space}',
whitespace = ' ',
}