Skip to content

Commit 2cec4d3

Browse files
committed
feat(material-experimental): add test harness for form-field
1 parent 2001965 commit 2cec4d3

File tree

7 files changed

+591
-0
lines changed

7 files changed

+591
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
4+
5+
ng_module(
6+
name = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material-experimental/form-field/testing",
12+
deps = [
13+
"//src/cdk/testing",
14+
"//src/material-experimental/input/testing",
15+
"//src/material-experimental/mdc-select:harness",
16+
],
17+
)
18+
19+
ng_test_library(
20+
name = "harness_tests_lib",
21+
srcs = ["shared.spec.ts"],
22+
deps = [
23+
":testing",
24+
"//src/cdk/testing",
25+
"//src/cdk/testing/testbed",
26+
"@npm//@angular/forms",
27+
"@npm//@angular/platform-browser",
28+
],
29+
)
30+
31+
ng_test_library(
32+
name = "unit_tests_lib",
33+
srcs = glob(
34+
["**/*.spec.ts"],
35+
exclude = ["shared.spec.ts"],
36+
),
37+
deps = [
38+
"//src/material/autocomplete",
39+
"//src/material/form-field",
40+
"//src/material/input",
41+
"//src/material/select",
42+
"//src/material-experimental/input/testing",
43+
"//src/material-experimental/mdc-select:harness",
44+
":harness_tests_lib",
45+
":testing",
46+
],
47+
)
48+
49+
ng_web_test_suite(
50+
name = "unit_tests",
51+
deps = [":unit_tests_lib"],
52+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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 {BaseHarnessFilters} from '@angular/cdk/testing';
10+
11+
export interface FormFieldHarnessFilters extends BaseHarnessFilters {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {MatInputHarness} from '@angular/material-experimental/input/testing';
2+
import {MatAutocompleteModule} from '@angular/material/autocomplete';
3+
import {MatFormFieldModule} from '@angular/material/form-field';
4+
import {MatInputModule} from '@angular/material/input';
5+
import {MatSelectModule} from '@angular/material/select';
6+
7+
// TODO(devversion): we cannot import the select harness through the module name
8+
// because it does not expose an entry-point that re-exports the harness
9+
import {MatSelectHarness} from '../../mdc-select/harness/select-harness';
10+
11+
import {MatFormFieldHarness} from './form-field-harness';
12+
import {runHarnessTests} from './shared.spec';
13+
14+
describe('Non-MDC-based MatFormFieldHarness', () => {
15+
runHarnessTests(
16+
[MatFormFieldModule, MatAutocompleteModule, MatInputModule, MatSelectModule],
17+
MatFormFieldHarness, MatInputHarness, MatSelectHarness);
18+
});
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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 {
10+
ComponentHarness,
11+
HarnessEnvironment,
12+
HarnessPredicate,
13+
TestElement
14+
} from '@angular/cdk/testing';
15+
import {MatInputHarness} from '@angular/material-experimental/input/testing';
16+
import {MatSelectHarness} from '../../mdc-select/harness/select-harness';
17+
import {FormFieldHarnessFilters} from './form-field-harness-filters';
18+
19+
// TODO(devversion): support datepicker harness once developed (COMP-203).
20+
// Also support chip list harness.
21+
/** Possible harnesses of controls which can be bound to a form-field. */
22+
export type FormFieldControlHarness = MatInputHarness|MatSelectHarness;
23+
24+
/**
25+
* Harness for interacting with a standard Material form-field's in tests.
26+
* @dynamic
27+
*/
28+
export class MatFormFieldHarness extends ComponentHarness {
29+
static hostSelector = '.mat-form-field';
30+
31+
/**
32+
* Gets a `HarnessPredicate` that can be used to search for an form-field with
33+
* specific attributes.
34+
* @param options Options for narrowing the search:
35+
* - `selector` finds a form-field that matches the given selector.
36+
* @return a `HarnessPredicate` configured with the given options.
37+
*/
38+
static with(options: FormFieldHarnessFilters = {}): HarnessPredicate<MatFormFieldHarness> {
39+
return new HarnessPredicate(MatFormFieldHarness, options);
40+
}
41+
42+
private _prefixContainer = this.locatorForOptional('.mat-form-field-prefix');
43+
private _suffixContainer = this.locatorForOptional('.mat-form-field-suffix');
44+
private _label = this.locatorForOptional('.mat-form-field-label');
45+
private _errors = this.locatorForAll('.mat-error');
46+
private _hints = this.locatorForAll('mat-hint,.mat-hint');
47+
48+
private _controlHarnessSelector =
49+
[MatInputHarness, MatSelectHarness].map(h => h.hostSelector).join(',');
50+
51+
/** Gets the appearance of the form-field. */
52+
async getAppearance(): Promise<'legacy'|'standard'|'fill'|'outline'> {
53+
const hostEl = await this.host();
54+
const [isLegacy, isStandard, isFill, isOutline] = await Promise.all([
55+
hostEl.hasClass('mat-form-field-appearance-legacy'),
56+
hostEl.hasClass('mat-form-field-appearance-standard'),
57+
hostEl.hasClass('mat-form-field-appearance-fill'),
58+
hostEl.hasClass('mat-form-field-appearance-outline'),
59+
]);
60+
if (isLegacy) {
61+
return 'legacy';
62+
} else if (isStandard) {
63+
return 'standard';
64+
} else if (isFill) {
65+
return 'fill';
66+
} else if (isOutline) {
67+
return 'outline';
68+
}
69+
throw Error('Could not determine appearance of form-field.');
70+
}
71+
72+
/** Gets the harness of the control that is bound to the form-field. */
73+
async getControl(): Promise<FormFieldControlHarness|null> {
74+
const environment: HarnessEnvironment<any> = (this as any).locatorFactory;
75+
// TODO(reviewer): not sure what we should do here. We need to construct a combined
76+
// query for all possible controls. This is necessary because if a form-control contains
77+
// multiple possible control candidates (i.e. a chip-list with matInput). We need to
78+
// find the control that matches first (to simulate the behavior of "@ContentChild").
79+
// The question is, how we can achieve this in a public manner. Should we expose these
80+
// functions? seems reasonable to me.
81+
const elementCandidates: TestElement[] =
82+
(await (environment as any).getAllRawElements(this._controlHarnessSelector))
83+
.map((rawElement: any) => (environment as any).createTestElement(rawElement));
84+
85+
if (!elementCandidates.length) {
86+
return null;
87+
}
88+
89+
const controlElement = elementCandidates[0];
90+
const [isInput, isSelect] = await Promise.all([
91+
controlElement.matchesSelector(MatInputHarness.hostSelector),
92+
controlElement.matchesSelector(MatSelectHarness.hostSelector),
93+
]);
94+
95+
if (isInput) {
96+
return (environment as any).createComponentHarness(MatInputHarness, controlElement);
97+
} else if (isSelect) {
98+
return (environment as any).createComponentHarness(MatSelectHarness, controlElement);
99+
}
100+
// This error will be reported if we add new harnesses to the form-field control
101+
// selector but forget checking for it here.
102+
throw Error('Found control of form-field but could not create harness.');
103+
}
104+
105+
/** Whether the form-field has a label. */
106+
async hasLabel(): Promise<boolean> {
107+
return (await this.host()).hasClass('mat-form-field-has-label');
108+
}
109+
110+
/** Gets the label of the form-field. */
111+
async getLabel(): Promise<string|null> {
112+
const labelEl = await this._label();
113+
return labelEl ? labelEl.text() : null;
114+
}
115+
116+
/** Whether the form-field has a floating label. */
117+
async hasFloatingLabel(): Promise<boolean> {
118+
return (await this.host()).hasClass('mat-form-field-can-float');
119+
}
120+
121+
/** Whether the label is currently floating. */
122+
async isLabelFloating(): Promise<boolean> {
123+
return (await this.host()).hasClass('mat-form-field-should-float');
124+
}
125+
126+
/** Whether the form-field is disabled. */
127+
async isDisabled(): Promise<boolean> {
128+
return (await this.host()).hasClass('mat-form-field-disabled');
129+
}
130+
131+
/** Whether the form-field is currently autofilled. */
132+
async isAutofilled(): Promise<boolean> {
133+
return (await this.host()).hasClass('mat-form-field-autofilled');
134+
}
135+
136+
/** Gets the theme color of the form-field. */
137+
async getThemeColor(): Promise<'primary'|'accent'|'warn'> {
138+
const hostEl = await this.host();
139+
const [isAccent, isWarn] =
140+
await Promise.all([hostEl.hasClass('mat-accent'), hostEl.hasClass('mat-warn')]);
141+
if (isAccent) {
142+
return 'accent';
143+
} else if (isWarn) {
144+
return 'warn';
145+
}
146+
return 'primary';
147+
}
148+
149+
/** Gets error messages which are currently displayed in the form-field. */
150+
async getErrorMessages(): Promise<string[]> {
151+
return Promise.all((await this._errors()).map(e => e.text()));
152+
}
153+
154+
/** Gets hint messages which are currently displayed in the form-field. */
155+
async getHintMessages(): Promise<string[]> {
156+
return Promise.all((await this._hints()).map(e => e.text()));
157+
}
158+
159+
/**
160+
* Gets a reference to the container element which contains all projected
161+
* prefixes of the form-field.
162+
*/
163+
async getPrefixContainer(): Promise<TestElement|null> {
164+
return this._prefixContainer();
165+
}
166+
167+
/**
168+
* Gets a reference to the container element which contains all projected
169+
* suffixes of the form-field.
170+
*/
171+
async getSuffixContainer(): Promise<TestElement|null> {
172+
return this._suffixContainer();
173+
}
174+
175+
/**
176+
* Whether the form control has been touched. Returns "null"
177+
* if no form control is set up.
178+
*/
179+
async isControlTouched(): Promise<boolean|null> {
180+
if (!await this._hasFormControl()) {
181+
return null;
182+
}
183+
return (await this.host()).hasClass('ng-touched');
184+
}
185+
186+
/**
187+
* Whether the form control is dirty. Returns "null"
188+
* if no form control is set up.
189+
*/
190+
async isControlDirty(): Promise<boolean|null> {
191+
if (!await this._hasFormControl()) {
192+
return null;
193+
}
194+
return (await this.host()).hasClass('ng-dirty');
195+
}
196+
197+
/**
198+
* Whether the form control is valid. Returns "null"
199+
* if no form control is set up.
200+
*/
201+
async isControlValid(): Promise<boolean|null> {
202+
if (!await this._hasFormControl()) {
203+
return null;
204+
}
205+
return (await this.host()).hasClass('ng-valid');
206+
}
207+
208+
/**
209+
* Whether the form control is pending validation. Returns "null"
210+
* if no form control is set up.
211+
*/
212+
async isControlPending(): Promise<boolean|null> {
213+
if (!await this._hasFormControl()) {
214+
return null;
215+
}
216+
return (await this.host()).hasClass('ng-pending');
217+
}
218+
219+
/** Checks whether the form-field control has set up a form control. */
220+
private async _hasFormControl(): Promise<boolean> {
221+
const hostEl = await this.host();
222+
// If no form "NgControl" is bound to the form-field control, the form-field
223+
// is not able to forward any control status classes. Therefore if either the
224+
// "ng-touched" or "ng-untouched" class is set, we know that it has a form control
225+
const [isTouched, isUntouched] =
226+
await Promise.all([hostEl.hasClass('ng-touched'), hostEl.hasClass('ng-untouched')]);
227+
return isTouched || isUntouched;
228+
}
229+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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 * from './public-api';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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 * from './form-field-harness';
10+
export * from './form-field-harness-filters';

0 commit comments

Comments
 (0)