Skip to content

Commit 8348afd

Browse files
committed
refactor(material-experimental/mdc-menu): de-duplicate test harness logic
De-duplicates the test harness logic between the base and MDC menu test harnesses.
1 parent 71b7b15 commit 8348afd

File tree

4 files changed

+105
-219
lines changed

4 files changed

+105
-219
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ ts_library(
1010
),
1111
module_name = "@angular/material-experimental/mdc-menu/testing",
1212
deps = [
13-
"//src/cdk/coercion",
1413
"//src/cdk/testing",
1514
"//src/material/menu/testing",
1615
],

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

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

9+
import {HarnessPredicate} from '@angular/cdk/testing';
910
import {
10-
ComponentHarness,
11-
ContentContainerComponentHarness,
12-
HarnessLoader,
13-
HarnessPredicate,
14-
TestElement,
15-
TestKey,
16-
} from '@angular/cdk/testing';
17-
import {coerceBooleanProperty} from '@angular/cdk/coercion';
18-
import {MenuHarnessFilters, MenuItemHarnessFilters} from '@angular/material/menu/testing';
11+
MenuHarnessFilters,
12+
MenuItemHarnessFilters,
13+
_MatMenuItemHarnessBase,
14+
_MatMenuHarnessBase
15+
} from '@angular/material/menu/testing';
1916

