Skip to content

feat: Initial aria test util docs and listbox/tabs/tree utils #7145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0991467
scaffolding and documenting tester api
LFDanLu Oct 2, 2024
9def1bd
fix strict mode
LFDanLu Oct 2, 2024
d6e7578
update utils for conformity and add intro
LFDanLu Oct 3, 2024
4b5bad1
add instalation, setup, and update method types
LFDanLu Oct 3, 2024
4b28f24
update docs with examples and update types
LFDanLu Oct 4, 2024
256e859
remove some todos
LFDanLu Oct 4, 2024
00a2fb0
forgot to remove a only
LFDanLu Oct 4, 2024
d460519
fix lint
LFDanLu Oct 4, 2024
aa1f7f4
fix lint again, for some reason local lint doesnt catch this one...
LFDanLu Oct 4, 2024
452cb3d
review comments
LFDanLu Dec 9, 2024
3c3a705
update select option methods to accept node, string, and index
LFDanLu Dec 9, 2024
6507b0e
Merge branch 'main' of github.com:adobe/react-spectrum into test_util…
LFDanLu Dec 9, 2024
29afbaf
fix tests and more consistency refactors
LFDanLu Dec 9, 2024
1dff5c1
updating copy per review
LFDanLu Dec 9, 2024
2f90006
feat: Next batch of aria utils (Listbox, Tabs, Tree) (#7505)
LFDanLu Dec 13, 2024
22ac60a
Merge branch 'main' of github.com:adobe/react-spectrum into test_util…
LFDanLu Jan 6, 2025
64e1b71
review comments
LFDanLu Jan 6, 2025
12bc1c5
Merge branch 'main' of github.com:adobe/react-spectrum into test_util…
LFDanLu Jan 9, 2025
5a9a265
small fixes from review
LFDanLu Jan 9, 2025
17c36d9
update testing pages to be more standalone as per review
LFDanLu Jan 10, 2025
7c5ed5c
add alpha badge
LFDanLu Jan 10, 2025
1273fea
review comments
LFDanLu Jan 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/@react-aria/test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"peerDependencies": {
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^13.0.0 || ^14.0.0",
"jest": "^29.5.0",
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
},
"publishConfig": {
Expand Down
141 changes: 105 additions & 36 deletions packages/@react-aria/test-utils/src/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,34 @@
*/

import {act, waitFor, within} from '@testing-library/react';
import {BaseTesterOpts, UserOpts} from './user';
import {ComboBoxTesterOpts, UserOpts} from './types';

export interface ComboBoxOptions extends UserOpts, BaseTesterOpts {
user?: any,
trigger?: HTMLElement
interface ComboBoxOpenOpts {
/**
* Whether the combobox opens on focus or needs to be manually opened via user action.
* @default 'manual'
*/
triggerBehavior?: 'focus' | 'manual',
/**
* What interaction type to use when opening the combobox. Defaults to the interaction type set on the tester.
*/
interactionType?: UserOpts['interactionType']
}

interface ComboBoxSelectOpts extends ComboBoxOpenOpts {
/**
* The index, text, or node of the option to select. Option nodes can be sourced via `options()`.
*/
option: number | string | HTMLElement
}

export class ComboBoxTester {
private user;
private _interactionType: UserOpts['interactionType'];
private _combobox: HTMLElement;
private _trigger: HTMLElement | undefined;
private _trigger: HTMLElement;

constructor(opts: ComboBoxOptions) {
constructor(opts: ComboBoxTesterOpts) {
let {root, trigger, user, interactionType} = opts;
this.user = user;
this._interactionType = interactionType || 'mouse';
Expand Down Expand Up @@ -52,11 +66,17 @@ export class ComboBoxTester {
}
}

setInteractionType = (type: UserOpts['interactionType']) => {
/**
* Set the interaction type used by the combobox tester.
*/
setInteractionType(type: UserOpts['interactionType']) {
this._interactionType = type;
};
}

open = async (opts: {triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) => {
/**
* Opens the combobox dropdown. Defaults to using the interaction type set on the combobox tester.
*/
async open(opts: ComboBoxOpenOpts = {}) {
let {triggerBehavior = 'manual', interactionType = this._interactionType} = opts;
let trigger = this.trigger;
let combobox = this.combobox;
Expand Down Expand Up @@ -96,18 +116,51 @@ export class ComboBoxTester {
return true;
}
});
};
}

/**
* Returns a option matching the specified index or text content.
*/
findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
let {
optionIndexOrText
} = opts;

let option;
let options = this.options();
let listbox = this.listbox;

selectOption = async (opts: {option?: HTMLElement, optionText?: string, triggerBehavior?: 'focus' | 'manual', interactionType?: UserOpts['interactionType']} = {}) => {
let {optionText, option, triggerBehavior, interactionType = this._interactionType} = opts;
if (typeof optionIndexOrText === 'number') {
option = options[optionIndexOrText];
} else if (typeof optionIndexOrText === 'string' && listbox != null) {
option = (within(listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement;
}

return option;
}

/**
* Selects the desired combobox option. Defaults to using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand.
* The desired option can be targeted via the option's node, the option's text, or the option's index.
*/
async selectOption(opts: ComboBoxSelectOpts) {
let {option, triggerBehavior, interactionType = this._interactionType} = opts;
if (!this.combobox.getAttribute('aria-controls')) {
await this.open({triggerBehavior});
}

let listbox = this.listbox;
if (!listbox) {
throw new Error('Combobox\'s listbox not found.');
}

if (listbox) {
if (!option && optionText) {
option = within(listbox).getByText(optionText);
if (typeof option === 'string' || typeof option === 'number') {
option = this.findOption({optionIndexOrText: option});
}

if (!option) {
throw new Error('Target option not found in the listbox.');
}

// TODO: keyboard method of selecting the the option is a bit tricky unless I simply simulate the user pressing the down arrow
Expand All @@ -118,7 +171,7 @@ export class ComboBoxTester {
await this.user.pointer({target: option, keys: '[TouchA]'});
}

if (option && option.getAttribute('href') == null) {
if (option.getAttribute('href') == null) {
await waitFor(() => {
if (document.contains(listbox)) {
throw new Error('Expected listbox element to not be in the document after selecting an option');
Expand All @@ -130,9 +183,12 @@ export class ComboBoxTester {
} else {
throw new Error("Attempted to select a option in the combobox, but the listbox wasn't found.");
}
};
}

close = async () => {
/**
* Closes the combobox dropdown.
*/
async close() {
let listbox = this.listbox;
if (listbox) {
act(() => this.combobox.focus());
Expand All @@ -146,43 +202,56 @@ export class ComboBoxTester {
}
});
}
};
}

get combobox() {
/**
* Returns the combobox.
*/
get combobox(): HTMLElement {
return this._combobox;
}

get trigger() {
/**
* Returns the combobox trigger button.
*/
get trigger(): HTMLElement {
return this._trigger;
}

get listbox() {
/**
* Returns the combobox's listbox if present.
*/
get listbox(): HTMLElement | null {
let listBoxId = this.combobox.getAttribute('aria-controls');
return listBoxId ? document.getElementById(listBoxId) || undefined : undefined;
return listBoxId ? document.getElementById(listBoxId) || null : null;
}

/**
* Returns the combobox's sections if present.
*/
get sections(): HTMLElement[] {
let listbox = this.listbox;
return listbox ? within(listbox).queryAllByRole('group') : [];
}

options = (opts: {element?: HTMLElement} = {}): HTMLElement[] | never[] => {
let {element} = opts;
element = element || this.listbox;
/**
* Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided via `element`.
*/
options(opts: {element?: HTMLElement} = {}): HTMLElement[] {
let {element = this.listbox} = opts;
let options = [];
if (element) {
options = within(element).queryAllByRole('option');
}

return options;
};

get sections() {
let listbox = this.listbox;
if (listbox) {
return within(listbox).queryAllByRole('group');
} else {
return [];
}
}

get focusedOption() {
/**
* Returns the currently focused option in the combobox's dropdown if any.
*/
get focusedOption(): HTMLElement | null {
let focusedOptionId = this.combobox.getAttribute('aria-activedescendant');
return focusedOptionId ? document.getElementById(focusedOptionId) : undefined;
return focusedOptionId ? document.getElementById(focusedOptionId) : null;
}
}
2 changes: 1 addition & 1 deletion packages/@react-aria/test-utils/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {act, fireEvent} from '@testing-library/react';
import {UserOpts} from './user';
import {UserOpts} from './types';

export const DEFAULT_LONG_PRESS_TIME = 500;

Expand Down
Loading
Loading