Skip to content

Commit d34d2a2

Browse files
authored
fix(cdk/testing): add code to keyboard events (#30188)
When the `UnitTestElement` dispatches keyboard event sequences, it sends out fake events which didn't have the `code` property. These changes add mappings for common keys to their codes. Fixes #27034.
1 parent 454d9f9 commit d34d2a2

File tree

7 files changed

+98
-47
lines changed

7 files changed

+98
-47
lines changed

src/cdk/testing/testbed/fake-events/dispatch-events.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ export function dispatchKeyboardEvent(
4343
keyCode?: number,
4444
key?: string,
4545
modifiers?: ModifierKeys,
46+
code?: string,
4647
): KeyboardEvent {
47-
return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, modifiers));
48+
return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, modifiers, code));
4849
}
4950

5051
/**

src/cdk/testing/testbed/fake-events/event-objects.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,20 @@ export function createKeyboardEvent(
133133
keyCode: number = 0,
134134
key: string = '',
135135
modifiers: ModifierKeys = {},
136+
code: string = '',
136137
) {
137138
return new KeyboardEvent(type, {
138139
bubbles: true,
139140
cancelable: true,
140141
composed: true, // Required for shadow DOM events.
141142
view: window,
142-
keyCode: keyCode,
143-
key: key,
143+
keyCode,
144+
key,
144145
shiftKey: modifiers.shift,
145146
metaKey: modifiers.meta,
146147
altKey: modifiers.alt,
147148
ctrlKey: modifiers.control,
149+
code,
148150
});
149151
}
150152

src/cdk/testing/testbed/fake-events/type-in-element.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,50 @@ const incrementalInputTypes = new Set([
2222
'url',
2323
]);
2424

25+
/**
26+
* Manual mapping of some common characters to their `code` in a keyboard event. Non-exhaustive, see
27+
* the tables on MDN for more info: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
28+
*/
29+
const charsToCodes: Record<string, string> = {
30+
' ': 'Space',
31+
'.': 'Period',
32+
',': 'Comma',
33+
'`': 'Backquote',
34+
'-': 'Minus',
35+
'=': 'Equal',
36+
'[': 'BracketLeft',
37+
']': 'BracketRight',
38+
'\\': 'Backslash',
39+
'/': 'Slash',
40+
"'": 'Quote',
41+
'"': 'Quote',
42+
';': 'Semicolon',
43+
};
44+
45+
/**
46+
* Determines the `KeyboardEvent.key` from a character. See #27034 and
47+
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
48+
*/
49+
function getKeyboardEventCode(char: string): string {
50+
if (char.length !== 1) {
51+
return '';
52+
}
53+
54+
const charCode = char.charCodeAt(0);
55+
56+
// Key is a letter between a and z, uppercase or lowercase.
57+
if ((charCode >= 97 && charCode <= 122) || (charCode >= 65 && charCode <= 90)) {
58+
return `Key${char.toUpperCase()}`;
59+
}
60+
61+
// Digits from 0 to 9.
62+
if (48 <= charCode && charCode <= 57) {
63+
return `Digit${char}`;
64+
}
65+
66+
return charsToCodes[char] ?? '';
67+
}
68+
2569
/**
2670
* Checks whether the given Element is a text input element.
2771
* @docs-private
@@ -60,7 +104,7 @@ export function typeInElement(
60104
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) {
61105
const first = modifiersAndKeys[0];
62106
let modifiers: ModifierKeys;
63-
let rest: (string | {keyCode?: number; key?: string})[];
107+
let rest: (string | {keyCode?: number; key?: string; code?: string})[];
64108
if (
65109
first !== undefined &&
66110
typeof first !== 'string' &&
@@ -75,10 +119,14 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[])
75119
}
76120
const isInput = isTextInput(element);
77121
const inputType = element.getAttribute('type') || 'text';
78-
const keys: {keyCode?: number; key?: string}[] = rest
122+
const keys: {keyCode?: number; key?: string; code?: string}[] = rest
79123
.map(k =>
80124
typeof k === 'string'
81-
? k.split('').map(c => ({keyCode: c.toUpperCase().charCodeAt(0), key: c}))
125+
? k.split('').map(c => ({
126+
keyCode: c.toUpperCase().charCodeAt(0),
127+
key: c,
128+
code: getKeyboardEventCode(c),
129+
}))
82130
: [k],
83131
)
84132
.reduce((arr, k) => arr.concat(k), []);
@@ -109,15 +157,15 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[])
109157
}
110158

111159
for (const key of keys) {
112-
dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers);
113-
dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers);
160+
dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers, key.code);
161+
dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers, key.code);
114162
if (isInput && key.key && key.key.length === 1) {
115163
if (enterValueIncrementally) {
116164
(element as HTMLInputElement | HTMLTextAreaElement).value += key.key;
117165
dispatchFakeEvent(element, 'input');
118166
}
119167
}
120-
dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers);
168+
dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers, key.code);
121169
}
122170

123171
// Since we weren't dispatching `input` events while sending the keys, we have to do it now.

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

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,37 @@ import {
3131

3232
/** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */
3333
const keyMap = {
34-
[TestKey.BACKSPACE]: {keyCode: keyCodes.BACKSPACE, key: 'Backspace'},
35-
[TestKey.TAB]: {keyCode: keyCodes.TAB, key: 'Tab'},
36-
[TestKey.ENTER]: {keyCode: keyCodes.ENTER, key: 'Enter'},
37-
[TestKey.SHIFT]: {keyCode: keyCodes.SHIFT, key: 'Shift'},
38-
[TestKey.CONTROL]: {keyCode: keyCodes.CONTROL, key: 'Control'},
39-
[TestKey.ALT]: {keyCode: keyCodes.ALT, key: 'Alt'},
40-
[TestKey.ESCAPE]: {keyCode: keyCodes.ESCAPE, key: 'Escape'},
41-
[TestKey.PAGE_UP]: {keyCode: keyCodes.PAGE_UP, key: 'PageUp'},
42-
[TestKey.PAGE_DOWN]: {keyCode: keyCodes.PAGE_DOWN, key: 'PageDown'},
43-
[TestKey.END]: {keyCode: keyCodes.END, key: 'End'},
44-
[TestKey.HOME]: {keyCode: keyCodes.HOME, key: 'Home'},
45-
[TestKey.LEFT_ARROW]: {keyCode: keyCodes.LEFT_ARROW, key: 'ArrowLeft'},
46-
[TestKey.UP_ARROW]: {keyCode: keyCodes.UP_ARROW, key: 'ArrowUp'},
47-
[TestKey.RIGHT_ARROW]: {keyCode: keyCodes.RIGHT_ARROW, key: 'ArrowRight'},
48-
[TestKey.DOWN_ARROW]: {keyCode: keyCodes.DOWN_ARROW, key: 'ArrowDown'},
49-
[TestKey.INSERT]: {keyCode: keyCodes.INSERT, key: 'Insert'},
50-
[TestKey.DELETE]: {keyCode: keyCodes.DELETE, key: 'Delete'},
51-
[TestKey.F1]: {keyCode: keyCodes.F1, key: 'F1'},
52-
[TestKey.F2]: {keyCode: keyCodes.F2, key: 'F2'},
53-
[TestKey.F3]: {keyCode: keyCodes.F3, key: 'F3'},
54-
[TestKey.F4]: {keyCode: keyCodes.F4, key: 'F4'},
55-
[TestKey.F5]: {keyCode: keyCodes.F5, key: 'F5'},
56-
[TestKey.F6]: {keyCode: keyCodes.F6, key: 'F6'},
57-
[TestKey.F7]: {keyCode: keyCodes.F7, key: 'F7'},
58-
[TestKey.F8]: {keyCode: keyCodes.F8, key: 'F8'},
59-
[TestKey.F9]: {keyCode: keyCodes.F9, key: 'F9'},
60-
[TestKey.F10]: {keyCode: keyCodes.F10, key: 'F10'},
61-
[TestKey.F11]: {keyCode: keyCodes.F11, key: 'F11'},
62-
[TestKey.F12]: {keyCode: keyCodes.F12, key: 'F12'},
63-
[TestKey.META]: {keyCode: keyCodes.META, key: 'Meta'},
64-
[TestKey.COMMA]: {keyCode: keyCodes.COMMA, key: ','},
34+
[TestKey.BACKSPACE]: {keyCode: keyCodes.BACKSPACE, key: 'Backspace', code: 'Backspace'},
35+
[TestKey.TAB]: {keyCode: keyCodes.TAB, key: 'Tab', code: 'Tab'},
36+
[TestKey.ENTER]: {keyCode: keyCodes.ENTER, key: 'Enter', code: 'Enter'},
37+
[TestKey.SHIFT]: {keyCode: keyCodes.SHIFT, key: 'Shift', code: 'ShiftLeft'},
38+
[TestKey.CONTROL]: {keyCode: keyCodes.CONTROL, key: 'Control', code: 'ControlLeft'},
39+
[TestKey.ALT]: {keyCode: keyCodes.ALT, key: 'Alt', code: 'AltLeft'},
40+
[TestKey.ESCAPE]: {keyCode: keyCodes.ESCAPE, key: 'Escape', code: 'Escape'},
41+
[TestKey.PAGE_UP]: {keyCode: keyCodes.PAGE_UP, key: 'PageUp', code: 'PageUp'},
42+
[TestKey.PAGE_DOWN]: {keyCode: keyCodes.PAGE_DOWN, key: 'PageDown', code: 'PageDown'},
43+
[TestKey.END]: {keyCode: keyCodes.END, key: 'End', code: 'End'},
44+
[TestKey.HOME]: {keyCode: keyCodes.HOME, key: 'Home', code: 'Home'},
45+
[TestKey.LEFT_ARROW]: {keyCode: keyCodes.LEFT_ARROW, key: 'ArrowLeft', code: 'ArrowLeft'},
46+
[TestKey.UP_ARROW]: {keyCode: keyCodes.UP_ARROW, key: 'ArrowUp', code: 'ArrowUp'},
47+
[TestKey.RIGHT_ARROW]: {keyCode: keyCodes.RIGHT_ARROW, key: 'ArrowRight', code: 'ArrowRight'},
48+
[TestKey.DOWN_ARROW]: {keyCode: keyCodes.DOWN_ARROW, key: 'ArrowDown', code: 'ArrowDown'},
49+
[TestKey.INSERT]: {keyCode: keyCodes.INSERT, key: 'Insert', code: 'Insert'},
50+
[TestKey.DELETE]: {keyCode: keyCodes.DELETE, key: 'Delete', code: 'Delete'},
51+
[TestKey.F1]: {keyCode: keyCodes.F1, key: 'F1', code: 'F1'},
52+
[TestKey.F2]: {keyCode: keyCodes.F2, key: 'F2', code: 'F2'},
53+
[TestKey.F3]: {keyCode: keyCodes.F3, key: 'F3', code: 'F3'},
54+
[TestKey.F4]: {keyCode: keyCodes.F4, key: 'F4', code: 'F4'},
55+
[TestKey.F5]: {keyCode: keyCodes.F5, key: 'F5', code: 'F5'},
56+
[TestKey.F6]: {keyCode: keyCodes.F6, key: 'F6', code: 'F6'},
57+
[TestKey.F7]: {keyCode: keyCodes.F7, key: 'F7', code: 'F7'},
58+
[TestKey.F8]: {keyCode: keyCodes.F8, key: 'F8', code: 'F8'},
59+
[TestKey.F9]: {keyCode: keyCodes.F9, key: 'F9', code: 'F9'},
60+
[TestKey.F10]: {keyCode: keyCodes.F10, key: 'F10', code: 'F10'},
61+
[TestKey.F11]: {keyCode: keyCodes.F11, key: 'F11', code: 'F11'},
62+
[TestKey.F12]: {keyCode: keyCodes.F12, key: 'F12', code: 'F12'},
63+
[TestKey.META]: {keyCode: keyCodes.META, key: 'Meta', code: 'MetaLeft'},
64+
[TestKey.COMMA]: {keyCode: keyCodes.COMMA, key: ',', code: 'Comma'},
6565
};
6666

