From 34769bd27fb479fed8bc2526f35901088c5e3a3e Mon Sep 17 00:00:00 2001 From: dedwardstech <50801476+dedwardstech@users.noreply.github.com> Date: Thu, 5 Dec 2019 02:11:54 -0500 Subject: [PATCH] feat: added modifier key support to keyboard api (#241) (#243) This commit adds support to spectator.keyboard.pressKey() for triggering key events with modifier keys. Example, spectator.keyboard.pressKey('ctrl.shift.a'). --- README.md | 2 + .../src/lib/internals/event-objects.ts | 37 +++++++++-- .../spectator/src/lib/internals/key-parser.ts | 65 +++++++++++++++++++ projects/spectator/src/lib/types.ts | 4 ++ .../test/events/events.component.html | 3 +- .../test/events/events.component.spec.ts | 5 ++ .../spectator/test/events/events.component.ts | 8 +++ 7 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 projects/spectator/src/lib/internals/key-parser.ts diff --git a/README.md b/README.md index eb5f6ea9..6a7fe3cd 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,8 @@ spectator.keyboard.pressEscape(); spectator.keyboard.pressTab(); spectator.keyboard.pressBackspace(); spectator.keyboard.pressKey('a'); +spectator.keyboard.pressKey('ctrl.a'); +spectator.keyboard.pressKey('ctrl.shift.a'); ``` ### Mouse helpers diff --git a/projects/spectator/src/lib/internals/event-objects.ts b/projects/spectator/src/lib/internals/event-objects.ts index b400c942..0e5837c2 100644 --- a/projects/spectator/src/lib/internals/event-objects.ts +++ b/projects/spectator/src/lib/internals/event-objects.ts @@ -2,6 +2,8 @@ * Credit - Angular Material */ +import { parseKeyOptions } from './key-parser'; + /** Creates a browser MouseEvent with the specified options. */ export function createMouseEvent(type: string, x: number = 0, y: number = 0, button: number = 0): MouseEvent { const event = document.createEvent('MouseEvent'); @@ -36,17 +38,38 @@ export function createTouchEvent(type: string, pageX: number = 0, pageY: number /** Dispatches a keydown event from an element. */ export function createKeyboardEvent(type: string, keyOrKeyCode: string | number, target?: Element): KeyboardEvent { - const key = typeof keyOrKeyCode === 'string' && keyOrKeyCode; - const keyCode = typeof keyOrKeyCode === 'number' && keyOrKeyCode; + const { key, keyCode, modifiers } = parseKeyOptions(keyOrKeyCode); const event = document.createEvent('KeyboardEvent') as any; const originalPreventDefault = event.preventDefault; // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`. if (event.initKeyEvent) { - event.initKeyEvent(type, true, true, window, 0, 0, 0, 0, 0, keyCode); + event.initKeyEvent(type, true, true, window, modifiers.control, modifiers.alt, modifiers.shift, modifiers.meta, keyCode); } else { - event.initKeyboardEvent(type, true, true, window, 0, key, 0, '', false); + // `initKeyboardEvent` expects to receive modifiers as a whitespace-delimited string + // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyboardEvent + const modifiersStr = (modifiers.control + ? 'Control ' + : '' + modifiers.alt + ? 'Alt ' + : '' + modifiers.shift + ? 'Shift ' + : '' + modifiers.meta + ? 'Meta' + : '' + ).trim(); + event.initKeyboardEvent( + type, + true /* canBubble */, + true /* cancelable */, + window /* view */, + 0 /* char */, + key /* key */, + 0 /* location */, + modifiersStr /* modifiersList */, + false /* repeat */ + ); } // Webkit Browsers don't set the keyCode when calling the init function. @@ -54,7 +77,11 @@ export function createKeyboardEvent(type: string, keyOrKeyCode: string | number, Object.defineProperties(event, { keyCode: { get: () => keyCode }, key: { get: () => key }, - target: { get: () => target } + target: { get: () => target }, + altKey: { get: () => !!modifiers.alt }, + ctrlKey: { get: () => !!modifiers.control }, + shiftKey: { get: () => !!modifiers.shift }, + metaKey: { get: () => !!modifiers.meta } }); // IE won't set `defaultPrevented` on synthetic events so we need to do it manually. diff --git a/projects/spectator/src/lib/internals/key-parser.ts b/projects/spectator/src/lib/internals/key-parser.ts new file mode 100644 index 00000000..217c1b9e --- /dev/null +++ b/projects/spectator/src/lib/internals/key-parser.ts @@ -0,0 +1,65 @@ +import { isNumber, isString } from '../types'; + +export interface ModifierKeys { + alt?: boolean; + control?: boolean; + shift?: boolean; + meta?: boolean; +} + +export interface KeyOptions { + key: string | false; + keyCode: number | false; + modifiers: ModifierKeys; +} + +export const parseKeyOptions = (keyOrKeyCode: string | number): KeyOptions => { + if (isNumber(keyOrKeyCode) && keyOrKeyCode) { + return { key: false, keyCode: keyOrKeyCode, modifiers: {} }; + } + + if (isString(keyOrKeyCode) && keyOrKeyCode) { + return parseKey(keyOrKeyCode as string); + } + + throw new Error('keyboard.pressKey() requires a valid key or keyCode'); +}; + +const parseKey = (keyStr: string): KeyOptions => { + if (keyStr.indexOf('.') < 0) { + return { key: keyStr, keyCode: false, modifiers: {} }; + } + + const keyParts = keyStr.split('.'); + const key = keyParts.pop() as string; + const modifiers = keyParts.reduce( + (mods, part) => { + switch (part) { + case 'control': + case 'ctrl': + mods.control = true; + + return mods; + case 'shift': + mods.shift = true; + + return mods; + case 'alt': + mods.alt = true; + + return mods; + case 'meta': + case 'cmd': + case 'win': + mods.meta = true; + + return mods; + default: + throw new Error(`invalid key modifier: ${part}`); + } + }, + { alt: false, control: false, shift: false, meta: false } + ); + + return { key, keyCode: false, modifiers }; +}; diff --git a/projects/spectator/src/lib/types.ts b/projects/spectator/src/lib/types.ts index 13deb73a..9607622b 100644 --- a/projects/spectator/src/lib/types.ts +++ b/projects/spectator/src/lib/types.ts @@ -24,6 +24,10 @@ export function isString(value: any): value is string { return typeof value === 'string'; } +export function isNumber(value: any): value is number { + return typeof value === 'number'; +} + export function isType(v: any): v is Type { return typeof v === 'function'; } diff --git a/projects/spectator/test/events/events.component.html b/projects/spectator/test/events/events.component.html index 9aafdfcc..d8f53d56 100644 --- a/projects/spectator/test/events/events.component.html +++ b/projects/spectator/test/events/events.component.html @@ -1,3 +1,4 @@

{{ event }}

- + diff --git a/projects/spectator/test/events/events.component.spec.ts b/projects/spectator/test/events/events.component.spec.ts index 47bdc274..2550a599 100644 --- a/projects/spectator/test/events/events.component.spec.ts +++ b/projects/spectator/test/events/events.component.spec.ts @@ -14,7 +14,12 @@ describe('EventsComponent', () => { expect(spectator.query('h1')).toHaveText('focus'); spectator.blur('input'); expect(spectator.query('h1')).toHaveText('blur'); + spectator.keyboard.pressKey('a', 'input'); expect(spectator.query('h1')).toHaveText('pressed a'); + spectator.keyboard.pressKey('ctrl.a', 'input'); + expect(spectator.query('h1')).toHaveText('pressed ctrl.a'); + spectator.keyboard.pressKey('ctrl.shift.a', 'input'); + expect(spectator.query('h1')).toHaveText('pressed ctrl.shift.a'); }); }); diff --git a/projects/spectator/test/events/events.component.ts b/projects/spectator/test/events/events.component.ts index 68dc0d15..802a0cad 100644 --- a/projects/spectator/test/events/events.component.ts +++ b/projects/spectator/test/events/events.component.ts @@ -19,4 +19,12 @@ export class EventsComponent { public onPressA(): void { this.event = 'pressed a'; } + + public onPressCtrlA(): void { + this.event = 'pressed ctrl.a'; + } + + public onPressCtrlShiftA(): void { + this.event = 'pressed ctrl.shift.a'; + } }