@react-aria/test-utils 1.0.0-alpha.2 → 1.0.0-alpha.3

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.
package/src/menu.ts CHANGED
@@ -15,16 +15,18 @@ import {BaseTesterOpts, UserOpts} from './user';
15
15
  import {triggerLongPress} from './events';
16
16
 
17
17
  export interface MenuOptions extends UserOpts, BaseTesterOpts {
18
- user: any
18
+ user?: any,
19
+ isSubmenu?: boolean
19
20
  }
20
21
  export class MenuTester {
21
22
  private user;
22
23
  private _interactionType: UserOpts['interactionType'];
23
24
  private _advanceTimer: UserOpts['advanceTimer'];
24
- private _trigger: HTMLElement;
25
+ private _trigger: HTMLElement | undefined;
26
+ private _isSubmenu: boolean = false;
25
27
 
26
28
  constructor(opts: MenuOptions) {
27
- let {root, user, interactionType, advanceTimer} = opts;
29
+ let {root, user, interactionType, advanceTimer, isSubmenu} = opts;
28
30
  this.user = user;
29
31
  this._interactionType = interactionType || 'mouse';
30
32
  this._advanceTimer = advanceTimer;
@@ -41,6 +43,8 @@ export class MenuTester {
41
43
  this._trigger = root;
42
44
  }
43
45
  }
46
+
47
+ this._isSubmenu = isSubmenu || false;
44
48
  }
45
49
 
