From c92def439bef7287918536e0f53cd9061bc86f44 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 16 Dec 2024 17:58:14 +0200 Subject: [PATCH] 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. (cherry picked from commit d34d2a2ca33834a18ca169040f2d81e353c355c5) --- .../testbed/fake-events/dispatch-events.ts | 3 +- .../testbed/fake-events/event-objects.ts | 6 +- .../testbed/fake-events/type-in-element.ts | 60 ++++++++++++++++-- src/cdk/testing/testbed/unit-test-element.ts | 62 +++++++++---------- .../testing/tests/cross-environment.spec.ts | 8 +-- .../tests/harnesses/main-component-harness.ts | 2 +- src/cdk/testing/tests/test-main-component.ts | 4 +- 7 files changed, 98 insertions(+), 47 deletions(-) diff --git a/src/cdk/testing/testbed/fake-events/dispatch-events.ts b/src/cdk/testing/testbed/fake-events/dispatch-events.ts index 3f0ca70569a4..b2cd3e9ae0df 100644 --- a/src/cdk/testing/testbed/fake-events/dispatch-events.ts +++ b/src/cdk/testing/testbed/fake-events/dispatch-events.ts @@ -43,8 +43,9 @@ export function dispatchKeyboardEvent( keyCode?: number, key?: string, modifiers?: ModifierKeys, + code?: string, ): KeyboardEvent { - return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, modifiers)); + return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, modifiers, code)); } /** diff --git a/src/cdk/testing/testbed/fake-events/event-objects.ts b/src/cdk/testing/testbed/fake-events/event-objects.ts index 6826f762affa..4329926a3e57 100644 --- a/src/cdk/testing/testbed/fake-events/event-objects.ts +++ b/src/cdk/testing/testbed/fake-events/event-objects.ts @@ -133,18 +133,20 @@ export function createKeyboardEvent( keyCode: number = 0, key: string = '', modifiers: ModifierKeys = {}, + code: string = '', ) { return new KeyboardEvent(type, { bubbles: true, cancelable: true, composed: true, // Required for shadow DOM events. view: window, - keyCode: keyCode, - key: key, + keyCode, + key, shiftKey: modifiers.shift, metaKey: modifiers.meta, altKey: modifiers.alt, ctrlKey: modifiers.control, + code, }); } diff --git a/src/cdk/testing/testbed/fake-events/type-in-element.ts b/src/cdk/testing/testbed/fake-events/type-in-element.ts index 3efc932d48b0..480c269c4c63 100644 --- a/src/cdk/testing/testbed/fake-events/type-in-element.ts +++ b/src/cdk/testing/testbed/fake-events/type-in-element.ts @@ -22,6 +22,50 @@ const incrementalInputTypes = new Set([ 'url', ]); +/** + * Manual mapping of some common characters to their `code` in a keyboard event. Non-exhaustive, see + * the tables on MDN for more info: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + */ +const charsToCodes: Record = { + ' ': 'Space', + '.': 'Period', + ',': 'Comma', + '`': 'Backquote', + '-': 'Minus', + '=': 'Equal', + '[': 'BracketLeft', + ']': 'BracketRight', + '\\': 'Backslash', + '/': 'Slash', + "'": 'Quote', + '"': 'Quote', + ';': 'Semicolon', +}; + +/** + * Determines the `KeyboardEvent.key` from a character. See #27034 and + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code + */ +function getKeyboardEventCode(char: string): string { + if (char.length !== 1) { + return ''; + } + + const charCode = char.charCodeAt(0); + + // Key is a letter between a and z, uppercase or lowercase. + if ((charCode >= 97 && charCode <= 122) || (charCode >= 65 && charCode <= 90)) { + return `Key${char.toUpperCase()}`; + } + + // Digits from 0 to 9. + if (48 <= charCode && charCode <= 57) { + return `Digit${char}`; + } + + return charsToCodes[char] ?? ''; +} + /** * Checks whether the given Element is a text input element. * @docs-private @@ -60,7 +104,7 @@ export function typeInElement( export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) { const first = modifiersAndKeys[0]; let modifiers: ModifierKeys; - let rest: (string | {keyCode?: number; key?: string})[]; + let rest: (string | {keyCode?: number; key?: string; code?: string})[]; if ( first !== undefined && typeof first !== 'string' && @@ -75,10 +119,14 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) } const isInput = isTextInput(element); const inputType = element.getAttribute('type') || 'text'; - const keys: {keyCode?: number; key?: string}[] = rest + const keys: {keyCode?: number; key?: string; code?: string}[] = rest .map(k => typeof k === 'string' - ? k.split('').map(c => ({keyCode: c.toUpperCase().charCodeAt(0), key: c})) + ? k.split('').map(c => ({ + keyCode: c.toUpperCase().charCodeAt(0), + key: c, + code: getKeyboardEventCode(c), + })) : [k], ) .reduce((arr, k) => arr.concat(k), []); @@ -109,15 +157,15 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) } for (const key of keys) { - dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers); - dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers); + dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers, key.code); + dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers, key.code); if (isInput && key.key && key.key.length === 1) { if (enterValueIncrementally) { (element as HTMLInputElement | HTMLTextAreaElement).value += key.key; dispatchFakeEvent(element, 'input'); } } - dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers); + dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers, key.code); } // Since we weren't dispatching `input` events while sending the keys, we have to do it now. diff --git a/src/cdk/testing/testbed/unit-test-element.ts b/src/cdk/testing/testbed/unit-test-element.ts index c2a23104bd86..0860a12e234c 100644 --- a/src/cdk/testing/testbed/unit-test-element.ts +++ b/src/cdk/testing/testbed/unit-test-element.ts @@ -31,37 +31,37 @@ import { /** Maps `TestKey` constants to the `keyCode` and `key` values used by native browser events. */ const keyMap = { - [TestKey.BACKSPACE]: {keyCode: keyCodes.BACKSPACE, key: 'Backspace'}, - [TestKey.TAB]: {keyCode: keyCodes.TAB, key: 'Tab'}, - [TestKey.ENTER]: {keyCode: keyCodes.ENTER, key: 'Enter'}, - [TestKey.SHIFT]: {keyCode: keyCodes.SHIFT, key: 'Shift'}, - [TestKey.CONTROL]: {keyCode: keyCodes.CONTROL, key: 'Control'}, - [TestKey.ALT]: {keyCode: keyCodes.ALT, key: 'Alt'}, - [TestKey.ESCAPE]: {keyCode: keyCodes.ESCAPE, key: 'Escape'}, - [TestKey.PAGE_UP]: {keyCode: keyCodes.PAGE_UP, key: 'PageUp'}, - [TestKey.PAGE_DOWN]: {keyCode: keyCodes.PAGE_DOWN, key: 'PageDown'}, - [TestKey.END]: {keyCode: keyCodes.END, key: 'End'}, - [TestKey.HOME]: {keyCode: keyCodes.HOME, key: 'Home'}, - [TestKey.LEFT_ARROW]: {keyCode: keyCodes.LEFT_ARROW, key: 'ArrowLeft'}, - [TestKey.UP_ARROW]: {keyCode: keyCodes.UP_ARROW, key: 'ArrowUp'}, - [TestKey.RIGHT_ARROW]: {keyCode: keyCodes.RIGHT_ARROW, key: 'ArrowRight'}, - [TestKey.DOWN_ARROW]: {keyCode: keyCodes.DOWN_ARROW, key: 'ArrowDown'}, - [TestKey.INSERT]: {keyCode: keyCodes.INSERT, key: 'Insert'}, - [TestKey.DELETE]: {keyCode: keyCodes.DELETE, key: 'Delete'}, - [TestKey.F1]: {keyCode: keyCodes.F1, key: 'F1'}, - [TestKey.F2]: {keyCode: keyCodes.F2, key: 'F2'}, - [TestKey.F3]: {keyCode: keyCodes.F3, key: 'F3'}, - [TestKey.F4]: {keyCode: keyCodes.F4, key: 'F4'}, - [TestKey.F5]: {keyCode: keyCodes.F5, key: 'F5'}, - [TestKey.F6]: {keyCode: keyCodes.F6, key: 'F6'}, - [TestKey.F7]: {keyCode: keyCodes.F7, key: 'F7'}, - [TestKey.F8]: {keyCode: keyCodes.F8, key: 'F8'}, - [TestKey.F9]: {keyCode: keyCodes.F9, key: 'F9'}, - [TestKey.F10]: {keyCode: keyCodes.F10, key: 'F10'}, - [TestKey.F11]: {keyCode: keyCodes.F11, key: 'F11'}, - [TestKey.F12]: {keyCode: keyCodes.F12, key: 'F12'}, - [TestKey.META]: {keyCode: keyCodes.META, key: 'Meta'}, - [TestKey.COMMA]: {keyCode: keyCodes.COMMA, key: ','}, + [TestKey.BACKSPACE]: {keyCode: keyCodes.BACKSPACE, key: 'Backspace', code: 'Backspace'}, + [TestKey.TAB]: {keyCode: keyCodes.TAB, key: 'Tab', code: 'Tab'}, + [TestKey.ENTER]: {keyCode: keyCodes.ENTER, key: 'Enter', code: 'Enter'}, + [TestKey.SHIFT]: {keyCode: keyCodes.SHIFT, key: 'Shift', code: 'ShiftLeft'}, + [TestKey.CONTROL]: {keyCode: keyCodes.CONTROL, key: 'Control', code: 'ControlLeft'}, + [TestKey.ALT]: {keyCode: keyCodes.ALT, key: 'Alt', code: 'AltLeft'}, + [TestKey.ESCAPE]: {keyCode: keyCodes.ESCAPE, key: 'Escape', code: 'Escape'}, + [TestKey.PAGE_UP]: {keyCode: keyCodes.PAGE_UP, key: 'PageUp', code: 'PageUp'}, + [TestKey.PAGE_DOWN]: {keyCode: keyCodes.PAGE_DOWN, key: 'PageDown', code: 'PageDown'}, + [TestKey.END]: {keyCode: keyCodes.END, key: 'End', code: 'End'}, + [TestKey.HOME]: {keyCode: keyCodes.HOME, key: 'Home', code: 'Home'}, + [TestKey.LEFT_ARROW]: {keyCode: keyCodes.LEFT_ARROW, key: 'ArrowLeft', code: 'ArrowLeft'}, + [TestKey.UP_ARROW]: {keyCode: keyCodes.UP_ARROW, key: 'ArrowUp', code: 'ArrowUp'}, + [TestKey.RIGHT_ARROW]: {keyCode: keyCodes.RIGHT_ARROW, key: 'ArrowRight', code: 'ArrowRight'}, + [TestKey.DOWN_ARROW]: {keyCode: keyCodes.DOWN_ARROW, key: 'ArrowDown', code: 'ArrowDown'}, + [TestKey.INSERT]: {keyCode: keyCodes.INSERT, key: 'Insert', code: 'Insert'}, + [TestKey.DELETE]: {keyCode: keyCodes.DELETE, key: 'Delete', code: 'Delete'}, + [TestKey.F1]: {keyCode: keyCodes.F1, key: 'F1', code: 'F1'}, + [TestKey.F2]: {keyCode: keyCodes.F2, key: 'F2', code: 'F2'}, + [TestKey.F3]: {keyCode: keyCodes.F3, key: 'F3', code: 'F3'}, + [TestKey.F4]: {keyCode: keyCodes.F4, key: 'F4', code: 'F4'}, + [TestKey.F5]: {keyCode: keyCodes.F5, key: 'F5', code: 'F5'}, + [TestKey.F6]: {keyCode: keyCodes.F6, key: 'F6', code: 'F6'}, + [TestKey.F7]: {keyCode: keyCodes.F7, key: 'F7', code: 'F7'}, + [TestKey.F8]: {keyCode: keyCodes.F8, key: 'F8', code: 'F8'}, + [TestKey.F9]: {keyCode: keyCodes.F9, key: 'F9', code: 'F9'}, + [TestKey.F10]: {keyCode: keyCodes.F10, key: 'F10', code: 'F10'}, + [TestKey.F11]: {keyCode: keyCodes.F11, key: 'F11', code: 'F11'}, + [TestKey.F12]: {keyCode: keyCodes.F12, key: 'F12', code: 'F12'}, + [TestKey.META]: {keyCode: keyCodes.META, key: 'Meta', code: 'MetaLeft'}, + [TestKey.COMMA]: {keyCode: keyCodes.COMMA, key: ',', code: 'Comma'}, }; /** A `TestElement` implementation for unit tests. */ diff --git a/src/cdk/testing/tests/cross-environment.spec.ts b/src/cdk/testing/tests/cross-environment.spec.ts index 1dc29256ee4d..661732f065b0 100644 --- a/src/cdk/testing/tests/cross-environment.spec.ts +++ b/src/cdk/testing/tests/cross-environment.spec.ts @@ -204,15 +204,15 @@ export function crossEnvironmentSpecs( }); it('should send enter key', async () => { - const specialKey = await harness.specaialKey(); + const specialKey = await harness.specialKey(); await harness.sendEnter(); - expect(await specialKey.text()).toBe('enter'); + expect(await specialKey.text()).toBe('Enter|Enter'); }); it('should send alt+j key', async () => { - const specialKey = await harness.specaialKey(); + const specialKey = await harness.specialKey(); await harness.sendAltJ(); - expect(await specialKey.text()).toBe('alt-j'); + expect(await specialKey.text()).toBe('alt-j|KeyJ'); }); it('should load required harness with ancestor selector restriction', async () => { diff --git a/src/cdk/testing/tests/harnesses/main-component-harness.ts b/src/cdk/testing/tests/harnesses/main-component-harness.ts index 3ec3a364c927..71d516ee5a28 100644 --- a/src/cdk/testing/tests/harnesses/main-component-harness.ts +++ b/src/cdk/testing/tests/harnesses/main-component-harness.ts @@ -68,7 +68,7 @@ export class MainComponentHarness extends ComponentHarness { SubComponentHarness.with({title: 'List of test tools', itemCount: 4}), ); readonly lastList = this.locatorFor(SubComponentHarness.with({selector: ':last-child'})); - readonly specaialKey = this.locatorFor('.special-key'); + readonly specialKey = this.locatorFor('.special-key'); readonly requiredAncestorRestrictedSubcomponent = this.locatorFor( SubComponentHarness.with({ancestor: '.other'}), diff --git a/src/cdk/testing/tests/test-main-component.ts b/src/cdk/testing/tests/test-main-component.ts index 117e34a9c2a7..2881127c673b 100644 --- a/src/cdk/testing/tests/test-main-component.ts +++ b/src/cdk/testing/tests/test-main-component.ts @@ -93,10 +93,10 @@ export class TestMainComponent implements OnDestroy { onKeyDown(event: KeyboardEvent) { if (event.keyCode === ENTER && event.key === 'Enter') { - this.specialKey = 'enter'; + this.specialKey = `Enter|${event.code}`; } if (event.key === 'j' && event.altKey) { - this.specialKey = 'alt-j'; + this.specialKey = `alt-j|${event.code}`; } }