Skip to content

Commit 576e694

Browse files
crisbetoandrewseguin
authored andcommitted
refactor(material-experimental/mdc-select): de-duplicate test harness logic (#21460)
Reworks the `MatSelectHarness` in order to de-duplicate the logic between the base and MDC implementations. (cherry picked from commit abdc4a0)
1 parent 278d71c commit 576e694

File tree

4 files changed

+89
-162
lines changed

4 files changed

+89
-162
lines changed

src/material-experimental/mdc-select/testing/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ts_library(
1414
"//src/cdk/testing",
1515
"//src/material-experimental/mdc-core/testing",
1616
"//src/material/form-field/testing/control",
17+
"//src/material/select/testing",
1718
],
1819
)
1920

src/material-experimental/mdc-select/testing/select-harness.ts

Lines changed: 9 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

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

1919

2020
/** Harness for interacting with an MDC-based mat-select in tests. */
21-
export class MatSelectHarness extends MatFormFieldControlHarness {
22-
private _documentRootLocator = this.documentRootLocatorFactory();
23-
private _backdrop = this._documentRootLocator.locatorFor('.cdk-overlay-backdrop');
24-
private _trigger = this.locatorFor('.mat-mdc-select-trigger');
25-
private _value = this.locatorFor('.mat-mdc-select-value');
26-
21+
export class MatSelectHarness extends _MatSelectHarnessBase<
22+
typeof MatOptionHarness, MatOptionHarness, OptionHarnessFilters,
23+
typeof MatOptgroupHarness, MatOptgroupHarness, OptgroupHarnessFilters
24+
> {
2725
static hostSelector = '.mat-mdc-select';
26+
protected _prefix = 'mat-mdc';
27+
protected _optionClass = MatOptionHarness;
28+
protected _optionGroupClass = MatOptgroupHarness;
2829

2930
/**
3031
* Gets a `HarnessPredicate` that can be used to search for a `MatSelectHarness` that meets
@@ -35,116 +36,4 @@ export class MatSelectHarness extends MatFormFieldControlHarness {
3536
static with(options: SelectHarnessFilters = {}): HarnessPredicate<MatSelectHarness> {
3637
return new HarnessPredicate(MatSelectHarness, options);
3738
}
38-
39-
/** Gets a boolean promise indicating if the select is disabled. */
40-
async isDisabled(): Promise<boolean> {
41-
return (await this.host()).hasClass('mat-mdc-select-disabled');
42-
}
43-
44-
/** Gets a boolean promise indicating if the select is valid. */
45-
async isValid(): Promise<boolean> {
46-
return !(await (await this.host()).hasClass('ng-invalid'));
47-
}
48-
49-
/** Gets a boolean promise indicating if the select is required. */
50-
async isRequired(): Promise<boolean> {
51-
return (await this.host()).hasClass('mat-mdc-select-required');
52-
}
53-
54-
/** Gets a boolean promise indicating if the select is empty (no value is selected). */
55-
async isEmpty(): Promise<boolean> {
56-
return (await this.host()).hasClass('mat-mdc-select-empty');
57-
}
58-
59-
/** Gets a boolean promise indicating if the select is in multi-selection mode. */
60-
async isMultiple(): Promise<boolean> {
61-
return (await this.host()).hasClass('mat-mdc-select-multiple');
62-
}
63-
64-
/** Gets a promise for the select's value text. */
65-
async getValueText(): Promise<string> {
66-
return (await this._value()).text();
67-
}
68-
69-
/** Focuses the select and returns a void promise that indicates when the action is complete. */
70-
async focus(): Promise<void> {
71-
return (await this.host()).focus();
72-
}
73-
74-
/** Blurs the select and returns a void promise that indicates when the action is complete. */
75-
async blur(): Promise<void> {
76-
return (await this.host()).blur();
77-
}
78-
79-
/** Whether the select is focused. */
80-
async isFocused(): Promise<boolean> {
81-
return (await this.host()).isFocused();
82-
}
83-
84-
/** Gets the options inside the select panel. */
85-
async getOptions(filter: Omit<OptionHarnessFilters, 'ancestor'> = {}):
86-
Promise<MatOptionHarness[]> {
87-
return this._documentRootLocator.locatorForAll(MatOptionHarness.with({
88-
...filter,
89-
ancestor: await this._getPanelSelector()
90-
}))();
91-
}
92-
93-
/** Gets the groups of options inside the panel. */
94-
async getOptionGroups(filter: Omit<OptgroupHarnessFilters, 'ancestor'> = {}):
95-
Promise<MatOptgroupHarness[]> {
96-
return this._documentRootLocator.locatorForAll(MatOptgroupHarness.with({
97-
...filter,
98-
ancestor: await this._getPanelSelector()
99-
}))();
100-
}
101-
102-
/** Gets whether the select is open. */
103-
async isOpen(): Promise<boolean> {
104-
return !!await this._documentRootLocator.locatorForOptional(await this._getPanelSelector())();
105-
}
106-
107-
/** Opens the select's panel. */
108-
async open(): Promise<void> {
109-
if (!await this.isOpen()) {
110-
return (await this._trigger()).click();
111-
}
112-
}
113-
114-
/**
115-
* Clicks the options that match the passed-in filter. If the select is in multi-selection
116-
* mode all options will be clicked, otherwise the harness will pick the first matching option.
117-
*/
118-
async clickOptions(filter: OptionHarnessFilters = {}): Promise<void> {
119-
await this.open();
120-
121-
const [isMultiple, options] =
122-
await parallel(() => [this.isMultiple(), this.getOptions(filter)]);
123-
124-
if (options.length === 0) {
125-
throw Error('Select does not have options matching the specified filter');
126-
}
127-
128-
if (isMultiple) {
129-
await parallel(() => options.map(option => option.click()));
130-
} else {
131-
await options[0].click();
132-
}
133-
}
134-
135-
/** Closes the select's panel. */
136-
async close(): Promise<void> {
137-
if (await this.isOpen()) {
138-
// This is the most consistent way that works both in both single and multi-select modes,
139-
// but it assumes that only one overlay is open at a time. We should be able to make it
140-
// a bit more precise after #16645 where we can dispatch an ESCAPE press to the host instead.
141-
return (await this._backdrop()).click();
142-
}
143-
}
144-
145-
/** Gets the selector that should be used to find this select's panel. */
146-
private async _getPanelSelector(): Promise<string> {
147-
const id = await (await this.host()).getAttribute('id');
148-
return `#${id}-panel`;
149-
}
15039
}

src/material/select/testing/select-harness.ts

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {HarnessPredicate, parallel} from '@angular/cdk/testing';
9+
import {
10+
HarnessPredicate,
11+
parallel,
12+
ComponentHarness,
13+
BaseHarnessFilters,
14+
ComponentHarnessConstructor,
15+
} from '@angular/cdk/testing';
1016
import {MatFormFieldControlHarness} from '@angular/material/form-field/testing/control';
1117
import {
1218
MatOptionHarness,
@@ -16,29 +22,25 @@ import {
1622
} from '@angular/material/core/testing';
1723
import {SelectHarnessFilters} from './select-harness-filters';
1824

19-
20-
/** Harness for interacting with a standard mat-select in tests. */
21-
export class MatSelectHarness extends MatFormFieldControlHarness {
25+
export abstract class _MatSelectHarnessBase<
26+
OptionType extends (ComponentHarnessConstructor<Option> & {
27+
with: (options?: OptionFilters) => HarnessPredicate<Option>}),
28+
Option extends ComponentHarness & {click(): Promise<void>},
29+
OptionFilters extends BaseHarnessFilters,
30+
OptionGroupType extends (ComponentHarnessConstructor<OptionGroup> & {
31+
with: (options?: OptionGroupFilters) => HarnessPredicate<OptionGroup>}),
32+
OptionGroup extends ComponentHarness,
33+
OptionGroupFilters extends BaseHarnessFilters
34+
> extends MatFormFieldControlHarness {
35+
protected abstract _prefix: string;
36+
protected abstract _optionClass: OptionType;
37+
protected abstract _optionGroupClass: OptionGroupType;
2238
private _documentRootLocator = this.documentRootLocatorFactory();
2339
private _backdrop = this._documentRootLocator.locatorFor('.cdk-overlay-backdrop');
24-
private _trigger = this.locatorFor('.mat-select-trigger');
25-
private _value = this.locatorFor('.mat-select-value');
26-
27-
static hostSelector = '.mat-select';
28-
29-
/**
30-
* Gets a `HarnessPredicate` that can be used to search for a `MatSelectHarness` that meets
31-
* certain criteria.
32-
* @param options Options for filtering which select instances are considered a match.
33-
* @return a `HarnessPredicate` configured with the given options.
34-
*/
35-
static with(options: SelectHarnessFilters = {}): HarnessPredicate<MatSelectHarness> {
36-
return new HarnessPredicate(MatSelectHarness, options);
37-
}
3840

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

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

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

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

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

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

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

8487
/** Gets the options inside the select panel. */
85-
async getOptions(filter: Omit<OptionHarnessFilters, 'ancestor'> = {}):
86-
Promise<MatOptionHarness[]> {
87-
return this._documentRootLocator.locatorForAll(MatOptionHarness.with({
88-
...filter,
88+
async getOptions(filter?: Omit<OptionFilters, 'ancestor'>): Promise<Option[]> {
89+
return this._documentRootLocator.locatorForAll(this._optionClass.with({
90+
...(filter || {}),
8991
ancestor: await this._getPanelSelector()
90-
}))();
92+
} as OptionFilters))();
9193
}
9294

9395
/** Gets the groups of options inside the panel. */
94-
async getOptionGroups(filter: Omit<OptgroupHarnessFilters, 'ancestor'> = {}):
95-
Promise<MatOptgroupHarness[]> {
96-
return this._documentRootLocator.locatorForAll(MatOptgroupHarness.with({
97-
...filter,
96+
async getOptionGroups(filter?: Omit<OptionGroupFilters, 'ancestor'>): Promise<OptionGroup[]> {
97+
return this._documentRootLocator.locatorForAll(this._optionGroupClass.with({
98+
...(filter || {}),
9899
ancestor: await this._getPanelSelector()
99-
}))();
100+
} as OptionGroupFilters))() as Promise<OptionGroup[]>;
100101
}
101102

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

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

121-
const [isMultiple, options] = await parallel(() => {
122-
return [this.isMultiple(), this.getOptions(filter)];
123-
});
123+
const [isMultiple, options] =
124+
await parallel(() => [this.isMultiple(), this.getOptions(filter)]);
124125

125126
if (options.length === 0) {
126127
throw Error('Select does not have options matching the specified filter');
@@ -149,3 +150,24 @@ export class MatSelectHarness extends MatFormFieldControlHarness {
149150
return `#${id}-panel`;
150151
}
151152
}
153+
154+
/** Harness for interacting with a standard mat-select in tests. */
155+
export class MatSelectHarness extends _MatSelectHarnessBase<
156+
typeof MatOptionHarness, MatOptionHarness, OptionHarnessFilters,
157+
typeof MatOptgroupHarness, MatOptgroupHarness, OptgroupHarnessFilters
158+
> {
159+
static hostSelector = '.mat-select';
160+
protected _prefix = 'mat';
161+
protected _optionClass = MatOptionHarness;
162+
protected _optionGroupClass = MatOptgroupHarness;
163+
164+
/**
165+
* Gets a `HarnessPredicate` that can be used to search for a `MatSelectHarness` that meets
166+
* certain criteria.
167+
* @param options Options for filtering which select instances are considered a match.
168+
* @return a `HarnessPredicate` configured with the given options.
169+
*/
170+
static with(options: SelectHarnessFilters = {}): HarnessPredicate<MatSelectHarness> {
171+
return new HarnessPredicate(MatSelectHarness, options);
172+
}
173+
}

tools/public_api_guard/material/select/testing.d.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
export declare class MatSelectHarness extends MatFormFieldControlHarness {
1+
export declare abstract class _MatSelectHarnessBase<OptionType extends (ComponentHarnessConstructor<Option> & {
2+
with: (options?: OptionFilters) => HarnessPredicate<Option>;
3+
}), Option extends ComponentHarness & {
4+
click(): Promise<void>;
5+
}, OptionFilters extends BaseHarnessFilters, OptionGroupType extends (ComponentHarnessConstructor<OptionGroup> & {
6+
with: (options?: OptionGroupFilters) => HarnessPredicate<OptionGroup>;
7+
}), OptionGroup extends ComponentHarness, OptionGroupFilters extends BaseHarnessFilters> extends MatFormFieldControlHarness {
8+
protected abstract _optionClass: OptionType;
9+
protected abstract _optionGroupClass: OptionGroupType;
10+
protected abstract _prefix: string;
211
blur(): Promise<void>;
3-
clickOptions(filter?: OptionHarnessFilters): Promise<void>;
12+
clickOptions(filter?: OptionFilters): Promise<void>;
413
close(): Promise<void>;
514
focus(): Promise<void>;
6-
getOptionGroups(filter?: Omit<OptgroupHarnessFilters, 'ancestor'>): Promise<MatOptgroupHarness[]>;
7-
getOptions(filter?: Omit<OptionHarnessFilters, 'ancestor'>): Promise<MatOptionHarness[]>;
15+
getOptionGroups(filter?: Omit<OptionGroupFilters, 'ancestor'>): Promise<OptionGroup[]>;
16+
getOptions(filter?: Omit<OptionFilters, 'ancestor'>): Promise<Option[]>;
817
getValueText(): Promise<string>;
918
isDisabled(): Promise<boolean>;
1019
isEmpty(): Promise<boolean>;
@@ -14,6 +23,12 @@ export declare class MatSelectHarness extends MatFormFieldControlHarness {
1423
isRequired(): Promise<boolean>;
1524
isValid(): Promise<boolean>;
1625
open(): Promise<void>;
26+
}
27+
28+
export declare class MatSelectHarness extends _MatSelectHarnessBase<typeof MatOptionHarness, MatOptionHarness, OptionHarnessFilters, typeof MatOptgroupHarness, MatOptgroupHarness, OptgroupHarnessFilters> {
29+
protected _optionClass: typeof MatOptionHarness;
30+
protected _optionGroupClass: typeof MatOptgroupHarness;
31+
protected _prefix: string;
1732
static hostSelector: string;
1833
static with(options?: SelectHarnessFilters): HarnessPredicate<MatSelectHarness>;
1934
}

0 commit comments

Comments
 (0)