@react-aria/test-utils 1.0.0-nightly.5042 → 1.0.0-rc.0

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 (124) hide show
  1. package/README.md +70 -0
  2. package/dist/import.mjs +6 -4
  3. package/dist/main.js +15 -23
  4. package/dist/main.js.map +1 -1
  5. package/dist/module.js +6 -4
  6. package/dist/module.js.map +1 -1
  7. package/dist/private/act.cjs +33 -0
  8. package/dist/private/act.cjs.map +1 -0
  9. package/dist/private/act.js +28 -0
  10. package/dist/private/act.js.map +1 -0
  11. package/dist/private/checkboxgroup.cjs +107 -0
  12. package/dist/private/checkboxgroup.cjs.map +1 -0
  13. package/dist/private/checkboxgroup.js +102 -0
  14. package/dist/private/checkboxgroup.js.map +1 -0
  15. package/dist/private/combobox.cjs +199 -0
  16. package/dist/private/combobox.cjs.map +1 -0
  17. package/dist/private/combobox.js +194 -0
  18. package/dist/private/combobox.js.map +1 -0
  19. package/dist/private/dialog.cjs +110 -0
  20. package/dist/private/dialog.cjs.map +1 -0
  21. package/dist/private/dialog.js +105 -0
  22. package/dist/private/dialog.js.map +1 -0
  23. package/dist/private/gridlist.cjs +173 -0
  24. package/dist/private/gridlist.cjs.map +1 -0
  25. package/dist/private/gridlist.js +168 -0
  26. package/dist/private/gridlist.js.map +1 -0
  27. package/dist/private/listbox.cjs +163 -0
  28. package/dist/private/listbox.cjs.map +1 -0
  29. package/dist/private/listbox.js +158 -0
  30. package/dist/private/listbox.js.map +1 -0
  31. package/dist/private/menu.cjs +265 -0
  32. package/dist/private/menu.cjs.map +1 -0
  33. package/dist/private/menu.js +260 -0
  34. package/dist/private/menu.js.map +1 -0
  35. package/dist/private/radiogroup.cjs +122 -0
  36. package/dist/private/radiogroup.cjs.map +1 -0
  37. package/dist/private/radiogroup.js +117 -0
  38. package/dist/private/radiogroup.js.map +1 -0
  39. package/dist/private/select.cjs +169 -0
  40. package/dist/private/select.cjs.map +1 -0
  41. package/dist/private/select.js +164 -0
  42. package/dist/private/select.js.map +1 -0
  43. package/dist/private/table.cjs +346 -0
  44. package/dist/private/table.cjs.map +1 -0
  45. package/dist/private/table.js +341 -0
  46. package/dist/private/table.js.map +1 -0
  47. package/dist/private/tabs.cjs +131 -0
  48. package/dist/private/tabs.cjs.map +1 -0
  49. package/dist/private/tabs.js +126 -0
  50. package/dist/private/tabs.js.map +1 -0
  51. package/dist/private/testSetup.cjs +87 -0
  52. package/dist/private/testSetup.cjs.map +1 -0
  53. package/dist/private/testSetup.js +81 -0
  54. package/dist/private/testSetup.js.map +1 -0
  55. package/dist/private/tree.cjs +181 -0
  56. package/dist/private/tree.cjs.map +1 -0
  57. package/dist/private/tree.js +176 -0
  58. package/dist/private/tree.js.map +1 -0
  59. package/dist/private/user.cjs +85 -0
  60. package/dist/private/user.cjs.map +1 -0
  61. package/dist/private/user.js +76 -0
  62. package/dist/private/user.js.map +1 -0
  63. package/dist/{userEventMaps.main.js → private/userEventMaps.cjs} +3 -3
  64. package/dist/private/userEventMaps.cjs.map +1 -0
  65. package/dist/{userEventMaps.mjs → private/userEventMaps.js} +3 -3
  66. package/dist/private/userEventMaps.js.map +1 -0
  67. package/dist/private/utils.cjs +136 -0
  68. package/dist/private/utils.cjs.map +1 -0
  69. package/dist/private/utils.js +127 -0
  70. package/dist/private/utils.js.map +1 -0
  71. package/dist/types/src/act.d.ts +4 -0
  72. package/dist/types/src/checkboxgroup.d.ts +47 -0
  73. package/dist/types/src/combobox.d.ts +87 -0
  74. package/dist/types/src/dialog.d.ts +37 -0
  75. package/dist/types/src/events.d.ts +25 -0
  76. package/dist/types/src/gridlist.d.ts +56 -0
  77. package/dist/types/src/index.d.ts +16 -0
  78. package/dist/types/src/listbox.d.ts +91 -0
  79. package/dist/types/src/menu.d.ts +112 -0
  80. package/dist/types/src/radiogroup.d.ts +47 -0
  81. package/dist/types/src/select.d.ts +74 -0
  82. package/dist/types/src/table.d.ts +120 -0
  83. package/dist/types/src/tabs.d.ts +59 -0
  84. package/dist/types/src/testSetup.d.ts +6 -0
  85. package/dist/types/src/tree.d.ts +62 -0
  86. package/dist/types/src/types.d.ts +143 -0
  87. package/dist/types/src/user.d.ts +49 -0
  88. package/dist/types/src/userEventMaps.d.ts +2 -0
  89. package/dist/types/src/utils.d.ts +29 -0
  90. package/package.json +26 -18
  91. package/src/act.ts +35 -0
  92. package/src/checkboxgroup.ts +165 -0
  93. package/src/combobox.ts +307 -0
  94. package/src/dialog.ts +155 -0
  95. package/src/gridlist.ts +278 -0
  96. package/src/index.ts +17 -3
  97. package/src/listbox.ts +300 -0
  98. package/src/menu.ts +479 -0
  99. package/src/radiogroup.ts +179 -0
  100. package/src/select.ts +273 -0
  101. package/src/table.ts +589 -0
  102. package/src/tabs.ts +204 -0
  103. package/src/testSetup.ts +41 -36
  104. package/src/tree.ts +290 -0
  105. package/src/types.ts +171 -0
  106. package/src/user.ts +153 -0
  107. package/src/userEventMaps.ts +1 -1
  108. package/src/utils.ts +155 -0
  109. package/dist/events.main.js +0 -37
  110. package/dist/events.main.js.map +0 -1
  111. package/dist/events.mjs +0 -31
  112. package/dist/events.module.js +0 -31
  113. package/dist/events.module.js.map +0 -1
  114. package/dist/testSetup.main.js +0 -82
  115. package/dist/testSetup.main.js.map +0 -1
  116. package/dist/testSetup.mjs +0 -76
  117. package/dist/testSetup.module.js +0 -76
  118. package/dist/testSetup.module.js.map +0 -1
  119. package/dist/types.d.ts +0 -16
  120. package/dist/types.d.ts.map +0 -1
  121. package/dist/userEventMaps.main.js.map +0 -1
  122. package/dist/userEventMaps.module.js +0 -38
  123. package/dist/userEventMaps.module.js.map +0 -1
  124. package/src/events.ts +0 -28
