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

Already on GitHub? Sign in to your account

Rework type #581

Merged
merged 43 commits into from
Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
66d80e3
chore: configure repo for typescript modules
ph-fritsche Mar 9, 2021
153aca8
refactor: convert and restructure utils
ph-fritsche Mar 9, 2021
fb61f97
refactor: convert and restructure type
ph-fritsche Mar 9, 2021
423595d
wip: keyboard mapping
ph-fritsche Mar 10, 2021
492493e
wip: new type implementation
ph-fritsche Mar 11, 2021
9863138
wip: fix textarea
ph-fritsche Mar 11, 2021
d078392
wip: fix bracketed key descriptor
ph-fritsche Mar 11, 2021
7dcd1a5
wip: delay before first char
ph-fritsche Mar 11, 2021
2b9ec50
wip: get activeElement before default keydown behavior
ph-fritsche Mar 11, 2021
3944223
fix: activeElement is never disabled
ph-fritsche Mar 11, 2021
421afee
wip: fix change event on date/time
ph-fritsche Mar 11, 2021
8497533
wip: fix value for date/number
ph-fritsche Mar 11, 2021
18f48c3
wip: keypress plugins
ph-fritsche Mar 11, 2021
a7cc548
test: do not expect bug
ph-fritsche Mar 11, 2021
a28f1c7
wip: add backspace
ph-fritsche Mar 11, 2021
61631a1
wip: add enter
ph-fritsche Mar 11, 2021
defe327
wip: add navigation keys
ph-fritsche Mar 11, 2021
eb8280b
wip: fix special keys
ph-fritsche Mar 11, 2021
eec73dd
wip: add space
ph-fritsche Mar 11, 2021
b22dc33
wip: fix selectall
ph-fritsche Mar 11, 2021
3c94d7b
test: fix expected events
ph-fritsche Mar 11, 2021
294409c
wip: add del
ph-fritsche Mar 11, 2021
2308fd7
wip: fix meta keys on mouseevents
ph-fritsche Mar 11, 2021
4fbfae6
wip: add autoclose
ph-fritsche Mar 11, 2021
a389ccc
wip: remove obsolete code
ph-fritsche Mar 11, 2021
e77dfb3
wip: fix escaped bracket
ph-fritsche Mar 12, 2021
514e0d2
wip: bump test coverage
ph-fritsche Mar 12, 2021
362c105
wip: reorganize files
ph-fritsche Mar 12, 2021
7f7bd62
feat: introduce keyboard API
ph-fritsche Mar 13, 2021
7cc8460
test: continue keyboard with state
ph-fritsche Mar 13, 2021
a1c78bc
fix: typings
ph-fritsche Mar 13, 2021
810c9a2
test: clear expected mock calls
ph-fritsche Mar 13, 2021
b8a78c8
chore: clean up config
ph-fritsche Mar 13, 2021
ab8461d
docs: add userEvent.keyboard
ph-fritsche Mar 13, 2021
c528645
docs: KeyboardEvent.code
ph-fritsche Mar 13, 2021
39b1d08
fix: relative import
ph-fritsche Mar 16, 2021
3d76f0f
docs: closing modifier
ph-fritsche Mar 16, 2021
3fae3bc
fix: annotate deprecated keyCode property
ph-fritsche Mar 16, 2021
781e0d6
refactor: remove TODO
ph-fritsche Mar 16, 2021
b409062
refactor: use jest-in-case
ph-fritsche Mar 16, 2021
64ffb17
fix: remove obsolete delay
ph-fritsche Mar 16, 2021
db00fe1
chore: disable @typescript-eslint/non-nullable-type-assertion-style
ph-fritsche Mar 16, 2021
f6c533e
chore: lint fix
ph-fritsche Mar 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
extends: './node_modules/kcd-scripts/eslint.js',
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ts'],
},
},
},
rules: {
'testing-library/no-dom-import': 0,
'@typescript-eslint/non-nullable-type-assertion-style': 0,
},
}
77 changes: 72 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ change the state of the checkbox.
- [`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-)
- [`keyboard(text, options)`](#keyboardtext-options)
- [`upload(element, file, [{ clickInit, changeInit }], [options])`](#uploadelement-file--clickinit-changeinit--options)
- [`clear(element)`](#clearelement)
- [`selectOptions(element, values)`](#selectoptionselement-values)
- [`deselectOptions(element, values)`](#deselectoptionselement-values)
Expand Down Expand Up @@ -178,10 +179,6 @@ 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`.

Expand Down Expand Up @@ -271,6 +268,76 @@ test('types into the input', () => {
})
```

### `keyboard(text, options)`

Simulates the keyboard events described by `text`. This is similar to
`userEvent.type()` but without any clicking or changing the selection range.

> You should use `userEvent.keyboard` if you want to just simulate pressing
> buttons on the keyboard. You should use `userEvent.type` if you just want to
> conveniently insert some text into an input field or textarea.

Keystrokes can be described:

- Per printable character
```js
userEvent.keyboard('foo') // translates to: f, o, o
```
The brackets `{` and `[` are used as special character and can be referenced
by doubling them.
```js
userEvent.keyboard('{{a[[') // translates to: {, a, [
ph-fritsche marked this conversation as resolved.
Show resolved Hide resolved
```
- Per
[KeyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
(only supports alphanumeric values of `key`)
```js
userEvent.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`.
- Per
[KeyboardEvent.code](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code)
```js
userEvent.keyboard('[ShiftLeft][KeyF][KeyO][KeyO]') // translates to: Shift, f, o, o
```
- Per legacy `userEvent.type` modifier/specialChar The modifiers like `{shift}`
ph-fritsche marked this conversation as resolved.
Show resolved Hide resolved
(note the lowercase) will automatically be kept pressed, just like before. You
ph-fritsche marked this conversation as resolved.
Show resolved Hide resolved
can cancel this behavior by adding a `/` to the end of the descriptor.
```js
userEvent.keyboard('{shift}{ctrl/}a{/shift}') // translates to: Shift(down), Control(down+up), a, Shift(up)
```

Keys can be kept pressed by adding a `>` to the end of the descriptor - and
lifted by adding a `/` to the beginning of the descriptor:

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

`userEvent.keyboard` returns a keyboard state that can be used to continue
keyboard operations.

```js
const keyboardState = userEvent.keyboard('[ControlLeft>]') // keydown [ControlLeft]
// ... inspect some changes ...
userEvent.keyboard('a', {keyboardState}) // press [KeyA] with active ctrlKey modifier
```

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

```js
userEvent.keyboard('?', {keyboardMap: myOwnLocaleKeyboardMap})
```

> 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 pass `autoModify: false` when using `userEvent.keyboard` in your code.

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

Uploads file to an `<input>`. For uploading multiple files use `<input>` with
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
"@testing-library/dom": "^7.28.1",
"@testing-library/jest-dom": "^5.11.6",
"@types/estree": "0.0.45",
"@types/jest-in-case": "^1.0.3",
"is-ci": "^2.0.0",
"jest-in-case": "^1.0.2",
"jest-serializer-ansi": "^1.0.3",
"kcd-scripts": "^7.5.1",
"typescript": "^4.1.2"
Expand Down
78 changes: 78 additions & 0 deletions src/__tests__/keyboard/getNextKeyDef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import cases from 'jest-in-case'
import {getNextKeyDef} from 'keyboard/getNextKeyDef'
ph-fritsche marked this conversation as resolved.
Show resolved Hide resolved
import {defaultKeyMap} from 'keyboard/keyMap'
import {keyboardKey, keyboardOptions} from 'keyboard/types'

const options: keyboardOptions = {
document,
keyboardMap: defaultKeyMap,
autoModify: false,
delay: 123,
}

cases(
'reference key per',
({text, key, code}) => {
expect(getNextKeyDef(`${text}foo`, options)).toEqual(
expect.objectContaining({
keyDef: expect.objectContaining({
key,
code,
}) as keyboardKey,
}),
)
},
{
code: {text: '[ControlLeft]', key: 'Control', code: 'ControlLeft'},
'unimplemented code': {text: '[Foo]', key: 'Unknown', code: 'Foo'},
key: {text: '{Control}', key: 'Control', code: 'ControlLeft'},
'unimplemented key': {text: '{Foo}', key: 'Foo', code: 'Unknown'},
'legacy modifier': {text: '{ctrl}', key: 'Control', code: 'ControlLeft'},
'printable character': {text: 'a', key: 'a', code: 'KeyA'},
'{ as printable': {text: '{{', key: '{', code: 'Unknown'},
'[ as printable': {text: '[[', key: '[', code: 'Unknown'},
},
)

cases(
'modifiers',
({text, modifiers}) => {
expect(getNextKeyDef(`${text}foo`, options)).toEqual(
expect.objectContaining(modifiers),
)
},
{
'no releasePrevious': {
text: '{Control}',
modifiers: {releasePrevious: false},
},
'releasePrevious per key': {
text: '{/Control}',
modifiers: {releasePrevious: true},
},
'releasePrevious per code': {
text: '[/ControlLeft]',
modifiers: {releasePrevious: true},
},
'default releaseSelf': {
text: '{Control}',
modifiers: {releaseSelf: true},
},
'keep key pressed per key': {
text: '{Control>}',
modifiers: {releaseSelf: false},
},
'keep key pressed per code': {
text: '[Control>]',
modifiers: {releaseSelf: false},
},
'no releaseSelf on legacy modifier': {
text: '{ctrl}',
modifiers: {releaseSelf: false},
},
'release legacy modifier': {
text: '{ctrl/}',
modifiers: {releaseSelf: true},
},
},
)
137 changes: 137 additions & 0 deletions src/__tests__/keyboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import userEvent from '../../index'
import {addListeners, setup} from '../helpers/utils'

it('type without focus', () => {
const {element} = setup('<input/>')
const {getEventSnapshot} = addListeners(document.body)

userEvent.keyboard('foo')

expect(element).toHaveValue('')
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: body

body - keydown: f (102)
body - keypress: f (102)
body - keyup: f (102)
body - keydown: o (111)
body - keypress: o (111)
body - keyup: o (111)
body - keydown: o (111)
body - keypress: o (111)
body - keyup: o (111)
`)
})

