Skip to content

Commit

Permalink
feat(material/form-field): add error harness (#25698)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
andrewseguin committed Sep 26, 2022
1 parent 8e9625e commit 36af2a3
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 11 deletions.
54 changes: 54 additions & 0 deletions src/material/form-field/testing/error-harness.ts
@@ -0,0 +1,54 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {
BaseHarnessFilters,
ComponentHarness,
ComponentHarnessConstructor,
HarnessPredicate,
} from '@angular/cdk/testing';

/** A set of criteria that can be used to filter a list of error harness instances. */
export interface ErrorHarnessFilters extends BaseHarnessFilters {
/** Only find instances whose text matches the given value. */
text?: string | RegExp;
}

export abstract class _MatErrorHarnessBase extends ComponentHarness {
/** Gets a promise for the error's label text. */
async getText(): Promise<string> {
return (await this.host()).text();
}

protected static _getErrorPredicate<T extends MatErrorHarness>(
type: ComponentHarnessConstructor<T>,
options: ErrorHarnessFilters,
): HarnessPredicate<T> {
return new HarnessPredicate(type, options).addOption('text', options.text, (harness, text) =>
HarnessPredicate.stringMatches(harness.getText(), text),
);
}
}

/** Harness for interacting with an MDC-based `mat-error` in tests. */
export class MatErrorHarness extends _MatErrorHarnessBase {
static hostSelector = '.mat-mdc-form-field-error';

/**
* Gets a `HarnessPredicate` that can be used to search for an error with specific
* attributes.
* @param options Options for filtering which error instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with<T extends MatErrorHarness>(
this: ComponentHarnessConstructor<T>,
options: ErrorHarnessFilters = {},
): HarnessPredicate<T> {
return _MatErrorHarnessBase._getErrorPredicate(this, options);
}
}
2 changes: 2 additions & 0 deletions src/material/form-field/testing/form-field-harness.spec.ts
@@ -1,4 +1,5 @@
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatErrorHarness} from './error-harness';
import {MatInputModule} from '@angular/material/input';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatInputHarness} from '@angular/material/input/testing';
Expand Down Expand Up @@ -30,6 +31,7 @@ describe('MDC-based MatFormFieldHarness', () => {
datepickerInputHarness: MatDatepickerInputHarness,
dateRangeInputHarness: MatDateRangeInputHarness,
isMdcImplementation: true,
errorHarness: MatErrorHarness,
},
);
});
26 changes: 21 additions & 5 deletions src/material/form-field/testing/form-field-harness.ts
Expand Up @@ -15,6 +15,7 @@ import {
parallel,
TestElement,
} from '@angular/cdk/testing';
import {ErrorHarnessFilters, MatErrorHarness} from './error-harness';
import {MatInputHarness} from '@angular/material/input/testing';
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
import {MatSelectHarness} from '@angular/material/select/testing';
Expand All @@ -24,18 +25,25 @@ import {
} from '@angular/material/datepicker/testing';
import {FormFieldHarnessFilters} from './form-field-harness-filters';

interface ErrorBase extends ComponentHarness {
getText(): Promise<string>;
}

export abstract class _MatFormFieldHarnessBase<
ControlHarness extends MatFormFieldControlHarness,
ErrorType extends ComponentHarnessConstructor<ErrorBase> & {
with: (options?: ErrorHarnessFilters) => HarnessPredicate<ErrorBase>;
},
> extends ComponentHarness {
protected abstract _prefixContainer: AsyncFactoryFn<TestElement | null>;
protected abstract _suffixContainer: AsyncFactoryFn<TestElement | null>;
protected abstract _label: AsyncFactoryFn<TestElement | null>;
protected abstract _errors: AsyncFactoryFn<TestElement[]>;
protected abstract _hints: AsyncFactoryFn<TestElement[]>;
protected abstract _inputControl: AsyncFactoryFn<ControlHarness | null>;
protected abstract _selectControl: AsyncFactoryFn<ControlHarness | null>;
protected abstract _datepickerInputControl: AsyncFactoryFn<ControlHarness | null>;
protected abstract _dateRangeInputControl: AsyncFactoryFn<ControlHarness | null>;
protected abstract _errorHarness: ErrorType;

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

/** Gets error messages which are currently displayed in the form-field. */
async getTextErrors(): Promise<string[]> {
const errors = await this._errors();
return parallel(() => errors.map(e => e.text()));
const errors = await this.getErrors();
return parallel(() => errors.map(e => e.getText()));
}

/** Gets all of the error harnesses in the form field. */
async getErrors(filter: ErrorHarnessFilters = {}): Promise<MatErrorHarness[]> {
return this.locatorForAll(this._errorHarness.with(filter))();
}

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

/** Harness for interacting with a MDC-based form-field's in tests. */
export class MatFormFieldHarness extends _MatFormFieldHarnessBase<FormFieldControlHarness> {
export class MatFormFieldHarness extends _MatFormFieldHarnessBase<
FormFieldControlHarness,
typeof MatErrorHarness
> {
static hostSelector = '.mat-mdc-form-field';

/**
Expand All @@ -238,12 +254,12 @@ export class MatFormFieldHarness extends _MatFormFieldHarnessBase<FormFieldContr
protected _prefixContainer = this.locatorForOptional('.mat-mdc-form-field-text-prefix');
protected _suffixContainer = this.locatorForOptional('.mat-mdc-form-field-text-suffix');
protected _label = this.locatorForOptional('.mdc-floating-label');
protected _errors = this.locatorForAll('.mat-mdc-form-field-error');
protected _hints = this.locatorForAll('.mat-mdc-form-field-hint');
protected _inputControl = this.locatorForOptional(MatInputHarness);
protected _selectControl = this.locatorForOptional(MatSelectHarness);
protected _datepickerInputControl = this.locatorForOptional(MatDatepickerInputHarness);
protected _dateRangeInputControl = this.locatorForOptional(MatDateRangeInputHarness);
protected _errorHarness = MatErrorHarness;
private _mdcTextField = this.locatorFor('.mat-mdc-text-field-wrapper');

/** Gets the appearance of the form-field. */
Expand Down
1 change: 1 addition & 0 deletions src/material/form-field/testing/public-api.ts
Expand Up @@ -13,3 +13,4 @@ export {MatFormFieldControlHarness} from '@angular/material/form-field/testing/c

export * from './form-field-harness-filters';
export * from './form-field-harness';
export * from './error-harness';
47 changes: 47 additions & 0 deletions src/material/form-field/testing/shared.spec.ts
@@ -1,4 +1,5 @@
import {ComponentHarness, HarnessLoader, HarnessPredicate, parallel} from '@angular/cdk/testing';
import {MatErrorHarness} from './error-harness';
import {createFakeEvent, dispatchFakeEvent} from '../../../cdk/testing/private';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component, Type} from '@angular/core';
Expand All @@ -17,13 +18,15 @@ export function runHarnessTests(
datepickerInputHarness,
dateRangeInputHarness,
isMdcImplementation,
errorHarness,
}: {
formFieldHarness: typeof MatFormFieldHarness;
inputHarness: Type<any>;
selectHarness: Type<any>;
datepickerInputHarness: Type<any>;
dateRangeInputHarness: Type<any>;
isMdcImplementation: boolean;
errorHarness: typeof MatErrorHarness;
},
) {
let fixture: ComponentFixture<FormFieldHarnessTest>;
Expand Down Expand Up @@ -194,6 +197,50 @@ export function runHarnessTests(
);
});

it('should be able to get error harnesses from the form-field harness', async () => {
const formFields = await loader.getAllHarnesses(formFieldHarness);
expect(await formFields[1].getErrors()).toEqual([]);

fixture.componentInstance.requiredControl.setValue('');
dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');

const formFieldErrorHarnesses = await formFields[1].getErrors();
if (isMdcImplementation) {
expect(formFieldErrorHarnesses.length).toBe(2);
expect(await formFieldErrorHarnesses[0].getText()).toBe('Error 1');
expect(await formFieldErrorHarnesses[1].getText()).toBe('Error 2');
} else {
expect(formFieldErrorHarnesses.length).toBe(1);
expect(await formFieldErrorHarnesses[0].getText()).toBe('Error 1');
}

const error1Harnesses = await formFields[1].getErrors({text: 'Error 1'});
expect(error1Harnesses.length).toBe(1);
expect(await error1Harnesses[0].getText()).toBe('Error 1');
});

it('should be able to directly load error harnesses', async () => {
const formFields = await loader.getAllHarnesses(formFieldHarness);
expect(await formFields[1].getErrors()).toEqual([]);

fixture.componentInstance.requiredControl.setValue('');
dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');

const errorHarnesses = await loader.getAllHarnesses(errorHarness);
if (isMdcImplementation) {
expect(errorHarnesses.length).toBe(2);
expect(await errorHarnesses[0].getText()).toBe('Error 1');
expect(await errorHarnesses[1].getText()).toBe('Error 2');
} else {
expect(errorHarnesses.length).toBe(1);
expect(await errorHarnesses[0].getText()).toBe('Error 1');
}

const error1Harnesses = await loader.getAllHarnesses(errorHarness.with({text: 'Error 1'}));
expect(error1Harnesses.length).toBe(1);
expect(await error1Harnesses[0].getText()).toBe('Error 1');
});

it('should be able to get hint messages of form-field', async () => {
const formFields = await loader.getAllHarnesses(formFieldHarness);
expect(await formFields[1].getTextHints()).toEqual(['Hint 1', 'Hint 2']);
Expand Down
1 change: 1 addition & 0 deletions src/material/legacy-form-field/testing/BUILD.bazel
Expand Up @@ -31,6 +31,7 @@ ng_test_library(
"//src/material/core",
"//src/material/datepicker",
"//src/material/datepicker/testing",
"//src/material/form-field/testing",
"//src/material/form-field/testing:harness_tests_lib",
"//src/material/legacy-autocomplete",
"//src/material/legacy-form-field",
Expand Down
33 changes: 33 additions & 0 deletions src/material/legacy-form-field/testing/error-harness.ts
@@ -0,0 +1,33 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ComponentHarnessConstructor, HarnessPredicate} from '@angular/cdk/testing';

import {_MatErrorHarnessBase, ErrorHarnessFilters} from '@angular/material/form-field/testing';

/**
* Harness for interacting with a `mat-error` in tests.
* @deprecated Use `MatErrorHarness` from `@angular/material/form-field/testing` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
* @breaking-change 17.0.0
*/
export class MatLegacyErrorHarness extends _MatErrorHarnessBase {
static hostSelector = '.mat-error';

/**
* Gets a `HarnessPredicate` that can be used to search for an error with specific
* attributes.
* @param options Options for filtering which error instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with<T extends MatLegacyErrorHarness>(
this: ComponentHarnessConstructor<T>,
options: ErrorHarnessFilters = {},
): HarnessPredicate<T> {
return _MatErrorHarnessBase._getErrorPredicate(this, options);
}
}
@@ -1,3 +1,4 @@
import {MatLegacyErrorHarness} from './error-harness';
import {MatLegacyAutocompleteModule} from '@angular/material/legacy-autocomplete';
import {MatNativeDateModule} from '@angular/material/core';
import {MatDatepickerModule} from '@angular/material/datepicker';
Expand Down Expand Up @@ -31,6 +32,7 @@ describe('Non-MDC-based MatFormFieldHarness', () => {
datepickerInputHarness: MatDatepickerInputHarness,
dateRangeInputHarness: MatDateRangeInputHarness,
isMdcImplementation: false,
errorHarness: MatLegacyErrorHarness,
},
);
});
7 changes: 6 additions & 1 deletion src/material/legacy-form-field/testing/form-field-harness.ts
Expand Up @@ -17,6 +17,7 @@ import {
} from '@angular/material/form-field/testing';
import {MatLegacyInputHarness} from '@angular/material/legacy-input/testing';
import {MatLegacySelectHarness} from '@angular/material/legacy-select/testing';
import {MatLegacyErrorHarness} from './error-harness';

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

/**
Expand Down Expand Up @@ -65,6 +69,7 @@ export class MatLegacyFormFieldHarness extends _MatFormFieldHarnessBase<LegacyFo
protected _selectControl = this.locatorForOptional(MatLegacySelectHarness);
protected _datepickerInputControl = this.locatorForOptional(MatDatepickerInputHarness);
protected _dateRangeInputControl = this.locatorForOptional(MatDateRangeInputHarness);
protected _errorHarness = MatLegacyErrorHarness;

/** Gets the appearance of the form-field. */
async getAppearance(): Promise<'legacy' | 'standard' | 'fill' | 'outline'> {
Expand Down
9 changes: 9 additions & 0 deletions src/material/legacy-form-field/testing/public-api.ts
Expand Up @@ -7,6 +7,7 @@
*/

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

// Re-export the base control harness from the "form-field/testing/control" entry-point. To
// avoid circular dependencies, harnesses for form-field controls (i.e. input, select)
Expand All @@ -26,3 +27,11 @@ export {
*/
FormFieldHarnessFilters as LegacyFormFieldHarnessFilters,
} from '@angular/material/form-field/testing';

export {
/**
* @deprecated Use `ErrorHarnessFilters` from `@angular/material/form-field/testing` instead. See https://material.angular.io/guide/mdc-migration for information about migrating.
* @breaking-change 17.0.0
*/
ErrorHarnessFilters as LegacyErrorHarnessFilters,
} from '@angular/material/form-field/testing';
30 changes: 26 additions & 4 deletions tools/public_api_guard/material/form-field-testing.md
Expand Up @@ -16,6 +16,11 @@ import { MatInputHarness } from '@angular/material/input/testing';
import { MatSelectHarness } from '@angular/material/select/testing';
import { TestElement } from '@angular/cdk/testing';

// @public
export interface ErrorHarnessFilters extends BaseHarnessFilters {
text?: string | RegExp;
}

// @public
export type FormFieldControlHarness = MatInputHarness | MatSelectHarness | MatDatepickerInputHarness | MatDateRangeInputHarness;

Expand All @@ -25,16 +30,30 @@ export interface FormFieldHarnessFilters extends BaseHarnessFilters {
hasErrors?: boolean;
}

// @public
export class MatErrorHarness extends _MatErrorHarnessBase {
// (undocumented)
static hostSelector: string;
static with<T extends MatErrorHarness>(this: ComponentHarnessConstructor<T>, options?: ErrorHarnessFilters): HarnessPredicate<T>;
}

// @public (undocumented)
export abstract class _MatErrorHarnessBase extends ComponentHarness {
// (undocumented)
protected static _getErrorPredicate<T extends MatErrorHarness>(type: ComponentHarnessConstructor<T>, options: ErrorHarnessFilters): HarnessPredicate<T>;
getText(): Promise<string>;
}

export { MatFormFieldControlHarness }

// @public
export class MatFormFieldHarness extends _MatFormFieldHarnessBase<FormFieldControlHarness> {
export class MatFormFieldHarness extends _MatFormFieldHarnessBase<FormFieldControlHarness, typeof MatErrorHarness> {
// (undocumented)
protected _datepickerInputControl: AsyncFactoryFn<MatDatepickerInputHarness | null>;
// (undocumented)
protected _dateRangeInputControl: AsyncFactoryFn<MatDateRangeInputHarness | null>;
// (undocumented)
protected _errors: AsyncFactoryFn<TestElement[]>;
protected _errorHarness: typeof MatErrorHarness;
getAppearance(): Promise<'fill' | 'outline'>;
hasLabel(): Promise<boolean>;
// (undocumented)
Expand All @@ -56,17 +75,20 @@ export class MatFormFieldHarness extends _MatFormFieldHarnessBase<FormFieldContr
}

// @public (undocumented)
export abstract class _MatFormFieldHarnessBase<ControlHarness extends MatFormFieldControlHarness> extends ComponentHarness {
export abstract class _MatFormFieldHarnessBase<ControlHarness extends MatFormFieldControlHarness, ErrorType extends ComponentHarnessConstructor<ErrorBase> & {
with: (options?: ErrorHarnessFilters) => HarnessPredicate<ErrorBase>;
}> extends ComponentHarness {
// (undocumented)
protected abstract _datepickerInputControl: AsyncFactoryFn<ControlHarness | null>;
// (undocumented)
protected abstract _dateRangeInputControl: AsyncFactoryFn<ControlHarness | null>;
// (undocumented)
protected abstract _errors: AsyncFactoryFn<TestElement[]>;
protected abstract _errorHarness: ErrorType;
abstract getAppearance(): Promise<string>;
getControl(): Promise<ControlHarness | null>;
getControl<X extends MatFormFieldControlHarness>(type: ComponentHarnessConstructor<X>): Promise<X | null>;
getControl<X extends MatFormFieldControlHarness>(type: HarnessPredicate<X>): Promise<X | null>;
getErrors(filter?: ErrorHarnessFilters): Promise<MatErrorHarness[]>;
getLabel(): Promise<string | null>;
getPrefixText(): Promise<string>;
getSuffixText(): Promise<string>;
Expand Down

0 comments on commit 36af2a3

Please sign in to comment.