Skip to content

Commit ce6e2ca

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

File tree

5 files changed

+371
-0
lines changed

5 files changed

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

0 commit comments

Comments
 (0)