Skip to content

Commit 43b5665

Browse files
committed
fix(cdk/testing): TestElement sendKeys method should throw if no keys have been specified
Previously, calling `sendKeys` without any keys resulted in a runtime exception `Cannot read X of undefined` error being thrown. This is because the first element of the passed arguments to `sendKeys` has been considered always truthy. This assumption is wrong since arbitrary amount of arguments can be passed due to the spread parameter. To ensure consistent and reasonable behavior of this function, we fix the runtime exception mentioned above, but throw if no keys have been determined (not necessarily only if the arguments length is zero).
1 parent f951bf4 commit 43b5665

File tree

7 files changed

+63
-10
lines changed

7 files changed

+63
-10
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
_getTextWithExcludedElements,
1111
ElementDimensions,
12+
getNoKeysSpecifiedError,
1213
ModifierKeys,
1314
TestElement,
1415
TestKey,
@@ -118,7 +119,7 @@ export class ProtractorElement implements TestElement {
118119
const first = modifiersAndKeys[0];
119120
let modifiers: ModifierKeys;
120121
let rest: (string | TestKey)[];
121-
if (typeof first !== 'string' && typeof first !== 'number') {
122+
if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
122123
modifiers = first;
123124
rest = modifiersAndKeys.slice(1);
124125
} else {
@@ -133,6 +134,12 @@ export class ProtractorElement implements TestElement {
133134
// so avoid it if no modifier keys are required.
134135
.map(k => modifierKeys.length > 0 ? Key.chord(...modifierKeys, k) : k);
135136

137+
// Throw an error if no keys have been specified. Calling this function with no
138+
// keys should not result in a focus event being dispatched unexpectedly.
139+
if (keys.length === 0) {
140+
throw getNoKeysSpecifiedError();
141+
}
142+
136143
return this.element.sendKeys(...keys);
137144
}
138145

src/cdk/testing/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
export * from './component-harness';
1010
export * from './harness-environment';
1111
export * from './test-element';
12+
export * from './test-element-errors';
1213
export * from './element-dimensions';
1314
export * from './text-filtering';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* Returns an error which reports that no keys have been specified.
11+
* @docs-private
12+
*/
13+
export function getNoKeysSpecifiedError() {
14+
return Error('No keys have been specified.');
15+
}

src/cdk/testing/test-element.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,16 @@ export interface TestElement {
9090
mouseAway(): Promise<void>;
9191

9292
/**
93-
* Sends the given string to the input as a series of key presses. Also fires input events
94-
* and attempts to add the string to the Element's value.
93+
* Sends the given string to the input as a series of key presses. Also fires input
94+
* events and attempts to add the string to the Element's value.
95+
* @throws An error if no keys have been specified.
9596
*/
9697
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
9798

9899
/**
99-
* Sends the given string to the input as a series of key presses. Also fires input events
100-
* and attempts to add the string to the Element's value.
100+
* Sends the given string to the input as a series of key presses. Also fires input
101+
* events and attempts to add the string to the Element's value.
102+
* @throws An error if no keys have been specified.
101103
*/
102104
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
103105

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

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

9-
import {ModifierKeys} from '@angular/cdk/testing';
9+
import {getNoKeysSpecifiedError, ModifierKeys} from '@angular/cdk/testing';
1010
import {dispatchFakeEvent, dispatchKeyboardEvent} from './dispatch-events';
1111
import {triggerFocus} from './element-focus';
1212

@@ -20,7 +20,7 @@ export function isTextInput(element: Element): element is HTMLInputElement | HTM
2020
}
2121

2222
/**
23-
* Focuses an input, sets its value and dispatches
23+
* If keys have been specified, focuses an input, sets its value and dispatches
2424
* the `input` event, simulating the user typing.
2525
* @param element Element onto which to set the value.
2626
* @param keys The keys to send to the element.
@@ -30,7 +30,7 @@ export function typeInElement(
3030
element: HTMLElement, ...keys: (string | {keyCode?: number, key?: string})[]): void;
3131

3232
/**
33-
* Focuses an input, sets its value and dispatches
33+
* If keys have been specified, focuses an input, sets its value and dispatches
3434
* the `input` event, simulating the user typing.
3535
* @param element Element onto which to set the value.
3636
* @param modifiers Modifier keys that are held while typing.
@@ -40,11 +40,12 @@ export function typeInElement(
4040
export function typeInElement(element: HTMLElement, modifiers: ModifierKeys,
4141
...keys: (string | {keyCode?: number, key?: string})[]): void;
4242

43-
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
43+
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) {
4444
const first = modifiersAndKeys[0];
4545
let modifiers: ModifierKeys;
4646
let rest: (string | {keyCode?: number, key?: string})[];
47-
if (typeof first !== 'string' && first.keyCode === undefined && first.key === undefined) {
47+
if (first !== undefined && typeof first !== 'string' && first.keyCode === undefined &&
48+
first.key === undefined) {
4849
modifiers = first;
4950
rest = modifiersAndKeys.slice(1);
5051
} else {
@@ -56,6 +57,12 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
5657
k.split('').map(c => ({keyCode: c.toUpperCase().charCodeAt(0), key: c})) : [k])
5758
.reduce((arr, k) => arr.concat(k), []);
5859

60+
// Throw an error if no keys have been specified. Calling this function with no
61+
// keys should not result in a focus event being dispatched unexpectedly.
62+
if (keys.length === 0) {
63+
throw getNoKeysSpecifiedError();
64+
}
65+
5966
triggerFocus(element);
6067
for (const key of keys) {
6168
dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers);

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
ComponentHarness,
1111
ComponentHarnessConstructor,
12+
getNoKeysSpecifiedError,
1213
HarnessLoader,
1314
TestElement,
1415
} from '@angular/cdk/testing';
@@ -303,6 +304,17 @@ export function crossEnvironmentSpecs(
303304
harness = await getMainComponentHarnessFromEnvironment();
304305
});
305306

307+
async function expectAsyncError(fn: () => Promise<void>, expected: Error) {
308+
let error: Error|null = null;
309+
try {
310+
await fn();
311+
} catch (e) {
312+
error = e;
313+
}
314+
expect(error).not.toBe(null);
315+
expect(error!.message).toBe(expected.message);
316+
}
317+
306318
it('should be able to clear', async () => {
307319
const input = await harness.input();
308320
await input.sendKeys('Yi');
@@ -312,6 +324,13 @@ export function crossEnvironmentSpecs(
312324
expect(await input.getProperty('value')).toBe('');
313325
});
314326

327+
it('sendKeys method should throw if no keys have been specified', async () => {
328+
const input = await harness.input();
329+
await expectAsyncError(() => input.sendKeys(), getNoKeysSpecifiedError());
330+
await expectAsyncError(() => input.sendKeys(''), getNoKeysSpecifiedError());
331+
await expectAsyncError(() => input.sendKeys('', ''), getNoKeysSpecifiedError());
332+
});
333+
315334
it('should be able to click', async () => {
316335
const counter = await harness.counter();
317336
expect(await counter.text()).toBe('0');

tools/public_api_guard/cdk/testing.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface ElementDimensions {
4242
width: number;
4343
}
4444

45+
export declare function getNoKeysSpecifiedError(): Error;
46+
4547
export declare abstract class HarnessEnvironment<E> implements HarnessLoader, LocatorFactory {
4648
protected rawRootElement: E;
4749
rootElement: TestElement;

0 commit comments

Comments
 (0)