Skip to content

Commit

Permalink
fix(cdk/testing): TestElement sendKeys method should throw if no keys…
Browse files Browse the repository at this point in the history
… 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).
  • Loading branch information
devversion committed Jan 31, 2022
1 parent 2575b00 commit 4ab10e8
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 10 deletions.
9 changes: 8 additions & 1 deletion src/cdk/testing/protractor/protractor-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {
_getTextWithExcludedElements,
ElementDimensions,
getNoKeysSpecifiedError,
ModifierKeys,
TestElement,
TestKey,
Expand Down Expand Up @@ -161,7 +162,7 @@ export class ProtractorElement implements TestElement {
const first = modifiersAndKeys[0];
let modifiers: ModifierKeys;
let rest: (string | TestKey)[];
if (typeof first !== 'string' && typeof first !== 'number') {
if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
modifiers = first;
rest = modifiersAndKeys.slice(1);
} else {
Expand All @@ -177,6 +178,12 @@ export class ProtractorElement implements TestElement {
// so avoid it if no modifier keys are required.
.map(k => (modifierKeys.length > 0 ? Key.chord(...modifierKeys, k) : k));

// Throw an error if no keys have been specified. Calling this function with no
// keys should not result in a focus event being dispatched unexpectedly.
if (keys.length === 0) {
throw getNoKeysSpecifiedError();
}

return this.element.sendKeys(...keys);
}

Expand Down
1 change: 1 addition & 0 deletions src/cdk/testing/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
export * from './component-harness';
export * from './harness-environment';
export * from './test-element';
export * from './test-element-errors';
export * from './element-dimensions';
export * from './text-filtering';
export * from './change-detection';
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
_getTextWithExcludedElements,
ElementDimensions,
EventData,
getNoKeysSpecifiedError,
ModifierKeys,
TestElement,
TestKey,
Expand Down Expand Up @@ -111,7 +112,7 @@ export class SeleniumWebDriverElement implements TestElement {
const first = modifiersAndKeys[0];
let modifiers: ModifierKeys;
let rest: (string | TestKey)[];
if (typeof first !== 'string' && typeof first !== 'number') {
if (first !== undefined && typeof first !== 'string' && typeof first !== 'number') {
modifiers = first;
rest = modifiersAndKeys.slice(1);
} else {
Expand All @@ -127,6 +128,12 @@ export class SeleniumWebDriverElement implements TestElement {
// so avoid it if no modifier keys are required.
.map(k => (modifierKeys.length > 0 ? webdriver.Key.chord(...modifierKeys, k) : k));

// Throw an error if no keys have been specified. Calling this function with no
// keys should not result in a focus event being dispatched unexpectedly.
if (keys.length === 0) {
throw getNoKeysSpecifiedError();
}

await this.element().sendKeys(...keys);
await this._stabilize();
}
Expand Down
15 changes: 15 additions & 0 deletions src/cdk/testing/test-element-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* Returns an error which reports that no keys have been specified.
* @docs-private
*/
export function getNoKeysSpecifiedError() {
return Error('No keys have been specified.');
}
6 changes: 4 additions & 2 deletions src/cdk/testing/test-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,14 @@ export interface TestElement {
* Sends the given string to the input as a series of key presses. Also fires input events
* and attempts to add the string to the Element's value. Note that some environments cannot
* reproduce native browser behavior for keyboard shortcuts such as Tab, Ctrl + A, etc.
* @throws An error if no keys have been specified.
*/
sendKeys(...keys: (string | TestKey)[]): Promise<void>;

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

Expand Down
23 changes: 17 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 @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ModifierKeys} from '@angular/cdk/testing';
import {getNoKeysSpecifiedError, ModifierKeys} from '@angular/cdk/testing';
import {PERIOD} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from './dispatch-events';
import {triggerFocus} from './element-focus';
Expand All @@ -32,7 +32,7 @@ export function isTextInput(element: Element): element is HTMLInputElement | HTM
}

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

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

export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any[]) {
const first = modifiersAndKeys[0];
let modifiers: ModifierKeys;
let rest: (string | {keyCode?: number; key?: string})[];
if (typeof first !== 'string' && first.keyCode === undefined && first.key === undefined) {
if (
first !== undefined &&
typeof first !== 'string' &&
first.keyCode === undefined &&
first.key === undefined
) {
modifiers = first;
rest = modifiersAndKeys.slice(1);
} else {
Expand All @@ -78,13 +83,19 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
)
.reduce((arr, k) => arr.concat(k), []);

// Throw an error if no keys have been specified. Calling this function with no
// keys should not result in a focus event being dispatched unexpectedly.
if (keys.length === 0) {
throw getNoKeysSpecifiedError();
}

// We simulate the user typing in a value by incrementally assigning the value below. The problem
// is that for some input types, the browser won't allow for an invalid value to be set via the
// `value` property which will always be the case when going character-by-character. If we detect
// such an input, we have to set the value all at once or listeners to the `input` event (e.g.
// the `ReactiveFormsModule` uses such an approach) won't receive the correct value.
const enterValueIncrementally =
inputType === 'number' && keys.length > 0
inputType === 'number'
? // The value can be set character by character in number inputs if it doesn't have any decimals.
keys.every(key => key.key !== '.' && key.keyCode !== PERIOD)
: incrementalInputTypes.has(inputType);
Expand Down
19 changes: 19 additions & 0 deletions src/cdk/testing/tests/cross-environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {
ComponentHarness,
ComponentHarnessConstructor,
getNoKeysSpecifiedError,
HarnessLoader,
HarnessPredicate,
parallel,
Expand Down Expand Up @@ -345,6 +346,17 @@ export function crossEnvironmentSpecs(
harness = await getMainComponentHarnessFromEnvironment();
});

async function expectAsyncError(fn: () => Promise<void>, expected: Error) {
let error: Error | null = null;
try {
await fn();
} catch (e) {
error = e;
}
expect(error).not.toBe(null);
expect(error!.message).toBe(expected.message);
}

it('should be able to clear', async () => {
const input = await harness.input();
await input.sendKeys('Yi');
Expand All @@ -354,6 +366,13 @@ export function crossEnvironmentSpecs(
expect(await input.getProperty<string>('value')).toBe('');
});

it('sendKeys method should throw if no keys have been specified', async () => {
const input = await harness.input();
await expectAsyncError(() => input.sendKeys(), getNoKeysSpecifiedError());
await expectAsyncError(() => input.sendKeys(''), getNoKeysSpecifiedError());
await expectAsyncError(() => input.sendKeys('', ''), getNoKeysSpecifiedError());
});

it('should be able to click', async () => {
const counter = await harness.counter();
expect(await counter.text()).toBe('0');
Expand Down
3 changes: 3 additions & 0 deletions tools/public_api_guard/cdk/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export type EventData = string | number | boolean | undefined | null | EventData
[key: string]: EventData;
};

// @public
export function getNoKeysSpecifiedError(): Error;

// @public
export function _getTextWithExcludedElements(element: Element, excludeSelector: string): string;

Expand Down

0 comments on commit 4ab10e8

Please sign in to comment.