46
50
  setInteractionType = (type: UserOpts['interactionType']) => {
@@ -49,12 +53,12 @@ export class MenuTester {
49
53
 
50
54
  // TODO: this has been common to select as well, maybe make select use it? Or make a generic method. Will need to make error messages generic
51
55
  // One difference will be that it supports long press as well
52
- open = async (opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) => {
56
+ open = async (opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType'], direction?: 'up' | 'down'} = {}) => {
53
57
  let {
54
58
  needsLongPress,
55
- interactionType = this._interactionType
59
+ interactionType = this._interactionType,
60
+ direction
56
61
  } = opts;
57
-
58
62
  let trigger = this.trigger;
59
63
  let isDisabled = trigger.hasAttribute('disabled');
60
64
  if (interactionType === 'mouse' || interactionType === 'touch') {
@@ -70,8 +74,16 @@ export class MenuTester {
70
74
  await this.user.pointer({target: trigger, keys: '[TouchA]'});
71
75
  }
72
76
  } else if (interactionType === 'keyboard' && !isDisabled) {
73
- act(() => trigger.focus());
74
- await this.user.keyboard('[Enter]');
77
+ if (direction === 'up') {
78
+ act(() => trigger.focus());
79
+ await this.user.keyboard('[ArrowUp]');
80
+ } else if (direction === 'down') {
81
+ act(() => trigger.focus());
82
+ await this.user.keyboard('[ArrowDown]');
83
+ } else {
84
+ act(() => trigger.focus());
85
+ await this.user.keyboard('[Enter]');
86
+ }
75
87
  }
76
88
 
77
89
  await waitFor(() => {
@@ -95,24 +107,37 @@ export class MenuTester {
95
107
 
96
108
  // TODO: also very similar to select, barring potential long press support
97
109
  // Close on select is also kinda specific?
98
- selectOption = async (opts: {option?: HTMLElement, optionText?: string, menuSelectionMode?: 'single' | 'multiple', needsLongPress?: boolean, closesOnSelect?: boolean, interactionType?: UserOpts['interactionType']}) => {
110
+ selectOption = async (opts: {
111
+ option?: HTMLElement,
112
+ optionText?: string,
113
+ menuSelectionMode?: 'single' | 'multiple',
114
+ needsLongPress?: boolean,
115
+ closesOnSelect?: boolean,
116
+ interactionType?: UserOpts['interactionType'],
117
+ keyboardActivation?: 'Space' | 'Enter'
118
+ }) => {
99
119
  let {
100
120
  optionText,
101
121
  menuSelectionMode = 'single',
102
122
  needsLongPress,
103
123
  closesOnSelect = true,
104
124
  option,
105
- interactionType = this._interactionType
125
+ interactionType = this._interactionType,
126
+ keyboardActivation = 'Enter'
106
127
  } = opts;
107
128
  let trigger = this.trigger;
108
- if (!trigger.getAttribute('aria-controls')) {
129
+
130
+ if (!trigger.getAttribute('aria-controls') && !trigger.hasAttribute('aria-expanded')) {
109
131
  await this.open({needsLongPress});
110
132
  }
111
133
 
112
134
  let menu = this.menu;
113
135
  if (menu) {
114
136
  if (!option && optionText) {
115
- option = within(menu).getByText(optionText);
137
+ option = (within(menu!).getByText(optionText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement;
138
+ }
139
+ if (!option) {
140
+ throw new Error('No option found in the menu.');
116
141
  }
117
142
 
118
143
  if (interactionType === 'keyboard') {
@@ -120,8 +145,8 @@ export class MenuTester {
120
145
  act(() => menu.focus());
121
146
  }
122
147
 
123
- await this.user.keyboard(optionText);
124
- await this.user.keyboard('[Enter]');
148
+ await this.keyboardNavigateToOption({option});
149
+ await this.user.keyboard(`[${keyboardActivation}]`);
125
150
  } else {
126
151
  if (interactionType === 'mouse') {
127
152
  await this.user.click(option);
@@ -129,8 +154,9 @@ export class MenuTester {
129
154
  await this.user.pointer({target: option, keys: '[TouchA]'});
130
155
  }
131
156
  }
157
+ act(() => {jest.runAllTimers();});
132
158
 
133
- if (option && option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect) {
159
+ if (option && option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) {
134
160
  await waitFor(() => {
135
161
  if (document.activeElement !== trigger) {
136
162
  throw new Error(`Expected the document.activeElement after selecting an option to be the menu trigger but got ${document.activeElement}`);
@@ -156,6 +182,7 @@ export class MenuTester {
156
182
  needsLongPress,
157
183
  interactionType = this._interactionType
158
184
  } = opts;
185
+
159
186
  let trigger = this.trigger;
160
187
  let isDisabled = trigger.hasAttribute('disabled');
161
188
  if (!trigger.getAttribute('aria-controls') && !isDisabled) {
@@ -171,8 +198,18 @@ export class MenuTester {
171
198
  submenu = within(menu).getByText(submenuTriggerText);
172
199
  }
173
200
 
174
- let submenuTriggerTester = new MenuTester({user: this.user, interactionType: interactionType, root: submenu});
175
- await submenuTriggerTester.open();
201
+ let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenu, isSubmenu: true});
202
+ if (interactionType === 'mouse') {
203
+ await this.user.pointer({target: submenu});
204
+ act(() => {jest.runAllTimers();});
205
+ } else if (interactionType === 'keyboard') {
206
+ await this.keyboardNavigateToOption({option: submenu});
207
+ await this.user.keyboard('[ArrowRight]');
208
+ act(() => {jest.runAllTimers();});
209
+ } else {
210
+ await submenuTriggerTester.open();
211
+ }
212
+
176
213
 
177
214
  return submenuTriggerTester;
178
215
  }
@@ -181,6 +218,28 @@ export class MenuTester {
181
218
  return null;
182
219
  };
183
220
 
221
+ keyboardNavigateToOption = async (opts: {option: HTMLElement}) => {
222
+ let {option} = opts;
223
+ let options = this.options;
224
+ let targetIndex = options.indexOf(option);
225
+ if (targetIndex === -1) {
226
+ throw new Error('Option provided is not in the menu');
227
+ }
228
+ if (document.activeElement === this.menu) {
229
+ await this.user.keyboard('[ArrowDown]');
230
+ }
231
+ let currIndex = options.indexOf(document.activeElement as HTMLElement);
232
+ if (targetIndex === -1) {
233
+ throw new Error('ActiveElement is not in the menu');
234
+ }
235
+ let direction = targetIndex > currIndex ? 'down' : 'up';
236
+
237
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
238
+ await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
239
+ }
240
+ };
241
+
242
+
184
243
  close = async () => {
185
244
  let menu = this.menu;
186
245
  if (menu) {
@@ -202,6 +261,9 @@ export class MenuTester {
202
261
  };
203
262
 
204
263
  get trigger() {
264
+ if (!this._trigger) {
265
+ throw new Error('No trigger element found for menu.');
266
+ }
205
267
  return this._trigger;
206
268
  }
207
269
 
@@ -210,9 +272,9 @@ export class MenuTester {
210
272
  return menuId ? document.getElementById(menuId) : undefined;
211
273
  }
212
274
 
213
- get options(): HTMLElement[] | never[] {
275
+ get options(): HTMLElement[] {
214
276
  let menu = this.menu;
215
- let options = [];
277
+ let options: HTMLElement[] = [];
216
278
  if (menu) {
217
279
  options = within(menu).queryAllByRole('menuitem');
218
280
  if (options.length === 0) {
package/src/select.ts CHANGED
@@ -15,7 +15,7 @@ import {BaseTesterOpts, UserOpts} from './user';
15
15
 
16
16
  export interface SelectOptions extends UserOpts, BaseTesterOpts {
17
17
  // TODO: I think the type grabbed from the testing library dist for UserEvent is breaking the build, will need to figure out a better place to grab from
18
- user: any
18
+ user?: any
19
19
  }
20
20
  export class SelectTester {
21
21
  private user;
package/src/table.ts CHANGED
@@ -14,7 +14,7 @@ import {act, fireEvent, waitFor, within} from '@testing-library/react';
14
14
  import {BaseTesterOpts, UserOpts} from './user';
15
15
  import {pressElement, triggerLongPress} from './events';
16
16
  export interface TableOptions extends UserOpts, BaseTesterOpts {
17
- user: any,
17
+ user?: any,
18
18
  advanceTimer: UserOpts['advanceTimer']
19
19
  }
20
20
 
@@ -68,15 +68,6 @@ export class TableTester {
68
68
  await pressElement(this.user, cell, interactionType);
69
69
  }
70
70
  }
71
-
72
- // Handle cases where the table may transition in response to the row selection/deselection
73
- await act(async () => {
74
- if (this._advanceTimer == null) {
75
- throw new Error('No advanceTimers provided for table transition.');
76
- }
77
-
78
- await this._advanceTimer(200);
79
- });
80
71
  };
81
72
 
82
73
  toggleSort = async (opts: {index?: number, text?: string, interactionType?: UserOpts['interactionType']} = {}) => {
@@ -145,12 +136,12 @@ export class TableTester {
145
136
  }
146
137
 
147
138
  // Handle cases where the table may transition in response to the row selection/deselection
148
- await act(async () => {
149
- if (this._advanceTimer == null) {
150
- throw new Error('No advanceTimers provided for table transition.');
151
- }
139
+ if (!this._advanceTimer) {
140
+ throw new Error('No advanceTimers provided for table transition.');
141
+ }
152
142
 
153
- await this._advanceTimer(200);
143
+ await act(async () => {
144
+ await this._advanceTimer?.(200);
154
145
  });
155
146
 
156
147
  await waitFor(() => {
package/src/testSetup.ts CHANGED
@@ -14,9 +14,8 @@
14
14
  * Enables reading pageX/pageY from fireEvent.mouse*(..., {pageX: ..., pageY: ...}).
15
15
  */
16
16
  export function installMouseEvent() {
17
+ let oldMouseEvent = MouseEvent;
17
18
  beforeAll(() => {
18
- let oldMouseEvent = MouseEvent;
19
- // @ts-ignore
20
19
  global.MouseEvent = class FakeMouseEvent extends MouseEvent {
21
20
  _init: {pageX: number, pageY: number};
22
21
  constructor(name, init) {
@@ -30,12 +29,9 @@ export function installMouseEvent() {
30
29
  return this._init.pageY;
31
30
  }
32
31
  };
33
- // @ts-ignore
34
- global.MouseEvent.oldMouseEvent = oldMouseEvent;
35
32
  });
36
33
  afterAll(() => {
37
- // @ts-ignore
38
- global.MouseEvent = global.MouseEvent.oldMouseEvent;
34
+ global.MouseEvent = oldMouseEvent;
39
35
  });
40
36
  }
41
37
 
package/src/user.ts CHANGED
@@ -68,6 +68,6 @@ export class User {
68
68
  }
69
69
 
70
70
  createTester<T extends PatternNames>(patternName: T, opts: ObjectOptionsTypes<T>): ObjectType<T> {
71
- return new (keyToUtil)[patternName]({...opts, user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer}) as ObjectType<T>;
71
+ return new (keyToUtil)[patternName]({user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts}) as ObjectType<T>;
72
72
  }
73
73
  }