@react-aria/test-utils 1.0.0-alpha.6 → 1.0.0-alpha.8
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 +5 -4
- package/dist/combobox.main.js.map +1 -1
- package/dist/combobox.mjs +5 -4
- package/dist/combobox.module.js +5 -4
- package/dist/combobox.module.js.map +1 -1
- package/dist/events.main.js +31 -1
- package/dist/events.main.js.map +1 -1
- package/dist/events.mjs +30 -2
- package/dist/events.module.js +30 -2
- package/dist/events.module.js.map +1 -1
- package/dist/gridlist.main.js +23 -9
- package/dist/gridlist.main.js.map +1 -1
- package/dist/gridlist.mjs +24 -10
- package/dist/gridlist.module.js +24 -10
- package/dist/gridlist.module.js.map +1 -1
- package/dist/listbox.main.js +22 -13
- package/dist/listbox.main.js.map +1 -1
- package/dist/listbox.mjs +23 -14
- package/dist/listbox.module.js +23 -14
- package/dist/listbox.module.js.map +1 -1
- package/dist/menu.main.js +1 -1
- package/dist/menu.main.js.map +1 -1
- package/dist/menu.mjs +1 -1
- package/dist/menu.module.js +1 -1
- package/dist/menu.module.js.map +1 -1
- package/dist/select.main.js +7 -4
- package/dist/select.main.js.map +1 -1
- package/dist/select.mjs +7 -4
- package/dist/select.module.js +7 -4
- package/dist/select.module.js.map +1 -1
- package/dist/table.main.js +90 -9
- package/dist/table.main.js.map +1 -1
- package/dist/table.mjs +92 -11
- package/dist/table.module.js +92 -11
- package/dist/table.module.js.map +1 -1
- package/dist/tabs.main.js +1 -1
- package/dist/tabs.main.js.map +1 -1
- package/dist/tabs.mjs +1 -1
- package/dist/tabs.module.js +1 -1
- package/dist/tabs.module.js.map +1 -1
- package/dist/tree.main.js +27 -8
- package/dist/tree.main.js.map +1 -1
- package/dist/tree.mjs +28 -9
- package/dist/tree.module.js +28 -9
- package/dist/tree.module.js.map +1 -1
- package/dist/types.d.ts +31 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/userEventMaps.main.js.map +1 -1
- package/dist/userEventMaps.module.js.map +1 -1
- package/package.json +7 -3
- package/src/combobox.ts +10 -7
- package/src/events.ts +34 -2
- package/src/gridlist.ts +35 -12
- package/src/listbox.ts +47 -19
- package/src/menu.ts +1 -1
- package/src/select.ts +10 -4
- package/src/table.ts +163 -10
- package/src/tabs.ts +1 -1
- package/src/tree.ts +40 -10
- package/src/types.ts +11 -1
- package/src/userEventMaps.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":";;;;;;AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event
|
|
1
|
+
{"mappings":";;;;;;AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.main.js.map"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"mappings":"AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event
|
|
1
|
+
{"mappings":"AAEO,IAAI,4CAA2B;IACpC;QAAC,MAAM;QAAa,aAAa;QAAS,QAAQ;QAAW,QAAQ;QAAG,OAAO;QAAG,UAAU;IAAG;IAC/F;QAAC,MAAM;QAAc,aAAa;QAAS,QAAQ;IAAW;IAC9D;QAAC,MAAM;QAAe,aAAa;QAAS,QAAQ;IAAW;IAC/D;QAAC,MAAM;QAAU,aAAa;QAAS,QAAQ;QAAG,OAAO;IAAC;IAC1D;QAAC,MAAM;QAAU,aAAa;IAAO;IACrC;QAAC,MAAM;QAAU,aAAa;IAAO;CACtC","sources":["packages/@react-aria/test-utils/src/userEventMaps.ts"],"sourcesContent":["import {pointerKey} from '@testing-library/user-event';\n\nexport let pointerMap: pointerKey[] = [\n {name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5},\n {name: 'MouseRight', pointerType: 'mouse', button: 'secondary'},\n {name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary'},\n {name: 'TouchA', pointerType: 'touch', height: 1, width: 1},\n {name: 'TouchB', pointerType: 'touch'},\n {name: 'TouchC', pointerType: 'touch'}\n] as unknown as pointerKey[];\n"],"names":[],"version":3,"file":"userEventMaps.module.js.map"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-aria/test-utils",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "Testing utils for react-aria patterns",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/main.js",
|
|
7
7
|
"module": "dist/module.js",
|
|
8
8
|
"exports": {
|
|
9
|
-
"
|
|
9
|
+
"source": "./src/index.ts",
|
|
10
|
+
"types": [
|
|
11
|
+
"./dist/types.d.ts",
|
|
12
|
+
"./src/index.ts"
|
|
13
|
+
],
|
|
10
14
|
"import": "./dist/import.mjs",
|
|
11
15
|
"require": "./dist/main.js"
|
|
12
16
|
},
|
|
@@ -33,5 +37,5 @@
|
|
|
33
37
|
"publishConfig": {
|
|
34
38
|
"access": "public"
|
|
35
39
|
},
|
|
36
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "265b4d7f107905ee1c6e87a8af1613ab440a6849"
|
|
37
41
|
}
|
package/src/combobox.ts
CHANGED
|
@@ -55,14 +55,17 @@ export class ComboBoxTester {
|
|
|
55
55
|
if (trigger) {
|
|
56
56
|
this._trigger = trigger;
|
|
57
57
|
} else {
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this._trigger = this._combobox;
|
|
58
|
+
let buttons = within(root).queryAllByRole('button', {hidden: true});
|
|
59
|
+
|
|
60
|
+
if (buttons.length === 1) {
|
|
61
|
+
trigger = buttons[0];
|
|
62
|
+
} else if (buttons.length > 1) {
|
|
63
|
+
trigger = buttons.find(button => button.hasAttribute('aria-haspopup'));
|
|
65
64
|
}
|
|
65
|
+
|
|
66
|
+
// For cases like https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ where the combobox
|
|
67
|
+
// is also the trigger button
|
|
68
|
+
this._trigger = trigger || this._combobox;
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
package/src/events.ts
CHANGED
|
@@ -14,6 +14,37 @@ import {act, fireEvent} from '@testing-library/react';
|
|
|
14
14
|
import {UserOpts} from './types';
|
|
15
15
|
|
|
16
16
|
export const DEFAULT_LONG_PRESS_TIME = 500;
|
|
17
|
+
function testPlatform(re: RegExp) {
|
|
18
|
+
return typeof window !== 'undefined' && window.navigator != null
|
|
19
|
+
? re.test(window.navigator['userAgentData']?.platform || window.navigator.platform)
|
|
20
|
+
: false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cached(fn: () => boolean) {
|
|
24
|
+
if (process.env.NODE_ENV === 'test') {
|
|
25
|
+
return fn;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let res: boolean | null = null;
|
|
29
|
+
return () => {
|
|
30
|
+
if (res == null) {
|
|
31
|
+
res = fn();
|
|
32
|
+
}
|
|
33
|
+
return res;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isMac = cached(function () {
|
|
38
|
+
return testPlatform(/^Mac/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export function getAltKey(): 'Alt' | 'ControlLeft' {
|
|
42
|
+
return isMac() ? 'Alt' : 'ControlLeft';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getMetaKey(): 'MetaLeft' | 'ControlLeft' {
|
|
46
|
+
return isMac() ? 'MetaLeft' : 'ControlLeft';
|
|
47
|
+
}
|
|
17
48
|
|
|
18
49
|
/**
|
|
19
50
|
* Simulates a "long press" event on a element.
|
|
@@ -58,9 +89,10 @@ export async function triggerLongPress(opts: {element: HTMLElement, advanceTimer
|
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
// Docs cannot handle the types that userEvent actually declares, so hopefully this sub set is okay
|
|
61
|
-
export async function pressElement(user: {click: (element: Element) => Promise<void>, keyboard: (keys: string) => Promise<void>, pointer: (opts: {target: Element, keys: string}) => Promise<void>}, element: HTMLElement, interactionType: UserOpts['interactionType']): Promise<void> {
|
|
92
|
+
export async function pressElement(user: {click: (element: Element) => Promise<void>, keyboard: (keys: string) => Promise<void>, pointer: (opts: {target: Element, keys: string, coords?: any}) => Promise<void>}, element: HTMLElement, interactionType: UserOpts['interactionType']): Promise<void> {
|
|
62
93
|
if (interactionType === 'mouse') {
|
|
63
|
-
|
|
94
|
+
// Add coords with pressure so this isn't detected as a virtual click
|
|
95
|
+
await user.pointer({target: element, keys: '[MouseLeft]', coords: {pressure: .5}});
|
|
64
96
|
} else if (interactionType === 'keyboard') {
|
|
65
97
|
// TODO: For the keyboard flow, I wonder if it would be reasonable to just do fireEvent directly on the obtained row node or if we should
|
|
66
98
|
// stick to simulting an actual user's keyboard operations as closely as possible
|
package/src/gridlist.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {act, within} from '@testing-library/react';
|
|
14
|
+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
|
|
14
15
|
import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types';
|
|
15
|
-
import {pressElement, triggerLongPress} from './events';
|
|
16
16
|
|
|
17
17
|
interface GridListToggleRowOpts extends ToggleGridRowOpts {}
|
|
18
18
|
interface GridListRowActionOpts extends GridRowActionOpts {}
|
|
@@ -57,20 +57,21 @@ export class GridListTester {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// TODO: RTL
|
|
60
|
-
private async keyboardNavigateToRow(opts: {row: HTMLElement}) {
|
|
61
|
-
let {row} = opts;
|
|
60
|
+
private async keyboardNavigateToRow(opts: {row: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
|
|
61
|
+
let {row, selectionOnNav = 'default'} = opts;
|
|
62
|
+
let altKey = getAltKey();
|
|
62
63
|
let rows = this.rows;
|
|
63
64
|
let targetIndex = rows.indexOf(row);
|
|
64
65
|
if (targetIndex === -1) {
|
|
65
66
|
throw new Error('Option provided is not in the gridlist');
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
if (document.activeElement !== this._gridlist
|
|
69
|
+
if (document.activeElement !== this._gridlist && !this._gridlist.contains(document.activeElement)) {
|
|
69
70
|
act(() => this._gridlist.focus());
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
if (document.activeElement === this._gridlist) {
|
|
73
|
-
await this.user.keyboard('[ArrowDown]');
|
|
74
|
+
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
|
|
74
75
|
} else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
|
|
75
76
|
do {
|
|
76
77
|
await this.user.keyboard('[ArrowLeft]');
|
|
@@ -82,22 +83,33 @@ export class GridListTester {
|
|
|
82
83
|
}
|
|
83
84
|
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
84
85
|
|
|
86
|
+
if (selectionOnNav === 'none') {
|
|
87
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
88
|
+
}
|
|
85
89
|
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
|
|
86
90
|
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
|
|
87
91
|
}
|
|
92
|
+
if (selectionOnNav === 'none') {
|
|
93
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
94
|
+
}
|
|
88
95
|
};
|
|
89
96
|
|
|
90
97
|
/**
|
|
91
98
|
* Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester.
|
|
99
|
+
* Note that this will endevor to always add/remove JUST the provided row to the set of selected rows.
|
|
92
100
|
*/
|
|
93
101
|
async toggleRowSelection(opts: GridListToggleRowOpts): Promise<void> {
|
|
94
102
|
let {
|
|
95
103
|
row,
|
|
96
104
|
needsLongPress,
|
|
97
105
|
checkboxSelection = true,
|
|
98
|
-
interactionType = this._interactionType
|
|
106
|
+
interactionType = this._interactionType,
|
|
107
|
+
selectionBehavior = 'toggle'
|
|
99
108
|
} = opts;
|
|
100
109
|
|
|
110
|
+
let altKey = getAltKey();
|
|
111
|
+
let metaKey = getMetaKey();
|
|
112
|
+
|
|
101
113
|
if (typeof row === 'string' || typeof row === 'number') {
|
|
102
114
|
row = this.findRow({rowIndexOrText: row});
|
|
103
115
|
}
|
|
@@ -116,9 +128,15 @@ export class GridListTester {
|
|
|
116
128
|
|
|
117
129
|
// this would be better than the check to do nothing in events.ts
|
|
118
130
|
// also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly
|
|
119
|
-
if (interactionType === 'keyboard' && !checkboxSelection) {
|
|
120
|
-
await this.keyboardNavigateToRow({row});
|
|
121
|
-
|
|
131
|
+
if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
|
|
132
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
|
|
133
|
+
if (selectionBehavior === 'replace') {
|
|
134
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
135
|
+
}
|
|
136
|
+
await this.user.keyboard('[Space]');
|
|
137
|
+
if (selectionBehavior === 'replace') {
|
|
138
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
139
|
+
}
|
|
122
140
|
return;
|
|
123
141
|
}
|
|
124
142
|
if (rowCheckbox && checkboxSelection) {
|
|
@@ -132,9 +150,14 @@ export class GridListTester {
|
|
|
132
150
|
|
|
133
151
|
// Note that long press interactions with rows is strictly touch only for grid rows
|
|
134
152
|
await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
|
|
135
|
-
|
|
136
153
|
} else {
|
|
137
|
-
|
|
154
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
155
|
+
await this.user.keyboard(`[${metaKey}>]`);
|
|
156
|
+
}
|
|
157
|
+
await pressElement(this.user, row, interactionType);
|
|
158
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
159
|
+
await this.user.keyboard(`[/${metaKey}]`);
|
|
160
|
+
}
|
|
138
161
|
}
|
|
139
162
|
}
|
|
140
163
|
}
|
|
@@ -166,7 +189,7 @@ export class GridListTester {
|
|
|
166
189
|
return;
|
|
167
190
|
}
|
|
168
191
|
|
|
169
|
-
await this.keyboardNavigateToRow({row});
|
|
192
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
|
|
170
193
|
await this.user.keyboard('[Enter]');
|
|
171
194
|
} else {
|
|
172
195
|
await pressElement(this.user, row, interactionType);
|
package/src/listbox.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {act, within} from '@testing-library/react';
|
|
14
|
+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
|
|
14
15
|
import {ListBoxTesterOpts, UserOpts} from './types';
|
|
15
|
-
import {pressElement, triggerLongPress} from './events';
|
|
16
16
|
|
|
17
17
|
interface ListBoxToggleOptionOpts {
|
|
18
18
|
/**
|
|
@@ -31,7 +31,16 @@ interface ListBoxToggleOptionOpts {
|
|
|
31
31
|
/**
|
|
32
32
|
* Whether the option needs to be long pressed to be selected. Depends on the listbox's implementation.
|
|
33
33
|
*/
|
|
34
|
-
needsLongPress?: boolean
|
|
34
|
+
needsLongPress?: boolean,
|
|
35
|
+
/**
|
|
36
|
+
* Whether the listbox has a selectionBehavior of "toggle" or "replace" (aka highlight selection). This affects the user operations
|
|
37
|
+
* required to toggle option selection by adding modifier keys during user actions, useful when performing multi-option selection in a "selectionBehavior: 'replace'" listbox.
|
|
38
|
+
* If you would like to still simulate user actions (aka press) without these modifiers keys for a "selectionBehavior: replace" listbox, simply omit this option.
|
|
39
|
+
* See the [RAC Listbox docs](https://react-spectrum.adobe.com/react-aria/ListBox.html#selection-behavior) for more info on this behavior.
|
|
40
|
+
*
|
|
41
|
+
* @default 'toggle'
|
|
42
|
+
*/
|
|
43
|
+
selectionBehavior?: 'toggle' | 'replace'
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
interface ListBoxOptionActionOpts extends Omit<ListBoxToggleOptionOpts, 'keyboardActivation' | 'needsLongPress'> {
|
|
@@ -85,44 +94,51 @@ export class ListBoxTester {
|
|
|
85
94
|
|
|
86
95
|
// TODO: this is basically the same as menu except for the error message, refactor later so that they share
|
|
87
96
|
// TODO: this also doesn't support grid layout yet
|
|
88
|
-
private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
|
|
89
|
-
let {option} = opts;
|
|
97
|
+
private async keyboardNavigateToOption(opts: {option: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
|
|
98
|
+
let {option, selectionOnNav = 'default'} = opts;
|
|
99
|
+
let altKey = getAltKey();
|
|
90
100
|
let options = this.options();
|
|
91
101
|
let targetIndex = options.indexOf(option);
|
|
92
102
|
if (targetIndex === -1) {
|
|
93
103
|
throw new Error('Option provided is not in the listbox');
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
if (document.activeElement !== this._listbox
|
|
106
|
+
if (document.activeElement !== this._listbox && !this._listbox.contains(document.activeElement)) {
|
|
97
107
|
act(() => this._listbox.focus());
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await this.user.keyboard('[ArrowDown]');
|
|
101
|
-
|
|
102
|
-
// TODO: not sure about doing same while loop that exists in other implementations of keyboardNavigateToOption,
|
|
103
|
-
// feels like it could break easily
|
|
104
|
-
if (document.activeElement?.getAttribute('role') !== 'option') {
|
|
105
|
-
await act(async () => {
|
|
106
|
-
option.focus();
|
|
107
|
-
});
|
|
108
|
+
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
let currIndex = options.indexOf(document.activeElement as HTMLElement);
|
|
111
112
|
if (currIndex === -1) {
|
|
112
113
|
throw new Error('ActiveElement is not in the listbox');
|
|
113
114
|
}
|
|
114
|
-
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
115
115
|
|
|
116
|
+
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
117
|
+
if (selectionOnNav === 'none') {
|
|
118
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
119
|
+
}
|
|
116
120
|
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
|
|
117
121
|
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
|
|
118
122
|
}
|
|
123
|
+
if (selectionOnNav === 'none') {
|
|
124
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
125
|
+
}
|
|
119
126
|
};
|
|
120
127
|
|
|
121
128
|
/**
|
|
122
129
|
* Toggles the selection for the specified listbox option. Defaults to using the interaction type set on the listbox tester.
|
|
123
130
|
*/
|
|
124
131
|
async toggleOptionSelection(opts: ListBoxToggleOptionOpts): Promise<void> {
|
|
125
|
-
let {
|
|
132
|
+
let {
|
|
133
|
+
option,
|
|
134
|
+
needsLongPress,
|
|
135
|
+
keyboardActivation = 'Enter',
|
|
136
|
+
interactionType = this._interactionType,
|
|
137
|
+
selectionBehavior = 'toggle'
|
|
138
|
+
} = opts;
|
|
139
|
+
|
|
140
|
+
let altKey = getAltKey();
|
|
141
|
+
let metaKey = getMetaKey();
|
|
126
142
|
|
|
127
143
|
if (typeof option === 'string' || typeof option === 'number') {
|
|
128
144
|
option = this.findOption({optionIndexOrText: option});
|
|
@@ -137,8 +153,14 @@ export class ListBoxTester {
|
|
|
137
153
|
return;
|
|
138
154
|
}
|
|
139
155
|
|
|
140
|
-
await this.keyboardNavigateToOption({option});
|
|
156
|
+
await this.keyboardNavigateToOption({option, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
|
|
157
|
+
if (selectionBehavior === 'replace') {
|
|
158
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
159
|
+
}
|
|
141
160
|
await this.user.keyboard(`[${keyboardActivation}]`);
|
|
161
|
+
if (selectionBehavior === 'replace') {
|
|
162
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
163
|
+
}
|
|
142
164
|
} else {
|
|
143
165
|
if (needsLongPress && interactionType === 'touch') {
|
|
144
166
|
if (this._advanceTimer == null) {
|
|
@@ -147,7 +169,13 @@ export class ListBoxTester {
|
|
|
147
169
|
|
|
148
170
|
await triggerLongPress({element: option, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
|
|
149
171
|
} else {
|
|
172
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
173
|
+
await this.user.keyboard(`[${metaKey}>]`);
|
|
174
|
+
}
|
|
150
175
|
await pressElement(this.user, option, interactionType);
|
|
176
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
177
|
+
await this.user.keyboard(`[/${metaKey}]`);
|
|
178
|
+
}
|
|
151
179
|
}
|
|
152
180
|
}
|
|
153
181
|
}
|
|
@@ -177,7 +205,7 @@ export class ListBoxTester {
|
|
|
177
205
|
return;
|
|
178
206
|
}
|
|
179
207
|
|
|
180
|
-
await this.keyboardNavigateToOption({option});
|
|
208
|
+
await this.keyboardNavigateToOption({option, selectionOnNav: 'none'});
|
|
181
209
|
await this.user.keyboard('[Enter]');
|
|
182
210
|
} else {
|
|
183
211
|
await pressElement(this.user, option, interactionType);
|
package/src/menu.ts
CHANGED
|
@@ -215,7 +215,7 @@ export class MenuTester {
|
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
if (document.activeElement !== menu
|
|
218
|
+
if (document.activeElement !== menu && !menu.contains(document.activeElement)) {
|
|
219
219
|
act(() => menu.focus());
|
|
220
220
|
}
|
|
221
221
|
|
package/src/select.ts
CHANGED
|
@@ -37,11 +37,17 @@ export class SelectTester {
|
|
|
37
37
|
this.user = user;
|
|
38
38
|
this._interactionType = interactionType || 'mouse';
|
|
39
39
|
// Handle case where the wrapper element is provided rather than the Select's button (aka RAC)
|
|
40
|
-
let
|
|
41
|
-
|
|
40
|
+
let buttons = within(root).queryAllByRole('button');
|
|
41
|
+
let triggerButton;
|
|
42
|
+
if (buttons.length === 0) {
|
|
42
43
|
triggerButton = root;
|
|
44
|
+
} else if (buttons.length === 1) {
|
|
45
|
+
triggerButton = buttons[0];
|
|
46
|
+
} else {
|
|
47
|
+
triggerButton = buttons.find(button => button.hasAttribute('aria-haspopup'));
|
|
43
48
|
}
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
this._trigger = triggerButton ?? root;
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
53
|
* Set the interaction type used by the select tester.
|
|
@@ -183,7 +189,7 @@ export class SelectTester {
|
|
|
183
189
|
return;
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
if (document.activeElement !== listbox
|
|
192
|
+
if (document.activeElement !== listbox && !listbox.contains(document.activeElement)) {
|
|
187
193
|
act(() => listbox.focus());
|
|
188
194
|
}
|
|
189
195
|
await this.keyboardNavigateToOption({option});
|
package/src/table.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {act, waitFor, within} from '@testing-library/react';
|
|
14
|
+
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
|
|
14
15
|
import {GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types';
|
|
15
|
-
import {pressElement, triggerLongPress} from './events';
|
|
16
16
|
|
|
17
17
|
interface TableToggleRowOpts extends ToggleGridRowOpts {}
|
|
18
18
|
interface TableToggleSortOpts {
|
|
@@ -25,6 +25,12 @@ interface TableToggleSortOpts {
|
|
|
25
25
|
*/
|
|
26
26
|
interactionType?: UserOpts['interactionType']
|
|
27
27
|
}
|
|
28
|
+
interface TableColumnHeaderActionOpts extends TableToggleSortOpts {
|
|
29
|
+
/**
|
|
30
|
+
* The index of the column header action to trigger.
|
|
31
|
+
*/
|
|
32
|
+
action: number
|
|
33
|
+
}
|
|
28
34
|
interface TableRowActionOpts extends GridRowActionOpts {}
|
|
29
35
|
|
|
30
36
|
export class TableTester {
|
|
@@ -48,6 +54,55 @@ export class TableTester {
|
|
|
48
54
|
this._interactionType = type;
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
// TODO: RTL
|
|
58
|
+
private async keyboardNavigateToRow(opts: {row: HTMLElement, selectionOnNav?: 'default' | 'none'}) {
|
|
59
|
+
let {row, selectionOnNav = 'default'} = opts;
|
|
60
|
+
let altKey = getAltKey();
|
|
61
|
+
let rows = this.rows;
|
|
62
|
+
let targetIndex = rows.indexOf(row);
|
|
63
|
+
if (targetIndex === -1) {
|
|
64
|
+
throw new Error('Row provided is not in the table');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Move focus into the table
|
|
68
|
+
if (document.activeElement !== this._table && !this._table.contains(document.activeElement)) {
|
|
69
|
+
act(() => this._table.focus());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (document.activeElement === this._table) {
|
|
73
|
+
await this.user.keyboard('[ArrowDown]');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If focus is currently somewhere in the first row group (aka on a column), we want to keyboard navigate downwards till we reach the rows
|
|
77
|
+
if (this.rowGroups[0].contains(document.activeElement)) {
|
|
78
|
+
do {
|
|
79
|
+
await this.user.keyboard('[ArrowDown]');
|
|
80
|
+
} while (!this.rowGroups[1].contains(document.activeElement));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Move focus onto the row itself
|
|
84
|
+
if (this.rowGroups[1].contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
|
|
85
|
+
do {
|
|
86
|
+
await this.user.keyboard('[ArrowLeft]');
|
|
87
|
+
} while (document.activeElement!.getAttribute('role') !== 'row');
|
|
88
|
+
}
|
|
89
|
+
let currIndex = rows.indexOf(document.activeElement as HTMLElement);
|
|
90
|
+
if (currIndex === -1) {
|
|
91
|
+
throw new Error('Current active element is not on any of the table rows');
|
|
92
|
+
}
|
|
93
|
+
let direction = targetIndex > currIndex ? 'down' : 'up';
|
|
94
|
+
|
|
95
|
+
if (selectionOnNav === 'none') {
|
|
96
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
97
|
+
}
|
|
98
|
+
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
|
|
99
|
+
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
|
|
100
|
+
}
|
|
101
|
+
if (selectionOnNav === 'none') {
|
|
102
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
51
106
|
/**
|
|
52
107
|
* Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester.
|
|
53
108
|
*/
|
|
@@ -56,9 +111,13 @@ export class TableTester {
|
|
|
56
111
|
row,
|
|
57
112
|
needsLongPress,
|
|
58
113
|
checkboxSelection = true,
|
|
59
|
-
interactionType = this._interactionType
|
|
114
|
+
interactionType = this._interactionType,
|
|
115
|
+
selectionBehavior = 'toggle'
|
|
60
116
|
} = opts;
|
|
61
117
|
|
|
118
|
+
let altKey = getMetaKey();
|
|
119
|
+
let metaKey = getMetaKey();
|
|
120
|
+
|
|
62
121
|
if (typeof row === 'string' || typeof row === 'number') {
|
|
63
122
|
row = this.findRow({rowIndexOrText: row});
|
|
64
123
|
}
|
|
@@ -69,12 +128,15 @@ export class TableTester {
|
|
|
69
128
|
|
|
70
129
|
let rowCheckbox = within(row).queryByRole('checkbox');
|
|
71
130
|
|
|
72
|
-
if (interactionType === 'keyboard' && !checkboxSelection) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
await this.user.keyboard('
|
|
131
|
+
if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
|
|
132
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
|
|
133
|
+
if (selectionBehavior === 'replace') {
|
|
134
|
+
await this.user.keyboard(`[${altKey}>]`);
|
|
135
|
+
}
|
|
136
|
+
await this.user.keyboard('[Space]');
|
|
137
|
+
if (selectionBehavior === 'replace') {
|
|
138
|
+
await this.user.keyboard(`[/${altKey}]`);
|
|
139
|
+
}
|
|
78
140
|
return;
|
|
79
141
|
}
|
|
80
142
|
if (rowCheckbox && checkboxSelection) {
|
|
@@ -89,7 +151,13 @@ export class TableTester {
|
|
|
89
151
|
// Note that long press interactions with rows is strictly touch only for grid rows
|
|
90
152
|
await triggerLongPress({element: cell, advanceTimer: this._advanceTimer, pointerOpts: {pointerType: 'touch'}});
|
|
91
153
|
} else {
|
|
154
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
155
|
+
await this.user.keyboard(`[${metaKey}>]`);
|
|
156
|
+
}
|
|
92
157
|
await pressElement(this.user, cell, interactionType);
|
|
158
|
+
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
|
|
159
|
+
await this.user.keyboard(`[/${metaKey}]`);
|
|
160
|
+
}
|
|
93
161
|
}
|
|
94
162
|
}
|
|
95
163
|
};
|
|
@@ -184,6 +252,92 @@ export class TableTester {
|
|
|
184
252
|
}
|
|
185
253
|
}
|
|
186
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Triggers an action for the specified table column menu. Defaults to using the interaction type set on the table tester.
|
|
257
|
+
*/
|
|
258
|
+
async triggerColumnHeaderAction(opts: TableColumnHeaderActionOpts): Promise<void> {
|
|
259
|
+
let {
|
|
260
|
+
column,
|
|
261
|
+
interactionType = this._interactionType,
|
|
262
|
+
action
|
|
263
|
+
} = opts;
|
|
264
|
+
|
|
265
|
+
let columnheader;
|
|
266
|
+
if (typeof column === 'number') {
|
|
267
|
+
columnheader = this.columns[column];
|
|
268
|
+
} else if (typeof column === 'string') {
|
|
269
|
+
columnheader = within(this.rowGroups[0]).getByText(column);
|
|
270
|
+
while (columnheader && !/columnheader/.test(columnheader.getAttribute('role'))) {
|
|
271
|
+
columnheader = columnheader.parentElement;
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
columnheader = column;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let menuButton = within(columnheader).queryByRole('button');
|
|
278
|
+
if (menuButton) {
|
|
279
|
+
// TODO: Focus management is all kinda of messed up if I just use .focus and Space to open the sort menu. Seems like
|
|
280
|
+
// the focused key doesn't get properly set to the desired column header. Have to do this strange flow where I focus the
|
|
281
|
+
// column header except if the active element is already the menu button within the column header
|
|
282
|
+
if (interactionType === 'keyboard' && document.activeElement !== menuButton) {
|
|
283
|
+
await pressElement(this.user, columnheader, interactionType);
|
|
284
|
+
} else {
|
|
285
|
+
await pressElement(this.user, menuButton, interactionType);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
if (menuButton.getAttribute('aria-controls') == null) {
|
|
290
|
+
throw new Error('No aria-controls found on table column dropdown menu trigger element.');
|
|
291
|
+
} else {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
let menuId = menuButton.getAttribute('aria-controls');
|
|
297
|
+
await waitFor(() => {
|
|
298
|
+
if (!menuId || document.getElementById(menuId) == null) {
|
|
299
|
+
throw new Error(`Table column header menu with id of ${menuId} not found in document.`);
|
|
300
|
+
} else {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (menuId) {
|
|
306
|
+
let menu = document.getElementById(menuId);
|
|
307
|
+
if (menu) {
|
|
308
|
+
await pressElement(this.user, within(menu).getAllByRole('menuitem')[action], interactionType);
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
if (document.contains(menu)) {
|
|
312
|
+
throw new Error('Expected table column menu listbox to not be in the document after selecting an option');
|
|
313
|
+
} else {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Handle cases where the table may transition in response to the row selection/deselection
|
|
321
|
+
if (!this._advanceTimer) {
|
|
322
|
+
throw new Error('No advanceTimers provided for table transition.');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await act(async () => {
|
|
326
|
+
await this._advanceTimer?.(200);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await waitFor(() => {
|
|
330
|
+
if (document.activeElement !== menuButton) {
|
|
331
|
+
throw new Error(`Expected the document.activeElement to be the table column menu button but got ${document.activeElement}`);
|
|
332
|
+
} else {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
throw new Error('No menu button found on table column header.');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
187
341
|
/**
|
|
188
342
|
* Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester.
|
|
189
343
|
*/
|
|
@@ -205,8 +359,7 @@ export class TableTester {
|
|
|
205
359
|
if (needsDoubleClick) {
|
|
206
360
|
await this.user.dblClick(row);
|
|
207
361
|
} else if (interactionType === 'keyboard') {
|
|
208
|
-
|
|
209
|
-
act(() => row.focus());
|
|
362
|
+
await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
|
|
210
363
|
await this.user.keyboard('[Enter]');
|
|
211
364
|
} else {
|
|
212
365
|
await pressElement(this.user, row, interactionType);
|
package/src/tabs.ts
CHANGED
|
@@ -137,7 +137,7 @@ export class TabsTester {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (interactionType === 'keyboard') {
|
|
140
|
-
if (document.activeElement !== this._tablist
|
|
140
|
+
if (document.activeElement !== this._tablist && !this._tablist.contains(document.activeElement)) {
|
|
141
141
|
act(() => this._tablist.focus());
|
|
142
142
|
}
|
|
143
143
|
|