@@ -64,9 +64,10 @@ export class MenuTester {
64
64
private _advanceTimer : UserOpts [ 'advanceTimer' ] ;
65
65
private _trigger : HTMLElement | undefined ;
66
66
private _isSubmenu : boolean = false ;
67
+ private _rootMenu : HTMLElement | undefined ;
67
68
68
69
constructor ( opts : MenuTesterOpts ) {
69
- let { root, user, interactionType, advanceTimer, isSubmenu} = opts ;
70
+ let { root, user, interactionType, advanceTimer, isSubmenu, rootMenu } = opts ;
70
71
this . user = user ;
71
72
this . _interactionType = interactionType || 'mouse' ;
72
73
this . _advanceTimer = advanceTimer ;
@@ -85,6 +86,7 @@ export class MenuTester {
85
86
}
86
87
87
88
this . _isSubmenu = isSubmenu || false ;
89
+ this . _rootMenu = rootMenu ;
88
90
}
89
91
90
92
/**
@@ -226,20 +228,56 @@ export class MenuTester {
226
228
await this . user . pointer ( { target : option , keys : '[TouchA]' } ) ;
227
229
}
228
230
}
229
- act ( ( ) => { jest . runAllTimers ( ) ; } ) ;
230
231
231
- if ( option . getAttribute ( 'href' ) == null && option . getAttribute ( 'aria-haspopup' ) == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && ! this . _isSubmenu ) {
232
+ // This chain of waitFors is needed in place of running all timers since we don't know how long transitions may take, or what action
233
+ // the menu option select may trigger.
234
+ if (
235
+ ! ( menuSelectionMode === 'single' && ! closesOnSelect ) &&
236
+ ! ( menuSelectionMode === 'multiple' && ( keyboardActivation === 'Space' || interactionType === 'mouse' ) )
237
+ ) {
238
+ // For RSP, clicking on a submenu option seems to briefly lose focus to the body before moving to the clicked option in the test so we need to wait
239
+ // for focus to be coerced to somewhere else in place of running all timers.
240
+ if ( this . _isSubmenu ) {
241
+ await waitFor ( ( ) => {
242
+ if ( document . activeElement === document . body ) {
243
+ throw new Error ( 'Expected focus to move to somewhere other than the body after selecting a submenu option.' ) ;
244
+ } else {
245
+ return true ;
246
+ }
247
+ } ) ;
248
+ }
249
+
250
+ // If user isn't trying to select multiple menu options or closeOnSelect is true then we can assume that
251
+ // the menu will close or some action is triggered. In cases like that focus should move somewhere after the menu closes
252
+ // but we can't really know where so just make sure it doesn't get lost to the body.
232
253
await waitFor ( ( ) => {
233
- if ( document . activeElement !== trigger ) {
234
- throw new Error ( ` Expected the document.activeElement after selecting an option to be the menu trigger but got ${ document . activeElement } ` ) ;
254
+ if ( document . activeElement === option ) {
255
+ throw new Error ( ' Expected focus after selecting an option to move away from the option.' ) ;
235
256
} else {
236
257
return true ;
237
258
}
238
259
} ) ;
239
260
240
- if ( document . contains ( menu ) ) {
241
- throw new Error ( 'Expected menu element to not be in the document after selecting an option' ) ;
261
+ // We'll also want to wait for focus to move away from the original submenu trigger since the entire submenu tree should
262
+ // close. In React 16, focus actually makes it all the way to the root menu's submenu trigger so we need check the root menu
263
+ if ( this . _isSubmenu ) {
264
+ await waitFor ( ( ) => {
265
+ if ( document . activeElement === this . trigger || this . _rootMenu ?. contains ( document . activeElement ) ) {
266
+ throw new Error ( 'Expected focus after selecting an submenu option to move away from the original submenu trigger.' ) ;
267
+ } else {
268
+ return true ;
269
+ }
270
+ } ) ;
242
271
}
272
+
273
+ // Finally wait for focus to be coerced somewhere final when the menu tree is removed from the DOM
274
+ await waitFor ( ( ) => {
275
+ if ( document . activeElement === document . body ) {
276
+ throw new Error ( 'Expected focus to move to somewhere other than the body after selecting a menu option.' ) ;
277
+ } else {
278
+ return true ;
279
+ }
280
+ } ) ;
243
281
}
244
282
} else {
245
283
throw new Error ( "Attempted to select a option in the menu, but menu wasn't found." ) ;
@@ -269,18 +307,30 @@ export class MenuTester {
269
307
submenuTrigger = ( within ( menu ! ) . getByText ( submenuTrigger ) . closest ( '[role=menuitem]' ) ) ! as HTMLElement ;
270
308
}
271
309
272
- let submenuTriggerTester = new MenuTester ( { user : this . user , interactionType : this . _interactionType , root : submenuTrigger , isSubmenu : true } ) ;
310
+ let submenuTriggerTester = new MenuTester ( {
311
+ user : this . user ,
312
+ interactionType : this . _interactionType ,
313
+ root : submenuTrigger ,
314
+ isSubmenu : true ,
315
+ advanceTimer : this . _advanceTimer ,
316
+ rootMenu : ( this . _isSubmenu ? this . _rootMenu : this . menu ) || undefined
317
+ } ) ;
273
318
if ( interactionType === 'mouse' ) {
274
319
await this . user . pointer ( { target : submenuTrigger } ) ;
275
- act ( ( ) => { jest . runAllTimers ( ) ; } ) ;
276
320
} else if ( interactionType === 'keyboard' ) {
277
321
await this . keyboardNavigateToOption ( { option : submenuTrigger } ) ;
278
322
await this . user . keyboard ( '[ArrowRight]' ) ;
279
- act ( ( ) => { jest . runAllTimers ( ) ; } ) ;
280
323
} else {
281
324
await submenuTriggerTester . open ( ) ;
282
325
}
283
326
327
+ await waitFor ( ( ) => {
328
+ if ( submenuTriggerTester . _trigger ?. getAttribute ( 'aria-expanded' ) !== 'true' ) {
329
+ throw new Error ( 'aria-expanded for the submenu trigger wasn\'t changed to "true", unable to confirm the existance of the submenu' ) ;
330
+ } else {
331
+ return true ;
332
+ }
333
+ } ) ;
284
334
285
335
return submenuTriggerTester ;
286
336
}
0 commit comments