Skip to content

Commit 57e698b

Browse files
committed
feat(material/menu/testing): finish implementing harness
1 parent 46446e3 commit 57e698b

File tree

8 files changed

+289
-139
lines changed

8 files changed

+289
-139
lines changed

src/cdk/testing/component-harness.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export interface LocatorFactory {
157157
* should be inherited when defining user's own harness.
158158
*/
159159
export abstract class ComponentHarness {
160-
constructor(private readonly locatorFactory: LocatorFactory) {}
160+
constructor(protected readonly locatorFactory: LocatorFactory) {}
161161

162162
/** Gets a `Promise` for the `TestElement` representing the host element of the component. */
163163
async host(): Promise<TestElement> {

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

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
1010
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1111
import {MatMenuItemHarness} from './menu-item-harness';
12-
import {MenuHarnessFilters} from '@angular/material/menu/testing';
12+
import {MenuHarnessFilters, MenuItemHarnessFilters} from '@angular/material/menu/testing';
1313

1414
/**
1515
* Harness for interacting with a MDC-based mat-menu in tests.
@@ -29,7 +29,7 @@ export class MatMenuHarness extends ComponentHarness {
2929
*/
3030
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
3131
return new HarnessPredicate(MatMenuHarness, options)
32-
.addOption('text', options.triggerText,
32+
.addOption('triggerText', options.triggerText,
3333
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
3434
}
3535

@@ -65,23 +65,7 @@ export class MatMenuHarness extends ComponentHarness {
6565
throw Error('not implemented');
6666
}
6767

68-
async getItems(): Promise<MatMenuItemHarness[]> {
69-
throw Error('not implemented');
70-
}
71-
72-
async getItemLabels(): Promise<string[]> {
73-
throw Error('not implemented');
74-
}
75-
76-
async getItemByLabel(): Promise<MatMenuItemHarness> {
77-
throw Error('not implemented');
78-
}
79-
80-
async getItemByIndex(): Promise<MatMenuItemHarness> {
81-
throw Error('not implemented');
82-
}
83-
84-
async getFocusedItem(): Promise<MatMenuItemHarness> {
68+
async getItems(filters: MenuItemHarnessFilters = {}): Promise<MatMenuItemHarness[]> {
8569
throw Error('not implemented');
8670
}
8771
}

src/material/menu/testing/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ ng_test_library(
2626
deps = [
2727
":testing",
2828
"//src/cdk/overlay",
29+
"//src/cdk/private/testing",
2930
"//src/cdk/testing",
3031
"//src/cdk/testing/testbed",
3132
"//src/material/menu",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export interface MenuHarnessFilters extends BaseHarnessFilters {
1414

1515
export interface MenuItemHarnessFilters extends BaseHarnessFilters {
1616
text?: string | RegExp;
17+
hasSubmenu?: boolean;
1718
}

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

Lines changed: 106 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
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 {MenuHarnessFilters} from './menu-harness-filters';
12-
import {MatMenuItemHarness} from './menu-item-harness';
11+
import {MenuHarnessFilters, MenuItemHarnessFilters} from './menu-harness-filters';
1312

1413
/**
1514
* Harness for interacting with a standard mat-menu in tests.
@@ -18,6 +17,8 @@ import {MatMenuItemHarness} from './menu-item-harness';
1817
export class MatMenuHarness extends ComponentHarness {
1918
static hostSelector = '.mat-menu-trigger';
2019

20+
private _documentRootLocator = this.documentRootLocatorFactory();
21+
2122
// TODO: potentially extend MatButtonHarness
2223

2324
/**
@@ -29,7 +30,7 @@ export class MatMenuHarness extends ComponentHarness {
2930
*/
3031
static with(options: MenuHarnessFilters = {}): HarnessPredicate<MatMenuHarness> {
3132
return new HarnessPredicate(MatMenuHarness, options)
32-
.addOption('text', options.triggerText,
33+
.addOption('triggerText', options.triggerText,
3334
(harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text));
3435
}
3536

@@ -39,8 +40,9 @@ export class MatMenuHarness extends ComponentHarness {
3940
return coerceBooleanProperty(await disabled);
4041
}
4142

43+
/** Whether the menu is open. */
4244
async isOpen(): Promise<boolean> {
43-
throw Error('not implemented');
45+
return !!(await this._getMenuPanel());
4446
}
4547

4648
async getTriggerText(): Promise<string> {
@@ -58,30 +60,117 @@ export class MatMenuHarness extends ComponentHarness {
5860
}
5961

6062
async open(): Promise<void> {
61-
throw Error('not implemented');
63+
if (!await this.isOpen()) {
64+
return (await this.host()).click();
65+
}
6266
}
6367

6468
async close(): Promise<void> {
65-
throw Error('not implemented');
69+
const panel = await this._getMenuPanel();
70+
if (panel) {
71+
return panel.sendKeys(TestKey.ESCAPE);
72+
}
73+
}
74+
75+
async getItems(filters: Omit<MenuItemHarnessFilters, 'ancestor'> = {}):
76+
Promise<MatMenuItemHarness[]> {
77+
const panelId = await this._getPanelId();
78+
if (panelId) {
79+
return this._documentRootLocator.locatorForAll(
80+
MatMenuItemHarness.with({...filters, ancestor: `#${panelId}`}))();
81+
}
82+
return [];
83+
}
84+
85+
async selectItem(...itemFilters: MenuItemHarnessFilters[]):
86+
Promise<void> {
87+
itemFilters = itemFilters.length ? itemFilters : [{}];
88+
await this.open();
89+
const items = await this.getItems(itemFilters[0]);
90+
if (!items.length) {
91+
throw Error(`Could not find item matching ${JSON.stringify(itemFilters[0])}`);
92+
}
93+
94+
if (itemFilters.length === 1) {
95+
return await items[0].click();
96+
}
97+
98+
const menu = await items[0].getSubmenu();
99+
if (!menu) {
100+
throw Error(`Item matching ${JSON.stringify(itemFilters[0])} does not have a submenu`);
101+
}
102+
return menu.selectItem(...itemFilters.slice(1));
103+
}
104+
105+
private async _getMenuPanel(): Promise<TestElement | null> {
106+
const panelId = await this._getPanelId();
107+
return panelId ? this._documentRootLocator.locatorForOptional(`#${panelId}`)() : null;
108+
}
109+
110+
private async _getPanelId(): Promise<string | null> {
111+
const panelId = await (await this.host()).getAttribute('aria-controls');
112+
return panelId || null;
113+
}
114+
}
115+
116+
117+
/**
118+
* Harness for interacting with a standard mat-menu in tests.
119+
* @dynamic
120+
*/
121+
export class MatMenuItemHarness extends ComponentHarness {
122+
static hostSelector = '.mat-menu-item';
123+
124+
/**
125+
* Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes.
126+
* @param options Options for narrowing the search:
127+
* - `selector` finds a menu item whose host element matches the given selector.
128+
* - `label` finds a menu item with specific label text.
129+
* @return a `HarnessPredicate` configured with the given options.
130+
*/
131+
static with(options: MenuItemHarnessFilters = {}): HarnessPredicate<MatMenuItemHarness> {
132+
return new HarnessPredicate(MatMenuItemHarness, options)
133+
.addOption('text', options.text,
134+
(harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
135+
.addOption('hasSubmenu', options.hasSubmenu,
136+
async (harness, hasSubmenu) => (await harness.hasSubmenu()) === hasSubmenu);
66137
}
67138

68-
async getItems(): Promise<MatMenuItemHarness[]> {
69-
throw Error('not implemented');
139+
/** Gets a boolean promise indicating if the menu is disabled. */
140+
async isDisabled(): Promise<boolean> {
141+
const disabled = (await this.host()).getAttribute('disabled');
142+
return coerceBooleanProperty(await disabled);
70143
}
71144

72-
async getItemLabels(): Promise<string[]> {
73-
throw Error('not implemented');
145+
async getText(): Promise<string> {
146+
return (await this.host()).text();
147+
}
148+
149+
/** Focuses the menu and returns a void promise that indicates when the action is complete. */
150+
async focus(): Promise<void> {
151+
return (await this.host()).focus();
152+
}
153+
154+
/** Blurs the menu and returns a void promise that indicates when the action is complete. */
155+
async blur(): Promise<void> {
156+
return (await this.host()).blur();
74157
}
75158

76-
async getItemByLabel(): Promise<MatMenuItemHarness> {
77-
throw Error('not implemented');
159+
/** Clicks the menu item. */
160+
async click(): Promise<void> {
161+
return (await this.host()).click();
78162
}
79163

80-
async getItemByIndex(): Promise<MatMenuItemHarness> {
81-
throw Error('not implemented');
164+
/** Whether this item has a submenu. */
165+
async hasSubmenu(): Promise<boolean> {
166+
return (await this.host()).matchesSelector(MatMenuHarness.hostSelector);
82167
}
83168

84-
async getFocusedItem(): Promise<MatMenuItemHarness> {
85-
throw Error('not implemented');
169+
/** Gets the submenu associated with this menu item, or null if none. */
170+
async getSubmenu(): Promise<MatMenuHarness | null> {
171+
if (await this.hasSubmenu()) {
172+
return new MatMenuHarness(this.locatorFactory);
173+
}
174+
return null;
86175
}
87176
}

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

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/material/menu/testing/public-api.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@
88

99
export * from './menu-harness';
1010
export * from './menu-harness-filters';
11-
export * from './menu-item-harness';

0 commit comments

Comments
 (0)