Skip to content

Commit

Permalink
fix(cdk/testing): add code to keyboard events (#30188)
Browse files Browse the repository at this point in the history
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 d34d2a2)
  • Loading branch information
crisbeto committed Dec 16, 2024
1 parent 1456074 commit c92def4
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 47 deletions.
3 changes: 2 additions & 1 deletion src/cdk/testing/testbed/fake-events/dispatch-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/cdk/testing/testbed/fake-events/event-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

Expand Down
60 changes: 54 additions & 6 deletions src/cdk/testing/testbed/fake-events/type-in-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
' ': '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
Expand Down Expand Up @@ -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' &&
Expand All @@ -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), []);
Expand Down Expand Up @@ -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.
Expand Down
62 changes: 31 additions & 31 deletions src/cdk/testing/testbed/unit-test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
8 changes: 4 additions & 4 deletions src/cdk/testing/tests/cross-environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion src/cdk/testing/tests/harnesses/main-component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'}),
Expand Down
4 changes: 2 additions & 2 deletions src/cdk/testing/tests/test-main-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}
}

Expand Down

0 comments on commit c92def4

Please sign in to comment.