From 684451f11b946374dca8a9650fce9fa316d0f032 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 30 Dec 2021 08:26:22 +0100 Subject: [PATCH 1/2] fix(keyboard): parse escaped bracket followed by descriptor (#814) --- src/utils/keyDef/readNextDescriptor.ts | 18 +++--- tests/keyboard/index.ts | 4 +- tests/keyboard/parseKeyDef.ts | 84 +++++++++++++++++++------- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/utils/keyDef/readNextDescriptor.ts b/src/utils/keyDef/readNextDescriptor.ts index 713b5431..e451dada 100644 --- a/src/utils/keyDef/readNextDescriptor.ts +++ b/src/utils/keyDef/readNextDescriptor.ts @@ -21,15 +21,7 @@ export function readNextDescriptor(text: string) { pos += startBracket.length - // `foo{{bar` is an escaped char at position 3, - // but `foo{{{>5}bar` should be treated as `{` pressed down for 5 keydowns. - const startBracketRepeated = startBracket - ? (text.match(new RegExp(`^\\${startBracket}+`)) as RegExpMatchArray)[0] - .length - : 0 - const isEscapedChar = - startBracketRepeated === 2 || - (startBracket === '{' && startBracketRepeated > 3) + const isEscapedChar = new RegExp(`^\\${startBracket}{2}`).test(text) const type = isEscapedChar ? '' : startBracket @@ -64,7 +56,13 @@ function readTag( pos += releasePreviousModifier.length - const descriptor = text.slice(pos).match(/^\w+/)?.[0] + const escapedDescriptor = startBracket === '{' && text[pos] === '\\' + + pos += Number(escapedDescriptor) + + const descriptor = escapedDescriptor + ? text[pos] + : text.slice(pos).match(startBracket === '{' ? /^\w+|^[^}>/]/ : /^\w+/)?.[0] assertDescriptor(descriptor, text, pos) diff --git a/tests/keyboard/index.ts b/tests/keyboard/index.ts index 9631218f..1acb25d4 100644 --- a/tests/keyboard/index.ts +++ b/tests/keyboard/index.ts @@ -78,8 +78,8 @@ it('type asynchronous', async () => { }) it('error in async', async () => { - await expect(userEvent.keyboard('{!', {delay: 1})).rejects.toThrowError( - 'Expected key descriptor but found "!" in "{!"', + await expect(userEvent.keyboard('[!', {delay: 1})).rejects.toThrowError( + 'Expected key descriptor but found "!" in "[!"', ) }) diff --git a/tests/keyboard/parseKeyDef.ts b/tests/keyboard/parseKeyDef.ts index e894855b..578c7d11 100644 --- a/tests/keyboard/parseKeyDef.ts +++ b/tests/keyboard/parseKeyDef.ts @@ -5,28 +5,66 @@ import {keyboardKey} from '#src/keyboard/types' cases( 'reference key per', - ({text, key, code}) => { + ({text, keyDef}) => { 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, - }) + keyDef = Array.isArray(keyDef) ? keyDef : [keyDef] + expect(parsed).toHaveLength(2 + keyDef.length) + expect(parsed.slice(1, -1)).toEqual( + keyDef.map(d => + expect.objectContaining({ + keyDef: expect.objectContaining(d) 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'}, - 'printable character': {text: 'a', key: 'a', code: 'KeyA'}, - 'modifiers as printable characters': {text: '/', key: '/', code: 'Unknown'}, - '{ as printable': {text: '{{', key: '{', code: 'Unknown'}, - '[ as printable': {text: '[[', key: '[', code: 'Unknown'}, + code: { + text: '[ControlLeft]', + keyDef: {key: 'Control', code: 'ControlLeft'}, + }, + 'unimplemented code': { + text: '[Foo]', + keyDef: {key: 'Unknown', code: 'Foo'}, + }, + key: { + text: '{Control}', + keyDef: {key: 'Control', code: 'ControlLeft'}, + }, + 'unimplemented key': { + text: '{Foo}', + keyDef: {key: 'Foo', code: 'Unknown'}, + }, + 'printable character': { + text: 'a', + keyDef: {key: 'a', code: 'KeyA'}, + }, + 'modifiers as printable characters': { + text: '/', + keyDef: {key: '/', code: 'Unknown'}, + }, + '{ as printable': { + text: '{{', + keyDef: {key: '{', code: 'Unknown'}, + }, + '{ as printable followed by descriptor': { + text: '{{{foo}', + keyDef: [ + {key: '{', code: 'Unknown'}, + {key: 'foo', code: 'Unknown'}, + ], + }, + '{ as key with modifiers': { + text: '{\\{>5/}', + keyDef: {key: '{', code: 'Unknown'}, + }, + 'modifier as key with modifiers': { + text: '{/\\/>5/}', + keyDef: {key: '/', code: 'Unknown'}, + }, + '[ as printable': { + text: '[[', + keyDef: {key: '[', code: 'Unknown'}, + }, }, ) @@ -79,10 +117,6 @@ cases( expect(() => parseKeyDef(defaultKeyMap, `${text}`)).toThrow(expectedError) }, { - 'invalid descriptor': { - text: '{!}', - expectedError: 'but found "!" in "{!}"', - }, 'missing descriptor': { text: '', expectedError: 'but found "" in ""', @@ -99,5 +133,9 @@ cases( text: '{a>3)', expectedError: 'but found ")" in "{a>3)"', }, + 'unescaped modifier': { + text: '{/>5}', + expectedError: 'but found ">" in "{/>5}"', + }, }, ) From e9635f656298ef1f96bd255b0d89031e04441537 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 30 Dec 2021 08:28:47 +0100 Subject: [PATCH 2/2] feat(keyboard): apply modifier state (#815) --- src/keyboard/getEventProps.ts | 21 ------ src/keyboard/index.ts | 18 +++-- src/keyboard/keyMap.ts | 5 ++ src/keyboard/keyboardAction.ts | 7 +- src/keyboard/plugins/character.ts | 2 +- src/keyboard/plugins/combination.ts | 2 +- src/keyboard/plugins/functional.ts | 71 +---------------- src/keyboard/plugins/index.ts | 7 +- src/keyboard/plugins/modifiers.ts | 92 +++++++++++++++++++++++ src/keyboard/types.ts | 24 ++++-- src/utils/index.ts | 3 + src/utils/keyboard/getKeyEventProps.ts | 10 +++ src/utils/keyboard/getUIEventModifiers.ts | 18 +++++ src/utils/pointer/firePointerEvents.ts | 6 +- tests/_helpers/utils.ts | 11 ++- tests/keyboard/plugin/functional.ts | 34 --------- tests/keyboard/plugin/modifiers.ts | 73 ++++++++++++++++++ 17 files changed, 256 insertions(+), 148 deletions(-) delete mode 100644 src/keyboard/getEventProps.ts create mode 100644 src/keyboard/plugins/modifiers.ts create mode 100644 src/utils/keyboard/getKeyEventProps.ts create mode 100644 src/utils/keyboard/getUIEventModifiers.ts create mode 100644 tests/keyboard/plugin/modifiers.ts diff --git a/src/keyboard/getEventProps.ts b/src/keyboard/getEventProps.ts deleted file mode 100644 index 35d860df..00000000 --- a/src/keyboard/getEventProps.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {keyboardKey, keyboardState} from './types' - -export function getKeyEventProps(keyDef: keyboardKey, state: keyboardState) { - return { - key: keyDef.key, - code: keyDef.code, - altKey: state.modifiers.alt, - ctrlKey: state.modifiers.ctrl, - metaKey: state.modifiers.meta, - shiftKey: state.modifiers.shift, - } -} - -export function getMouseEventProps(state: keyboardState) { - return { - altKey: state.modifiers.alt, - ctrlKey: state.modifiers.ctrl, - metaKey: state.modifiers.meta, - shiftKey: state.modifiers.shift, - } -} diff --git a/src/keyboard/index.ts b/src/keyboard/index.ts index 885af720..5c679763 100644 --- a/src/keyboard/index.ts +++ b/src/keyboard/index.ts @@ -20,11 +20,19 @@ export function createKeyboardState(): keyboardState { pressed: [], carryChar: '', modifiers: { - alt: false, - caps: false, - ctrl: false, - meta: false, - shift: false, + Alt: false, + AltGraph: false, + Control: false, + CapsLock: false, + Fn: false, + FnLock: false, + Meta: false, + NumLock: false, + ScrollLock: false, + Shift: false, + Symbol: false, + SymbolLock: false, }, + modifierPhase: {}, } } diff --git a/src/keyboard/keyMap.ts b/src/keyboard/keyMap.ts index 7b77d360..b486cbdf 100644 --- a/src/keyboard/keyMap.ts +++ b/src/keyboard/keyMap.ts @@ -72,5 +72,10 @@ export const defaultKeyMap: keyboardKey[] = [ {code: 'PageUp', key: 'PageUp'}, {code: 'PageDown', key: 'PageDown'}, + // Special keys that are not part of a default US-layout but included for specific behavior + {code: 'Fn', key: 'Fn'}, + {code: 'Symbol', key: 'Symbol'}, + {code: 'AltRight', key: 'AltGraph'}, + // TODO: add mappings ] diff --git a/src/keyboard/keyboardAction.ts b/src/keyboard/keyboardAction.ts index 0e8b1b96..a6476607 100644 --- a/src/keyboard/keyboardAction.ts +++ b/src/keyboard/keyboardAction.ts @@ -1,9 +1,8 @@ import {fireEvent} from '@testing-library/dom' import {Config} from '../setup' -import {getActiveElement, wait} from '../utils' +import {getActiveElement, getKeyEventProps, wait} from '../utils' import {behaviorPlugin, keyboardKey} from './types' import * as plugins from './plugins' -import {getKeyEventProps} from './getEventProps' export interface KeyboardAction { keyDef: keyboardKey @@ -162,7 +161,7 @@ function applyPlugins( function hasKeyPress(keyDef: keyboardKey, config: Config) { return ( (keyDef.key?.length === 1 || keyDef.key === 'Enter') && - !config.keyboardState.modifiers.ctrl && - !config.keyboardState.modifiers.alt + !config.keyboardState.modifiers.Control && + !config.keyboardState.modifiers.Alt ) } diff --git a/src/keyboard/plugins/character.ts b/src/keyboard/plugins/character.ts index 62dfb146..1a7234b2 100644 --- a/src/keyboard/plugins/character.ts +++ b/src/keyboard/plugins/character.ts @@ -171,7 +171,7 @@ export const keypressBehavior: behaviorPlugin[] = [ prepareInput( '\n', element, - isContentEditable(element) && !keyboardState.modifiers.shift + isContentEditable(element) && !keyboardState.modifiers.Shift ? 'insertParagraph' : 'insertLineBreak', )?.commit() diff --git a/src/keyboard/plugins/combination.ts b/src/keyboard/plugins/combination.ts index eecdae8e..28492e2d 100644 --- a/src/keyboard/plugins/combination.ts +++ b/src/keyboard/plugins/combination.ts @@ -8,7 +8,7 @@ import {selectAll} from '../../utils' export const keydownBehavior: behaviorPlugin[] = [ { matches: (keyDef, element, {keyboardState}) => - keyDef.code === 'KeyA' && keyboardState.modifiers.ctrl, + keyDef.code === 'KeyA' && keyboardState.modifiers.Control, handle: (keyDef, element) => selectAll(element), }, ] diff --git a/src/keyboard/plugins/functional.ts b/src/keyboard/plugins/functional.ts index ba592dfc..dbba404e 100644 --- a/src/keyboard/plugins/functional.ts +++ b/src/keyboard/plugins/functional.ts @@ -9,53 +9,16 @@ import { blur, focus, getTabDestination, + getUIEventModifiers, hasFormSubmit, isClickableInput, isEditable, isElementType, prepareInput, } from '../../utils' -import {getKeyEventProps, getMouseEventProps} from '../getEventProps' import {behaviorPlugin} from '../types' -const modifierKeys = { - Alt: 'alt', - Control: 'ctrl', - Shift: 'shift', - Meta: 'meta', -} as const - -export const preKeydownBehavior: behaviorPlugin[] = [ - // modifierKeys switch on the modifier BEFORE the keydown event - ...Object.entries(modifierKeys).map( - ([key, modKey]): behaviorPlugin => ({ - matches: keyDef => keyDef.key === key, - handle: (keyDef, element, {keyboardState}) => { - keyboardState.modifiers[modKey] = true - }, - }), - ), - - // AltGraph produces an extra keydown for Control - // The modifier does not change - { - matches: keyDef => keyDef.key === 'AltGraph', - handle: (keyDef, element, {keyboardMap, keyboardState}) => { - const ctrlKeyDef = keyboardMap.find( - k => k.key === 'Control', - ) ?? /* istanbul ignore next */ {key: 'Control', code: 'Control'} - fireEvent.keyDown(element, getKeyEventProps(ctrlKeyDef, keyboardState)) - }, - }, -] - export const keydownBehavior: behaviorPlugin[] = [ - { - matches: keyDef => keyDef.key === 'CapsLock', - handle: (keyDef, element, {keyboardState}) => { - keyboardState.modifiers.caps = !keyboardState.modifiers.caps - }, - }, { matches: (keyDef, element) => keyDef.key === 'Backspace' && isEditable(element), @@ -66,7 +29,7 @@ export const keydownBehavior: behaviorPlugin[] = [ { matches: keyDef => keyDef.key === 'Tab', handle: (keyDef, element, {keyboardState}) => { - const dest = getTabDestination(element, keyboardState.modifiers.shift) + const dest = getTabDestination(element, keyboardState.modifiers.Shift) if (dest === element.ownerDocument.body) { blur(element) } else { @@ -103,7 +66,7 @@ export const keypressBehavior: behaviorPlugin[] = [ // Links with href defined should handle Enter the same as a click (isElementType(element, 'a') && Boolean(element.href))), handle: (keyDef, element, {keyboardState}) => { - fireEvent.click(element, getMouseEventProps(keyboardState)) + fireEvent.click(element, getUIEventModifiers(keyboardState)) }, }, { @@ -122,38 +85,12 @@ export const keypressBehavior: behaviorPlugin[] = [ }, ] -export const preKeyupBehavior: behaviorPlugin[] = [ - // modifierKeys switch off the modifier BEFORE the keyup event - ...Object.entries(modifierKeys).map( - ([key, modKey]): behaviorPlugin => ({ - matches: keyDef => keyDef.key === key, - handle: (keyDef, element, {keyboardState}) => { - keyboardState.modifiers[modKey] = false - }, - }), - ), -] - export const keyupBehavior: behaviorPlugin[] = [ { matches: (keyDef, element) => keyDef.key === ' ' && isClickableInput(element), handle: (keyDef, element, {keyboardState}) => { - fireEvent.click(element, getMouseEventProps(keyboardState)) - }, - }, -] - -export const postKeyupBehavior: behaviorPlugin[] = [ - // AltGraph produces an extra keyup for Control - // The modifier does not change - { - matches: keyDef => keyDef.key === 'AltGraph', - handle: (keyDef, element, {keyboardMap, keyboardState}) => { - const ctrlKeyDef = keyboardMap.find( - k => k.key === 'Control', - ) ?? /* istanbul ignore next */ {key: 'Control', code: 'Control'} - fireEvent.keyUp(element, getKeyEventProps(ctrlKeyDef, keyboardState)) + fireEvent.click(element, getUIEventModifiers(keyboardState)) }, }, ] diff --git a/src/keyboard/plugins/index.ts b/src/keyboard/plugins/index.ts index 34da2be9..46fdcfc4 100644 --- a/src/keyboard/plugins/index.ts +++ b/src/keyboard/plugins/index.ts @@ -4,9 +4,10 @@ import * as controlKeys from './control' import * as characterKeys from './character' import * as functionalKeys from './functional' import * as combination from './combination' +import * as modifiers from './modifiers' export const preKeydownBehavior: behaviorPlugin[] = [ - ...functionalKeys.preKeydownBehavior, + ...modifiers.preKeydownBehavior, ] export const keydownBehavior: behaviorPlugin[] = [ @@ -22,11 +23,11 @@ export const keypressBehavior: behaviorPlugin[] = [ ] export const preKeyupBehavior: behaviorPlugin[] = [ - ...functionalKeys.preKeyupBehavior, + ...modifiers.preKeyupBehavior, ] export const keyupBehavior: behaviorPlugin[] = [...functionalKeys.keyupBehavior] export const postKeyupBehavior: behaviorPlugin[] = [ - ...functionalKeys.postKeyupBehavior, + ...modifiers.postKeyupBehavior, ] diff --git a/src/keyboard/plugins/modifiers.ts b/src/keyboard/plugins/modifiers.ts new file mode 100644 index 00000000..b470a2a7 --- /dev/null +++ b/src/keyboard/plugins/modifiers.ts @@ -0,0 +1,92 @@ +/** + * This file should contain behavior for modifier keys: + * https://www.w3.org/TR/uievents-key/#keys-modifier + */ + +import {fireEvent} from '@testing-library/dom' +import {getKeyEventProps} from '../../utils' +import {behaviorPlugin} from '../types' + +const modifierKeys = [ + 'Alt', + 'AltGraph', + 'Control', + 'Fn', + 'Meta', + 'Shift', + 'Symbol', +] as const +type ModififierKey = typeof modifierKeys[number] + +const modifierLocks = [ + 'CapsLock', + 'FnLock', + 'NumLock', + 'ScrollLock', + 'SymbolLock', +] as const +type ModififierLockKey = typeof modifierLocks[number] + +// modifierKeys switch on the modifier BEFORE the keydown event +export const preKeydownBehavior: behaviorPlugin[] = [ + { + matches: keyDef => modifierKeys.includes(keyDef.key as ModififierKey), + handle: (keyDef, element, {keyboardMap, keyboardState}) => { + keyboardState.modifiers[keyDef.key as ModififierKey] = true + + // AltGraph produces an extra keydown for Control + // The modifier does not change + if (keyDef.key === 'AltGraph') { + const ctrlKeyDef = keyboardMap.find( + k => k.key === 'Control', + ) ?? /* istanbul ignore next */ {key: 'Control', code: 'Control'} + fireEvent.keyDown(element, getKeyEventProps(ctrlKeyDef, keyboardState)) + } + }, + }, + { + matches: keyDef => modifierLocks.includes(keyDef.key as ModififierLockKey), + handle: (keyDef, element, {keyboardState}) => { + const key = keyDef.key as ModififierLockKey + keyboardState.modifierPhase[key] = keyboardState.modifiers[key] + + if (!keyboardState.modifierPhase[key]) { + keyboardState.modifiers[key] = true + } + }, + }, +] + +// modifierKeys switch off the modifier BEFORE the keyup event +export const preKeyupBehavior: behaviorPlugin[] = [ + { + matches: keyDef => modifierKeys.includes(keyDef.key as ModififierKey), + handle: (keyDef, element, {keyboardState}) => { + keyboardState.modifiers[keyDef.key as ModififierKey] = false + }, + }, + { + matches: keyDef => modifierLocks.includes(keyDef.key as ModififierLockKey), + handle: (keyDef, element, {keyboardState}) => { + const key = keyDef.key as ModififierLockKey + + if (keyboardState.modifierPhase[key]) { + keyboardState.modifiers[key] = false + } + }, + }, +] + +export const postKeyupBehavior: behaviorPlugin[] = [ + // AltGraph produces an extra keyup for Control + // The modifier does not change + { + matches: keyDef => keyDef.key === 'AltGraph', + handle: (keyDef, element, {keyboardMap, keyboardState}) => { + const ctrlKeyDef = keyboardMap.find( + k => k.key === 'Control', + ) ?? /* istanbul ignore next */ {key: 'Control', code: 'Control'} + fireEvent.keyUp(element, getKeyEventProps(ctrlKeyDef, keyboardState)) + }, + }, +] diff --git a/src/keyboard/types.ts b/src/keyboard/types.ts index 395e7518..3f2a5cd5 100644 --- a/src/keyboard/types.ts +++ b/src/keyboard/types.ts @@ -13,11 +13,25 @@ export type keyboardState = { Active modifiers */ modifiers: { - alt: boolean - caps: boolean - ctrl: boolean - meta: boolean - shift: boolean + Alt: boolean + AltGraph: boolean + CapsLock: boolean + Control: boolean + Fn: boolean + FnLock: boolean + Meta: boolean + NumLock: boolean + ScrollLock: boolean + Shift: boolean + Symbol: boolean + SymbolLock: boolean + } + modifierPhase: { + CapsLock?: boolean + FnLock?: boolean + NumLock?: boolean + ScrollLock?: boolean + SymbolLock?: boolean } /** diff --git a/src/utils/index.ts b/src/utils/index.ts index 58d3d1a4..989f288c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -27,6 +27,9 @@ export * from './focus/selectAll' export * from './focus/selection' export * from './focus/selector' +export * from './keyboard/getKeyEventProps' +export * from './keyboard/getUIEventModifiers' + export * from './keyDef/readNextDescriptor' export * from './misc/eventWrapper' diff --git a/src/utils/keyboard/getKeyEventProps.ts b/src/utils/keyboard/getKeyEventProps.ts new file mode 100644 index 00000000..e51cb982 --- /dev/null +++ b/src/utils/keyboard/getKeyEventProps.ts @@ -0,0 +1,10 @@ +import {keyboardKey, keyboardState} from '../../keyboard/types' +import {getUIEventModifiers} from './getUIEventModifiers' + +export function getKeyEventProps(keyDef: keyboardKey, state: keyboardState) { + return { + key: keyDef.key, + code: keyDef.code, + ...getUIEventModifiers(state), + } +} diff --git a/src/utils/keyboard/getUIEventModifiers.ts b/src/utils/keyboard/getUIEventModifiers.ts new file mode 100644 index 00000000..ccd6d160 --- /dev/null +++ b/src/utils/keyboard/getUIEventModifiers.ts @@ -0,0 +1,18 @@ +import type {keyboardState} from '../../keyboard' + +export function getUIEventModifiers(keyboardState: keyboardState) { + return { + altKey: keyboardState.modifiers.Alt, + ctrlKey: keyboardState.modifiers.Control, + metaKey: keyboardState.modifiers.Meta, + shiftKey: keyboardState.modifiers.Shift, + modifierAltGraph: keyboardState.modifiers.AltGraph, + modifierCapsLock: keyboardState.modifiers.CapsLock, + modifierFn: keyboardState.modifiers.Fn, + modifierFnLock: keyboardState.modifiers.FnLock, + modifierNumLock: keyboardState.modifiers.NumLock, + modifierScrollLock: keyboardState.modifiers.ScrollLock, + modifierSymbol: keyboardState.modifiers.Symbol, + modifierSymbolLock: keyboardState.modifiers.SymbolLock, + } +} diff --git a/src/utils/pointer/firePointerEvents.ts b/src/utils/pointer/firePointerEvents.ts index da58ef94..d0691fdf 100644 --- a/src/utils/pointer/firePointerEvents.ts +++ b/src/utils/pointer/firePointerEvents.ts @@ -3,6 +3,7 @@ import {eventMap} from '@testing-library/dom/dist/event-map' import type {pointerState} from '../../pointer/types' import type {keyboardState} from '../../keyboard/types' import {getMouseButton, getMouseButtons, MouseButton} from './mouseButtons' +import {getUIEventModifiers} from '../keyboard/getUIEventModifiers' export function firePointerEvent( target: Element, @@ -29,10 +30,7 @@ export function firePointerEvent( ) { const init: MouseEventInit & PointerEventInit = { ...coords, - altKey: keyboardState.modifiers.alt, - ctrlKey: keyboardState.modifiers.ctrl, - metaKey: keyboardState.modifiers.meta, - shiftKey: keyboardState.modifiers.shift, + ...getUIEventModifiers(keyboardState), } if (type === 'click' || type.startsWith('pointer')) { init.pointerId = pointerId diff --git a/tests/_helpers/utils.ts b/tests/_helpers/utils.ts index 3c9003b4..95e47a00 100644 --- a/tests/_helpers/utils.ts +++ b/tests/_helpers/utils.ts @@ -340,11 +340,16 @@ function addListeners( generalListener.mockClear() eventHandlerCalls.current = [] } - const getEvents = (type?: string) => + const getEvents = ( + type?: T, + ): Array => generalListener.mock.calls .map(([e]) => e) - .filter(e => !type || e.type === type) - const eventWasFired = (eventType: string) => getEvents(eventType).length > 0 + .filter(e => !type || e.type === type) as Array< + GlobalEventHandlersEventMap[T] + > + const eventWasFired = (eventType: keyof GlobalEventHandlersEventMap) => + getEvents(eventType).length > 0 function getClickEventsSnapshot() { const lines = getEvents().map(e => diff --git a/tests/keyboard/plugin/functional.ts b/tests/keyboard/plugin/functional.ts index 358c9e0c..cb76f35d 100644 --- a/tests/keyboard/plugin/functional.ts +++ b/tests/keyboard/plugin/functional.ts @@ -205,37 +205,3 @@ test('tab through elements', async () => { expect(getUISelection(elements[1])).toHaveProperty('startOffset', 0) expect(getUISelection(elements[1])).toHaveProperty('endOffset', 3) }) - -test.each([ - ['Shift', 'shiftKey'], - ['Control', 'ctrlKey'], - ['Alt', 'altKey'], - ['Meta', 'metaKey'], -])('Trigger modifier: %s', async (key, modifier) => { - const {element, getEvents} = setup(`
`) - const user = userEvent.setup() - element.focus() - - await user.keyboard(`{${key}>}`) - const modifierDown = getEvents('keydown')[0] - expect(modifierDown).toHaveProperty('key', key) - expect(modifierDown).toHaveProperty(modifier, true) - - await user.keyboard('a') - expect(getEvents('keydown')[1]).toHaveProperty(modifier, true) - expect(getEvents('keyup')[0]).toHaveProperty(modifier, true) - - await user.keyboard(`{/${key}}`) - const modifierUp = getEvents('keyup')[1] - expect(modifierUp).toHaveProperty('key', key) - expect(modifierUp).toHaveProperty(modifier, false) -}) - -test('switch CapsLock modifier', async () => { - // This is currently an implementation detail, - // but will be required for `autoModify`. - const keyboardState = await userEvent.keyboard('[CapsLock]') - expect(keyboardState.modifiers).toHaveProperty('caps', true) - await userEvent.keyboard('[CapsLock]', {keyboardState}) - expect(keyboardState.modifiers).toHaveProperty('caps', false) -}) diff --git a/tests/keyboard/plugin/modifiers.ts b/tests/keyboard/plugin/modifiers.ts new file mode 100644 index 00000000..0bc51ae4 --- /dev/null +++ b/tests/keyboard/plugin/modifiers.ts @@ -0,0 +1,73 @@ +import userEvent from '#src' +import {setup} from '#testHelpers/utils' + +test.each([ + ['Shift', 'shiftKey'], + ['Control', 'ctrlKey'], + ['Alt', 'altKey'], + ['Meta', 'metaKey'], +])('Trigger modifier: %s', async (key, modifier) => { + const {element, getEvents} = setup(`
`) + const user = userEvent.setup() + element.focus() + + await user.keyboard(`{${key}>}`) + const modifierDown = getEvents('keydown')[0] + expect(modifierDown).toHaveProperty('key', key) + expect(modifierDown).toHaveProperty(modifier, true) + + await user.keyboard('a') + expect(getEvents('keydown')[1]).toHaveProperty(modifier, true) + expect(getEvents('keyup')[0]).toHaveProperty(modifier, true) + + await user.keyboard(`{/${key}}`) + const modifierUp = getEvents('keyup')[1] + expect(modifierUp).toHaveProperty('key', key) + expect(modifierUp).toHaveProperty(modifier, false) +}) + +test.each([['AltGraph'], ['Fn'], ['Symbol']])( + 'Trigger modifier: %s', + async key => { + const {element, getEvents} = setup(`
`) + const user = userEvent.setup() + element.focus() + + await user.keyboard(`{${key}>}`) + const modifierDown = getEvents('keydown')[key === 'AltGraph' ? 1 : 0] + expect(modifierDown).toHaveProperty('key', key) + expect(modifierDown.getModifierState(key)).toBe(true) + + await user.keyboard('a') + expect( + getEvents('keydown')[key === 'AltGraph' ? 2 : 1].getModifierState(key), + ).toBe(true) + + await user.keyboard(`{/${key}}`) + const modifierUp = getEvents('keyup')[1] + expect(modifierUp.getModifierState(key)).toBe(false) + }, +) + +test.each([ + ['CapsLock'], + ['FnLock'], + ['NumLock'], + ['ScrollLock'], + ['SymbolLock'], +])('Switch lock modifier: %s', async key => { + const {element, getEvents} = setup(`
`) + const user = userEvent.setup() + element.focus() + + await user.keyboard(`{${key}}`) + const modifierOn = getEvents('keydown')[0] + expect(modifierOn.getModifierState(key)).toBe(true) + + await user.keyboard(`a`) + expect(getEvents('keydown')[1].getModifierState(key)).toBe(true) + + await user.keyboard(`{${key}}`) + const modifierOff = getEvents('keyup')[2] + expect(modifierOff.getModifierState(key)).toBe(false) +})