package/src/tabs.ts ADDED
@@ -0,0 +1,204 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {act} from './act';
14
+ import {Direction, Orientation, TabsTesterOpts, UserOpts} from './types';
15
+ import {formatTargetNode, pressElement} from './utils';
16
+ import {within} from '@testing-library/dom';
17
+
18
+ interface TriggerTabOptions {
19
+ /**
20
+ * What interaction type to use when triggering a tab. Defaults to the interaction type set on the
21
+ * tester.
22
+ */
23
+ interactionType?: UserOpts['interactionType'];
24
+ /**
25
+ * The index, text, or node of the tab to toggle selection for.
26
+ */
27
+ tab: number | string | HTMLElement;
28
+ /**
29
+ * Whether the tab needs to be activated manually rather than on focus.
30
+ */
31
+ manualActivation?: boolean;
32
+ }
33
+
34
+ export class TabsTester {
35
+ private user;
36
+ private _interactionType: UserOpts['interactionType'];
37
+ private _tablist: HTMLElement;
38
+ private _direction: Direction;
39
+
40
+ constructor(opts: TabsTesterOpts) {
41
+ let {root, user, interactionType, direction} = opts;
42
+ this.user = user;
43
+ this._interactionType = interactionType || 'mouse';
44
+ this._direction = direction || 'ltr';
45
+
46
+ this._tablist = root;
47
+ let tablist = within(root).queryAllByRole('tablist');
48
+ if (tablist.length > 0) {
49
+ this._tablist = tablist[0];
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Set the interaction type used by the tabs tester.
55
+ */
56
+ setInteractionType(type: UserOpts['interactionType']): void {
57
+ this._interactionType = type;
58
+ }
59
+
60
+ /**
61
+ * Returns a tab matching the specified index or text content.
62
+ */
63
+ findTab(opts: {indexOrText: number | string}): HTMLElement {
64
+ let {indexOrText} = opts;
65
+
66
+ let tab;
67
+ let tabs = this.getTabs();
68
+ if (typeof indexOrText === 'number') {
69
+ tab = tabs[indexOrText];
70
+ } else if (typeof indexOrText === 'string') {
71
+ tab = within(this._tablist).getByText(indexOrText).closest('[role=tab]')! as HTMLElement;
72
+ }
73
+
74
+ return tab;
75
+ }
76
+
77
+ private async keyboardNavigateToTab(opts: {tab: HTMLElement; orientation?: Orientation}) {
78
+ let {tab, orientation = 'vertical'} = opts;
79
+ let tabs = this.getTabs();
80
+ tabs = tabs.filter(
81
+ tab => !(tab.hasAttribute('disabled') || tab.getAttribute('aria-disabled') === 'true')
82
+ );
83
+ if (tabs.length === 0) {
84
+ throw new Error(
85
+ 'Tablist doesnt have any non-disabled tabs. Please double check your tabs implementation.'
86
+ );
87
+ }
88
+
89
+ let targetIndex = tabs.indexOf(tab);
90
+ if (targetIndex === -1) {
91
+ throw new Error('Tab provided is not in the tablist');
92
+ }
93
+
94
+ if (!this._tablist.contains(document.activeElement)) {
95
+ let selectedTab = this.getSelectedTab();
96
+ if (selectedTab != null) {
97
+ act(() => selectedTab.focus());
98
+ } else {
99
+ act(() => tabs[0]?.focus());
100
+ }
101
+ }
102
+
103
+ let currIndex = tabs.indexOf(document.activeElement as HTMLElement);
104
+ if (currIndex === -1) {
105
+ throw new Error('ActiveElement is not in the tablist');
106
+ }
107
+
108
+ let arrowUp = 'ArrowUp';
109
+ let arrowDown = 'ArrowDown';
110
+ if (orientation === 'horizontal') {
111
+ if (this._direction === 'ltr') {
112
+ arrowUp = 'ArrowLeft';
113
+ arrowDown = 'ArrowRight';
114
+ } else {
115
+ arrowUp = 'ArrowRight';
116
+ arrowDown = 'ArrowLeft';
117
+ }
118
+ }
119
+
120
+ let movementDirection = targetIndex > currIndex ? 'down' : 'up';
121
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
122
+ await this.user.keyboard(`[${movementDirection === 'down' ? arrowDown : arrowUp}]`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Triggers the specified tab. Defaults to using the interaction type set on the tabs tester.
128
+ */
129
+ async triggerTab(opts: TriggerTabOptions): Promise<void> {
130
+ let {tab, interactionType = this._interactionType, manualActivation} = opts;
131
+
132
+ if (typeof tab === 'string' || typeof tab === 'number') {
133
+ tab = this.findTab({indexOrText: tab});
134
+ }
135
+
136
+ if (!tab) {
137
+ throw new Error(`Target tab "${formatTargetNode(opts.tab)}" not found in the tablist.`);
138
+ } else if (tab.hasAttribute('disabled')) {
139
+ throw new Error(`Target tab "${formatTargetNode(opts.tab)}" is disabled.`);
140
+ }
141
+
142
+ if (interactionType === 'keyboard') {
143
+ if (
144
+ document.activeElement !== this._tablist &&
145
+ !this._tablist.contains(document.activeElement)
146
+ ) {
147
+ act(() => this._tablist.focus());
148
+ }
149
+
150
+ let tabsOrientation = this._tablist.getAttribute('aria-orientation') || 'horizontal';
151
+ await this.keyboardNavigateToTab({tab, orientation: tabsOrientation as Orientation});
152
+ if (manualActivation) {
153
+ await this.user.keyboard('[Enter]');
154
+ }
155
+ } else {
156
+ await pressElement(this.user, tab, interactionType);
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Returns the tablist.
162
+ */
163
+ getTablist(): HTMLElement {
164
+ return this._tablist;
165
+ }
166
+
167
+ /**
168
+ * Returns the tabpanels.
169
+ */
170
+ getTabpanels(): HTMLElement[] {
171
+ let tabpanels = [] as HTMLElement[];
172
+ for (let tab of this.getTabs()) {
173
+ let controlId = tab.getAttribute('aria-controls');
174
+ let panel = controlId != null ? document.getElementById(controlId) : null;
175
+ if (panel != null) {
176
+ tabpanels.push(panel);
177
+ }
178
+ }
179
+
180
+ return tabpanels;
181
+ }
182
+
183
+ /**
184
+ * Returns the tabs in the tablist.
185
+ */
186
+ getTabs(): HTMLElement[] {
187
+ return within(this.getTablist()).queryAllByRole('tab');
188
+ }
189
+
190
+ /**
191
+ * Returns the currently selected tab in the tablist if any.
192
+ */
193
+ getSelectedTab(): HTMLElement | null {
194
+ return this.getTabs().find(tab => tab.getAttribute('aria-selected') === 'true') || null;
195
+ }
196
+
197
+ /**
198
+ * Returns the currently active tabpanel if any.
199
+ */
200
+ getActiveTabpanel(): HTMLElement | null {
201
+ let activeTabpanelId = this.getSelectedTab()?.getAttribute('aria-controls');
202
+ return activeTabpanelId ? document.getElementById(activeTabpanelId) : null;
203
+ }
204
+ }
package/src/testSetup.ts CHANGED
@@ -13,12 +13,11 @@
13
13
  /**
14
14
  * Enables reading pageX/pageY from fireEvent.mouse*(..., {pageX: ..., pageY: ...}).
15
15
  */
16
- export function installMouseEvent() {
16
+ export function installMouseEvent(): void {
17
+ let oldMouseEvent = MouseEvent;
17
18
  beforeAll(() => {
18
- let oldMouseEvent = MouseEvent;
19
- // @ts-ignore
20
19
  global.MouseEvent = class FakeMouseEvent extends MouseEvent {
21
- _init: {pageX: number, pageY: number};
20
+ _init: {pageX: number; pageY: number};
22
21
  constructor(name, init) {
23
22
  super(name, init);
24
23
  this._init = init;
@@ -30,44 +29,50 @@ 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
 
42
- export function installPointerEvent() {
43
- beforeAll(() => {
44
- // @ts-ignore
45
- global.PointerEvent = class FakePointerEvent extends MouseEvent {
46
- _init: {pageX: number, pageY: number, pointerType: string, pointerId: number, width: number, height: number};
47
- constructor(name, init) {
48
- super(name, init);
49
- this._init = init;
50
- }
51
- get pointerType() {
52
- return this._init.pointerType ?? 'mouse';
53
- }
54
- get pointerId() {
55
- return this._init.pointerId;
56
- }
57
- get pageX() {
58
- return this._init.pageX;
59
- }
60
- get pageY() {
61
- return this._init.pageY;
62
- }
63
- get width() {
64
- return this._init.width;
65
- }
66
- get height() {
67
- return this._init.height;
68
- }
38
+ export function definePointerEvent(): void {
39
+ // @ts-ignore
40
+ global.PointerEvent = class FakePointerEvent extends MouseEvent {
41
+ _init: {
42
+ pageX: number;
43
+ pageY: number;
44
+ pointerType: string;
45
+ pointerId: number;
46
+ width: number;
47
+ height: number;
69
48
  };
70
- });
49
+ constructor(name, init) {
50
+ super(name, init);
51
+ this._init = init;
52
+ }
53
+ get pointerType() {
54
+ return this._init.pointerType ?? 'mouse';
55
+ }
56
+ get pointerId() {
57
+ return this._init.pointerId;
58
+ }
59
+ get pageX() {
60
+ return this._init.pageX;
61
+ }
62
+ get pageY() {
63
+ return this._init.pageY;
64
+ }
65
+ get width() {
66
+ return this._init.width;
67
+ }
68
+ get height() {
69
+ return this._init.height;
70
+ }
71
+ };
72
+ }
73
+
74
+ export function installPointerEvent(): void {
75
+ beforeAll(definePointerEvent);
71
76
  afterAll(() => {
72
77
  // @ts-ignore
73
78
  delete global.PointerEvent;
package/src/tree.ts ADDED
@@ -0,0 +1,290 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {act} from './act';
14
+ import {
15
+ BaseGridRowInteractionOpts,
16
+ Direction,
17
+ GridRowActionOpts,
18
+ ToggleGridRowOpts,
19
+ TreeTesterOpts,
20
+ UserOpts
21
+ } from './types';
22
+ import {formatTargetNode, getAltKey, getMetaKey, pressElement, triggerLongPress} from './utils';
23
+ import {within} from '@testing-library/dom';
24
+
25
+ interface TreeToggleExpansionOpts extends BaseGridRowInteractionOpts {}
26
+ interface TreeToggleRowOpts extends ToggleGridRowOpts {}
27
+ interface TreeRowActionOpts extends GridRowActionOpts {}
28
+
29
+ export class TreeTester {
30
+ private user;
31
+ private _interactionType: UserOpts['interactionType'];
32
+ private _advanceTimer: UserOpts['advanceTimer'];
33
+ private _direction: Direction;
34
+ private _tree: HTMLElement;
35
+
36
+ constructor(opts: TreeTesterOpts) {
37
+ let {root, user, interactionType, advanceTimer, direction} = opts;
38
+ this.user = user;
39
+ this._interactionType = interactionType || 'mouse';
40
+ this._advanceTimer = advanceTimer;
41
+ this._direction = direction || 'ltr';
42
+ this._tree = root;
43
+ if (root.getAttribute('role') !== 'treegrid') {
44
+ let tree = within(root).queryByRole('treegrid');
45
+ if (tree) {
46
+ this._tree = tree;
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Set the interaction type used by the tree tester.
53
+ */
54
+ setInteractionType(type: UserOpts['interactionType']): void {
55
+ this._interactionType = type;
56
+ }
57
+
58
+ /**
59
+ * Returns a row matching the specified index or text content.
60
+ */
61
+ findRow(opts: {indexOrText: number | string}): HTMLElement {
62
+ let {indexOrText} = opts;
63
+
64
+ let row;
65
+ if (typeof indexOrText === 'number') {
66
+ row = this.getRows()[indexOrText];
67
+ } else if (typeof indexOrText === 'string') {
68
+ row = within(this.getTree()!).getByText(indexOrText).closest('[role=row]')! as HTMLElement;
69
+ }
70
+
71
+ return row;
72
+ }
73
+
74
+ private async keyboardNavigateToRow(opts: {
75
+ row: HTMLElement;
76
+ selectionOnNav?: 'default' | 'none';
77
+ }) {
78
+ let {row, selectionOnNav = 'default'} = opts;
79
+ let altKey = getAltKey();
80
+ let rows = this.getRows();
81
+ let targetIndex = rows.indexOf(row);
82
+ if (targetIndex === -1) {
83
+ throw new Error('Row provided is not in the tree');
84
+ }
85
+
86
+ if (document.activeElement !== this._tree && !this._tree.contains(document.activeElement)) {
87
+ act(() => this._tree.focus());
88
+ }
89
+
90
+ let focusPrevKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
91
+ if (document.activeElement === this.getTree()) {
92
+ await this.user.keyboard(
93
+ `${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`
94
+ );
95
+ } else if (
96
+ this._tree.contains(document.activeElement) &&
97
+ document.activeElement!.getAttribute('role') !== 'row'
98
+ ) {
99
+ do {
100
+ await this.user.keyboard(`[${focusPrevKey}]`);
101
+ } while (document.activeElement!.getAttribute('role') !== 'row');
102
+ }
103
+ let currIndex = rows.indexOf(document.activeElement as HTMLElement);
104
+ if (currIndex === -1) {
105
+ throw new Error('ActiveElement is not in the tree');
106
+ }
107
+ let direction = targetIndex > currIndex ? 'down' : 'up';
108
+
109
+ if (selectionOnNav === 'none') {
110
+ await this.user.keyboard(`[${altKey}>]`);
111
+ }
112
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
113
+ await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
114
+ }
115
+ if (selectionOnNav === 'none') {
116
+ await this.user.keyboard(`[/${altKey}]`);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Toggles the selection for the specified tree row. Defaults to using the interaction type set on
122
+ * the tree tester. Note that this will endevor to always add/remove JUST the provided row to the
123
+ * set of selected rows.
124
+ */
125
+ async toggleRowSelection(opts: TreeToggleRowOpts): Promise<void> {
126
+ let {
127
+ row,
128
+ needsLongPress,
129
+ checkboxSelection = true,
130
+ interactionType = this._interactionType,
131
+ selectionBehavior = 'toggle'
132
+ } = opts;
133
+
134
+ let altKey = getAltKey();
135
+ let metaKey = getMetaKey();
136
+
137
+ if (typeof row === 'string' || typeof row === 'number') {
138
+ row = this.findRow({indexOrText: row});
139
+ }
140
+
141
+ if (!row) {
142
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the tree.`);
143
+ }
144
+
145
+ let rowCheckbox = within(row).queryByRole('checkbox');
146
+
147
+ if (
148
+ rowCheckbox?.getAttribute('disabled') === '' ||
149
+ row?.getAttribute('aria-disabled') === 'true'
150
+ ) {
151
+ throw new Error(`Cannot toggle selection on disabled row "${formatTargetNode(opts.row)}".`);
152
+ }
153
+
154
+ // this would be better than the check to do nothing in events.ts
155
+ // also, it'd be good to be able to trigger selection on the row instead of having to go to the checkbox directly
156
+ if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
157
+ await this.keyboardNavigateToRow({
158
+ row,
159
+ selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'
160
+ });
161
+ if (selectionBehavior === 'replace') {
162
+ await this.user.keyboard(`[${altKey}>]`);
163
+ }
164
+ await this.user.keyboard('[Space]');
165
+ if (selectionBehavior === 'replace') {
166
+ await this.user.keyboard(`[/${altKey}]`);
167
+ }
168
+ return;
169
+ }
170
+ if (rowCheckbox && checkboxSelection) {
171
+ await pressElement(this.user, rowCheckbox, interactionType);
172
+ } else {
173
+ let cell = within(row).getAllByRole('gridcell')[0];
174
+ if (needsLongPress && interactionType === 'touch') {
175
+ // Note that long press interactions with rows is strictly touch only for grid rows
176
+ await triggerLongPress({
177
+ element: cell,
178
+ advanceTimer: this._advanceTimer!,
179
+ pointerOpts: {pointerType: 'touch'}
180
+ });
181
+ } else {
182
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
183
+ await this.user.keyboard(`[${metaKey}>]`);
184
+ }
185
+ await pressElement(this.user, row, interactionType);
186
+ if (selectionBehavior === 'replace' && interactionType !== 'touch') {
187
+ await this.user.keyboard(`[/${metaKey}]`);
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Toggles the expansion for the specified tree row. Defaults to using the interaction type set on
195
+ * the tree tester.
196
+ */
197
+ async toggleRowExpansion(opts: TreeToggleExpansionOpts): Promise<void> {
198
+ let {row, interactionType = this._interactionType} = opts;
199
+ if (!this.getTree().contains(document.activeElement)) {
200
+ act(() => this.getTree().focus());
201
+ }
202
+
203
+ if (typeof row === 'string' || typeof row === 'number') {
204
+ row = this.findRow({indexOrText: row});
205
+ }
206
+
207
+ if (!row) {
208
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the tree.`);
209
+ } else if (row.getAttribute('aria-expanded') == null) {
210
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" is not expandable.`);
211
+ }
212
+
213
+ if (row.getAttribute('aria-disabled') === 'true') {
214
+ throw new Error(`Cannot toggle expansion on disabled row "${formatTargetNode(opts.row)}".`);
215
+ }
216
+
217
+ if (interactionType === 'mouse' || interactionType === 'touch') {
218
+ let rowExpander = within(row).getAllByRole('button')[0]; // what happens if the button is not first? how can we differentiate?
219
+ await pressElement(this.user, rowExpander, interactionType);
220
+ } else if (interactionType === 'keyboard') {
221
+ await this.keyboardNavigateToRow({row});
222
+ let collapseKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
223
+ let expandKey = this._direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
224
+ if (row.getAttribute('aria-expanded') === 'true') {
225
+ await this.user.keyboard(`[${collapseKey}]`);
226
+ } else {
227
+ await this.user.keyboard(`[${expandKey}]`);
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Triggers the action for the specified tree row. Defaults to using the interaction type set on
234
+ * the tree tester.
235
+ */
236
+ async triggerRowAction(opts: TreeRowActionOpts): Promise<void> {
237
+ let {row, needsDoubleClick, interactionType = this._interactionType} = opts;
238
+
239
+ if (typeof row === 'string' || typeof row === 'number') {
240
+ row = this.findRow({indexOrText: row});
241
+ }
242
+
243
+ if (!row) {
244
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the tree.`);
245
+ }
246
+
247
+ if (row.getAttribute('aria-disabled') === 'true') {
248
+ throw new Error(`Cannot trigger row action on disabled row "${formatTargetNode(opts.row)}".`);
249
+ }
250
+
251
+ if (needsDoubleClick) {
252
+ await this.user.dblClick(row);
253
+ } else if (interactionType === 'keyboard') {
254
+ await this.keyboardNavigateToRow({row});
255
+ await this.user.keyboard('[Enter]');
256
+ } else {
257
+ await pressElement(this.user, row, interactionType);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Returns the tree.
263
+ */
264
+ getTree(): HTMLElement {
265
+ return this._tree;
266
+ }
267
+
268
+ /**
269
+ * Returns the tree's rows if any.
270
+ */
271
+ getRows(): HTMLElement[] {
272
+ return within(this.getTree()).queryAllByRole('row');
273
+ }
274
+
275
+ /**
276
+ * Returns the tree's selected rows if any.
277
+ */
278
+ getSelectedRows(): HTMLElement[] {
279
+ return this.getRows().filter(row => row.getAttribute('aria-selected') === 'true');
280
+ }
281
+
282
+ /**
283
+ * Returns the tree's cells if any. Can be filtered against a specific row if provided via
284
+ * `element`.
285
+ */
286
+ getCells(opts: {element?: HTMLElement} = {}): HTMLElement[] {
287
+ let {element = this.getTree()} = opts;
288
+ return within(element).queryAllByRole('gridcell');
289
+ }
290
+ }