Skip to content

Commit 1f5fc52

Browse files
asynclizcopybara-github
authored andcommitted
chore(text-field): add harness input methods
PiperOrigin-RevId: 449024607
1 parent f25d08d commit 1f5fc52

File tree

2 files changed

+318
-2
lines changed

2 files changed

+318
-2
lines changed

components/text-field/harness.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,57 @@ import {TextField} from './lib/text-field.js';
1616
export class TextFieldHarness extends Harness<TextField> {
1717
readonly field = this.getField();
1818

19+
/** Used to track whether or not a change event should be dispatched. */
20+
protected valueBeforeChange = '';
21+
22+
/**
23+
* Simulates a user typing a value one character at a time. This will fire
24+
* multiple input events.
25+
*
26+
* Use focus/blur to ensure change events are fired.
27+
*
28+
* @example
29+
* await harness.focusWithKeyboard();
30+
* await harness.inputValue('value'); // input events
31+
* await harness.blur(); // change event
32+
*
33+
* @param value The value to simulating typing.
34+
*/
35+
async inputValue(value: string) {
36+
for (const char of value) {
37+
this.simulateKeypress(await this.getInteractiveElement(), char);
38+
this.simulateInput(await this.getInteractiveElement(), char);
39+
}
40+
}
41+
42+
/**
43+
* Simulates a user deleting part of a value with the backspace key.
44+
* By default, the entire value is deleted. This will fire a single input
45+
* event.
46+
*
47+
* Use focus/blur to ensure change events are fired.
48+
*
49+
* @example
50+
* await harness.focusWithKeyboard();
51+
* await harness.deleteValue(); // input event
52+
* await harness.blur(); // change event
53+
*
54+
* @param beginIndex The starting index of the value to delete.
55+
* @param endIndex The ending index of the value to delete.
56+
*/
57+
async deleteValue(beginIndex?: number, endIndex?: number) {
58+
this.simulateKeypress(await this.getInteractiveElement(), 'Backspace');
59+
this.simulateDeletion(
60+
await this.getInteractiveElement(), beginIndex, endIndex);
61+
}
62+
63+
override async reset() {
64+
this.element.value =
65+
''; // TODO(b/443725652): replace with this.element.reset();
66+
this.valueBeforeChange = '';
67+
await super.reset();
68+
}
69+
1970
override async hoverEnter() {
2071
await super.hoverEnter();
2172
await (await this.field).hoverEnter();
@@ -27,30 +78,74 @@ export class TextFieldHarness extends Harness<TextField> {
2778
}
2879

2980
override async focusWithKeyboard() {
81+
this.valueBeforeChange = this.element.value;
3082
await super.focusWithKeyboard();
3183
await (await this.field).focusWithKeyboard();
3284
}
3385

3486
override async focusWithPointer() {
87+
this.valueBeforeChange = this.element.value;
3588
await super.focusWithPointer();
3689
await (await this.field).focusWithPointer();
3790
}
3891

3992
override async blur() {
4093
await super.blur();
4194
await (await this.field).blur();
95+
this.simulateChangeIfNeeded(await this.getInteractiveElement());
96+
}
97+
98+
protected simulateInput(
99+
element: HTMLInputElement, charactersToAppend: string,
100+
init?: InputEventInit) {
101+
element.value += charactersToAppend;
102+
if (!init) {
103+
init = {
104+
inputType: 'insertText',
105+
isComposing: false,
106+
data: charactersToAppend,
107+
};
108+
}
109+
110+
element.dispatchEvent(new InputEvent('input', init));
111+
}
112+
113+
protected simulateDeletion(
114+
element: HTMLInputElement, beginIndex?: number, endIndex?: number,
115+
init?: InputEventInit) {
116+
const deletedCharacters = element.value.slice(beginIndex, endIndex);
117+
element.value = element.value.substring(0, beginIndex ?? 0) +
118+
element.value.substring(endIndex ?? element.value.length);
119+
if (!init) {
120+
init = {
121+
inputType: 'deleteContentBackward',
122+
isComposing: false,
123+
data: deletedCharacters,
124+
};
125+
}
126+
127+
element.dispatchEvent(new InputEvent('input', init));
128+
}
129+
130+
protected simulateChangeIfNeeded(element: HTMLInputElement) {
131+
if (this.valueBeforeChange === element.value) {
132+
return;
133+
}
134+
135+
this.valueBeforeChange = element.value;
136+
element.dispatchEvent(new Event('change'));
42137
}
43138

44139
protected override async getInteractiveElement() {
45140
await this.element.updateComplete;
46141
return this.element.renderRoot.querySelector('.md3-text-field__input') as
47-
HTMLElement;
142+
HTMLInputElement;
48143
}
49144

50145
protected async getField() {
51146
await this.element.updateComplete;
52147
const field = this.element.renderRoot.querySelector(
53-
'md-filled-field,md-outlined-field') as Field;
148+
'.md3-text-field__field') as Field;
54149
return new FieldHarness(field);
55150
}
56151
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/**
2+
* @license
3+
* Copyright 2022 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import 'jasmine';
8+
import './filled-text-field.js';
9+
10+
import {html} from 'lit';
11+
12+
import {Environment} from '../testing/environment.js';
13+
import {Harness} from '../testing/harness.js';
14+
15+
import {TextFieldHarness} from './harness.js';
16+
17+
describe('TextFieldHarness', () => {
18+
const env = new Environment();
19+
20+
function setupTest() {
21+
const root =
22+
env.render(html`<md-filled-text-field></md-filled-text-field>`);
23+
const instance = root.querySelector('md-filled-text-field');
24+
if (!instance) {
25+
throw new Error('Failed to query md-filled-text-field.');
26+
}
27+
28+
return new TextFieldHarness(instance);
29+
}
30+
31+
describe('inputValue()', () => {
32+
it('should emit key events for each character typed', async () => {
33+
// Setup.
34+
const harness = setupTest();
35+
const keydownHandler = jasmine.createSpy('keydownHandler');
36+
harness.element.addEventListener('keydown', keydownHandler);
37+
// Test case.
38+
await harness.inputValue('abc');
39+
// Assertion.
40+
expect(keydownHandler).toHaveBeenCalledTimes(3);
41+
expect(keydownHandler).toHaveBeenCalledWith(jasmine.any(KeyboardEvent));
42+
expect(keydownHandler.calls.allArgs()).toEqual([
43+
[jasmine.objectContaining({key: 'a'})],
44+
[jasmine.objectContaining({key: 'b'})],
45+
[jasmine.objectContaining({key: 'c'})],
46+
]);
47+
});
48+
49+
it('should emit input events for each character typed', async () => {
50+
// Setup.
51+
const harness = setupTest();
52+
const inputHandler = jasmine.createSpy('inputHandler');
53+
harness.element.addEventListener('input', inputHandler);
54+
// Test case.
55+
await harness.inputValue('abc');
56+
// Assertion.
57+
expect(inputHandler).toHaveBeenCalledTimes(3);
58+
expect(inputHandler).toHaveBeenCalledWith(jasmine.any(InputEvent));
59+
});
60+
});
61+
62+
describe('deleteValue()', () => {
63+
it('should press the Backspace key', async () => {
64+
// Setup.
65+
const harness = setupTest();
66+
const keydownHandler = jasmine.createSpy('keydownHandler');
67+
harness.element.addEventListener('keydown', keydownHandler);
68+
harness.element.value = 'Value';
69+
// Test case.
70+
await harness.deleteValue();
71+
// Assertion.
72+
expect(keydownHandler).toHaveBeenCalledTimes(1);
73+
expect(keydownHandler).toHaveBeenCalledWith(jasmine.any(KeyboardEvent));
74+
expect(keydownHandler).toHaveBeenCalledWith(jasmine.objectContaining({
75+
key: 'Backspace'
76+
}));
77+
});
78+
79+
it('should delete the entire value by default', async () => {
80+
// Setup.
81+
const harness = setupTest();
82+
harness.element.value = 'Value';
83+
// Test case.
84+
await harness.deleteValue();
85+
// Assertion.
86+
expect(harness.element.value).toBe('');
87+
});
88+
89+
it('should allow deleting part of the value', async () => {
90+
// Setup.
91+
const harness = setupTest();
92+
harness.element.value = 'Value';
93+
// Test case.
94+
await harness.deleteValue(1, 4);
95+
// Assertion.
96+
expect(harness.element.value).toBe('Ve');
97+
});
98+
});
99+
100+
describe('reset()', () => {
101+
it('should set the value to an empty string', async () => {
102+
// Setup.
103+
const harness = setupTest();
104+
harness.element.value = 'Value';
105+
// Test case.
106+
await harness.reset();
107+
// Assertion.
108+
expect(harness.element.value).toBe('');
109+
});
110+
111+
it('should call super.reset()', async () => {
112+
// Setup.
113+
const harness = setupTest();
114+
spyOn(Harness.prototype, 'reset');
115+
// Test case.
116+
await harness.reset();
117+
// Assertion.
118+
expect(Harness.prototype.reset).toHaveBeenCalledTimes(1);
119+
});
120+
});
121+
122+
describe('field harness', () => {
123+
it('should call hoverEnter() on the field harness', async () => {
124+
// Setup.
125+
const harness = setupTest();
126+
const field = await harness.field;
127+
spyOn(field, 'hoverEnter').and.callThrough();
128+
// Test case.
129+
await harness.hoverEnter();
130+
// Assertion.
131+
expect(field.hoverEnter).toHaveBeenCalledTimes(1);
132+
});
133+
134+
it('should call hoverLeave() on the field harness', async () => {
135+
// Setup.
136+
const harness = setupTest();
137+
const field = await harness.field;
138+
spyOn(field, 'hoverLeave').and.callThrough();
139+
// Test case.
140+
await harness.hoverLeave();
141+
// Assertion.
142+
expect(field.hoverLeave).toHaveBeenCalledTimes(1);
143+
});
144+
145+
it('should call focusWithKeyboard() on the field harness', async () => {
146+
// Setup.
147+
const harness = setupTest();
148+
const field = await harness.field;
149+
spyOn(field, 'focusWithKeyboard').and.callThrough();
150+
// Test case.
151+
await harness.focusWithKeyboard();
152+
// Assertion.
153+
expect(field.focusWithKeyboard).toHaveBeenCalledTimes(1);
154+
});
155+
156+
it('should call focusWithPointer() on the field harness', async () => {
157+
// Setup.
158+
const harness = setupTest();
159+
const field = await harness.field;
160+
spyOn(field, 'focusWithPointer').and.callThrough();
161+
// Test case.
162+
await harness.focusWithPointer();
163+
// Assertion.
164+
expect(field.focusWithPointer).toHaveBeenCalledTimes(1);
165+
});
166+
167+
it('should call blur() on the field harness', async () => {
168+
// Setup.
169+
const harness = setupTest();
170+
const field = await harness.field;
171+
spyOn(field, 'blur').and.callThrough();
172+
// Test case.
173+
await harness.blur();
174+
// Assertion.
175+
expect(field.blur).toHaveBeenCalledTimes(1);
176+
});
177+
});
178+
179+
describe('simulating change events', () => {
180+
it('should dispatch change if value changes after focus and blur',
181+
async () => {
182+
// Setup.
183+
const harness = setupTest();
184+
const changeHandler = jasmine.createSpy('changeHandler');
185+
harness.element.addEventListener('change', changeHandler);
186+
// Test case.
187+
await harness.focusWithKeyboard();
188+
await harness.inputValue('value');
189+
await harness.blur();
190+
// Assertion.
191+
expect(changeHandler).toHaveBeenCalledTimes(1);
192+
});
193+
194+
it('should not dispatch change if value does not change', async () => {
195+
// Setup.
196+
const harness = setupTest();
197+
const changeHandler = jasmine.createSpy('changeHandler');
198+
harness.element.value = 'value';
199+
harness.element.addEventListener('change', changeHandler);
200+
// Test case.
201+
await harness.focusWithKeyboard();
202+
await harness.blur();
203+
// Assertion.
204+
expect(changeHandler).not.toHaveBeenCalled();
205+
});
206+
207+
it('should not dispatch change if reset', async () => {
208+
// Setup.
209+
const harness = setupTest();
210+
const changeHandler = jasmine.createSpy('changeHandler');
211+
await harness.focusWithKeyboard();
212+
await harness.inputValue('value');
213+
harness.element.addEventListener('change', changeHandler);
214+
// Test case.
215+
await harness.reset();
216+
await harness.blur();
217+
// Assertion.
218+
expect(changeHandler).not.toHaveBeenCalled();
219+
});
220+
});
221+
});

0 commit comments

Comments
 (0)