Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make several API and implementation improvements #348

Merged
merged 7 commits into from Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
110 changes: 68 additions & 42 deletions README.md
Expand Up @@ -52,16 +52,17 @@ change the state of the checkbox.

- [Installation](#installation)
- [API](#api)
- [`click(element)`](#clickelement)
- [`dblClick(element)`](#dblclickelement)
- [`async type(element, text, [options])`](#async-typeelement-text-options)
- [`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)
- [`toggleSelectOptions(element, values)`](#toggleselectoptionselement-values)
- [`deselectOptions(element, values)`](#deselectoptionselement-values)
- [`tab({shift, focusTrap})`](#tabshift-focustrap)
- [`async hover(element)`](#async-hoverelement)
- [`async unhover(element)`](#async-unhoverelement)
- [`hover(element)`](#hoverelement)
- [`unhover(element)`](#unhoverelement)
- [`paste(element, text, eventInit, options)`](#pasteelement-text-eventinit-options)
- [Issues](#issues)
- [Contributors 鉁╙(#contributors-)
- [LICENSE](#license)
Expand Down Expand Up @@ -94,7 +95,7 @@ var userEvent = require('@testing-library/user-event')

## API

### `click(element)`
### `click(element, eventInit, options)`

Clicks `element`, depending on what `element` is it can have different side
effects.
Expand Down Expand Up @@ -127,7 +128,10 @@ See the
[`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent)
constructor documentation for more options.

### `dblClick(element)`
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.
Expand All @@ -147,7 +151,7 @@ test('double click', () => {
})
```

### `async type(element, text, [options])`
### `type(element, text, [options])`

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

Expand All @@ -156,38 +160,43 @@ import React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

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

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

If `options.allAtOnce` is `true`, `type` will write `text` at once rather than
one character at the time. `false` is the default value.

`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.
different behavior for fast or slow users. If you do this, you need to make sure
to `await`!

`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). |
| `{esc}` | Escape | N/A | |
| `{backspace}` | Backspace | N/A | Will delete the previous character (or the characters within the `selectedRange`). |
| `{shift}` | Shift | `shiftKey` | Does **not** capitalize following characters. |
| `{ctrl}` | Control | `ctrlKey` | |
| `{alt}` | Alt | `altKey` | |
| `{meta}` | OS | `metaKey` | |
| Text string | Key | Modifier | Notes |
| ------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{enter}` | Enter | N/A | Will insert a newline character (`<textarea />` only). |
| `{esc}` | Escape | N/A | |
| `{backspace}` | Backspace | N/A | Will delete the previous character (or the characters within the `selectedRange`). |
| `{del}` | Delete | N/A | Will delete the next character (or the characters within the `selectedRange`) |
| `{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) |
| `{shift}` | Shift | `shiftKey` | Does **not** capitalize following characters. |
| `{ctrl}` | Control | `ctrlKey` | |
| `{alt}` | Alt | `altKey` | |
| `{meta}` | OS | `metaKey` | |

> **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.).
> 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 -->

Expand Down Expand Up @@ -308,16 +317,17 @@ userEvent.selectOptions(screen.getByTestId('select-multiple'), [
])
```

### `toggleSelectOptions(element, values)`
### `deselectOptions(element, values)`

Toggle the specified option(s) of a `<select multiple>` element.
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('toggleSelectOptions', () => {
test('deselectOptions', () => {
render(
<select multiple>
<option value="1">A</option>
Expand All @@ -326,14 +336,12 @@ test('toggleSelectOptions', () => {
</select>,
)

userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1', '3'])

expect(screen.getByText('A').selected).toBe(true)
expect(screen.getByText('C').selected).toBe(true)

userEvent.toggleSelectOptions(screen.getByRole('listbox'), ['1'])

expect(screen.getByText('A').selected).toBe(false)
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'])
})
```

Expand Down Expand Up @@ -397,7 +405,7 @@ it('should cycle elements in document tab order', () => {
})
```

### `async hover(element)`
### `hover(element)`

Hovers over `element`.

Expand All @@ -407,26 +415,43 @@ import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Tooltip from '../tooltip'

test('hover', async () => {
test('hover', () => {
const messageText = 'Hello'
render(
<Tooltip messageText={messageText}>
<TrashIcon aria-label="Delete" />
</Tooltip>,
)

await userEvent.hover(screen.getByLabelText(/delete/i))
userEvent.hover(screen.getByLabelText(/delete/i))
expect(screen.getByText(messageText)).toBeInTheDocument()
await userEvent.unhover(screen.getByLabelText(/delete/i))
userEvent.unhover(screen.getByLabelText(/delete/i))
expect(screen.queryByText(messageText)).not.toBeInTheDocument()
})
```

### `async unhover(element)`
### `unhover(element)`

Unhovers out of `element`.

> See [above](#async-hoverelement) for an example
> 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`).

## Issues

Expand Down Expand Up @@ -503,6 +528,7 @@ Thanks goes to these people ([emoji key][emojis]):

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors][all-contributors] specification.
Expand Down
8 changes: 0 additions & 8 deletions jest.config.js
Expand Up @@ -3,12 +3,4 @@ const config = require('kcd-scripts/jest')
module.exports = {
...config,
testEnvironment: 'jest-environment-jsdom',
coverageThreshold: {
global: {
branches: 93,
functions: 93,
lines: 93,
statements: 93,
},
},
}
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -41,9 +41,10 @@
"@babel/runtime": "^7.10.2"
},
"devDependencies": {
"@testing-library/dom": "^7.9.0",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/dom": "^7.16.0",
"@testing-library/jest-dom": "^5.10.1",
"is-ci": "^2.0.0",
"jest-serializer-ansi": "^1.0.3",
"kcd-scripts": "^6.2.3"
},
"peerDependencies": {
Expand Down
8 changes: 8 additions & 0 deletions src/.eslintrc
@@ -0,0 +1,8 @@
{
"rules": {
// everything in this directory is intentionally running in series, not parallel
// because user's cannot fire multiple events at the same time and we need
// all events fired in a predictable order.
"no-await-in-loop": "off"
}
}
50 changes: 50 additions & 0 deletions src/__mocks__/@testing-library/dom.js
@@ -0,0 +1,50 @@
// this helps us track what the state is before and after an event is fired
// this is needed for determining the snapshot values
const actual = jest.requireActual('@testing-library/dom')

function getTrackedElementValues(element) {
return {
value: element.value,
checked: element.checked,
selectionStart: element.selectionStart,
selectionEnd: element.selectionEnd,

// unfortunately, changing a select option doesn't happen within fireEvent
// but rather imperatively via `options.selected = newValue`
// because of this we don't (currently) have a way to track before/after
// in a given fireEvent call.
}
}

function wrapWithTestData(fn) {
return (element, init) => {
const before = getTrackedElementValues(element)
const testData = {before}

// put it on the element so the event handler can grab it
element.testData = testData
const result = fn(element, init)

const after = getTrackedElementValues(element)
Object.assign(testData, {after})

// elete the testData for the next event
delete element.testData
return result
}
}

const mockFireEvent = wrapWithTestData(actual.fireEvent)

for (const key of Object.keys(actual.fireEvent)) {
if (typeof actual.fireEvent[key] === 'function') {
mockFireEvent[key] = wrapWithTestData(actual.fireEvent[key], key)
} else {
mockFireEvent[key] = actual.fireEvent[key]
}
}

module.exports = {
...actual,
fireEvent: mockFireEvent,
}
66 changes: 66 additions & 0 deletions src/__tests__/blur.js
@@ -0,0 +1,66 @@
import {blur} from '../blur'
import {focus} from '../focus'
import {setup} from './helpers/utils'

test('blur a button', () => {
const {element, getEventSnapshot, clearEventCalls} = setup(`<button />`)
focus(element)
clearEventCalls()
blur(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: button

button - blur
button - focusout
`)
expect(element).not.toHaveFocus()
})

test('no events fired on an unblurable input', () => {
const {element, getEventSnapshot, clearEventCalls} = setup(`<div />`)
focus(element)
clearEventCalls()
blur(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: div`,
)
expect(element).not.toHaveFocus()
})

test('blur with tabindex', () => {
const {element, getEventSnapshot, clearEventCalls} = setup(
`<div tabindex="0" />`,
)
focus(element)
clearEventCalls()
blur(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: div

div - blur
div - focusout
`)
expect(element).not.toHaveFocus()
})

test('no events fired on a disabled blurable input', () => {
const {element, getEventSnapshot, clearEventCalls} = setup(
`<button disabled />`,
)
focus(element)
clearEventCalls()
blur(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: button`,
)
expect(element).not.toHaveFocus()
})

test('no events fired if the element is not focused', () => {
const {element, getEventSnapshot} = setup(`<button />`)
blur(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: button`,
)
expect(element).not.toHaveFocus()
})