Skip to content

Commit 36af2a3

Browse files
authored
feat(material/form-field): add error harness (#25698)
* feat(material/form-field): add error harness * feat(material/form-field): deprecate legacy error harness * fix(material/form-field): use error harness for all getters
1 parent 8e9625e commit 36af2a3

File tree

12 files changed

+217
-11
lines changed

12 files changed

+217
-11
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
BaseHarnessFilters,
11+
ComponentHarness,
12+
ComponentHarnessConstructor,
13+
HarnessPredicate,
14+
} from '@angular/cdk/testing';
15+
16+
/** A set of criteria that can be used to filter a list of error harness instances. */
17+
export interface ErrorHarnessFilters extends BaseHarnessFilters {
18+
/** Only find instances whose text matches the given value. */
19+
text?: string | RegExp;
20+
}
21+
22+
export abstract class _MatErrorHarnessBase extends ComponentHarness {
23+
/** Gets a promise for the error's label text. */
24+
async getText(): Promise<string> {
25+
return (await this.host()).text();
26+
}
27+
28+
protected static _getErrorPredicate<T extends MatErrorHarness>(
29+
type: ComponentHarnessConstructor<T>,
30+
options: ErrorHarnessFilters,
31+
): HarnessPredicate<T> {
32+
return new HarnessPredicate(type, options).addOption('text', options.text, (harness, text) =>
33+
HarnessPredicate.stringMatches(harness.getText(), text),
34+
);
35+
}
36+
}
37+
38+
/** Harness for interacting with an MDC-based `mat-error` in tests. */
39+
export class MatErrorHarness extends _MatErrorHarnessBase {
40+
static hostSelector = '.mat-mdc-form-field-error';
41+
42+
/**
43+
* Gets a `HarnessPredicate` that can be used to search for an error with specific
44+
* attributes.
45+
* @param options Options for filtering which error instances are considered a match.
46+
* @return a `HarnessPredicate` configured with the given options.
47+
*/
48+
static with<T extends MatErrorHarness>(
49+
this: ComponentHarnessConstructor<T>,
50+
options: ErrorHarnessFilters = {},
51+
): HarnessPredicate<T> {
52+
return _MatErrorHarnessBase._getErrorPredicate(this, options);
53+
}
54+
}

src/material/form-field/testing/form-field-harness.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {MatFormFieldModule} from '@angular/material/form-field';
2+
import {MatErrorHarness} from './error-harness';
23
import {MatInputModule} from '@angular/material/input';
34
import {MatAutocompleteModule} from '@angular/material/autocomplete';
45
import {MatInputHarness} from '@angular/material/input/testing';
@@ -30,6 +31,7 @@ describe('MDC-based MatFormFieldHarness', () => {
3031
datepickerInputHarness: MatDatepickerInputHarness,
3132
dateRangeInputHarness: MatDateRangeInputHarness,
3233
isMdcImplementation: true,
34+
errorHarness: MatErrorHarness,
3335
},
3436
);
3537
});

src/material/form-field/testing/form-field-harness.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
parallel,
1616
TestElement,
1717
} from '@angular/cdk/testing';
18+
import {ErrorHarnessFilters, MatErrorHarness} from './error-harness';
1819
import {MatInputHarness} from '@angular/material/input/testing';
1920
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
2021
import {MatSelectHarness} from '@angular/material/select/testing';
@@ -24,18 +25,25 @@ import {
2425
} from '@angular/material/datepicker/testing';
2526
import {FormFieldHarnessFilters} from './form-field-harness-filters';
2627