6767
/** A `TestElement` implementation for unit tests. */

src/cdk/testing/tests/cross-environment.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,15 @@ export function crossEnvironmentSpecs(
204204
});
205205

206206
it('should send enter key', async () => {
207-
const specialKey = await harness.specaialKey();
207+
const specialKey = await harness.specialKey();
208208
await harness.sendEnter();
209-
expect(await specialKey.text()).toBe('enter');
209+
expect(await specialKey.text()).toBe('Enter|Enter');
210210
});
211211

212212
it('should send alt+j key', async () => {
213-
const specialKey = await harness.specaialKey();
213+
const specialKey = await harness.specialKey();
214214
await harness.sendAltJ();
215-
expect(await specialKey.text()).toBe('alt-j');
215+
expect(await specialKey.text()).toBe('alt-j|KeyJ');
216216
});
217217

218218
it('should load required harness with ancestor selector restriction', async () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class MainComponentHarness extends ComponentHarness {
6868
SubComponentHarness.with({title: 'List of test tools', itemCount: 4}),
6969
);
7070
readonly lastList = this.locatorFor(SubComponentHarness.with({selector: ':last-child'}));
71-
readonly specaialKey = this.locatorFor('.special-key');
71+
readonly specialKey = this.locatorFor('.special-key');
7272

7373
readonly requiredAncestorRestrictedSubcomponent = this.locatorFor(
7474
SubComponentHarness.with({ancestor: '.other'}),

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ export class TestMainComponent implements OnDestroy {
9393

9494
onKeyDown(event: KeyboardEvent) {
9595
if (event.keyCode === ENTER && event.key === 'Enter') {
96-
this.specialKey = 'enter';
96+
this.specialKey = `Enter|${event.code}`;
9797
}
9898
if (event.key === 'j' && event.altKey) {
99-
this.specialKey = 'alt-j';
99+
this.specialKey = `alt-j|${event.code}`;
100100
}
101101
}
102102

0 commit comments

Comments
 (0)