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' ;
1010import { 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. */
1714export 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. */
85137export 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}
0 commit comments