Skip to content

fix(material-experimental/mdc-menu/testing): add unimplemented methods #20352

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
Aug 20, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,5 @@ import {MatMenuModule} from '../index';
import {MatMenuHarness} from './menu-harness';

describe('MDC-based MatMenuHarness', () => {
it('TODO: re-enable after implementing missing methods', () => expect(true).toBe(true));
if (false) {
runHarnessTests(MatMenuModule, MatMenuHarness as any);
}
runHarnessTests(MatMenuModule, MatMenuHarness as any);
});
125 changes: 91 additions & 34 deletions src/material-experimental/mdc-menu/testing/menu-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,23 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
import {ComponentHarness, HarnessPredicate, TestElement, TestKey} from '@angular/cdk/testing';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {
MenuHarnessFilters,
MenuItemHarnessFilters
} from '@angular/material/menu/testing';
import {MenuHarnessFilters, MenuItemHarnessFilters} from '@angular/material/menu/testing';

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

private _documentRootLocator = this.documentRootLocatorFactory();

// TODO: potentially extend MatButtonHarness

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

/** Gets a boolean promise indicating if the menu is disabled. */
/** Whether the menu is disabled. */
async isDisabled(): Promise<boolean> {
const disabled = (await this.host()).getAttribute('disabled');
return coerceBooleanProperty(await disabled);
}

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

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

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

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

/** Opens the menu. */
async open(): Promise<void> {
throw Error('not implemented');
if (!await this.isOpen()) {
return (await this.host()).click();
}
}

/** Closes the menu. */
async close(): Promise<void> {
throw Error('not implemented');
const panel = await this._getMenuPanel();
if (panel) {
return panel.sendKeys(TestKey.ESCAPE);
}
}

/**
* Gets a list of `MatMenuItemHarness` representing the items in the menu.
* @param filters Optionally filters which menu items are included.
*/
async getItems(filters: Omit<MenuItemHarnessFilters, 'ancestor'> = {}):
Promise<MatMenuItemHarness[]> {
throw Error('not implemented');
const panelId = await this._getPanelId();
if (panelId) {
return this._documentRootLocator.locatorForAll(
MatMenuItemHarness.with({...filters, ancestor: `#${panelId}`}))();
}
return [];
}

async clickItem(filter: Omit<MenuItemHarnessFilters, 'ancestor'>,
...filters: Omit<MenuItemHarnessFilters, 'ancestor'>[]): Promise<void> {
throw Error('not implemented');
/**
* Clicks an item in the menu, and optionally continues clicking items in subsequent sub-menus.
* @param itemFilter A filter used to represent which item in the menu should be clicked. The
* first matching menu item will be clicked.
* @param subItemFilters A list of filters representing the items to click in any subsequent
* sub-menus. The first item in the sub-menu matching the corresponding filter in
* `subItemFilters` will be clicked.
*/
async clickItem(
itemFilter: Omit<MenuItemHarnessFilters, 'ancestor'>,
...subItemFilters: Omit<MenuItemHarnessFilters, 'ancestor'>[]): Promise<void> {
await this.open();
const items = await this.getItems(itemFilter);
if (!items.length) {
throw Error(`Could not find item matching ${JSON.stringify(itemFilter)}`);
}

if (!subItemFilters.length) {
return await items[0].click();
}

const menu = await items[0].getSubmenu();
if (!menu) {
throw Error(`Item matching ${JSON.stringify(itemFilter)} does not have a submenu`);
}
return menu.clickItem(...subItemFilters as [Omit<MenuItemHarnessFilters, 'ancestor'>]);
}

/** Gets the menu panel associated with this menu. */
private async _getMenuPanel(): Promise<TestElement | null> {
const panelId = await this._getPanelId();
return panelId ? this._documentRootLocator.locatorForOptional(`#${panelId}`)() : null;
}

/** Gets the id of the menu panel associated with this menu. */
private async _getPanelId(): Promise<string | null> {
const panelId = await (await this.host()).getAttribute('aria-controls');
return panelId || null;
}
}


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

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

/** Gets a boolean promise indicating if the menu is disabled. */
/** Whether the menu is disabled. */
async isDisabled(): Promise<boolean> {
const disabled = (await this.host()).getAttribute('disabled');
return coerceBooleanProperty(await disabled);
}

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

/**
* Focuses the menu item and returns a void promise that indicates when the action is complete.
*/
/** Focuses the menu item. */
async focus(): Promise<void> {
return (await this.host()).focus();
}

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

/** Clicks the menu item. */
async click(): Promise<void> {
throw Error('not implemented');
return (await this.host()).click();
}

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

/** Gets the submenu associated with this menu item, or null if none. */
async getSubmenu(): Promise<MatMenuHarness | null> {
throw Error('not implemented');
if (await this.hasSubmenu()) {
return new MatMenuHarness(this.locatorFactory);
}
return null;
}
}
10 changes: 5 additions & 5 deletions src/material/menu/testing/shared.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function runHarnessTests(
});

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

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

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

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

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

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