2017
/** Harness for interacting with an MDC-based mat-menu in tests. */
21-
export class MatMenuHarness extends ContentContainerComponentHarness<string> {
18+
export class MatMenuHarness extends _MatMenuHarnessBase<
19+
typeof MatMenuItemHarness, MatMenuItemHarness, MenuItemHarnessFilters> {
2220
/** The selector for the host element of a `MatMenu` instance. */
2321
static hostSelector = '.mat-menu-trigger';
24-
25-
private _documentRootLocator = this.documentRootLocatorFactory();
26-
27-
// TODO: potentially extend MatButtonHarness
22+
protected _itemClass = MatMenuItemHarness;
2823

2924
/**
3025
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuHarness` that meets certain
@@ -37,118 +32,14 @@ export class MatMenuHarness extends ContentContainerComponentHarness<string> {
3732
.addOption('triggerText', options.triggerText,
3833
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
3934
}
40-
41-
/** Whether the menu is disabled. */
42-
async isDisabled(): Promise<boolean> {
43-
const disabled = (await this.host()).getAttribute('disabled');
44-
return coerceBooleanProperty(await disabled);
45-
}
46-
47-
/** Whether the menu is open. */
48-
async isOpen(): Promise<boolean> {
49-
return !!(await this._getMenuPanel());
50-
}
51-
52-
/** Gets the text of the menu's trigger element. */
53-
async getTriggerText(): Promise<string> {
54-
return (await this.host()).text();
55-
}
56-
57-
/** Focuses the menu. */
58-
async focus(): Promise<void> {
59-
return (await this.host()).focus();
60-
}
61-
62-
/** Blurs the menu. */
63-
async blur(): Promise<void> {
64-
return (await this.host()).blur();
65-
}
66-
67-
/** Whether the menu is focused. */
68-
async isFocused(): Promise<boolean> {
69-
return (await this.host()).isFocused();
70-
}
71-
72-
/** Opens the menu. */
73-
async open(): Promise<void> {
74-
if (!await this.isOpen()) {
75-
return (await this.host()).click();
76-
}
77-
}
78-
79-
/** Closes the menu. */
80-
async close(): Promise<void> {
81-
const panel = await this._getMenuPanel();
82-
if (panel) {
83-
return panel.sendKeys(TestKey.ESCAPE);
84-
}
85-
}
86-
87-
/**
88-
* Gets a list of `MatMenuItemHarness` representing the items in the menu.
89-
* @param filters Optionally filters which menu items are included.
90-
*/
91-
async getItems(filters: Omit<MenuItemHarnessFilters, 'ancestor'> = {}):
92-
Promise<MatMenuItemHarness[]> {
93-
const panelId = await this._getPanelId();
94-
if (panelId) {
95-
return this._documentRootLocator.locatorForAll(
96-
MatMenuItemHarness.with({...filters, ancestor: `#${panelId}`}))();
97-
}
98-
return [];
99-
}
100-
101-
/**
102-
* Clicks an item in the menu, and optionally continues clicking items in subsequent sub-menus.
103-
* @param itemFilter A filter used to represent which item in the menu should be clicked. The
104-
* first matching menu item will be clicked.
105-
* @param subItemFilters A list of filters representing the items to click in any subsequent
106-
* sub-menus. The first item in the sub-menu matching the corresponding filter in
107-
* `subItemFilters` will be clicked.
108-
*/
109-
async clickItem(
110-
itemFilter: Omit<MenuItemHarnessFilters, 'ancestor'>,
111-
...subItemFilters: Omit<MenuItemHarnessFilters, 'ancestor'>[]): Promise<void> {
112-
await this.open();
113-
const items = await this.getItems(itemFilter);
114-
if (!items.length) {
115-
throw Error(`Could not find item matching ${JSON.stringify(itemFilter)}`);
116-
}
117-
118-
if (!subItemFilters.length) {
119-
return await items[0].click();
120-
}
121-
122-
const menu = await items[0].getSubmenu();
123-
if (!menu) {
124-
throw Error(`Item matching ${JSON.stringify(itemFilter)} does not have a submenu`);
125-
}
126-
return menu.clickItem(...subItemFilters as [Omit<MenuItemHarnessFilters, 'ancestor'>]);
127-
}
128-
129-
protected async getRootHarnessLoader(): Promise<HarnessLoader> {
130-
const panelId = await this._getPanelId();
131-
return this.documentRootLocatorFactory().harnessLoaderFor(`#${panelId}`);
132-
}
133-
134-
/** Gets the menu panel associated with this menu. */
135-
private async _getMenuPanel(): Promise<TestElement | null> {
136-
const panelId = await this._getPanelId();
137-
return panelId ? this._documentRootLocator.locatorForOptional(`#${panelId}`)() : null;
138-
}
139-
140-
/** Gets the id of the menu panel associated with this menu. */
141-
private async _getPanelId(): Promise<string | null> {
142-
const panelId = await (await this.host()).getAttribute('aria-controls');
143-
return panelId || null;
144-
}
14535
}
14636

147-
14837
/** Harness for interacting with an MDC-based mat-menu-item in tests. */
149-
export class MatMenuItemHarness extends ComponentHarness {
38+
export class MatMenuItemHarness extends
39+
_MatMenuItemHarnessBase<typeof MatMenuHarness, MatMenuHarness> {
15040
/** The selector for the host element of a `MatMenuItem` instance. */
15141
static hostSelector = '.mat-mdc-menu-item';
42+
protected _menuClass = MatMenuHarness;
15243

15344
/**
15445
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuItemHarness` that meets
@@ -163,48 +54,4 @@ export class MatMenuItemHarness extends ComponentHarness {
16354
.addOption('hasSubmenu', options.hasSubmenu,
16455
async (harness, hasSubmenu) => (await harness.hasSubmenu()) === hasSubmenu);
16556
}
166-
167-
/** Whether the menu is disabled. */
168-
async isDisabled(): Promise<boolean> {
169-
const disabled = (await this.host()).getAttribute('disabled');
170-
return coerceBooleanProperty(await disabled);
171-
}
172-
173-
/** Gets the text of the menu item. */
174-
async getText(): Promise<string> {
175-
return (await this.host()).text();
176-
}
177-
178-
/** Focuses the menu item. */
179-
async focus(): Promise<void> {
180-
return (await this.host()).focus();
181-
}
182-
183-
/** Blurs the menu item. */
184-
async blur(): Promise<void> {
185-
return (await this.host()).blur();
186-
}
187-
188-
/** Whether the menu item is focused. */
189-
async isFocused(): Promise<boolean> {
190-
return (await this.host()).isFocused();
191-
}
192-
193-
/** Clicks the menu item. */
194-
async click(): Promise<void> {
195-
return (await this.host()).click();
196-
}
197-
198-
/** Whether this item has a submenu. */
199-
async hasSubmenu(): Promise<boolean> {
200-
return (await this.host()).matchesSelector(MatMenuHarness.hostSelector);
201-
}
202-
203-
/** Gets the submenu associated with this menu item, or null if none. */
204-
async getSubmenu(): Promise<MatMenuHarness | null> {
205-
if (await this.hasSubmenu()) {
206-
return new MatMenuHarness(this.locatorFactory);
207-
}
208-
return null;
209-
}
21057
}

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

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
*/
88

99
import {
10+
BaseHarnessFilters,
11+
ComponentHarness,
12+
ComponentHarnessConstructor,
1013
ContentContainerComponentHarness,
1114
HarnessLoader,
1215
HarnessPredicate,
@@ -16,27 +19,19 @@ import {
1619
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1720
import {MenuHarnessFilters, MenuItemHarnessFilters} from './menu-harness-filters';
1821

19-
/** Harness for interacting with a standard mat-menu in tests. */
20-
export class MatMenuHarness extends ContentContainerComponentHarness<string> {
21-
/** The selector for the host element of a `MatMenu` instance. */
22-
static hostSelector = '.mat-menu-trigger';
23-
22+
export abstract class _MatMenuHarnessBase<
23+
ItemType extends (ComponentHarnessConstructor<Item> & {
24+
with: (options?: ItemFilters) => HarnessPredicate<Item>}),
25+
Item extends ComponentHarness & {
26+
click(): Promise<void>,
27+
getSubmenu(): Promise<_MatMenuHarnessBase<ItemType, Item, ItemFilters> | null>},
28+
ItemFilters extends BaseHarnessFilters
29+
> extends ContentContainerComponentHarness<string> {
2430
private _documentRootLocator = this.documentRootLocatorFactory();
31+
protected abstract _itemClass: ItemType;
2532

2633
// TODO: potentially extend MatButtonHarness
2734

28-
/**
29-
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuHarness` that meets certain
30-
* criteria.
31-
* @param options Options for filtering which menu instances are considered a match.
32-
* @return a `HarnessPredicate` configured with the given options.
33-
*/
34-
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
35-
return new HarnessPredicate(MatMenuHarness, options)
36-
.addOption('triggerText', options.triggerText,
37-
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
38-
}
39-
4035
/** Whether the menu is disabled. */
4136
async isDisabled(): Promise<boolean> {
4237
const disabled = (await this.host()).getAttribute('disabled');
@@ -87,12 +82,13 @@ export class MatMenuHarness extends ContentContainerComponentHarness<string> {
8782
* Gets a list of `MatMenuItemHarness` representing the items in the menu.
8883
* @param filters Optionally filters which menu items are included.
8984
*/
90-
async getItems(filters: Omit<MenuItemHarnessFilters, 'ancestor'> = {}):
91-
Promise<MatMenuItemHarness[]> {
85+
async getItems(filters?: Omit<ItemFilters, 'ancestor'>): Promise<Item[]> {
9286
const panelId = await this._getPanelId();
9387
if (panelId) {
94-
return this._documentRootLocator.locatorForAll(
95-
MatMenuItemHarness.with({...filters, ancestor: `#${panelId}`}))();
88+
return this._documentRootLocator.locatorForAll(this._itemClass.with({
89+
...(filters || {}),
90+
ancestor: `#${panelId}`
91+
} as ItemFilters))();
9692
}
9793
return [];
9894
}
@@ -106,8 +102,8 @@ export class MatMenuHarness extends ContentContainerComponentHarness<string> {
106102
* `subItemFilters` will be clicked.
107103
*/
108104
async clickItem(
109-
itemFilter: Omit<MenuItemHarnessFilters, 'ancestor'>,
110-
...subItemFilters: Omit<MenuItemHarnessFilters, 'ancestor'>[]): Promise<void> {
105+
itemFilter: Omit<ItemFilters, 'ancestor'>,
106+
...subItemFilters: Omit<ItemFilters, 'ancestor'>[]): Promise<void> {
111107
await this.open();
112108
const items = await this.getItems(itemFilter);
113109
if (!items.length) {
@@ -122,7 +118,7 @@ export class MatMenuHarness extends ContentContainerComponentHarness<string> {
122118
if (!menu) {
123119
throw Error(`Item matching ${JSON.stringify(itemFilter)} does not have a submenu`);
124120
}
125-
return menu.clickItem(...subItemFilters as [Omit<MenuItemHarnessFilters, 'ancestor'>]);
121+
return menu.clickItem(...subItemFilters as [Omit<ItemFilters, 'ancestor'>]);
126122
}
127123

128124
protected async getRootHarnessLoader(): Promise<HarnessLoader> {
@@ -143,25 +139,11 @@ export class MatMenuHarness extends ContentContainerComponentHarness<string> {
143139
}
144140
}
145141

146-
147-
/** Harness for interacting with a standard mat-menu-item in tests. */
148-
export class MatMenuItemHarness extends ContentContainerComponentHarness<string> {
149-
/** The selector for the host element of a `MatMenuItem` instance. */
150-
static hostSelector = '.mat-menu-item';
151-
152-
/**
153-
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuItemHarness` that meets
154-
* certain criteria.
155-
* @param options Options for filtering which menu item instances are considered a match.
156-
* @return a `HarnessPredicate` configured with the given options.
157-
*/
158-
static with(options: MenuItemHarnessFilters = {}): HarnessPredicate<MatMenuItemHarness> {
159-
return new HarnessPredicate(MatMenuItemHarness, options)
160-
.addOption('text', options.text,
161-
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
162-
.addOption('hasSubmenu', options.hasSubmenu,
163-
async (harness, hasSubmenu) => (await harness.hasSubmenu()) === hasSubmenu);
164-
}
142+
export abstract class _MatMenuItemHarnessBase<
143+
MenuType extends ComponentHarnessConstructor<Menu>,
144+
Menu extends ComponentHarness,
145+
> extends ContentContainerComponentHarness<string> {
146+
protected abstract _menuClass: MenuType;
165147

166148
/** Whether the menu is disabled. */
167149
async isDisabled(): Promise<boolean> {
@@ -196,14 +178,57 @@ export class MatMenuItemHarness extends ContentContainerComponentHarness<string>
196178

197179
/** Whether this item has a submenu. */
198180
async hasSubmenu(): Promise<boolean> {
199-
return (await this.host()).matchesSelector(MatMenuHarness.hostSelector);
181+
return (await this.host()).matchesSelector(this._menuClass.hostSelector);
200182
}
201183

202184
/** Gets the submenu associated with this menu item, or null if none. */
203-
async getSubmenu(): Promise<MatMenuHarness | null> {
185+
async getSubmenu(): Promise<Menu | null> {
204186
if (await this.hasSubmenu()) {
205-
return new MatMenuHarness(this.locatorFactory);
187+
return new this._menuClass(this.locatorFactory);
206188
}
207189
return null;
208190
}
209191
}
192+
193+
194+
/** Harness for interacting with a standard mat-menu in tests. */
195+
export class MatMenuHarness extends _MatMenuHarnessBase<
196+
typeof MatMenuItemHarness, MatMenuItemHarness, MenuItemHarnessFilters> {
197+
/** The selector for the host element of a `MatMenu` instance. */
198+
static hostSelector = '.mat-menu-trigger';
199+
protected _itemClass = MatMenuItemHarness;
200+
201+
/**
202+
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuHarness` that meets certain
203+
* criteria.
204+
* @param options Options for filtering which menu instances are considered a match.
205+
* @return a `HarnessPredicate` configured with the given options.
206+
*/
207+
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
208+
return new HarnessPredicate(MatMenuHarness, options)
209+
.addOption('triggerText', options.triggerText,
210+
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
211+
}
212+
}
213+
214+
/** Harness for interacting with a standard mat-menu-item in tests. */
215+
export class MatMenuItemHarness extends
216+
_MatMenuItemHarnessBase<typeof MatMenuHarness, MatMenuHarness> {
217+
/** The selector for the host element of a `MatMenuItem` instance. */
218+
static hostSelector = '.mat-menu-item';
219+
protected _menuClass = MatMenuHarness;
220+
221+
/**
222+
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuItemHarness` that meets
223+
* certain criteria.
224+
* @param options Options for filtering which menu item instances are considered a match.
225+
* @return a `HarnessPredicate` configured with the given options.
226+
*/
227+
static with(options: MenuItemHarnessFilters = {}): HarnessPredicate<MatMenuItemHarness> {
228+
return new HarnessPredicate(MatMenuItemHarness, options)
229+
.addOption('text', options.text,
230+
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
231+
.addOption('hasSubmenu', options.hasSubmenu,
232+
async (harness, hasSubmenu) => (await harness.hasSubmenu()) === hasSubmenu);
233+
}
234+
}

0 commit comments

Comments
 (0)