it('type with focus', () => {
const {element} = setup('<input/>')
const {getEventSnapshot} = addListeners(document.body)
Comment on lines +27 to +28
Copy link
Member

Choose a reason for hiding this comment

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

Curious why this isn't:

Suggested change
const {element} = setup('<input/>')
const {getEventSnapshot} = addListeners(document.body)
const {element, getEventSnapshot} = setup('<input/>')

Copy link
Member

Choose a reason for hiding this comment

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

Same for other tests in this file. Not sure why we care about the events fired on the document.body

Copy link
Member Author

Choose a reason for hiding this comment

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

Because keyboard does not move the focus.
If the focus is not on the input, there are no events on the input.
The eventSnapshot on document.body tells the full story for these tests and provides a meaningful error message both for wrong events and wrong event targets.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, yeah, that makes sense 👍

;(element as HTMLInputElement).focus()

userEvent.keyboard('foo')

expect(element).toHaveValue('foo')
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: body

input[value=""] - focusin
input[value=""] - keydown: f (102)
input[value=""] - keypress: f (102)
input[value="f"] - input
input[value="f"] - keyup: f (102)
input[value="f"] - keydown: o (111)
input[value="f"] - keypress: o (111)
input[value="fo"] - input
input[value="fo"] - keyup: o (111)
input[value="fo"] - keydown: o (111)
input[value="fo"] - keypress: o (111)
input[value="foo"] - input
input[value="foo"] - keyup: o (111)
`)
})

it('type asynchronous', async () => {
const {element} = setup('<input/>')
const {getEventSnapshot} = addListeners(document.body)
;(element as HTMLInputElement).focus()

// eslint-disable-next-line testing-library/no-await-sync-events
await userEvent.keyboard('foo', {delay: 1})

expect(element).toHaveValue('foo')
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: body

input[value=""] - focusin
input[value=""] - keydown: f (102)
input[value=""] - keypress: f (102)
input[value="f"] - input
input[value="f"] - keyup: f (102)
input[value="f"] - keydown: o (111)
input[value="f"] - keypress: o (111)
input[value="fo"] - input
input[value="fo"] - keyup: o (111)
input[value="fo"] - keydown: o (111)
input[value="fo"] - keypress: o (111)
input[value="foo"] - input
input[value="foo"] - keyup: o (111)
`)
})

