Skip to content

Commit f864665

Browse files
committed
feat(material-experimental): add test harness for form-field
1 parent f537fba commit f864665

File tree

10 files changed

+623
-1
lines changed

10 files changed

+623
-1
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: 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/select/testing",
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+
":harness_tests_lib",
39+
":testing",
40+
"//src/material-experimental/input/testing",
41+
"//src/material-experimental/select/testing",
42+
"//src/material/autocomplete",
43+
"//src/material/form-field",
44+
"//src/material/input",
45+
"//src/material/select",
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: 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: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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 {MatInputHarness} from '@angular/material-experimental/input/testing';
16+
import {MatSelectHarness} from '@angular/material-experimental/select/testing';
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+
* Base class for custom form-field control harnesses. Harnesses for
26+
* custom controls with form-fields need to implement this interface.
27+
*/
28+
export interface CustomFormFieldControlHarness extends ComponentHarness {}
29+
30+
/**
31+
* Harness for interacting with a standard Material form-field's in tests.
32+
* @dynamic
33+
*/
34+
export class MatFormFieldHarness extends ComponentHarness {
35+
static hostSelector = '.mat-form-field';
36+
37+
/**
38+
* Gets a `HarnessPredicate` that can be used to search for an form-field with
39+
* specific attributes.
40+
* @param options Options for narrowing the search:
41+
* - `selector` finds a form-field that matches the given selector.
42+
* @return a `HarnessPredicate` configured with the given options.
43+
*/
44+
static with(options: FormFieldHarnessFilters = {}): HarnessPredicate<MatFormFieldHarness> {
45+
return new HarnessPredicate(MatFormFieldHarness, options);
46+
}
47+
48+
private _prefixContainer = this.locatorForOptional('.mat-form-field-prefix');
49+
private _suffixContainer = this.locatorForOptional('.mat-form-field-suffix');
50+
private _label = this.locatorForOptional('.mat-form-field-label');
51+
private _errors = this.locatorForAll('.mat-error');
52+
private _hints = this.locatorForAll('mat-hint,.mat-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<'legacy'|'standard'|'fill'|'outline'> {
59+
const hostClasses = await (await this.host()).getAttribute('class');
60+
if (hostClasses !== null) {
61+
const appearanceMatch =
62+
hostClasses.match(/mat-form-field-appearance-(legacy|standard|fill|outline)(?:$| )/);
63+
if (appearanceMatch) {
64+
return appearanceMatch[1] as 'legacy' | 'standard' | 'fill' | 'outline';
65+
}
66+
}
67+
throw Error('Could not determine appearance of form-field.');
68+
}
69+
70+
/**
71+
* Gets the harness of the control that is bound to the form-field. Only
72+
* default controls such as "MatInputHarness" and "MatSelectHarness" are
73+
* supported.
74+
*/
75+
async getControl(): Promise<FormFieldControlHarness|null>;
76+
77+
/**
78+
* Gets the harness of the control that is bound to the form-field. Searches
79+
* for a control that matches the specified harness type.
80+
*/
81+
async getControl<X extends CustomFormFieldControlHarness>(type: ComponentHarnessConstructor<X>):
82+
Promise<X|null>;
83+
84+
// Implementation of the "getControl" method overload signatures.
85+
async getControl<X extends CustomFormFieldControlHarness>(type?: ComponentHarnessConstructor<X>) {
86+
if (type) {
87+
return this.locatorForOptional(type)();
88+
}
89+
const hostEl = await this.host();
90+
const [isInput, isSelect] = await Promise.all([
91+
hostEl.hasClass('mat-form-field-type-mat-input'),
92+
hostEl.hasClass('mat-form-field-type-mat-select'),
93+
]);
94+
if (isInput) {
95+
return this._inputControl();
96+
} else if (isSelect) {
97+
return this._selectControl();
98+
}
99+
return null;
100+
}
101+
102+
/** Whether the form-field has a label. */
103+
async hasLabel(): Promise<boolean> {
104+
return (await this.host()).hasClass('mat-form-field-has-label');
105+
}
106+
107+
/** Gets the label of the form-field. */
108+
async getLabel(): Promise<string|null> {
109+
const labelEl = await this._label();
110+
return labelEl ? labelEl.text() : null;
111+
}
112+
113+
/** Whether the form-field has a floating label. */
114+
async hasFloatingLabel(): Promise<boolean> {
115+
return (await this.host()).hasClass('mat-form-field-can-float');
116+
}
117+
118+
/** Whether the label is currently floating. */
119+
async isLabelFloating(): Promise<boolean> {
120+
return (await this.host()).hasClass('mat-form-field-should-float');
121+
}
122+
123+
/** Whether the form-field is disabled. */
124+
async isDisabled(): Promise<boolean> {
125+
return (await this.host()).hasClass('mat-form-field-disabled');
126+
}
127+
128+
/** Whether the form-field is currently autofilled. */
129+
async isAutofilled(): Promise<boolean> {
130+
return (await this.host()).hasClass('mat-form-field-autofilled');
131+
}
132+
133+
/** Gets the theme color of the form-field. */
134+
async getThemeColor(): Promise<'primary'|'accent'|'warn'> {
135+
const hostEl = await this.host();
136+
const [isAccent, isWarn] =
137+
await Promise.all([hostEl.hasClass('mat-accent'), hostEl.hasClass('mat-warn')]);
138+
if (isAccent) {
139+
return 'accent';
140+
} else if (isWarn) {
141+
return 'warn';
142+
}
143+
return 'primary';
144+
}
145+
146+
/** Gets error messages which are currently displayed in the form-field. */
147+
async getErrorMessages(): Promise<string[]> {
148+
return Promise.all((await this._errors()).map(e => e.text()));
149+
}
150+
151+
/** Gets hint messages which are currently displayed in the form-field. */
152+
async getHintMessages(): Promise<string[]> {
153+
return Promise.all((await this._hints()).map(e => e.text()));
154+
}
155+
156+
/**
157+
* Gets a reference to the container element which contains all projected
158+
* prefixes of the form-field.
159+
*/
160+
async getPrefixContainer(): Promise<TestElement|null> {
161+
return this._prefixContainer();
162+
}
163+
164+
/**
165+
* Gets a reference to the container element which contains all projected
166+
* suffixes of the form-field.
167+
*/
168+
async getSuffixContainer(): Promise<TestElement|null> {
169+
return this._suffixContainer();
170+
}
171+
172+
/**
173+
* Whether the form control has been touched. Returns "null"
174+
* if no form control is set up.
175+
*/
176+
async isControlTouched(): Promise<boolean|null> {
177+
if (!await this._hasFormControl()) {
178+
return null;
179+
}
180+
return (await this.host()).hasClass('ng-touched');
181+
}
182+
183+
/**
184+
* Whether the form control is dirty. Returns "null"
185+
* if no form control is set up.
186+
*/
187+
async isControlDirty(): Promise<boolean|null> {
188+
if (!await this._hasFormControl()) {
189+
return null;
190+
}
191+
return (await this.host()).hasClass('ng-dirty');
192+
}
193+
194+
/**
195+
* Whether the form control is valid. Returns "null"
196+
* if no form control is set up.
197+
*/
198+
async isControlValid(): Promise<boolean|null> {
199+
if (!await this._hasFormControl()) {
200+
return null;
201+
}
202+
return (await this.host()).hasClass('ng-valid');
203+
}
204+
205+
/**
206+
* Whether the form control is pending validation. Returns "null"
207+
* if no form control is set up.
208+
*/
209+
async isControlPending(): Promise<boolean|null> {
210+
if (!await this._hasFormControl()) {
211+
return null;
212+
}
213+
return (await this.host()).hasClass('ng-pending');
214+
}
215+
216+
/** Checks whether the form-field control has set up a form control. */
217+
private async _hasFormControl(): Promise<boolean> {
218+
const hostEl = await this.host();
219+
// If no form "NgControl" is bound to the form-field control, the form-field
220+
// is not able to forward any control status classes. Therefore if either the
221+
// "ng-touched" or "ng-untouched" class is set, we know that it has a form control
222+
const [isTouched, isUntouched] =
223+
await Promise.all([hostEl.hasClass('ng-touched'), hostEl.hasClass('ng-untouched')]);
224+
return isTouched || isUntouched;
225+
}
226+
}
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)