Skip to content

Commit a654a33

Browse files
authored
fix: Support React 19 and remove Jest reliance in test utils (#7686)
* attempt to get rid of jest calls in menu util * update RSP testing docs to directly mention mocks that maybe needed * bump versions of RTL to 16 * use alternative to calling jest run timers in menu option selection * fixing types and properly testing long press * fix lint * revert to pre testing library bump for clean slate * fix build and another submenu edge case now we shouldnt need to call runAllTimers after selectOption * fix react 16 bug * update return type of advanceTimer and docs copy * move some general fixes from selectionMode="replace" branch here * get rid of unneeded async * getting rid of extraneous dep * fix lint
1 parent 5dcf9ce commit a654a33

File tree

26 files changed

+179
-78
lines changed

26 files changed

+179
-78
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
"@tailwindcss/postcss": "^4.0.0",
126126
"@testing-library/dom": "^10.1.0",
127127
"@testing-library/jest-dom": "^5.16.5",
128-
"@testing-library/react": "^15.0.7",
128+
"@testing-library/react": "^16.0.0",
129129
"@testing-library/user-event": "patch:@testing-library/user-event@npm%3A14.6.1#~/.yarn/patches/@testing-library-user-event-npm-14.6.1-5da7e1d4e2.patch",
130130
"@types/react": "npm:[email protected]",
131131
"@types/react-dom": "npm:[email protected]",

packages/@react-aria/test-utils/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
"@swc/helpers": "^0.5.0"
2626
},
2727
"peerDependencies": {
28-
"@testing-library/react": "^15.0.7",
28+
"@testing-library/react": "^16.0.0",
2929
"@testing-library/user-event": "^14.0.0",
30-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
30+
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
31+
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
3132
},
3233
"publishConfig": {
3334
"access": "public"

packages/@react-aria/test-utils/src/events.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const DEFAULT_LONG_PRESS_TIME = 500;
2222
* @param opts.advanceTimer - Function that when called advances the timers in your test suite by a specific amount of time(ms).
2323
* @param opts.pointeropts - Options to pass to the simulated event. Defaults to mouse. See https://testing-library.com/docs/dom-testing-library/api-events/#fireevent for more info.
2424
*/
25-
export async function triggerLongPress(opts: {element: HTMLElement, advanceTimer: (time?: number) => void | Promise<unknown>, pointerOpts?: Record<string, any>}): Promise<void> {
25+
export async function triggerLongPress(opts: {element: HTMLElement, advanceTimer: (time: number) => unknown | Promise<unknown>, pointerOpts?: Record<string, any>}): Promise<void> {
2626
// TODO: note that this only works if the code from installPointerEvent is called somewhere in the test BEFORE the
2727
// render. Perhaps we should rely on the user setting that up since I'm not sure there is a great way to set that up here in the
2828
// util before first render. Will need to document it well

packages/@react-aria/test-utils/src/gridlist.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export class GridListTester {
6464
if (targetIndex === -1) {
6565
throw new Error('Option provided is not in the gridlist');
6666
}
67+
68+
if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) {
69+
act(() => this._gridlist.focus());
70+
}
71+
6772
if (document.activeElement === this._gridlist) {
6873
await this.user.keyboard('[ArrowDown]');
6974
} else if (this._gridlist.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
@@ -161,10 +166,6 @@ export class GridListTester {
161166
return;
162167
}
163168

164-
if (document.activeElement !== this._gridlist || !this._gridlist.contains(document.activeElement)) {
165-
act(() => this._gridlist.focus());
166-
}
167-
168169
await this.keyboardNavigateToRow({row});
169170
await this.user.keyboard('[Enter]');
170171
} else {

packages/@react-aria/test-utils/src/listbox.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ export class ListBoxTester {
9393
throw new Error('Option provided is not in the listbox');
9494
}
9595

96-
if (document.activeElement === this._listbox) {
97-
await this.user.keyboard('[ArrowDown]');
96+
if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
97+
act(() => this._listbox.focus());
9898
}
9999

100+
await this.user.keyboard('[ArrowDown]');
101+
100102
// TODO: not sure about doing same while loop that exists in other implementations of keyboardNavigateToOption,
101103
// feels like it could break easily
102104
if (document.activeElement?.getAttribute('role') !== 'option') {
@@ -135,10 +137,6 @@ export class ListBoxTester {
135137
return;
136138
}
137139

138-
if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
139-
act(() => this._listbox.focus());
140-
}
141-
142140
await this.keyboardNavigateToOption({option});
143141
await this.user.keyboard(`[${keyboardActivation}]`);
144142
} else {
@@ -179,10 +177,6 @@ export class ListBoxTester {
179177
return;
180178
}
181179

182-
if (document.activeElement !== this._listbox || !this._listbox.contains(document.activeElement)) {
183-
act(() => this._listbox.focus());
184-
}
185-
186180
await this.keyboardNavigateToOption({option});
187181
await this.user.keyboard('[Enter]');
188182
} else {

packages/@react-aria/test-utils/src/menu.ts

+60-10
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ export class MenuTester {
6464
private _advanceTimer: UserOpts['advanceTimer'];
6565
private _trigger: HTMLElement | undefined;
6666
private _isSubmenu: boolean = false;
67+
private _rootMenu: HTMLElement | undefined;
6768

6869
constructor(opts: MenuTesterOpts) {
69-
let {root, user, interactionType, advanceTimer, isSubmenu} = opts;
70+
let {root, user, interactionType, advanceTimer, isSubmenu, rootMenu} = opts;
7071
this.user = user;
7172
this._interactionType = interactionType || 'mouse';
7273
this._advanceTimer = advanceTimer;
@@ -85,6 +86,7 @@ export class MenuTester {
8586
}
8687

8788
this._isSubmenu = isSubmenu || false;
89+
this._rootMenu = rootMenu;
8890
}
8991

9092
/**
@@ -226,20 +228,56 @@ export class MenuTester {
226228
await this.user.pointer({target: option, keys: '[TouchA]'});
227229
}
228230
}
229-
act(() => {jest.runAllTimers();});
230231

231-
if (option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) {
232+
// This chain of waitFors is needed in place of running all timers since we don't know how long transitions may take, or what action
233+
// the menu option select may trigger.
234+
if (
235+
!(menuSelectionMode === 'single' && !closesOnSelect) &&
236+
!(menuSelectionMode === 'multiple' && (keyboardActivation === 'Space' || interactionType === 'mouse'))
237+
) {
238+
// For RSP, clicking on a submenu option seems to briefly lose focus to the body before moving to the clicked option in the test so we need to wait
239+
// for focus to be coerced to somewhere else in place of running all timers.
240+
if (this._isSubmenu) {
241+
await waitFor(() => {
242+
if (document.activeElement === document.body) {
243+
throw new Error('Expected focus to move to somewhere other than the body after selecting a submenu option.');
244+
} else {
245+
return true;
246+
}
247+
});
248+
}
249+
250+
// If user isn't trying to select multiple menu options or closeOnSelect is true then we can assume that
251+
// the menu will close or some action is triggered. In cases like that focus should move somewhere after the menu closes
252+
// but we can't really know where so just make sure it doesn't get lost to the body.
232253
await waitFor(() => {
233-
if (document.activeElement !== trigger) {
234-
throw new Error(`Expected the document.activeElement after selecting an option to be the menu trigger but got ${document.activeElement}`);
254+
if (document.activeElement === option) {
255+
throw new Error('Expected focus after selecting an option to move away from the option.');
235256
} else {
236257
return true;
237258
}
238259
});
239260

240-
if (document.contains(menu)) {
241-
throw new Error('Expected menu element to not be in the document after selecting an option');
261+
// We'll also want to wait for focus to move away from the original submenu trigger since the entire submenu tree should
262+
// close. In React 16, focus actually makes it all the way to the root menu's submenu trigger so we need check the root menu
263+
if (this._isSubmenu) {
264+
await waitFor(() => {
265+
if (document.activeElement === this.trigger || this._rootMenu?.contains(document.activeElement)) {
266+
throw new Error('Expected focus after selecting an submenu option to move away from the original submenu trigger.');
267+
} else {
268+
return true;
269+
}
270+
});
242271
}
272+
273+
// Finally wait for focus to be coerced somewhere final when the menu tree is removed from the DOM
274+
await waitFor(() => {
275+
if (document.activeElement === document.body) {
276+
throw new Error('Expected focus to move to somewhere other than the body after selecting a menu option.');
277+
} else {
278+
return true;
279+
}
280+
});
243281
}
244282
} else {
245283
throw new Error("Attempted to select a option in the menu, but menu wasn't found.");
@@ -269,18 +307,30 @@ export class MenuTester {
269307
submenuTrigger = (within(menu!).getByText(submenuTrigger).closest('[role=menuitem]'))! as HTMLElement;
270308
}
271309

272-
let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenuTrigger, isSubmenu: true});
310+
let submenuTriggerTester = new MenuTester({
311+
user: this.user,
312+
interactionType: this._interactionType,
313+
root: submenuTrigger,
314+
isSubmenu: true,
315+
advanceTimer: this._advanceTimer,
316+
rootMenu: (this._isSubmenu ? this._rootMenu : this.menu) || undefined
317+
});
273318
if (interactionType === 'mouse') {
274319
await this.user.pointer({target: submenuTrigger});
275-
act(() => {jest.runAllTimers();});
276320
} else if (interactionType === 'keyboard') {
277321
await this.keyboardNavigateToOption({option: submenuTrigger});
278322
await this.user.keyboard('[ArrowRight]');
279-
act(() => {jest.runAllTimers();});
280323
} else {
281324
await submenuTriggerTester.open();
282325
}
283326

327+
await waitFor(() => {
328+
if (submenuTriggerTester._trigger?.getAttribute('aria-expanded') !== 'true') {
329+
throw new Error('aria-expanded for the submenu trigger wasn\'t changed to "true", unable to confirm the existance of the submenu');
330+
} else {
331+
return true;
332+
}
333+
});
284334

285335
return submenuTriggerTester;
286336
}

packages/@react-aria/test-utils/src/tree.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export class TreeTester {
7171
if (targetIndex === -1) {
7272
throw new Error('Option provided is not in the tree');
7373
}
74+
75+
if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) {
76+
act(() => this._tree.focus());
77+
}
78+
7479
if (document.activeElement === this.tree) {
7580
await this.user.keyboard('[ArrowDown]');
7681
} else if (this._tree.contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
@@ -206,10 +211,6 @@ export class TreeTester {
206211
return;
207212
}
208213

209-
if (document.activeElement !== this._tree || !this._tree.contains(document.activeElement)) {
210-
act(() => this._tree.focus());
211-
}
212-
213214
await this.keyboardNavigateToRow({row});
214215
await this.user.keyboard('[Enter]');
215216
} else {

packages/@react-aria/test-utils/src/types.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ export interface UserOpts {
2222
* @default mouse
2323
*/
2424
interactionType?: 'mouse' | 'touch' | 'keyboard',
25-
// If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))}
26-
// A real timer user would pass async () => await new Promise((resolve) => setTimeout(resolve, waitTime))
25+
// If using fake timers user should provide something like (time) => jest.advanceTimersByTime(time))}.
26+
// A real timer user would pass (waitTime) => new Promise((resolve) => setTimeout(resolve, waitTime))
2727
// Time is in ms.
2828
/**
2929
* A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). This can be overridden
3030
* at the aria pattern tester level if needed.
3131
*/
32-
advanceTimer?: (time?: number) => void | Promise<unknown>
32+
advanceTimer?: (time: number) => unknown | Promise<unknown>
3333
}
3434

3535
export interface BaseTesterOpts extends UserOpts {
@@ -69,7 +69,11 @@ export interface MenuTesterOpts extends BaseTesterOpts {
6969
/**
7070
* Whether the current menu is a submenu.
7171
*/
72-
isSubmenu?: boolean
72+
isSubmenu?: boolean,
73+
/**
74+
* The root menu of the menu tree. Only available if the menu is a submenu.
75+
*/
76+
rootMenu?: HTMLElement
7377
}
7478

7579
export interface SelectTesterOpts extends BaseTesterOpts {

packages/@react-aria/test-utils/src/user.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ type TesterOpts<T> =
6767
T extends 'Tree' ? TreeTesterOpts :
6868
never;
6969

70-
let defaultAdvanceTimer = async (waitTime: number | undefined) => await new Promise((resolve) => setTimeout(resolve, waitTime));
70+
let defaultAdvanceTimer = (waitTime: number | undefined) => new Promise((resolve) => setTimeout(resolve, waitTime));
7171

7272
export class User {
7373
private user;

packages/@react-spectrum/combobox/docs/ComboBox.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ import {theme} from '@react-spectrum/theme-default';
10061006
import {User} from '@react-spectrum/test-utils';
10071007

10081008
let testUtilUser = new User({interactionType: 'mouse'});
1009-
// ...
1009+
// Other setup, be sure to check out the suggested mocks mentioned above in https://react-spectrum.adobe.com/react-spectrum/ComboBox.html#testing
10101010

10111011
it('ComboBox can select an option via keyboard', async function () {
10121012
// Render your test component/app and initialize the combobox tester

packages/@react-spectrum/list/docs/ListView.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,7 @@ import {theme} from '@react-spectrum/theme-default';
12051205
import {User} from '@react-spectrum/test-utils';
12061206

12071207
let testUtilUser = new User({interactionType: 'mouse'});
1208-
// ...
1208+
// Other setup, be sure to check out the suggested mocks mentioned above in https://react-spectrum.adobe.com/react-spectrum/ListView.html#testing
12091209

12101210
it('ListView can select a row via keyboard', async function () {
12111211
// Render your test component/app and initialize the gridlist tester

packages/@react-spectrum/listbox/docs/ListBox.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ import {theme} from '@react-spectrum/theme-default';
423423
import {User} from '@react-spectrum/test-utils';
424424

425425
let testUtilUser = new User({interactionType: 'mouse'});
426-
// ...
426+
// Other setup, be sure to check out the suggested mocks mentioned above in https://react-spectrum.adobe.com/react-spectrum/ListBox.html#testing
427427

428428
it('ListBox can select an option via keyboard', async function () {
429429
// Render your test component/app and initialize the listbox tester

packages/@react-spectrum/menu/docs/MenuTrigger.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ import {theme} from '@react-spectrum/theme-default';
270270
import {User} from '@react-spectrum/test-utils';
271271

272272
let testUtilUser = new User({interactionType: 'mouse'});
273-
// ...
273+
// Other setup, be sure to check out the suggested mocks mentioned above in https://react-spectrum.adobe.com/react-spectrum/MenuTrigger.html#testing
274274

275275
it('Menu can open its submenu via keyboard', async function () {
276276
// Render your test component/app and initialize the menu tester

packages/@react-spectrum/picker/docs/Picker.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ import {theme} from '@react-spectrum/theme-default';
602602
import {User} from '@react-spectrum/test-utils';
603603

604604
let testUtilUser = new User({interactionType: 'mouse'});
605-
// ...
605+
// Other setup, be sure to check out the suggested mocks mentioned above in https://react-spectrum.adobe.com/react-spectrum/Picker.html#testing
606606

607607
it('Picker can select an option via keyboard', async function () {
608608
// Render your test component/app and initialize the select tester

packages/@react-spectrum/s2/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"@parcel/macros": "^2.14.0",
125125
"@react-aria/test-utils": "1.0.0-alpha.3",
126126
"@testing-library/dom": "^10.1.0",
127-
"@testing-library/react": "^15.0.7",
127+
"@testing-library/react": "^16.0.0",
128128
"@testing-library/user-event": "^14.0.0",
129129
"jest": "^29.5.0"
130130
},

packages/@react-spectrum/table/docs/TableView.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1974,7 +1974,7 @@ import {theme} from '@react-spectrum/theme-default';
19741974
import {User} from '@react-spectrum/test-utils';
19751975

19761976
let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime});
1977-
// ...
1977+
// Other setup, be sure to check out the suggested mocks mentioned above in https://react-spectrum.adobe.com/react-spectrum/TableView.html#testing
19781978

19791979
it('TableView can toggle row selection', async function () {
19801980
// Render your test component/app and initialize the table tester

0 commit comments

Comments
 (0)