@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.
- package/dist/combobox.main.js +116 -82
- package/dist/combobox.main.js.map +1 -1
- package/dist/combobox.mjs +117 -83
- package/dist/combobox.module.js +117 -83
- package/dist/combobox.module.js.map +1 -1
- package/dist/events.main.js.map +1 -1
- package/dist/events.module.js.map +1 -1
- package/dist/gridlist.main.js +102 -59
- package/dist/gridlist.main.js.map +1 -1
- package/dist/gridlist.mjs +103 -60
- package/dist/gridlist.module.js +103 -60
- package/dist/gridlist.module.js.map +1 -1
- package/dist/listbox.main.js +135 -0
- package/dist/listbox.main.js.map +1 -0
- package/dist/listbox.mjs +130 -0
- package/dist/listbox.module.js +130 -0
- package/dist/listbox.module.js.map +1 -0
- package/dist/main.js.map +1 -1
- package/dist/menu.main.js +195 -161
- package/dist/menu.main.js.map +1 -1
- package/dist/menu.mjs +196 -162
- package/dist/menu.module.js +196 -162
- package/dist/menu.module.js.map +1 -1
- package/dist/module.js.map +1 -1
- package/dist/select.main.js +116 -70
- package/dist/select.main.js.map +1 -1
- package/dist/select.mjs +117 -71
- package/dist/select.module.js +117 -71
- package/dist/select.module.js.map +1 -1
- package/dist/table.main.js +170 -139
- package/dist/table.main.js.map +1 -1
- package/dist/table.mjs +171 -140
- package/dist/table.module.js +171 -140
- package/dist/table.module.js.map +1 -1
- package/dist/tabs.main.js +133 -0
- package/dist/tabs.main.js.map +1 -0
- package/dist/tabs.mjs +128 -0
- package/dist/tabs.module.js +128 -0
- package/dist/tabs.module.js.map +1 -0
- package/dist/tree.main.js +165 -0
- package/dist/tree.main.js.map +1 -0
- package/dist/tree.mjs +160 -0
- package/dist/tree.module.js +160 -0
- package/dist/tree.module.js.map +1 -0
- package/dist/types.d.ts +607 -145
- package/dist/types.d.ts.map +1 -1
- package/dist/user.main.js +15 -4
- package/dist/user.main.js.map +1 -1
- package/dist/user.mjs +15 -4
- package/dist/user.module.js +15 -4
- package/dist/user.module.js.map +1 -1
- package/package.json +2 -3
- package/src/combobox.ts +105 -36
- package/src/events.ts +1 -1
- package/src/gridlist.ts +146 -59
- package/src/index.ts +1 -1
- package/src/listbox.ts +226 -0
- package/src/menu.ts +152 -62
- package/src/select.ts +137 -44
- package/src/table.ts +129 -64
- package/src/tabs.ts +198 -0
- package/src/tree.ts +248 -0
- package/src/types.ts +133 -0
- 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 {
|
|
14
|
+
import {MenuTesterOpts, UserOpts} from './types';
|
|
15
15
|
import {triggerLongPress} from './events';
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 (
|
|
137
|
-
option =
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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:
|
|
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:
|
|
274
|
+
await this.user.pointer({target: submenuTrigger});
|
|
204
275
|
act(() => {jest.runAllTimers();});
|
|
205
276
|
} else if (interactionType === 'keyboard') {
|
|
206
|
-
await this.keyboardNavigateToOption({option:
|
|
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
|
-
|
|
292
|
+
private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
|
|
222
293
|
let {option} = opts;
|
|
223
|
-
let options = this.options;
|
|
224
|
-
let targetIndex = options.
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) :
|
|
353
|
+
return menuId ? document.getElementById(menuId) : null;
|
|
273
354
|
}
|
|
274
355
|
|
|
275
|
-
|
|
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
|
-
|
|
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(
|
|
377
|
+
options = within(element).queryAllByRole('menuitemradio');
|
|
282
378
|
if (options.length === 0) {
|
|
283
|
-
options = within(
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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 {
|
|
14
|
+
import {SelectTesterOpts, UserOpts} from './types';
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Returns the select's trigger.
|
|
231
|
+
*/
|
|
232
|
+
get trigger(): HTMLElement {
|
|
141
233
|
return this._trigger;
|
|
142
234
|
}
|
|
143
235
|
|
|
144
|
-
|
|
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) :
|
|
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
|
-
|
|
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
|
}
|