Skip to content

Commit cd313a3

Browse files
authored
feat: Reusable tests (#7011)
* Reusable tests * Add more tests * Fixing lint * fix lint and types * Fix tests for s2 with icons * fix lint * Add some submenu tests * Add tests for RSPv3 since that's where I'm moving from * fix types and lint * fix lock file * Fix React 16&17 tests * Remove duplicates, improve menu utility, allow tester overrides, add more interaction types * fix 17 tests * fix lint * removed some dead code * Fix merge conflicts * remove unnecessary change * update lock file * fix lint
1 parent 37fbf02 commit cd313a3

File tree

16 files changed

+1705
-562
lines changed

16 files changed

+1705
-562
lines changed

__mocks__/svg.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default function SvgrURL() {
2+
return <svg><g></g></svg>;
3+
};
4+
export const ReactComponent = (props) => <svg {...props} />;

jest.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ module.exports = {
9191

9292
// A map from regular expressions to module names that allow to stub out resources with a single module
9393
moduleNameMapper: {
94-
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
94+
'^bundle-text:.*\\.svg$': '<rootDir>/__mocks__/fileMock.js',
95+
'\\.svg$': '<rootDir>/__mocks__/svg.js',
96+
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
9597
'\\.(css|styl)$': 'identity-obj-proxy'
9698
},
9799

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {act, waitFor, within} from '@testing-library/react';
1414
import {BaseTesterOpts, UserOpts} from './user';
1515

1616
export interface ComboBoxOptions extends UserOpts, BaseTesterOpts {
17-
user: any,
17+
user?: any,
1818
trigger?: HTMLElement
1919
}
2020

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {BaseTesterOpts, UserOpts} from './user';
1515
import {pressElement} from './events';
1616

1717
export interface GridListOptions extends UserOpts, BaseTesterOpts {
18-
user: any
18+
user?: any
1919
}
2020
export class GridListTester {
2121
private user;

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

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@ import {BaseTesterOpts, UserOpts} from './user';
1515
import {triggerLongPress} from './events';
1616

1717
export interface MenuOptions extends UserOpts, BaseTesterOpts {
18-
user: any
18+
user?: any,
19+
isSubmenu?: boolean
1920
}
2021
export class MenuTester {
2122
private user;
2223
private _interactionType: UserOpts['interactionType'];
2324
private _advanceTimer: UserOpts['advanceTimer'];
24-
private _trigger: HTMLElement;
25+
private _trigger: HTMLElement | undefined;
26+
private _isSubmenu: boolean = false;
2527

2628
constructor(opts: MenuOptions) {
27-
let {root, user, interactionType, advanceTimer} = opts;
29+
let {root, user, interactionType, advanceTimer, isSubmenu} = opts;
2830
this.user = user;
2931
this._interactionType = interactionType || 'mouse';
3032
this._advanceTimer = advanceTimer;
@@ -41,6 +43,8 @@ export class MenuTester {
4143
this._trigger = root;
4244
}
4345
}
46+
47+
this._isSubmenu = isSubmenu || false;
4448
}
4549

4650
setInteractionType = (type: UserOpts['interactionType']) => {
@@ -49,12 +53,12 @@ export class MenuTester {
4953

5054
// 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
5155
// One difference will be that it supports long press as well
52-
open = async (opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType']} = {}) => {
56+
open = async (opts: {needsLongPress?: boolean, interactionType?: UserOpts['interactionType'], direction?: 'up' | 'down'} = {}) => {
5357
let {
5458
needsLongPress,
55-
interactionType = this._interactionType
59+
interactionType = this._interactionType,
60+
direction
5661
} = opts;
57-
5862
let trigger = this.trigger;
5963
let isDisabled = trigger.hasAttribute('disabled');
6064
if (interactionType === 'mouse' || interactionType === 'touch') {
@@ -70,8 +74,16 @@ export class MenuTester {
7074
await this.user.pointer({target: trigger, keys: '[TouchA]'});
7175
}
7276
} else if (interactionType === 'keyboard' && !isDisabled) {
73-
act(() => trigger.focus());
74-
await this.user.keyboard('[Enter]');
77+
if (direction === 'up') {
78+
act(() => trigger.focus());
79+
await this.user.keyboard('[ArrowUp]');
80+
} else if (direction === 'down') {
81+
act(() => trigger.focus());
82+
await this.user.keyboard('[ArrowDown]');
83+
} else {
84+
act(() => trigger.focus());
85+
await this.user.keyboard('[Enter]');
86+
}
7587
}
7688

7789
await waitFor(() => {
@@ -95,42 +107,57 @@ export class MenuTester {
95107

96108
// TODO: also very similar to select, barring potential long press support
97109
// Close on select is also kinda specific?
98-
selectOption = async (opts: {option?: HTMLElement, optionText?: string, menuSelectionMode?: 'single' | 'multiple', needsLongPress?: boolean, closesOnSelect?: boolean, interactionType?: UserOpts['interactionType']}) => {
110+
selectOption = async (opts: {
111+
option?: HTMLElement,
112+
optionText?: string,
113+
menuSelectionMode?: 'single' | 'multiple',
114+
needsLongPress?: boolean,
115+
closesOnSelect?: boolean,
116+
interactionType?: UserOpts['interactionType'],
117+
keyboardActivation?: 'Space' | 'Enter'
118+
}) => {
99119
let {
100120
optionText,
101121
menuSelectionMode = 'single',
102122
needsLongPress,
103123
closesOnSelect = true,
104124
option,
105-
interactionType = this._interactionType
125+
interactionType = this._interactionType,
126+
keyboardActivation = 'Enter'
106127
} = opts;
107128
let trigger = this.trigger;
108-
if (!trigger.getAttribute('aria-controls')) {
129+
130+
if (!trigger.getAttribute('aria-controls') && !trigger.hasAttribute('aria-expanded')) {
109131
await this.open({needsLongPress});
110132
}
111133

112134
let menu = this.menu;
113135
if (menu) {
114136
if (!option && optionText) {
115-
option = within(menu).getByText(optionText);
137+
// @ts-ignore
138+
option = (within(menu!).getByText(optionText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))!;
139+
}
140+
if (!option) {
141+
throw new Error('No option found in the menu.');
116142
}
117143

118144
if (interactionType === 'keyboard') {
119145
if (document.activeElement !== menu || !menu.contains(document.activeElement)) {
120146
act(() => menu.focus());
121147
}
122148

123-
await this.user.keyboard(optionText);
124-
await this.user.keyboard('[Enter]');
149+
await this.keyboardNavigateToOption({option});
150+
await this.user.keyboard(`[${keyboardActivation}]`);
125151
} else {
126152
if (interactionType === 'mouse') {
127153
await this.user.click(option);
128154
} else {
129155
await this.user.pointer({target: option, keys: '[TouchA]'});
130156
}
131157
}
158+
act(() => {jest.runAllTimers();});
132159

133-
if (option && option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect) {
160+
if (option && option.getAttribute('href') == null && option.getAttribute('aria-haspopup') == null && menuSelectionMode === 'single' && closesOnSelect && keyboardActivation !== 'Space' && !this._isSubmenu) {
134161
await waitFor(() => {
135162
if (document.activeElement !== trigger) {
136163
throw new Error(`Expected the document.activeElement after selecting an option to be the menu trigger but got ${document.activeElement}`);
@@ -156,6 +183,7 @@ export class MenuTester {
156183
needsLongPress,
157184
interactionType = this._interactionType
158185
} = opts;
186+
159187
let trigger = this.trigger;
160188
let isDisabled = trigger.hasAttribute('disabled');
161189
if (!trigger.getAttribute('aria-controls') && !isDisabled) {
@@ -171,8 +199,18 @@ export class MenuTester {
171199
submenu = within(menu).getByText(submenuTriggerText);
172200
}
173201

174-
let submenuTriggerTester = new MenuTester({user: this.user, interactionType: interactionType, root: submenu});
175-
await submenuTriggerTester.open();
202+
let submenuTriggerTester = new MenuTester({user: this.user, interactionType: this._interactionType, root: submenu, isSubmenu: true});
203+
if (interactionType === 'mouse') {
204+
await this.user.pointer({target: submenu});
205+
act(() => {jest.runAllTimers();});
206+
} else if (interactionType === 'keyboard') {
207+
await this.keyboardNavigateToOption({option: submenu});
208+
await this.user.keyboard('[ArrowRight]');
209+
act(() => {jest.runAllTimers();});
210+
} else {
211+
await submenuTriggerTester.open();
212+
}
213+
176214

177215
return submenuTriggerTester;
178216
}
@@ -181,6 +219,28 @@ export class MenuTester {
181219
return null;
182220
};
183221

222+
keyboardNavigateToOption = async (opts: {option: HTMLElement}) => {
223+
let {option} = opts;
224+
let options = this.options;
225+
let targetIndex = options.indexOf(option);
226+
if (targetIndex === -1) {
227+
throw new Error('Option provided is not in the menu');
228+
}
229+
if (document.activeElement === this.menu) {
230+
await this.user.keyboard('[ArrowDown]');
231+
}
232+
let currIndex = options.indexOf(document.activeElement as HTMLElement);
233+
if (targetIndex === -1) {
234+
throw new Error('ActiveElement is not in the menu');
235+
}
236+
let direction = targetIndex > currIndex ? 'down' : 'up';
237+
238+
for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
239+
await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
240+
}
241+
};
242+
243+
184244
close = async () => {
185245
let menu = this.menu;
186246
if (menu) {
@@ -202,6 +262,9 @@ export class MenuTester {
202262
};
203263

204264
get trigger() {
265+
if (!this._trigger) {
266+
throw new Error('No trigger element found for menu.');
267+
}
205268
return this._trigger;
206269
}
207270

@@ -210,9 +273,9 @@ export class MenuTester {
210273
return menuId ? document.getElementById(menuId) : undefined;
211274
}
212275

213-
get options(): HTMLElement[] | never[] {
276+
get options(): HTMLElement[] {
214277
let menu = this.menu;
215-
let options = [];
278+
let options: HTMLElement[] = [];
216279
if (menu) {
217280
options = within(menu).queryAllByRole('menuitem');
218281
if (options.length === 0) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {BaseTesterOpts, UserOpts} from './user';
1515

1616
export interface SelectOptions extends UserOpts, BaseTesterOpts {
1717
// TODO: I think the type grabbed from the testing library dist for UserEvent is breaking the build, will need to figure out a better place to grab from
18-
user: any
18+
user?: any
1919
}
2020
export class SelectTester {
2121
private user;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {act, fireEvent, waitFor, within} from '@testing-library/react';
1414
import {BaseTesterOpts, UserOpts} from './user';
1515
import {pressElement, triggerLongPress} from './events';
1616
export interface TableOptions extends UserOpts, BaseTesterOpts {
17-
user: any,
17+
user?: any,
1818
advanceTimer: UserOpts['advanceTimer']
1919
}
2020

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,6 @@ export class User {
6868
}
6969

7070
createTester<T extends PatternNames>(patternName: T, opts: ObjectOptionsTypes<T>): ObjectType<T> {
71-
return new (keyToUtil)[patternName]({...opts, user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer}) as ObjectType<T>;
71+
return new (keyToUtil)[patternName]({user: this.user, interactionType: this.interactionType, advanceTimer: this.advanceTimer, ...opts}) as ObjectType<T>;
7272
}
7373
}

0 commit comments

Comments
 (0)