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

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 (64) hide show
  1. package/dist/combobox.main.js +116 -82
  2. package/dist/combobox.main.js.map +1 -1
  3. package/dist/combobox.mjs +117 -83
  4. package/dist/combobox.module.js +117 -83
  5. package/dist/combobox.module.js.map +1 -1
  6. package/dist/events.main.js.map +1 -1
  7. package/dist/events.module.js.map +1 -1
  8. package/dist/gridlist.main.js +102 -59
  9. package/dist/gridlist.main.js.map +1 -1
  10. package/dist/gridlist.mjs +103 -60
  11. package/dist/gridlist.module.js +103 -60
  12. package/dist/gridlist.module.js.map +1 -1
  13. package/dist/listbox.main.js +135 -0
  14. package/dist/listbox.main.js.map +1 -0
  15. package/dist/listbox.mjs +130 -0
  16. package/dist/listbox.module.js +130 -0
  17. package/dist/listbox.module.js.map +1 -0
  18. package/dist/main.js.map +1 -1
  19. package/dist/menu.main.js +195 -161
  20. package/dist/menu.main.js.map +1 -1
  21. package/dist/menu.mjs +196 -162
  22. package/dist/menu.module.js +196 -162
  23. package/dist/menu.module.js.map +1 -1
  24. package/dist/module.js.map +1 -1
  25. package/dist/select.main.js +116 -70
  26. package/dist/select.main.js.map +1 -1
  27. package/dist/select.mjs +117 -71
  28. package/dist/select.module.js +117 -71
  29. package/dist/select.module.js.map +1 -1
  30. package/dist/table.main.js +170 -139
  31. package/dist/table.main.js.map +1 -1
  32. package/dist/table.mjs +171 -140
  33. package/dist/table.module.js +171 -140
  34. package/dist/table.module.js.map +1 -1
  35. package/dist/tabs.main.js +133 -0
  36. package/dist/tabs.main.js.map +1 -0
  37. package/dist/tabs.mjs +128 -0
  38. package/dist/tabs.module.js +128 -0
  39. package/dist/tabs.module.js.map +1 -0
  40. package/dist/tree.main.js +165 -0
  41. package/dist/tree.main.js.map +1 -0
  42. package/dist/tree.mjs +160 -0
  43. package/dist/tree.module.js +160 -0
  44. package/dist/tree.module.js.map +1 -0
  45. package/dist/types.d.ts +607 -145
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/user.main.js +15 -4
  48. package/dist/user.main.js.map +1 -1
  49. package/dist/user.mjs +15 -4
  50. package/dist/user.module.js +15 -4
  51. package/dist/user.module.js.map +1 -1
  52. package/package.json +2 -3
  53. package/src/combobox.ts +105 -36
  54. package/src/events.ts +1 -1
  55. package/src/gridlist.ts +146 -59
  56. package/src/index.ts +1 -1
  57. package/src/listbox.ts +226 -0
  58. package/src/menu.ts +152 -62
  59. package/src/select.ts +137 -44
  60. package/src/table.ts +129 -64
  61. package/src/tabs.ts +198 -0
  62. package/src/tree.ts +248 -0
  63. package/src/types.ts +133 -0
  64. package/src/user.ts +62 -37
package/src/menu.ts CHANGED
@@ -11,13 +11,53 @@
11
11
  */
12
12
 
13
13
  import {act, waitFor, within} from '@testing-library/react';
14
- import {BaseTesterOpts, UserOpts} from './user';
14
+ import {MenuTesterOpts, UserOpts} from './types';
15
15
  import {triggerLongPress} from './events';
16
16
 
