Skip to content

feat: document user-event v14 #980

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jan 10, 2022
6 changes: 5 additions & 1 deletion docs/ecosystem-user-event.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
id: ecosystem-user-event
title: user-event
title: user-event v13
---

import Tabs from '@theme/Tabs'
Expand All @@ -10,6 +10,10 @@ import TabItem from '@theme/TabItem'
advanced simulation of browser interactions than the built-in
[`fireEvent`](dom-testing-library/api-events.mdx#fireevent) method.

> This page describes `user-event@13.5.0`.
If you are starting or actively working on a project,
we recommend to use [`user-event@14.0.0-beta`](user-event/intro) instead, as it includes important bug fixes and new features.

## Installation

<Tabs defaultValue="npm" values={[{ label: 'npm', value: 'npm' }, { label: 'Yarn', value: 'yarn' }]}>
Expand Down
49 changes: 49 additions & 0 deletions docs/user-event/api-clipboard.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
id: clipboard
title: Clipboard
---

Note that the
[Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard) is
usually not available outside of secure context.
To enable testing of workflows involving the clipboard,
[`userEvent.setup()`](setup) replaces `window.navigator.clipboard` with a stub.

## copy()

```ts
copy(): Promise<DataTransfer|undefined>
```

Copy the current selection.

If [`writeToClipboard`](options#writetoclipboard) is `true`, this will also
write the data to the `Clipboard`.

## cut()

```ts
cut(): Promise<DataTransfer|undefined>
```

Cut the current selection.

If [`writeToClipboard`](options#writetoclipboard) is `true`, this will also
write the data to the `Clipboard`.

When performed in editable context, it removes the selected content from the
document.

## paste()

```ts
paste(clipboardData?: DataTransfer|string): Promise<void>
```

Paste data into the document.

When called without `clipboardData`, the content to be pasted is read from the
`Clipboard`.

When performed in editable context, the pasted content is inserted into to the
document.
83 changes: 83 additions & 0 deletions docs/user-event/api-convenience.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
id: convenience
title: Convenience APIs
---

The following APIs are shortcuts for equivalent calls to the underlying
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure these API's should be "hidden" in the convenience section because most of the users will probably be using them.
Either we should think of a way to rename them, change the order of the docs so they'll be first or mention them in the pointer and keyboard pages.

Copy link
Member Author

@ph-fritsche ph-fritsche Jan 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't know. On one hand they exist because they are easy to use and so - yes - they are probably used often, but on the other hand they are self-explanatory and merely documented for sake of completeness. The auto-completion in IDE would probably be enough.

Maybe this could be solved by another blog post walking through some examples with heavy use of these convenience methods and mentioning the post with a short excerpt in the introduction.
A mention in pointer and keyboard might also be fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we should rely on the IDE auto-completion for this one and IMO the package's functionality should be easily found in the docs and not in a personal blog post (but we should also think of writing a blog post about user-event 14 in the testing-library blog).

I strongly prefer a mention in pointer and keyboard pages :)

[`pointer()`](pointer) and [`keyboard()`](keyboard) APIs.

## Clicks

### click()

```ts
click(element: Element): Promise<void>
```

```js
pointer([{target: element}, {keys: '[MouseLeft]', target: element}])
```

The first action might be skipped per [`skipHover`](options#skiphover).

### dblClick()

```ts
dblClick(element: Element): Promise<void>
```

```js
pointer([{target: element}, {keys: '[MouseLeft][MouseLeft]', target: element}])
```

### tripleClick()

```ts
tripleClick(element: Element): Promise<void>
```

```js
pointer([
{target: element},
{keys: '[MouseLeft][MouseLeft][MouseLeft]', target: element},
])
```

## Mouse movement

### hover()

```ts
hover(element: Element): Promise<void>
```

```js
pointer({target: element})
```

### unhover()

```ts
hover(element: Element): Promise<void>
```

```js
pointer({target: element.ownerDocument.body})
```

## Keyboard

### tab()

```ts
tab(options: {shift?: boolean})
```

```js
// without shift
keyboard('{Tab}')
// with shift=true
keyboard('{Shift>}{Tab}{/Shift}')
// with shift=false
keyboard('[/ShiftLeft][/ShiftRight]{Tab}')
```
83 changes: 83 additions & 0 deletions docs/user-event/api-keyboard.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
id: keyboard
title: Keyboard
---

```ts
keyboard(input: KeyboardInput): Promise<void>
```

The `keyboard` API allows to simulate interactions with a keyboard. It accepts a
`string` describing the key actions.

Keystrokes can be described:

- Per printable character
```js
keyboard('foo') // translates to: f, o, o
```
The brackets `{` and `[` are used as special characters and can be referenced
by doubling them.
```js
keyboard('{{a[[') // translates to: {, a, [
```
- Per
[KeyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)

```js
keyboard('{Shift}{f}{o}{o}') // translates to: Shift, f, o, o
```

This does not keep any key pressed. So `Shift` will be lifted before pressing
`f`.

Characters with special meaning inside the key descriptor can be escaped by
prefixing them with a backslash `\`.

```js
keyboard('{}}') // translates to: }
```

- Per
[KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code)
```js
keyboard('[ShiftLeft][KeyF][KeyO][KeyO]') // translates to: Shift, f, o, o
```

Keys can be kept pressed by adding a `>` to the end of the descriptor.
If this should result in repeated `keydown` events, you can add the number of
repetitions.
If the key should also be released after this, add a slash `/` to the end of the
descriptor.

```js
keyboard('{a>}') // press a without releasing it
keyboard('{a>5}') // press a without releasing it and trigger 5 keydown
keyboard('{a>5/}') // press a for 5 keydown and then release it
```

A previously pressed key can be lifted by prefixing the descriptor with `/`.

```js
keyboard('{/a}') // release a previously pressed a
```

This allows to simulate key combinations.

```js
keyboard('{Shift>}A{/Shift}') // translates to: Shift(down), A, Shift(up)
```

The mapping of `key` to `code` is performed by a
[default key map](https://github.com/testing-library/user-event/blob/beta/src/keyboard/keyMap.ts)
portraying a "default" US-keyboard. You can provide your own local keyboard
mapping per [`keyboardMap`](options#keyboardmap) option.

Currently the different `key` meanings of single keys are treated as different
keys.

> Future versions might try to interpolate the modifiers needed to reach a
> printable key on the keyboard. E.g. Automatically pressing `{Shift}` when
> CapsLock is not active and `A` is referenced. If you don't wish this behavior,
> you can deactivate the [`autoModify`](options#automodify) option to opt out
> of this non-breaking change.
138 changes: 138 additions & 0 deletions docs/user-event/api-pointer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
id: pointer
title: Pointer
---

```ts
pointer(input: PointerInput): Promise<void>
```

The `pointer` API allows to simulate interactions with pointer devices. It
accepts a single pointer action or an array of them.

```ts
type PointerInput = PointerActionInput | Array<PointerActionInput>
```

> Our primary target audience tests per `jest` in a `jsdom` environment and there
is no layout in `jsdom`. This means that different from your browser the
elements don't exist in a specific position, layer and size.
We don't try to determine if the pointer action you describe is possible at that
position in your layout.
Comment on lines +17 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean this?

Suggested change
> Our primary target audience tests per `jest` in a `jsdom` environment and there
is no layout in `jsdom`. This means that different from your browser the
elements don't exist in a specific position, layer and size.
We don't try to determine if the pointer action you describe is possible at that
position in your layout.
> Our primary target audience tests per `jest` in a `jsdom` environment and there
is no layout in `jsdom`. This means that the elements specific position, layer, and size could be different from your browser.
We don't try to determine if the pointer action you describe is possible at that
position in your layout.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No this literally means that there is no specific position, layer, or size. Elements can have IDL properties and/or CSS styles that usually correspond to elements being at a specific position, having specific dimensions, or being above/under each other. But jsdom has no layout. The properties and styles exist, but they have no effect.

document.body.innerHTML=`<img style="position: relative; left: 50px;"/>`
$("img").style.left // '50px'
$("img").offsetLeft // 0

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Got it, the comment made it click to me - would it be worth it to also mention it in the docs?

every character has effective width zero and is at effective offset 0,0. (While there might be properties applied that suggest otherwise but have no effect.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. I feel like it might be better to write a blog post about this topic explaining the concept and then adding a link. Really explaining this, and what difference it makes to testing might be worth it, but is out of scope for these docs.


## Pointer action

There are two types of actions: press and move.

### <a name="press" href="#"/>Pressing a button or touching the screen

A pointer action is a press action if it defines a key to be pressed, to be
released, or both.

```js
pointer({keys: '[MouseLeft]'})
```

You can declare multiple press actions (on the same position) at once which will
be resolved to multiple actions internally. If you don't need to declare any
other properties you can also just supply the `keys` string.

```js
pointer({keys: '[MouseLeft][MouseRight]'})
// or
pointer('[MouseLeft][MouseRight]')
```

In order to press a button without releasing it, the button name is suffixed with
`>`.
For just releasing a previously pressed button, the tag is started with `/`.

```js
pointer('[MouseLeft>]') // press the left mouse button
pointer('[/MouseLeft]') // release the left mouse button
```

Which buttons are available depends on the [`pointerMap`](options#pointermap).

### <a name="move" href="#"/>Moving a pointer

Every pointer action that is not a press action describes a pointer movement.

You can declare which pointer is moved per `pointerName` property. This defaults
to `mouse`.

Note that the `mouse` pointer (`pointerId: 1`) is also the only pointer that
always exists and has a position. A `touch` pointer only exists while the screen
is touched and receives a new `pointerId` every time. For these pointers, we use
the "button" name from the press action as `pointerName`.

```js
pointer([
// touch the screen at element1
{keys: '[TouchA>]', target: element1},
// move the touch pointer to element2
{pointerName: 'TouchA', target: element2},
// release the touch pointer at the last position (element2)
{keys: '[/TouchA]'},
])
```

## Pointer position

### PointerTarget

```ts
interface PointerTarget {
target: Element
coords?: PointerCoords
}
```

The `PointerTarget` props allows to describe the position of the pointer on the
document.
The `coords` you provide are applied as-is to the resulting
[`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) and
can be omitted.
The `target` should be the element receiving the pointer input in the browser.
This is the topmost element that can receive pointer event at those coordinates.

### SelectionTarget

```ts
interface SelectionTarget {
node?: Node
offset?: number
}
```

Pointer actions can alter the selection in the document.
In the browser every pointer position corresponds with a DOM position. This is a
DOM node and a DOM offset which usually translates to the character closest to
the pointer position.
As all character in a no-layout environment are in the same layout position we
assume a pointer position to be closest to the last descendant of the pointer
`target`.

If you provide `offset`, we assume the pointer position to be closest to the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me like an example will be good here to make our users understand it properly.

Copy link
Member Author

@ph-fritsche ph-fritsche Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an example. I hope this makes it clear enough. Might be worth explaining layout positions and DOM offsets and how the browser handles selection per pointer in a blog post, but this would break the mold of docs specific to the library.

`offset-th` character of `target.textContent`.

If you also provide `node`, we treat `node` and `offset` as the exact DOM
position to be used for any selection.

```jsx
// element: <div><span>foo</span><span>bar</span></div>
// | marking the cursor.
// [ ] marking a selection.

pointer({target: element, offset: 2, keys: '[MouseLeft]'})
// => <div><span>fo|o</span><span>bar</span></div>

pointer([
{target: element, offset: 2, keys: '[MouseLeft>]'},
{offset: 5},
])
// => <div><span>fo[o</span><span>ba]r</span></div>

pointer({target: element, node: element, offset: 1, keys: '[MouseLeft]'})
// => <div><span>foo</span>|<span>bar</span></div>
```
Loading