Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cdk/testing): TestElement sendKeys method should throw if no keys have been specified #18271

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
20 changes: 20 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,18 @@ export function crossEnvironmentSpecs(
harness = await getMainComponentHarnessFromEnvironment();
});

async function expectAsyncError(fn: () => Promise<void>, expected: Error) {
let error: unknown | null = null;
try {
await fn();
} catch (e: unknown) {
error = e;
}
expect(error).not.toBe(null);
expect(error instanceof Error).toBe(true);
expect((error as 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 +367,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