Skip to content

Commit e0a59b1

Browse files
committed
feat(cdk-experimental/testing): Improve keyboard event support in harnesses
- Adds support for special keys like <kbd>ENTER</kbd>, <kbd>ESCAPE</kbd>, etc. - Adds support for modifier keys (e.g. <kbd>SHIFT</kbd>, etc.) - Improve code sharing for sending key events between our existing tests and the test harnesses.
1 parent 8e321ae commit e0a59b1

File tree

20 files changed

+243
-160
lines changed

20 files changed

+243
-160
lines changed

src/cdk-experimental/dialog/dialog.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,8 @@ describe('Dialog', () => {
308308
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
309309
let container = overlayContainerElement.querySelector('cdk-dialog-container') as HTMLElement;
310310
dispatchKeyboardEvent(document.body, 'keydown', A);
311-
dispatchKeyboardEvent(document.body, 'keydown', A, backdrop);
312-
dispatchKeyboardEvent(document.body, 'keydown', A, container);
311+
dispatchKeyboardEvent(document.body, 'keydown', A, undefined, backdrop);
312+
dispatchKeyboardEvent(document.body, 'keydown', A, undefined, container);
313313

314314
expect(spy).toHaveBeenCalledTimes(3);
315315
}));

src/cdk-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ describe('CDK Popover Edit', () => {
484484

485485
describe('arrow keys', () => {
486486
const dispatchKey = (cell: HTMLElement, keyCode: number) =>
487-
dispatchKeyboardEvent(cell, 'keydown', keyCode, cell);
487+
dispatchKeyboardEvent(cell, 'keydown', keyCode, undefined, cell);
488488

489489
it('moves focus up/down/left/right and prevents default', () => {
490490
const rowCells = getRowCells();

src/cdk-experimental/testing/testbed/unit-test-element.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,15 @@
77
*/
88

99
import {
10-
dispatchFakeEvent,
11-
dispatchKeyboardEvent,
10+
clearElement,
1211
dispatchMouseEvent,
12+
isTextInput,
1313
triggerBlur,
14-
triggerFocus
14+
triggerFocus,
15+
typeInElement
1516
} from '@angular/cdk/testing';
1617
import {TestElement} from '../test-element';
1718

18-
function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement {
19-
return element.nodeName.toLowerCase() === 'input' ||
20-
element.nodeName.toLowerCase() === 'textarea' ;
21-
}
22-
2319
/** A `TestElement` implementation for unit tests. */
2420
export class UnitTestElement implements TestElement {
2521
constructor(readonly element: Element, private _stabilize: () => Promise<void>) {}
@@ -35,9 +31,7 @@ export class UnitTestElement implements TestElement {
3531
if (!isTextInput(this.element)) {
3632
throw Error('Attempting to clear an invalid element');
3733
}
38-
triggerFocus(this.element as HTMLElement);
39-
this.element.value = '';
40-
dispatchFakeEvent(this.element, 'input');
34+
clearElement(this.element);
4135
await this._stabilize();
4236
}
4337

@@ -68,19 +62,7 @@ export class UnitTestElement implements TestElement {
6862

6963
async sendKeys(keys: string): Promise<void> {
7064
await this._stabilize();
71-
triggerFocus(this.element as HTMLElement);
72-
for (const key of keys) {
73-
const keyCode = key.charCodeAt(0);
74-
dispatchKeyboardEvent(this.element, 'keydown', keyCode);
75-
dispatchKeyboardEvent(this.element, 'keypress', keyCode);
76-
if (isTextInput(this.element)) {
77-
this.element.value += key;
78-
}
79-
dispatchKeyboardEvent(this.element, 'keyup', keyCode);
80-
if (isTextInput(this.element)) {
81-
dispatchFakeEvent(this.element, 'input');
82-
}
83-
}
65+
typeInElement(this.element as HTMLElement, keys);
8466
await this._stabilize();
8567
}
8668

src/cdk/a11y/key-manager/list-key-manager.spec.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -655,9 +655,9 @@ describe('Key managers', () => {
655655
});
656656

657657
it('should debounce the input key presses', fakeAsync(() => {
658-
keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o')); // types "o"
659-
keyManager.onKeydown(createKeyboardEvent('keydown', 78, undefined, 'n')); // types "n"
660-
keyManager.onKeydown(createKeyboardEvent('keydown', 69, undefined, 'e')); // types "e"
658+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o"
659+
keyManager.onKeydown(createKeyboardEvent('keydown', 78, 'n')); // types "n"
660+
keyManager.onKeydown(createKeyboardEvent('keydown', 69, 'e')); // types "e"
661661

662662
expect(keyManager.activeItem).not.toBe(itemList.items[0]);
663663

@@ -667,15 +667,15 @@ describe('Key managers', () => {
667667
}));
668668

669669
it('should focus the first item that starts with a letter', fakeAsync(() => {
670-
keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t"
670+
keyManager.onKeydown(createKeyboardEvent('keydown', 84, 't')); // types "t"
671671

672672
tick(debounceInterval);
673673

674674
expect(keyManager.activeItem).toBe(itemList.items[1]);
675675
}));
676676

677677
it('should not move focus if a modifier, that is not allowed, is pressed', fakeAsync(() => {
678-
const tEvent = createKeyboardEvent('keydown', 84, undefined, 't');
678+
const tEvent = createKeyboardEvent('keydown', 84, 't');
679679
Object.defineProperty(tEvent, 'ctrlKey', {get: () => true});
680680

681681
expect(keyManager.activeItem).toBeFalsy();
@@ -687,7 +687,7 @@ describe('Key managers', () => {
687687
}));
688688

689689
it('should always allow the shift key', fakeAsync(() => {
690-
const tEvent = createKeyboardEvent('keydown', 84, undefined, 't');
690+
const tEvent = createKeyboardEvent('keydown', 84, 't');
691691
Object.defineProperty(tEvent, 'shiftKey', {get: () => true});
692692

693693
expect(keyManager.activeItem).toBeFalsy();
@@ -699,17 +699,17 @@ describe('Key managers', () => {
699699
}));
700700

701701
it('should focus the first item that starts with sequence of letters', fakeAsync(() => {
702-
keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t"
703-
keyManager.onKeydown(createKeyboardEvent('keydown', 72, undefined, 'h')); // types "h"
702+
keyManager.onKeydown(createKeyboardEvent('keydown', 84, 't')); // types "t"
703+
keyManager.onKeydown(createKeyboardEvent('keydown', 72, 'h')); // types "h"
704704

705705
tick(debounceInterval);
706706

707707
expect(keyManager.activeItem).toBe(itemList.items[2]);
708708
}));
709709

710710
it('should cancel any pending timers if a navigation key is pressed', fakeAsync(() => {
711-
keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t"
712-
keyManager.onKeydown(createKeyboardEvent('keydown', 72, undefined, 'h')); // types "h"
711+
keyManager.onKeydown(createKeyboardEvent('keydown', 84, 't')); // types "t"
712+
keyManager.onKeydown(createKeyboardEvent('keydown', 72, 'h')); // types "h"
713713
keyManager.onKeydown(fakeKeyEvents.downArrow);
714714

715715
tick(debounceInterval);
@@ -724,7 +724,7 @@ describe('Key managers', () => {
724724
new FakeFocusable('три')
725725
];
726726

727-
const keyboardEvent = createKeyboardEvent('keydown', 68, undefined, 'д');
727+
const keyboardEvent = createKeyboardEvent('keydown', 68, 'д');
728728

729729
keyManager.onKeydown(keyboardEvent); // types "д"
730730
tick(debounceInterval);
@@ -739,15 +739,15 @@ describe('Key managers', () => {
739739
new FakeFocusable('`!?')
740740
];
741741

742-
keyManager.onKeydown(createKeyboardEvent('keydown', 192, undefined, '`')); // types "`"
742+
keyManager.onKeydown(createKeyboardEvent('keydown', 192, '`')); // types "`"
743743
tick(debounceInterval);
744744
expect(keyManager.activeItem).toBe(itemList.items[2]);
745745

746-
keyManager.onKeydown(createKeyboardEvent('keydown', 51, undefined, '3')); // types "3"
746+
keyManager.onKeydown(createKeyboardEvent('keydown', 51, '3')); // types "3"
747747
tick(debounceInterval);
748748
expect(keyManager.activeItem).toBe(itemList.items[1]);
749749

750-
keyManager.onKeydown(createKeyboardEvent('keydown', 219, undefined, '[')); // types "["
750+
keyManager.onKeydown(createKeyboardEvent('keydown', 219, '[')); // types "["
751751
tick(debounceInterval);
752752
expect(keyManager.activeItem).toBe(itemList.items[0]);
753753
}));
@@ -756,7 +756,7 @@ describe('Key managers', () => {
756756
expect(keyManager.activeItem).toBeFalsy();
757757

758758
itemList.items[0].disabled = true;
759-
keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o')); // types "o"
759+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o"
760760
tick(debounceInterval);
761761

762762
expect(keyManager.activeItem).toBeFalsy();
@@ -772,7 +772,7 @@ describe('Key managers', () => {
772772
];
773773

774774
keyManager.setActiveItem(1);
775-
keyManager.onKeydown(createKeyboardEvent('keydown', 66, undefined, 'b'));
775+
keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b'));
776776
tick(debounceInterval);
777777

778778
expect(keyManager.activeItem).toBe(itemList.items[3]);
@@ -788,31 +788,31 @@ describe('Key managers', () => {
788788
];
789789

790790
keyManager.setActiveItem(3);
791-
keyManager.onKeydown(createKeyboardEvent('keydown', 66, undefined, 'b'));
791+
keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b'));
792792
tick(debounceInterval);
793793

794794
expect(keyManager.activeItem).toBe(itemList.items[0]);
795795
}));
796796

797797
it('should wrap back around if the last item is active', fakeAsync(() => {
798798
keyManager.setActiveItem(2);
799-
keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o'));
799+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o'));
800800
tick(debounceInterval);
801801

802802
expect(keyManager.activeItem).toBe(itemList.items[0]);
803803
}));
804804

805805
it('should be able to select the first item', fakeAsync(() => {
806806
keyManager.setActiveItem(-1);
807-
keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o'));
807+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o'));
808808
tick(debounceInterval);
809809

810810
expect(keyManager.activeItem).toBe(itemList.items[0]);
811811
}));
812812

813813
it('should not do anything if there is no match', fakeAsync(() => {
814814
keyManager.setActiveItem(1);
815-
keyManager.onKeydown(createKeyboardEvent('keydown', 87, undefined, 'w'));
815+
keyManager.onKeydown(createKeyboardEvent('keydown', 87, 'w'));
816816
tick(debounceInterval);
817817

818818
expect(keyManager.activeItem).toBe(itemList.items[1]);

src/cdk/overlay/keyboard/overlay-keyboard-dispatcher.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,11 @@ describe('OverlayKeyboardDispatcher', () => {
104104
instance.attach(new ComponentPortal(TestComponent));
105105
instance.keydownEvents().subscribe(spy);
106106

107-
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, instance.overlayElement);
107+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, instance.overlayElement);
108108
expect(spy).toHaveBeenCalledTimes(1);
109109

110110
instance.detach();
111-
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, instance.overlayElement);
111+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, instance.overlayElement);
112112

113113
expect(spy).toHaveBeenCalledTimes(1);
114114
});
@@ -120,11 +120,11 @@ describe('OverlayKeyboardDispatcher', () => {
120120
instance.attach(new ComponentPortal(TestComponent));
121121
instance.keydownEvents().subscribe(spy);
122122

123-
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, instance.overlayElement);
123+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, instance.overlayElement);
124124
expect(spy).toHaveBeenCalledTimes(1);
125125

126126
instance.dispose();
127-
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, instance.overlayElement);
127+
dispatchKeyboardEvent(document.body, 'keydown', ESCAPE, undefined, instance.overlayElement);
128128

129129
expect(spy).toHaveBeenCalledTimes(1);
130130
});

src/cdk/testing/dispatch-events.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?:
3434
* Shorthand to dispatch a keyboard event with a specified key code.
3535
* @docs-private
3636
*/
37-
export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number, target?: Element):
38-
KeyboardEvent {
39-
return dispatchEvent(node, createKeyboardEvent(type, keyCode, target)) as KeyboardEvent;
37+
export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number, key?: string,
38+
target?: Element): KeyboardEvent {
39+
return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, target)) as KeyboardEvent;
4040
}
4141

4242
/**

src/cdk/testing/event-objects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function createTouchEvent(type: string, pageX = 0, pageY = 0) {
6363
* Dispatches a keydown event from an element.
6464
* @docs-private
6565
*/
66-
export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string) {
66+
export function createKeyboardEvent(type: string, keyCode: number, key?: string, target?: Element) {
6767
let event = document.createEvent('KeyboardEvent') as any;
6868
let originalPreventDefault = event.preventDefault;
6969

src/cdk/testing/type-in-element.ts

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,95 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {dispatchFakeEvent} from './dispatch-events';
9+
import {dispatchFakeEvent, dispatchKeyboardEvent} from './dispatch-events';
10+
import {triggerFocus} from './element-focus';
11+
12+
/** Modifier keys that may be held while typing. */
13+
export interface KeyModifiers {
14+
control?: boolean;
15+
alt?: boolean;
16+
shift?: boolean;
17+
meta?: boolean;
18+
}
19+
20+
/**
21+
* Represents a special key that does not result in a character being inputed in a text field.
22+
* @docs-private
23+
*/
24+
export interface SpecialKey {
25+
keyCode: number;
26+
key?: string;
27+
}
28+
29+
/**
30+
* Checks whether the given Element is a text input element.
31+
* @docs-private
32+
*/
33+
export function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement {
34+
return element.nodeName.toLowerCase() === 'input' ||
35+
element.nodeName.toLowerCase() === 'textarea' ;
36+
}
37+
38+
/**
39+
* Focuses an input, sets its value and dispatches
40+
* the `input` event, simulating the user typing.
41+
* @param element Element onto which to set the value.
42+
* @param keys The keys to send to the element.
43+
* @docs-private
44+
*/
45+
export function typeInElement(element: HTMLElement, ...keys: (string | SpecialKey)[]): void;
1046

1147
/**
1248
* Focuses an input, sets its value and dispatches
1349
* the `input` event, simulating the user typing.
14-
* @param value Value to be set on the input.
1550
* @param element Element onto which to set the value.
51+
* @param modifiers Modifier keys that are held while typing.
52+
* @param keys The keys to send to the element.
53+
* @docs-private
54+
*/
55+
export function typeInElement(
56+
element: HTMLElement, modifiers: KeyModifiers, ...keys: (string | SpecialKey)[]): void;
57+
58+
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
59+
const first = modifiersAndKeys[0];
60+
let modifiers: KeyModifiers;
61+
let keys: (string | SpecialKey)[];
62+
if (typeof first !== 'string' && first.keyCode === undefined) {
63+
modifiers = first;
64+
keys = modifiersAndKeys.slice(1);
65+
} else {
66+
modifiers = {};
67+
keys = modifiersAndKeys;
68+
}
69+
70+
// TODO: pass through modifiers
71+
triggerFocus(element);
72+
for (const keyOrStr of keys) {
73+
if (typeof keyOrStr === 'string') {
74+
for (const key of keyOrStr) {
75+
const keyCode = key.charCodeAt(0);
76+
dispatchKeyboardEvent(element, 'keydown', keyCode);
77+
dispatchKeyboardEvent(element, 'keypress', keyCode);
78+
if (isTextInput(element)) {
79+
element.value += key;
80+
dispatchFakeEvent(element, 'input');
81+
}
82+
dispatchKeyboardEvent(element, 'keyup', keyCode);
83+
}
84+
} else {
85+
dispatchKeyboardEvent(element, 'keydown', keyOrStr.keyCode, keyOrStr.key);
86+
dispatchKeyboardEvent(element, 'keypress', keyOrStr.keyCode, keyOrStr.key);
87+
dispatchKeyboardEvent(element, 'keyup', keyOrStr.keyCode, keyOrStr.key);
88+
}
89+
}
90+
}
91+
92+
/**
93+
* Clears the text in an input or textarea element.
1694
* @docs-private
1795
*/
18-
export function typeInElement(value: string, element: HTMLInputElement) {
19-
element.focus();
20-
element.value = value;
96+
export function clearElement(element: HTMLInputElement | HTMLTextAreaElement) {
97+
triggerFocus(element as HTMLElement);
98+
element.value = '';
2199
dispatchFakeEvent(element, 'input');
22100
}

0 commit comments

Comments
 (0)