Skip to content

refactor(material-experimental/mdc-select): de-duplicate test harness logic #21460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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