Skip to content

Commit c8bf616

Browse files
committed
feat(material-experimental): add test harness for input
Adds a test harness for the `MatInput` implementation. Resolves COMP-182
1 parent 3c87bee commit c8bf616

File tree

5 files changed

+362
-0
lines changed

5 files changed

+362
-0
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@
9393
/src/material-experimental/mdc-checkbox/** @mmalerba
9494
/src/material-experimental/mdc-chips/** @mmalerba
9595
/src/material-experimental/mdc-helpers/** @mmalerba
96+
# Note to implementer: please repossess
97+
/src/material-experimental/mdc-input/** @devversion
9698
/src/material-experimental/mdc-menu/** @crisbeto
9799
/src/material-experimental/mdc-progress-spinner/** @andrewseguin
98100
/src/material-experimental/mdc-progress-bar/** @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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
};
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 be able to get id of input', async () => {
59+
const inputs = await loader.getAllHarnesses(inputHarness);
60+
expect(inputs.length).toBe(3);
61+
expect(await inputs[0].getId()).toMatch(/mat-input-\d+/);
62+
expect(await inputs[1].getId()).toMatch(/mat-input-\d+/);
63+
expect(await inputs[2].getId()).toBe('myTextarea');
64+
});
65+
66+
it('should be able to get name of input', async () => {
67+
const inputs = await loader.getAllHarnesses(inputHarness);
68+
expect(inputs.length).toBe(3);
69+
expect(await inputs[0].getName()).toBe('favorite-food');
70+
expect(await inputs[1].getName()).toBe('');
71+
expect(await inputs[2].getName()).toBe('');
72+
});
73+
74+
it('should be able to get value of input', async () => {
75+
const inputs = await loader.getAllHarnesses(inputHarness);
76+
expect(inputs.length).toBe(3);
77+
expect(await inputs[0].getValue()).toBe('Sushi');
78+
expect(await inputs[1].getValue()).toBe('');
79+
expect(await inputs[2].getValue()).toBe('');
80+
});
81+
82+
it('should be able to set value of input', async () => {
83+
const inputs = await loader.getAllHarnesses(inputHarness);
84+
expect(inputs.length).toBe(3);
85+
expect(await inputs[0].getValue()).toBe('Sushi');
86+
expect(await inputs[1].getValue()).toBe('');
87+
88+
await inputs[0].setValue('');
89+
await inputs[2].setValue('new-value');
90+
91+
expect(await inputs[0].getValue()).toBe('');
92+
expect(await inputs[2].getValue()).toBe('new-value');
93+
});
94+
95+
it('should be able to get disabled state', async () => {
96+
const inputs = await loader.getAllHarnesses(inputHarness);
97+
expect(inputs.length).toBe(3);
98+
99+
expect(await inputs[0].isDisabled()).toBe(false);
100+
expect(await inputs[1].isDisabled()).toBe(false);
101+
expect(await inputs[2].isDisabled()).toBe(false);
102+
103+
fixture.componentInstance.disabled = true;
104+
fixture.detectChanges();
105+
106+
expect(await inputs[1].isDisabled()).toBe(true);
107+
});
108+
109+
it('should be able to get readonly state', async () => {
110+
const inputs = await loader.getAllHarnesses(inputHarness);
111+
expect(inputs.length).toBe(3);
112+
113+
expect(await inputs[0].isReadonly()).toBe(false);
114+
expect(await inputs[1].isReadonly()).toBe(false);
115+
expect(await inputs[2].isReadonly()).toBe(false);
116+
117+
fixture.componentInstance.readonly = true;
118+
fixture.detectChanges();
119+
120+
expect(await inputs[1].isReadonly()).toBe(true);
121+
});
122+
123+
it('should be able to get required state', async () => {
124+
const inputs = await loader.getAllHarnesses(inputHarness);
125+
expect(inputs.length).toBe(3);
126+
127+
expect(await inputs[0].isRequired()).toBe(false);
128+
expect(await inputs[1].isRequired()).toBe(false);
129+
expect(await inputs[2].isRequired()).toBe(false);
130+
131+
fixture.componentInstance.required = true;
132+
fixture.detectChanges();
133+
134+
expect(await inputs[1].isRequired()).toBe(true);
135+
});
136+
137+
it('should be able to get placeholder of input', async () => {
138+
const inputs = await loader.getAllHarnesses(inputHarness);
139+
expect(inputs.length).toBe(3);
140+
expect(await inputs[0].getPlaceholder()).toBe('Favorite food');
141+
expect(await inputs[1].getPlaceholder()).toBe('');
142+
expect(await inputs[2].getPlaceholder()).toBe('Leave a comment');
143+
});
144+
145+
it('should be able to get type of input', async () => {
146+
const inputs = await loader.getAllHarnesses(inputHarness);
147+
expect(inputs.length).toBe(3);
148+
expect(await inputs[0].getType()).toBe('text');
149+
expect(await inputs[1].getType()).toBe('number');
150+
expect(await inputs[2].getType()).toBe('textarea');
151+
152+
fixture.componentInstance.inputType = 'text';
153+
fixture.detectChanges();
154+
155+
expect(await inputs[1].getType()).toBe('text');
156+
});
157+
158+
it('should be able to focus input', async () => {
159+
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'}));
160+
expect(getActiveElementTagName()).not.toBe('input');
161+
await input.focus();
162+
expect(getActiveElementTagName()).toBe('input');
163+
});
164+
165+
it('should be able to blur input', async () => {
166+
const input = await loader.getHarness(inputHarness.with({name: 'favorite-food'}));
167+
expect(getActiveElementTagName()).not.toBe('input');
168+
await input.focus();
169+
expect(getActiveElementTagName()).toBe('input');
170+
await input.blur();
171+
expect(getActiveElementTagName()).not.toBe('input');
172+
});
173+
}
174+
175+
function getActiveElementTagName() {
176+
return document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
177+
}
178+
179+
@Component({
180+
template: `
181+
<mat-form-field>
182+
<input matInput placeholder="Favorite food" value="Sushi" name="favorite-food">
183+
</mat-form-field>
184+
185+
<mat-form-field>
186+
<input matInput [type]="inputType"
187+
[readonly]="readonly"
188+
[disabled]="disabled"
189+
[required]="required">
190+
</mat-form-field>
191+
192+
<mat-form-field>
193+
<textarea id="myTextarea" matInput placeholder="Leave a comment"></textarea>
194+
</mat-form-field>
195+
`
196+
})
197+
class InputHarnessTest {
198+
inputType = 'number';
199+
readonly = false;
200+
disabled = false;
201+
required = false;
202+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
* @return a `HarnessPredicate` configured with the given options.
26+
*/
27+
static with(options: InputHarnessFilters = {}): HarnessPredicate<MatInputHarness> {
28+
return new HarnessPredicate(MatInputHarness)
29+
.addOption(
30+
'name', options.name, async (harness, name) => (await harness.getName()) === name)
31+
.addOption('id', options.id, async (harness, id) => (await harness.getId()) === id);
32+
}
33+
34+
/** Whether the input is disabled. */
35+
async isDisabled(): Promise<boolean> {
36+
return (await this.host()).getPropertyValue('disabled')!;
37+
}
38+
39+
/** Whether the input is required. */
40+
async isRequired(): Promise<boolean> {
41+
return (await this.host()).getPropertyValue('required')!;
42+
}
43+
44+
/** Whether the input is readonly. */
45+
async isReadonly(): Promise<boolean> {
46+
return (await this.host()).getPropertyValue('readOnly')!;
47+
}
48+
49+
/** Gets the value of the input. */
50+
async getValue(): Promise<string> {
51+
// The "value" property of the native input is never undefined.
52+
return (await (await this.host()).getPropertyValue('value'))!;
53+
}
54+
55+
/** Gets the name of the input. */
56+
async getName(): Promise<string> {
57+
// The "name" property of the native input is never undefined.
58+
return (await (await this.host()).getPropertyValue('name'))!;
59+
}
60+
61+
/**
62+
* Gets the type of the input. Returns "textarea" if the input is
63+
* a textarea.
64+
*/
65+
async getType(): Promise<string> {
66+
// The "type" property of the native input is never undefined.
67+
return (await (await this.host()).getPropertyValue('type'))!;
68+
}
69+
70+
/** Gets the placeholder of the input. / */
71+
async getPlaceholder(): Promise<string> {
72+
// The "placeholder" property of the native input is never undefined.
73+
return (await (await this.host()).getPropertyValue('placeholder'))!;
74+
}
75+
76+
/** Gets the id of the input. */
77+
async getId(): Promise<string> {
78+
// The input directive always assigns a unique id to the input in
79+
// case no id has been explicitly specified.
80+
return (await (await this.host()).getPropertyValue('id'))!;
81+
}
82+
83+
/**
84+
* Focuses the input and returns a promise that indicates when the
85+
* action is complete.
86+
*/
87+
async focus(): Promise<void> {
88+
return (await this.host()).focus();
89+
}
90+
91+
/**
92+
* Blurs the input and returns a promise that indicates when the
93+
* action is complete.
94+
*/
95+
async blur(): Promise<void> {
96+
return (await this.host()).blur();
97+
}
98+
99+
/**
100+
* Sets the value of the input. The value will be set by simulating
101+
* keypresses that correspond to the given value.
102+
*/
103+
async setValue(newValue: string): Promise<void> {
104+
const inputEl = await this.host();
105+
await inputEl.clear();
106+
// We don't want to send keys for the value if the value is an empty
107+
// string in order to clear the value. Sending keys with an empty string
108+
// still results in unnecessary focus events.
109+
if (newValue) {
110+
await inputEl.sendKeys(newValue);
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)