Skip to content

Commit 9be3c46

Browse files
devversioncrisbeto
authored andcommitted
fix(cdk/testing): TestElement sendKeys method should throw if no keys have been specified (#18271)
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). (cherry picked from commit caf88cc)
1 parent 005ec32 commit 9be3c46

File tree

8 files changed

+76
-10
lines changed

8 files changed

+76
-10
lines changed

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

+8-1
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,
@@ -161,7 +162,7 @@ export class ProtractorElement implements TestElement {
161162
const first = modifiersAndKeys[0];
162163
let modifiers: ModifierKeys;
163164
let rest: (string | TestKey)[];
164-
if (typeof first !== 'string' && typeof first !== 'number') {
165+
if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
165166
modifiers = first;
166167
rest = modifiersAndKeys.slice(1);
167168
} else {
@@ -177,6 +178,12 @@ export class ProtractorElement implements TestElement {
177178
// so avoid it if no modifier keys are required.
178179
.map(k => (modifierKeys.length > 0 ? Key.chord(...modifierKeys, k) : k));
179180

181+
// Throw an error if no keys have been specified. Calling this function with no
182+
// keys should not result in a focus event being dispatched unexpectedly.
183+
if (keys.length === 0) {
184+
throw getNoKeysSpecifiedError();
185+
}
186+
180187
return this.element.sendKeys(...keys);
181188
}
182189

src/cdk/testing/public-api.ts

+1
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

+8-1
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,
@@ -111,7 +112,7 @@ export class SeleniumWebDriverElement implements TestElement {
111112
const first = modifiersAndKeys[0];
112113
let modifiers: ModifierKeys;
113114
let rest: (string | TestKey)[];
114-
if (typeof first !== 'string' && typeof first !== 'number') {
115+
if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
115116
modifiers = first;
116117
rest = modifiersAndKeys.slice(1);
117118
} else {
@@ -127,6 +128,12 @@ export class SeleniumWebDriverElement implements TestElement {
127128
// so avoid it if no modifier keys are required.
128129
.map(k => (modifierKeys.length > 0 ? webdriver.Key.chord(...modifierKeys, k) : k));
129130

131+
// Throw an error if no keys have been specified. Calling this function with no
132+
// keys should not result in a focus event being dispatched unexpectedly.
133+
if (keys.length === 0) {
134+
throw getNoKeysSpecifiedError();
135+
}
136+
130137
await this.element().sendKeys(...keys);
131138
await this._stabilize();
132139
}
+15
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

+4-2
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,14 @@ export interface TestElement {
119119
* Sends the given string to the input as a series of key presses. Also fires input events
120120
* and attempts to add the string to the Element's value. Note that some environments cannot
121121
* reproduce native browser behavior for keyboard shortcuts such as Tab, Ctrl + A, etc.
122+
* @throws An error if no keys have been specified.
122123
*/
123124
sendKeys(...keys: (string | TestKey)[]): Promise<void>;
124125

125126
/**
126-
* Sends the given string to the input as a series of key presses. Also fires input events
127-
* and attempts to add the string to the Element's value.
127+
* Sends the given string to the input as a series of key presses. Also fires input
128+
* events and attempts to add the string to the Element's value.
129+
* @throws An error if no keys have been specified.
128130
*/
129131
sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>;
130132

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

+17-6
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';
@@ -32,7 +32,7 @@ export function isTextInput(element: Element): element is HTMLInputElement | HTM
3232
}
3333

3434
/**
35-
* Focuses an input, sets its value and dispatches
35+
* If keys have been specified, focuses an input, sets its value and dispatches
3636
* the `input` event, simulating the user typing.
3737
* @param element Element onto which to set the value.
3838
* @param keys The keys to send to the element.
@@ -44,7 +44,7 @@ export function typeInElement(
4444
): void;
4545

4646
/**
47-
* Focuses an input, sets its value and dispatches
47+
* If keys have been specified, focuses an input, sets its value and dispatches
4848
* the `input` event, simulating the user typing.
4949
* @param element Element onto which to set the value.
5050
* @param modifiers Modifier keys that are held while typing.
@@ -57,11 +57,16 @@ export function typeInElement(
5757
...keys: (string | {keyCode?: number; key?: string})[]
5858
): void;
5959

60-
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
60+
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) {
6161
const first = modifiersAndKeys[0];
6262
let modifiers: ModifierKeys;
6363
let rest: (string | {keyCode?: number; key?: string})[];
64-
if (typeof first !== 'string' && first.keyCode === undefined && first.key === undefined) {
64+
if (
65+
first !== undefined &&
66+
typeof first !== 'string' &&
67+
first.keyCode === undefined &&
68+
first.key === undefined
69+
) {
6570
modifiers = first;
6671
rest = modifiersAndKeys.slice(1);
6772
} else {
@@ -78,13 +83,19 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
7883
)
7984
.reduce((arr, k) => arr.concat(k), []);
8085

86+
// Throw an error if no keys have been specified. Calling this function with no
87+
// keys should not result in a focus event being dispatched unexpectedly.
88+
if (keys.length === 0) {
89+
throw getNoKeysSpecifiedError();
90+
}
91+
8192
// We simulate the user typing in a value by incrementally assigning the value below. The problem
8293
// is that for some input types, the browser won't allow for an invalid value to be set via the
8394
// `value` property which will always be the case when going character-by-character. If we detect
8495
// such an input, we have to set the value all at once or listeners to the `input` event (e.g.
8596
// the `ReactiveFormsModule` uses such an approach) won't receive the correct value.
8697
const enterValueIncrementally =
87-
inputType === 'number' && keys.length > 0
98+
inputType === 'number'
8899
? // The value can be set character by character in number inputs if it doesn't have any decimals.
89100
keys.every(key => key.key !== '.' && key.keyCode !== PERIOD)
90101
: incrementalInputTypes.has(inputType);

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

+20
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
ComponentHarness,
1111
ComponentHarnessConstructor,
12+
getNoKeysSpecifiedError,
1213
HarnessLoader,
1314
HarnessPredicate,
1415
parallel,
@@ -345,6 +346,18 @@ export function crossEnvironmentSpecs(
345346
harness = await getMainComponentHarnessFromEnvironment();
346347
});
347348

349+
async function expectAsyncError(fn: () => Promise<void>, expected: Error) {
350+
let error: unknown | null = null;
351+
try {
352+
await fn();
353+
} catch (e: unknown) {
354+
error = e;
355+
}
356+
expect(error).not.toBe(null);
357+
expect(error instanceof Error).toBe(true);
358+
expect((error as Error).message).toBe(expected.message);
359+
}
360+
348361
it('should be able to clear', async () => {
349362
const input = await harness.input();
350363
await input.sendKeys('Yi');
@@ -354,6 +367,13 @@ export function crossEnvironmentSpecs(
354367
expect(await input.getProperty<string>('value')).toBe('');
355368
});
356369

370+
it('sendKeys method should throw if no keys have been specified', async () => {
371+
const input = await harness.input();
372+
await expectAsyncError(() => input.sendKeys(), getNoKeysSpecifiedError());
373+
await expectAsyncError(() => input.sendKeys(''), getNoKeysSpecifiedError());
374+
await expectAsyncError(() => input.sendKeys('', ''), getNoKeysSpecifiedError());
375+
});
376+
357377
it('should be able to click', async () => {
358378
const counter = await harness.counter();
359379
expect(await counter.text()).toBe('0');

tools/public_api_guard/cdk/testing.md

+3
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)