@react-aria/test-utils 1.0.0-alpha.4 → 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/combobox.main.js.map +1 -1
  2. package/dist/combobox.module.js.map +1 -1
  3. package/dist/events.main.js +42 -4
  4. package/dist/events.main.js.map +1 -1
  5. package/dist/events.mjs +42 -4
  6. package/dist/events.module.js +42 -4
  7. package/dist/events.module.js.map +1 -1
  8. package/dist/gridlist.main.js +1 -1
  9. package/dist/gridlist.main.js.map +1 -1
  10. package/dist/gridlist.mjs +1 -1
  11. package/dist/gridlist.module.js +1 -1
  12. package/dist/gridlist.module.js.map +1 -1
  13. package/dist/listbox.main.js +2 -3
  14. package/dist/listbox.main.js.map +1 -1
  15. package/dist/listbox.mjs +2 -3
  16. package/dist/listbox.module.js +2 -3
  17. package/dist/listbox.module.js.map +1 -1
  18. package/dist/menu.main.js +39 -19
  19. package/dist/menu.main.js.map +1 -1
  20. package/dist/menu.mjs +39 -19
  21. package/dist/menu.module.js +39 -19
  22. package/dist/menu.module.js.map +1 -1
  23. package/dist/select.main.js.map +1 -1
  24. package/dist/select.module.js.map +1 -1
  25. package/dist/table.main.js +0 -4
  26. package/dist/table.main.js.map +1 -1
  27. package/dist/table.mjs +1 -5
  28. package/dist/table.module.js +1 -5
  29. package/dist/table.module.js.map +1 -1
  30. package/dist/tabs.main.js.map +1 -1
  31. package/dist/tabs.module.js.map +1 -1
  32. package/dist/testSetup.main.js.map +1 -1
  33. package/dist/testSetup.module.js.map +1 -1
  34. package/dist/tree.main.js +1 -1
  35. package/dist/tree.main.js.map +1 -1
  36. package/dist/tree.mjs +1 -1
  37. package/dist/tree.module.js +1 -1
  38. package/dist/tree.module.js.map +1 -1
  39. package/dist/types.d.ts +7 -3
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/user.main.js +1 -1
  42. package/dist/user.main.js.map +1 -1
  43. package/dist/user.mjs +1 -1
  44. package/dist/user.module.js +1 -1
  45. package/dist/user.module.js.map +1 -1
  46. package/package.json +6 -5
  47. package/src/combobox.ts +4 -4
  48. package/src/events.ts +30 -5
  49. package/src/gridlist.ts +8 -7
  50. package/src/listbox.ts +7 -13
  51. package/src/menu.ts +64 -14
  52. package/src/select.ts +4 -4
  53. package/src/table.ts +7 -11
  54. package/src/tabs.ts +2 -2
  55. package/src/testSetup.ts +2 -2
  56. package/src/tree.ts +9 -8
  57. package/src/types.ts +8 -4
  58. package/src/user.ts +1 -1
package/src/menu.ts CHANGED
@@ -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,12 +86,13 @@ export class MenuTester {
85
86
  }
86
87
 
87
88
  this._isSubmenu = isSubmenu || false;
89
+ this._rootMenu = rootMenu;
88
90
  }
89
91
 
90
92
  /**
91
93
  * Set the interaction type used by the menu tester.
92
94
  */
