Skip to content

Commit 60b4843

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 b761dbc commit 60b4843

File tree

8 files changed

+73
-12
lines changed

8 files changed

+73
-12
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,
@@ -157,7 +158,7 @@ export class ProtractorElement implements TestElement {
157158
const first = modifiersAndKeys[0];
158159
let modifiers: ModifierKeys;
159160
let rest: (string | TestKey)[];
160-
if (typeof first !== 'string' && typeof first !== 'number') {
161+
if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
161162
modifiers = first;
162163
rest = modifiersAndKeys.slice(1);
163164
} else {
@@ -172,6 +173,12 @@ export class ProtractorElement implements TestElement {
172173
// so avoid it if no modifier keys are required.
173174
.map(k => modifierKeys.length > 0 ? Key.chord(...modifierKeys, k) : k);
174175

176+
// Throw an error if no keys have been specified. Calling this function with no
177+
// keys should not result in a focus event being dispatched unexpectedly.
178+
if (keys.length === 0) {
179+
throw getNoKeysSpecifiedError();
180+
}
181+
175182
return this.element.sendKeys(...keys);
176183
}
177184

src/cdk/testing/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
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';
1415
export * from './change-detection';

src/cdk/testing/selenium-webdriver/selenium-web-driver-element.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
_getTextWithExcludedElements,
1111
ElementDimensions,
1212
EventData,
13+
getNoKeysSpecifiedError,
1314
ModifierKeys,
1415
TestElement,
1516
TestKey,
@@ -108,7 +109,7 @@ export class SeleniumWebDriverElement implements TestElement {
108109
const first = modifiersAndKeys[0];
109110
let modifiers: ModifierKeys;
110111
let rest: (string | TestKey)[];
111-
if (typeof first !== 'string' && typeof first !== 'number') {
112+
if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
112113
modifiers = first;
113114
rest = modifiersAndKeys.slice(1);
114115
} else {
@@ -123,6 +124,12 @@ export class SeleniumWebDriverElement implements TestElement {
123124
// so avoid it if no modifier keys are required.
124125
.map(k => modifierKeys.length > 0 ? webdriver.Key.chord(...modifierKeys, k) : k);
125126

127+
// Throw an error if no keys have been specified. Calling this function with no
128+
// keys should not result in a focus event being dispatched unexpectedly.
129+
if (keys.length === 0) {
130+
throw getNoKeysSpecifiedError();
131+
}
132+
126133
await this.element().sendKeys(...keys);
127134
await this._stabilize();
128135
}
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
@@ -110,14 +110,16 @@ export interface TestElement {
110110
mouseAway(): Promise<void>;
111111

112112
/**
113-
* Sends the given string to the input as a series of key presses. Also fires input events
114-
* and attempts to add the string to the Element's value.
113+
* Sends the given string to the input as a series of key presses. Also fires input
114+
* events and attempts to add the string to the Element's value.
115+
* @throws An error if no keys have been specified.
115116
*/
116117
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
117118

118119
/**
119-
* Sends the given string to the input as a series of key presses. Also fires input events
120-
* and attempts to add the string to the Element's value.
120+
* Sends the given string to the input as a series of key presses. Also fires input
121+
* events and attempts to add the string to the Element's value.
122+
* @throws An error if no keys have been specified.
121123
*/
122124
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
123125

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

Lines changed: 13 additions & 6 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 {PERIOD} from '@angular/cdk/keycodes';
1111
import {dispatchFakeEvent, dispatchKeyboardEvent} from './dispatch-events';
1212
import {triggerFocus} from './element-focus';
@@ -25,7 +25,7 @@ export function isTextInput(element: Element): element is HTMLInputElement | HTM
2525
}
2626

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

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

48-
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
48+
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) {
4949
const first = modifiersAndKeys[0];
5050
let modifiers: ModifierKeys;
5151
let rest: (string | {keyCode?: number, key?: string})[];
52-
if (typeof first !== 'string' && first.keyCode === undefined && first.key === undefined) {
52+
if (first !== undefined && typeof first !== 'string' && first.keyCode === undefined &&
53+
first.key === undefined) {
5354
modifiers = first;
5455
rest = modifiersAndKeys.slice(1);
5556
} else {
@@ -63,12 +64,18 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
6364
k.split('').map(c => ({keyCode: c.toUpperCase().charCodeAt(0), key: c})) : [k])
6465
.reduce((arr, k) => arr.concat(k), []);
6566

67+
// Throw an error if no keys have been specified. Calling this function with no
68+
// keys should not result in a focus event being dispatched unexpectedly.
69+
if (keys.length === 0) {
70+
throw getNoKeysSpecifiedError();
71+
}
72+
6673
// We simulate the user typing in a value by incrementally assigning the value below. The problem
6774
// is that for some input types, the browser won't allow for an invalid value to be set via the
6875
// `value` property which will always be the case when going character-by-character. If we detect
6976
// such an input, we have to set the value all at once or listeners to the `input` event (e.g.
7077
// the `ReactiveFormsModule` uses such an approach) won't receive the correct value.
71-
const enterValueIncrementally = inputType === 'number' && keys.length > 0 ?
78+
const enterValueIncrementally = inputType === 'number' ?
7279
// The value can be set character by character in number inputs if it doesn't have any decimals.
7380
keys.every(key => key.key !== '.' && key.keyCode !== PERIOD) :
7481
incrementalInputTypes.has(inputType);

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
parallel,
1415
TestElement,
@@ -320,6 +321,17 @@ export function crossEnvironmentSpecs(
320321
harness = await getMainComponentHarnessFromEnvironment();
321322
});
322323

324+
async function expectAsyncError(fn: () => Promise<void>, expected: Error) {
325+
let error: Error|null = null;
326+
try {
327+
await fn();
328+
} catch (e) {
329+
error = e;
330+
}
331+
expect(error).not.toBe(null);
332+
expect(error!.message).toBe(expected.message);
333+
}
334+
323335
it('should be able to clear', async () => {
324336
const input = await harness.input();
325337
await input.sendKeys('Yi');
@@ -329,6 +341,13 @@ export function crossEnvironmentSpecs(
329341
expect(await input.getProperty<string>('value')).toBe('');
330342
});
331343

344+
it('sendKeys method should throw if no keys have been specified', async () => {
345+
const input = await harness.input();
346+
await expectAsyncError(() => input.sendKeys(), getNoKeysSpecifiedError());
347+
await expectAsyncError(() => input.sendKeys(''), getNoKeysSpecifiedError());
348+
await expectAsyncError(() => input.sendKeys('', ''), getNoKeysSpecifiedError());
349+
});
350+
332351
it('should be able to click', async () => {
333352
const counter = await harness.counter();
334353
expect(await counter.text()).toBe('0');

tools/public_api_guard/cdk/testing.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export type EventData = string | number | boolean | undefined | null | EventData
7676
[key: string]: EventData;
7777
};
7878

79+
// @public
80+
export function getNoKeysSpecifiedError(): Error;
81+
7982
// @public
8083
export function _getTextWithExcludedElements(element: Element, excludeSelector: string): string;
8184

0 commit comments

Comments
 (0)