Skip to content

Commit 286038c

Browse files
authored
feat(cdk-experimental/testing): Improve keyboard event support in harnesses (#16645)
* 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. * Add support for modifier keys to `typeInElement` * add better key support to harnesses * fix lint * address feedback * address feedback
1 parent 6416bab commit 286038c

32 files changed

+485
-193
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/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ ng_module(
1212
],
1313
),
1414
module_name = "@angular/cdk-experimental/testing",
15+
deps = [
16+
"//src/cdk/testing",
17+
],
1518
)
1619

1720
ng_web_test_suite(

src/cdk-experimental/testing/protractor/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ts_library(
1111
module_name = "@angular/cdk-experimental/testing/protractor",
1212
deps = [
1313
"//src/cdk-experimental/testing",
14+
"//src/cdk/testing",
1415
"@npm//protractor",
1516
],
1617
)

src/cdk-experimental/testing/protractor/protractor-element.ts

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,61 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {browser, ElementFinder} from 'protractor';
10-
import {TestElement} from '../test-element';
9+
import {ModifierKeys} from '@angular/cdk/testing';
10+
import {browser, ElementFinder, Key} from 'protractor';
11+
import {TestElement, TestKey} from '../test-element';
12+
13+
/** Maps the `TestKey` constants to Protractor's `Key` constants. */
14+
const keyMap = {
15+
[TestKey.BACKSPACE]: Key.BACK_SPACE,
16+
[TestKey.TAB]: Key.TAB,
17+
[TestKey.ENTER]: Key.ENTER,
18+
[TestKey.SHIFT]: Key.SHIFT,
19+
[TestKey.CONTROL]: Key.CONTROL,
20+
[TestKey.ALT]: Key.ALT,
21+
[TestKey.ESCAPE]: Key.ESCAPE,
22+
[TestKey.PAGE_UP]: Key.PAGE_UP,
23+
[TestKey.PAGE_DOWN]: Key.PAGE_DOWN,
24+
[TestKey.END]: Key.END,
25+
[TestKey.HOME]: Key.HOME,
26+
[TestKey.LEFT_ARROW]: Key.ARROW_LEFT,
27+
[TestKey.UP_ARROW]: Key.ARROW_UP,
28+
[TestKey.RIGHT_ARROW]: Key.ARROW_RIGHT,
29+
[TestKey.DOWN_ARROW]: Key.ARROW_DOWN,
30+
[TestKey.INSERT]: Key.INSERT,
31+
[TestKey.DELETE]: Key.DELETE,
32+
[TestKey.F1]: Key.F1,
33+
[TestKey.F2]: Key.F2,
34+
[TestKey.F3]: Key.F3,
35+
[TestKey.F4]: Key.F4,
36+
[TestKey.F5]: Key.F5,
37+
[TestKey.F6]: Key.F6,
38+
[TestKey.F7]: Key.F7,
39+
[TestKey.F8]: Key.F8,
40+
[TestKey.F9]: Key.F9,
41+
[TestKey.F10]: Key.F10,
42+
[TestKey.F11]: Key.F11,
43+
[TestKey.F12]: Key.F12,
44+
[TestKey.META]: Key.META
45+
};
46+
47+
/** Converts a `ModifierKeys` object to a list of Protractor `Key`s. */
48+
function toProtractorModifierKeys(modifiers: ModifierKeys): string[] {
49+
const result: string[] = [];
50+
if (modifiers.control) {
51+
result.push(Key.CONTROL);
52+
}
53+
if (modifiers.alt) {
54+
result.push(Key.ALT);
55+
}
56+
if (modifiers.shift) {
57+
result.push(Key.SHIFT);
58+
}
59+
if (modifiers.meta) {
60+
result.push(Key.META);
61+
}
62+
return result;
63+
}
1164

1265
/** A `TestElement` implementation for Protractor. */
1366
export class ProtractorElement implements TestElement {
@@ -39,8 +92,26 @@ export class ProtractorElement implements TestElement {
3992
.perform();
4093
}
4194

42-
async sendKeys(keys: string): Promise<void> {
43-
return this.element.sendKeys(keys);
95+
async sendKeys(...keys: (string | TestKey)[]): Promise<void>;
96+
async sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
97+
async sendKeys(...modifiersAndKeys: any[]): Promise<void> {
98+
const first = modifiersAndKeys[0];
99+
let modifiers: ModifierKeys;
100+
let rest: (string | TestKey)[];
101+
if (typeof first !== 'string' && typeof first !== 'number') {
102+
modifiers = first;
103+
rest = modifiersAndKeys.slice(1);
104+
} else {
105+
modifiers = {};
106+
rest = modifiersAndKeys;
107+
}
108+
109+
const modifierKeys = toProtractorModifierKeys(modifiers);
110+
const keys = rest.map(k => typeof k === 'string' ? k.split('') : [keyMap[k]])
111+
.reduce((arr, k) => arr.concat(k), [])
112+
.map(k => Key.chord(...modifierKeys, k));
113+
114+
return this.element.sendKeys(...keys);
44115
}
45116

46117
async text(): Promise<string> {

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,48 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
/** Keyboard keys that do not result in input characters. */
10+
import {ModifierKeys} from '@angular/cdk/testing';
11+
12+
/** An enum of non-text keys that can be used with the `sendKeys` method. */
13+
// NOTE: This is a separate enum from `@angular/cdk/keycodes` because we don't necessarily want to
14+
// support every possible keyCode. We also can't rely on Protractor's `Key` because we don't want a
15+
// dependency on any particular testing framework here. Instead we'll just maintain this supported
16+
// list of keys and let individual concrete `HarnessEnvironment` classes map them to whatever key
17+
// representation is used in its respective testing framework.
18+
export enum TestKey {
19+
BACKSPACE,
20+
TAB,
21+
ENTER,
22+
SHIFT,
23+
CONTROL,
24+
ALT,
25+
ESCAPE,
26+
PAGE_UP,
27+
PAGE_DOWN,
28+
END,
29+
HOME,
30+
LEFT_ARROW,
31+
UP_ARROW,
32+
RIGHT_ARROW,
33+
DOWN_ARROW,
34+
INSERT,
35+
DELETE,
36+
F1,
37+
F2,
38+
F3,
39+
F4,
40+
F5,
41+
F6,
42+
F7,
43+
F8,
44+
F9,
45+
F10,
46+
F11,
47+
F12,
48+
META
49+
}
50+
951
/**
1052
* This acts as a common interface for DOM elements across both unit and e2e tests. It is the
1153
* interface through which the ComponentHarness interacts with the component's DOM.
@@ -33,7 +75,13 @@ export interface TestElement {
3375
* Sends the given string to the input as a series of key presses. Also fires input events
3476
* and attempts to add the string to the Element's value.
3577
*/
36-
sendKeys(keys: string): Promise<void>;
78+
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
79+
80+
/**
81+
* Sends the given string to the input as a series of key presses. Also fires input events
82+
* and attempts to add the string to the Element's value.
83+
*/
84+
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
3785

3886
/** Gets the text from the element. */
3987
text(): Promise<string>;

src/cdk-experimental/testing/testbed/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ts_library(
1111
module_name = "@angular/cdk-experimental/testing/testbed",
1212
deps = [
1313
"//src/cdk-experimental/testing",
14+
"//src/cdk/keycodes",
1415
"//src/cdk/testing",
1516
"@npm//@angular/core",
1617
],

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

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,51 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import * as keyCodes from '@angular/cdk/keycodes';
910
import {
10-
dispatchFakeEvent,
11-
dispatchKeyboardEvent,
11+
clearElement,
1212
dispatchMouseEvent,
13+
isTextInput,
14+
ModifierKeys,
1315
triggerBlur,
14-
triggerFocus
16+
triggerFocus,
17+
typeInElement
1518
} from '@angular/cdk/testing';
16-
import {TestElement} from '../test-element';
19+
import {TestElement, TestKey} from '../test-element';
1720

18-
function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement {
19-
return element.nodeName.toLowerCase() === 'input' ||
20-
element.nodeName.toLowerCase() === 'textarea' ;
21-
}
21+
/** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */
22+
const keyMap = {
23+
[TestKey.BACKSPACE]: {keyCode: keyCodes.BACKSPACE, key: 'Backspace'},
24+
[TestKey.TAB]: {keyCode: keyCodes.TAB, key: 'Tab'},
25+
[TestKey.ENTER]: {keyCode: keyCodes.ENTER, key: 'Enter'},
26+
[TestKey.SHIFT]: {keyCode: keyCodes.SHIFT, key: 'Shift'},
27+
[TestKey.CONTROL]: {keyCode: keyCodes.CONTROL, key: 'Control'},
28+
[TestKey.ALT]: {keyCode: keyCodes.ALT, key: 'Alt'},
29+
[TestKey.ESCAPE]: {keyCode: keyCodes.ESCAPE, key: 'Escape'},
30+
[TestKey.PAGE_UP]: {keyCode: keyCodes.PAGE_UP, key: 'PageUp'},
31+
[TestKey.PAGE_DOWN]: {keyCode: keyCodes.PAGE_DOWN, key: 'PageDown'},
32+
[TestKey.END]: {keyCode: keyCodes.END, key: 'End'},
33+
[TestKey.HOME]: {keyCode: keyCodes.HOME, key: 'Home'},
34+
[TestKey.LEFT_ARROW]: {keyCode: keyCodes.LEFT_ARROW, key: 'ArrowLeft'},
35+
[TestKey.UP_ARROW]: {keyCode: keyCodes.UP_ARROW, key: 'ArrowUp'},
36+
[TestKey.RIGHT_ARROW]: {keyCode: keyCodes.RIGHT_ARROW, key: 'ArrowRight'},
37+
[TestKey.DOWN_ARROW]: {keyCode: keyCodes.DOWN_ARROW, key: 'ArrowDown'},
38+
[TestKey.INSERT]: {keyCode: keyCodes.INSERT, key: 'Insert'},
39+
[TestKey.DELETE]: {keyCode: keyCodes.DELETE, key: 'Delete'},
40+
[TestKey.F1]: {keyCode: keyCodes.F1, key: 'F1'},
41+
[TestKey.F2]: {keyCode: keyCodes.F2, key: 'F2'},
42+
[TestKey.F3]: {keyCode: keyCodes.F3, key: 'F3'},
43+
[TestKey.F4]: {keyCode: keyCodes.F4, key: 'F4'},
44+
[TestKey.F5]: {keyCode: keyCodes.F5, key: 'F5'},
45+
[TestKey.F6]: {keyCode: keyCodes.F6, key: 'F6'},
46+
[TestKey.F7]: {keyCode: keyCodes.F7, key: 'F7'},
47+
[TestKey.F8]: {keyCode: keyCodes.F8, key: 'F8'},
48+
[TestKey.F9]: {keyCode: keyCodes.F9, key: 'F9'},
49+
[TestKey.F10]: {keyCode: keyCodes.F10, key: 'F10'},
50+
[TestKey.F11]: {keyCode: keyCodes.F11, key: 'F11'},
51+
[TestKey.F12]: {keyCode: keyCodes.F12, key: 'F12'},
52+
[TestKey.META]: {keyCode: keyCodes.META, key: 'Meta'}
53+
};
2254

2355
/** A `TestElement` implementation for unit tests. */
2456
export class UnitTestElement implements TestElement {
@@ -35,9 +67,7 @@ export class UnitTestElement implements TestElement {
3567
if (!isTextInput(this.element)) {
3668
throw Error('Attempting to clear an invalid element');
3769
}
38-
triggerFocus(this.element as HTMLElement);
39-
this.element.value = '';
40-
dispatchFakeEvent(this.element, 'input');
70+
clearElement(this.element);
4171
await this._stabilize();
4272
}
4373

@@ -66,21 +96,12 @@ export class UnitTestElement implements TestElement {
6696
await this._stabilize();
6797
}
6898

69-
async sendKeys(keys: string): Promise<void> {
99+
async sendKeys(...keys: (string | TestKey)[]): Promise<void>;
100+
async sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
101+
async sendKeys(...modifiersAndKeys: any[]): Promise<void> {
70102
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-
}
103+
const args = modifiersAndKeys.map(k => typeof k === 'number' ? keyMap[k as TestKey] : k);
104+
typeInElement(this.element as HTMLElement, ...args);
84105
await this._stabilize();
85106
}
86107

src/cdk-experimental/testing/tests/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ng_module(
1414
assets = glob(["**/*.html"]),
1515
module_name = "@angular/cdk-experimental/testing/tests",
1616
deps = [
17+
"//src/cdk/keycodes",
1718
"@npm//@angular/forms",
1819
],
1920
)

src/cdk-experimental/testing/tests/harnesses/main-component-harness.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {ComponentHarness} from '../../component-harness';
10-
import {TestElement} from '../../test-element';
10+
import {TestElement, TestKey} from '../../test-element';
1111
import {SubComponentHarness} from './sub-component-harness';
1212

1313
export class WrongComponentHarness extends ComponentHarness {
@@ -47,6 +47,7 @@ export class MainComponentHarness extends ComponentHarness {
4747
readonly testLists = this.locatorForAll(SubComponentHarness.with({title: /test/}));
4848
readonly requiredFourIteamToolsLists =
4949
this.locatorFor(SubComponentHarness.with({title: 'List of test tools', itemCount: 4}));
50+
readonly specaialKey = this.locatorFor('.special-key');
5051

5152
private _testTools = this.locatorFor(SubComponentHarness);
5253

@@ -66,4 +67,12 @@ export class MainComponentHarness extends ComponentHarness {
6667
const subComponent = await this._testTools();
6768
return subComponent.getItems();
6869
}
70+
71+
async sendEnter(): Promise<void> {
72+
return (await this.input()).sendKeys(TestKey.ENTER);
73+
}
74+
75+
async sendAltJ(): Promise<void> {
76+
return (await this.input()).sendKeys({alt: true}, 'j');
77+
}
6978
}

src/cdk-experimental/testing/tests/protractor.e2e.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@ describe('ProtractorHarnessEnvironment', () => {
155155
const globalEl = await harness.globalEl();
156156
expect(await globalEl.text()).toBe('I am a sibling!');
157157
});
158+
159+
it('should send enter key', async () => {
160+
const specialKey = await harness.specaialKey();
161+
await harness.sendEnter();
162+
expect(await specialKey.text()).toBe('enter');
163+
});
164+
165+
it('should send alt+j key', async () => {
166+
const specialKey = await harness.specaialKey();
167+
await harness.sendAltJ();
168+
expect(await specialKey.text()).toBe('alt-j');
169+
});
158170
});
159171

160172
describe('TestElement', () => {

src/cdk-experimental/testing/tests/test-main-component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ <h1 style="height: 50px">Main Component</h1>
88
<div id="asyncCounter">{{asyncCounter}}</div>
99
</div>
1010
<div class="inputs">
11-
<input [(ngModel)]="input" id="input" aria-label="input">
11+
<input [(ngModel)]="input" id="input" aria-label="input" (keydown)="onKeyDown($event)">
12+
<span class="special-key">{{specialKey}}</span>
1213
<div id="value">Input: {{input}}</div>
1314
<textarea id="memo" aria-label="memo">{{memo}}</textarea>
1415
</div>

src/cdk-experimental/testing/tests/test-main-component.ts

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

9+
import {ENTER} from '@angular/cdk/keycodes';
910
import {
1011
ChangeDetectionStrategy,
1112
ChangeDetectorRef,
@@ -35,6 +36,7 @@ export class TestMainComponent {
3536
testTools: string[];
3637
testMethods: string[];
3738
_isHovering: boolean;
39+
specialKey = '';
3840

3941
onMouseOver() {
4042
this._isHovering = true;
@@ -64,4 +66,13 @@ export class TestMainComponent {
6466
this._cdr.markForCheck();
6567
}, 500);
6668
}
69+
70+
onKeyDown(event: KeyboardEvent) {
71+
if (event.keyCode === ENTER && event.key === 'Enter') {
72+
this.specialKey = 'enter';
73+
}
74+
if (event.key === 'j' && event.altKey) {
75+
this.specialKey = 'alt-j';
76+
}
77+
}
6778
}

0 commit comments

Comments
 (0)