/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import { ComponentHarnessConstructor, ContentContainerComponentHarness, HarnessLoader, HarnessPredicate, TestElement, } from '@angular/cdk/testing'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {MenuHarnessFilters, MenuItemHarnessFilters} from './menu-harness-filters'; /** Harness for interacting with a mat-menu in tests. */ export class MatMenuHarness extends ContentContainerComponentHarness { private _documentRootLocator = this.documentRootLocatorFactory(); /** The selector for the host element of a `MatMenu` instance. */ static hostSelector = '.mat-mdc-menu-trigger'; /** * Gets a `HarnessPredicate` that can be used to search for a menu with specific attributes. * @param options Options for filtering which menu instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ static with( this: ComponentHarnessConstructor, options: MenuHarnessFilters = {}, ): HarnessPredicate { return new HarnessPredicate(this, options).addOption( 'triggerText', options.triggerText, (harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text), ); } /** Whether the menu is disabled. */ async isDisabled(): Promise { const disabled = (await this.host()).getAttribute('disabled'); return coerceBooleanProperty(await disabled); } /** Whether the menu is open. */ async isOpen(): Promise { return !!(await this._getMenuPanel()); } /** Gets the text of the menu's trigger element. */ async getTriggerText(): Promise { return (await this.host()).text(); } /** Focuses the menu. */ async focus(): Promise { return (await this.host()).focus(); } /** Blurs the menu. */ async blur(): Promise { return (await this.host()).blur(); } /** Whether the menu is focused. */ async isFocused(): Promise { return (await this.host()).isFocused(); } /** Opens the menu. */ async open(): Promise { if (!(await this.isOpen())) { return (await this.host()).click(); } } /** Closes the menu. */ async close(): Promise { const panel = await this._getMenuPanel(); if (panel) { return panel.click(); } } /** * Gets a list of `MatMenuItemHarness` representing the items in the menu. * @param filters Optionally filters which menu items are included. */ async getItems( filters?: Omit, ): Promise { const panelId = await this._getPanelId(); if (panelId) { return this._documentRootLocator.locatorForAll( MatMenuItemHarness.with({ ...(filters || {}), ancestor: `#${panelId}`, } as MenuItemHarnessFilters), )(); } return []; } /** * 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, ...subItemFilters: Omit[] ): Promise { 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])); } protected override async getRootHarnessLoader(): Promise { const panelId = await this._getPanelId(); return this.documentRootLocatorFactory().harnessLoaderFor(`#${panelId}`); } /** Gets the menu panel associated with this menu. */ private async _getMenuPanel(): Promise { 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 { const panelId = await (await this.host()).getAttribute('aria-controls'); return panelId || null; } } export class MatMenuItemHarness extends ContentContainerComponentHarness { /** 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 item with specific attributes. * @param options Options for filtering which menu item instances are considered a match. * @return a `HarnessPredicate` configured with the given options. */ static with( this: ComponentHarnessConstructor, options: MenuItemHarnessFilters = {}, ): HarnessPredicate { return new HarnessPredicate(this, options) .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text), ) .addOption( 'hasSubmenu', options.hasSubmenu, async (harness, hasSubmenu) => (await harness.hasSubmenu()) === hasSubmenu, ); } /** Whether the menu is disabled. */ async isDisabled(): Promise { const disabled = (await this.host()).getAttribute('disabled'); return coerceBooleanProperty(await disabled); } /** Gets the text of the menu item. */ async getText(): Promise { return (await this.host()).text(); } /** Focuses the menu item. */ async focus(): Promise { return (await this.host()).focus(); } /** Blurs the menu item. */ async blur(): Promise { return (await this.host()).blur(); } /** Whether the menu item is focused. */ async isFocused(): Promise { return (await this.host()).isFocused(); } /** Clicks the menu item. */ async click(): Promise { return (await this.host()).click(); } /** Whether this item has a submenu. */ async hasSubmenu(): Promise { return (await this.host()).matchesSelector(MatMenuHarness.hostSelector); } /** Gets the submenu associated with this menu item, or null if none. */ async getSubmenu(): Promise { if (await this.hasSubmenu()) { return new MatMenuHarness(this.locatorFactory); } return null; } }