Skip to content

Commit df75e5f

Browse files
authored
fix(keyboard): parse keyboard input without nesting (#793)
1 parent ca214d4 commit df75e5f

File tree

7 files changed

+110
-121
lines changed

7 files changed

+110
-121
lines changed

src/keyboard/getNextKeyDef.ts

-53
This file was deleted.

src/keyboard/index.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import {Config, UserEvent} from '../setup'
2-
import {keyboardImplementation, releaseAllKeys} from './keyboardImplementation'
2+
import {keyboardAction, KeyboardAction, releaseAllKeys} from './keyboardAction'
3+
import {parseKeyDef} from './parseKeyDef'
34
import type {keyboardState, keyboardKey} from './types'
45

56
export {releaseAllKeys}
67
export type {keyboardKey, keyboardState}
78

89
export async function keyboard(this: UserEvent, text: string): Promise<void> {
9-
return keyboardImplementation(this[Config], text)
10+
const {keyboardMap} = this[Config]
11+
12+
const actions: KeyboardAction[] = parseKeyDef(keyboardMap, text)
13+
14+
return keyboardAction(this[Config], actions)
1015
}
1116

1217
export function createKeyboardState(): keyboardState {

src/keyboard/keyboardImplementation.ts src/keyboard/keyboardAction.ts

+36-37
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,62 @@
11
import {fireEvent} from '@testing-library/dom'
22
import {Config} from '../setup'
33
import {getActiveElement, wait} from '../utils'
4-
import {getNextKeyDef} from './getNextKeyDef'
54
import {behaviorPlugin, keyboardKey} from './types'
65
import * as plugins from './plugins'
76
import {getKeyEventProps} from './getEventProps'
87

9-
export async function keyboardImplementation(
8+
export interface KeyboardAction {
9+
keyDef: keyboardKey
10+
releasePrevious: boolean
11+
releaseSelf: boolean
12+
repeat: number
13+
}
14+
15+
export async function keyboardAction(
1016
config: Config,
11-
text: string,
12-
): Promise<void> {
13-
const {document, keyboardState, keyboardMap, delay} = config
14-
const getCurrentElement = () => getActive(document)
17+
actions: KeyboardAction[],
18+
) {
19+
for (let i = 0; i < actions.length; i++) {
20+
await keyboardKeyAction(config, actions[i])
1521

16-
const {keyDef, consumedLength, releasePrevious, releaseSelf, repeat} =
17-
keyboardState.repeatKey ?? getNextKeyDef(keyboardMap, text)
22+
if (typeof config.delay === 'number' && i < actions.length - 1) {
23+
await wait(config.delay)
24+
}
25+
}
26+
}
1827

19-
const pressed = keyboardState.pressed.find(p => p.keyDef === keyDef)
28+
async function keyboardKeyAction(
29+
config: Config,
30+
{keyDef, releasePrevious, releaseSelf, repeat}: KeyboardAction,
31+
) {
32+
const {document, keyboardState, delay} = config
33+
const getCurrentElement = () => getActive(document)
2034

2135
// Release the key automatically if it was pressed before.
22-
// Do not release the key on iterations on `state.repeatKey`.
23-
if (pressed && !keyboardState.repeatKey) {
36+
const pressed = keyboardState.pressed.find(p => p.keyDef === keyDef)
37+
if (pressed) {
2438
await keyup(keyDef, getCurrentElement, config, pressed.unpreventedDefault)
2539
}
2640

2741
if (!releasePrevious) {
28-
const unpreventedDefault = await keydown(keyDef, getCurrentElement, config)
42+
let unpreventedDefault = true
43+
for (let i = 1; i <= repeat; i++) {
44+
unpreventedDefault = await keydown(keyDef, getCurrentElement, config)
45+
46+
if (unpreventedDefault && hasKeyPress(keyDef, config)) {
47+
await keypress(keyDef, getCurrentElement, config)
48+
}
2949

30-
if (unpreventedDefault && hasKeyPress(keyDef, config)) {
31-
await keypress(keyDef, getCurrentElement, config)
50+
if (typeof delay === 'number' && i < repeat) {
51+
await wait(delay)
52+
}
3253
}
3354

3455
// Release the key only on the last iteration on `state.repeatKey`.
35-
if (releaseSelf && repeat <= 1) {
56+
if (releaseSelf) {
3657
await keyup(keyDef, getCurrentElement, config, unpreventedDefault)
3758
}
3859
}
39-
40-
if (repeat > 1) {
41-
keyboardState.repeatKey = {
42-
// don't consume again on the next iteration
43-
consumedLength: 0,
44-
keyDef,
45-
releasePrevious,
46-
releaseSelf,
47-
repeat: repeat - 1,
48-
}
49-
} else {
50-
delete keyboardState.repeatKey
51-
}
52-
53-
if (text.length > consumedLength || repeat > 1) {
54-
if (typeof delay === 'number') {
55-
await wait(delay)
56-
}
57-
58-
return keyboardImplementation(config, text.slice(consumedLength))
59-
}
60-
return void undefined
6160
}
6261

6362
function getActive(document: Document): Element {

src/keyboard/parseKeyDef.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {readNextDescriptor} from '../utils'
2+
import {keyboardKey} from './types'
3+
4+
/**
5+
* Parse key defintions per `keyboardMap`
6+
*
7+
* Keys can be referenced by `{key}` or `{special}` as well as physical locations per `[code]`.
8+
* Everything else will be interpreted as a typed character - e.g. `a`.
9+
* Brackets `{` and `[` can be escaped by doubling - e.g. `foo[[bar` translates to `foo[bar`.
10+
* Keeping the key pressed can be written as `{key>}`.
11+
* When keeping the key pressed you can choose how long (how many keydown and keypress) the key is pressed `{key>3}`.
12+
* You can then release the key per `{key>3/}` or keep it pressed and continue with the next key.
13+
*/
14+
export function parseKeyDef(keyboardMap: keyboardKey[], text: string) {
15+
const defs: Array<{
16+
keyDef: keyboardKey
17+
releasePrevious: boolean
18+
releaseSelf: boolean
19+
repeat: number
20+
}> = []
21+
22+
do {
23+
const {
24+
type,
25+
descriptor,
26+
consumedLength,
27+
releasePrevious,
28+
releaseSelf = true,
29+
repeat,
30+
} = readNextDescriptor(text)
31+
32+
const keyDef = keyboardMap.find(def => {
33+
if (type === '[') {
34+
return def.code?.toLowerCase() === descriptor.toLowerCase()
35+
} else if (type === '{') {
36+
return def.key?.toLowerCase() === descriptor.toLowerCase()
37+
}
38+
return def.key === descriptor
39+
}) ?? {
40+
key: 'Unknown',
41+
code: 'Unknown',
42+
[type === '[' ? 'code' : 'key']: descriptor,
43+
}
44+
45+
defs.push({keyDef, releasePrevious, releaseSelf, repeat})
46+
47+
text = text.slice(consumedLength)
48+
} while (text)
49+
50+
return defs
51+
}

src/keyboard/types.ts

-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {Config} from '../setup'
2-
import {getNextKeyDef} from './getNextKeyDef'
32

43
/**
54
* @internal Do not create/alter this by yourself as this type might be subject to changes.
@@ -42,11 +41,6 @@ export type keyboardState = {
4241
E.g. ^1
4342
*/
4443
carryChar: string
45-
46-
/**
47-
Repeat keydown and keypress event
48-
*/
49-
repeatKey?: ReturnType<typeof getNextKeyDef>
5044
}
5145

5246
export enum DOM_KEY_LOCATION {
File renamed without changes.

tests/keyboard/getNextKeyDef.ts tests/keyboard/parseKeyDef.ts

+16-23
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
11
import cases from 'jest-in-case'
2-
import {getNextKeyDef} from '#src/keyboard/getNextKeyDef'
2+
import {parseKeyDef} from '#src/keyboard/parseKeyDef'
33
import {defaultKeyMap} from '#src/keyboard/keyMap'
44
import {keyboardKey} from '#src/keyboard/types'
55

66
cases(
77
'reference key per',
88
({text, key, code}) => {
9-
expect(getNextKeyDef(defaultKeyMap, `${text}foo`)).toEqual(
10-
expect.objectContaining({
11-
keyDef: expect.objectContaining({
12-
key,
13-
code,
14-
}) as keyboardKey,
15-
consumedLength: text.length,
16-
}),
17-
)
18-
expect(getNextKeyDef(defaultKeyMap, `${text}/foo`)).toEqual(
19-
expect.objectContaining({
20-
keyDef: expect.objectContaining({
21-
key,
22-
code,
23-
}) as keyboardKey,
24-
consumedLength: text.length,
25-
}),
26-
)
9+
const parsed = parseKeyDef(defaultKeyMap, `/${text}/`)
10+
expect(parsed).toHaveLength(3)
11+
expect(parsed[1]).toEqual({
12+
keyDef: expect.objectContaining({
13+
key,
14+
code,
15+
}) as keyboardKey,
16+
releasePrevious: false,
17+
releaseSelf: true,
18+
repeat: 1,
19+
})
2720
},
2821
{
2922
code: {text: '[ControlLeft]', key: 'Control', code: 'ControlLeft'},
@@ -40,9 +33,9 @@ cases(
4033
cases(
4134
'modifiers',
4235
({text, modifiers}) => {
43-
expect(getNextKeyDef(defaultKeyMap, `${text}foo`)).toEqual(
44-
expect.objectContaining(modifiers),
45-
)
36+
const parsed = parseKeyDef(defaultKeyMap, `/${text}/`)
37+
expect(parsed).toHaveLength(3)
38+
expect(parsed[1]).toEqual(expect.objectContaining(modifiers))
4639
},
4740
{
4841
'no releasePrevious': {
@@ -83,7 +76,7 @@ cases(
8376
cases(
8477
'errors',
8578
({text, expectedError}) => {
86-
expect(() => getNextKeyDef(defaultKeyMap, `${text}`)).toThrow(expectedError)
79+
expect(() => parseKeyDef(defaultKeyMap, `${text}`)).toThrow(expectedError)
8780
},
8881
{
8982
'invalid descriptor': {

0 commit comments

Comments
 (0)