93
- setInteractionType(type: UserOpts['interactionType']) {
95
+ setInteractionType(type: UserOpts['interactionType']): void {
94
96
  this._interactionType = type;
95
97
  }
96
98
 
@@ -99,7 +101,7 @@ export class MenuTester {
99
101
  /**
100
102
  * Opens the menu. Defaults to using the interaction type set on the menu tester.
101
103
  */
102
- async open(opts: MenuOpenOpts = {}) {
104
+ async open(opts: MenuOpenOpts = {}): Promise<void> {
103
105
  let {
104
106
  needsLongPress,
105
107
  interactionType = this._interactionType,
@@ -178,7 +180,7 @@ export class MenuTester {
178
180
  * Selects the desired menu option. Defaults to using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand.
179
181
  * The desired option can be targeted via the option's node, the option's text, or the option's index.
180
182
  */
181
- async selectOption(opts: MenuSelectOpts) {
183
+ async selectOption(opts: MenuSelectOpts): Promise<void> {
182
184
  let {
183
185
  menuSelectionMode = 'single',
184
186
  needsLongPress,
@@ -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
  }
@@ -314,7 +364,7 @@ export class MenuTester {
314
364
  /**
315
365
  * Closes the menu.
316
366
  */
317
- async close() {
367
+ async close(): Promise<void> {
318
368
  let menu = this.menu;
319
369
  if (menu) {
320
370
  act(() => menu.focus());
package/src/select.ts CHANGED
@@ -46,14 +46,14 @@ export class SelectTester {
46
46
  /**
47
47
  * Set the interaction type used by the select tester.
48
48
  */
49
- setInteractionType(type: UserOpts['interactionType']) {
49
+ setInteractionType(type: UserOpts['interactionType']): void {
50
50
  this._interactionType = type;
51
51
  }
52
52
 
53
53
  /**
54
54
  * Opens the select. Defaults to using the interaction type set on the select tester.
55
55
  */
56
- async open(opts: SelectOpenOpts = {}) {
56
+ async open(opts: SelectOpenOpts = {}): Promise<void> {
57
57
  let {
58
58
  interactionType = this._interactionType
59
59
  } = opts;
@@ -89,7 +89,7 @@ export class SelectTester {
89
89
  /**
90
90
  * Closes the select.
91
91
  */
92
- async close() {
92
+ async close(): Promise<void> {
93
93
  let listbox = this.listbox;
94
94
  if (listbox) {
95
95
  act(() => listbox.focus());
@@ -155,7 +155,7 @@ export class SelectTester {
155
155
  * Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand.
156
156
  * The desired option can be targeted via the option's node, the option's text, or the option's index.
157
157
  */
158
- async selectOption(opts: SelectTriggerOptionOpts) {
158
+ async selectOption(opts: SelectTriggerOptionOpts): Promise<void> {
159
159
  let {
160
160
  option,
161
161
  interactionType = this._interactionType
package/src/table.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {act, fireEvent, waitFor, within} from '@testing-library/react';
13
+ import {act, waitFor, within} from '@testing-library/react';
14
14
  import {GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types';
15
15
  import {pressElement, triggerLongPress} from './events';
16
16
 
@@ -44,14 +44,14 @@ export class TableTester {
44
44
  /**
45
45
  * Set the interaction type used by the table tester.
46
46
  */
47
- setInteractionType(type: UserOpts['interactionType']) {
47
+ setInteractionType(type: UserOpts['interactionType']): void {
48
48
  this._interactionType = type;
49
49
  }
50
50
 
51
51
  /**
52
52
  * Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester.
53
53
  */
54
- async toggleRowSelection(opts: TableToggleRowOpts) {
54
+ async toggleRowSelection(opts: TableToggleRowOpts): Promise<void> {
55
55
  let {
56
56
  row,
57
57
  needsLongPress,
@@ -88,10 +88,6 @@ export class TableTester {
88
88
 
89
89
  // Note that long press interactions with rows is strictly touch only for grid rows
90
90
  await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
91
- // TODO: interestingly enough, we need to do a followup click otherwise future row selections may not fire properly?
92
- // To reproduce, try removing this, forcing toggleRowSelection to hit "needsLongPress ? await triggerLongPress(cell) : await action(cell);" and
93
- // run Table.test's "should support long press to enter selection mode on touch" test to see what happens
94
- await fireEvent.click(cell);
95
91
  } else {
96
92
  await pressElement(this.user, cell, interactionType);
97
93
  }
@@ -101,7 +97,7 @@ export class TableTester {
101
97
  /**
102
98
  * Toggles the sort order for the specified table column. Defaults to using the interaction type set on the table tester.
103
99
  */
104
- async toggleSort(opts: TableToggleSortOpts) {
100
+ async toggleSort(opts: TableToggleSortOpts): Promise<void> {
105
101
  let {
106
102
  column,
107
103
  interactionType = this._interactionType
@@ -191,7 +187,7 @@ export class TableTester {
191
187
  /**
192
188
  * Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester.
193
189
  */
194
- async triggerRowAction(opts: TableRowActionOpts) {
190
+ async triggerRowAction(opts: TableRowActionOpts): Promise<void> {
195
191
  let {
196
192
  row,
197
193
  needsDoubleClick,
@@ -225,7 +221,7 @@ export class TableTester {
225
221
  /**
226
222
  * Toggle selection for all rows in the table. Defaults to using the interaction type set on the table tester.
227
223
  */
228
- async toggleSelectAll(opts: {interactionType?: UserOpts['interactionType']} = {}) {
224
+ async toggleSelectAll(opts: {interactionType?: UserOpts['interactionType']} = {}): Promise<void> {
229
225
  let {
230
226
  interactionType = this._interactionType
231
227
  } = opts;
@@ -264,7 +260,7 @@ export class TableTester {
264
260
  /**
265
261
  * Returns a cell matching the specified text content.
266
262
  */
267
- findCell(opts: {text: string}) {
263
+ findCell(opts: {text: string}): HTMLElement {
268
264
  let {
269
265
  text
270
266
  } = opts;
package/src/tabs.ts CHANGED
@@ -51,7 +51,7 @@ export class TabsTester {
51
51
  /**
52
52
  * Set the interaction type used by the tabs tester.
53
53
  */
54
- setInteractionType(type: UserOpts['interactionType']) {
54
+ setInteractionType(type: UserOpts['interactionType']): void {
55
55
  this._interactionType = type;
56
56
  }
57
57
 
@@ -119,7 +119,7 @@ export class TabsTester {
119
119
  /**
120
120
  * Triggers the specified tab. Defaults to using the interaction type set on the tabs tester.
121
121
  */
122
- async triggerTab(opts: TriggerTabOptions) {
122
+ async triggerTab(opts: TriggerTabOptions): Promise<void> {
123
123
  let {
124
124
  tab,
125
125
  interactionType = this._interactionType,
package/src/testSetup.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  /**
14
14
  * Enables reading pageX/pageY from fireEvent.mouse*(..., {pageX: ..., pageY: ...}).
15
15
  */
16
- export function installMouseEvent() {
16
+ export function installMouseEvent(): void {
17
17
  let oldMouseEvent = MouseEvent;
18
18
  beforeAll(() => {
19
19
  global.MouseEvent = class FakeMouseEvent extends MouseEvent {
@@ -35,7 +35,7 @@ export function installMouseEvent() {
35
35
  });
36
36
  }
37
37
 
38
- export function installPointerEvent() {
38
+ export function installPointerEvent(): void {
39
39
  beforeAll(() => {
40
40
  // @ts-ignore
41
41
  global.PointerEvent = class FakePointerEvent extends MouseEvent {
package/src/tree.ts CHANGED
@@ -41,7 +41,7 @@ export class TreeTester {
41
41
  /**
42
42
  * Set the interaction type used by the tree tester.
43
43
  */
44
- setInteractionType(type: UserOpts['interactionType']) {
44
+ setInteractionType(type: UserOpts['interactionType']): void {
45
45
  this._interactionType = type;
46
46
  };
47
47
 
@@ -71,6 +71,11 @@ export class TreeTester {
71
71
  if (targetIndex === -1) {
72
72
  throw new Error('Option provided is not in the tree');
73
73
  }
74
+
75
+ if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) {
76
+ act(() => this._tree.focus());
77
+ }
78
+
74
79
  if (document.activeElement === this.tree) {
75
80
  await this.user.keyboard('[ArrowDown]');
76
81
  } else if (this._tree.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
@@ -92,7 +97,7 @@ export class TreeTester {
92
97
  /**
93
98
  * Toggles the selection for the specified tree row. Defaults to using the interaction type set on the tree tester.
94
99
  */
95
- async toggleRowSelection(opts: TreeToggleRowOpts) {
100
+ async toggleRowSelection(opts: TreeToggleRowOpts): Promise<void> {
96
101
  let {
97
102
  row,
98
103
  needsLongPress,
@@ -143,7 +148,7 @@ export class TreeTester {
143
148
  /**
144
149
  * Toggles the expansion for the specified tree row. Defaults to using the interaction type set on the tree tester.
145
150
  */
146
- async toggleRowExpansion(opts: TreeToggleExpansionOpts) {
151
+ async toggleRowExpansion(opts: TreeToggleExpansionOpts): Promise<void> {
147
152
  let {
148
153
  row,
149
154
  interactionType = this._interactionType
@@ -184,7 +189,7 @@ export class TreeTester {
184
189
  /**
185
190
  * Triggers the action for the specified tree row. Defaults to using the interaction type set on the tree tester.
186
191
  */
187
- async triggerRowAction(opts: TreeRowActionOpts) {
192
+ async triggerRowAction(opts: TreeRowActionOpts): Promise<void> {
188
193
  let {
189
194
  row,
190
195
  needsDoubleClick,
@@ -206,10 +211,6 @@ export class TreeTester {
206
211
  return;
207
212
  }
208
213
 
209
- if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) {
210
- act(() => this._tree.focus());
211
- }
212
-
213
214
  await this.keyboardNavigateToRow({row});
214
215
  await this.user.keyboard('[Enter]');
215
216
  } else {
package/src/types.ts CHANGED
@@ -22,14 +22,14 @@ export interface UserOpts {
22
22
  * @default mouse
23
23
  */
24
24
  interactionType?: 'mouse' | 'touch' | 'keyboard',
25
- // If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))}
26
- // A real timer user would pass async () => await new Promise((resolve) => setTimeout(resolve, waitTime))
25
+ // If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))}.
26
+ // A real timer user would pass (waitTime) => new Promise((resolve) => setTimeout(resolve, waitTime))
27
27
  // Time is in ms.
28
28
  /**
29
29
  * A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). This can be overridden
30
30
  * at the aria pattern tester level if needed.
31
31
  */
32
- advanceTimer?: (time?: number) => void | Promise<unknown>
32
+ advanceTimer?: (time: number) => unknown | Promise<unknown>
33
33
  }
34
34
 
35
35
  export interface BaseTesterOpts extends UserOpts {
@@ -69,7 +69,11 @@ export interface MenuTesterOpts extends BaseTesterOpts {
69
69
  /**
70
70
  * Whether the current menu is a submenu.
71
71
  */
72
- isSubmenu?: boolean
72
+ isSubmenu?: boolean,
73
+ /**
74
+ * The root menu of the menu tree. Only available if the menu is a submenu.
75
+ */
76
+ rootMenu?: HTMLElement
73
77
  }
74
78
 
75
79
  export interface SelectTesterOpts extends BaseTesterOpts {
package/src/user.ts CHANGED
@@ -67,7 +67,7 @@ type TesterOpts<T> =
67
67
  T extends 'Tree' ? TreeTesterOpts :
68
68
  never;
69
69
 
70
- let defaultAdvanceTimer = async (waitTime: number | undefined) => await new Promise((resolve) => setTimeout(resolve, waitTime));
70
+ let defaultAdvanceTimer = (waitTime: number | undefined) => new Promise((resolve) => setTimeout(resolve, waitTime));
71
71
 
72
72
  export class User {
73
73
  private user;