Skip to content

Commit

Permalink
feat: added modifier key support to keyboard api (#241) (#243)
Browse files Browse the repository at this point in the history
This commit adds support to spectator.keyboard.pressKey() for triggering
key events with modifier keys.

Example, spectator.keyboard.pressKey('ctrl.shift.a').
  • Loading branch information
dedwardstech authored and NetanelBasal committed Dec 5, 2019
1 parent 20168ef commit 34769bd
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 6 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 32 additions & 5 deletions projects/spectator/src/lib/internals/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -36,25 +38,50 @@ 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.
// See related bug https://bugs.webkit.org/show_bug.cgi?id=16735
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.
Expand Down
65 changes: 65 additions & 0 deletions projects/spectator/src/lib/internals/key-parser.ts
Original file line number Diff line number Diff line change
@@ -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 };
};
4 changes: 4 additions & 0 deletions projects/spectator/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
return typeof v === 'function';
}
Expand Down
3 changes: 2 additions & 1 deletion projects/spectator/test/events/events.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<h1>{{ event }}</h1>
<input type="text" (blur)="onBlur()" (focus)="onFocus()" (keyup.a)="onPressA()">
<input type="text" (blur)="onBlur()" (focus)="onFocus()" (keyup.a)="onPressA()" (keyup.control.a)="onPressCtrlA()"
(keyup.control.shift.a)="onPressCtrlShiftA()">

5 changes: 5 additions & 0 deletions projects/spectator/test/events/events.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
8 changes: 8 additions & 0 deletions projects/spectator/test/events/events.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}

0 comments on commit 34769bd

Please sign in to comment.