|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.dev/license |
| 7 | + */ |
| 8 | + |
| 9 | +import { |
| 10 | + ComponentHarnessConstructor, |
| 11 | + ContentContainerComponentHarness, |
| 12 | + HarnessLoader, |
| 13 | + HarnessPredicate, |
| 14 | + TestElement, |
| 15 | +} from '@angular/cdk/testing'; |
| 16 | +import {ContextMenuHarnessFilters, MenuItemHarnessFilters} from './menu-harness-filters'; |
| 17 | +import {clickItemImplementation, MatMenuItemHarness} from './menu-harness'; |
| 18 | + |
| 19 | +/** Harness for interacting with context menus in tests. */ |
| 20 | +export class MatContextMenuHarness extends ContentContainerComponentHarness<string> { |
| 21 | + private _documentRootLocator = this.documentRootLocatorFactory(); |
| 22 | + |
| 23 | + /** The selector for the host element of a `MatContextMenu` instance. */ |
| 24 | + static hostSelector = '.mat-context-menu-trigger'; |
| 25 | + |
| 26 | + /** |
| 27 | + * Gets a `HarnessPredicate` that can be used to search for a context menu with specific |
| 28 | + * attributes. |
| 29 | + * @param options Options for filtering which menu instances are considered a match. |
| 30 | + * @return a `HarnessPredicate` configured with the given options. |
| 31 | + */ |
| 32 | + static with<T extends MatContextMenuHarness>( |
| 33 | + this: ComponentHarnessConstructor<T>, |
| 34 | + options: ContextMenuHarnessFilters = {}, |
| 35 | + ): HarnessPredicate<T> { |
| 36 | + return new HarnessPredicate(this, options); |
| 37 | + } |
| 38 | + |
| 39 | + /** Whether the menu is open. */ |
| 40 | + async isOpen(): Promise<boolean> { |
| 41 | + return !!(await this._getMenuPanel()); |
| 42 | + } |
| 43 | + |
| 44 | + /** |
| 45 | + * Opens the menu. |
| 46 | + * @param relativeX X coordinate, relative to the element, to dispatch the opening click at. |
| 47 | + * @param relativeY Y coordinate, relative to the element, to dispatch the opening click at. |
| 48 | + */ |
| 49 | + async open(relativeX = 0, relativeY = 0): Promise<void> { |
| 50 | + if (!(await this.isOpen())) { |
| 51 | + return (await this.host()).rightClick(relativeX, relativeY); |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + /** Closes the menu. */ |
| 56 | + async close(): Promise<void> { |
| 57 | + const panel = await this._getMenuPanel(); |
| 58 | + if (panel) { |
| 59 | + return panel.click(); |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + /** Gets whether the context menu trigger is disabled. */ |
| 64 | + async isDisabled(): Promise<boolean> { |
| 65 | + const host = await this.host(); |
| 66 | + return host.hasClass('mat-context-menu-trigger-disabled'); |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * Gets a list of `MatMenuItemHarness` representing the items in the menu. |
| 71 | + * @param filters Optionally filters which menu items are included. |
| 72 | + */ |
| 73 | + async getItems( |
| 74 | + filters?: Omit<MenuItemHarnessFilters, 'ancestor'>, |
| 75 | + ): Promise<MatMenuItemHarness[]> { |
| 76 | + const panelId = await this._getPanelId(); |
| 77 | + if (panelId) { |
| 78 | + return this._documentRootLocator.locatorForAll( |
| 79 | + MatMenuItemHarness.with({ |
| 80 | + ...(filters || {}), |
| 81 | + ancestor: `#${panelId}`, |
| 82 | + } as MenuItemHarnessFilters), |
| 83 | + )(); |
| 84 | + } |
| 85 | + return []; |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * Clicks an item in the menu, and optionally continues clicking items in subsequent sub-menus. |
| 90 | + * @param itemFilter A filter used to represent which item in the menu should be clicked. The |
| 91 | + * first matching menu item will be clicked. |
| 92 | + * @param subItemFilters A list of filters representing the items to click in any subsequent |
| 93 | + * sub-menus. The first item in the sub-menu matching the corresponding filter in |
| 94 | + * `subItemFilters` will be clicked. |
| 95 | + */ |
| 96 | + async clickItem( |
| 97 | + itemFilter: Omit<MenuItemHarnessFilters, 'ancestor'>, |
| 98 | + ...subItemFilters: Omit<MenuItemHarnessFilters, 'ancestor'>[] |
| 99 | + ): Promise<void> { |
| 100 | + await this.open(); |
| 101 | + return clickItemImplementation(await this.getItems(itemFilter), itemFilter, subItemFilters); |
| 102 | + } |
| 103 | + |
| 104 | + protected override async getRootHarnessLoader(): Promise<HarnessLoader> { |
| 105 | + const panelId = await this._getPanelId(); |
| 106 | + return this.documentRootLocatorFactory().harnessLoaderFor(`#${panelId}`); |
| 107 | + } |
| 108 | + |
| 109 | + /** Gets the menu panel associated with this menu. */ |
| 110 | + private async _getMenuPanel(): Promise<TestElement | null> { |
| 111 | + const panelId = await this._getPanelId(); |
| 112 | + return panelId ? this._documentRootLocator.locatorForOptional(`#${panelId}`)() : null; |
| 113 | + } |
| 114 | + |
| 115 | + /** Gets the id of the menu panel associated with this menu. */ |
| 116 | + private async _getPanelId(): Promise<string | null> { |
| 117 | + const panelId = await (await this.host()).getAttribute('aria-controls'); |
| 118 | + return panelId || null; |
| 119 | + } |
| 120 | +} |
0 commit comments