Skip to content

Commit

Permalink
fix(keyboard): parse keyboard input without nesting (#793)
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed Nov 28, 2021
1 parent 86860cc commit fafa677
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 121 deletions.
53 changes: 0 additions & 53 deletions src/keyboard/getNextKeyDef.ts

This file was deleted.

9 changes: 7 additions & 2 deletions src/keyboard/index.ts
@@ -1,12 +1,17 @@
import {Config, UserEvent} from '../setup'
import {keyboardImplementation, releaseAllKeys} from './keyboardImplementation'
import {keyboardAction, KeyboardAction, releaseAllKeys} from './keyboardAction'
import {parseKeyDef} from './parseKeyDef'
import type {keyboardState, keyboardKey} from './types'

export {releaseAllKeys}
export type {keyboardKey, keyboardState}

export async function keyboard(this: UserEvent, text: string): Promise<void> {
return keyboardImplementation(this[Config], text)
const {keyboardMap} = this[Config]

const actions: KeyboardAction[] = parseKeyDef(keyboardMap, text)

return keyboardAction(this[Config], actions)
}

export function createKeyboardState(): keyboardState {
Expand Down
@@ -1,63 +1,62 @@
import {fireEvent} from '@testing-library/dom'
import {Config} from '../setup'
import {getActiveElement, wait} from '../utils'
import {getNextKeyDef} from './getNextKeyDef'
import {behaviorPlugin, keyboardKey} from './types'
import * as plugins from './plugins'
import {getKeyEventProps} from './getEventProps'

export async function keyboardImplementation(
export interface KeyboardAction {
keyDef: keyboardKey
releasePrevious: boolean
releaseSelf: boolean
repeat: number
}

export async function keyboardAction(
config: Config,
text: string,
): Promise<void> {
const {document, keyboardState, keyboardMap, delay} = config
const getCurrentElement = () => getActive(document)
actions: KeyboardAction[],
) {
for (let i = 0; i < actions.length; i++) {
await keyboardKeyAction(config, actions[i])

const {keyDef, consumedLength, releasePrevious, releaseSelf, repeat} =
keyboardState.repeatKey ?? getNextKeyDef(keyboardMap, text)
if (typeof config.delay === 'number' && i < actions.length - 1) {
await wait(config.delay)
}
}
}

const pressed = keyboardState.pressed.find(p => p.keyDef === keyDef)
async function keyboardKeyAction(
config: Config,
{keyDef, releasePrevious, releaseSelf, repeat}: KeyboardAction,
) {
const {document, keyboardState, delay} = config
const getCurrentElement = () => getActive(document)

// Release the key automatically if it was pressed before.
// Do not release the key on iterations on `state.repeatKey`.
if (pressed && !keyboardState.repeatKey) {
const pressed = keyboardState.pressed.find(p => p.keyDef === keyDef)
if (pressed) {
await keyup(keyDef, getCurrentElement, config, pressed.unpreventedDefault)
}

if (!releasePrevious) {
const unpreventedDefault = await keydown(keyDef, getCurrentElement, config)
let unpreventedDefault = true
for (let i = 1; i <= repeat; i++) {
unpreventedDefault = await keydown(keyDef, getCurrentElement, config)

if (unpreventedDefault && hasKeyPress(keyDef, config)) {
await keypress(keyDef, getCurrentElement, config)
}

if (unpreventedDefault && hasKeyPress(keyDef, config)) {
await keypress(keyDef, getCurrentElement, config)
if (typeof delay === 'number' && i < repeat) {
await wait(delay)
}
}

// Release the key only on the last iteration on `state.repeatKey`.
if (releaseSelf && repeat <= 1) {
if (releaseSelf) {
await keyup(keyDef, getCurrentElement, config, unpreventedDefault)
}
}

if (repeat > 1) {
keyboardState.repeatKey = {
// don't consume again on the next iteration
consumedLength: 0,
keyDef,
releasePrevious,
releaseSelf,
repeat: repeat - 1,
}
} else {
delete keyboardState.repeatKey
}

if (text.length > consumedLength || repeat > 1) {
if (typeof delay === 'number') {
await wait(delay)
}

return keyboardImplementation(config, text.slice(consumedLength))
}
return void undefined
}

function getActive(document: Document): Element {
Expand Down
51 changes: 51 additions & 0 deletions src/keyboard/parseKeyDef.ts
@@ -0,0 +1,51 @@
import {readNextDescriptor} from '../utils'
import {keyboardKey} from './types'

/**
* Parse key defintions per `keyboardMap`
*
* Keys can be referenced by `{key}` or `{special}` as well as physical locations per `[code]`.
* Everything else will be interpreted as a typed character - e.g. `a`.
* Brackets `{` and `[` can be escaped by doubling - e.g. `foo[[bar` translates to `foo[bar`.
* Keeping the key pressed can be written as `{key>}`.
* When keeping the key pressed you can choose how long (how many keydown and keypress) the key is pressed `{key>3}`.
* You can then release the key per `{key>3/}` or keep it pressed and continue with the next key.
*/
export function parseKeyDef(keyboardMap: keyboardKey[], text: string) {
const defs: Array<{
keyDef: keyboardKey
releasePrevious: boolean
releaseSelf: boolean
repeat: number
}> = []

do {
const {
type,
descriptor,
consumedLength,
releasePrevious,
releaseSelf = true,
repeat,
} = readNextDescriptor(text)

const keyDef = keyboardMap.find(def => {
if (type === '[') {
return def.code?.toLowerCase() === descriptor.toLowerCase()
} else if (type === '{') {
return def.key?.toLowerCase() === descriptor.toLowerCase()
}
return def.key === descriptor
}) ?? {
key: 'Unknown',
code: 'Unknown',
[type === '[' ? 'code' : 'key']: descriptor,
}

defs.push({keyDef, releasePrevious, releaseSelf, repeat})

text = text.slice(consumedLength)
} while (text)

return defs
}
6 changes: 0 additions & 6 deletions src/keyboard/types.ts
@@ -1,5 +1,4 @@
import {Config} from '../setup'
import {getNextKeyDef} from './getNextKeyDef'

/**
* @internal Do not create/alter this by yourself as this type might be subject to changes.
Expand Down Expand Up @@ -42,11 +41,6 @@ export type keyboardState = {
E.g. ^1
*/
carryChar: string

/**
Repeat keydown and keypress event
*/
repeatKey?: ReturnType<typeof getNextKeyDef>
}

export enum DOM_KEY_LOCATION {
Expand Down
File renamed without changes.
39 changes: 16 additions & 23 deletions tests/keyboard/getNextKeyDef.ts → tests/keyboard/parseKeyDef.ts
@@ -1,29 +1,22 @@
import cases from 'jest-in-case'
import {getNextKeyDef} from '#src/keyboard/getNextKeyDef'
import {parseKeyDef} from '#src/keyboard/parseKeyDef'
import {defaultKeyMap} from '#src/keyboard/keyMap'
import {keyboardKey} from '#src/keyboard/types'

cases(
'reference key per',
({text, key, code}) => {
expect(getNextKeyDef(defaultKeyMap, `${text}foo`)).toEqual(
expect.objectContaining({
keyDef: expect.objectContaining({
key,
code,
}) as keyboardKey,
consumedLength: text.length,
}),
)
expect(getNextKeyDef(defaultKeyMap, `${text}/foo`)).toEqual(
expect.objectContaining({
keyDef: expect.objectContaining({
key,
code,
}) as keyboardKey,
consumedLength: text.length,
}),
)
const parsed = parseKeyDef(defaultKeyMap, `/${text}/`)
expect(parsed).toHaveLength(3)
expect(parsed[1]).toEqual({
keyDef: expect.objectContaining({
key,
code,
}) as keyboardKey,
releasePrevious: false,
releaseSelf: true,
repeat: 1,
})
},
{
code: {text: '[ControlLeft]', key: 'Control', code: 'ControlLeft'},
Expand All @@ -40,9 +33,9 @@ cases(
cases(
'modifiers',
({text, modifiers}) => {
expect(getNextKeyDef(defaultKeyMap, `${text}foo`)).toEqual(
expect.objectContaining(modifiers),
)
const parsed = parseKeyDef(defaultKeyMap, `/${text}/`)
expect(parsed).toHaveLength(3)
expect(parsed[1]).toEqual(expect.objectContaining(modifiers))
},
{
'no releasePrevious': {
Expand Down Expand Up @@ -83,7 +76,7 @@ cases(
cases(
'errors',
({text, expectedError}) => {
expect(() => getNextKeyDef(defaultKeyMap, `${text}`)).toThrow(expectedError)
expect(() => parseKeyDef(defaultKeyMap, `${text}`)).toThrow(expectedError)
},
{
'invalid descriptor': {
Expand Down

0 comments on commit fafa677

Please sign in to comment.