Skip to content

Commit

Permalink
refactor(material-experimental/mdc-select): de-duplicate test harness…
Browse files Browse the repository at this point in the history
… logic (angular#21460)

Reworks the `MatSelectHarness` in order to de-duplicate the logic between the base and
MDC implementations.
  • Loading branch information
crisbeto authored and mmalerba committed Jan 28, 2021
1 parent a916aba commit b32b78b
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 162 deletions.
1 change: 1 addition & 0 deletions src/material-experimental/mdc-select/testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ts_library(
"//src/cdk/testing",
"//src/material-experimental/mdc-core/testing",
"//src/material/form-field/testing/control",
"//src/material/select/testing",
],
)

Expand Down
129 changes: 9 additions & 120 deletions src/material-experimental/mdc-select/testing/select-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {HarnessPredicate, parallel} from '@angular/cdk/testing';
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
import {HarnessPredicate} from '@angular/cdk/testing';
import {_MatSelectHarnessBase} from '@angular/material/select/testing';
import {
MatOptionHarness,
MatOptgroupHarness,
Expand All @@ -18,13 +18,14 @@ import {SelectHarnessFilters} from './select-harness-filters';


/** Harness for interacting with an MDC-based mat-select in tests. */
export class MatSelectHarness extends MatFormFieldControlHarness {
private _documentRootLocator = this.documentRootLocatorFactory();
private _backdrop = this._documentRootLocator.locatorFor('.cdk-overlay-backdrop');
private _trigger = this.locatorFor('.mat-mdc-select-trigger');
private _value = this.locatorFor('.mat-mdc-select-value');

export class MatSelectHarness extends _MatSelectHarnessBase<
typeof MatOptionHarness, MatOptionHarness, OptionHarnessFilters,
typeof MatOptgroupHarness, MatOptgroupHarness, OptgroupHarnessFilters
> {
static hostSelector = '.mat-mdc-select';
protected _prefix = 'mat-mdc';
protected _optionClass = MatOptionHarness;
protected _optionGroupClass = MatOptgroupHarness;

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatSelectHarness` that meets
Expand All @@ -35,116 +36,4 @@ export class MatSelectHarness extends MatFormFieldControlHarness {
static with(options: SelectHarnessFilters = {}): HarnessPredicate<MatSelectHarness> {
return new HarnessPredicate(MatSelectHarness, options);
}

/** Gets a boolean promise indicating if the select is disabled. */
async isDisabled(): Promise<boolean> {
return (await this.host()).hasClass('mat-mdc-select-disabled');
}

/** Gets a boolean promise indicating if the select is valid. */
async isValid(): Promise<boolean> {
return !(await (await this.host()).hasClass('ng-invalid'));
}

/** Gets a boolean promise indicating if the select is required. */
async isRequired(): Promise<boolean> {
return (await this.host()).hasClass('mat-mdc-select-required');
}

/** Gets a boolean promise indicating if the select is empty (no value is selected). */
async isEmpty(): Promise<boolean> {
return (await this.host()).hasClass('mat-mdc-select-empty');
}

/** Gets a boolean promise indicating if the select is in multi-selection mode. */
async isMultiple(): Promise<boolean> {
return (await this.host()).hasClass('mat-mdc-select-multiple');
}

/** Gets a promise for the select's value text. */
async getValueText(): Promise<string> {
return (await this._value()).text();
}

/** Focuses the select and returns a void promise that indicates when the action is complete. */
async focus(): Promise<void> {
return (await this.host()).focus();
}

/** Blurs the select and returns a void promise that indicates when the action is complete. */
async blur(): Promise<void> {
return (await this.host()).blur();
}

/** Whether the select is focused. */
async isFocused(): Promise<boolean> {
return (await this.host()).isFocused();
}

/** Gets the options inside the select panel. */
async getOptions(filter: Omit<OptionHarnessFilters, 'ancestor'> = {}):
Promise<MatOptionHarness[]> {
return this._documentRootLocator.locatorForAll(MatOptionHarness.with({
...filter,
ancestor: await this._getPanelSelector()
}))();
}

/** Gets the groups of options inside the panel. */
async getOptionGroups(filter: Omit<OptgroupHarnessFilters, 'ancestor'> = {}):
Promise<MatOptgroupHarness[]> {
return this._documentRootLocator.locatorForAll(MatOptgroupHarness.with({
...filter,
ancestor: await this._getPanelSelector()
}))();
}

/** Gets whether the select is open. */
async isOpen(): Promise<boolean> {
return !!await this._documentRootLocator.locatorForOptional(await this._getPanelSelector())();
}

/** Opens the select's panel. */
async open(): Promise<void> {
if (!await this.isOpen()) {
return (await this._trigger()).click();
}
}

/**
* Clicks the options that match the passed-in filter. If the select is in multi-selection
* mode all options will be clicked, otherwise the harness will pick the first matching option.
*/
async clickOptions(filter: OptionHarnessFilters = {}): Promise<void> {
await this.open();

const [isMultiple, options] =
await parallel(() => [this.isMultiple(), this.getOptions(filter)]);

if (options.length === 0) {
throw Error('Select does not have options matching the specified filter');
}

if (isMultiple) {
await parallel(() => options.map(option => option.click()));
} else {
await options[0].click();
}
}

/** Closes the select's panel. */
async close(): Promise<void> {
if (await this.isOpen()) {
// This is the most consistent way that works both in both single and multi-select modes,
// but it assumes that only one overlay is open at a time. We should be able to make it
// a bit more precise after #16645 where we can dispatch an ESCAPE press to the host instead.
return (await this._backdrop()).click();
}
}

/** Gets the selector that should be used to find this select's panel. */
private async _getPanelSelector(): Promise<string> {
const id = await (await this.host()).getAttribute('id');
return `#${id}-panel`;
}
}
98 changes: 60 additions & 38 deletions src/material/select/testing/select-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/

import {HarnessPredicate, parallel} from '@angular/cdk/testing';
import {
HarnessPredicate,
parallel,
ComponentHarness,
BaseHarnessFilters,
ComponentHarnessConstructor,
} from '@angular/cdk/testing';
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
import {
MatOptionHarness,
Expand All @@ -16,29 +22,25 @@ import {
} from '@angular/material/core/testing';
import {SelectHarnessFilters} from './select-harness-filters';


/** Harness for interacting with a standard mat-select in tests. */
export class MatSelectHarness extends MatFormFieldControlHarness {
export abstract class _MatSelectHarnessBase<
OptionType extends (ComponentHarnessConstructor<Option> & {
with: (options?: OptionFilters) => HarnessPredicate<Option>}),
Option extends ComponentHarness & {click(): Promise<void>},
OptionFilters extends BaseHarnessFilters,
OptionGroupType extends (ComponentHarnessConstructor<OptionGroup> & {
with: (options?: OptionGroupFilters) => HarnessPredicate<OptionGroup>}),
OptionGroup extends ComponentHarness,
OptionGroupFilters extends BaseHarnessFilters
> extends MatFormFieldControlHarness {
protected abstract _prefix: string;
protected abstract _optionClass: OptionType;
protected abstract _optionGroupClass: OptionGroupType;
private _documentRootLocator = this.documentRootLocatorFactory();
private _backdrop = this._documentRootLocator.locatorFor('.cdk-overlay-backdrop');
private _trigger = this.locatorFor('.mat-select-trigger');
private _value = this.locatorFor('.mat-select-value');

static hostSelector = '.mat-select';

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatSelectHarness` that meets
* certain criteria.
* @param options Options for filtering which select instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: SelectHarnessFilters = {}): HarnessPredicate<MatSelectHarness> {
return new HarnessPredicate(MatSelectHarness, options);
}

/** Gets a boolean promise indicating if the select is disabled. */
async isDisabled(): Promise<boolean> {
return (await this.host()).hasClass('mat-select-disabled');
return (await this.host()).hasClass(`${this._prefix}-select-disabled`);
}

/** Gets a boolean promise indicating if the select is valid. */
Expand All @@ -48,22 +50,23 @@ export class MatSelectHarness extends MatFormFieldControlHarness {

/** Gets a boolean promise indicating if the select is required. */
async isRequired(): Promise<boolean> {
return (await this.host()).hasClass('mat-select-required');
return (await this.host()).hasClass(`${this._prefix}-select-required`);
}

/** Gets a boolean promise indicating if the select is empty (no value is selected). */
async isEmpty(): Promise<boolean> {
return (await this.host()).hasClass('mat-select-empty');
return (await this.host()).hasClass(`${this._prefix}-select-empty`);
}

/** Gets a boolean promise indicating if the select is in multi-selection mode. */
async isMultiple(): Promise<boolean> {
return (await this.host()).hasClass('mat-select-multiple');
return (await this.host()).hasClass(`${this._prefix}-select-multiple`);
}

/** Gets a promise for the select's value text. */
async getValueText(): Promise<string> {
return (await this._value()).text();
const value = await this.locatorFor(`.${this._prefix}-select-value`)();
return value.text();
}

/** Focuses the select and returns a void promise that indicates when the action is complete. */
Expand All @@ -82,21 +85,19 @@ export class MatSelectHarness extends MatFormFieldControlHarness {
}

/** Gets the options inside the select panel. */
async getOptions(filter: Omit<OptionHarnessFilters, 'ancestor'> = {}):
Promise<MatOptionHarness[]> {
return this._documentRootLocator.locatorForAll(MatOptionHarness.with({
...filter,
async getOptions(filter?: Omit<OptionFilters, 'ancestor'>): Promise<Option[]> {
return this._documentRootLocator.locatorForAll(this._optionClass.with({
...(filter || {}),
ancestor: await this._getPanelSelector()
}))();
} as OptionFilters))();
}

/** Gets the groups of options inside the panel. */
async getOptionGroups(filter: Omit<OptgroupHarnessFilters, 'ancestor'> = {}):
Promise<MatOptgroupHarness[]> {
return this._documentRootLocator.locatorForAll(MatOptgroupHarness.with({
...filter,
async getOptionGroups(filter?: Omit<OptionGroupFilters, 'ancestor'>): Promise<OptionGroup[]> {
return this._documentRootLocator.locatorForAll(this._optionGroupClass.with({
...(filter || {}),
ancestor: await this._getPanelSelector()
}))();
} as OptionGroupFilters))() as Promise<OptionGroup[]>;
}

/** Gets whether the select is open. */
Expand All @@ -107,20 +108,20 @@ export class MatSelectHarness extends MatFormFieldControlHarness {
/** Opens the select's panel. */
async open(): Promise<void> {
if (!await this.isOpen()) {
return (await this._trigger()).click();
const trigger = await this.locatorFor(`.${this._prefix}-select-trigger`)();
return trigger.click();
}
}

/**
* Clicks the options that match the passed-in filter. If the select is in multi-selection
* mode all options will be clicked, otherwise the harness will pick the first matching option.
*/
async clickOptions(filter: OptionHarnessFilters = {}): Promise<void> {
async clickOptions(filter?: OptionFilters): Promise<void> {
await this.open();

const [isMultiple, options] = await parallel(() => {
return [this.isMultiple(), this.getOptions(filter)];
});
const [isMultiple, options] =
await parallel(() => [this.isMultiple(), this.getOptions(filter)]);

if (options.length === 0) {
throw Error('Select does not have options matching the specified filter');
Expand Down Expand Up @@ -149,3 +150,24 @@ export class MatSelectHarness extends MatFormFieldControlHarness {
return `#${id}-panel`;
}
}

/** Harness for interacting with a standard mat-select in tests. */
export class MatSelectHarness extends _MatSelectHarnessBase<
typeof MatOptionHarness, MatOptionHarness, OptionHarnessFilters,
typeof MatOptgroupHarness, MatOptgroupHarness, OptgroupHarnessFilters
> {
static hostSelector = '.mat-select';
protected _prefix = 'mat';
protected _optionClass = MatOptionHarness;
protected _optionGroupClass = MatOptgroupHarness;

/**
* Gets a `HarnessPredicate` that can be used to search for a `MatSelectHarness` that meets
* certain criteria.
* @param options Options for filtering which select instances are considered a match.
* @return a `HarnessPredicate` configured with the given options.
*/
static with(options: SelectHarnessFilters = {}): HarnessPredicate<MatSelectHarness> {
return new HarnessPredicate(MatSelectHarness, options);
}
}
23 changes: 19 additions & 4 deletions tools/public_api_guard/material/select/testing.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
export declare class MatSelectHarness extends MatFormFieldControlHarness {
export declare abstract class _MatSelectHarnessBase<OptionType extends (ComponentHarnessConstructor<Option> & {
with: (options?: OptionFilters) => HarnessPredicate<Option>;
}), Option extends ComponentHarness & {
click(): Promise<void>;
}, OptionFilters extends BaseHarnessFilters, OptionGroupType extends (ComponentHarnessConstructor<OptionGroup> & {
with: (options?: OptionGroupFilters) => HarnessPredicate<OptionGroup>;
}), OptionGroup extends ComponentHarness, OptionGroupFilters extends BaseHarnessFilters> extends MatFormFieldControlHarness {
protected abstract _optionClass: OptionType;
protected abstract _optionGroupClass: OptionGroupType;
protected abstract _prefix: string;
blur(): Promise<void>;
clickOptions(filter?: OptionHarnessFilters): Promise<void>;
clickOptions(filter?: OptionFilters): Promise<void>;
close(): Promise<void>;
focus(): Promise<void>;
getOptionGroups(filter?: Omit<OptgroupHarnessFilters, 'ancestor'>): Promise<MatOptgroupHarness[]>;
getOptions(filter?: Omit<OptionHarnessFilters, 'ancestor'>): Promise<MatOptionHarness[]>;
getOptionGroups(filter?: Omit<OptionGroupFilters, 'ancestor'>): Promise<OptionGroup[]>;
getOptions(filter?: Omit<OptionFilters, 'ancestor'>): Promise<Option[]>;
getValueText(): Promise<string>;
isDisabled(): Promise<boolean>;
isEmpty(): Promise<boolean>;
Expand All @@ -14,6 +23,12 @@ export declare class MatSelectHarness extends MatFormFieldControlHarness {
isRequired(): Promise<boolean>;
isValid(): Promise<boolean>;
open(): Promise<void>;
}

export declare class MatSelectHarness extends _MatSelectHarnessBase<typeof MatOptionHarness, MatOptionHarness, OptionHarnessFilters, typeof MatOptgroupHarness, MatOptgroupHarness, OptgroupHarnessFilters> {
protected _optionClass: typeof MatOptionHarness;
protected _optionGroupClass: typeof MatOptgroupHarness;
protected _prefix: string;
static hostSelector: string;
static with(options?: SelectHarnessFilters): HarnessPredicate<MatSelectHarness>;
}
Expand Down

0 comments on commit b32b78b

Please sign in to comment.