Skip to content

Commit 944837f

Browse files
devversionjelbourn
authored andcommitted
feat(material-experimental/mdc-form-field): add test harness (#18165)
1 parent 1a53f03 commit 944837f

File tree

10 files changed

+408
-43
lines changed

10 files changed

+408
-43
lines changed

src/material-experimental/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ entryPoints = [
88
"mdc-chips",
99
"mdc-chips/testing",
1010
"mdc-form-field",
11+
"mdc-form-field/testing",
1112
"mdc-input",
1213
"mdc-list",
1314
"mdc-menu",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 = "testing",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/material-experimental/mdc-form-field/testing",
12+
deps = [
13+
"//src/cdk/testing",
14+
"//src/material/form-field/testing",
15+
"//src/material/form-field/testing/control",
16+
"//src/material/input/testing",
17+
"//src/material/select/testing",
18+
],
19+
)
20+
21+
filegroup(
22+
name = "source-files",
23+
srcs = glob(["**/*.ts"]),
24+
)
25+
26+
ng_test_library(
27+
name = "unit_tests_lib",
28+
srcs = glob(
29+
["**/*.spec.ts"],
30+
exclude = ["shared.spec.ts"],
31+
),
32+
deps = [
33+
":testing",
34+
"//src/cdk/overlay",
35+
"//src/material-experimental/mdc-form-field",
36+
"//src/material-experimental/mdc-input",
37+
"//src/material/autocomplete",
38+
"//src/material/core",
39+
"//src/material/form-field/testing:harness_tests_lib",
40+
"//src/material/input/testing",
41+
"//src/material/select",
42+
"//src/material/select/testing",
43+
"@npm//@angular/common",
44+
],
45+
)
46+
47+
ng_web_test_suite(
48+
name = "unit_tests",
49+
static_files = [
50+
"@npm//:node_modules/@material/textfield/dist/mdc.textfield.js",
51+
"@npm//:node_modules/@material/line-ripple/dist/mdc.lineRipple.js",
52+
"@npm//:node_modules/@material/notched-outline/dist/mdc.notchedOutline.js",
53+
"@npm//:node_modules/@material/floating-label/dist/mdc.floatingLabel.js",
54+
],
55+
deps = [
56+
":unit_tests_lib",
57+
"//src/material-experimental:mdc_require_config.js",
58+
],
59+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {OverlayModule} from '@angular/cdk/overlay';
2+
import {CommonModule} from '@angular/common';
3+
import {NgModule} from '@angular/core';
4+
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
5+
import {MatInputModule} from '@angular/material-experimental/mdc-input';
6+
import {MatAutocompleteModule} from '@angular/material/autocomplete';
7+
import {MatCommonModule, MatOptionModule} from '@angular/material/core';
8+
import {MatInputHarness} from '@angular/material/input/testing';
9+
import {
10+
MAT_SELECT_SCROLL_STRATEGY_PROVIDER,
11+
MatSelect,
12+
MatSelectTrigger
13+
} from '@angular/material/select';
14+
import {MatSelectHarness} from '@angular/material/select/testing';
15+
import {runHarnessTests} from '@angular/material/form-field/testing/shared.spec';
16+
import {MatFormFieldHarness} from './form-field-harness';
17+
18+
// TODO: remove this once there is a `MatSelect` module which does not come
19+
// with the form-field module provided. This is a copy of the `MatSelect` module
20+
// that does not provide any form-field module.
21+
@NgModule({
22+
imports: [CommonModule, OverlayModule, MatOptionModule, MatCommonModule],
23+
exports: [MatSelect, MatSelectTrigger, MatOptionModule, MatCommonModule],
24+
declarations: [MatSelect, MatSelectTrigger],
25+
providers: [MAT_SELECT_SCROLL_STRATEGY_PROVIDER]
26+
})
27+
export class SelectWithoutFormFieldModule {
28+
}
29+
30+
describe('MDC-based MatFormFieldHarness', () => {
31+
runHarnessTests(
32+
[MatFormFieldModule, MatAutocompleteModule, MatInputModule, SelectWithoutFormFieldModule], {
33+
formFieldHarness: MatFormFieldHarness as any,
34+
inputHarness: MatInputHarness,
35+
selectHarness: MatSelectHarness,
36+
isMdcImplementation: true,
37+
});
38+
});
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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+
ComponentHarnessConstructor,
12+
HarnessPredicate,
13+
HarnessQuery,
14+
TestElement
15+
} from '@angular/cdk/testing';
16+
import {FormFieldHarnessFilters} from '@angular/material/form-field/testing';
17+
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
18+
import {MatInputHarness} from '@angular/material/input/testing';
19+
import {MatSelectHarness} from '@angular/material/select/testing';
20+
21+
// TODO(devversion): support datepicker harness once developed (COMP-203).
22+
// Also support chip list harness.
23+
/** Possible harnesses of controls which can be bound to a form-field. */
24+
export type FormFieldControlHarness = MatInputHarness|MatSelectHarness;
25+
26+
/** Harness for interacting with a MDC-based form-field's in tests. */
27+
export class MatFormFieldHarness extends ComponentHarness {
28+
static hostSelector = '.mat-mdc-form-field';
29+
30+
/**
31+
* Gets a `HarnessPredicate` that can be used to search for a `MatFormFieldHarness` that meets
32+
* certain criteria.
33+
* @param options Options for filtering which form field instances are considered a match.
34+
* @return a `HarnessPredicate` configured with the given options.
35+
*/
36+
static with(options: FormFieldHarnessFilters = {}): HarnessPredicate<MatFormFieldHarness> {
37+
return new HarnessPredicate(MatFormFieldHarness, options)
38+
.addOption(
39+
'floatingLabelText', options.floatingLabelText,
40+
async (harness, text) => HarnessPredicate.stringMatches(await harness.getLabel(), text))
41+
.addOption(
42+
'hasErrors', options.hasErrors,
43+
async (harness, hasErrors) => await harness.hasErrors() === hasErrors);
44+
}
45+
46+
private _mdcTextField = this.locatorFor('.mat-mdc-text-field-wrapper');
47+
48+
private _prefixContainer = this.locatorForOptional('.mat-mdc-form-field-prefix');
49+
private _suffixContainer = this.locatorForOptional('.mat-mdc-form-field-suffix');
50+
private _label = this.locatorForOptional('.mdc-floating-label');
51+
private _errors = this.locatorForAll('.mat-mdc-form-field-error');
52+
private _hints = this.locatorForAll('.mat-mdc-form-field-hint');
53+
54+
private _inputControl = this.locatorForOptional(MatInputHarness);
55+
private _selectControl = this.locatorForOptional(MatSelectHarness);
56+
57+
/** Gets the appearance of the form-field. */
58+
async getAppearance(): Promise<'fill'|'outline'> {
59+
const textFieldEl = await this._mdcTextField();
60+
if (await textFieldEl.hasClass('mdc-text-field--outlined')) {
61+
return 'outline';
62+
}
63+
return 'fill';
64+
}
65+
66+
/**
67+
* Gets the harness of the control that is bound to the form-field. Only
68+
* default controls such as "MatInputHarness" and "MatSelectHarness" are
69+
* supported.
70+
*/
71+
async getControl(): Promise<FormFieldControlHarness|null>;
72+
73+
/**
74+
* Gets the harness of the control that is bound to the form-field. Searches
75+
* for a control that matches the specified harness type.
76+
*/
77+
async getControl<X extends MatFormFieldControlHarness>(type: ComponentHarnessConstructor<X>):
78+
Promise<X|null>;
79+
80+
/**
81+
* Gets the harness of the control that is bound to the form-field. Searches
82+
* for a control that matches the specified harness predicate.
83+
*/
84+
async getControl<X extends MatFormFieldControlHarness>(type: HarnessPredicate<X>):
85+
Promise<X|null>;
86+
87+
// Implementation of the "getControl" method overload signatures.
88+
async getControl<X extends MatFormFieldControlHarness>(type?: HarnessQuery<X>) {
89+
if (type) {
90+
return this.locatorForOptional(type)();
91+
}
92+
const hostEl = await this.host();
93+
const [isInput, isSelect] = await Promise.all([
94+
hostEl.hasClass('mat-mdc-form-field-type-mat-input'),
95+
hostEl.hasClass('mat-mdc-form-field-type-mat-select'),
96+
]);
97+
if (isInput) {
98+
return this._inputControl();
99+
} else if (isSelect) {
100+
return this._selectControl();
101+
}
102+
return null;
103+
}
104+
105+
/** Whether the form-field has a label. */
106+
async hasLabel(): Promise<boolean> {
107+
return (await this._label()) !== null;
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 errors. */
117+
async hasErrors(): Promise<boolean> {
118+
return (await this.getTextErrors()).length > 0;
119+
}
120+
121+
/** Whether the label is currently floating. */
122+
async isLabelFloating(): Promise<boolean> {
123+
const labelEl = await this._label();
124+
return labelEl !== null ? await labelEl.hasClass('mdc-floating-label--float-above') : false;
125+
}
126+
127+
/** Whether the form-field is disabled. */
128+
async isDisabled(): Promise<boolean> {
129+
return (await this.host()).hasClass('mat-form-field-disabled');
130+
}
131+
132+
/** Whether the form-field is currently autofilled. */
133+
async isAutofilled(): Promise<boolean> {
134+
return (await this.host()).hasClass('mat-form-field-autofilled');
135+
}
136+
137+
/** Gets the theme color of the form-field. */
138+
async getThemeColor(): Promise<'primary'|'accent'|'warn'> {
139+
const hostEl = await this.host();
140+
const [isAccent, isWarn] =
141+
await Promise.all([hostEl.hasClass('mat-accent'), hostEl.hasClass('mat-warn')]);
142+
if (isAccent) {
143+
return 'accent';
144+
} else if (isWarn) {
145+
return 'warn';
146+
}
147+
return 'primary';
148+
}
149+
150+
/** Gets error messages which are currently displayed in the form-field. */
151+
async getTextErrors(): Promise<string[]> {
152+
return Promise.all((await this._errors()).map(e => e.text()));
153+
}
154+
155+
/** Gets hint messages which are currently displayed in the form-field. */
156+
async getTextHints(): Promise<string[]> {
157+
return Promise.all((await this._hints()).map(e => e.text()));
158+
}
159+
160+
/**
161+
* Gets a reference to the container element which contains all projected
162+
* prefixes of the form-field.
163+
*/
164+
async getHarnessLoaderForPrefix(): Promise<TestElement|null> {
165+
return this._prefixContainer();
166+
}
167+
168+
/**
169+
* Gets a reference to the container element which contains all projected
170+
* suffixes of the form-field.
171+
*/
172+
async getHarnessLoaderForSuffix(): Promise<TestElement|null> {
173+
return this._suffixContainer();
174+
}
175+
176+
/**
177+
* Whether the form control has been touched. Returns "null"
178+
* if no form control is set up.
179+
*/
180+
async isControlTouched(): Promise<boolean|null> {
181+
if (!await this._hasFormControl()) {
182+
return null;
183+
}
184+
return (await this.host()).hasClass('ng-touched');
185+
}
186+
187+
/**
188+
* Whether the form control is dirty. Returns "null"
189+
* if no form control is set up.
190+
*/
191+
async isControlDirty(): Promise<boolean|null> {
192+
if (!await this._hasFormControl()) {
193+
return null;
194+
}
195+
return (await this.host()).hasClass('ng-dirty');
196+
}
197+
198+
/**
199+
* Whether the form control is valid. Returns "null"
200+
* if no form control is set up.
201+
*/
202+
async isControlValid(): Promise<boolean|null> {
203+
if (!await this._hasFormControl()) {
204+
return null;
205+
}
206+
return (await this.host()).hasClass('ng-valid');
207+
}
208+
209+
/**
210+
* Whether the form control is pending validation. Returns "null"
211+
* if no form control is set up.
212+
*/
213+
async isControlPending(): Promise<boolean|null> {
214+
if (!await this._hasFormControl()) {
215+
return null;
216+
}
217+
return (await this.host()).hasClass('ng-pending');
218+
}
219+
220+
/** Checks whether the form-field control has set up a form control. */
221+
private async _hasFormControl(): Promise<boolean> {
222+
const hostEl = await this.host();
223+
// If no form "NgControl" is bound to the form-field control, the form-field
224+
// is not able to forward any control status classes. Therefore if either the
225+
// "ng-touched" or "ng-untouched" class is set, we know that it has a form control
226+
const [isTouched, isUntouched] =
227+
await Promise.all([hostEl.hasClass('ng-touched'), hostEl.hasClass('ng-untouched')]);
228+
return isTouched || isUntouched;
229+
}
230+
}
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
// Re-export everything from the "form-field/testing/control" entry-point. To avoid
10+
// circular dependencies, harnesses for default form-field controls (i.e. input, select)
11+
// need to import the base form-field control harness through a separate entry-point.
12+
export * from '@angular/material/form-field/testing/control';
13+
14+
export {FormFieldHarnessFilters} from '@angular/material/form-field/testing';
15+
export * from './form-field-harness';
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
import {MatInputHarness} from '@angular/material/input/testing';
2-
import {MatSelectHarness} from '@angular/material/select/testing';
31
import {MatAutocompleteModule} from '@angular/material/autocomplete';
42
import {MatFormFieldModule} from '@angular/material/form-field';
53
import {MatInputModule} from '@angular/material/input';
4+
import {MatInputHarness} from '@angular/material/input/testing';
65
import {MatSelectModule} from '@angular/material/select';
6+
import {MatSelectHarness} from '@angular/material/select/testing';
77

88
import {MatFormFieldHarness} from './form-field-harness';
99
import {runHarnessTests} from './shared.spec';
1010

1111
describe('Non-MDC-based MatFormFieldHarness', () => {
12-
runHarnessTests(
13-
[MatFormFieldModule, MatAutocompleteModule, MatInputModule, MatSelectModule],
14-
MatFormFieldHarness, MatInputHarness, MatSelectHarness);
12+
runHarnessTests([MatFormFieldModule, MatAutocompleteModule, MatInputModule, MatSelectModule], {
13+
formFieldHarness: MatFormFieldHarness,
14+
inputHarness: MatInputHarness,
15+
selectHarness: MatSelectHarness,
16+
isMdcImplementation: false,
17+
});
1518
});

0 commit comments

Comments
 (0)