Skip to content

Commit ab9a847

Browse files
devversionjelbourn
authored andcommitted
feat(material-experimental): add test harness for input (#16674)
Adds a test harness for the `MatInput` implementation. Resolves COMP-182
1 parent 59cc66f commit ab9a847

File tree

5 files changed

+368
-0
lines changed

5 files changed

+368
-0
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@
9494
/src/material-experimental/mdc-checkbox/** @mmalerba
9595
/src/material-experimental/mdc-chips/** @mmalerba
9696
/src/material-experimental/mdc-helpers/** @mmalerba
97+
# Note to implementer: please repossess
98+
/src/material-experimental/mdc-input/** @devversion
9799
/src/material-experimental/mdc-menu/** @crisbeto
98100
/src/material-experimental/mdc-select/** @crisbeto
99101
/src/material-experimental/mdc-progress-spinner/** @andrewseguin
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")
4+
5+
ts_library(
6+
name = "harness",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//src/cdk-experimental/testing",
13+
"//src/cdk/coercion",
14+
],
15+
)
16+
17+
ng_test_library(
18+
name = "harness_tests",
19+
srcs = glob(["**/*.spec.ts"]),
20+
deps = [
21+
":harness",
22+
"//src/cdk-experimental/testing",
23+
"//src/cdk-experimental/testing/testbed",
24+
"//src/material/input",
25+
"@npm//@angular/forms",
26+
"@npm//@angular/platform-browser",
27+
],
28+
)
29+
30+
ng_web_test_suite(
31+
name = "tests",
32+
deps = [":harness_tests"],
33+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
export type InputHarnessFilters = {
10+
id?: string;
11+
name?: string;
12+
value?: string;
13+
};
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import {HarnessLoader} from '@angular/cdk-experimental/testing';
2+
import {TestbedHarnessEnvironment} from '@angular/cdk-experimental/testing/testbed';
3+
import {Component} from '@angular/core';
4+
import {ComponentFixture, TestBed} from '@angular/core/testing';
5+
import {ReactiveFormsModule} from '@angular/forms';
6+
import {MatInputModule} from '@angular/material/input';
7+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
8+
9+
import {MatInputHarness} from './input-harness';
10+
11+
let fixture: ComponentFixture<InputHarnessTest>;
12+
let loader: HarnessLoader;
13+
let inputHarness: typeof MatInputHarness;
14+
15+
describe('MatInputHarness', () => {
16+
describe('non-MDC-based', () => {
17+
beforeEach(async () => {
18+
await TestBed
19+
.configureTestingModule({
20+
imports: [NoopAnimationsModule, MatInputModule, ReactiveFormsModule],
21+
declarations: [InputHarnessTest],
22+
})
23+
.compileComponents();
24+
25+
fixture = TestBed.createComponent(InputHarnessTest);
26+
fixture.detectChanges();
27+
loader = TestbedHarnessEnvironment.loader(fixture);
28+
inputHarness = MatInputHarness;
29+
});
30+
31+
runTests();
32+
});
33+
34+
describe(
35+
'MDC-based',
36+
() => {
37+
// TODO: run tests for MDC based input once implemented.
38+
});
39+
});
40+
41+
/** Shared tests to run on both the original and MDC-based input's. */
42+
function runTests() {
43+
it('should load all input harnesses', async () => {
44+
const inputs = await loader.getAllHarnesses(inputHarness);
45+
expect(inputs.length).toBe(3);
46+
});
47+
48+
it('should load input with specific id', async () => {
49+
const inputs = await loader.getAllHarnesses(inputHarness.with({id: 'myTextarea'}));
50+
expect(inputs.length).toBe(1);
51+
});
52+
53+
it('should load input with specific name', async () => {
54+
const inputs = await loader.getAllHarnesses(inputHarness.with({name: 'favorite-food'}));
55+
expect(inputs.length).toBe(1);
56+
});
57+
58+
it('should load input with specific value', async () => {
59+
const inputs = await loader.getAllHarnesses(inputHarness.with({value: 'Sushi'}));
60+
expect(inputs.length).toBe(1);
61+
});
62+
63+
it('should be able to get id of input', async () => {
64+
const inputs = await loader.getAllHarnesses(inputHarness);
65+
expect(inputs.length).toBe(3);
66+
expect(await inputs[0].getId()).toMatch(/mat-input-\d+/);
67+
expect(await inputs[1].getId()).toMatch(/mat-input-\d+/);
68+
expect(await inputs[2].getId()).toBe('myTextarea');
69+
});
70+
71+
it('should be able to get name of input', async () => {
72+
const inputs = await loader.getAllHarnesses(inputHarness);
73+
expect(inputs.length).toBe(3);
74+
expect(await inputs[0].getName()).toBe('favorite-food');
75+
expect(await inputs[1].getName()).toBe('');
76+
expect(await inputs[2].getName()).toBe('');
77+
});
78+
79+
it('should be able to get value of input', async () => {
80+
const inputs = await loader.getAllHarnesses(inputHarness);
81+
expect(inputs.length).toBe(3);
82+
expect(await inputs[0].getValue()).toBe('Sushi');
83+
expect(await inputs[1].getValue()).toBe('');
84+
expect(await inputs[2].getValue()).toBe('');
85+
});
86+
87+
it('should be able to set value of input', async () => {
88+
const inputs = await loader.getAllHarnesses(inputHarness);
89+
expect(inputs.length).toBe(3);
90+
expect(await inputs[0].getValue()).toBe('Sushi');
91+
expect(await inputs[1].getValue()).toBe('');
92+
93+
await inputs[0].setValue('');
94+
await inputs[2].setValue('new-value');
95+
96+
expect(await inputs[0].getValue()).toBe('');
97+
expect(await inputs[2].getValue()).toBe('new-value');
98+
});
99+
100+
it('should be able to get disabled state', async () => {
101+
const inputs = await loader.getAllHarnesses(inputHarness);
102+
expect(inputs.length).toBe(3);
103+
104+
expect(await inputs[0].isDisabled()).toBe(false);
105+
expect(await inputs[1].isDisabled()).toBe(false);
106+
expect(await inputs[2].isDisabled()).toBe(false);
107+
108+
fixture.componentInstance.disabled = true;
109+
110+
expect(await inputs[1].isDisabled()).toBe(true);
111+
});
112+
113+
it('should be able to get readonly state', async () => {
114+
const inputs = await loader.getAllHarnesses(inputHarness);
115+
expect(inputs.length).toBe(3);
116+
117+
expect(await inputs[0].isReadonly()).toBe(false);
118+
expect(await inputs[1].isReadonly()).toBe(false);
119+
expect(await inputs[2].isReadonly()).toBe(false);
120+
121+
fixture.componentInstance.readonly = true;
122+
123+
expect(await inputs[1].isReadonly()).toBe(true);
124+
});
125+
126+
it('should be able to get required state', async () => {
127+
const inputs = await loader.getAllHarnesses(inputHarness);
128+
expect(inputs.length).toBe(3);
129+
130+
expect(await inputs[0].isRequired()).toBe(false);
131+
expect(await inputs[1].isRequired()).toBe(false);
132+
expect(await inputs[2].isRequired()).toBe(false);
133+
134+
fixture.componentInstance.required = true;
135+
136+
expect(await inputs[1].isRequired()).toBe(true);
137+
});
138+
139+
it('should be able to get placeholder of input', async () => {
140+
const inputs = await loader.getAllHarnesses(inputHarness);
141+
expect(inputs.length).toBe(3);
142+
expect(await inputs[0].getPlaceholder()).toBe('Favorite food');
143+
expect(await inputs[1].getPlaceholder()).toBe('');
144+
expect(await inputs[2].getPlaceholder()).toBe('Leave a comment');
145+
});
146+
147+
it('should be able to get type of input', async () => {
148+
const inputs = await loader.getAllHarnesses(inputHarness);
149+
expect(inputs.length).toBe(3);
150+
expect(await inputs[0].getType()).toBe('text');
151+
expect(await inputs[1].getType()).toBe('number');
152+
expect(await inputs[2].getType()).toBe('textarea');
153+
154+
fixture.componentInstance.inputType = 'text';
155+
156+
expect(await inputs[1].getType()).toBe('text');
157+
});
158+
159+
it('should be able to focus input', async () => {
160+
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'}));
161+
expect(getActiveElementTagName()).not.toBe('input');
162+
await input.focus();
163+
expect(getActiveElementTagName()).toBe('input');
164+
});
165+
166+
it('should be able to blur input', async () => {
167+
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'}));
168+
expect(getActiveElementTagName()).not.toBe('input');
169+
await input.focus();
170+
expect(getActiveElementTagName()).toBe('input');
171+
await input.blur();
172+
expect(getActiveElementTagName()).not.toBe('input');
173+
});
174+
}
175+
176+
function getActiveElementTagName() {
177+
return document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
178+
}
179+
180+
@Component({
181+
template: `
182+
<mat-form-field>
183+
<input matInput placeholder="Favorite food" value="Sushi" name="favorite-food">
184+
</mat-form-field>
185+
186+
<mat-form-field>
187+
<input matInput [type]="inputType"
188+
[readonly]="readonly"
189+
[disabled]="disabled"
190+
[required]="required">
191+
</mat-form-field>
192+
193+
<mat-form-field>
194+
<textarea id="myTextarea" matInput placeholder="Leave a comment"></textarea>
195+
</mat-form-field>
196+
`
197+
})
198+
class InputHarnessTest {
199+
inputType = 'number';
200+
readonly = false;
201+
disabled = false;
202+
required = false;
203+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
import {ComponentHarness, HarnessPredicate} from '@angular/cdk-experimental/testing';
10+
import {InputHarnessFilters} from './input-harness-filters';
11+
12+
/**
13+
* Harness for interacting with a standard Material inputs in tests.
14+
* @dynamic
15+
*/
16+
export class MatInputHarness extends ComponentHarness {
17+
static hostSelector = '[matInput]';
18+
19+
/**
20+
* Gets a `HarnessPredicate` that can be used to search for an input with
21+
* specific attributes.
22+
* @param options Options for narrowing the search:
23+
* - `name` finds an input with specific name.
24+
* - `id` finds an input with specific id.
25+
* - `value` finds an input with specific value.
26+
* @return a `HarnessPredicate` configured with the given options.
27+
*/
28+
static with(options: InputHarnessFilters = {}): HarnessPredicate<MatInputHarness> {
29+
// TODO(devversion): "name" and "id" can be removed once components#16848 is merged.
30+
return new HarnessPredicate(MatInputHarness)
31+
.addOption(
32+
'name', options.name, async (harness, name) => (await harness.getName()) === name)
33+
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id)
34+
.addOption(
35+
'value', options.value, async (harness, value) => (await harness.getValue()) === value);
36+
}
37+
38+
/** Whether the input is disabled. */
39+
async isDisabled(): Promise<boolean> {
40+
return (await this.host()).getProperty('disabled')!;
41+
}
42+
43+
/** Whether the input is required. */
44+
async isRequired(): Promise<boolean> {
45+
return (await this.host()).getProperty('required')!;
46+
}
47+
48+
/** Whether the input is readonly. */
49+
async isReadonly(): Promise<boolean> {
50+
return (await this.host()).getProperty('readOnly')!;
51+
}
52+
53+
/** Gets the value of the input. */
54+
async getValue(): Promise<string> {
55+
// The "value" property of the native input is never undefined.
56+
return (await (await this.host()).getProperty('value'))!;
57+
}
58+
59+
/** Gets the name of the input. */
60+
async getName(): Promise<string> {
61+
// The "name" property of the native input is never undefined.
62+
return (await (await this.host()).getProperty('name'))!;
63+
}
64+
65+
/**
66+
* Gets the type of the input. Returns "textarea" if the input is
67+
* a textarea.
68+
*/
69+
async getType(): Promise<string> {
70+
// The "type" property of the native input is never undefined.
71+
return (await (await this.host()).getProperty('type'))!;
72+
}
73+
74+
/** Gets the placeholder of the input. / */
75+
async getPlaceholder(): Promise<string> {
76+
// The "placeholder" property of the native input is never undefined.
77+
return (await (await this.host()).getProperty('placeholder'))!;
78+
}
79+
80+
/** Gets the id of the input. */
81+
async getId(): Promise<string> {
82+
// The input directive always assigns a unique id to the input in
83+
// case no id has been explicitly specified.
84+
return (await (await this.host()).getProperty('id'))!;
85+
}
86+
87+
/**
88+
* Focuses the input and returns a promise that indicates when the
89+
* action is complete.
90+
*/
91+
async focus(): Promise<void> {
92+
return (await this.host()).focus();
93+
}
94+
95+
/**
96+
* Blurs the input and returns a promise that indicates when the
97+
* action is complete.
98+
*/
99+
async blur(): Promise<void> {
100+
return (await this.host()).blur();
101+
}
102+
103+
/**
104+
* Sets the value of the input. The value will be set by simulating
105+
* keypresses that correspond to the given value.
106+
*/
107+
async setValue(newValue: string): Promise<void> {
108+
const inputEl = await this.host();
109+
await inputEl.clear();
110+
// We don't want to send keys for the value if the value is an empty
111+
// string in order to clear the value. Sending keys with an empty string
112+
// still results in unnecessary focus events.
113+
if (newValue) {
114+
await inputEl.sendKeys(newValue);
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)