describe('error', () => {
afterEach(() => {
;(console.error as jest.MockedFunction<typeof console.error>).mockClear()
})

it('error in sync', async () => {
const err = jest.spyOn(console, 'error')
err.mockImplementation(() => {})

userEvent.keyboard('{!')

// the catch will be asynchronous
await Promise.resolve()

expect(err).toHaveBeenCalledWith(expect.any(Error))
expect(err.mock.calls[0][0]).toHaveProperty(
'message',
'Expected key descriptor but found "!" in "{!"',
)
})

it('error in async', async () => {
const promise = userEvent.keyboard('{!', {delay: 1})

return expect(promise).rejects.toThrowError(
'Expected key descriptor but found "!" in "{!"',
)
})
})

it('continue typing with state', () => {
const {element, getEventSnapshot, clearEventCalls} = setup('<input/>')
;(element as HTMLInputElement).focus()
clearEventCalls()

const state = userEvent.keyboard('[ShiftRight>]')

expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value=""]

input[value=""] - keydown: Shift (16) {shift}
`)
clearEventCalls()

userEvent.keyboard('F[/ShiftRight]', {keyboardState: state})

expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="F"]

input[value=""] - keydown: F (70) {shift}
input[value=""] - keypress: F (70) {shift}
input[value="F"] - input
"{CURSOR}" -> "F{CURSOR}"
input[value="F"] - keyup: F (70) {shift}
input[value="F"] - keyup: Shift (16)
`)
})
5 changes: 3 additions & 2 deletions src/__tests__/type-modifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,11 @@ test('{shift}a{/shift}', () => {
`)
})

test('{capslock}a{/capslock}', () => {
test('{capslock}a{capslock}', () => {
const {element, getEventSnapshot} = setup('<input />')

userEvent.type(element, '{capslock}a{/capslock}')
// The old behavior to treat {/capslock} like {capslock} makes no sense
userEvent.type(element, '{capslock}a{capslock}')

expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: input[value="a"]
Expand Down
Loading