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): unable to assign value to number inputs using sendKeys #22395

Merged
merged 1 commit into from
Apr 13, 2021
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
1 change: 1 addition & 0 deletions src/cdk/keycodes/keycodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const SEMICOLON = 186; // Firefox (Gecko) fires 59 for SEMICOLON
export const EQUALS = 187; // Firefox (Gecko) fires 61 for EQUALS
export const COMMA = 188;
export const DASH = 189; // Firefox (Gecko) fires 173 for DASH/MINUS
export const PERIOD = 190;
export const SLASH = 191;
export const APOSTROPHE = 192;
export const TILDE = 192;
Expand Down
39 changes: 35 additions & 4 deletions src/cdk/testing/testbed/fake-events/type-in-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
*/

import {ModifierKeys} from '@angular/cdk/testing';
import {PERIOD} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from './dispatch-events';
import {triggerFocus} from './element-focus';

/** Input types for which the value can be entered incrementally. */
const incrementalInputTypes =
new Set(['text', 'email', 'hidden', 'password', 'search', 'tel', 'url']);

/**
* Checks whether the given Element is a text input element.
* @docs-private
*/
export function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement {
const nodeName = element.nodeName.toLowerCase();
return nodeName === 'input' || nodeName === 'textarea' ;
return nodeName === 'input' || nodeName === 'textarea';
}

/**
Expand Down Expand Up @@ -51,21 +56,47 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
modifiers = {};
rest = modifiersAndKeys;
}
const isInput = isTextInput(element);
const inputType = element.getAttribute('type') || 'text';
const keys: {keyCode?: number, key?: string}[] = rest
.map(k => typeof k === 'string' ?
k.split('').map(c => ({keyCode: c.toUpperCase().charCodeAt(0), key: c})) : [k])
.reduce((arr, k) => arr.concat(k), []);

// 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 ?
// 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);

triggerFocus(element);

// When we aren't entering the value incrementally, assign it all at once ahead
// of time so that any listeners to the key events below will have access to it.
if (!enterValueIncrementally) {
(element as HTMLInputElement).value = keys.reduce((value, key) => value + (key.key || ''), '');
}

for (const key of keys) {
dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers);
dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers);
if (isTextInput(element) && key.key && key.key.length === 1) {
element.value += key.key;
dispatchFakeEvent(element, 'input');
if (isInput && key.key && key.key.length === 1) {
if (enterValueIncrementally) {
(element as HTMLInputElement | HTMLTextAreaElement).value += key.key;
dispatchFakeEvent(element, 'input');
}
}
dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers);
}

// Since we weren't dispatching `input` events while sending the keys, we have to do it now.
if (!enterValueIncrementally) {
dispatchFakeEvent(element, 'input');
}
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/cdk/testing/tests/cross-environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,15 @@ export function crossEnvironmentSpecs(
expect(await getActiveElementId()).toBe(await input.getAttribute('id'));
});

it('should be able to type in values with a decimal', async () => {
const input = await harness.numberInput();
const value = await harness.numberInputValue();
await input.sendKeys('123.456');

expect(await input.getProperty('value')).toBe('123.456');
expect(await value.text()).toBe('Number value: 123.456');
});

it('should be able to retrieve dimensions', async () => {
const dimensions = await (await harness.title()).getDimensions();
expect(dimensions).toEqual(jasmine.objectContaining({height: 100, width: 200}));
Expand Down
2 changes: 2 additions & 0 deletions src/cdk/testing/tests/harnesses/main-component-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export class MainComponentHarness extends ComponentHarness {
readonly multiSelect = this.locatorFor('#multi-select');
readonly multiSelectValue = this.locatorFor('#multi-select-value');
readonly multiSelectChangeEventCounter = this.locatorFor('#multi-select-change-counter');
readonly numberInput = this.locatorFor('#number-input');
readonly numberInputValue = this.locatorFor('#number-input-value');
readonly contextmenuTestResult = this.locatorFor('.contextmenu-test-result');
// Allow null for element
readonly nullItem = this.locatorForOptional('wrong locator');
Expand Down
4 changes: 2 additions & 2 deletions src/cdk/testing/tests/test-components-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@

import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {TestMainComponent} from './test-main-component';
import {TestShadowBoundary, TestSubShadowBoundary} from './test-shadow-boundary';
import {TestSubComponent} from './test-sub-component';

@NgModule({
imports: [CommonModule, FormsModule],
imports: [CommonModule, FormsModule, ReactiveFormsModule],
declarations: [TestMainComponent, TestSubComponent, TestShadowBoundary, TestSubShadowBoundary],
exports: [TestMainComponent, TestSubComponent, TestShadowBoundary, TestSubShadowBoundary]
})
Expand Down
3 changes: 3 additions & 0 deletions src/cdk/testing/tests/test-main-component.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ <h1 style="height: 100px; width: 200px;">Main Component</h1>
<div id="multi-select-change-counter">Change events: {{multiSelectChangeEventCount}}</div>
<div (myCustomEvent)="basicEvent = basicEvent + 1" id="custom-event-basic">Basic event: {{basicEvent}}</div>
<div (myCustomEvent)="onCustomEvent($event)" id="custom-event-object">Event with object: {{customEventData}}</div>

<input id="number-input" type="number" aria-label="Enter a number" [formControl]="numberControl">
<div id="number-input-value">Number value: {{numberControl.value}}</div>
</div>
<div class="subcomponents">
<test-sub class="test-special" title="test tools" [items]="testTools"></test-sub>
Expand Down
2 changes: 2 additions & 0 deletions src/cdk/testing/tests/test-main-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {ENTER} from '@angular/cdk/keycodes';
import {_supportsShadowDom} from '@angular/cdk/platform';
import {FormControl} from '@angular/forms';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Expand Down Expand Up @@ -45,6 +46,7 @@ export class TestMainComponent implements OnDestroy {
_shadowDomSupported = _supportsShadowDom();
clickResult = {x: -1, y: -1};
rightClickResult = {x: -1, y: -1, button: -1};
numberControl = new FormControl();

@ViewChild('clickTestElement') clickTestElement: ElementRef<HTMLElement>;
@ViewChild('taskStateResult') taskStateResultElement: ElementRef<HTMLElement>;
Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/cdk/keycodes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export declare const PAGE_UP = 33;

export declare const PAUSE = 19;

export declare const PERIOD = 190;

export declare const PLUS_SIGN = 43;

export declare const PRINT_SCREEN = 44;
Expand Down