Skip to content

Commit

Permalink
feat(material-experimental/mdc-form-field): add test harness (#18165)
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion authored and jelbourn committed Jan 22, 2020
1 parent 1a53f03 commit 944837f
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 43 deletions.
1 change: 1 addition & 0 deletions src/material-experimental/config.bzl
Expand Up @@ -8,6 +8,7 @@ entryPoints = [
"mdc-chips",
"mdc-chips/testing",
"mdc-form-field",
"mdc-form-field/testing",
"mdc-input",
"mdc-list",
"mdc-menu",
Expand Down
59 changes: 59 additions & 0 deletions src/material-experimental/mdc-form-field/testing/BUILD.bazel
@@ -0,0 +1,59 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ng_test_library", "ng_web_test_suite", "ts_library")

ts_library(
name = "testing",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/material-experimental/mdc-form-field/testing",
deps = [
"//src/cdk/testing",
"//src/material/form-field/testing",
"//src/material/form-field/testing/control",
"//src/material/input/testing",
"//src/material/select/testing",
],
)

filegroup(
name = "source-files",
srcs = glob(["**/*.ts"]),
)

ng_test_library(
name = "unit_tests_lib",
srcs = glob(
["**/*.spec.ts"],
exclude = ["shared.spec.ts"],
),
deps = [
":testing",
"//src/cdk/overlay",
"//src/material-experimental/mdc-form-field",
"//src/material-experimental/mdc-input",
"//src/material/autocomplete",
"//src/material/core",
"//src/material/form-field/testing:harness_tests_lib",
"//src/material/input/testing",
"//src/material/select",
"//src/material/select/testing",
"@npm//@angular/common",
],
)

ng_web_test_suite(
name = "unit_tests",
static_files = [
"@npm//:node_modules/@material/textfield/dist/mdc.textfield.js",
"@npm//:node_modules/@material/line-ripple/dist/mdc.lineRipple.js",
"@npm//:node_modules/@material/notched-outline/dist/mdc.notchedOutline.js",
"@npm//:node_modules/@material/floating-label/dist/mdc.floatingLabel.js",
],
deps = [
":unit_tests_lib",
"//src/material-experimental:mdc_require_config.js",
],
)
@@ -0,0 +1,38 @@
import {OverlayModule} from '@angular/cdk/overlay';
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
import {MatInputModule} from '@angular/material-experimental/mdc-input';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatCommonModule, MatOptionModule} from '@angular/material/core';
import {MatInputHarness} from '@angular/material/input/testing';
import {
MAT_SELECT_SCROLL_STRATEGY_PROVIDER,
MatSelect,
MatSelectTrigger
} from '@angular/material/select';
import {MatSelectHarness} from '@angular/material/select/testing';
import {runHarnessTests} from '@angular/material/form-field/testing/shared.spec';
import {MatFormFieldHarness} from './form-field-harness';

// TODO: remove this once there is a `MatSelect` module which does not come
// with the form-field module provided. This is a copy of the `MatSelect` module
// that does not provide any form-field module.
@NgModule({
imports: [CommonModule, OverlayModule, MatOptionModule, MatCommonModule],
exports: [MatSelect, MatSelectTrigger, MatOptionModule, MatCommonModule],
declarations: [MatSelect, MatSelectTrigger],
providers: [MAT_SELECT_SCROLL_STRATEGY_PROVIDER]
})
export class SelectWithoutFormFieldModule {
}

describe('MDC-based MatFormFieldHarness', () => {
runHarnessTests(
[MatFormFieldModule, MatAutocompleteModule, MatInputModule, SelectWithoutFormFieldModule], {
formFieldHarness: MatFormFieldHarness as any,
inputHarness: MatInputHarness,
selectHarness: MatSelectHarness,
isMdcImplementation: true,
});
});
230 changes: 230 additions & 0 deletions src/material-experimental/mdc-form-field/testing/form-field-harness.ts
@@ -0,0 +1,230 @@
/**
* @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 {
ComponentHarness,
ComponentHarnessConstructor,
HarnessPredicate,
HarnessQuery,
TestElement
} from '@angular/cdk/testing';
import {FormFieldHarnessFilters} from '@angular/material/form-field/testing';
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
import {MatInputHarness} from '@angular/material/input/testing';
import {MatSelectHarness} from '@angular/material/select/testing';

// TODO(devversion): support datepicker harness once developed (COMP-203).
// Also support chip list harness.
/** Possible harnesses of controls which can be bound to a form-field. */
export type FormFieldControlHarness = MatInputHarness|MatSelectHarness;

/** Harness for interacting with a MDC-based form-field's in tests. */
export class MatFormFieldHarness extends ComponentHarness {
static hostSelector = '.mat-mdc-form-field';

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatFormFieldHarness` that meets
* certain criteria.
* @param options Options for filtering which form field instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: FormFieldHarnessFilters = {}): HarnessPredicate<MatFormFieldHarness> {
return new HarnessPredicate(MatFormFieldHarness, options)
.addOption(
'floatingLabelText', options.floatingLabelText,
async (harness, text) => HarnessPredicate.stringMatches(await harness.getLabel(), text))
.addOption(
'hasErrors', options.hasErrors,
async (harness, hasErrors) => await harness.hasErrors() === hasErrors);
}

private _mdcTextField = this.locatorFor('.mat-mdc-text-field-wrapper');

private _prefixContainer = this.locatorForOptional('.mat-mdc-form-field-prefix');
private _suffixContainer = this.locatorForOptional('.mat-mdc-form-field-suffix');
private _label = this.locatorForOptional('.mdc-floating-label');
private _errors = this.locatorForAll('.mat-mdc-form-field-error');
private _hints = this.locatorForAll('.mat-mdc-form-field-hint');

private _inputControl = this.locatorForOptional(MatInputHarness);
private _selectControl = this.locatorForOptional(MatSelectHarness);

/** Gets the appearance of the form-field. */
async getAppearance(): Promise<'fill'|'outline'> {
const textFieldEl = await this._mdcTextField();
if (await textFieldEl.hasClass('mdc-text-field--outlined')) {
return 'outline';
}
return 'fill';
}

/**
* Gets the harness of the control that is bound to the form-field. Only
* default controls such as "MatInputHarness" and "MatSelectHarness" are
* supported.
*/
async getControl(): Promise<FormFieldControlHarness|null>;

/**
* Gets the harness of the control that is bound to the form-field. Searches
* for a control that matches the specified harness type.
*/
async getControl<X extends MatFormFieldControlHarness>(type: ComponentHarnessConstructor<X>):
Promise<X|null>;

/**
* Gets the harness of the control that is bound to the form-field. Searches
* for a control that matches the specified harness predicate.
*/
async getControl<X extends MatFormFieldControlHarness>(type: HarnessPredicate<X>):
Promise<X|null>;

// Implementation of the "getControl" method overload signatures.
async getControl<X extends MatFormFieldControlHarness>(type?: HarnessQuery<X>) {
if (type) {
return this.locatorForOptional(type)();
}
const hostEl = await this.host();
const [isInput, isSelect] = await Promise.all([
hostEl.hasClass('mat-mdc-form-field-type-mat-input'),
hostEl.hasClass('mat-mdc-form-field-type-mat-select'),
]);
if (isInput) {
return this._inputControl();
} else if (isSelect) {
return this._selectControl();
}
return null;
}

/** Whether the form-field has a label. */
async hasLabel(): Promise<boolean> {
return (await this._label()) !== null;
}

/** Gets the label of the form-field. */
async getLabel(): Promise<string|null> {
const labelEl = await this._label();
return labelEl ? labelEl.text() : null;
}

/** Whether the form-field has errors. */
async hasErrors(): Promise<boolean> {
return (await this.getTextErrors()).length > 0;
}

/** Whether the label is currently floating. */
async isLabelFloating(): Promise<boolean> {
const labelEl = await this._label();
return labelEl !== null ? await labelEl.hasClass('mdc-floating-label--float-above') : false;
}

/** Whether the form-field is disabled. */
async isDisabled(): Promise<boolean> {
return (await this.host()).hasClass('mat-form-field-disabled');
}

/** Whether the form-field is currently autofilled. */
async isAutofilled(): Promise<boolean> {
return (await this.host()).hasClass('mat-form-field-autofilled');
}

/** Gets the theme color of the form-field. */
async getThemeColor(): Promise<'primary'|'accent'|'warn'> {
const hostEl = await this.host();
const [isAccent, isWarn] =
await Promise.all([hostEl.hasClass('mat-accent'), hostEl.hasClass('mat-warn')]);
if (isAccent) {
return 'accent';
} else if (isWarn) {
return 'warn';
}
return 'primary';
}

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

/** Gets hint messages which are currently displayed in the form-field. */
async getTextHints(): Promise<string[]> {
return Promise.all((await this._hints()).map(e => e.text()));
}

/**
* Gets a reference to the container element which contains all projected
* prefixes of the form-field.
*/
async getHarnessLoaderForPrefix(): Promise<TestElement|null> {
return this._prefixContainer();
}

/**
* Gets a reference to the container element which contains all projected
* suffixes of the form-field.
*/
async getHarnessLoaderForSuffix(): Promise<TestElement|null> {
return this._suffixContainer();
}

/**
* Whether the form control has been touched. Returns "null"
* if no form control is set up.
*/
async isControlTouched(): Promise<boolean|null> {
if (!await this._hasFormControl()) {
return null;
}
return (await this.host()).hasClass('ng-touched');
}

/**
* Whether the form control is dirty. Returns "null"
* if no form control is set up.
*/
async isControlDirty(): Promise<boolean|null> {
if (!await this._hasFormControl()) {
return null;
}
return (await this.host()).hasClass('ng-dirty');
}

/**
* Whether the form control is valid. Returns "null"
* if no form control is set up.
*/
async isControlValid(): Promise<boolean|null> {
if (!await this._hasFormControl()) {
return null;
}
return (await this.host()).hasClass('ng-valid');
}

/**
* Whether the form control is pending validation. Returns "null"
* if no form control is set up.
*/
async isControlPending(): Promise<boolean|null> {
if (!await this._hasFormControl()) {
return null;
}
return (await this.host()).hasClass('ng-pending');
}

/** Checks whether the form-field control has set up a form control. */
private async _hasFormControl(): Promise<boolean> {
const hostEl = await this.host();
// If no form "NgControl" is bound to the form-field control, the form-field
// is not able to forward any control status classes. Therefore if either the
// "ng-touched" or "ng-untouched" class is set, we know that it has a form control
const [isTouched, isUntouched] =
await Promise.all([hostEl.hasClass('ng-touched'), hostEl.hasClass('ng-untouched')]);
return isTouched || isUntouched;
}
}
9 changes: 9 additions & 0 deletions src/material-experimental/mdc-form-field/testing/index.ts
@@ -0,0 +1,9 @@
/**
* @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
*/

export * from './public-api';
15 changes: 15 additions & 0 deletions src/material-experimental/mdc-form-field/testing/public-api.ts
@@ -0,0 +1,15 @@
/**
* @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
*/

// Re-export everything from the "form-field/testing/control" entry-point. To avoid
// circular dependencies, harnesses for default form-field controls (i.e. input, select)
// need to import the base form-field control harness through a separate entry-point.
export * from '@angular/material/form-field/testing/control';

export {FormFieldHarnessFilters} from '@angular/material/form-field/testing';
export * from './form-field-harness';
13 changes: 8 additions & 5 deletions src/material/form-field/testing/form-field-harness.spec.ts
@@ -1,15 +1,18 @@
import {MatInputHarness} from '@angular/material/input/testing';
import {MatSelectHarness} from '@angular/material/select/testing';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatInputHarness} from '@angular/material/input/testing';
import {MatSelectModule} from '@angular/material/select';
import {MatSelectHarness} from '@angular/material/select/testing';

import {MatFormFieldHarness} from './form-field-harness';
import {runHarnessTests} from './shared.spec';

describe('Non-MDC-based MatFormFieldHarness', () => {
runHarnessTests(
[MatFormFieldModule, MatAutocompleteModule, MatInputModule, MatSelectModule],
MatFormFieldHarness, MatInputHarness, MatSelectHarness);
runHarnessTests([MatFormFieldModule, MatAutocompleteModule, MatInputModule, MatSelectModule], {
formFieldHarness: MatFormFieldHarness,
inputHarness: MatInputHarness,
selectHarness: MatSelectHarness,
isMdcImplementation: false,
});
});

0 comments on commit 944837f

Please sign in to comment.