Skip to content

Commit 7464d4b

Browse files
devversionjelbourn
authored andcommitted
feat(material-experimental/form-field): add test harness (#17138)
1 parent 2112fac commit 7464d4b

17 files changed

+690
-5
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
# Material experimental package
9191
/src/material-experimental/* @jelbourn
9292
/src/material-experimental/input/** @devversion
93+
/src/material-experimental/form-field/** @devversion
9394
/src/material-experimental/mdc-autocomplete/** @crisbeto
9495
/src/material-experimental/mdc-button/** @andrewseguin
9596
/src/material-experimental/mdc-card/** @mmalerba
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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/form-field/testing",
12+
deps = [
13+
"//src/cdk/testing",
14+
"//src/material-experimental/form-field/testing/control",
15+
"//src/material-experimental/input/testing",
16+
"//src/material-experimental/select/testing",
17+
],
18+
)
19+
20+
ng_test_library(
21+
name = "harness_tests_lib",
22+
srcs = ["shared.spec.ts"],
23+
deps = [
24+
":testing",
25+
"//src/cdk/testing",
26+
"//src/cdk/testing/testbed",
27+
"@npm//@angular/forms",
28+
"@npm//@angular/platform-browser",
29+
],
30+
)
31+
32+
ng_test_library(
33+
name = "unit_tests_lib",
34+
srcs = glob(
35+
["**/*.spec.ts"],
36+
exclude = ["shared.spec.ts"],
37+
),
38+
deps = [
39+
":harness_tests_lib",
40+
":testing",
41+
"//src/material-experimental/input/testing",
42+
"//src/material-experimental/select/testing",
43+
"//src/material/autocomplete",
44+
"//src/material/form-field",
45+
"//src/material/input",
46+
"//src/material/select",
47+
],
48+
)
49+
50+
ng_web_test_suite(
51+
name = "unit_tests",
52+
deps = [":unit_tests_lib"],
53+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ts_library")
4+
5+
ts_library(
6+
name = "control",
7+
srcs = glob(["**/*.ts"]),
8+
module_name = "@angular/material-experimental/form-field/testing/control",
9+
deps = ["//src/cdk/testing"],
10+
)
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+
import {ComponentHarness} from '@angular/cdk/testing';
10+
11+
/**
12+
* Base class for custom form-field control harnesses. Harnesses for
13+
* custom controls with form-fields need to implement this interface.
14+
*/
15+
export abstract class MatFormFieldControlHarness extends ComponentHarness {}
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 './form-field-control-harness';
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {MatInputHarness} from '@angular/material-experimental/input/testing';
2+
import {MatSelectHarness} from '@angular/material-experimental/select/testing';
3+
import {MatAutocompleteModule} from '@angular/material/autocomplete';
4+
import {MatFormFieldModule} from '@angular/material/form-field';
5+
import {MatInputModule} from '@angular/material/input';
6+
import {MatSelectModule} from '@angular/material/select';
7+
8+
import {MatFormFieldHarness} from './form-field-harness';
9+
import {runHarnessTests} from './shared.spec';
10+
11+
describe('Non-MDC-based MatFormFieldHarness', () => {
12+
runHarnessTests(
13+
[MatFormFieldModule, MatAutocompleteModule, MatInputModule, MatSelectModule],
14+
MatFormFieldHarness, MatInputHarness, MatSelectHarness);
15+
});
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+
TestElement
14+
} from '@angular/cdk/testing';
15+
import {
16+
MatFormFieldControlHarness
17+
} from '@angular/material-experimental/form-field/testing/control';
18+
import {MatInputHarness} from '@angular/material-experimental/input/testing';
19+
import {MatSelectHarness} from '@angular/material-experimental/select/testing';
20+
import {FormFieldHarnessFilters} from './form-field-harness-filters';
21+
22+
// TODO(devversion): support datepicker harness once developed (COMP-203).
23+
// Also support chip list harness.
24+
/** Possible harnesses of controls which can be bound to a form-field. */
25+
export type FormFieldControlHarness = MatInputHarness|MatSelectHarness;
26+
/**
27+
* Harness for interacting with a standard Material form-field's in tests.
28+
* @dynamic
29+
*/
30+
export class MatFormFieldHarness extends ComponentHarness {
31+
static hostSelector = '.mat-form-field';
32+
33+
/**
34+
* Gets a `HarnessPredicate` that can be used to search for an form-field with
35+
* specific attributes.
36+
* @param options Options for narrowing the search:
37+
* - `selector` finds a form-field that matches the given selector.
38+
* @return a `HarnessPredicate` configured with the given options.
39+
*/
40+
static with(options: FormFieldHarnessFilters = {}): HarnessPredicate<MatFormFieldHarness> {
41+
return new HarnessPredicate(MatFormFieldHarness, options);
42+
}
43+
44+
private _prefixContainer = this.locatorForOptional('.mat-form-field-prefix');
45+
private _suffixContainer = this.locatorForOptional('.mat-form-field-suffix');
46+
private _label = this.locatorForOptional('.mat-form-field-label');
47+
private _errors = this.locatorForAll('.mat-error');
48+
private _hints = this.locatorForAll('mat-hint,.mat-hint');
49+
50+
private _inputControl = this.locatorForOptional(MatInputHarness);
51+
private _selectControl = this.locatorForOptional(MatSelectHarness);
52+
53+
/** Gets the appearance of the form-field. */
54+
async getAppearance(): Promise<'legacy'|'standard'|'fill'|'outline'> {
55+
const hostClasses = await (await this.host()).getAttribute('class');
56+
if (hostClasses !== null) {
57+
const appearanceMatch =
58+
hostClasses.match(/mat-form-field-appearance-(legacy|standard|fill|outline)(?:$| )/);
59+
if (appearanceMatch) {
60+
return appearanceMatch[1] as 'legacy' | 'standard' | 'fill' | 'outline';
61+
}
62+
}
63+
throw Error('Could not determine appearance of form-field.');
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?: ComponentHarnessConstructor<X>|
89+
HarnessPredicate<X>) {
90+
if (type) {
91+
return this.locatorForOptional(type)();
92+
}
93+
const hostEl = await this.host();
94+
const [isInput, isSelect] = await Promise.all([
95+
hostEl.hasClass('mat-form-field-type-mat-input'),
96+
hostEl.hasClass('mat-form-field-type-mat-select'),
97+
]);
98+
if (isInput) {
99+
return this._inputControl();
100+
} else if (isSelect) {
101+
return this._selectControl();
102+
}
103+
return null;
104+
}
105+
106+
/** Whether the form-field has a label. */
107+
async hasLabel(): Promise<boolean> {
108+
return (await this.host()).hasClass('mat-form-field-has-label');
109+
}
110+
111+
/** Gets the label of the form-field. */
112+
async getLabel(): Promise<string|null> {
113+
const labelEl = await this._label();
114+
return labelEl ? labelEl.text() : null;
115+
}
116+
117+
/** Whether the form-field has a floating label. */
118+
async hasFloatingLabel(): Promise<boolean> {
119+
return (await this.host()).hasClass('mat-form-field-can-float');
120+
}
121+
122+
/** Whether the label is currently floating. */
123+
async isLabelFloating(): Promise<boolean> {
124+
return (await this.host()).hasClass('mat-form-field-should-float');
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 getErrorMessages(): 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 getHintMessages(): 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 getPrefixContainer(): 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 getSuffixContainer(): 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-experimental/form-field/testing/control';
13+
14+
export * from './form-field-harness';
15+
export * from './form-field-harness-filters';

0 commit comments

Comments
 (0)