17
- export interface MenuOptions extends UserOpts, BaseTesterOpts {
18
- user?: any,
19
- isSubmenu?: boolean
17
+ interface MenuOpenOpts {
18
+ /**
19
+ * Whether the menu needs to be long pressed to open.
20
+ */
21
+ needsLongPress?: boolean,
22
+ /**
23
+ * What interaction type to use when opening the menu. Defaults to the interaction type set on the tester.
24
+ */
25
+ interactionType?: UserOpts['interactionType'],
26
+ /**
27
+ * Whether to open the menu via ArrowUp or ArrowDown if in keyboard modality.
28
+ */
29
+ direction?: 'up' | 'down'
20
30
  }
31
+
32
+ interface MenuSelectOpts extends MenuOpenOpts {
33
+ /**
34
+ * The index, text, or node of the option to select. Option nodes can be sourced via `options()`.
35
+ */
36
+ option: number | string | HTMLElement,
37
+ /**
38
+ * The menu's selection mode. Will affect whether or not the menu is expected to be closed upon option selection.
39
+ * @default 'single'
40
+ */
41
+ menuSelectionMode?: 'single' | 'multiple',
42
+ /**
43
+ * Whether or not the menu closes on select. Depends on menu implementation and configuration.
44
+ * @default true
45
+ */
46
+ closesOnSelect?: boolean,
47
+ /**
48
+ * Whether the option should be triggered by Space or Enter in keyboard modality.
49
+ * @default 'Enter'
50
+ */
51
+ keyboardActivation?: 'Space' | 'Enter'
52
+ }
53
+
54
+ interface MenuOpenSubmenuOpts extends MenuOpenOpts {
55
+ /**
56
+ * The text or node of the submenu trigger to open. Available submenu trigger nodes can be sourced via `submenuTriggers`.
57
+ */
58
+ submenuTrigger: string | HTMLElement
59
+ }
60
+
21
61
  export class MenuTester {
22
62
  private user;
23
63
  private _interactionType: UserOpts['interactionType'];
@@ -25,7 +65,7 @@ export class MenuTester {
25
65
  private _trigger: HTMLElement | undefined;
26
66
  private _isSubmenu: boolean = false;
27
67
 
28
- constructor(opts: MenuOptions) {
68
+ constructor(opts: MenuTesterOpts) {
29
69
  let {root, user, interactionType, advanceTimer, isSubmenu} = opts;
30
70
  this.user = user;
31
71
  this._interactionType = interactionType || 'mouse';
@@ -47,13 +87,19 @@ export class MenuTester {
47
87
  this._isSubmenu = isSubmenu || false;
48
88
  }
49
89
 
50
- setInteractionType = (type: UserOpts['interactionType']) => {
90
+ /**
91
+ * Set the interaction type used by the menu tester.
92
+ */
93
+ setInteractionType(type: UserOpts['interactionType']) {
51
94
  this._interactionType = type;
52
- };
95
+ }
53
96
 
54
97
  // 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
55
98
  // One difference will be that it supports long press as well
56
- open = async (opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType'], direction?: 'up' | 'down'} = {}) => {
99
+ /**
100
+ * Opens the menu. Defaults to using the interaction type set on the menu tester.
101
+ */
102
+ async open(opts: MenuOpenOpts = {}) {
57
103
  let {
58
104
  needsLongPress,
59
105
  interactionType = this._interactionType,
@@ -103,21 +149,37 @@ export class MenuTester {
103
149
  }
104
150
  });
105
151
  }
106
- };
152
+ }
153
+
154
+ /**
155
+ * Returns a option matching the specified index or text content.
156
+ */
157
+ findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
158
+ let {
159
+ optionIndexOrText
160
+ } = opts;
161
+
162
+ let option;
163
+ let options = this.options();
164
+ let menu = this.menu;
165
+
166
+ if (typeof optionIndexOrText === 'number') {
167
+ option = options[optionIndexOrText];
168
+ } else if (typeof optionIndexOrText === 'string' && menu != null) {
169
+ option = (within(menu!).getByText(optionIndexOrText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement;
170
+ }
171
+
172
+ return option;
173
+ }
107
174
 
108
175
  // TODO: also very similar to select, barring potential long press support
109
176
  // Close on select is also kinda specific?
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
- }) => {
177
+ /**
178
+ * 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
+ * The desired option can be targeted via the option's node, the option's text, or the option's index.
180
+ */
181
+ async selectOption(opts: MenuSelectOpts) {
119
182
  let {
120
- optionText,
121
183
  menuSelectionMode = 'single',
122
184
  needsLongPress,
123
185
  closesOnSelect = true,
@@ -132,15 +194,25 @@ export class MenuTester {
132
194
  }
133
195
 
134
196
  let menu = this.menu;
197
+
198
+ if (!menu) {
199
+ throw new Error('Menu not found.');
200
+ }
201
+
135
202
  if (menu) {
136
- if (!option && optionText) {
137
- option = (within(menu!).getByText(optionText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement;
203
+ if (typeof option === 'string' || typeof option === 'number') {
204
+ option = this.findOption({optionIndexOrText: option});
138
205
  }
206
+
139
207
  if (!option) {
140
- throw new Error('No option found in the menu.');
208
+ throw new Error('Target option not found in the menu.');
141
209
  }
142
210
 
143
211
  if (interactionType === 'keyboard') {
212
+ if (option?.getAttribute('aria-disabled') === 'true') {
213
+ return;
214
+ }
215
+
144
216
  if (document.activeElement !== menu || !menu.contains(document.activeElement)) {
145
217
  act(() => menu.focus());
146
218
  }
@@ -156,7 +228,7 @@ export class MenuTester {
156
228
  }
157
229
  act(() => {jest.runAllTimers();});
158
230
 
159
- if (option && option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) {
231
+ if (option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) {
160
232
  await waitFor(() => {
161
233
  if (document.activeElement !== trigger) {
162
234
  throw new Error(`Expected the document.activeElement after selecting an option to be the menu trigger but got ${document.activeElement}`);
@@ -172,13 +244,15 @@ export class MenuTester {
172
244
  } else {
173
245
  throw new Error("Attempted to select a option in the menu, but menu wasn't found.");
174
246
  }
175
- };
247
+ }
176
248
 
177
249
  // TODO: update this to remove needsLongPress if we wanna make the user call open first always
178
- openSubmenu = async (opts: {submenuTrigger?: HTMLElement, submenuTriggerText?: string, needsLongPress?: boolean, interactionType?: UserOpts['interactionType']}): Promise<MenuTester | null> => {
250
+ /**
251
+ * Opens the submenu. Defaults to using the interaction type set on the menu tester. The submenu trigger can be targeted via the trigger's node or the trigger's text.
252
+ */
253
+ async openSubmenu(opts: MenuOpenSubmenuOpts): Promise<MenuTester | null> {
179
254
  let {
180
255
  submenuTrigger,
181
- submenuTriggerText,
182
256
  needsLongPress,
183
257
  interactionType = this._interactionType
184
258
  } = opts;
@@ -191,19 +265,16 @@ export class MenuTester {
191
265
  if (!isDisabled) {
192
266
  let menu = this.menu;
193
267
  if (menu) {
194
- let submenu;
195
- if (submenuTrigger) {
196
- submenu = submenuTrigger;
197
- } else if (submenuTriggerText) {
198
- submenu = within(menu).getByText(submenuTriggerText);
268
+ if (typeof submenuTrigger === 'string') {
269
+ submenuTrigger = (within(menu!).getByText(submenuTrigger).closest('[role=menuitem]'))! as HTMLElement;
199
270
  }
200
271
 
201
- let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenu, isSubmenu: true});
272
+ let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenuTrigger, isSubmenu: true});
202
273
  if (interactionType === 'mouse') {
203
- await this.user.pointer({target: submenu});
274
+ await this.user.pointer({target: submenuTrigger});
204
275
  act(() => {jest.runAllTimers();});
205
276
  } else if (interactionType === 'keyboard') {
206
- await this.keyboardNavigateToOption({option: submenu});
277
+ await this.keyboardNavigateToOption({option: submenuTrigger});
207
278
  await this.user.keyboard('[ArrowRight]');
208
279
  act(() => {jest.runAllTimers();});
209
280
  } else {
@@ -216,12 +287,13 @@ export class MenuTester {
216
287
  }
217
288
 
218
289
  return null;
219
- };
290
+ }
220
291
 
221
- keyboardNavigateToOption = async (opts: {option: HTMLElement}) => {
292
+ private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
222
293
  let {option} = opts;
223
- let options = this.options;
224
- let targetIndex = options.indexOf(option);
294
+ let options = this.options();
295
+ let targetIndex = options.findIndex(opt => (opt === option) || opt.contains(option));
296
+
225
297
  if (targetIndex === -1) {
226
298
  throw new Error('Option provided is not in the menu');
227
299
  }
@@ -229,7 +301,7 @@ export class MenuTester {
229
301
  await this.user.keyboard('[ArrowDown]');
230
302
  }
231
303
  let currIndex = options.indexOf(document.activeElement as HTMLElement);
232
- if (targetIndex === -1) {
304
+ if (currIndex === -1) {
233
305
  throw new Error('ActiveElement is not in the menu');
234
306
  }
235
307
  let direction = targetIndex > currIndex ? 'down' : 'up';
@@ -239,8 +311,10 @@ export class MenuTester {
239
311
  }
240
312
  };
241
313
 
242
-
243
- close = async () => {
314
+ /**
315
+ * Closes the menu.
316
+ */
317
+ async close() {
244
318
  let menu = this.menu;
245
319
  if (menu) {
246
320
  act(() => menu.focus());
@@ -258,29 +332,51 @@ export class MenuTester {
258
332
  throw new Error('Expected the menu to not be in the document after closing it.');
259
333
  }
260
334
  }
261
- };
335
+ }
262
336
 
263
- get trigger() {
337
+ /**
338
+ * Returns the menu's trigger.
339
+ */
340
+ get trigger(): HTMLElement {
264
341
  if (!this._trigger) {
265
342
  throw new Error('No trigger element found for menu.');
266
343
  }
344
+
267
345
  return this._trigger;
268
346
  }
269
347
 
270
- get menu() {
348
+ /**
349
+ * Returns the menu if present.
350
+ */
351
+ get menu(): HTMLElement | null {
271
352
  let menuId = this.trigger.getAttribute('aria-controls');
272
- return menuId ? document.getElementById(menuId) : undefined;
353
+ return menuId ? document.getElementById(menuId) : null;
273
354
  }
274
355
 
275
- get options(): HTMLElement[] {
356
+ /**
357
+ * Returns the menu's sections if any.
358
+ */
359
+ get sections(): HTMLElement[] {
276
360
  let menu = this.menu;
277
- let options: HTMLElement[] = [];
278
361
  if (menu) {
279
- options = within(menu).queryAllByRole('menuitem');
362
+ return within(menu).queryAllByRole('group');
363
+ } else {
364
+ return [];
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Returns the menu's options if present. Can be filtered to a subsection of the menu if provided via `element`.
370
+ */
371
+ options(opts: {element?: HTMLElement} = {}): HTMLElement[] {
372
+ let {element = this.menu} = opts;
373
+ let options: HTMLElement[] = [];
374
+ if (element) {
375
+ options = within(element).queryAllByRole('menuitem');
280
376
  if (options.length === 0) {
281
- options = within(menu).queryAllByRole('menuitemradio');
377
+ options = within(element).queryAllByRole('menuitemradio');
282
378
  if (options.length === 0) {
283
- options = within(menu).queryAllByRole('menuitemcheckbox');
379
+ options = within(element).queryAllByRole('menuitemcheckbox');
284
380
  }
285
381
  }
286
382
  }
@@ -288,19 +384,13 @@ export class MenuTester {
288
384
  return options;
289
385
  }
290
386
 
291
- get sections() {
292
- let menu = this.menu;
293
- if (menu) {
294
- return within(menu).queryAllByRole('group');
295
- } else {
296
- return [];
297
- }
298
- }
299
-
300
- get submenuTriggers() {
301
- let options = this.options;
387
+ /**
388
+ * Returns the menu's submenu triggers if any.
389
+ */
390
+ get submenuTriggers(): HTMLElement[] {
391
+ let options = this.options();
302
392
  if (options.length > 0) {
303
- return this.options.filter(item => item.getAttribute('aria-haspopup') != null);
393
+ return options.filter(item => item.getAttribute('aria-haspopup') != null);
304
394
  }
305
395
 
306
396
  return [];
package/src/select.ts CHANGED
@@ -11,18 +11,28 @@
11
11
  */
12
12
 
13
13
  import {act, waitFor, within} from '@testing-library/react';
14
- import {BaseTesterOpts, UserOpts} from './user';
14
+ import {SelectTesterOpts, UserOpts} from './types';
15
15
 
16
- export interface SelectOptions extends UserOpts, BaseTesterOpts {
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
16
+ interface SelectOpenOpts {
17
+ /**
18
+ * What interaction type to use when opening the select. Defaults to the interaction type set on the tester.
19
+ */
20
+ interactionType?: UserOpts['interactionType']
19
21
  }
22
+
23
+ interface SelectTriggerOptionOpts extends SelectOpenOpts {
24
+ /**
25
+ * The index, text, or node of the option to select. Option nodes can be sourced via `options()`.
26
+ */
27
+ option: number | string | HTMLElement
28
+ }
29
+
20
30
  export class SelectTester {
21
31
  private user;
22
32
  private _interactionType: UserOpts['interactionType'];
23
33
  private _trigger: HTMLElement;
24
34
 
25
- constructor(opts: SelectOptions) {
35
+ constructor(opts: SelectTesterOpts) {
26
36
  let {root, user, interactionType} = opts;
27
37
  this.user = user;
28
38
  this._interactionType = interactionType || 'mouse';
@@ -33,12 +43,17 @@ export class SelectTester {
33
43
  }
34
44
  this._trigger = triggerButton;
35
45
  }
36
-
37
- setInteractionType = (type: UserOpts['interactionType']) => {
46
+ /**
47
+ * Set the interaction type used by the select tester.
48
+ */
49
+ setInteractionType(type: UserOpts['interactionType']) {
38
50
  this._interactionType = type;
39
- };
51
+ }
40
52
 
41
- open = async (opts: {interactionType?: UserOpts['interactionType']} = {}) => {
53
+ /**
54
+ * Opens the select. Defaults to using the interaction type set on the select tester.
55
+ */
56
+ async open(opts: SelectOpenOpts = {}) {
42
57
  let {
43
58
  interactionType = this._interactionType
44
59
  } = opts;
@@ -69,11 +84,80 @@ export class SelectTester {
69
84
  return true;
70
85
  }
71
86
  });
87
+ }
88
+
89
+ /**
90
+ * Closes the select.
91
+ */
92
+ async close() {
93
+ let listbox = this.listbox;
94
+ if (listbox) {
95
+ act(() => listbox.focus());
96
+ await this.user.keyboard('[Escape]');
97
+ }
98
+
99
+ await waitFor(() => {
100
+ if (document.activeElement !== this._trigger) {
101
+ throw new Error(`Expected the document.activeElement after closing the select dropdown to be the select component trigger but got ${document.activeElement}`);
102
+ } else {
103
+ return true;
104
+ }
105
+ });
106
+
107
+ if (listbox && document.contains(listbox)) {
108
+ throw new Error('Expected the select element listbox to not be in the document after closing the dropdown.');
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Returns a option matching the specified index or text content.
114
+ */
115
+ findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
116
+ let {
117
+ optionIndexOrText
118
+ } = opts;
119
+
120
+ let option;
121
+ let options = this.options();
122
+ let listbox = this.listbox;
123
+
124
+ if (typeof optionIndexOrText === 'number') {
125
+ option = options[optionIndexOrText];
126
+ } else if (typeof optionIndexOrText === 'string' && listbox != null) {
127
+ option = (within(listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement;
128
+ }
129
+
130
+ return option;
131
+ }
132
+
133
+ private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
134
+ let {option} = opts;
135
+ let options = this.options();
136
+ let targetIndex = options.indexOf(option);
137
+ if (targetIndex === -1) {
138
+ throw new Error('Option provided is not in the listbox');
139
+ }
140
+ if (document.activeElement === this.listbox) {
141
+ await this.user.keyboard('[ArrowDown]');
142
+ }
143
+ let currIndex = options.indexOf(document.activeElement as HTMLElement);
144
+ if (currIndex === -1) {
145
+ throw new Error('ActiveElement is not in the listbox');
146
+ }
147
+ let direction = targetIndex > currIndex ? 'down' : 'up';
148
+
149
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
150
+ await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
151
+ }
72
152
  };
73
153
 
74
- selectOption = async (opts: {optionText: string, interactionType?: UserOpts['interactionType']}) => {
154
+ /**
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
+ * The desired option can be targeted via the option's node, the option's text, or the option's index.
157
+ */
158
+ async selectOption(opts: SelectTriggerOptionOpts) {
75
159
  let {
76
- optionText,
160
+ option,
77
161
  interactionType = this._interactionType
78
162
  } = opts || {};
79
163
  let trigger = this.trigger;
@@ -81,16 +165,28 @@ export class SelectTester {
81
165
  await this.open();
82
166
  }
83
167
  let listbox = this.listbox;
168
+ if (!listbox) {
169
+ throw new Error('Select\'s listbox not found.');
170
+ }
171
+
84
172
  if (listbox) {
85
- let option = within(listbox).getByText(optionText);
173
+ if (typeof option === 'string' || typeof option === 'number') {
174
+ option = this.findOption({optionIndexOrText: option});
175
+ }
176
+
177
+ if (!option) {
178
+ throw new Error('Target option not found in the listbox.');
179
+ }
180
+
86
181
  if (interactionType === 'keyboard') {
182
+ if (option?.getAttribute('aria-disabled') === 'true') {
183
+ return;
184
+ }
185
+
87
186
  if (document.activeElement !== listbox || !listbox.contains(document.activeElement)) {
88
187
  act(() => listbox.focus());
89
188
  }
90
-
91
- // TODO: this simulates typeahead, do we want to add a helper util for that? Not sure if users would really need that for
92
- // their test
93
- await this.user.keyboard(optionText);
189
+ await this.keyboardNavigateToOption({option});
94
190
  await this.user.keyboard('[Enter]');
95
191
  } else {
96
192
  // TODO: what if the user needs to scroll the list to find the option? What if there are multiple matches for text (hopefully the picker options are pretty unique)
@@ -101,7 +197,7 @@ export class SelectTester {
101
197
  }
102
198
  }
103
199
 
104
- if (option.getAttribute('href') == null) {
200
+ if (option?.getAttribute('href') == null) {
105
201
  await waitFor(() => {
106
202
  if (document.activeElement !== this._trigger) {
107
203
  throw new Error(`Expected the document.activeElement after selecting an option to be the select component trigger but got ${document.activeElement}`);
@@ -115,43 +211,40 @@ export class SelectTester {
115
211
  }
116
212
  }
117
213
  }
118
- };
214
+ }
119
215
 
120
- close = async () => {
121
- let listbox = this.listbox;
122
- if (listbox) {
123
- act(() => listbox.focus());
124
- await this.user.keyboard('[Escape]');
216
+ /**
217
+ * Returns the select's options if present. Can be filtered to a subsection of the listbox if provided via `element`.
218
+ */
219
+ options(opts: {element?: HTMLElement} = {}): HTMLElement[] {
220
+ let {element = this.listbox} = opts;
221
+ let options = [];
222
+ if (element) {
223
+ options = within(element).queryAllByRole('option');
125
224
  }
126
225
 
127
- await waitFor(() => {
128
- if (document.activeElement !== this._trigger) {
129
- throw new Error(`Expected the document.activeElement after closing the select dropdown to be the select component trigger but got ${document.activeElement}`);
130
- } else {
131
- return true;
132
- }
133
- });
134
-
135
- if (listbox && document.contains(listbox)) {
136
- throw new Error('Expected the select element listbox to not be in the document after closing the dropdown.');
137
- }
138
- };
226
+ return options;
227
+ }
139
228
 
140
- get trigger() {
229
+ /**
230
+ * Returns the select's trigger.
231
+ */
232
+ get trigger(): HTMLElement {
141
233
  return this._trigger;
142
234
  }
143
235
 
144
- get listbox() {
236
+ /**
237
+ * Returns the select's listbox if present.
238
+ */
239
+ get listbox(): HTMLElement | null {
145
240
  let listBoxId = this.trigger.getAttribute('aria-controls');
146
- return listBoxId ? document.getElementById(listBoxId) : undefined;
147
- }
148
-
149
- get options() {
150
- let listbox = this.listbox;
151
- return listbox ? within(listbox).queryAllByRole('option') : [];
241
+ return listBoxId ? document.getElementById(listBoxId) : null;
152
242
  }
153
243
 
154
- get sections() {
244
+ /**
245
+ * Returns the select's sections if present.
246
+ */
247
+ get sections(): HTMLElement[] {
155
248
  let listbox = this.listbox;
156
249
  return listbox ? within(listbox).queryAllByRole('group') : [];
157
250
  }