Skip to content

Commit 97eb522

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

File tree

12 files changed

+634
-1
lines changed

12 files changed

+634
-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/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+
":harness_tests_lib",
39+
":testing",
40+
"//src/material-experimental/input/testing",
41+
"//src/material-experimental/mdc-select:harness",
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: 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: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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';
16+
// TODO(devversion): we cannot import the select harness through the module name
17+
// because it does not expose an entry-point that re-exports the harness
18+
import {MatSelectHarness} from '../../mdc-select/harness/select-harness';
19+
import {FormFieldHarnessFilters} from './form-field-harness-filters';
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+
/**
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 _controlHarnessSelector =
51+
[MatInputHarness, MatSelectHarness].map(h => h.hostSelector).join(',');
52+
53+
/** Gets the appearance of the form-field. */
54+
async getAppearance(): Promise<'legacy'|'standard'|'fill'|'outline'> {
55+
const hostEl = await this.host();
56+
const [isLegacy, isStandard, isFill, isOutline] = await Promise.all([
57+
hostEl.hasClass('mat-form-field-appearance-legacy'),
58+
hostEl.hasClass('mat-form-field-appearance-standard'),
59+
hostEl.hasClass('mat-form-field-appearance-fill'),
60+
hostEl.hasClass('mat-form-field-appearance-outline'),
61+
]);
62+
if (isLegacy) {
63+
return 'legacy';
64+
} else if (isStandard) {
65+
return 'standard';
66+
} else if (isFill) {
67+
return 'fill';
68+
} else if (isOutline) {
69+
return 'outline';
70+
}
71+
throw Error('Could not determine appearance of form-field.');
72+
}
73+
74+
/** Gets the harness of the control that is bound to the form-field. */
75+
async getControl(): Promise<FormFieldControlHarness|null> {
76+
const environment: HarnessEnvironment<any> = (this as any).locatorFactory;
77+
// TODO(reviewer): not sure what we should do here. We need to construct a combined
78+
// query for all possible controls. This is necessary because if a form-control contains
79+
// multiple possible control candidates (i.e. a chip-list with matInput). We need to
80+
// find the control that matches first (to simulate the behavior of "@ContentChild").
81+
// The question is, how we can achieve this in a public manner. Should we expose these
82+
// functions? seems reasonable to me.
83+
const elementCandidates: TestElement[] =
84+
(await (environment as any).getAllRawElements(this._controlHarnessSelector))
85+
.map((rawElement: any) => (environment as any).createTestElement(rawElement));
86+
87+
if (!elementCandidates.length) {
88+
return null;
89+
}
90+
91+
const controlElement = elementCandidates[0];
92+
const [isInput, isSelect] = await Promise.all([
93+
controlElement.matchesSelector(MatInputHarness.hostSelector),
94+
controlElement.matchesSelector(MatSelectHarness.hostSelector),
95+
]);
96+
97+
if (isInput) {
98+
return (environment as any).createComponentHarness(MatInputHarness, controlElement);
99+
} else if (isSelect) {
100+
return (environment as any).createComponentHarness(MatSelectHarness, controlElement);
101+
}
102+
// This error will be reported if we add new harnesses to the form-field control
103+
// selector but forget checking for it here.
104+
throw Error('Found control of form-field but could not create harness.');
105+
}
106+
107+
/** Whether the form-field has a label. */
108+
async hasLabel(): Promise<boolean> {
109+
return (await this.host()).hasClass('mat-form-field-has-label');
110+
}
111+
112+
/** Gets the label of the form-field. */
113+
async getLabel(): Promise<string|null> {
114+
const labelEl = await this._label();
115+
return labelEl ? labelEl.text() : null;
116+
}
117+
118+
/** Whether the form-field has a floating label. */
119+
async hasFloatingLabel(): Promise<boolean> {
120+
return (await this.host()).hasClass('mat-form-field-can-float');
121+
}
122+
123+
/** Whether the label is currently floating. */
124+
async isLabelFloating(): Promise<boolean> {
125+
return (await this.host()).hasClass('mat-form-field-should-float');
126+
}
127+
128+
/** Whether the form-field is disabled. */
129+
async isDisabled(): Promise<boolean> {
130+
return (await this.host()).hasClass('mat-form-field-disabled');
131+
}
132+
133+
/** Whether the form-field is currently autofilled. */
134+
async isAutofilled(): Promise<boolean> {
135+
return (await this.host()).hasClass('mat-form-field-autofilled');
136+
}
137+
138+
/** Gets the theme color of the form-field. */
139+
async getThemeColor(): Promise<'primary'|'accent'|'warn'> {
140+
const hostEl = await this.host();
141+
const [isAccent, isWarn] =
142+
await Promise.all([hostEl.hasClass('mat-accent'), hostEl.hasClass('mat-warn')]);
143+
if (isAccent) {
144+
return 'accent';
145+
} else if (isWarn) {
146+
return 'warn';
147+
}
148+
return 'primary';
149+
}
150+
151+
/** Gets error messages which are currently displayed in the form-field. */
152+
async getErrorMessages(): Promise<string[]> {
153+
return Promise.all((await this._errors()).map(e => e.text()));
154+
}
155+
156+
/** Gets hint messages which are currently displayed in the form-field. */
157+
async getHintMessages(): Promise<string[]> {
158+
return Promise.all((await this._hints()).map(e => e.text()));
159+
}
160+
161+
/**
162+
* Gets a reference to the container element which contains all projected
163+
* prefixes of the form-field.
164+
*/
165+
async getPrefixContainer(): Promise<TestElement|null> {
166+
return this._prefixContainer();
167+
}
168+
169+
/**
170+
* Gets a reference to the container element which contains all projected
171+
* suffixes of the form-field.
172+
*/
173+
async getSuffixContainer(): Promise<TestElement|null> {
174+
return this._suffixContainer();
175+
}
176+
177+
/**
178+
* Whether the form control has been touched. Returns "null"
179+
* if no form control is set up.
180+
*/
181+
async isControlTouched(): Promise<boolean|null> {
182+
if (!await this._hasFormControl()) {
183+
return null;
184+
}
185+
return (await this.host()).hasClass('ng-touched');
186+
}
187+
188+
/**
189+
* Whether the form control is dirty. Returns "null"
190+
* if no form control is set up.
191+
*/
192+
async isControlDirty(): Promise<boolean|null> {
193+
if (!await this._hasFormControl()) {
194+
return null;
195+
}
196+
return (await this.host()).hasClass('ng-dirty');
197+
}
198+
199+
/**
200+
* Whether the form control is valid. Returns "null"
201+
* if no form control is set up.
202+
*/
203+
async isControlValid(): Promise<boolean|null> {
204+
if (!await this._hasFormControl()) {
205+
return null;
206+
}
207+
return (await this.host()).hasClass('ng-valid');
208+
}
209+
210+
/**
211+
* Whether the form control is pending validation. Returns "null"
212+
* if no form control is set up.
213+
*/
214+
async isControlPending(): Promise<boolean|null> {
215+
if (!await this._hasFormControl()) {
216+
return null;
217+
}
218+
return (await this.host()).hasClass('ng-pending');
219+
}
220+
221+
/** Checks whether the form-field control has set up a form control. */
222+
private async _hasFormControl(): Promise<boolean> {
223+
const hostEl = await this.host();
224+
// If no form "NgControl" is bound to the form-field control, the form-field
225+
// is not able to forward any control status classes. Therefore if either the
226+
// "ng-touched" or "ng-untouched" class is set, we know that it has a form control
227+
const [isTouched, isUntouched] =
228+
await Promise.all([hostEl.hasClass('ng-touched'), hostEl.hasClass('ng-untouched')]);
229+
return isTouched || isUntouched;
230+
}
231+
}
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)