Skip to content

Commit 0178813

Browse files
crisbetowagnermaciel
authored andcommitted
fix(material-experimental/mdc-menu/testing): add unimplemented methods (#20352)
Adds the logic for the methods that weren't implemented for the MDC-based menu harness. (cherry picked from commit e784f8f)
1 parent 1768036 commit 0178813

File tree

3 files changed

+97
-43
lines changed

3 files changed

+97
-43
lines changed

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,5 @@ import {MatMenuModule} from '../index';
33
import {MatMenuHarness} from './menu-harness';
44

55
describe('MDC-based MatMenuHarness', () => {
6-
it('TODO: re-enable after implementing missing methods', () => expect(true).toBe(true));
7-
if (false) {
8-
runHarnessTests(MatMenuModule, MatMenuHarness as any);
9-
}
6+
runHarnessTests(MatMenuModule, MatMenuHarness as any);
107
});

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

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

9-
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
9+
import {ComponentHarness, HarnessPredicate, TestElement, TestKey} from '@angular/cdk/testing';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
11-
import {
12-
MenuHarnessFilters,
13-
MenuItemHarnessFilters
14-
} from '@angular/material/menu/testing';
11+
import {MenuHarnessFilters, MenuItemHarnessFilters} from '@angular/material/menu/testing';
1512

16-
/** Harness for interacting with a MDC-based mat-menu in tests. */
13+
/** Harness for interacting with an MDC-based mat-menu in tests. */
1714
export class MatMenuHarness extends ComponentHarness {
15+
/** The selector for the host element of a `MatMenu` instance. */
1816
static hostSelector = '.mat-menu-trigger';
1917

18+
private _documentRootLocator = this.documentRootLocatorFactory();
19+
2020
// TODO: potentially extend MatButtonHarness
2121

2222
/**
23-
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
24-
* @param options Options for narrowing the search:
25-
* - `selector` finds a menu whose host element matches the given selector.
26-
* - `label` finds a menu with specific label text.
23+
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuHarness` that meets certain
24+
* criteria.
25+
* @param options Options for filtering which menu instances are considered a match.
2726
* @return a `HarnessPredicate` configured with the given options.
2827
*/
2928
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
@@ -32,26 +31,28 @@ export class MatMenuHarness extends ComponentHarness {
3231
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
3332
}
3433

35-
/** Gets a boolean promise indicating if the menu is disabled. */
34+
/** Whether the menu is disabled. */
3635
async isDisabled(): Promise<boolean> {
3736
const disabled = (await this.host()).getAttribute('disabled');
3837
return coerceBooleanProperty(await disabled);
3938
}
4039

40+
/** Whether the menu is open. */
4141
async isOpen(): Promise<boolean> {
42-
throw Error('not implemented');
42+
return !!(await this._getMenuPanel());
4343
}
4444

45+
/** Gets the text of the menu's trigger element. */
4546
async getTriggerText(): Promise<string> {
4647
return (await this.host()).text();
4748
}
4849

49-
/** Focuses the menu and returns a void promise that indicates when the action is complete. */
50+
/** Focuses the menu. */
5051
async focus(): Promise<void> {
5152
return (await this.host()).focus();
5253
}
5354

54-
/** Blurs the menu and returns a void promise that indicates when the action is complete. */
55+
/** Blurs the menu. */
5556
async blur(): Promise<void> {
5657
return (await this.host()).blur();
5758
}
@@ -61,35 +62,86 @@ export class MatMenuHarness extends ComponentHarness {
6162
return (await this.host()).isFocused();
6263
}
6364

65+
/** Opens the menu. */
6466
async open(): Promise<void> {
65-
throw Error('not implemented');
67+
if (!await this.isOpen()) {
68+
return (await this.host()).click();
69+
}
6670
}
6771

72+
/** Closes the menu. */
6873
async close(): Promise<void> {
69-
throw Error('not implemented');
74+
const panel = await this._getMenuPanel();
75+
if (panel) {
76+
return panel.sendKeys(TestKey.ESCAPE);
77+
}
7078
}
7179

80+
/**
81+
* Gets a list of `MatMenuItemHarness` representing the items in the menu.
82+
* @param filters Optionally filters which menu items are included.
83+
*/
7284
async getItems(filters: Omit<MenuItemHarnessFilters, 'ancestor'> = {}):
7385
Promise<MatMenuItemHarness[]> {
74-
throw Error('not implemented');
86+
const panelId = await this._getPanelId();
87+
if (panelId) {
88+
return this._documentRootLocator.locatorForAll(
89+
MatMenuItemHarness.with({...filters, ancestor: `#${panelId}`}))();
90+
}
91+
return [];
7592
}
7693

77-
async clickItem(filter: Omit<MenuItemHarnessFilters, 'ancestor'>,
78-
...filters: Omit<MenuItemHarnessFilters, 'ancestor'>[]): Promise<void> {
79-
throw Error('not implemented');
94+
/**
95+
* Clicks an item in the menu, and optionally continues clicking items in subsequent sub-menus.
96+
* @param itemFilter A filter used to represent which item in the menu should be clicked. The
97+
* first matching menu item will be clicked.
98+
* @param subItemFilters A list of filters representing the items to click in any subsequent
99+
* sub-menus. The first item in the sub-menu matching the corresponding filter in
100+
* `subItemFilters` will be clicked.
101+
*/
102+
async clickItem(
103+
itemFilter: Omit<MenuItemHarnessFilters, 'ancestor'>,
104+
...subItemFilters: Omit<MenuItemHarnessFilters, 'ancestor'>[]): Promise<void> {
105+
await this.open();
106+
const items = await this.getItems(itemFilter);
107+
if (!items.length) {
108+
throw Error(`Could not find item matching ${JSON.stringify(itemFilter)}`);
109+
}
110+
111+
if (!subItemFilters.length) {
112+
return await items[0].click();
113+
}
114+
115+
const menu = await items[0].getSubmenu();
116+
if (!menu) {
117+
throw Error(`Item matching ${JSON.stringify(itemFilter)} does not have a submenu`);
118+
}
119+
return menu.clickItem(...subItemFilters as [Omit<MenuItemHarnessFilters, 'ancestor'>]);
120+
}
121+
122+
/** Gets the menu panel associated with this menu. */
123+
private async _getMenuPanel(): Promise<TestElement | null> {
124+
const panelId = await this._getPanelId();
125+
return panelId ? this._documentRootLocator.locatorForOptional(`#${panelId}`)() : null;
126+
}
127+
128+
/** Gets the id of the menu panel associated with this menu. */
129+
private async _getPanelId(): Promise<string | null> {
130+
const panelId = await (await this.host()).getAttribute('aria-controls');
131+
return panelId || null;
80132
}
81133
}
82134

83135

84-
/** Harness for interacting with a standard mat-menu in tests. */
136+
/** Harness for interacting with an MDC-based mat-menu-item in tests. */
85137
export class MatMenuItemHarness extends ComponentHarness {
86-
static hostSelector = '.mat-menu-item';
138+
/** The selector for the host element of a `MatMenuItem` instance. */
139+
static hostSelector = '.mat-mdc-menu-item';
87140

88141
/**
89-
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
90-
* @param options Options for narrowing the search:
91-
* - `selector` finds a menu item whose host element matches the given selector.
92-
* - `label` finds a menu item with specific label text.
142+
* Gets a `HarnessPredicate` that can be used to search for a `MatMenuItemHarness` that meets
143+
* certain criteria.
144+
* @param options Options for filtering which menu item instances are considered a match.
93145
* @return a `HarnessPredicate` configured with the given options.
94146
*/
95147
static with(options: MenuItemHarnessFilters = {}): HarnessPredicate<MatMenuItemHarness> {
@@ -100,24 +152,23 @@ export class MatMenuItemHarness extends ComponentHarness {
100152
async (harness, hasSubmenu) => (await harness.hasSubmenu()) === hasSubmenu);
101153
}
102154

103-
/** Gets a boolean promise indicating if the menu is disabled. */
155+
/** Whether the menu is disabled. */
104156
async isDisabled(): Promise<boolean> {
105157
const disabled = (await this.host()).getAttribute('disabled');
106158
return coerceBooleanProperty(await disabled);
107159
}
108160

161+
/** Gets the text of the menu item. */
109162
async getText(): Promise<string> {
110163
return (await this.host()).text();
111164
}
112165

113-
/**
114-
* Focuses the menu item and returns a void promise that indicates when the action is complete.
115-
*/
166+
/** Focuses the menu item. */
116167
async focus(): Promise<void> {
117168
return (await this.host()).focus();
118169
}
119170

120-
/** Blurs the menu item and returns a void promise that indicates when the action is complete. */
171+
/** Blurs the menu item. */
121172
async blur(): Promise<void> {
122173
return (await this.host()).blur();
123174
}
@@ -127,15 +178,21 @@ export class MatMenuItemHarness extends ComponentHarness {
127178
return (await this.host()).isFocused();
128179
}
129180

181+
/** Clicks the menu item. */
130182
async click(): Promise<void> {
131-
throw Error('not implemented');
183+
return (await this.host()).click();
132184
}
133185

186+
/** Whether this item has a submenu. */
134187
async hasSubmenu(): Promise<boolean> {
135-
throw Error('not implemented');
188+
return (await this.host()).matchesSelector(MatMenuHarness.hostSelector);
136189
}
137190

191+
/** Gets the submenu associated with this menu item, or null if none. */
138192
async getSubmenu(): Promise<MatMenuHarness | null> {
139-
throw Error('not implemented');
193+
if (await this.hasSubmenu()) {
194+
return new MatMenuHarness(this.locatorFactory);
195+
}
196+
return null;
140197
}
141198
}

src/material/menu/testing/shared.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export function runHarnessTests(
123123
});
124124

125125
it('should get submenus', async () => {
126-
const menu1 = await loader.getHarness(MatMenuHarness.with({triggerText: 'Menu 1'}));
126+
const menu1 = await loader.getHarness(menuHarness.with({triggerText: 'Menu 1'}));
127127

128128
await menu1.open();
129129
let submenus = await menu1.getItems({hasSubmenu: true});
@@ -147,25 +147,25 @@ export function runHarnessTests(
147147
});
148148

149149
it('should select item in top-level menu', async () => {
150-
const menu1 = await loader.getHarness(MatMenuHarness.with({triggerText: 'Menu 1'}));
150+
const menu1 = await loader.getHarness(menuHarness.with({triggerText: 'Menu 1'}));
151151
await menu1.clickItem({text: /Leaf/});
152152
expect(fixture.componentInstance.lastClickedLeaf).toBe(1);
153153
});
154154

155155
it('should throw when item is not found', async () => {
156-
const menu1 = await loader.getHarness(MatMenuHarness.with({triggerText: 'Menu 1'}));
156+
const menu1 = await loader.getHarness(menuHarness.with({triggerText: 'Menu 1'}));
157157
await expectAsync(menu1.clickItem({text: 'Fake Item'})).toBeRejectedWithError(
158158
/Could not find item matching {"text":"Fake Item"}/);
159159
});
160160

161161
it('should select item in nested menu', async () => {
162-
const menu1 = await loader.getHarness(MatMenuHarness.with({triggerText: 'Menu 1'}));
162+
const menu1 = await loader.getHarness(menuHarness.with({triggerText: 'Menu 1'}));
163163
await menu1.clickItem({text: 'Menu 3'}, {text: 'Menu 4'}, {text: /Leaf/});
164164
expect(fixture.componentInstance.lastClickedLeaf).toBe(3);
165165
});
166166

167167
it('should throw when intermediate item does not have submenu', async () => {
168-
const menu1 = await loader.getHarness(MatMenuHarness.with({triggerText: 'Menu 1'}));
168+
const menu1 = await loader.getHarness(menuHarness.with({triggerText: 'Menu 1'}));
169169
await expectAsync(menu1.clickItem({text: 'Leaf Item 1'}, {})).toBeRejectedWithError(
170170
/Item matching {"text":"Leaf Item 1"} does not have a submenu/);
171171
});

0 commit comments

Comments
 (0)