28+
interface ErrorBase extends ComponentHarness {
29+
getText(): Promise<string>;
30+
}
31+
2732
export abstract class _MatFormFieldHarnessBase<
2833
ControlHarness extends MatFormFieldControlHarness,
34+
ErrorType extends ComponentHarnessConstructor<ErrorBase> & {
35+
with: (options?: ErrorHarnessFilters) => HarnessPredicate<ErrorBase>;
36+
},
2937
> extends ComponentHarness {
3038
protected abstract _prefixContainer: AsyncFactoryFn<TestElement | null>;
3139
protected abstract _suffixContainer: AsyncFactoryFn<TestElement | null>;
3240
protected abstract _label: AsyncFactoryFn<TestElement | null>;
33-
protected abstract _errors: AsyncFactoryFn<TestElement[]>;
3441
protected abstract _hints: AsyncFactoryFn<TestElement[]>;
3542
protected abstract _inputControl: AsyncFactoryFn<ControlHarness | null>;
3643
protected abstract _selectControl: AsyncFactoryFn<ControlHarness | null>;
3744
protected abstract _datepickerInputControl: AsyncFactoryFn<ControlHarness | null>;
3845
protected abstract _dateRangeInputControl: AsyncFactoryFn<ControlHarness | null>;
46+
protected abstract _errorHarness: ErrorType;
3947

4048
/** Gets the appearance of the form-field. */
4149
abstract getAppearance(): Promise<string>;
@@ -122,8 +130,13 @@ export abstract class _MatFormFieldHarnessBase<
122130

123131
/** Gets error messages which are currently displayed in the form-field. */
124132
async getTextErrors(): Promise<string[]> {
125-
const errors = await this._errors();
126-
return parallel(() => errors.map(e => e.text()));
133+
const errors = await this.getErrors();
134+
return parallel(() => errors.map(e => e.getText()));
135+
}
136+
137+
/** Gets all of the error harnesses in the form field. */
138+
async getErrors(filter: ErrorHarnessFilters = {}): Promise<MatErrorHarness[]> {
139+
return this.locatorForAll(this._errorHarness.with(filter))();
127140
}
128141

129142
/** Gets hint messages which are currently displayed in the form-field. */
@@ -211,7 +224,10 @@ export type FormFieldControlHarness =
211224
| MatDateRangeInputHarness;
212225

213226
/** Harness for interacting with a MDC-based form-field's in tests. */
214-
export class MatFormFieldHarness extends _MatFormFieldHarnessBase<FormFieldControlHarness> {
227+
export class MatFormFieldHarness extends _MatFormFieldHarnessBase<
228+
FormFieldControlHarness,
229+
typeof MatErrorHarness
230+
> {
215231
static hostSelector = '.mat-mdc-form-field';
216232

217233
/**
@@ -238,12 +254,12 @@ export class MatFormFieldHarness extends _MatFormFieldHarnessBase<FormFieldContr
238254
protected _prefixContainer = this.locatorForOptional('.mat-mdc-form-field-text-prefix');
239255
protected _suffixContainer = this.locatorForOptional('.mat-mdc-form-field-text-suffix');
240256
protected _label = this.locatorForOptional('.mdc-floating-label');
241-
protected _errors = this.locatorForAll('.mat-mdc-form-field-error');
242257
protected _hints = this.locatorForAll('.mat-mdc-form-field-hint');
243258
protected _inputControl = this.locatorForOptional(MatInputHarness);
244259
protected _selectControl = this.locatorForOptional(MatSelectHarness);
245260
protected _datepickerInputControl = this.locatorForOptional(MatDatepickerInputHarness);
246261
protected _dateRangeInputControl = this.locatorForOptional(MatDateRangeInputHarness);
262+
protected _errorHarness = MatErrorHarness;
247263
private _mdcTextField = this.locatorFor('.mat-mdc-text-field-wrapper');
248264

249265
/** Gets the appearance of the form-field. */

src/material/form-field/testing/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export {MatFormFieldControlHarness} from '@angular/material/form-field/testing/c
1313

1414
export * from './form-field-harness-filters';
1515
export * from './form-field-harness';
16+
export * from './error-harness';

src/material/form-field/testing/shared.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {ComponentHarness, HarnessLoader, HarnessPredicate, parallel} from '@angular/cdk/testing';
2+
import {MatErrorHarness} from './error-harness';
23
import {createFakeEvent, dispatchFakeEvent} from '../../../cdk/testing/private';
34
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
45
import {Component, Type} from '@angular/core';
@@ -17,13 +18,15 @@ export function runHarnessTests(
1718
datepickerInputHarness,
1819
dateRangeInputHarness,
1920
isMdcImplementation,
21+
errorHarness,
2022
}: {
2123
formFieldHarness: typeof MatFormFieldHarness;
2224
inputHarness: Type<any>;
2325
selectHarness: Type<any>;
2426
datepickerInputHarness: Type<any>;
2527
dateRangeInputHarness: Type<any>;
2628
isMdcImplementation: boolean;
29+
errorHarness: typeof MatErrorHarness;
2730
},
2831
) {
2932
let fixture: ComponentFixture<FormFieldHarnessTest>;
@@ -194,6 +197,50 @@ export function runHarnessTests(
194197
);
195198
});
196199

200+
it('should be able to get error harnesses from the form-field harness', async () => {
201+
const formFields = await loader.getAllHarnesses(formFieldHarness);
202+
expect(await formFields[1].getErrors()).toEqual([]);
203+
204+
fixture.componentInstance.requiredControl.setValue('');
205+
dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');
206+
207+
const formFieldErrorHarnesses = await formFields[1].getErrors();
208+
if (isMdcImplementation) {
209+
expect(formFieldErrorHarnesses.length).toBe(2);
210+
expect(await formFieldErrorHarnesses[0].getText()).toBe('Error 1');
211+
expect(await formFieldErrorHarnesses[1].getText()).toBe('Error 2');
212+
} else {
213+
expect(formFieldErrorHarnesses.length).toBe(1);
214+
expect(await formFieldErrorHarnesses[0].getText()).toBe('Error 1');
215+
}
216+
217+
const error1Harnesses = await formFields[1].getErrors({text: 'Error 1'});
218+
expect(error1Harnesses.length).toBe(1);
219+
expect(await error1Harnesses[0].getText()).toBe('Error 1');
220+
});
221+
222+
it('should be able to directly load error harnesses', async () => {
223+
const formFields = await loader.getAllHarnesses(formFieldHarness);
224+
expect(await formFields[1].getErrors()).toEqual([]);
225+
226+
fixture.componentInstance.requiredControl.setValue('');
227+
dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');
228+
229+
const errorHarnesses = await loader.getAllHarnesses(errorHarness);
230+
if (isMdcImplementation) {
231+
expect(errorHarnesses.length).toBe(2);
232+
expect(await errorHarnesses[0].getText()).toBe('Error 1');
233+
expect(await errorHarnesses[1].getText()).toBe('Error 2');
234+
} else {
235+
expect(errorHarnesses.length).toBe(1);
236+
expect(await errorHarnesses[0].getText()).toBe('Error 1');
237+
}
238+
239+
const error1Harnesses = await loader.getAllHarnesses(errorHarness.with({text: 'Error 1'}));
240+
expect(error1Harnesses.length).toBe(1);
241+
expect(await error1Harnesses[0].getText()).toBe('Error 1');
242+
});
243+
197244
it('should be able to get hint messages of form-field', async () => {
198245
const formFields = await loader.getAllHarnesses(formFieldHarness);
199246
expect(await formFields[1].getTextHints()).toEqual(['Hint 1', 'Hint 2']);

src/material/legacy-form-field/testing/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ ng_test_library(
3131
"//src/material/core",
3232
"//src/material/datepicker",
3333
"//src/material/datepicker/testing",
34+
"//src/material/form-field/testing",
3435
"//src/material/form-field/testing:harness_tests_lib",
3536
"//src/material/legacy-autocomplete",
3637
"//src/material/legacy-form-field",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing';
10+
11+
import {_MatErrorHarnessBase, ErrorHarnessFilters} from '@angular/material/form-field/testing';
12+
13+
/**
14+
* Harness for interacting with a `mat-error` in tests.
15+
* @deprecated Use `MatErrorHarness` from `@angular/material/form-field/testing` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
16+
* @breaking-change 17.0.0
17+
*/
18+
export class MatLegacyErrorHarness extends _MatErrorHarnessBase {
19+
static hostSelector = '.mat-error';
20+
21+
/**
22+
* Gets a `HarnessPredicate` that can be used to search for an error with specific
23+
* attributes.
24+
* @param options Options for filtering which error instances are considered a match.
25+
* @return a `HarnessPredicate` configured with the given options.
26+
*/
27+
static with<T extends MatLegacyErrorHarness>(
28+
this: ComponentHarnessConstructor<T>,
29+
options: ErrorHarnessFilters = {},
30+
): HarnessPredicate<T> {
31+
return _MatErrorHarnessBase._getErrorPredicate(this, options);
32+
}
33+
}

src/material/legacy-form-field/testing/form-field-harness.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {MatLegacyErrorHarness} from './error-harness';
12
import {MatLegacyAutocompleteModule} from '@angular/material/legacy-autocomplete';
23
import {MatNativeDateModule} from '@angular/material/core';
34
import {MatDatepickerModule} from '@angular/material/datepicker';
@@ -31,6 +32,7 @@ describe('Non-MDC-based MatFormFieldHarness', () => {
3132
datepickerInputHarness: MatDatepickerInputHarness,
3233
dateRangeInputHarness: MatDateRangeInputHarness,
3334
isMdcImplementation: false,
35+
errorHarness: MatLegacyErrorHarness,
3436
},
3537
);
3638
});

src/material/legacy-form-field/testing/form-field-harness.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '@angular/material/form-field/testing';
1818
import {MatLegacyInputHarness} from '@angular/material/legacy-input/testing';
1919
import {MatLegacySelectHarness} from '@angular/material/legacy-select/testing';
20+
import {MatLegacyErrorHarness} from './error-harness';
2021

2122
// TODO(devversion): support support chip list harness
2223
/**
@@ -35,7 +36,10 @@ export type LegacyFormFieldControlHarness =
3536
* @deprecated Use `MatFormFieldHarness` from `@angular/material/form-field/testing` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
3637
* @breaking-change 17.0.0
3738
*/
38-
export class MatLegacyFormFieldHarness extends _MatFormFieldHarnessBase<LegacyFormFieldControlHarness> {
39+
export class MatLegacyFormFieldHarness extends _MatFormFieldHarnessBase<
40+
LegacyFormFieldControlHarness,
41+
typeof MatLegacyErrorHarness
42+
> {
3943
static hostSelector = '.mat-form-field';
4044

4145
/**
@@ -65,6 +69,7 @@ export class MatLegacyFormFieldHarness extends _MatFormFieldHarnessBase<LegacyFo
6569
protected _selectControl = this.locatorForOptional(MatLegacySelectHarness);
6670
protected _datepickerInputControl = this.locatorForOptional(MatDatepickerInputHarness);
6771
protected _dateRangeInputControl = this.locatorForOptional(MatDateRangeInputHarness);
72+
protected _errorHarness = MatLegacyErrorHarness;
6873

6974
/** Gets the appearance of the form-field. */
7075
async getAppearance(): Promise<'legacy' | 'standard' | 'fill' | 'outline'> {

src/material/legacy-form-field/testing/public-api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
export {LegacyFormFieldControlHarness, MatLegacyFormFieldHarness} from './form-field-harness';
10+
export {MatLegacyErrorHarness} from './error-harness';
1011

1112
// Re-export the base control harness from the "form-field/testing/control" entry-point. To
1213
// avoid circular dependencies, harnesses for form-field controls (i.e. input, select)
@@ -26,3 +27,11 @@ export {
2627
*/
2728
FormFieldHarnessFilters as LegacyFormFieldHarnessFilters,
2829
} from '@angular/material/form-field/testing';
30+
31+
export {
32+
/**
33+
* @deprecated Use `ErrorHarnessFilters` from `@angular/material/form-field/testing` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
34+
* @breaking-change 17.0.0
35+
*/
36+
ErrorHarnessFilters as LegacyErrorHarnessFilters,
37+
} from '@angular/material/form-field/testing';

0 commit comments

Comments
 (0)