diff --git a/packages/@adobe/react-spectrum/docs/combobox/ComboBox.mdx b/packages/@adobe/react-spectrum/docs/combobox/ComboBox.mdx
index 7c4e54b76fe..4bb9373b6ad 100644
--- a/packages/@adobe/react-spectrum/docs/combobox/ComboBox.mdx
+++ b/packages/@adobe/react-spectrum/docs/combobox/ComboBox.mdx
@@ -1020,12 +1020,12 @@ it('ComboBox can select an option via keyboard', async function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'});
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeInTheDocument();
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
- let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ let options = comboboxTester.getOptions();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.getCombobox().value).toBe('One');
+ expect(comboboxTester.getListbox()).not.toBeInTheDocument();
});
```
diff --git a/packages/@adobe/react-spectrum/docs/list/ListView.mdx b/packages/@adobe/react-spectrum/docs/list/ListView.mdx
index f774d1e866d..84a2a7d386f 100644
--- a/packages/@adobe/react-spectrum/docs/list/ListView.mdx
+++ b/packages/@adobe/react-spectrum/docs/list/ListView.mdx
@@ -1218,17 +1218,17 @@ it('ListView can select a row via keyboard', async function () {
);
let gridListTester = testUtilUser.createTester('GridList', {root: getByTestId('test-gridlist'), interactionType: 'keyboard'});
- let row = gridListTester.rows[0];
+ let row = gridListTester.getRows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/@adobe/react-spectrum/docs/listbox/ListBox.mdx b/packages/@adobe/react-spectrum/docs/listbox/ListBox.mdx
index 28bb88b9c72..97cb8e5922a 100644
--- a/packages/@adobe/react-spectrum/docs/listbox/ListBox.mdx
+++ b/packages/@adobe/react-spectrum/docs/listbox/ListBox.mdx
@@ -437,7 +437,7 @@ it('ListBox can select an option via keyboard', async function () {
let listboxTester = testUtilUser.createTester('ListBox', {root: getByTestId('test-listbox'), interactionType: 'keyboard'});
await listboxTester.toggleOptionSelection({option: 4});
- expect(listboxTester.options()[4]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[4]).toHaveAttribute('aria-selected', 'true');
});
```
diff --git a/packages/@adobe/react-spectrum/docs/menu/MenuTrigger.mdx b/packages/@adobe/react-spectrum/docs/menu/MenuTrigger.mdx
index eb3d403692f..8435e7a24bc 100644
--- a/packages/@adobe/react-spectrum/docs/menu/MenuTrigger.mdx
+++ b/packages/@adobe/react-spectrum/docs/menu/MenuTrigger.mdx
@@ -285,16 +285,16 @@ it('Menu can open its submenu via keyboard', async function () {
let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'});
await menuTester.open();
- expect(menuTester.menu).toBeInTheDocument();
- let submenuTriggers = menuTester.submenuTriggers;
+ expect(menuTester.getMenu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.getSubmenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.getMenu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.getOptions()[0]});
+ expect(submenuTester.getMenu()).not.toBeInTheDocument();
+ expect(menuTester.getMenu()).not.toBeInTheDocument();
});
```
diff --git a/packages/@adobe/react-spectrum/docs/picker/Picker.mdx b/packages/@adobe/react-spectrum/docs/picker/Picker.mdx
index f522760486d..1213254c8e7 100644
--- a/packages/@adobe/react-spectrum/docs/picker/Picker.mdx
+++ b/packages/@adobe/react-spectrum/docs/picker/Picker.mdx
@@ -600,10 +600,10 @@ it('Picker can select an option via keyboard', async function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select…');
- await selectTester.selectOption({option: 'Cat'});
+ await selectTester.toggleOptionSelection({option: 'Cat'});
expect(trigger).toHaveTextContent('Cat');
});
```
diff --git a/packages/@adobe/react-spectrum/docs/table/TableView.mdx b/packages/@adobe/react-spectrum/docs/table/TableView.mdx
index 86e558292e2..718d6d52638 100644
--- a/packages/@adobe/react-spectrum/docs/table/TableView.mdx
+++ b/packages/@adobe/react-spectrum/docs/table/TableView.mdx
@@ -1986,22 +1986,22 @@ it('TableView can toggle row selection', async function () {
);
let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')});
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.getSelectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.getRows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/@adobe/react-spectrum/docs/tabs/Tabs.mdx b/packages/@adobe/react-spectrum/docs/tabs/Tabs.mdx
index 1d28a009f2b..a6e779f3627 100644
--- a/packages/@adobe/react-spectrum/docs/tabs/Tabs.mdx
+++ b/packages/@adobe/react-spectrum/docs/tabs/Tabs.mdx
@@ -662,11 +662,11 @@ it('Tabs can change selection via keyboard', async function () {
);
let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'});
- let tabs = tabsTester.tabs;
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ let tabs = tabsTester.getTabs();
+ expect(tabsTester.getSelectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[1]);
});
```
diff --git a/packages/@adobe/react-spectrum/docs/tree/TreeView.mdx b/packages/@adobe/react-spectrum/docs/tree/TreeView.mdx
index d9b7328adf4..ed582c03a9a 100644
--- a/packages/@adobe/react-spectrum/docs/tree/TreeView.mdx
+++ b/packages/@adobe/react-spectrum/docs/tree/TreeView.mdx
@@ -557,16 +557,16 @@ it('TreeView can select a row via keyboard', async function () {
let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'});
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(within(treeTester.getRows()[1]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).not.toBeChecked();
});
```
diff --git a/packages/@adobe/react-spectrum/test/checkbox/CheckboxGroup.test.js b/packages/@adobe/react-spectrum/test/checkbox/CheckboxGroup.test.js
index 5c577506b84..e991000c009 100644
--- a/packages/@adobe/react-spectrum/test/checkbox/CheckboxGroup.test.js
+++ b/packages/@adobe/react-spectrum/test/checkbox/CheckboxGroup.test.js
@@ -889,32 +889,32 @@ describe('CheckboxGroup', () => {
let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {
root: getByRole('group')
});
- expect(checkboxGroupTester.checkboxgroup).toHaveAttribute('role');
- let checkboxes = checkboxGroupTester.checkboxes;
+ expect(checkboxGroupTester.getCheckboxGroup()).toHaveAttribute('role');
+ let checkboxes = checkboxGroupTester.getCheckboxes();
await checkboxGroupTester.toggleCheckbox({checkbox: checkboxes[0]});
expect(checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4, interactionType: 'keyboard'});
expect(checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(2);
- let checkbox4 = checkboxGroupTester.findCheckbox({checkboxIndexOrText: 3});
+ let checkbox4 = checkboxGroupTester.findCheckbox({indexOrText: 3});
await checkboxGroupTester.toggleCheckbox({
checkbox: checkbox4,
interactionType: 'keyboard'
});
expect(checkboxes[3]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(3);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(3);
await checkboxGroupTester.toggleCheckbox({checkbox: 'Soccer', interactionType: 'keyboard'});
expect(checkboxes[0]).not.toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(2);
- let checkbox5 = checkboxGroupTester.findCheckbox({checkboxIndexOrText: 'Rugby'});
+ let checkbox5 = checkboxGroupTester.findCheckbox({indexOrText: 'Rugby'});
await checkboxGroupTester.toggleCheckbox({checkbox: checkbox5, interactionType: 'mouse'});
expect(checkboxes[4]).not.toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(1);
}
);
});
diff --git a/packages/@adobe/react-spectrum/test/combobox/ComboBox.test.js b/packages/@adobe/react-spectrum/test/combobox/ComboBox.test.js
index 997d54adc8c..fc26e484870 100644
--- a/packages/@adobe/react-spectrum/test/combobox/ComboBox.test.js
+++ b/packages/@adobe/react-spectrum/test/combobox/ComboBox.test.js
@@ -352,23 +352,23 @@ describe('ComboBox', function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('One');
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onFocus).not.toHaveBeenCalled();
comboboxTester.setInteractionType('keyboard');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
});
@@ -378,11 +378,11 @@ describe('ComboBox', function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('One');
- expect(comboboxTester.listbox).toBeFalsy();
- expect(comboboxTester.combobox.value).toBe('Blargh');
+ expect(comboboxTester.getListbox()).toBeFalsy();
+ expect(comboboxTester.getCombobox().value).toBe('Blargh');
expect(onOpenChange).not.toHaveBeenCalled();
expect(onFocus).toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
@@ -390,14 +390,14 @@ describe('ComboBox', function () {
comboboxTester.setInteractionType('keyboard');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
});
@@ -405,7 +405,7 @@ describe('ComboBox', function () {
let tree = renderComboBox();
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.getCombobox();
expect(combobox).not.toHaveAttribute('aria-controls');
expect(combobox).not.toHaveAttribute('aria-activedescendant');
expect(combobox).toHaveAttribute('aria-autocomplete', 'list');
@@ -418,12 +418,12 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- let items = comboboxTester.options();
+ let items = comboboxTester.getOptions();
expect(items).toHaveLength(1);
expect(combobox.value).toBe('On');
expect(items[0]).toHaveTextContent('One');
- expect(combobox).toHaveAttribute('aria-controls', comboboxTester.listbox.id);
+ expect(combobox).toHaveAttribute('aria-controls', comboboxTester.getListbox().id);
expect(combobox).not.toHaveAttribute('aria-activedescendant');
await user.keyboard('{ArrowDown}');
@@ -431,7 +431,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(combobox).toHaveAttribute('aria-activedescendant', comboboxTester.focusedOption.id);
+ expect(combobox).toHaveAttribute('aria-activedescendant', comboboxTester.getFocusedOption().id);
});
describe('refs', function () {
@@ -467,11 +467,11 @@ describe('ComboBox', function () {
let tree = renderComboBox({menuTrigger: 'focus'});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let button = comboboxTester.trigger;
- let combobox = comboboxTester.combobox;
+ let button = comboboxTester.getTrigger();
+ let combobox = comboboxTester.getCombobox();
await comboboxTester.open({triggerBehavior: 'focus'});
- let listbox = comboboxTester.listbox;
+ let listbox = comboboxTester.getListbox();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true, 'focus');
await testComboBoxOpen(combobox, button, listbox);
@@ -481,11 +481,11 @@ describe('ComboBox', function () {
let tree = renderComboBox({menuTrigger: 'focus'});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let button = comboboxTester.trigger;
- let combobox = comboboxTester.combobox;
+ let button = comboboxTester.getTrigger();
+ let combobox = comboboxTester.getCombobox();
await comboboxTester.open({triggerBehavior: 'manual'});
- let listbox = comboboxTester.listbox;
+ let listbox = comboboxTester.getListbox();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true, 'focus');
await testComboBoxOpen(combobox, button, listbox);
@@ -502,18 +502,18 @@ describe('ComboBox', function () {
trigger: button
});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeInTheDocument();
- expect(document.activeElement).toBe(comboboxTester.combobox);
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
+ expect(document.activeElement).toBe(comboboxTester.getCombobox());
- await user.click(comboboxTester.trigger);
+ await user.click(comboboxTester.getTrigger());
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
});
it("doesn't focus first item if there are items loaded", async function () {
@@ -536,21 +536,21 @@ describe('ComboBox', function () {
let tree = renderComboBox({});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.getCombobox();
expect(document.activeElement).not.toBe(combobox);
comboboxTester.setInteractionType('touch');
await comboboxTester.open();
- expect(document.activeElement).toBe(comboboxTester.combobox);
- expect(comboboxTester.listbox).toBeInTheDocument();
+ expect(document.activeElement).toBe(comboboxTester.getCombobox());
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
- let button = comboboxTester.trigger;
+ let button = comboboxTester.getTrigger();
fireEvent.touchStart(button, {targetTouches: [{identifier: 1}]});
fireEvent.touchEnd(button, {changedTouches: [{identifier: 1, clientX: 0, clientY: 0}]});
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
});
it("it doesn't reset the focused item when re-opening the menu", async function () {
@@ -558,15 +558,18 @@ describe('ComboBox', function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
await comboboxTester.open();
- expect(comboboxTester.combobox).not.toHaveAttribute('aria-activedescendant');
+ expect(comboboxTester.getCombobox()).not.toHaveAttribute('aria-activedescendant');
- let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
+ let options = comboboxTester.getOptions();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
+ expect(comboboxTester.getCombobox().value).toBe('One');
await comboboxTester.open();
- expect(comboboxTester.combobox).toHaveAttribute('aria-activedescendant', options[0].id);
+ expect(comboboxTester.getCombobox()).toHaveAttribute(
+ 'aria-activedescendant',
+ options[0].id
+ );
});
it('shows all items', async function () {
@@ -920,7 +923,7 @@ describe('ComboBox', function () {
let tree = renderComboBox({defaultSelectedKey: '2'});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.getCombobox();
expect(combobox.value).toBe('Two');
act(() => combobox.focus());
@@ -929,10 +932,10 @@ describe('ComboBox', function () {
expect(onInputChange).toHaveBeenCalledTimes(1);
expect(onInputChange).toHaveBeenLastCalledWith('Tw');
expect(combobox.value).toBe('Tw');
- expect(comboboxTester.options().length).toBe(1);
+ expect(comboboxTester.getOptions().length).toBe(1);
- await comboboxTester.selectOption({option: 'Two'});
- expect(comboboxTester.listbox).toBeFalsy();
+ await comboboxTester.toggleOptionSelection({option: 'Two'});
+ expect(comboboxTester.getListbox()).toBeFalsy();
expect(combobox.value).toBe('Two');
// selectionManager.select from useSingleSelectListState always calls onSelectionChange even if the key is the same
expect(onSelectionChange).toHaveBeenCalledTimes(1);
diff --git a/packages/@adobe/react-spectrum/test/dialog/DialogTrigger.test.js b/packages/@adobe/react-spectrum/test/dialog/DialogTrigger.test.js
index 5394b73e1c8..f1ed8c07288 100644
--- a/packages/@adobe/react-spectrum/test/dialog/DialogTrigger.test.js
+++ b/packages/@adobe/react-spectrum/test/dialog/DialogTrigger.test.js
@@ -89,7 +89,7 @@ describe('DialogTrigger', function () {
let button = getByRole('button');
let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
expect(dialog).toBeVisible();
let modal = getByTestId('modal');
@@ -137,7 +137,7 @@ describe('DialogTrigger', function () {
let button = getByRole('button');
let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'popover'});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
expect(dialog).toBeVisible();
let popover = getByTestId('popover');
@@ -290,7 +290,7 @@ describe('DialogTrigger', function () {
let button = getByRole('button');
let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
expect(document.activeElement).toBe(dialog);
await dialogTester.close();
// now that it's been unmounted, run the raf callback
diff --git a/packages/@adobe/react-spectrum/test/list/ListView.test.js b/packages/@adobe/react-spectrum/test/list/ListView.test.js
index c41f088f838..24d01bffc29 100644
--- a/packages/@adobe/react-spectrum/test/list/ListView.test.js
+++ b/packages/@adobe/react-spectrum/test/list/ListView.test.js
@@ -187,13 +187,13 @@ describe('ListView', function () {
expect(grid).toHaveAttribute('aria-rowcount', '3');
expect(grid).toHaveAttribute('aria-colcount', '1');
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows).toHaveLength(3);
expect(rows[0]).toHaveAttribute('aria-rowindex', '1');
expect(rows[1]).toHaveAttribute('aria-rowindex', '2');
expect(rows[2]).toHaveAttribute('aria-rowindex', '3');
- let gridCells = gridListTester.cells({element: rows[0]});
+ let gridCells = gridListTester.getCells({element: rows[0]});
expect(gridCells).toHaveLength(1);
expect(gridCells[0]).toHaveTextContent('Foo');
expect(gridCells[0]).toHaveAttribute('aria-colindex', '1');
@@ -897,14 +897,14 @@ describe('ListView', function () {
let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple'});
let grid = tree.getByRole('grid');
let gridListTester = testUtilUser.createTester('GridList', {root: grid});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
await gridListTester.toggleRowSelection({row: 0});
checkSelection(onSelectionChange, ['foo']);
onSelectionChange.mockClear();
expect(announce).toHaveBeenLastCalledWith('Foo selected.');
expect(announce).toHaveBeenCalledTimes(1);
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
await user.keyboard('{Control>}a{/Control}');
act(() => jest.runAllTimers());
@@ -912,7 +912,7 @@ describe('ListView', function () {
onSelectionChange.mockClear();
expect(announce).toHaveBeenLastCalledWith('All items selected.');
expect(announce).toHaveBeenCalledTimes(2);
- expect(gridListTester.selectedRows).toHaveLength(3);
+ expect(gridListTester.getSelectedRows()).toHaveLength(3);
fireEvent.keyDown(rows[0], {key: 'Escape'});
fireEvent.keyUp(rows[0], {key: 'Escape'});
@@ -920,7 +920,7 @@ describe('ListView', function () {
onSelectionChange.mockClear();
expect(announce).toHaveBeenLastCalledWith('No items selected.');
expect(announce).toHaveBeenCalledTimes(3);
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
});
describe('onAction', function () {
diff --git a/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js b/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
index 2d13e6723a4..7821cacdb08 100644
--- a/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
+++ b/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
@@ -109,11 +109,11 @@ describe('ListBox', function () {
it('renders properly', function () {
let tree = renderComponent();
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let listbox = listboxTester.listbox;
+ let listbox = listboxTester.getListbox();
expect(listbox).toBeTruthy();
expect(listbox).toHaveAttribute('aria-labelledby', 'label');
- let sections = listboxTester.sections;
+ let sections = listboxTester.getSections();
expect(sections.length).toBe(withSection.length);
for (let section of sections) {
@@ -130,7 +130,7 @@ describe('ListBox', function () {
}
}
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
expect(options.length).toBe(withSection.reduce((acc, curr) => acc + curr.children.length, 0));
let i = 1;
for (let option of options) {
@@ -141,11 +141,11 @@ describe('ListBox', function () {
expect(option).toHaveAttribute('aria-setsize');
}
- expect(listboxTester.findOption({optionIndexOrText: 'Foo'})).toBeTruthy();
- expect(listboxTester.findOption({optionIndexOrText: 'Bar'})).toBeTruthy();
- expect(listboxTester.findOption({optionIndexOrText: 'Baz'})).toBeTruthy();
- expect(listboxTester.findOption({optionIndexOrText: 'Blah'})).toBeTruthy();
- expect(listboxTester.findOption({optionIndexOrText: 'Bleh'})).toBeTruthy();
+ expect(listboxTester.findOption({indexOrText: 'Foo'})).toBeTruthy();
+ expect(listboxTester.findOption({indexOrText: 'Bar'})).toBeTruthy();
+ expect(listboxTester.findOption({indexOrText: 'Baz'})).toBeTruthy();
+ expect(listboxTester.findOption({indexOrText: 'Blah'})).toBeTruthy();
+ expect(listboxTester.findOption({indexOrText: 'Bleh'})).toBeTruthy();
});
it('renders with falsy id', function () {
@@ -214,7 +214,7 @@ describe('ListBox', function () {
});
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let selectedOptions = listboxTester.selectedOptions;
+ let selectedOptions = listboxTester.getSelectedOptions();
expect(selectedOptions).toHaveLength(1);
expect(selectedOptions[0]).toBe(document.activeElement);
expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true');
@@ -226,7 +226,7 @@ describe('ListBox', function () {
// Select a different listbox item via enter
await listboxTester.toggleOptionSelection({option: 4, interactionType: 'keyboard'});
- selectedOptions = listboxTester.selectedOptions;
+ selectedOptions = listboxTester.getSelectedOptions();
expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true');
itemText = within(selectedOptions[0]).getByText('Bleh');
expect(itemText).toBeTruthy();
@@ -282,7 +282,7 @@ describe('ListBox', function () {
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
// Trigger a menu item via space
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
await listboxTester.toggleOptionSelection({
option: 4,
keyboardActivation: 'Space',
@@ -890,9 +890,9 @@ describe('ListBox', function () {
let {rerender, getByRole, getByLabelText} = render();
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- let item = listboxTester.findOption({optionIndexOrText: 'Foo 1'});
- let listboxSections = listboxTester.sections;
- expect(listboxTester.options({element: listboxSections[0]})).toContain(item);
+ let item = listboxTester.findOption({indexOrText: 'Foo 1'});
+ let listboxSections = listboxTester.getSections();
+ expect(listboxTester.getOptions({element: listboxSections[0]})).toContain(item);
expect(listboxSections[0]).toBe(getByLabelText('Section 1'));
let sections2 = [
@@ -908,9 +908,9 @@ describe('ListBox', function () {
rerender();
listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- item = listboxTester.findOption({optionIndexOrText: 'Foo 1'});
- listboxSections = listboxTester.sections;
- expect(listboxTester.options({element: listboxSections[1]})).toContain(item);
+ item = listboxTester.findOption({indexOrText: 'Foo 1'});
+ listboxSections = listboxTester.getSections();
+ expect(listboxTester.getOptions({element: listboxSections[1]})).toContain(item);
expect(listboxSections[1]).toBe(getByLabelText('Section 2'));
});
diff --git a/packages/@adobe/react-spectrum/test/menu/MenuTrigger.test.js b/packages/@adobe/react-spectrum/test/menu/MenuTrigger.test.js
index 081d3b97ee4..0db0865daf5 100644
--- a/packages/@adobe/react-spectrum/test/menu/MenuTrigger.test.js
+++ b/packages/@adobe/react-spectrum/test/menu/MenuTrigger.test.js
@@ -126,7 +126,7 @@ describe('MenuTrigger', function () {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeInTheDocument();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
@@ -223,7 +223,7 @@ describe('MenuTrigger', function () {
async function openAndTriggerMenuItem(tree, role, selectionMode, triggerEvent) {
let menuTester = testUtilUser.createTester('Menu', {root: tree.container});
await menuTester.open();
- let menuItems = menuTester.options();
+ let menuItems = menuTester.getOptions();
let itemToAction = menuItems[1];
await triggerEvent(itemToAction);
act(() => {
@@ -255,7 +255,7 @@ describe('MenuTrigger', function () {
expect(onSelect).toBeCalledTimes(0);
}
- await menuTester.selectOption({
+ await menuTester.toggleOptionSelection({
option: 'Foo',
menuSelectionMode: 'single',
closesOnSelect: false
@@ -267,13 +267,36 @@ describe('MenuTrigger', function () {
expect(onSelect).toBeCalledTimes(1);
}
- expect(menuTester.menu).toBeInTheDocument();
+ expect(menuTester.getMenu()).toBeInTheDocument();
if (Component === MenuTrigger) {
- expect(menuTester.trigger).toHaveAttribute('aria-expanded', 'true');
+ expect(menuTester.getTrigger()).toHaveAttribute('aria-expanded', 'true');
expect(onOpenChange).toBeCalledTimes(1);
} else {
- expect(menuTester.trigger).toHaveAttribute('aria-expanded');
+ expect(menuTester.getTrigger()).toHaveAttribute('aria-expanded');
+ expect(onOpen).toBeCalledTimes(1);
+ expect(onClose).toBeCalledTimes(0);
+ }
+
+ await menuTester.toggleOptionSelection({
+ option: 'Bar',
+ menuSelectionMode: 'single',
+ closesOnSelect: false
+ });
+
+ if (Component === MenuTrigger) {
+ expect(onSelectionChange).toBeCalledTimes(2);
+ } else {
+ expect(onSelect).toBeCalledTimes(2);
+ }
+
+ expect(menuTester.getMenu()).toBeInTheDocument();
+
+ if (Component === MenuTrigger) {
+ expect(menuTester.getTrigger()).toHaveAttribute('aria-expanded', 'true');
+ expect(onOpenChange).toBeCalledTimes(1);
+ } else {
+ expect(menuTester.getTrigger()).toHaveAttribute('aria-expanded');
expect(onOpen).toBeCalledTimes(1);
expect(onClose).toBeCalledTimes(0);
}
@@ -294,14 +317,14 @@ describe('MenuTrigger', function () {
expect(onOpenChange).toBeCalledTimes(1);
expect(onSelectionChange).toBeCalledTimes(0);
menuTester.setInteractionType('keyboard');
- await menuTester.selectOption({
+ await menuTester.toggleOptionSelection({
option: 'Foo',
menuSelectionMode: 'single',
closesOnSelect: false
});
- expect(menuTester.menu).toBeInTheDocument();
- expect(menuTester.trigger).toHaveAttribute('aria-expanded', 'true');
+ expect(menuTester.getMenu()).toBeInTheDocument();
+ expect(menuTester.getTrigger()).toHaveAttribute('aria-expanded', 'true');
expect(onOpenChange).toBeCalledTimes(1);
}
);
@@ -327,14 +350,14 @@ describe('MenuTrigger', function () {
expect(onOpenChange).toBeCalledTimes(1);
expect(onSelectionChange).toBeCalledTimes(0);
- await menuTester.selectOption({
+ await menuTester.toggleOptionSelection({
option: 'Foo',
menuSelectionMode: 'multiple',
keyboardActivation: 'Space'
});
expect(onSelectionChange).toBeCalledTimes(1);
expect(onSelectionChange.mock.calls[0][0].has('Foo')).toBeTruthy();
- await menuTester.selectOption({
+ await menuTester.toggleOptionSelection({
option: 'Bar',
menuSelectionMode: 'multiple',
keyboardActivation: 'Space'
@@ -343,7 +366,7 @@ describe('MenuTrigger', function () {
expect(onSelectionChange.mock.calls[1][0].has('Bar')).toBeTruthy();
await menuTester.close();
- expect(menuTester.menu).not.toBeInTheDocument();
+ expect(menuTester.getMenu()).not.toBeInTheDocument();
expect(onOpenChange).toBeCalledTimes(2);
}
);
@@ -991,7 +1014,7 @@ describe('MenuTrigger', function () {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
await user.tab();
act(() => {
@@ -1001,7 +1024,7 @@ describe('MenuTrigger', function () {
jest.runAllTimers();
});
expect(menu).toBeInTheDocument();
- expect(document.activeElement).toBe(menuTester.options()[0]);
+ expect(document.activeElement).toBe(menuTester.getOptions()[0]);
});
});
diff --git a/packages/@adobe/react-spectrum/test/picker/Picker.test.js b/packages/@adobe/react-spectrum/test/picker/Picker.test.js
index c607b1ff994..92c6d270563 100644
--- a/packages/@adobe/react-spectrum/test/picker/Picker.test.js
+++ b/packages/@adobe/react-spectrum/test/picker/Picker.test.js
@@ -108,17 +108,17 @@ describe('Picker', function () {
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
expect(queryByRole('listbox')).toBeNull();
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.getListbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
expect(picker).toHaveAttribute('aria-expanded', 'true');
expect(picker).toHaveAttribute('aria-controls', listbox.id);
- let items = selectTester.options();
+ let items = selectTester.getOptions();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('One');
expect(items[1]).toHaveTextContent('Two');
@@ -218,18 +218,18 @@ describe('Picker', function () {
expect(queryByRole('listbox')).toBeNull();
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
selectTester.setInteractionType('keyboard');
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.getListbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
expect(picker).toHaveAttribute('aria-expanded', 'true');
expect(picker).toHaveAttribute('aria-controls', listbox.id);
- let items = selectTester.options();
+ let items = selectTester.getOptions();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('One');
expect(items[1]).toHaveTextContent('Two');
@@ -252,7 +252,7 @@ describe('Picker', function () {
expect(queryByRole('listbox')).toBeNull();
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
// TODO: for these keyboard event, IMO we don't have to include in the test utils since the user can pretty
// easily define what specific keyboard interactions they want to do. We can handle firing the various intermediate interactions
// for basic flows (aka we will handle firing Enter in selectTester.open())
@@ -260,14 +260,14 @@ describe('Picker', function () {
fireEvent.keyUp(picker, {key: 'ArrowDown'});
act(() => jest.runAllTimers());
- let listbox = selectTester.listbox;
+ let listbox = selectTester.getListbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
expect(picker).toHaveAttribute('aria-expanded', 'true');
expect(picker).toHaveAttribute('aria-controls', listbox.id);
- let items = selectTester.options();
+ let items = selectTester.getOptions();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('One');
expect(items[1]).toHaveTextContent('Two');
@@ -541,10 +541,10 @@ describe('Picker', function () {
expect(queryByRole('listbox')).toBeNull();
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.getListbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
@@ -963,12 +963,12 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
- let listbox = selectTester.listbox;
- let items = selectTester.options();
+ let listbox = selectTester.getListbox();
+ let items = selectTester.getOptions();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('One');
expect(items[1]).toHaveTextContent('Two');
@@ -976,7 +976,7 @@ describe('Picker', function () {
expect(document.activeElement).toBe(listbox);
- await selectTester.selectOption({option: 'Three'});
+ await selectTester.toggleOptionSelection({option: 'Three'});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('three');
@@ -995,12 +995,12 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
- let listbox = selectTester.listbox;
- let items = selectTester.options();
+ let listbox = selectTester.getListbox();
+ let items = selectTester.getOptions();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('Empty');
expect(items[1]).toHaveTextContent('Zero');
@@ -1008,19 +1008,19 @@ describe('Picker', function () {
expect(document.activeElement).toBe(listbox);
- await selectTester.selectOption({option: 'Empty'});
+ await selectTester.toggleOptionSelection({option: 'Empty'});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('');
expect(document.activeElement).toBe(picker);
expect(picker).toHaveTextContent('Empty');
- await selectTester.selectOption({option: 'Zero'});
+ await selectTester.toggleOptionSelection({option: 'Zero'});
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(onSelectionChange).toHaveBeenLastCalledWith('0');
expect(document.activeElement).toBe(picker);
expect(picker).toHaveTextContent('Zero');
- await selectTester.selectOption({option: 'False'});
+ await selectTester.toggleOptionSelection({option: 'False'});
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(onSelectionChange).toHaveBeenLastCalledWith('false');
expect(document.activeElement).toBe(picker);
@@ -1085,18 +1085,18 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
selectTester.setInteractionType('keyboard');
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
- let items = selectTester.options();
+ let items = selectTester.getOptions();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('One');
expect(items[1]).toHaveTextContent('Two');
expect(items[2]).toHaveTextContent('Three');
- await selectTester.selectOption({option: 'Two'});
+ await selectTester.toggleOptionSelection({option: 'Two'});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('two');
@@ -1161,23 +1161,23 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
expect(picker).toHaveTextContent('Select…');
expect(onOpenChangeSpy).toHaveBeenCalledTimes(0);
await selectTester.open();
expect(onOpenChangeSpy).toHaveBeenCalledTimes(1);
- let listbox = selectTester.listbox;
+ let listbox = selectTester.getListbox();
let label = getAllByText('Test')[0];
expect(listbox).toBeVisible();
expect(listbox).toHaveAttribute('aria-labelledby', label.id);
- let items = selectTester.options();
+ let items = selectTester.getOptions();
expect(items[0]).toHaveTextContent('One');
expect(items[1]).toHaveTextContent('Two');
expect(items[2]).toHaveTextContent('Three');
- await selectTester.selectOption({option: 'Three'});
+ await selectTester.toggleOptionSelection({option: 'Three'});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onOpenChangeSpy).toHaveBeenCalledTimes(2);
@@ -1365,15 +1365,15 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
- let listbox = selectTester.listbox;
- let items = selectTester.options();
+ let listbox = selectTester.getListbox();
+ let items = selectTester.getOptions();
expect(items.length).toBe(6);
- let groups = selectTester.sections;
+ let groups = selectTester.getSections();
expect(groups).toHaveLength(2);
expect(groups[0]).toHaveAttribute('aria-labelledby', getByText('Section 1').id);
@@ -1427,8 +1427,8 @@ describe('Picker', function () {
// Open again
await selectTester.open();
- listbox = selectTester.listbox;
- items = selectTester.options();
+ listbox = selectTester.getListbox();
+ items = selectTester.getOptions();
expect(items.length).toBe(6);
expect(document.activeElement).toBe(items[1]);
@@ -1535,14 +1535,14 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
expect(picker).toHaveTextContent('Two');
await selectTester.open();
- let items = selectTester.options();
+ let items = selectTester.getOptions();
expect(document.activeElement).toBe(items[1]);
- await selectTester.selectOption({option: 'Two'});
+ await selectTester.toggleOptionSelection({option: 'Two'});
expect(onSelectionChange).not.toHaveBeenCalled();
expect(document.activeElement).toBe(picker);
@@ -2138,7 +2138,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
let input = document.querySelector('[name=picker]');
expect(input).toHaveAttribute('required');
expect(picker).not.toHaveAttribute('aria-describedby');
@@ -2154,7 +2154,7 @@ describe('Picker', function () {
).toHaveTextContent('Constraints not satisfied');
expect(document.activeElement).toBe(picker);
- await selectTester.selectOption({option: 'One'});
+ await selectTester.toggleOptionSelection({option: 'One'});
expect(picker).not.toHaveAttribute('aria-describedby');
});
@@ -2177,7 +2177,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
let input = document.querySelector('[name=picker]');
expect(picker).not.toHaveAttribute('aria-describedby');
expect(input.validity.valid).toBe(false);
@@ -2192,7 +2192,7 @@ describe('Picker', function () {
).toHaveTextContent('Invalid value');
expect(document.activeElement).toBe(picker);
- await selectTester.selectOption({option: 'One'});
+ await selectTester.toggleOptionSelection({option: 'One'});
expect(picker).not.toHaveAttribute('aria-describedby');
});
@@ -2228,7 +2228,7 @@ describe('Picker', function () {
let {getByTestId} = render();
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
let input = document.querySelector('[name=picker]');
expect(picker).not.toHaveAttribute('aria-describedby');
@@ -2240,7 +2240,7 @@ describe('Picker', function () {
).toHaveTextContent('Invalid value.');
expect(input.validity.valid).toBe(false);
- await selectTester.selectOption({option: 'One'});
+ await selectTester.toggleOptionSelection({option: 'One'});
expect(picker).not.toHaveAttribute('aria-describedby');
expect(input.validity.valid).toBe(true);
});
@@ -2299,7 +2299,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
let input = document.querySelector('[name=picker]');
expect(input).toHaveAttribute('required');
expect(picker).not.toHaveAttribute('aria-describedby');
@@ -2314,7 +2314,7 @@ describe('Picker', function () {
document.getElementById(picker.getAttribute('aria-describedby'))
).toHaveTextContent('Constraints not satisfied');
- await selectTester.selectOption({option: 'One'});
+ await selectTester.toggleOptionSelection({option: 'One'});
expect(picker).not.toHaveAttribute('aria-describedby');
await user.click(getByTestId('reset'));
@@ -2341,7 +2341,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
let input = document.querySelector('[name=picker]');
expect(picker).toHaveAttribute('aria-describedby');
expect(
@@ -2349,7 +2349,7 @@ describe('Picker', function () {
).toHaveTextContent('Invalid value');
expect(input.validity.valid).toBe(true);
- await selectTester.selectOption({option: 'One'});
+ await selectTester.toggleOptionSelection({option: 'One'});
expect(picker).not.toHaveAttribute('aria-describedby');
});
@@ -2366,13 +2366,13 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.getTrigger();
expect(picker).toHaveAttribute('aria-describedby');
expect(
document.getElementById(picker.getAttribute('aria-describedby'))
).toHaveTextContent('Invalid value');
- await selectTester.selectOption({option: 'One'});
+ await selectTester.toggleOptionSelection({option: 'One'});
expect(picker).not.toHaveAttribute('aria-describedby');
});
});
diff --git a/packages/@adobe/react-spectrum/test/picker/TempUtilTest.test.js b/packages/@adobe/react-spectrum/test/picker/TempUtilTest.test.js
index 7e465438f0c..943441ac6b9 100644
--- a/packages/@adobe/react-spectrum/test/picker/TempUtilTest.test.js
+++ b/packages/@adobe/react-spectrum/test/picker/TempUtilTest.test.js
@@ -134,8 +134,8 @@ describe('Picker/Select ', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')});
- await selectTester.selectOption({option: 'Three'});
- expect(selectTester.trigger).toHaveTextContent('Three');
+ await selectTester.toggleOptionSelection({option: 'Three'});
+ expect(selectTester.getTrigger()).toHaveTextContent('Three');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('three');
});
@@ -160,8 +160,8 @@ describe('Picker/Select ', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')});
- await selectTester.selectOption({option: 'Cat'});
- expect(selectTester.trigger).toHaveTextContent('Cat');
+ await selectTester.toggleOptionSelection({option: 'Cat'});
+ expect(selectTester.getTrigger()).toHaveTextContent('Cat');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('cat');
});
@@ -247,8 +247,8 @@ describe('Picker/Select ', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')});
- await selectTester.selectOption({option: 'Three'});
- expect(selectTester.trigger).toHaveTextContent('Three');
+ await selectTester.toggleOptionSelection({option: 'Three'});
+ expect(selectTester.getTrigger()).toHaveTextContent('Three');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('three');
});
@@ -275,8 +275,8 @@ describe('Picker/Select ', function () {
let selectTester = testUtilUser.createTester('Select', {
root: screen.getAllByTestId('test')[0]
});
- await selectTester.selectOption({option: 'Cat'});
- expect(selectTester.trigger).toHaveTextContent('Cat');
+ await selectTester.toggleOptionSelection({option: 'Cat'});
+ expect(selectTester.getTrigger()).toHaveTextContent('Cat');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('cat');
});
@@ -306,7 +306,7 @@ describe('Picker/Select ', function () {
let selectTester = testUtilUser.createTester('Select', {root: screen.getByTestId('test')});
await selectTester.open();
- expect(await screen.findByTestId('tray')).toContainElement(selectTester.listbox);
+ expect(await screen.findByTestId('tray')).toContainElement(selectTester.getListbox());
});
});
});
diff --git a/packages/@adobe/react-spectrum/test/radio/Radio.test.js b/packages/@adobe/react-spectrum/test/radio/Radio.test.js
index 43e093a08a3..6a668365f13 100644
--- a/packages/@adobe/react-spectrum/test/radio/Radio.test.js
+++ b/packages/@adobe/react-spectrum/test/radio/Radio.test.js
@@ -1152,21 +1152,21 @@ describe('Radios', function () {
root: getByRole('radiogroup'),
direction
});
- let radios = radioGroupTester.radios;
+ let radios = radioGroupTester.getRadios();
await radioGroupTester.triggerRadio({radio: radios[0]});
expect(radios[0]).toBeChecked();
await radioGroupTester.triggerRadio({radio: 4, interactionType: 'keyboard'});
expect(radios[4]).toBeChecked();
- let radio4 = radioGroupTester.findRadio({radioIndexOrText: 3});
+ let radio4 = radioGroupTester.findRadio({indexOrText: 3});
await radioGroupTester.triggerRadio({radio: radio4, interactionType: 'keyboard'});
expect(radios[3]).toBeChecked();
await radioGroupTester.triggerRadio({radio: 'Dogs', interactionType: 'mouse'});
expect(radios[0]).toBeChecked();
- let radio5 = radioGroupTester.findRadio({radioIndexOrText: 'Chocobo'});
+ let radio5 = radioGroupTester.findRadio({indexOrText: 'Chocobo'});
await radioGroupTester.triggerRadio({radio: radio5, interactionType: 'mouse'});
expect(radios[4]).toBeChecked();
}
diff --git a/packages/@adobe/react-spectrum/test/table/TableTests.js b/packages/@adobe/react-spectrum/test/table/TableTests.js
index 82ba1d79183..741796f395f 100644
--- a/packages/@adobe/react-spectrum/test/table/TableTests.js
+++ b/packages/@adobe/react-spectrum/test/table/TableTests.js
@@ -2327,15 +2327,15 @@ export let tableTests = () => {
tableTester.setInteractionType('keyboard');
await tableTester.toggleRowSelection({row: 0});
- expect(tableTester.selectedRows).toHaveLength(1);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
expect(onSelectionChange).toHaveBeenCalledTimes(1);
await tableTester.toggleRowSelection({row: 1});
- expect(tableTester.selectedRows).toHaveLength(2);
+ expect(tableTester.getSelectedRows()).toHaveLength(2);
expect(onSelectionChange).toHaveBeenCalledTimes(2);
await user.keyboard('{Escape}');
- expect(tableTester.selectedRows).toHaveLength(2);
+ expect(tableTester.getSelectedRows()).toHaveLength(2);
expect(onSelectionChange).toHaveBeenCalledTimes(2);
});
@@ -2751,12 +2751,12 @@ export let tableTests = () => {
checkSelectAll(tree, 'unchecked');
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
checkRowSelection(rows.slice(1), false);
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(tableTester.rows.length);
+ expect(tableTester.getSelectedRows()).toHaveLength(tableTester.getRows().length);
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange.mock.calls[0][0]).toEqual('all');
checkRowSelection(rows.slice(1), true);
@@ -4897,7 +4897,7 @@ export let tableTests = () => {
let tree = render();
let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')});
tableTester.setInteractionType('keyboard');
- let columnheaders = tableTester.columns;
+ let columnheaders = tableTester.getColumns();
expect(columnheaders).toHaveLength(3);
expect(columnheaders[0]).not.toHaveAttribute('aria-sort');
expect(columnheaders[1]).not.toHaveAttribute('aria-sort');
diff --git a/packages/@adobe/react-spectrum/test/tabs/Tabs.test.js b/packages/@adobe/react-spectrum/test/tabs/Tabs.test.js
index 5f09f928321..b6583399e24 100644
--- a/packages/@adobe/react-spectrum/test/tabs/Tabs.test.js
+++ b/packages/@adobe/react-spectrum/test/tabs/Tabs.test.js
@@ -86,11 +86,11 @@ describe('Tabs', function () {
let container = renderComponent();
let tabsTester = testUtilUser.createTester('Tabs', {root: container.getByRole('tablist')});
- let tablist = tabsTester.tablist;
+ let tablist = tabsTester.getTablist();
expect(tablist).toBeTruthy();
expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.getTabs();
expect(tabs.length).toBe(3);
for (let tab of tabs) {
@@ -98,15 +98,15 @@ describe('Tabs', function () {
expect(tab).toHaveAttribute('aria-selected');
let isSelected = tab.getAttribute('aria-selected') === 'true';
if (isSelected) {
- expect(tab).toBe(tabsTester.selectedTab);
+ expect(tab).toBe(tabsTester.getSelectedTab());
expect(tab).toHaveAttribute('aria-controls');
let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
expect(tabpanel).toBeTruthy();
expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
expect(tabpanel).toHaveAttribute('role', 'tabpanel');
expect(tabpanel).toHaveTextContent(defaultItems[0].children);
- expect(tabpanel).toBe(tabsTester.activeTabpanel);
- expect(tabsTester.tabpanels).toHaveLength(1);
+ expect(tabpanel).toBe(tabsTester.getActiveTabpanel());
+ expect(tabsTester.getTabpanels()).toHaveLength(1);
}
}
});
@@ -163,7 +163,7 @@ describe('Tabs', function () {
interactionType: 'keyboard',
direction: 'rtl'
});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.getTabs();
window.addEventListener('keydown', onKeyDown);
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
@@ -1330,23 +1330,23 @@ describe('Tabs', function () {
let direction = props.locale === 'ar-AE' ? 'rtl' : 'ltr';
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist'), direction});
- expect(tabsTester.tablist).toHaveAttribute('aria-orientation', props.orientation);
- let tabs = tabsTester.tabs;
+ expect(tabsTester.getTablist()).toHaveAttribute('aria-orientation', props.orientation);
+ let tabs = tabsTester.getTabs();
await tabsTester.triggerTab({tab: tabs[0]});
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 4, interactionType: 'keyboard'});
- expect(tabsTester.selectedTab).toBe(tabs[4]);
- let tab4 = tabsTester.findTab({tabIndexOrText: 3});
+ expect(tabsTester.getSelectedTab()).toBe(tabs[4]);
+ let tab4 = tabsTester.findTab({indexOrText: 3});
await tabsTester.triggerTab({tab: tab4, interactionType: 'keyboard'});
- expect(tabsTester.selectedTab).toBe(tabs[3]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[3]);
await tabsTester.triggerTab({tab: 'Tab 1', interactionType: 'mouse'});
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[0]);
- let tab5 = tabsTester.findTab({tabIndexOrText: 'Tab 5'});
+ let tab5 = tabsTester.findTab({indexOrText: 'Tab 5'});
await tabsTester.triggerTab({tab: tab5, interactionType: 'mouse'});
- expect(tabsTester.selectedTab).toBe(tabs[4]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[4]);
}
);
});
diff --git a/packages/@adobe/react-spectrum/test/tree/TreeView.test.tsx b/packages/@adobe/react-spectrum/test/tree/TreeView.test.tsx
index 3edb6ef6199..a255deb0b26 100644
--- a/packages/@adobe/react-spectrum/test/tree/TreeView.test.tsx
+++ b/packages/@adobe/react-spectrum/test/tree/TreeView.test.tsx
@@ -428,7 +428,7 @@ describe('Tree', () => {
it('should support dynamic trees', () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows).toHaveLength(20);
// Check the rough structure to make sure dynamic rows are rendering as expected (just checks the expandable rows and their attributes)
@@ -498,10 +498,10 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- expect(treeTester.tree).toHaveAttribute('aria-multiselectable', 'true');
- let rows = treeTester.rows;
+ expect(treeTester.getTree()).toHaveAttribute('aria-multiselectable', 'true');
+ let rows = treeTester.getRows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -515,8 +515,8 @@ describe('Tree', () => {
expect(row2).toHaveAttribute('data-selected', 'true');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Projects-1']));
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row2);
let row1 = rows[1];
await treeTester.toggleRowSelection({row: row1});
@@ -526,8 +526,8 @@ describe('Tree', () => {
expect(row2).not.toHaveAttribute('data-selected');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Projects']));
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row1);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row1);
});
it('should prevent Esc from clearing selection if escapeKeyBehavior is "none"', async () => {
@@ -535,12 +535,12 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
let row1 = rows[1];
await treeTester.toggleRowSelection({row: row1});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['Projects']));
- expect(treeTester.selectedRows).toHaveLength(1);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
let row2 = rows[2];
await treeTester.toggleRowSelection({row: row2});
@@ -548,11 +548,11 @@ describe('Tree', () => {
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(
new Set(['Projects', 'Projects-1'])
);
- expect(treeTester.selectedRows).toHaveLength(2);
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
await user.keyboard('{Escape}');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
- expect(treeTester.selectedRows).toHaveLength(2);
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
});
it('should render a chevron for an expandable row marked with hasChildItems', () => {
@@ -763,7 +763,7 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
await treeTester.triggerRowAction({row: rows[0]});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenLastCalledWith('Photos');
@@ -772,7 +772,7 @@ describe('Tree', () => {
// Due to disabledBehavior being set to 'all' this expandable row has its action disabled
let disabledRow = rows[1];
expect(disabledRow).toHaveAttribute('data-disabled', 'true');
- await treeTester.triggerRowAction({row: disabledRow});
+ await expect(treeTester.triggerRowAction({row: disabledRow})).rejects.toThrow();
expect(onAction).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenCalledTimes(0);
@@ -809,9 +809,9 @@ describe('Tree', () => {
root: getByRole('treegrid'),
interactionType: type as 'keyboard' | 'mouse' | 'touch'
});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -830,8 +830,8 @@ describe('Tree', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['Projects-1']));
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row2);
let row1 = rows[1];
await treeTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
@@ -845,8 +845,8 @@ describe('Tree', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['Projects']));
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row1);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row1);
await treeTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
expect(row1).toHaveAttribute('aria-selected', 'false');
@@ -859,7 +859,7 @@ describe('Tree', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set([]));
- expect(treeTester.selectedRows).toHaveLength(0);
+ expect(treeTester.getSelectedRows()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
@@ -871,9 +871,9 @@ describe('Tree', () => {
root: getByRole('treegrid'),
interactionType: type as 'keyboard' | 'mouse' | 'touch'
});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -891,15 +891,15 @@ describe('Tree', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['Photos', 'Projects-1'])
);
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(treeTester.selectedRows[1]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(treeTester.getSelectedRows()[1]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['Projects-1'])
);
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row2);
}
let row1 = rows[1];
@@ -913,17 +913,17 @@ describe('Tree', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['Photos', 'Projects-1', 'Projects'])
);
- expect(treeTester.selectedRows).toHaveLength(3);
- expect(treeTester.selectedRows[1]).toBe(row1);
- expect(treeTester.selectedRows[2]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(3);
+ expect(treeTester.getSelectedRows()[1]).toBe(row1);
+ expect(treeTester.getSelectedRows()[2]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['Projects-1', 'Projects'])
);
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(treeTester.selectedRows[0]).toBe(row1);
- expect(treeTester.selectedRows[1]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(treeTester.getSelectedRows()[0]).toBe(row1);
+ expect(treeTester.getSelectedRows()[1]).toBe(row2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -937,15 +937,15 @@ describe('Tree', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['Photos', 'Projects-1'])
);
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(treeTester.selectedRows[1]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(treeTester.getSelectedRows()[1]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['Projects-1'])
);
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row2);
}
});
@@ -958,9 +958,9 @@ describe('Tree', () => {
root: getByRole('treegrid'),
interactionType: type as 'keyboard' | 'mouse' | 'touch'
});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -979,8 +979,8 @@ describe('Tree', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['Projects-1']));
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row2);
let row1 = rows[1];
await treeTester.toggleRowSelection({row: row1});
@@ -995,8 +995,8 @@ describe('Tree', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['Projects']));
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row1);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row1);
// pressing without modifier keys won't deselect the row
await treeTester.toggleRowSelection({row: row1});
@@ -1007,7 +1007,7 @@ describe('Tree', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(treeTester.selectedRows).toHaveLength(1);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(row1).toHaveAttribute('aria-selected', 'true');
@@ -1018,16 +1018,16 @@ describe('Tree', () => {
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(
new Set(['Projects', 'Projects-1'])
);
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(treeTester.selectedRows[0]).toBe(row1);
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(treeTester.getSelectedRows()[0]).toBe(row1);
await treeTester.toggleRowSelection({row: row1});
expect(row1).toHaveAttribute('aria-selected', 'false');
expect(row1).not.toHaveAttribute('data-selected');
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls[2][0])).toEqual(new Set(['Projects-1']));
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(treeTester.selectedRows[0]).toBe(row2);
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(treeTester.getSelectedRows()[0]).toBe(row2);
}
});
});
@@ -1262,7 +1262,7 @@ describe('Tree', () => {
it('should expand/collapse a row when clicking/using Enter on the row itself and there arent any other primary actions', async () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows).toHaveLength(20);
await user.tab();
@@ -1292,7 +1292,7 @@ describe('Tree', () => {
expect(new Set(onExpandedChange.mock.calls[0][0])).toEqual(
new Set(['Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(9);
await treeTester.toggleRowExpansion({
@@ -1318,7 +1318,7 @@ describe('Tree', () => {
'Reports-1AB'
])
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(20);
await user.keyboard('{ArrowDown}');
@@ -1353,7 +1353,7 @@ describe('Tree', () => {
expect(new Set(onExpandedChange.mock.calls[2][0])).toEqual(
new Set(['Projects', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(17);
// Check behavior of onExpandedChange when a nested row is already closed and the parent is collapsed
@@ -1368,7 +1368,7 @@ describe('Tree', () => {
expect(new Set(onExpandedChange.mock.calls[3][0])).toEqual(
new Set(['Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(9);
// Check that the nested collapsed row is still closed when the parent is reexpanded
@@ -1381,7 +1381,7 @@ describe('Tree', () => {
expect(new Set(onExpandedChange.mock.calls[4][0])).toEqual(
new Set(['Projects', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB'])
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(17);
});
@@ -1661,16 +1661,16 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let tree = treeTester.tree;
+ let tree = treeTester.getTree();
expect(tree).toHaveAttribute('data-empty', 'true');
expect(tree).not.toHaveAttribute('data-focused');
expect(tree).not.toHaveAttribute('data-focus-visible');
- let row = treeTester.rows[0];
+ let row = treeTester.getRows()[0];
expect(row).toHaveAttribute('aria-level', '1');
expect(row).not.toHaveAttribute('aria-posinset');
expect(row).not.toHaveAttribute('aria-setsize');
- let gridCell = treeTester.cells({element: row})[0];
+ let gridCell = treeTester.getCells({element: row})[0];
expect(gridCell).toHaveTextContent('No resultsNo results found.');
await user.tab();
diff --git a/packages/@react-aria/test-utils/README.md b/packages/@react-aria/test-utils/README.md
index 25125080234..fbeb3e7fab9 100644
--- a/packages/@react-aria/test-utils/README.md
+++ b/packages/@react-aria/test-utils/README.md
@@ -1,3 +1,73 @@
# @react-aria/test-utils
This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
+
+See the [React Aria testing docs](https://react-aria.adobe.com/testing#react-aria-test-utils) for usage.
+
+`@react-aria/test-utils` is a set of testing utilities that aims to make writing unit tests easier for consumers of React Aria or for users who have built their own components following the respective ARIA pattern specification.
+
+> **Requirements:** This library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). You need to be on React 18+ for these utilities to work.
+
+## Installation
+
+```
+npm install @react-aria/test-utils --dev
+```
+
+## Setup
+
+Initialize a `User` object at the top of your test file, and use it to create an ARIA pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
+
+```ts
+// YourTest.test.ts
+import {screen} from '@testing-library/react';
+import {User} from '@react-aria/test-utils';
+
+// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers.
+// 'interactionType' specifies what mode of interaction should be simulated by the tester
+// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press)
+let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime});
+// ...
+
+it('my test case', async function () {
+ // Render your test component/app
+ render();
+ // Initialize the table tester via providing the 'Table' pattern name and the root element of said table
+ let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')});
+
+ // ...
+});
+```
+
+## User API
+
+```ts
+class User {
+ constructor(opts?: {
+ interactionType?: 'mouse' | 'keyboard' | 'touch',
+ advanceTimer?: (time?: number) => void | Promise
+ });
+
+ createTester(patternName, opts): PatternTester;
+}
+```
+
+- `interactionType` — default modality used by testers created from this `User`. Individual testers can override this via `setInteractionType` or per-method options.
+- `advanceTimer` — used by testers to advance timers for interactions like long press. Pass `jest.advanceTimersByTime` (or your test framework's equivalent) when using fake timers.
+- `createTester(patternName, opts)` — returns a tester for the given ARIA pattern. `opts.root` is the root element of the component under test.
+
+## Patterns
+
+Below is a list of the ARIA patterns supported by `createTester`. See the accompanying component testing docs pages on the [React Aria docs site](https://react-aria.adobe.com/testing#react-aria-test-utils) for sample usage of each tester in a test suite.
+
+- [CheckboxGroup](https://react-aria.adobe.com/CheckboxGroup/testing)
+- [ComboBox](https://react-aria.adobe.com/ComboBox/testing)
+- Dialog via [Modal](https://react-aria.adobe.com/Modal/testing) / [Popover](https://react-aria.adobe.com/Popover/testing)
+- [GridList](https://react-aria.adobe.com/GridList/testing)
+- [ListBox](https://react-aria.adobe.com/ListBox/testing)
+- [Menu](https://react-aria.adobe.com/Menu/testing)
+- [RadioGroup](https://react-aria.adobe.com/RadioGroup/testing)
+- [Select](https://react-aria.adobe.com/Select/testing)
+- [Table](https://react-aria.adobe.com/Table/testing)
+- [Tabs](https://react-aria.adobe.com/Tabs/testing)
+- [Tree](https://react-aria.adobe.com/Tree/testing)
diff --git a/packages/@react-aria/test-utils/package.json b/packages/@react-aria/test-utils/package.json
index 97d965df7e6..e6c700f5910 100644
--- a/packages/@react-aria/test-utils/package.json
+++ b/packages/@react-aria/test-utils/package.json
@@ -33,7 +33,7 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
},
"peerDependencies": {
- "@testing-library/react": "^16.0.0",
+ "@testing-library/dom": "^10.0.0",
"@testing-library/user-event": "^14.0.0",
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
diff --git a/packages/@react-aria/test-utils/src/act.ts b/packages/@react-aria/test-utils/src/act.ts
new file mode 100644
index 00000000000..9c9a47f4300
--- /dev/null
+++ b/packages/@react-aria/test-utils/src/act.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import * as React from 'react';
+import * as ReactDOMTestUtils from 'react-dom/test-utils';
+
+let actImpl: typeof ReactDOMTestUtils.act;
+if (typeof React.act === 'function') {
+ actImpl = React.act;
+} else {
+ actImpl = ReactDOMTestUtils.act;
+}
+
+export const act: typeof actImpl = fn => {
+ // only wrap in act if in test environment, breaks vite browser test if test utils are used otherwise
+ if (
+ // @ts-ignore
+ typeof IS_REACT_ACT_ENVIRONMENT === 'boolean'
+ ? // @ts-ignore
+ IS_REACT_ACT_ENVIRONMENT
+ : typeof jest !== 'undefined'
+ ) {
+ return actImpl(fn);
+ }
+ return fn();
+};
diff --git a/packages/@react-aria/test-utils/src/checkboxgroup.ts b/packages/@react-aria/test-utils/src/checkboxgroup.ts
index d36ad75fe10..774ceda4aeb 100644
--- a/packages/@react-aria/test-utils/src/checkboxgroup.ts
+++ b/packages/@react-aria/test-utils/src/checkboxgroup.ts
@@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, within} from '@testing-library/react';
+import {act} from './act';
import {CheckboxGroupTesterOpts, UserOpts} from './types';
-import {pressElement} from './events';
+import {formatTargetNode, pressElement} from './utils';
+import {within} from '@testing-library/dom';
interface TriggerCheckboxOptions {
/**
@@ -53,14 +54,14 @@ export class CheckboxGroupTester {
/**
* Returns a checkbox matching the specified index or text content.
*/
- findCheckbox(opts: {checkboxIndexOrText: number | string}): HTMLElement {
- let {checkboxIndexOrText} = opts;
+ findCheckbox(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let checkbox;
- if (typeof checkboxIndexOrText === 'number') {
- checkbox = this.checkboxes[checkboxIndexOrText];
- } else if (typeof checkboxIndexOrText === 'string') {
- let label = within(this.checkboxgroup).getByText(checkboxIndexOrText);
+ if (typeof indexOrText === 'number') {
+ checkbox = this.getCheckboxes()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ let label = within(this.getCheckboxGroup()).getByText(indexOrText);
// Label may wrap the checkbox, or the actual label may be a sibling span, or the checkbox div could have the label within it
if (label) {
@@ -81,7 +82,7 @@ export class CheckboxGroupTester {
private async keyboardNavigateToCheckbox(opts: {checkbox: HTMLElement}) {
let {checkbox} = opts;
- let checkboxes = this.checkboxes;
+ let checkboxes = this.getCheckboxes();
checkboxes = checkboxes.filter(
checkbox =>
!(checkbox.hasAttribute('disabled') || checkbox.getAttribute('aria-disabled') === 'true')
@@ -97,7 +98,7 @@ export class CheckboxGroupTester {
throw new Error('Checkbox provided is not in the checkbox group.');
}
- if (!this.checkboxgroup.contains(document.activeElement)) {
+ if (!this.getCheckboxGroup().contains(document.activeElement)) {
act(() => checkboxes[0].focus());
}
@@ -119,13 +120,15 @@ export class CheckboxGroupTester {
let {checkbox, interactionType = this._interactionType} = opts;
if (typeof checkbox === 'string' || typeof checkbox === 'number') {
- checkbox = this.findCheckbox({checkboxIndexOrText: checkbox});
+ checkbox = this.findCheckbox({indexOrText: checkbox});
}
if (!checkbox) {
- throw new Error('Target checkbox not found in the checkboxgroup.');
+ throw new Error(
+ `Target checkbox "${formatTargetNode(opts.checkbox)}" not found in the checkboxgroup.`
+ );
} else if (checkbox.hasAttribute('disabled')) {
- throw new Error('Target checkbox is disabled.');
+ throw new Error(`Target checkbox "${formatTargetNode(opts.checkbox)}" is disabled.`);
}
if (interactionType === 'keyboard') {
@@ -139,22 +142,22 @@ export class CheckboxGroupTester {
/**
* Returns the checkboxgroup.
*/
- get checkboxgroup(): HTMLElement {
+ getCheckboxGroup(): HTMLElement {
return this._checkboxgroup;
}
/**
* Returns the checkboxes.
*/
- get checkboxes(): HTMLElement[] {
- return within(this.checkboxgroup).queryAllByRole('checkbox');
+ getCheckboxes(): HTMLElement[] {
+ return within(this.getCheckboxGroup()).queryAllByRole('checkbox');
}
/**
* Returns the currently selected checkboxes in the checkboxgroup if any.
*/
- get selectedCheckboxes(): HTMLElement[] {
- return this.checkboxes.filter(
+ getSelectedCheckboxes(): HTMLElement[] {
+ return this.getCheckboxes().filter(
checkbox =>
(checkbox as HTMLInputElement).checked || checkbox.getAttribute('aria-checked') === 'true'
);
diff --git a/packages/@react-aria/test-utils/src/combobox.ts b/packages/@react-aria/test-utils/src/combobox.ts
index 5d67e78e0e2..04d3b4e033a 100644
--- a/packages/@react-aria/test-utils/src/combobox.ts
+++ b/packages/@react-aria/test-utils/src/combobox.ts
@@ -10,8 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, waitFor, within} from '@testing-library/react';
+import {act} from './act';
import {ComboBoxTesterOpts, UserOpts} from './types';
+import {formatTargetNode} from './utils';
+import {waitFor, within} from '@testing-library/dom';
interface ComboBoxOpenOpts {
/**
@@ -29,9 +31,15 @@ interface ComboBoxOpenOpts {
interface ComboBoxSelectOpts extends ComboBoxOpenOpts {
/**
- * The index, text, or node of the option to select. Option nodes can be sourced via `options()`.
+ * The index, text, or node of the option to select. Option nodes can be sourced via
+ * `getOptions()`.
*/
option: number | string | HTMLElement;
+ /**
+ * Whether or not the combobox closes on selection. Defaults to `true` for single select
+ * comboboxes and `false` for multi-select comboboxes.
+ */
+ closesOnSelect?: boolean;
}
export class ComboBoxTester {
@@ -83,8 +91,8 @@ export class ComboBoxTester {
*/
async open(opts: ComboBoxOpenOpts = {}): Promise {
let {triggerBehavior = 'manual', interactionType = this._interactionType} = opts;
- let trigger = this.trigger;
- let combobox = this.combobox;
+ let trigger = this.getTrigger();
+ let combobox = this.getCombobox();
let isDisabled = trigger!.hasAttribute('disabled');
if (interactionType === 'mouse') {
@@ -93,8 +101,8 @@ export class ComboBoxTester {
} else {
await this.user.click(trigger);
}
- } else if (interactionType === 'keyboard' && this._trigger != null) {
- act(() => this._trigger!.focus());
+ } else if (interactionType === 'keyboard') {
+ act(() => combobox.focus());
if (triggerBehavior !== 'focus') {
await this.user.keyboard('{ArrowDown}');
}
@@ -126,72 +134,101 @@ export class ComboBoxTester {
/**
* Returns an option matching the specified index or text content.
*/
- findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
- let {optionIndexOrText} = opts;
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let option;
- let options = this.options();
- let listbox = this.listbox;
-
- if (typeof optionIndexOrText === 'number') {
- option = options[optionIndexOrText];
- } else if (typeof optionIndexOrText === 'string' && listbox != null) {
- option = within(listbox!)
- .getByText(optionIndexOrText)
- .closest('[role=option]')! as HTMLElement;
+ let options = this.getOptions();
+ let listbox = this.getListbox();
+
+ if (typeof indexOrText === 'number') {
+ option = options[indexOrText];
+ } else if (typeof indexOrText === 'string' && listbox != null) {
+ option = within(listbox!).getByText(indexOrText).closest('[role=option]')! as HTMLElement;
}
return option;
}
+ private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
+ let {option} = opts;
+ let combobox = this.getCombobox();
+ let options = this.getOptions();
+ let targetIndex = options.findIndex(opt => opt === option || opt.contains(option));
+ if (targetIndex === -1) {
+ throw new Error('Option provided is not in the combobox listbox.');
+ }
+
+ let getCurrentIndex = () => {
+ let id = combobox.getAttribute('aria-activedescendant');
+ if (!id) {
+ return -1;
+ }
+ return options.findIndex(opt => opt.id === id);
+ };
+
+ if (getCurrentIndex() === -1) {
+ await this.user.keyboard('[ArrowDown]');
+ }
+
+ let currIndex = getCurrentIndex();
+ if (currIndex === -1) {
+ throw new Error('Could not determine the current option in the combobox listbox.');
+ }
+
+ let direction = targetIndex > currIndex ? 'down' : 'up';
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
+ await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
+ }
+ }
+
/**
- * 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.
+ * Toggles the selection of the desired combobox option if possible. 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): Promise {
- let {option, triggerBehavior, interactionType = this._interactionType} = opts;
- if (!this.combobox.getAttribute('aria-controls')) {
+ async toggleOptionSelection(opts: ComboBoxSelectOpts): Promise {
+ let {option, triggerBehavior, interactionType = this._interactionType, closesOnSelect} = opts;
+ if (!this.getCombobox().getAttribute('aria-controls')) {
await this.open({triggerBehavior});
}
- let listbox = this.listbox;
+ let listbox = this.getListbox();
if (!listbox) {
throw new Error("Combobox's listbox not found.");
}
- if (listbox) {
- if (typeof option === 'string' || typeof option === 'number') {
- option = this.findOption({optionIndexOrText: option});
- }
+ if (typeof option === 'string' || typeof option === 'number') {
+ option = this.findOption({indexOrText: option});
+ }
- if (!option) {
- throw new Error('Target option not found in the listbox.');
- }
+ if (!option) {
+ throw new Error(`Target option "${formatTargetNode(opts.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
- // the required amount of times to reach the option. For now just click the option even in keyboard mode
- if (interactionType === 'mouse' || interactionType === 'keyboard') {
- await this.user.click(option);
- } else {
- await this.user.pointer({target: option, keys: '[TouchA]'});
- }
+ let isMultiSelect = listbox.getAttribute('aria-multiselectable') === 'true';
+ closesOnSelect = closesOnSelect ?? !isMultiSelect;
- 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'
- );
- } else {
- return true;
- }
- });
- }
+ if (interactionType === 'keyboard') {
+ await this.keyboardNavigateToOption({option});
+ await this.user.keyboard('[Enter]');
+ } else if (interactionType === 'mouse') {
+ await this.user.click(option);
} else {
- throw new Error(
- "Attempted to select a option in the combobox, but the listbox wasn't found."
- );
+ await this.user.pointer({target: option, keys: '[TouchA]'});
+ }
+
+ if (closesOnSelect && 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'
+ );
+ } else {
+ return true;
+ }
+ });
}
}
@@ -199,9 +236,9 @@ export class ComboBoxTester {
* Closes the combobox dropdown.
*/
async close(): Promise {
- let listbox = this.listbox;
+ let listbox = this.getListbox();
if (listbox) {
- act(() => this.combobox.focus());
+ act(() => this.getCombobox().focus());
await this.user.keyboard('[Escape]');
await waitFor(() => {
@@ -219,30 +256,30 @@ export class ComboBoxTester {
/**
* Returns the combobox.
*/
- get combobox(): HTMLElement {
+ getCombobox(): HTMLElement {
return this._combobox;
}
/**
* Returns the combobox trigger button.
*/
- get trigger(): HTMLElement {
+ getTrigger(): HTMLElement {
return this._trigger;
}
/**
* Returns the combobox's listbox if present.
*/
- get listbox(): HTMLElement | null {
- let listBoxId = this.combobox.getAttribute('aria-controls');
+ getListbox(): HTMLElement | null {
+ let listBoxId = this.getCombobox().getAttribute('aria-controls');
return listBoxId ? document.getElementById(listBoxId) || null : null;
}
/**
* Returns the combobox's sections if present.
*/
- get sections(): HTMLElement[] {
- let listbox = this.listbox;
+ getSections(): HTMLElement[] {
+ let listbox = this.getListbox();
return listbox ? within(listbox).queryAllByRole('group') : [];
}
@@ -250,8 +287,8 @@ export class ComboBoxTester {
* 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;
+ getOptions(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ let {element = this.getListbox()} = opts;
let options = [];
if (element) {
options = within(element).queryAllByRole('option');
@@ -263,8 +300,8 @@ export class ComboBoxTester {
/**
* Returns the currently focused option in the combobox's dropdown if any.
*/
- get focusedOption(): HTMLElement | null {
- let focusedOptionId = this.combobox.getAttribute('aria-activedescendant');
+ getFocusedOption(): HTMLElement | null {
+ let focusedOptionId = this.getCombobox().getAttribute('aria-activedescendant');
return focusedOptionId ? document.getElementById(focusedOptionId) : null;
}
}
diff --git a/packages/@react-aria/test-utils/src/dialog.ts b/packages/@react-aria/test-utils/src/dialog.ts
index 3f3795fe4c4..17a2c0a21ef 100644
--- a/packages/@react-aria/test-utils/src/dialog.ts
+++ b/packages/@react-aria/test-utils/src/dialog.ts
@@ -10,8 +10,9 @@
* governing permissions and limitations under the License.
*/
-import {act, waitFor, within} from '@testing-library/react';
+import {act} from './act';
import {DialogTesterOpts, UserOpts} from './types';
+import {waitFor, within} from '@testing-library/dom';
interface DialogOpenOpts {
/**
@@ -34,13 +35,17 @@ export class DialogTester {
this._interactionType = interactionType || 'mouse';
this._overlayType = overlayType || 'modal';
- // Handle case where element provided is a wrapper of the trigger button
- let trigger = within(root).queryByRole('button');
- if (trigger) {
- this._trigger = trigger;
+ // Handle case where element provided is a wrapper of the trigger button.
+ let buttons = within(root).queryAllByRole('button');
+ let triggerButton: HTMLElement | undefined;
+ if (buttons.length === 0) {
+ triggerButton = root;
+ } else if (buttons.length === 1) {
+ triggerButton = buttons[0];
} else {
- this._trigger = root;
+ triggerButton = buttons.find(button => button.hasAttribute('aria-haspopup'));
}
+ this._trigger = triggerButton ?? root;
}
/**
@@ -55,7 +60,7 @@ export class DialogTester {
*/
async open(opts: DialogOpenOpts = {}): Promise {
let {interactionType = this._interactionType} = opts;
- let trigger = this.trigger;
+ let trigger = this.getTrigger();
if (!trigger.hasAttribute('disabled')) {
if (interactionType === 'mouse') {
await this.user.click(trigger);
@@ -133,7 +138,7 @@ export class DialogTester {
/**
* Returns the dialog's trigger.
*/
- get trigger(): HTMLElement {
+ getTrigger(): HTMLElement {
if (!this._trigger) {
throw new Error('No trigger element found for dialog.');
}
@@ -144,7 +149,7 @@ export class DialogTester {
/**
* Returns the dialog if present.
*/
- get dialog(): HTMLElement | null {
+ getDialog(): HTMLElement | null {
return this._dialog && document.contains(this._dialog) ? this._dialog : null;
}
}
diff --git a/packages/@react-aria/test-utils/src/gridlist.ts b/packages/@react-aria/test-utils/src/gridlist.ts
index 1d26138b8cc..dd09b154b2c 100644
--- a/packages/@react-aria/test-utils/src/gridlist.ts
+++ b/packages/@react-aria/test-utils/src/gridlist.ts
@@ -10,9 +10,16 @@
* governing permissions and limitations under the License.
*/
-import {act, within} from '@testing-library/react';
-import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
-import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types';
+import {act} from './act';
+import {
+ Direction,
+ GridListTesterOpts,
+ GridRowActionOpts,
+ ToggleGridRowOpts,
+ UserOpts
+} from './types';
+import {formatTargetNode, getAltKey, getMetaKey, pressElement, triggerLongPress} from './utils';
+import {within} from '@testing-library/dom';
interface GridListToggleRowOpts extends ToggleGridRowOpts {}
interface GridListRowActionOpts extends GridRowActionOpts {}
@@ -21,14 +28,24 @@ export class GridListTester {
private user;
private _interactionType: UserOpts['interactionType'];
private _advanceTimer: UserOpts['advanceTimer'];
+ private _direction: Direction;
private _gridlist: HTMLElement;
+ private _layout: GridListTesterOpts['layout'];
constructor(opts: GridListTesterOpts) {
- let {root, user, interactionType, advanceTimer} = opts;
+ let {root, user, interactionType, advanceTimer, direction, layout} = opts;
this.user = user;
this._interactionType = interactionType || 'mouse';
this._advanceTimer = advanceTimer;
+ this._direction = direction || 'ltr';
+ this._layout = layout || 'stack';
this._gridlist = root;
+ if (root.getAttribute('role') !== 'grid') {
+ let gridlist = within(root).queryByRole('grid');
+ if (gridlist) {
+ this._gridlist = gridlist;
+ }
+ }
}
/**
@@ -41,30 +58,31 @@ export class GridListTester {
/**
* Returns a row matching the specified index or text content.
*/
- findRow(opts: {rowIndexOrText: number | string}): HTMLElement {
- let {rowIndexOrText} = opts;
+ findRow(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let row;
- if (typeof rowIndexOrText === 'number') {
- row = this.rows[rowIndexOrText];
- } else if (typeof rowIndexOrText === 'string') {
- row = within(this.gridlist!).getByText(rowIndexOrText).closest('[role=row]')! as HTMLElement;
+ if (typeof indexOrText === 'number') {
+ row = this.getRows()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ row = within(this.getGridlist()!)
+ .getByText(indexOrText)
+ .closest('[role=row]')! as HTMLElement;
}
return row;
}
- // TODO: RTL
private async keyboardNavigateToRow(opts: {
row: HTMLElement;
selectionOnNav?: 'default' | 'none';
}) {
let {row, selectionOnNav = 'default'} = opts;
let altKey = getAltKey();
- let rows = this.rows;
+ let rows = this.getRows();
let targetIndex = rows.indexOf(row);
if (targetIndex === -1) {
- throw new Error('Option provided is not in the gridlist');
+ throw new Error('Row provided is not in the gridlist');
}
if (
@@ -74,6 +92,7 @@ export class GridListTester {
act(() => this._gridlist.focus());
}
+ let focusPrevKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
if (document.activeElement === this._gridlist) {
await this.user.keyboard(
`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`
@@ -83,20 +102,42 @@ export class GridListTester {
document.activeElement!.getAttribute('role') !== 'row'
) {
do {
- await this.user.keyboard('[ArrowLeft]');
+ await this.user.keyboard(`[${focusPrevKey}]`);
} while (document.activeElement!.getAttribute('role') !== 'row');
}
let currIndex = rows.indexOf(document.activeElement as HTMLElement);
if (currIndex === -1) {
throw new Error('ActiveElement is not in the gridlist');
}
- let direction = targetIndex > currIndex ? 'down' : 'up';
if (selectionOnNav === 'none') {
await this.user.keyboard(`[${altKey}>]`);
}
- for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
- await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
+ if (this._layout === 'grid') {
+ while (document.activeElement !== row) {
+ let curr = (document.activeElement as HTMLElement).getBoundingClientRect();
+ let target = row.getBoundingClientRect();
+ let key: string;
+ // basically compare current position with desired position to determine if we need to go up/down/left/right
+ // use 1 in the comparison here for subpixels since getBoundingClientRect returns subpixels precision
+ if (Math.abs(curr.top - target.top) > 1) {
+ key = curr.top < target.top ? 'ArrowDown' : 'ArrowUp';
+ } else if (Math.abs(curr.left - target.left) > 1) {
+ key = curr.left < target.left ? 'ArrowRight' : 'ArrowLeft';
+ } else {
+ // if the diff in current vs desired is < 1 but it is claiming we arent focused on the target
+ // then we might be in a case where getBoundingClientRect isnt mocked
+ throw new Error(
+ 'Could not navigate to target row in grid layout. Did the test mock getBoundingClientRect?'
+ );
+ }
+ await this.user.keyboard(`[${key}]`);
+ }
+ } else {
+ let direction = targetIndex > currIndex ? 'down' : 'up';
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
+ await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
+ }
}
if (selectionOnNav === 'none') {
await this.user.keyboard(`[/${altKey}]`);
@@ -121,23 +162,20 @@ export class GridListTester {
let metaKey = getMetaKey();
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the gridlist.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the gridlist.`);
}
let rowCheckbox = within(row).queryByRole('checkbox');
- // TODO: we early return here because the checkbox/row can't be keyboard navigated to if the row is disabled usually
- // but we may to check for disabledBehavior (aka if the disable row gets skipped when keyboard navigating or not)
if (
- interactionType === 'keyboard' &&
- (rowCheckbox?.getAttribute('disabled') === '' ||
- row?.getAttribute('aria-disabled') === 'true')
+ rowCheckbox?.getAttribute('disabled') === '' ||
+ row?.getAttribute('aria-disabled') === 'true'
) {
- return;
+ throw new Error(`Cannot toggle selection on disabled row "${formatTargetNode(opts.row)}".`);
}
// this would be better than the check to do nothing in events.ts
@@ -161,14 +199,10 @@ export class GridListTester {
} else {
let cell = within(row).getAllByRole('gridcell')[0];
if (needsLongPress && interactionType === 'touch') {
- if (this._advanceTimer == null) {
- throw new Error('No advanceTimers provided for long press.');
- }
-
// Note that long press interactions with rows is strictly touch only for grid rows
await triggerLongPress({
element: cell,
- advanceTimer: this._advanceTimer,
+ advanceTimer: this._advanceTimer!,
pointerOpts: {pointerType: 'touch'}
});
} else {
@@ -183,8 +217,6 @@ export class GridListTester {
}
}
- // TODO: There is a more difficult use case where the row has/behaves as link, don't think we have a good way to determine that unless the
- // user specificlly tells us
/**
* Triggers the action for the specified gridlist row. Defaults to using the interaction type set
* on the gridlist tester.
@@ -193,20 +225,20 @@ export class GridListTester {
let {row, needsDoubleClick, interactionType = this._interactionType} = opts;
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the gridlist.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the gridlist.`);
+ }
+
+ if (row.getAttribute('aria-disabled') === 'true') {
+ throw new Error(`Cannot trigger row action on disabled row "${formatTargetNode(opts.row)}".`);
}
if (needsDoubleClick) {
await this.user.dblClick(row);
} else if (interactionType === 'keyboard') {
- if (row?.getAttribute('aria-disabled') === 'true') {
- return;
- }
-
await this.keyboardNavigateToRow({row, selectionOnNav: 'none'});
await this.user.keyboard('[Enter]');
} else {
@@ -217,30 +249,30 @@ export class GridListTester {
/**
* Returns the gridlist.
*/
- get gridlist(): HTMLElement {
+ getGridlist(): HTMLElement {
return this._gridlist;
}
/**
* Returns the gridlist's rows if any.
*/
- get rows(): HTMLElement[] {
- return within(this?.gridlist).queryAllByRole('row');
+ getRows(): HTMLElement[] {
+ return within(this.getGridlist()).queryAllByRole('row');
}
/**
* Returns the gridlist's selected rows if any.
*/
- get selectedRows(): HTMLElement[] {
- return this.rows.filter(row => row.getAttribute('aria-selected') === 'true');
+ getSelectedRows(): HTMLElement[] {
+ return this.getRows().filter(row => row.getAttribute('aria-selected') === 'true');
}
/**
* Returns the gridlist's cells if any. Can be filtered against a specific row if provided via
* `element`.
*/
- cells(opts: {element?: HTMLElement} = {}): HTMLElement[] {
- let {element = this.gridlist} = opts;
+ getCells(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ let {element = this.getGridlist()} = opts;
return within(element).queryAllByRole('gridcell');
}
}
diff --git a/packages/@react-aria/test-utils/src/index.ts b/packages/@react-aria/test-utils/src/index.ts
index 7ad297d0245..38c65998b4e 100644
--- a/packages/@react-aria/test-utils/src/index.ts
+++ b/packages/@react-aria/test-utils/src/index.ts
@@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
-export {triggerLongPress} from './events';
+export {triggerLongPress} from './utils';
export {installMouseEvent, installPointerEvent} from './testSetup';
export {pointerMap} from './userEventMaps';
export {User} from './user';
diff --git a/packages/@react-aria/test-utils/src/listbox.ts b/packages/@react-aria/test-utils/src/listbox.ts
index 12565e2f2d1..e196ab60264 100644
--- a/packages/@react-aria/test-utils/src/listbox.ts
+++ b/packages/@react-aria/test-utils/src/listbox.ts
@@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, within} from '@testing-library/react';
-import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
+import {act} from './act';
+import {formatTargetNode, getAltKey, getMetaKey, pressElement, triggerLongPress} from './utils';
import {ListBoxTesterOpts, UserOpts} from './types';
+import {within} from '@testing-library/dom';
interface ListBoxToggleOptionOpts {
/**
@@ -65,13 +66,21 @@ export class ListBoxTester {
private _interactionType: UserOpts['interactionType'];
private _advanceTimer: UserOpts['advanceTimer'];
private _listbox: HTMLElement;
+ private _layout: ListBoxTesterOpts['layout'];
constructor(opts: ListBoxTesterOpts) {
- let {root, user, interactionType, advanceTimer} = opts;
+ let {root, user, interactionType, advanceTimer, layout} = opts;
this.user = user;
this._interactionType = interactionType || 'mouse';
- this._listbox = root;
this._advanceTimer = advanceTimer;
+ this._layout = layout || 'stack';
+ this._listbox = root;
+ if (root.getAttribute('role') !== 'listbox') {
+ let listbox = within(root).queryByRole('listbox');
+ if (listbox) {
+ this._listbox = listbox;
+ }
+ }
}
/**
@@ -81,36 +90,33 @@ export class ListBoxTester {
this._interactionType = type;
}
- // TODO: now that we have listbox, perhaps select can make use of this tester internally
/**
* Returns a option matching the specified index or text content.
*/
- findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
- let {optionIndexOrText} = opts;
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let option;
- let options = this.options();
+ let options = this.getOptions();
- if (typeof optionIndexOrText === 'number') {
- option = options[optionIndexOrText];
- } else if (typeof optionIndexOrText === 'string') {
- option = within(this.listbox!)
- .getByText(optionIndexOrText)
+ if (typeof indexOrText === 'number') {
+ option = options[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ option = within(this.getListbox()!)
+ .getByText(indexOrText)
.closest('[role=option]')! as HTMLElement;
}
return option;
}
- // TODO: this is basically the same as menu except for the error message, refactor later so that they share
- // TODO: this also doesn't support grid layout yet
private async keyboardNavigateToOption(opts: {
option: HTMLElement;
selectionOnNav?: 'default' | 'none';
}) {
let {option, selectionOnNav = 'default'} = opts;
let altKey = getAltKey();
- let options = this.options();
+ let options = this.getOptions();
let targetIndex = options.indexOf(option);
if (targetIndex === -1) {
throw new Error('Option provided is not in the listbox');
@@ -131,12 +137,34 @@ export class ListBoxTester {
throw new Error('ActiveElement is not in the listbox');
}
- let direction = targetIndex > currIndex ? 'down' : 'up';
if (selectionOnNav === 'none') {
await this.user.keyboard(`[${altKey}>]`);
}
- for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
- await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
+ if (this._layout === 'grid') {
+ while (document.activeElement !== option) {
+ let curr = (document.activeElement as HTMLElement).getBoundingClientRect();
+ let target = option.getBoundingClientRect();
+ let key: string;
+ // compare current position with desired position to determine if we need to go up/down/left/right
+ // use 1 in the comparison here for subpixels since getBoundingClientRect returns subpixels precision
+ if (Math.abs(curr.top - target.top) > 1) {
+ key = curr.top < target.top ? 'ArrowDown' : 'ArrowUp';
+ } else if (Math.abs(curr.left - target.left) > 1) {
+ key = curr.left < target.left ? 'ArrowRight' : 'ArrowLeft';
+ } else {
+ // if the diff in current vs desired is < 1 but it is claiming we arent focused on the target
+ // then we might be in a case where getBoundingClientRect isnt mocked
+ throw new Error(
+ 'Could not navigate to target option in grid layout. Did the test mock getBoundingClientRect?'
+ );
+ }
+ await this.user.keyboard(`[${key}]`);
+ }
+ } else {
+ let direction = targetIndex > currIndex ? 'down' : 'up';
+ for (let i = 0; i < Math.abs(targetIndex - currIndex); i++) {
+ await this.user.keyboard(`[${direction === 'down' ? 'ArrowDown' : 'ArrowUp'}]`);
+ }
}
if (selectionOnNav === 'none') {
await this.user.keyboard(`[/${altKey}]`);
@@ -160,16 +188,18 @@ export class ListBoxTester {
let metaKey = getMetaKey();
if (typeof option === 'string' || typeof option === 'number') {
- option = this.findOption({optionIndexOrText: option});
+ option = this.findOption({indexOrText: option});
}
if (!option) {
- throw new Error('Target option not found in the listbox.');
+ throw new Error(`Target option "${formatTargetNode(opts.option)}" not found in the listbox.`);
}
if (interactionType === 'keyboard') {
if (option?.getAttribute('aria-disabled') === 'true') {
- return;
+ throw new Error(
+ `Cannot toggle selection on disabled option "${formatTargetNode(opts.option)}".`
+ );
}
await this.keyboardNavigateToOption({
@@ -185,13 +215,9 @@ export class ListBoxTester {
}
} else {
if (needsLongPress && interactionType === 'touch') {
- if (this._advanceTimer == null) {
- throw new Error('No advanceTimers provided for long press.');
- }
-
await triggerLongPress({
element: option,
- advanceTimer: this._advanceTimer,
+ advanceTimer: this._advanceTimer!,
pointerOpts: {pointerType: 'touch'}
});
} else {
@@ -214,18 +240,20 @@ export class ListBoxTester {
let {option, needsDoubleClick, interactionType = this._interactionType} = opts;
if (typeof option === 'string' || typeof option === 'number') {
- option = this.findOption({optionIndexOrText: option});
+ option = this.findOption({indexOrText: option});
}
if (!option) {
- throw new Error('Target option not found in the listbox.');
+ throw new Error(`Target option "${formatTargetNode(opts.option)}" not found in the listbox.`);
}
if (needsDoubleClick) {
await this.user.dblClick(option);
} else if (interactionType === 'keyboard') {
if (option?.getAttribute('aria-disabled') === 'true') {
- return;
+ throw new Error(
+ `Cannot trigger action on disabled option "${formatTargetNode(opts.option)}".`
+ );
}
await this.keyboardNavigateToOption({option, selectionOnNav: 'none'});
@@ -238,7 +266,7 @@ export class ListBoxTester {
/**
* Returns the listbox.
*/
- get listbox(): HTMLElement {
+ getListbox(): HTMLElement {
return this._listbox;
}
@@ -246,7 +274,7 @@ export class ListBoxTester {
* Returns the listbox options. Can be filtered to a subsection of the listbox if provided via
* `element`.
*/
- options(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ getOptions(opts: {element?: HTMLElement} = {}): HTMLElement[] {
let {element = this._listbox} = opts;
let options = [];
if (element) {
@@ -259,14 +287,14 @@ export class ListBoxTester {
/**
* Returns the listbox's selected options if any.
*/
- get selectedOptions(): HTMLElement[] {
- return this.options().filter(row => row.getAttribute('aria-selected') === 'true');
+ getSelectedOptions(): HTMLElement[] {
+ return this.getOptions().filter(row => row.getAttribute('aria-selected') === 'true');
}
/**
* Returns the listbox's sections if any.
*/
- get sections(): HTMLElement[] {
+ getSections(): HTMLElement[] {
return within(this._listbox).queryAllByRole('group');
}
}
diff --git a/packages/@react-aria/test-utils/src/menu.ts b/packages/@react-aria/test-utils/src/menu.ts
index 940c4412fe9..26876ed6c76 100644
--- a/packages/@react-aria/test-utils/src/menu.ts
+++ b/packages/@react-aria/test-utils/src/menu.ts
@@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, waitFor, within} from '@testing-library/react';
+import {act} from './act';
+import {formatTargetNode, triggerLongPress} from './utils';
import {MenuTesterOpts, UserOpts} from './types';
-import {triggerLongPress} from './events';
+import {waitFor, within} from '@testing-library/dom';
interface MenuOpenOpts {
/**
@@ -32,7 +33,8 @@ interface MenuOpenOpts {
interface MenuSelectOpts extends MenuOpenOpts {
/**
- * The index, text, or node of the option to select. Option nodes can be sourced via `options()`.
+ * The index, text, or node of the option to select. Option nodes can be sourced via
+ * `getOptions()`.
*/
option: number | string | HTMLElement;
/**
@@ -59,7 +61,7 @@ interface MenuSelectOpts extends MenuOpenOpts {
interface MenuOpenSubmenuOpts extends MenuOpenOpts {
/**
* The text or node of the submenu trigger to open. Available submenu trigger nodes can be sourced
- * via `submenuTriggers`.
+ * via `getSubmenuTriggers()`.
*/
submenuTrigger: string | HTMLElement;
}
@@ -82,13 +84,17 @@ export class MenuTester {
if (root.getAttribute('role') === 'menuitem') {
this._trigger = root;
} else {
- // Handle case where element provided is a wrapper of the trigger button
- let trigger = within(root).queryByRole('button');
- if (trigger) {
- this._trigger = trigger;
+ // Handle case where element provided is a wrapper of the trigger button.
+ let buttons = within(root).queryAllByRole('button');
+ let triggerButton: HTMLElement | undefined;
+ if (buttons.length === 0) {
+ triggerButton = root;
+ } else if (buttons.length === 1) {
+ triggerButton = buttons[0];
} else {
- this._trigger = root;
+ triggerButton = buttons.find(button => button.hasAttribute('aria-haspopup'));
}
+ this._trigger = triggerButton ?? root;
}
this._isSubmenu = isSubmenu || false;
@@ -102,24 +108,19 @@ export class MenuTester {
this._interactionType = type;
}
- // 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
- // One difference will be that it supports long press as well
/**
* Opens the menu. Defaults to using the interaction type set on the menu tester.
*/
async open(opts: MenuOpenOpts = {}): Promise {
let {needsLongPress, interactionType = this._interactionType, direction} = opts;
- let trigger = this.trigger;
+ let trigger = this.getTrigger();
let isDisabled = trigger.hasAttribute('disabled');
if (interactionType === 'mouse' || interactionType === 'touch') {
if (needsLongPress) {
- if (this._advanceTimer == null) {
- throw new Error('No advanceTimers provided for long press.');
- }
let pointerType = interactionType === 'mouse' ? 'mouse' : 'touch';
await triggerLongPress({
element: trigger,
- advanceTimer: this._advanceTimer,
+ advanceTimer: this._advanceTimer!,
pointerOpts: {pointerType}
});
} else if (interactionType === 'mouse') {
@@ -162,32 +163,30 @@ export class MenuTester {
/**
* Returns a option matching the specified index or text content.
*/
- findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
- let {optionIndexOrText} = opts;
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let option;
- let options = this.options();
- let menu = this.menu;
+ let options = this.getOptions();
+ let menu = this.getMenu();
- if (typeof optionIndexOrText === 'number') {
- option = options[optionIndexOrText];
- } else if (typeof optionIndexOrText === 'string' && menu != null) {
+ if (typeof indexOrText === 'number') {
+ option = options[indexOrText];
+ } else if (typeof indexOrText === 'string' && menu != null) {
option = within(menu!)
- .getByText(optionIndexOrText)
+ .getByText(indexOrText)
.closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]')! as HTMLElement;
}
return option;
}
- // TODO: also very similar to select, barring potential long press support
- // Close on select is also kinda specific?
/**
- * Selects the desired menu option. Defaults to using the interaction type set on the menu tester.
- * If necessary, will open the menu dropdown beforehand. The desired option can be targeted via
- * the option's node, the option's text, or the option's index.
+ * Toggles the selection of the desired menu option if possible. Defaults to using the interaction
+ * type set on the menu tester. If necessary, will open the menu 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: MenuSelectOpts): Promise {
+ async toggleOptionSelection(opts: MenuSelectOpts): Promise {
let {
menuSelectionMode = 'single',
needsLongPress,
@@ -196,13 +195,13 @@ export class MenuTester {
interactionType = this._interactionType,
keyboardActivation = 'Enter'
} = opts;
- let trigger = this.trigger;
+ let trigger = this.getTrigger();
if (!trigger.getAttribute('aria-controls') && !trigger.hasAttribute('aria-expanded')) {
await this.open({needsLongPress});
}
- let menu = this.menu;
+ let menu = this.getMenu();
if (!menu) {
throw new Error('Menu not found.');
@@ -210,16 +209,16 @@ export class MenuTester {
if (menu) {
if (typeof option === 'string' || typeof option === 'number') {
- option = this.findOption({optionIndexOrText: option});
+ option = this.findOption({indexOrText: option});
}
if (!option) {
- throw new Error('Target option not found in the menu.');
+ throw new Error(`Target option "${formatTargetNode(opts.option)}" not found in the menu.`);
}
if (interactionType === 'keyboard') {
if (option?.getAttribute('aria-disabled') === 'true') {
- return;
+ throw new Error(`Cannot select disabled option "${formatTargetNode(opts.option)}".`);
}
if (document.activeElement !== menu && !menu.contains(document.activeElement)) {
@@ -277,7 +276,7 @@ export class MenuTester {
if (this._isSubmenu) {
await waitFor(() => {
if (
- document.activeElement === this.trigger ||
+ document.activeElement === this.getTrigger() ||
this._rootMenu?.contains(document.activeElement)
) {
throw new Error(
@@ -305,71 +304,83 @@ export class MenuTester {
}
}
- // TODO: update this to remove needsLongPress if we wanna make the user call open first always
/**
* Opens the submenu. Defaults to using the interaction type set on the menu tester. The submenu
* trigger can be targeted via the trigger's node or the trigger's text.
*/
- async openSubmenu(opts: MenuOpenSubmenuOpts): Promise {
+ async openSubmenu(opts: MenuOpenSubmenuOpts): Promise {
let {submenuTrigger, needsLongPress, interactionType = this._interactionType} = opts;
- let trigger = this.trigger;
+ let trigger = this.getTrigger();
let isDisabled = trigger.hasAttribute('disabled');
- if (!trigger.getAttribute('aria-controls') && !isDisabled) {
+ if (isDisabled) {
+ throw new Error(
+ `Cannot open submenu because its parent menu's trigger "${formatTargetNode(trigger)}" is disabled.`
+ );
+ }
+ if (!trigger.getAttribute('aria-controls')) {
await this.open({needsLongPress});
}
- if (!isDisabled) {
- let menu = this.menu;
- if (menu) {
- if (typeof submenuTrigger === 'string') {
- submenuTrigger = within(menu!)
- .getByText(submenuTrigger)
- .closest('[role=menuitem]')! as HTMLElement;
- }
+ let menu = this.getMenu();
+ if (!menu) {
+ throw new Error(
+ `Cannot open submenu, parent menu didn't open on trigger "${formatTargetNode(trigger)}" press.`
+ );
+ }
+ if (typeof submenuTrigger === 'string') {
+ submenuTrigger = within(menu!)
+ .getByText(submenuTrigger)
+ .closest('[role=menuitem]')! as HTMLElement;
+ }
- let submenuTriggerTester = new MenuTester({
- user: this.user,
- interactionType: this._interactionType,
- root: submenuTrigger,
- isSubmenu: true,
- advanceTimer: this._advanceTimer,
- rootMenu: (this._isSubmenu ? this._rootMenu : this.menu) || undefined
- });
- if (interactionType === 'mouse') {
- await this.user.pointer({target: submenuTrigger});
- } else if (interactionType === 'keyboard') {
- await this.keyboardNavigateToOption({option: submenuTrigger});
- await this.user.keyboard('[ArrowRight]');
- } else {
- await submenuTriggerTester.open();
- }
+ if (
+ submenuTrigger.getAttribute('aria-disabled') === 'true' ||
+ submenuTrigger.hasAttribute('disabled')
+ ) {
+ throw new Error(
+ `Cannot open submenu because its trigger "${formatTargetNode(submenuTrigger)}" is disabled.`
+ );
+ }
- await waitFor(() => {
- if (submenuTriggerTester._trigger?.getAttribute('aria-expanded') !== 'true') {
- throw new Error(
- 'aria-expanded for the submenu trigger wasn\'t changed to "true", unable to confirm the existance of the submenu'
- );
- } else {
- return true;
- }
- });
+ let submenuTriggerTester = new MenuTester({
+ user: this.user,
+ interactionType: this._interactionType,
+ root: submenuTrigger,
+ isSubmenu: true,
+ advanceTimer: this._advanceTimer,
+ rootMenu: (this._isSubmenu ? this._rootMenu : this.getMenu()) || undefined
+ });
+ if (interactionType === 'mouse') {
+ await this.user.pointer({target: submenuTrigger});
+ } else if (interactionType === 'keyboard') {
+ await this.keyboardNavigateToOption({option: submenuTrigger});
+ await this.user.keyboard('[ArrowRight]');
+ } else {
+ await submenuTriggerTester.open();
+ }
- return submenuTriggerTester;
+ await waitFor(() => {
+ if (submenuTriggerTester._trigger?.getAttribute('aria-expanded') !== 'true') {
+ throw new Error(
+ 'aria-expanded for the submenu trigger wasn\'t changed to "true", unable to confirm the existance of the submenu'
+ );
+ } else {
+ return true;
}
- }
+ });
- return null;
+ return submenuTriggerTester;
}
private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
let {option} = opts;
- let options = this.options();
+ let options = this.getOptions();
let targetIndex = options.findIndex(opt => opt === option || opt.contains(option));
if (targetIndex === -1) {
throw new Error('Option provided is not in the menu');
}
- if (document.activeElement === this.menu) {
+ if (document.activeElement === this.getMenu()) {
await this.user.keyboard('[ArrowDown]');
}
let currIndex = options.indexOf(document.activeElement as HTMLElement);
@@ -387,13 +398,13 @@ export class MenuTester {
* Closes the menu.
*/
async close(): Promise {
- let menu = this.menu;
+ let menu = this.getMenu();
if (menu) {
act(() => menu.focus());
await this.user.keyboard('[Escape]');
await waitFor(() => {
- if (document.activeElement !== this.trigger) {
+ if (document.activeElement !== this.getTrigger()) {
throw new Error(
`Expected the document.activeElement after closing the menu to be the menu trigger but got ${document.activeElement}`
);
@@ -411,7 +422,7 @@ export class MenuTester {
/**
* Returns the menu's trigger.
*/
- get trigger(): HTMLElement {
+ getTrigger(): HTMLElement {
if (!this._trigger) {
throw new Error('No trigger element found for menu.');
}
@@ -422,16 +433,16 @@ export class MenuTester {
/**
* Returns the menu if present.
*/
- get menu(): HTMLElement | null {
- let menuId = this.trigger.getAttribute('aria-controls');
+ getMenu(): HTMLElement | null {
+ let menuId = this.getTrigger().getAttribute('aria-controls');
return menuId ? document.getElementById(menuId) : null;
}
/**
* Returns the menu's sections if any.
*/
- get sections(): HTMLElement[] {
- let menu = this.menu;
+ getSections(): HTMLElement[] {
+ let menu = this.getMenu();
if (menu) {
return within(menu).queryAllByRole('group');
} else {
@@ -443,27 +454,22 @@ export class MenuTester {
* Returns the menu's options if present. Can be filtered to a subsection of the menu if provided
* via `element`.
*/
- options(opts: {element?: HTMLElement} = {}): HTMLElement[] {
- let {element = this.menu} = opts;
- let options: HTMLElement[] = [];
- if (element) {
- options = within(element).queryAllByRole('menuitem');
- if (options.length === 0) {
- options = within(element).queryAllByRole('menuitemradio');
- if (options.length === 0) {
- options = within(element).queryAllByRole('menuitemcheckbox');
- }
- }
+ getOptions(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ let {element = this.getMenu()} = opts;
+ if (!element) {
+ return [];
}
- return options;
+ return Array.from(
+ element.querySelectorAll('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]')
+ ) as HTMLElement[];
}
/**
* Returns the menu's submenu triggers if any.
*/
- get submenuTriggers(): HTMLElement[] {
- let options = this.options();
+ getSubmenuTriggers(): HTMLElement[] {
+ let options = this.getOptions();
if (options.length > 0) {
return options.filter(item => item.getAttribute('aria-haspopup') != null);
}
diff --git a/packages/@react-aria/test-utils/src/radiogroup.ts b/packages/@react-aria/test-utils/src/radiogroup.ts
index deec38e3533..658be09f053 100644
--- a/packages/@react-aria/test-utils/src/radiogroup.ts
+++ b/packages/@react-aria/test-utils/src/radiogroup.ts
@@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, within} from '@testing-library/react';
+import {act} from './act';
import {Direction, Orientation, RadioGroupTesterOpts, UserOpts} from './types';
-import {pressElement} from './events';
+import {formatTargetNode, pressElement} from './utils';
+import {within} from '@testing-library/dom';
interface TriggerRadioOptions {
/**
@@ -55,14 +56,14 @@ export class RadioGroupTester {
/**
* Returns a radio matching the specified index or text content.
*/
- findRadio(opts: {radioIndexOrText: number | string}): HTMLElement {
- let {radioIndexOrText} = opts;
+ findRadio(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let radio;
- if (typeof radioIndexOrText === 'number') {
- radio = this.radios[radioIndexOrText];
- } else if (typeof radioIndexOrText === 'string') {
- let label = within(this.radiogroup).getByText(radioIndexOrText);
+ if (typeof indexOrText === 'number') {
+ radio = this.getRadios()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ let label = within(this.getRadioGroup()).getByText(indexOrText);
// Label may wrap the radio, or the actual label may be a sibling span, or the radio div could have the label within it
if (label) {
radio = within(label).queryByRole('radio');
@@ -82,7 +83,7 @@ export class RadioGroupTester {
private async keyboardNavigateToRadio(opts: {radio: HTMLElement; orientation?: Orientation}) {
let {radio, orientation = 'vertical'} = opts;
- let radios = this.radios;
+ let radios = this.getRadios();
radios = radios.filter(
radio => !(radio.hasAttribute('disabled') || radio.getAttribute('aria-disabled') === 'true')
);
@@ -97,8 +98,8 @@ export class RadioGroupTester {
throw new Error('Radio provided is not in the radio group.');
}
- if (!this.radiogroup.contains(document.activeElement)) {
- let selectedRadio = this.selectedRadio;
+ if (!this.getRadioGroup().contains(document.activeElement)) {
+ let selectedRadio = this.getSelectedRadio();
if (selectedRadio != null) {
act(() => selectedRadio.focus());
} else {
@@ -136,13 +137,15 @@ export class RadioGroupTester {
let {radio, interactionType = this._interactionType} = opts;
if (typeof radio === 'string' || typeof radio === 'number') {
- radio = this.findRadio({radioIndexOrText: radio});
+ radio = this.findRadio({indexOrText: radio});
}
if (!radio) {
- throw new Error('Target radio not found in the radio group.');
+ throw new Error(
+ `Target radio "${formatTargetNode(opts.radio)}" not found in the radio group.`
+ );
} else if (radio.hasAttribute('disabled')) {
- throw new Error('Target radio is disabled.');
+ throw new Error(`Target radio "${formatTargetNode(opts.radio)}" is disabled.`);
}
if (interactionType === 'keyboard') {
@@ -156,21 +159,21 @@ export class RadioGroupTester {
/**
* Returns the radiogroup.
*/
- get radiogroup(): HTMLElement {
+ getRadioGroup(): HTMLElement {
return this._radiogroup;
}
/**
* Returns the radios.
*/
- get radios(): HTMLElement[] {
- return within(this.radiogroup).queryAllByRole('radio');
+ getRadios(): HTMLElement[] {
+ return within(this.getRadioGroup()).queryAllByRole('radio');
}
/**
* Returns the currently selected radio in the radiogroup if any.
*/
- get selectedRadio(): HTMLElement | null {
- return this.radios.find(radio => (radio as HTMLInputElement).checked) || null;
+ getSelectedRadio(): HTMLElement | null {
+ return this.getRadios().find(radio => (radio as HTMLInputElement).checked) || null;
}
}
diff --git a/packages/@react-aria/test-utils/src/select.ts b/packages/@react-aria/test-utils/src/select.ts
index 1dadfd77dbf..42bdec41fcd 100644
--- a/packages/@react-aria/test-utils/src/select.ts
+++ b/packages/@react-aria/test-utils/src/select.ts
@@ -10,8 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, waitFor, within} from '@testing-library/react';
+import {act} from './act';
+import {formatTargetNode} from './utils';
import {SelectTesterOpts, UserOpts} from './types';
+import {waitFor, within} from '@testing-library/dom';
interface SelectOpenOpts {
/**
@@ -23,7 +25,8 @@ interface SelectOpenOpts {
interface SelectTriggerOptionOpts extends SelectOpenOpts {
/**
- * The index, text, or node of the option to select. Option nodes can be sourced via `options()`.
+ * The index, text, or node of the option to select. Option nodes can be sourced via
+ * `getOptions()`.
*/
option: number | string | HTMLElement;
/**
@@ -69,7 +72,7 @@ export class SelectTester {
*/
async open(opts: SelectOpenOpts = {}): Promise {
let {interactionType = this._interactionType} = opts;
- let trigger = this.trigger;
+ let trigger = this.getTrigger();
let isDisabled = trigger.hasAttribute('disabled');
if (interactionType === 'mouse') {
@@ -102,7 +105,7 @@ export class SelectTester {
* Closes the select.
*/
async close(): Promise {
- let listbox = this.listbox;
+ let listbox = this.getListbox();
if (listbox) {
act(() => listbox.focus());
await this.user.keyboard('[Escape]');
@@ -128,19 +131,17 @@ export class SelectTester {
/**
* Returns a option matching the specified index or text content.
*/
- findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
- let {optionIndexOrText} = opts;
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let option;
- let options = this.options();
- let listbox = this.listbox;
+ let options = this.getOptions();
+ let listbox = this.getListbox();
- if (typeof optionIndexOrText === 'number') {
- option = options[optionIndexOrText];
- } else if (typeof optionIndexOrText === 'string' && listbox != null) {
- option = within(listbox!)
- .getByText(optionIndexOrText)
- .closest('[role=option]')! as HTMLElement;
+ if (typeof indexOrText === 'number') {
+ option = options[indexOrText];
+ } else if (typeof indexOrText === 'string' && listbox != null) {
+ option = within(listbox!).getByText(indexOrText).closest('[role=option]')! as HTMLElement;
}
return option;
@@ -148,12 +149,12 @@ export class SelectTester {
private async keyboardNavigateToOption(opts: {option: HTMLElement}) {
let {option} = opts;
- let options = this.options();
+ let options = this.getOptions();
let targetIndex = options.indexOf(option);
if (targetIndex === -1) {
throw new Error('Option provided is not in the listbox');
}
- if (document.activeElement === this.listbox) {
+ if (document.activeElement === this.getListbox()) {
await this.user.keyboard('[ArrowDown]');
}
let currIndex = options.indexOf(document.activeElement as HTMLElement);
@@ -168,69 +169,67 @@ export class SelectTester {
}
/**
- * Selects the desired select option. Defaults to using the interaction type set on the select
- * tester. If necessary, will open the select dropdown beforehand. The desired option can be
- * targeted via the option's node, the option's text, or the option's index.
+ * Toggles the selection of the desired select option if possible. Defaults to using the
+ * interaction type set on the select tester. If necessary, will open the select 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: SelectTriggerOptionOpts): Promise {
+ async toggleOptionSelection(opts: SelectTriggerOptionOpts): Promise {
let {option, closesOnSelect, interactionType = this._interactionType} = opts || {};
- let trigger = this.trigger;
+ let trigger = this.getTrigger();
if (!trigger.getAttribute('aria-controls')) {
await this.open();
}
- let listbox = this.listbox;
+ let listbox = this.getListbox();
if (!listbox) {
throw new Error("Select's listbox not found.");
}
- if (listbox) {
- if (typeof option === 'string' || typeof option === 'number') {
- option = this.findOption({optionIndexOrText: option});
- }
+ if (typeof option === 'string' || typeof option === 'number') {
+ option = this.findOption({indexOrText: option});
+ }
- if (!option) {
- throw new Error('Target option not found in the listbox.');
- }
+ if (!option) {
+ throw new Error(`Target option "${formatTargetNode(opts.option)}" not found in the listbox.`);
+ }
- let isMultiSelect = listbox.getAttribute('aria-multiselectable') === 'true';
- let isSingleSelect = !isMultiSelect;
- closesOnSelect = closesOnSelect ?? isSingleSelect;
+ let isMultiSelect = listbox.getAttribute('aria-multiselectable') === 'true';
+ let isSingleSelect = !isMultiSelect;
+ closesOnSelect = closesOnSelect ?? isSingleSelect;
- if (interactionType === 'keyboard') {
- if (option?.getAttribute('aria-disabled') === 'true') {
- return;
- }
+ if (interactionType === 'keyboard') {
+ if (option?.getAttribute('aria-disabled') === 'true') {
+ throw new Error(`Cannot select disabled option "${formatTargetNode(opts.option)}".`);
+ }
- if (document.activeElement !== listbox && !listbox.contains(document.activeElement)) {
- act(() => listbox.focus());
- }
- await this.keyboardNavigateToOption({option});
- await this.user.keyboard('[Enter]');
+ if (document.activeElement !== listbox && !listbox.contains(document.activeElement)) {
+ act(() => listbox.focus());
+ }
+ await this.keyboardNavigateToOption({option});
+ await this.user.keyboard('[Enter]');
+ } else {
+ if (interactionType === 'mouse') {
+ await this.user.click(option);
} else {
- // TODO: what if the user needs to scroll the list to find the option? What if there are multiple matches for text (hopefully the picker options are pretty unique)
- if (interactionType === 'mouse') {
- await this.user.click(option);
- } else {
- await this.user.pointer({target: option, keys: '[TouchA]'});
- }
+ await this.user.pointer({target: option, keys: '[TouchA]'});
}
+ }
- if (closesOnSelect && option?.getAttribute('href') == null) {
- await waitFor(() => {
- if (document.activeElement !== this._trigger) {
- throw new Error(
- `Expected the document.activeElement after selecting an option to be the select component trigger but got ${document.activeElement}`
- );
- } else {
- return true;
- }
- });
-
- if (document.contains(listbox)) {
+ if (closesOnSelect && option?.getAttribute('href') == null) {
+ await waitFor(() => {
+ if (document.activeElement !== this._trigger) {
throw new Error(
- 'Expected select element listbox to not be in the document after selecting an option'
+ `Expected the document.activeElement after selecting an option to be the select component trigger but got ${document.activeElement}`
);
+ } else {
+ return true;
}
+ });
+
+ if (document.contains(listbox)) {
+ throw new Error(
+ 'Expected select element listbox to not be in the document after selecting an option'
+ );
}
}
}
@@ -239,8 +238,8 @@ export class SelectTester {
* Returns the select'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;
+ getOptions(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ let {element = this.getListbox()} = opts;
let options = [];
if (element) {
options = within(element).queryAllByRole('option');
@@ -252,23 +251,23 @@ export class SelectTester {
/**
* Returns the select's trigger.
*/
- get trigger(): HTMLElement {
+ getTrigger(): HTMLElement {
return this._trigger;
}
/**
* Returns the select's listbox if present.
*/
- get listbox(): HTMLElement | null {
- let listBoxId = this.trigger.getAttribute('aria-controls');
+ getListbox(): HTMLElement | null {
+ let listBoxId = this.getTrigger().getAttribute('aria-controls');
return listBoxId ? document.getElementById(listBoxId) : null;
}
/**
* Returns the select's sections if present.
*/
- get sections(): HTMLElement[] {
- let listbox = this.listbox;
+ getSections(): HTMLElement[] {
+ let listbox = this.getListbox();
return listbox ? within(listbox).queryAllByRole('group') : [];
}
}
diff --git a/packages/@react-aria/test-utils/src/table.ts b/packages/@react-aria/test-utils/src/table.ts
index 2541dbeb6c5..2fa0d2ddb48 100644
--- a/packages/@react-aria/test-utils/src/table.ts
+++ b/packages/@react-aria/test-utils/src/table.ts
@@ -10,15 +10,17 @@
* governing permissions and limitations under the License.
*/
-import {act, waitFor, within} from '@testing-library/react';
+import {act} from './act';
import {
BaseGridRowInteractionOpts,
+ Direction,
GridRowActionOpts,
TableTesterOpts,
ToggleGridRowOpts,
UserOpts
} from './types';
-import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
+import {formatTargetNode, getAltKey, getMetaKey, pressElement, triggerLongPress} from './utils';
+import {waitFor, within} from '@testing-library/dom';
interface TableToggleRowOpts extends ToggleGridRowOpts {}
interface TableToggleExpansionOpts extends BaseGridRowInteractionOpts {}
@@ -45,14 +47,23 @@ export class TableTester {
private user;
private _interactionType: UserOpts['interactionType'];
private _advanceTimer: UserOpts['advanceTimer'];
+ private _direction: Direction;
private _table: HTMLElement;
constructor(opts: TableTesterOpts) {
- let {root, user, interactionType, advanceTimer} = opts;
+ let {root, user, interactionType, advanceTimer, direction} = opts;
this.user = user;
this._interactionType = interactionType || 'mouse';
this._advanceTimer = advanceTimer;
+ this._direction = direction || 'ltr';
this._table = root;
+ let role = root.getAttribute('role');
+ if (role !== 'grid' && role !== 'treegrid') {
+ let table = within(root).queryByRole('grid') || within(root).queryByRole('treegrid');
+ if (table) {
+ this._table = table;
+ }
+ }
}
/**
@@ -62,14 +73,13 @@ export class TableTester {
this._interactionType = type;
}
- // TODO: RTL
private async keyboardNavigateToRow(opts: {
row: HTMLElement;
selectionOnNav?: 'default' | 'none';
}) {
let {row, selectionOnNav = 'default'} = opts;
let altKey = getAltKey();
- let rows = this.rows;
+ let rows = this.getRows();
let targetIndex = rows.indexOf(row);
if (targetIndex === -1) {
throw new Error('Row provided is not in the table');
@@ -84,20 +94,22 @@ export class TableTester {
await this.user.keyboard('[ArrowDown]');
}
+ let rowGroups = this.getRowGroups();
// 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
- if (this.rowGroups[0].contains(document.activeElement)) {
+ if (rowGroups[0].contains(document.activeElement)) {
do {
await this.user.keyboard('[ArrowDown]');
- } while (!this.rowGroups[1].contains(document.activeElement));
+ } while (!rowGroups[1].contains(document.activeElement));
}
// Move focus onto the row itself
+ let focusPrevKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
if (
- this.rowGroups[1].contains(document.activeElement) &&
+ rowGroups[1].contains(document.activeElement) &&
document.activeElement!.getAttribute('role') !== 'row'
) {
do {
- await this.user.keyboard('[ArrowLeft]');
+ await this.user.keyboard(`[${focusPrevKey}]`);
} while (document.activeElement!.getAttribute('role') !== 'row');
}
let currIndex = rows.indexOf(document.activeElement as HTMLElement);
@@ -130,19 +142,26 @@ export class TableTester {
selectionBehavior = 'toggle'
} = opts;
- let altKey = getMetaKey();
+ let altKey = getAltKey();
let metaKey = getMetaKey();
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the table.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the table.`);
}
let rowCheckbox = within(row).queryByRole('checkbox');
+ if (
+ rowCheckbox?.getAttribute('disabled') === '' ||
+ row.getAttribute('aria-disabled') === 'true'
+ ) {
+ throw new Error(`Cannot toggle selection on disabled row "${formatTargetNode(opts.row)}".`);
+ }
+
if (interactionType === 'keyboard' && (!checkboxSelection || !rowCheckbox)) {
await this.keyboardNavigateToRow({
row,
@@ -162,14 +181,10 @@ export class TableTester {
} else {
let cell = within(row).getAllByRole('gridcell')[0];
if (needsLongPress && interactionType === 'touch') {
- if (this._advanceTimer == null) {
- throw new Error('No advanceTimers provided for long press.');
- }
-
// Note that long press interactions with rows is strictly touch only for grid rows
await triggerLongPress({
element: cell,
- advanceTimer: this._advanceTimer,
+ advanceTimer: this._advanceTimer!,
pointerOpts: {pointerType: 'touch'}
});
} else {
@@ -190,38 +205,38 @@ export class TableTester {
*/
async toggleRowExpansion(opts: TableToggleExpansionOpts): Promise {
let {row, interactionType = this._interactionType} = opts;
- if (!this.table.contains(document.activeElement)) {
+ if (!this.getTable().contains(document.activeElement)) {
await act(async () => {
- this.table.focus();
+ this.getTable().focus();
});
}
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the table.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the table.`);
} else if (row.getAttribute('aria-expanded') == null) {
- throw new Error('Target row is not expandable.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" is not expandable.`);
+ }
+
+ if (row.getAttribute('aria-disabled') === 'true') {
+ throw new Error(`Cannot toggle expansion on disabled row "${formatTargetNode(opts.row)}".`);
}
if (interactionType === 'mouse' || interactionType === 'touch') {
let rowExpander = within(row).getAllByRole('button')[0]; // what happens if the button is not first? how can we differentiate?
await pressElement(this.user, rowExpander, interactionType);
} else if (interactionType === 'keyboard') {
- if (row?.getAttribute('aria-disabled') === 'true') {
- return;
- }
-
- // TODO: We always Use Option/Ctrl when keyboard navigating so selection isn't changed
- // in selectionmode="replace"/highlight selection when navigating to the row that the user wants
- // to expand. Discuss if this is useful or not
+ // note that our keyboard navigation makes sure selection isn't changes
await this.keyboardNavigateToRow({row});
+ let collapseKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
+ let expandKey = this._direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
if (row.getAttribute('aria-expanded') === 'true') {
- await this.user.keyboard('[ArrowLeft]');
+ await this.user.keyboard(`[${collapseKey}]`);
} else {
- await this.user.keyboard('[ArrowRight]');
+ await this.user.keyboard(`[${expandKey}]`);
}
}
}
@@ -235,9 +250,9 @@ export class TableTester {
let columnheader;
if (typeof column === 'number') {
- columnheader = this.columns[column];
+ columnheader = this.getColumns()[column];
} else if (typeof column === 'string') {
- columnheader = within(this.rowGroups[0]).getByText(column);
+ columnheader = within(this.getRowGroups()[0]).getByText(column);
while (columnheader && !/columnheader/.test(columnheader.getAttribute('role'))) {
columnheader = columnheader.parentElement;
}
@@ -304,12 +319,8 @@ export class TableTester {
}
// Handle cases where the table may transition in response to the row selection/deselection
- if (!this._advanceTimer) {
- throw new Error('No advanceTimers provided for table transition.');
- }
-
await act(async () => {
- await this._advanceTimer?.(200);
+ await this._advanceTimer!(200);
});
await waitFor(() => {
@@ -335,9 +346,9 @@ export class TableTester {
let columnheader;
if (typeof column === 'number') {
- columnheader = this.columns[column];
+ columnheader = this.getColumns()[column];
} else if (typeof column === 'string') {
- columnheader = within(this.rowGroups[0]).getByText(column);
+ columnheader = within(this.getRowGroups()[0]).getByText(column);
while (columnheader && !/columnheader/.test(columnheader.getAttribute('role'))) {
columnheader = columnheader.parentElement;
}
@@ -395,12 +406,8 @@ export class TableTester {
}
// Handle cases where the table may transition in response to the row selection/deselection
- if (!this._advanceTimer) {
- throw new Error('No advanceTimers provided for table transition.');
- }
-
await act(async () => {
- await this._advanceTimer?.(200);
+ await this._advanceTimer!(200);
});
await waitFor(() => {
@@ -425,11 +432,15 @@ export class TableTester {
let {row, needsDoubleClick, interactionType = this._interactionType} = opts;
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the table.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the table.`);
+ }
+
+ if (row.getAttribute('aria-disabled') === 'true') {
+ throw new Error(`Cannot trigger row action on disabled row "${formatTargetNode(opts.row)}".`);
}
if (needsDoubleClick) {
@@ -442,9 +453,6 @@ export class TableTester {
}
}
- // TODO: should there be utils for drag and drop and column resizing? For column resizing, I'm not entirely convinced that users will be doing that in their tests.
- // For DnD, it might be tricky to do for keyboard DnD since we wouldn't know what valid drop zones there are... Similarly, for simulating mouse drag and drop the coordinates depend
- // on the mocks the user sets up for their row height/etc.
// Additionally, should we also support keyboard navigation/typeahead? Those felt like they could be very easily replicated by the user via user.keyboard already and don't really
// add much value if we provide that to them
/**
@@ -453,11 +461,15 @@ export class TableTester {
*/
async toggleSelectAll(opts: {interactionType?: UserOpts['interactionType']} = {}): Promise {
let {interactionType = this._interactionType} = opts;
- let checkbox = within(this.table).getByLabelText('Select All');
if (interactionType === 'keyboard') {
- // TODO: using the .focus -> trigger keyboard Enter approach doesn't work for some reason, for now just trigger select all with click.
- await this.user.click(checkbox);
+ let metaKey = getMetaKey();
+ let table = this.getTable();
+ if (document.activeElement !== table && !table.contains(document.activeElement)) {
+ act(() => table.focus());
+ }
+ await this.user.keyboard(`[${metaKey}>]a[/${metaKey}]`);
} else {
+ let checkbox = within(this.getTable()).getByLabelText('Select All');
await pressElement(this.user, checkbox, interactionType);
}
}
@@ -465,16 +477,16 @@ export class TableTester {
/**
* Returns a row matching the specified index or text content.
*/
- findRow(opts: {rowIndexOrText: number | string}): HTMLElement {
- let {rowIndexOrText} = opts;
+ findRow(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let row;
- let rows = this.rows;
- let bodyRowGroup = this.rowGroups[1];
- if (typeof rowIndexOrText === 'number') {
- row = rows[rowIndexOrText];
- } else if (typeof rowIndexOrText === 'string') {
- row = within(bodyRowGroup).getByText(rowIndexOrText);
+ let rows = this.getRows();
+ let bodyRowGroup = this.getRowGroups()[1];
+ if (typeof indexOrText === 'number') {
+ row = rows[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ row = within(bodyRowGroup).getByText(indexOrText);
while (row && row.getAttribute('role') !== 'row') {
row = row.parentElement;
}
@@ -489,7 +501,7 @@ export class TableTester {
findCell(opts: {text: string}): HTMLElement {
let {text} = opts;
- let cell = within(this.table).getByText(text);
+ let cell = within(this.getTable()).getByText(text);
if (cell) {
while (cell && !/gridcell|rowheader|columnheader/.test(cell.getAttribute('role') || '')) {
if (cell.parentElement) {
@@ -506,14 +518,14 @@ export class TableTester {
/**
* Returns the table.
*/
- get table(): HTMLElement {
+ getTable(): HTMLElement {
return this._table;
}
/**
* Returns the row groups within the table.
*/
- get rowGroups(): HTMLElement[] {
+ getRowGroups(): HTMLElement[] {
let table = this._table;
return table ? within(table).queryAllByRole('rowgroup') : [];
}
@@ -521,38 +533,57 @@ export class TableTester {
/**
* Returns the columns within the table.
*/
- get columns(): HTMLElement[] {
- let headerRowGroup = this.rowGroups[0];
+ getColumns(): HTMLElement[] {
+ let headerRowGroup = this.getRowGroups()[0];
return headerRowGroup ? within(headerRowGroup).queryAllByRole('columnheader') : [];
}
/**
- * Returns the rows within the table if any.
+ * Returns the rows within the table if any. Can be filtered to a specific row group if provided
+ * via `element`.
*/
- get rows(): HTMLElement[] {
- return this.rowGroups.slice(1).flatMap(rowGroup => within(rowGroup).queryAllByRole('row'));
+ getRows(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ let {element} = opts;
+ if (element != null) {
+ return within(element).queryAllByRole('row');
+ }
+ return this.getRowGroups()
+ .slice(1)
+ .flatMap(rowGroup => within(rowGroup).queryAllByRole('row'));
}
/**
* Returns the currently selected rows within the table if any.
*/
- get selectedRows(): HTMLElement[] {
- return this.rows.filter(row => row.getAttribute('aria-selected') === 'true');
+ getSelectedRows(): HTMLElement[] {
+ return this.getRows().filter(row => row.getAttribute('aria-selected') === 'true');
+ }
+
+ /**
+ * Returns the footer rows within the table if any.
+ */
+ getFooterRows(): HTMLElement[] {
+ let rowGroups = this.getRowGroups();
+ if (rowGroups.length < 3) {
+ return [];
+ }
+ let footerRowGroup = rowGroups.at(-1);
+ return footerRowGroup ? within(footerRowGroup).queryAllByRole('row') : [];
}
/**
* Returns the row headers within the table if any.
*/
- get rowHeaders(): HTMLElement[] {
- return within(this.table).queryAllByRole('rowheader');
+ getRowHeaders(): HTMLElement[] {
+ return within(this.getTable()).queryAllByRole('rowheader');
}
/**
* Returns the cells within the table if any. Can be filtered against a specific row if provided
* via `element`.
*/
- cells(opts: {element?: HTMLElement} = {}): HTMLElement[] {
- let {element = this.table} = opts;
+ getCells(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ let {element = this.getTable()} = opts;
return within(element).queryAllByRole('gridcell');
}
}
diff --git a/packages/@react-aria/test-utils/src/tabs.ts b/packages/@react-aria/test-utils/src/tabs.ts
index d58ae129cc8..a7df7442c35 100644
--- a/packages/@react-aria/test-utils/src/tabs.ts
+++ b/packages/@react-aria/test-utils/src/tabs.ts
@@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, within} from '@testing-library/react';
+import {act} from './act';
import {Direction, Orientation, TabsTesterOpts, UserOpts} from './types';
-import {pressElement} from './events';
+import {formatTargetNode, pressElement} from './utils';
+import {within} from '@testing-library/dom';
interface TriggerTabOptions {
/**
@@ -56,28 +57,26 @@ export class TabsTester {
this._interactionType = type;
}
- // TODO: This is pretty similar across most the utils, refactor to make it generic?
/**
* Returns a tab matching the specified index or text content.
*/
- findTab(opts: {tabIndexOrText: number | string}): HTMLElement {
- let {tabIndexOrText} = opts;
+ findTab(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let tab;
- let tabs = this.tabs;
- if (typeof tabIndexOrText === 'number') {
- tab = tabs[tabIndexOrText];
- } else if (typeof tabIndexOrText === 'string') {
- tab = within(this._tablist).getByText(tabIndexOrText).closest('[role=tab]')! as HTMLElement;
+ let tabs = this.getTabs();
+ if (typeof indexOrText === 'number') {
+ tab = tabs[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ tab = within(this._tablist).getByText(indexOrText).closest('[role=tab]')! as HTMLElement;
}
return tab;
}
- // TODO: also quite similar across more utils albeit with orientation, refactor to make generic
private async keyboardNavigateToTab(opts: {tab: HTMLElement; orientation?: Orientation}) {
let {tab, orientation = 'vertical'} = opts;
- let tabs = this.tabs;
+ let tabs = this.getTabs();
tabs = tabs.filter(
tab => !(tab.hasAttribute('disabled') || tab.getAttribute('aria-disabled') === 'true')
);
@@ -93,7 +92,7 @@ export class TabsTester {
}
if (!this._tablist.contains(document.activeElement)) {
- let selectedTab = this.selectedTab;
+ let selectedTab = this.getSelectedTab();
if (selectedTab != null) {
act(() => selectedTab.focus());
} else {
@@ -131,13 +130,13 @@ export class TabsTester {
let {tab, interactionType = this._interactionType, manualActivation} = opts;
if (typeof tab === 'string' || typeof tab === 'number') {
- tab = this.findTab({tabIndexOrText: tab});
+ tab = this.findTab({indexOrText: tab});
}
if (!tab) {
- throw new Error('Target tab not found in the tablist.');
+ throw new Error(`Target tab "${formatTargetNode(opts.tab)}" not found in the tablist.`);
} else if (tab.hasAttribute('disabled')) {
- throw new Error('Target tab is disabled.');
+ throw new Error(`Target tab "${formatTargetNode(opts.tab)}" is disabled.`);
}
if (interactionType === 'keyboard') {
@@ -161,16 +160,16 @@ export class TabsTester {
/**
* Returns the tablist.
*/
- get tablist(): HTMLElement {
+ getTablist(): HTMLElement {
return this._tablist;
}
/**
* Returns the tabpanels.
*/
- get tabpanels(): HTMLElement[] {
+ getTabpanels(): HTMLElement[] {
let tabpanels = [] as HTMLElement[];
- for (let tab of this.tabs) {
+ for (let tab of this.getTabs()) {
let controlId = tab.getAttribute('aria-controls');
let panel = controlId != null ? document.getElementById(controlId) : null;
if (panel != null) {
@@ -184,22 +183,22 @@ export class TabsTester {
/**
* Returns the tabs in the tablist.
*/
- get tabs(): HTMLElement[] {
- return within(this.tablist).queryAllByRole('tab');
+ getTabs(): HTMLElement[] {
+ return within(this.getTablist()).queryAllByRole('tab');
}
/**
* Returns the currently selected tab in the tablist if any.
*/
- get selectedTab(): HTMLElement | null {
- return this.tabs.find(tab => tab.getAttribute('aria-selected') === 'true') || null;
+ getSelectedTab(): HTMLElement | null {
+ return this.getTabs().find(tab => tab.getAttribute('aria-selected') === 'true') || null;
}
/**
* Returns the currently active tabpanel if any.
*/
- get activeTabpanel(): HTMLElement | null {
- let activeTabpanelId = this.selectedTab?.getAttribute('aria-controls');
+ getActiveTabpanel(): HTMLElement | null {
+ let activeTabpanelId = this.getSelectedTab()?.getAttribute('aria-controls');
return activeTabpanelId ? document.getElementById(activeTabpanelId) : null;
}
}
diff --git a/packages/@react-aria/test-utils/src/tree.ts b/packages/@react-aria/test-utils/src/tree.ts
index 9cd06f73e9a..b5f899dac4d 100644
--- a/packages/@react-aria/test-utils/src/tree.ts
+++ b/packages/@react-aria/test-utils/src/tree.ts
@@ -10,37 +10,41 @@
* governing permissions and limitations under the License.
*/
-import {act, within} from '@testing-library/react';
+import {act} from './act';
import {
BaseGridRowInteractionOpts,
+ Direction,
GridRowActionOpts,
ToggleGridRowOpts,
TreeTesterOpts,
UserOpts
} from './types';
-import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
+import {formatTargetNode, getAltKey, getMetaKey, pressElement, triggerLongPress} from './utils';
+import {within} from '@testing-library/dom';
interface TreeToggleExpansionOpts extends BaseGridRowInteractionOpts {}
interface TreeToggleRowOpts extends ToggleGridRowOpts {}
interface TreeRowActionOpts extends GridRowActionOpts {}
-// TODO: this ended up being pretty much the same as gridlist, refactor so it extends from gridlist
export class TreeTester {
private user;
private _interactionType: UserOpts['interactionType'];
private _advanceTimer: UserOpts['advanceTimer'];
+ private _direction: Direction;
private _tree: HTMLElement;
constructor(opts: TreeTesterOpts) {
- let {root, user, interactionType, advanceTimer} = opts;
+ let {root, user, interactionType, advanceTimer, direction} = opts;
this.user = user;
this._interactionType = interactionType || 'mouse';
this._advanceTimer = advanceTimer;
+ this._direction = direction || 'ltr';
this._tree = root;
- // TODO: should all helpers do this?
- let tree = within(root).queryByRole('treegrid');
- if (root.getAttribute('role') !== 'treegrid' && tree) {
- this._tree = tree;
+ if (root.getAttribute('role') !== 'treegrid') {
+ let tree = within(root).queryByRole('treegrid');
+ if (tree) {
+ this._tree = tree;
+ }
}
}
@@ -54,37 +58,37 @@ export class TreeTester {
/**
* Returns a row matching the specified index or text content.
*/
- findRow(opts: {rowIndexOrText: number | string}): HTMLElement {
- let {rowIndexOrText} = opts;
+ findRow(opts: {indexOrText: number | string}): HTMLElement {
+ let {indexOrText} = opts;
let row;
- if (typeof rowIndexOrText === 'number') {
- row = this.rows[rowIndexOrText];
- } else if (typeof rowIndexOrText === 'string') {
- row = within(this.tree!).getByText(rowIndexOrText).closest('[role=row]')! as HTMLElement;
+ if (typeof indexOrText === 'number') {
+ row = this.getRows()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ row = within(this.getTree()!).getByText(indexOrText).closest('[role=row]')! as HTMLElement;
}
return row;
}
- // TODO: RTL
private async keyboardNavigateToRow(opts: {
row: HTMLElement;
selectionOnNav?: 'default' | 'none';
}) {
let {row, selectionOnNav = 'default'} = opts;
let altKey = getAltKey();
- let rows = this.rows;
+ let rows = this.getRows();
let targetIndex = rows.indexOf(row);
if (targetIndex === -1) {
- throw new Error('Option provided is not in the tree');
+ throw new Error('Row provided is not in the tree');
}
if (document.activeElement !== this._tree && !this._tree.contains(document.activeElement)) {
act(() => this._tree.focus());
}
- if (document.activeElement === this.tree) {
+ let focusPrevKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
+ if (document.activeElement === this.getTree()) {
await this.user.keyboard(
`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`
);
@@ -93,7 +97,7 @@ export class TreeTester {
document.activeElement!.getAttribute('role') !== 'row'
) {
do {
- await this.user.keyboard('[ArrowLeft]');
+ await this.user.keyboard(`[${focusPrevKey}]`);
} while (document.activeElement!.getAttribute('role') !== 'row');
}
let currIndex = rows.indexOf(document.activeElement as HTMLElement);
@@ -131,23 +135,20 @@ export class TreeTester {
let metaKey = getMetaKey();
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the tree.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the tree.`);
}
let rowCheckbox = within(row).queryByRole('checkbox');
- // TODO: we early return here because the checkbox can't be keyboard navigated to if the row is disabled usually
- // but we may to check for disabledBehavior (aka if the disable row gets skipped when keyboard navigating or not)
if (
- interactionType === 'keyboard' &&
- (rowCheckbox?.getAttribute('disabled') === '' ||
- row?.getAttribute('aria-disabled') === 'true')
+ rowCheckbox?.getAttribute('disabled') === '' ||
+ row?.getAttribute('aria-disabled') === 'true'
) {
- return;
+ throw new Error(`Cannot toggle selection on disabled row "${formatTargetNode(opts.row)}".`);
}
// this would be better than the check to do nothing in events.ts
@@ -171,18 +172,13 @@ export class TreeTester {
} else {
let cell = within(row).getAllByRole('gridcell')[0];
if (needsLongPress && interactionType === 'touch') {
- if (this._advanceTimer == null) {
- throw new Error('No advanceTimers provided for long press.');
- }
-
// Note that long press interactions with rows is strictly touch only for grid rows
await triggerLongPress({
element: cell,
- advanceTimer: this._advanceTimer,
+ advanceTimer: this._advanceTimer!,
pointerOpts: {pointerType: 'touch'}
});
} else {
- // TODO add modifiers here? Maybe move into pressElement if we get more cases for different types of modifier keys
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
await this.user.keyboard(`[${metaKey}>]`);
}
@@ -200,38 +196,35 @@ export class TreeTester {
*/
async toggleRowExpansion(opts: TreeToggleExpansionOpts): Promise {
let {row, interactionType = this._interactionType} = opts;
- if (!this.tree.contains(document.activeElement)) {
- await act(async () => {
- this.tree.focus();
- });
+ if (!this.getTree().contains(document.activeElement)) {
+ act(() => this.getTree().focus());
}
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the tree.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the tree.`);
} else if (row.getAttribute('aria-expanded') == null) {
- throw new Error('Target row is not expandable.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" is not expandable.`);
+ }
+
+ if (row.getAttribute('aria-disabled') === 'true') {
+ throw new Error(`Cannot toggle expansion on disabled row "${formatTargetNode(opts.row)}".`);
}
if (interactionType === 'mouse' || interactionType === 'touch') {
let rowExpander = within(row).getAllByRole('button')[0]; // what happens if the button is not first? how can we differentiate?
await pressElement(this.user, rowExpander, interactionType);
} else if (interactionType === 'keyboard') {
- if (row?.getAttribute('aria-disabled') === 'true') {
- return;
- }
-
- // TODO: We always Use Option/Ctrl when keyboard navigating so selection isn't changed
- // in selectionmode="replace"/highlight selection when navigating to the row that the user wants
- // to expand. Discuss if this is useful or not
await this.keyboardNavigateToRow({row});
+ let collapseKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
+ let expandKey = this._direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
if (row.getAttribute('aria-expanded') === 'true') {
- await this.user.keyboard('[ArrowLeft]');
+ await this.user.keyboard(`[${collapseKey}]`);
} else {
- await this.user.keyboard('[ArrowRight]');
+ await this.user.keyboard(`[${expandKey}]`);
}
}
}
@@ -244,22 +237,20 @@ export class TreeTester {
let {row, needsDoubleClick, interactionType = this._interactionType} = opts;
if (typeof row === 'string' || typeof row === 'number') {
- row = this.findRow({rowIndexOrText: row});
+ row = this.findRow({indexOrText: row});
}
if (!row) {
- throw new Error('Target row not found in the tree.');
+ throw new Error(`Target row "${formatTargetNode(opts.row)}" not found in the tree.`);
+ }
+
+ if (row.getAttribute('aria-disabled') === 'true') {
+ throw new Error(`Cannot trigger row action on disabled row "${formatTargetNode(opts.row)}".`);
}
if (needsDoubleClick) {
await this.user.dblClick(row);
} else if (interactionType === 'keyboard') {
- if (row?.getAttribute('aria-disabled') === 'true') {
- return;
- }
-
- // TODO: same as above, uses the modifier key to make sure we don't modify selection state on row focus
- // as we keyboard navigate to the row we want activate
await this.keyboardNavigateToRow({row});
await this.user.keyboard('[Enter]');
} else {
@@ -270,30 +261,30 @@ export class TreeTester {
/**
* Returns the tree.
*/
- get tree(): HTMLElement {
+ getTree(): HTMLElement {
return this._tree;
}
/**
* Returns the tree's rows if any.
*/
- get rows(): HTMLElement[] {
- return within(this?.tree).queryAllByRole('row');
+ getRows(): HTMLElement[] {
+ return within(this.getTree()).queryAllByRole('row');
}
/**
* Returns the tree's selected rows if any.
*/
- get selectedRows(): HTMLElement[] {
- return this.rows.filter(row => row.getAttribute('aria-selected') === 'true');
+ getSelectedRows(): HTMLElement[] {
+ return this.getRows().filter(row => row.getAttribute('aria-selected') === 'true');
}
/**
* Returns the tree's cells if any. Can be filtered against a specific row if provided via
* `element`.
*/
- cells(opts: {element?: HTMLElement} = {}): HTMLElement[] {
- let {element = this.tree} = opts;
+ getCells(opts: {element?: HTMLElement} = {}): HTMLElement[] {
+ let {element = this.getTree()} = opts;
return within(element).queryAllByRole('gridcell');
}
}
diff --git a/packages/@react-aria/test-utils/src/types.ts b/packages/@react-aria/test-utils/src/types.ts
index 727316debee..f4e15102863 100644
--- a/packages/@react-aria/test-utils/src/types.ts
+++ b/packages/@react-aria/test-utils/src/types.ts
@@ -38,6 +38,12 @@ export interface BaseTesterOpts extends UserOpts {
user?: any;
/** The base element for the given tester (e.g. the table, menu trigger button, etc). */
root: HTMLElement;
+ /**
+ * The horizontal layout direction, typically affected by locale.
+ *
+ * @default 'ltr'
+ */
+ direction?: Direction;
}
export interface CheckboxGroupTesterOpts extends BaseTesterOpts {}
@@ -67,13 +73,22 @@ export interface DialogTesterOpts extends BaseTesterOpts {
overlayType?: 'modal' | 'popover';
}
-export interface GridListTesterOpts extends BaseTesterOpts {}
+export interface GridListTesterOpts extends BaseTesterOpts {
+ /**
+ * The layout of the gridlist.
+ *
+ * @default 'stack'
+ */
+ layout?: 'stack' | 'grid';
+}
export interface ListBoxTesterOpts extends BaseTesterOpts {
/**
- * A function used by the test utils to advance timers during interactions.
+ * The layout of the listbox.
+ *
+ * @default 'stack'
*/
- advanceTimer?: UserOpts['advanceTimer'];
+ layout?: 'stack' | 'grid';
}
export interface MenuTesterOpts extends BaseTesterOpts {
@@ -91,14 +106,7 @@ export interface MenuTesterOpts extends BaseTesterOpts {
rootMenu?: HTMLElement;
}
-export interface RadioGroupTesterOpts extends BaseTesterOpts {
- /**
- * The horizontal layout direction, typically affected by locale.
- *
- * @default 'ltr'
- */
- direction?: Direction;
-}
+export interface RadioGroupTesterOpts extends BaseTesterOpts {}
export interface SelectTesterOpts extends BaseTesterOpts {
/**
@@ -109,28 +117,11 @@ export interface SelectTesterOpts extends BaseTesterOpts {
root: HTMLElement;
}
-export interface TableTesterOpts extends BaseTesterOpts {
- /**
- * A function used by the test utils to advance timers during interactions.
- */
- advanceTimer?: UserOpts['advanceTimer'];
-}
+export interface TableTesterOpts extends BaseTesterOpts {}
-export interface TabsTesterOpts extends BaseTesterOpts {
- /**
- * The horizontal layout direction, typically affected by locale.
- *
- * @default 'ltr'
- */
- direction?: Direction;
-}
+export interface TabsTesterOpts extends BaseTesterOpts {}
-export interface TreeTesterOpts extends BaseTesterOpts {
- /**
- * A function used by the test utils to advance timers during interactions.
- */
- advanceTimer?: UserOpts['advanceTimer'];
-}
+export interface TreeTesterOpts extends BaseTesterOpts {}
export interface BaseGridRowInteractionOpts {
/**
@@ -157,7 +148,6 @@ export interface ToggleGridRowOpts extends BaseGridRowInteractionOpts {
* @default 'true'
*/
checkboxSelection?: boolean;
- // TODO: this api feels a bit confusing tbh...
/**
* Whether the grid has a selectionBehavior of "toggle" or "replace" (aka highlight selection).
* This affects the user operations required to toggle row selection by adding modifier keys
@@ -165,7 +155,7 @@ export interface ToggleGridRowOpts extends BaseGridRowInteractionOpts {
* 'replace'" grid. If you would like to still simulate user actions (aka press) without these
* modifiers keys for a "selectionBehavior: replace" grid, simply omit this option. See the
* "Selection Behavior" section of the appropriate React Aria Component docs for more information
- * (e.g. https://react-spectrum.adobe.com/react-aria/Tree.html#selection-behavior).
+ * (e.g. https://react-aria.adobe.com/Tree#selection-and-actions).
*
* @default 'toggle'
*/
diff --git a/packages/@react-aria/test-utils/src/events.ts b/packages/@react-aria/test-utils/src/utils.ts
similarity index 67%
rename from packages/@react-aria/test-utils/src/events.ts
rename to packages/@react-aria/test-utils/src/utils.ts
index 7631cf5f203..d9d6e0e6747 100644
--- a/packages/@react-aria/test-utils/src/events.ts
+++ b/packages/@react-aria/test-utils/src/utils.ts
@@ -10,7 +10,8 @@
* governing permissions and limitations under the License.
*/
-import {act, fireEvent} from '@testing-library/react';
+import {act} from './act';
+import {fireEvent} from '@testing-library/dom';
import {UserOpts} from './types';
export const DEFAULT_LONG_PRESS_TIME = 500;
@@ -46,6 +47,13 @@ export function getMetaKey(): 'MetaLeft' | 'ControlLeft' {
return isMac() ? 'MetaLeft' : 'ControlLeft';
}
+export function formatTargetNode(value: number | string | HTMLElement): string {
+ if (typeof HTMLElement !== 'undefined' && value instanceof HTMLElement) {
+ return (value.cloneNode(false) as HTMLElement).outerHTML;
+ }
+ return String(value);
+}
+
/**
* Simulates a "long press" event on a element.
*
@@ -61,54 +69,68 @@ export async function triggerLongPress(opts: {
advanceTimer: (time: number) => unknown | Promise;
pointerOpts?: Record;
}): Promise {
- // TODO: note that this only works if the code from installPointerEvent is called somewhere in the test BEFORE the
- // 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
- // util before first render. Will need to document it well
let {element, advanceTimer, pointerOpts = {}} = opts;
let pointerType = pointerOpts.pointerType ?? 'mouse';
- let shouldFireCompatibilityEvents = fireEvent.pointerDown(element, {pointerType, ...pointerOpts});
+ let shouldFireCompatibilityEvents = false;
+ act(() => {
+ shouldFireCompatibilityEvents = fireEvent.pointerDown(element, {pointerType, ...pointerOpts});
+ });
let shouldFocus = true;
if (shouldFireCompatibilityEvents) {
if (pointerType === 'touch') {
- shouldFocus = shouldFireCompatibilityEvents = fireEvent.touchStart(element, {
- targetTouches: [
- {
- identifier: pointerOpts.pointerId,
- clientX: pointerOpts.clientX,
- clientY: pointerOpts.clientY
- }
- ]
+ act(() => {
+ shouldFocus = shouldFireCompatibilityEvents = fireEvent.touchStart(element, {
+ targetTouches: [
+ {
+ identifier: pointerOpts.pointerId,
+ clientX: pointerOpts.clientX,
+ clientY: pointerOpts.clientY
+ }
+ ]
+ });
});
} else if (pointerType === 'mouse') {
- shouldFocus = fireEvent.mouseDown(element, pointerOpts);
+ act(() => {
+ shouldFocus = fireEvent.mouseDown(element, pointerOpts);
+ });
if (shouldFocus) {
act(() => element.focus());
}
}
}
await act(async () => await advanceTimer(DEFAULT_LONG_PRESS_TIME));
- fireEvent.pointerUp(element, {pointerType, ...pointerOpts});
+ act(() => {
+ fireEvent.pointerUp(element, {pointerType, ...pointerOpts});
+ });
if (shouldFireCompatibilityEvents) {
if (pointerType === 'touch') {
- shouldFocus = fireEvent.touchEnd(element, {
- targetTouches: [
- {
- identifier: pointerOpts.pointerId,
- clientX: pointerOpts.clientX,
- clientY: pointerOpts.clientY
- }
- ]
+ act(() => {
+ shouldFocus = fireEvent.touchEnd(element, {
+ targetTouches: [
+ {
+ identifier: pointerOpts.pointerId,
+ clientX: pointerOpts.clientX,
+ clientY: pointerOpts.clientY
+ }
+ ]
+ });
+ shouldFocus = fireEvent.mouseDown(element, pointerOpts);
});
- shouldFocus = fireEvent.mouseDown(element, pointerOpts);
if (shouldFocus) {
act(() => element.focus());
}
- fireEvent.mouseUp(element, pointerOpts);
+ act(() => {
+ fireEvent.mouseUp(element, pointerOpts);
+ });
} else if (pointerType === 'mouse') {
- fireEvent.mouseUp(element, pointerOpts);
+ act(() => {
+ fireEvent.mouseUp(element, pointerOpts);
+ });
}
}
- fireEvent.click(element, {detail: 1, ...pointerOpts});
+ act(() => {
+ fireEvent.click(element, {detail: 1, ...pointerOpts});
+ });
}
// Docs cannot handle the types that userEvent actually declares, so hopefully this sub set is okay
@@ -125,9 +147,6 @@ export async function pressElement(
// Add coords with pressure so this isn't detected as a virtual click
await user.pointer({target: element, keys: '[MouseLeft]', coords: {pressure: 0.5}});
} else if (interactionType === 'keyboard') {
- // 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
- // stick to simulting an actual user's keyboard operations as closely as possible
- // There are problems when using this approach though, actions like trying to trigger the select all checkbox and stuff behave oddly.
act(() => element.focus());
await user.keyboard('[Space]');
} else if (interactionType === 'touch') {
diff --git a/packages/@react-spectrum/s2/test/CheckboxGroup.browser.test.tsx b/packages/@react-spectrum/s2/test/CheckboxGroup.browser.test.tsx
new file mode 100644
index 00000000000..be123bbc927
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/CheckboxGroup.browser.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {Checkbox} from '../src/Checkbox';
+import {CheckboxGroup} from '../src/CheckboxGroup';
+import {expect, it} from 'vitest';
+import React from 'react';
+import {render} from './utils/render';
+import {User} from '@react-aria/test-utils';
+
+function CheckboxGroupExample() {
+ return (
+
+
+ Product Updates
+
+
+ Security Alerts
+
+
+ Marketing Emails
+
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('toggles a checkbox via $interactionType', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('CheckboxGroup', {
+ root: container.querySelector('[role=group]') as HTMLElement,
+ interactionType
+ });
+ let checkboxes = tester.getCheckboxes();
+ await tester.toggleCheckbox({checkbox: checkboxes[2]});
+ expect(tester.getSelectedCheckboxes()).toContain(checkboxes[2]);
+});
diff --git a/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx b/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx
index f41661100cd..6f3158174cd 100644
--- a/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx
+++ b/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx
@@ -84,29 +84,29 @@ describe('CheckboxGroup', () => {
let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {
root: getByRole('group')
});
- expect(checkboxGroupTester.checkboxgroup).toHaveAttribute('role');
- let checkboxes = checkboxGroupTester.checkboxes;
+ expect(checkboxGroupTester.getCheckboxGroup()).toHaveAttribute('role');
+ let checkboxes = checkboxGroupTester.getCheckboxes();
await checkboxGroupTester.toggleCheckbox({checkbox: checkboxes[0]});
expect(checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4, interactionType: 'keyboard'});
expect(checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(2);
- let checkbox4 = checkboxGroupTester.findCheckbox({checkboxIndexOrText: 3});
+ let checkbox4 = checkboxGroupTester.findCheckbox({indexOrText: 3});
await checkboxGroupTester.toggleCheckbox({checkbox: checkbox4, interactionType: 'keyboard'});
expect(checkboxes[3]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(3);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(3);
await checkboxGroupTester.toggleCheckbox({checkbox: 'Soccer', interactionType: 'keyboard'});
expect(checkboxes[0]).not.toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(2);
- let checkbox5 = checkboxGroupTester.findCheckbox({checkboxIndexOrText: 'Rugby'});
+ let checkbox5 = checkboxGroupTester.findCheckbox({indexOrText: 'Rugby'});
await checkboxGroupTester.toggleCheckbox({checkbox: checkbox5, interactionType: 'mouse'});
expect(checkboxes[4]).not.toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(1);
}
);
});
diff --git a/packages/@react-spectrum/s2/test/Combobox.browser.test.tsx b/packages/@react-spectrum/s2/test/Combobox.browser.test.tsx
new file mode 100644
index 00000000000..c8e5c99201d
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/Combobox.browser.test.tsx
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {ComboBox, ComboBoxItem} from '../src/ComboBox';
+import {expect, it} from 'vitest';
+import React from 'react';
+import {render} from './utils/render';
+import {User} from '@react-aria/test-utils';
+
+let items = [
+ {id: 1, name: 'Aardvark'},
+ {id: 2, name: 'Cat'},
+ {id: 3, name: 'Dog'},
+ {id: 4, name: 'Kangaroo'},
+ {id: 5, name: 'Koala'},
+ {id: 6, name: 'Penguin'},
+ {id: 7, name: 'Snake'},
+ {id: 8, name: 'Turtle'},
+ {id: 9, name: 'Wombat'}
+];
+
+function ComboBoxExample() {
+ return (
+
+ {item => (
+
+ {item.name}
+
+ )}
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('selects an option via $interactionType', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('ComboBox', {root: container, interactionType});
+ await tester.toggleOptionSelection({option: 2});
+ expect(tester.getCombobox()).toHaveValue('Dog');
+});
diff --git a/packages/@react-spectrum/s2/test/Combobox.test.tsx b/packages/@react-spectrum/s2/test/Combobox.test.tsx
index c3244e2f749..da708ca4439 100644
--- a/packages/@react-spectrum/s2/test/Combobox.test.tsx
+++ b/packages/@react-spectrum/s2/test/Combobox.test.tsx
@@ -73,15 +73,17 @@ describe('Combobox', () => {
let tree = render({[]});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
expect(options).toHaveLength(1);
- expect(comboboxTester.listbox).toBeTruthy();
+ expect(comboboxTester.getListbox()).toBeTruthy();
expect(options[0]).toHaveTextContent('No results');
- expect(within(comboboxTester.listbox!).getByTestId('loadMoreSentinel')).toBeInTheDocument();
+ expect(
+ within(comboboxTester.getListbox()!).getByTestId('loadMoreSentinel')
+ ).toBeInTheDocument();
});
it('should only call loadMore whenever intersection is detected', async () => {
@@ -102,7 +104,7 @@ describe('Combobox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
@@ -158,7 +160,7 @@ describe('Combobox', () => {
expect(announce).toHaveBeenLastCalledWith('5 options available.');
expect(
- within(comboboxTester.listbox!).getByRole('progressbar', {hidden: true})
+ within(comboboxTester.getListbox()!).getByRole('progressbar', {hidden: true})
).toBeInTheDocument();
await user.keyboard('C');
@@ -174,13 +176,13 @@ describe('Combobox', () => {
interactionType: 'mouse'
});
await comboboxTester.open();
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
}
tree.rerender();
- options = comboboxTester.options();
+ options = comboboxTester.getOptions();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
}
@@ -194,7 +196,7 @@ describe('Combobox', () => {
];
tree.rerender();
- options = comboboxTester.options();
+ options = comboboxTester.getOptions();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
}
@@ -229,7 +231,7 @@ describe('Combobox', () => {
});
let buttons = tree.getAllByRole('button');
expect(buttons).toHaveLength(2);
- expect(buttons[1]).toBe(comboboxTester.trigger);
+ expect(buttons[1]).toBe(comboboxTester.getTrigger());
await user.click(buttons[0]);
@@ -271,26 +273,26 @@ describe('Combobox', () => {
interactionType: 'mouse'
});
await dialogTester.open();
- expect(dialogTester.dialog).toBeVisible();
+ expect(dialogTester.getDialog()).toBeVisible();
act(() => {
jest.runAllTimers();
});
let comboboxTester = testUtilUser.createTester('ComboBox', {
- root: dialogTester.dialog!,
+ root: dialogTester.getDialog()!,
interactionType: 'mouse'
});
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeVisible();
+ expect(comboboxTester.getListbox()).toBeVisible();
act(() => {
jest.runAllTimers();
});
let backdrop = document.querySelector('[style*="--visual-viewport-height"]');
await user.click(backdrop!);
- await waitFor(() => expect(comboboxTester.listbox).toBeNull());
+ await waitFor(() => expect(comboboxTester.getListbox()).toBeNull());
await user.click(backdrop!);
- expect(dialogTester.dialog).toBeNull();
+ expect(dialogTester.getDialog()).toBeNull();
});
it('should label the input with the prefix', () => {
diff --git a/packages/@react-spectrum/s2/test/EditableTableView.test.tsx b/packages/@react-spectrum/s2/test/EditableTableView.test.tsx
index 636a99d9fdc..3cd6a142b83 100644
--- a/packages/@react-spectrum/s2/test/EditableTableView.test.tsx
+++ b/packages/@react-spectrum/s2/test/EditableTableView.test.tsx
@@ -292,7 +292,7 @@ describe('TableView', () => {
overlayType: 'modal'
});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
expect(dialog).toBeVisible();
let input = within(dialog!).getByRole('textbox');
@@ -307,7 +307,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
// navigate to Farmer column
await user.keyboard('{ArrowRight}');
@@ -320,13 +320,13 @@ describe('TableView', () => {
overlayType: 'modal'
});
await dialogTester.open();
- dialog = dialogTester.dialog;
+ dialog = dialogTester.getDialog();
// TODO: also weird that it is dialog.dialog?
expect(dialog).toBeVisible();
let selectTester = testUtilUser.createTester('Select', {root: dialog!});
- expect(selectTester.trigger).toHaveFocus();
- await selectTester.selectOption({option: 'Steven'});
+ expect(selectTester.getTrigger()).toHaveFocus();
+ await selectTester.toggleOptionSelection({option: 'Steven'});
act(() => {
jest.runAllTimers();
});
@@ -341,7 +341,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
expect(
- within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByText('Steven')
+ within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByText('Steven')
).toBeInTheDocument();
await user.tab();
@@ -349,7 +349,7 @@ describe('TableView', () => {
await user.tab({shift: true});
expect(
- within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByRole('button', {
+ within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByRole('button', {
name: 'Edit farmer'
})
).toHaveFocus();
@@ -393,7 +393,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Peaches'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Peaches'})).toBeInTheDocument();
});
it('should be cancellable through the buttons in the dialog', async () => {
@@ -421,7 +421,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
@@ -448,7 +448,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
});
@@ -474,7 +474,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
});
});
@@ -501,7 +501,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
let button = within(tableTester.findCell({text: 'Apples Crisp'})).getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveFocus();
@@ -536,7 +536,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
let button = within(tableTester.findCell({text: 'Apples Crisp'})).getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveFocus();
@@ -737,7 +737,7 @@ describe('TableView', () => {
overlayType: 'modal'
});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
expect(dialog).toBeVisible();
let input = within(dialog!).getByRole('textbox');
@@ -752,7 +752,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
// navigate to Farmer column
await user.keyboard('{ArrowRight}');
@@ -765,13 +765,13 @@ describe('TableView', () => {
overlayType: 'modal'
});
await dialogTester.open();
- dialog = dialogTester.dialog;
+ dialog = dialogTester.getDialog();
// TODO: also weird that it is dialog.dialog?
expect(dialog).toBeVisible();
let selectTester = testUtilUser.createTester('Select', {root: dialog!});
- expect(selectTester.trigger).toHaveFocus();
- await selectTester.selectOption({option: 'Steven'});
+ expect(selectTester.getTrigger()).toHaveFocus();
+ await selectTester.toggleOptionSelection({option: 'Steven'});
act(() => {
jest.runAllTimers();
});
@@ -786,7 +786,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
expect(
- within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByText('Steven')
+ within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByText('Steven')
).toBeInTheDocument();
await user.tab();
@@ -794,7 +794,7 @@ describe('TableView', () => {
await user.tab({shift: true});
expect(
- within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByRole('button', {
+ within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByRole('button', {
name: 'Edit farmer'
})
).toHaveFocus();
@@ -838,7 +838,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Peaches'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Peaches'})).toBeInTheDocument();
});
it('should be cancellable through the buttons in the dialog', async () => {
@@ -866,7 +866,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
@@ -893,7 +893,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
});
@@ -919,7 +919,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
});
});
@@ -946,7 +946,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
let button = within(tableTester.findCell({text: 'Apples Crisp'})).getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveFocus();
@@ -981,7 +981,7 @@ describe('TableView', () => {
});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
let button = within(tableTester.findCell({text: 'Apples Crisp'})).getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveFocus();
diff --git a/packages/@react-spectrum/s2/test/Menu.browser.test.tsx b/packages/@react-spectrum/s2/test/Menu.browser.test.tsx
new file mode 100644
index 00000000000..dd15f6d7442
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/Menu.browser.test.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {Button} from '../src/Button';
+import {expect, it, vi} from 'vitest';
+import {Menu, MenuItem, MenuTrigger} from '../src/Menu';
+import React from 'react';
+import {render} from './utils/render';
+import {User} from '@react-aria/test-utils';
+
+let items = [
+ {id: 1, name: 'Aardvark'},
+ {id: 2, name: 'Cat'},
+ {id: 3, name: 'Dog'},
+ {id: 4, name: 'Kangaroo'},
+ {id: 5, name: 'Koala'},
+ {id: 6, name: 'Penguin'},
+ {id: 7, name: 'Snake'},
+ {id: 8, name: 'Turtle'},
+ {id: 9, name: 'Wombat'}
+];
+
+function MenuExample({onAction}) {
+ return (
+
+
+
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('triggers a menu item via $interactionType', async ({interactionType}) => {
+ let onAction = vi.fn();
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('Menu', {
+ root: container.querySelector('button') as HTMLElement,
+ interactionType
+ });
+ await tester.open();
+ await tester.toggleOptionSelection({option: 2});
+ expect(onAction).toHaveBeenCalledWith(3);
+});
diff --git a/packages/@react-spectrum/s2/test/Menu.test.tsx b/packages/@react-spectrum/s2/test/Menu.test.tsx
index 1d6a4706bad..83972931466 100644
--- a/packages/@react-spectrum/s2/test/Menu.test.tsx
+++ b/packages/@react-spectrum/s2/test/Menu.test.tsx
@@ -171,13 +171,13 @@ describe('long press support', function () {
);
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')});
- await user.click(menuTester.trigger);
+ await user.click(menuTester.getTrigger());
act(() => {
jest.runAllTimers();
});
- expect(menuTester.menu).toBeFalsy();
+ expect(menuTester.getMenu()).toBeFalsy();
await menuTester.open({needsLongPress: true});
- expect(menuTester.menu).toBeTruthy();
+ expect(menuTester.getMenu()).toBeTruthy();
});
it('should open the menu on longPress (ToggleButton)', async function () {
@@ -191,14 +191,14 @@ describe('long press support', function () {
);
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')});
- await user.click(menuTester.trigger);
+ await user.click(menuTester.getTrigger());
act(() => {
jest.runAllTimers();
});
- expect(menuTester.menu).toBeFalsy();
- expect(menuTester.trigger).toHaveAttribute('data-selected', 'true');
+ expect(menuTester.getMenu()).toBeFalsy();
+ expect(menuTester.getTrigger()).toHaveAttribute('data-selected', 'true');
await menuTester.open({needsLongPress: true});
- expect(menuTester.menu).toBeTruthy();
+ expect(menuTester.getMenu()).toBeTruthy();
});
});
diff --git a/packages/@react-spectrum/s2/test/Picker.browser.test.tsx b/packages/@react-spectrum/s2/test/Picker.browser.test.tsx
new file mode 100644
index 00000000000..1ce29a20769
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/Picker.browser.test.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {expect, it} from 'vitest';
+import {Picker, PickerItem} from '../src/Picker';
+import React from 'react';
+import {render} from './utils/render';
+import {User} from '@react-aria/test-utils';
+
+let items = [
+ {id: 1, name: 'Aardvark'},
+ {id: 2, name: 'Cat'},
+ {id: 3, name: 'Dog'},
+ {id: 4, name: 'Kangaroo'},
+ {id: 5, name: 'Koala'},
+ {id: 6, name: 'Penguin'},
+ {id: 7, name: 'Snake'},
+ {id: 8, name: 'Turtle'},
+ {id: 9, name: 'Wombat'}
+];
+
+function PickerExample() {
+ return (
+
+ {(item: (typeof items)[number]) => {item.name}}
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('selects an option via $interactionType', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('Select', {root: container, interactionType});
+ await tester.toggleOptionSelection({option: 2});
+ expect(tester.getTrigger()).toHaveTextContent('Dog');
+});
diff --git a/packages/@react-spectrum/s2/test/Picker.test.tsx b/packages/@react-spectrum/s2/test/Picker.test.tsx
index 4eeea7d015c..e111ed0a90d 100644
--- a/packages/@react-spectrum/s2/test/Picker.test.tsx
+++ b/packages/@react-spectrum/s2/test/Picker.test.tsx
@@ -72,7 +72,7 @@ describe('Picker', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: tree.container});
- expect(selectTester.listbox).toBeFalsy();
+ expect(selectTester.getListbox()).toBeFalsy();
selectTester.setInteractionType('mouse');
await selectTester.open();
@@ -114,14 +114,14 @@ describe('Picker', () => {
let selectTester = testUtilUser.createTester('Select', {root: tree.container});
await selectTester.open();
- let options = selectTester.options();
+ let options = selectTester.getOptions();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
expect(option).toHaveAttribute('aria-setsize', `${items.length}`);
}
tree.rerender();
- options = selectTester.options();
+ options = selectTester.getOptions();
for (let [index, option] of options.entries()) {
if (index === options.length - 1) {
// The last row is the loader here which shouldn't have posinset
@@ -136,7 +136,7 @@ describe('Picker', () => {
let newItems = [...items, {name: 'Chocolate Mint'}, {name: 'Chocolate Chip Cookie Dough'}];
tree.rerender();
- options = selectTester.options();
+ options = selectTester.getOptions();
for (let [index, option] of options.entries()) {
expect(option).toHaveAttribute('aria-posinset', `${index + 1}`);
expect(option).toHaveAttribute('aria-setsize', `${newItems.length}`);
@@ -170,8 +170,8 @@ describe('Picker', () => {
interactionType: 'mouse'
});
await selectTester.open();
- await selectTester.selectOption({option: 0});
- await selectTester.selectOption({option: 2});
+ await selectTester.toggleOptionSelection({option: 0});
+ await selectTester.toggleOptionSelection({option: 2});
await selectTester.close();
// check that the clicked items are rendered in the custom renderValue output
@@ -210,8 +210,8 @@ describe('Picker', () => {
interactionType: 'mouse'
});
await selectTester.open();
- await selectTester.selectOption({option: 0});
- await selectTester.selectOption({option: 2});
+ await selectTester.toggleOptionSelection({option: 0});
+ await selectTester.toggleOptionSelection({option: 2});
await selectTester.close();
expect(spy).toHaveBeenCalledWith(
@@ -246,7 +246,7 @@ describe('Picker', () => {
let selectTester = testUtilUser.createTester('Select', {root: tree.getByTestId('testpicker')});
let buttons = tree.getAllByRole('button');
expect(buttons).toHaveLength(2);
- expect(buttons[1]).toBe(selectTester.trigger);
+ expect(buttons[1]).toBe(selectTester.getTrigger());
await user.click(buttons[0]);
diff --git a/packages/@react-spectrum/s2/test/RadioGroup.browser.test.tsx b/packages/@react-spectrum/s2/test/RadioGroup.browser.test.tsx
new file mode 100644
index 00000000000..b951b8baaeb
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/RadioGroup.browser.test.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {expect, it} from 'vitest';
+import {Radio, RadioGroup} from '../src/RadioGroup';
+import React from 'react';
+import {render} from './utils/render';
+import {User} from '@react-aria/test-utils';
+
+function RadioGroupExample() {
+ return (
+
+
+ Standard Shipping (Free)
+
+
+ Expedited Shipping ($9.99)
+
+
+ Overnight Shipping ($19.99)
+
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('triggers a radio via $interactionType', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('RadioGroup', {
+ root: container.querySelector('[role=radiogroup]') as HTMLElement,
+ interactionType
+ });
+ let radios = tester.getRadios();
+ await tester.triggerRadio({radio: radios[1]});
+ expect(tester.getSelectedRadio()).toBe(radios[1]);
+});
diff --git a/packages/@react-spectrum/s2/test/RadioGroup.test.tsx b/packages/@react-spectrum/s2/test/RadioGroup.test.tsx
index 9473f83c55e..79d3bb76085 100644
--- a/packages/@react-spectrum/s2/test/RadioGroup.test.tsx
+++ b/packages/@react-spectrum/s2/test/RadioGroup.test.tsx
@@ -54,22 +54,25 @@ describe('RadioGroup', () => {
root: getByRole('radiogroup'),
direction
});
- expect(radioGroupTester.radiogroup).toHaveAttribute('aria-orientation', props.orientation);
- let radios = radioGroupTester.radios;
+ expect(radioGroupTester.getRadioGroup()).toHaveAttribute(
+ 'aria-orientation',
+ props.orientation
+ );
+ let radios = radioGroupTester.getRadios();
await radioGroupTester.triggerRadio({radio: radios[0]});
expect(radios[0]).toBeChecked();
await radioGroupTester.triggerRadio({radio: 4, interactionType: 'keyboard'});
expect(radios[4]).toBeChecked();
- let radio4 = radioGroupTester.findRadio({radioIndexOrText: 3});
+ let radio4 = radioGroupTester.findRadio({indexOrText: 3});
await radioGroupTester.triggerRadio({radio: radio4, interactionType: 'keyboard'});
expect(radios[3]).toBeChecked();
await radioGroupTester.triggerRadio({radio: 'Dogs', interactionType: 'mouse'});
expect(radios[0]).toBeChecked();
- let radio5 = radioGroupTester.findRadio({radioIndexOrText: 'Chocobo'});
+ let radio5 = radioGroupTester.findRadio({indexOrText: 'Chocobo'});
await radioGroupTester.triggerRadio({radio: radio5, interactionType: 'mouse'});
expect(radios[4]).toBeChecked();
@@ -79,9 +82,9 @@ describe('RadioGroup', () => {
// instead of using ArrowLeft/ArrowRight
await user.keyboard('[ArrowLeft]');
if (props.locale === 'ar-AE' && props.orientation === 'horizontal') {
- expect(radioGroupTester.selectedRadio).toBe(radios[0]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[0]);
} else {
- expect(radioGroupTester.selectedRadio).toBe(radios[3]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[3]);
}
}
);
diff --git a/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx b/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx
index d43995a4cc5..c2a89f122fb 100644
--- a/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx
+++ b/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx
@@ -127,7 +127,7 @@ describe('SelectBoxGroup', () => {
let listboxTester = testUtilUser.createTester('ListBox', {root: screen.getByRole('listbox')});
await listboxTester.toggleOptionSelection({option: 0});
- expect(listboxTester.options()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'true');
});
it('handles uncontrolled click selection in multiple mode', async () => {
@@ -137,8 +137,8 @@ describe('SelectBoxGroup', () => {
await listboxTester.toggleOptionSelection({option: 0});
await listboxTester.toggleOptionSelection({option: 1});
- expect(listboxTester.options()[0]).toHaveAttribute('aria-selected', 'true');
- expect(listboxTester.options()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[1]).toHaveAttribute('aria-selected', 'true');
});
it('handles uncontrolled selection toggle', async () => {
@@ -146,12 +146,12 @@ describe('SelectBoxGroup', () => {
let listboxTester = testUtilUser.createTester('ListBox', {root: screen.getByRole('listbox')});
await listboxTester.toggleOptionSelection({option: 0});
- expect(listboxTester.options()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'true');
// Toggle off in single mode by selecting another
await listboxTester.toggleOptionSelection({option: 1});
- expect(listboxTester.options()[0]).toHaveAttribute('aria-selected', 'false');
- expect(listboxTester.options()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(listboxTester.getOptions()[1]).toHaveAttribute('aria-selected', 'true');
});
it('handles uncontrolled keyboard selection', async () => {
@@ -173,7 +173,7 @@ describe('SelectBoxGroup', () => {
let listboxTester = testUtilUser.createTester('ListBox', {root: screen.getByRole('listbox')});
await listboxTester.toggleOptionSelection({option: 0});
- expect(listboxTester.options()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'true');
});
it('handles controlled multiple selection', async () => {
@@ -183,8 +183,8 @@ describe('SelectBoxGroup', () => {
await listboxTester.toggleOptionSelection({option: 0});
await listboxTester.toggleOptionSelection({option: 1});
- expect(listboxTester.options()[0]).toHaveAttribute('aria-selected', 'true');
- expect(listboxTester.options()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[1]).toHaveAttribute('aria-selected', 'true');
});
it('calls onSelectionChange when selection changes in controlled mode', async () => {
diff --git a/packages/@react-spectrum/s2/test/TableView.browser.test.tsx b/packages/@react-spectrum/s2/test/TableView.browser.test.tsx
new file mode 100644
index 00000000000..cb64439d332
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/TableView.browser.test.tsx
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {Cell, Column, Row, TableBody, TableHeader, TableView} from '../src/TableView';
+import {expect, it} from 'vitest';
+import React from 'react';
+import {render} from './utils/render';
+import {User} from '@react-aria/test-utils';
+
+let columns = [
+ {name: 'Title', id: 'title', isRowHeader: true},
+ {name: 'Status', id: 'status'},
+ {name: 'Payment Method', id: 'paymentMethod'},
+ {name: 'Price', id: 'price'}
+];
+
+const items = [
+ {id: 1, title: 'Website Design', status: 'Paid', paymentMethod: 'Credit Card', price: 1200},
+ {id: 2, title: 'Logo Creation', status: 'Pending', paymentMethod: 'PayPal', price: 350},
+ {id: 3, title: 'SEO Optimization', status: 'Overdue', paymentMethod: 'Bank Transfer', price: 800},
+ {id: 4, title: 'Social Media Setup', status: 'Paid', paymentMethod: 'Debit Card', price: 450},
+ {id: 5, title: 'Content Writing', status: 'Pending', paymentMethod: 'Credit Card', price: 600}
+];
+
+function TableExample() {
+ return (
+
+
+ {column => {column.name}}
+
+
+ {item => (
+
+ {column => | {item[column.id]} | }
+
+ )}
+
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('selects a row via $interactionType', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('Table', {
+ root: container.querySelector('[role=grid]') as HTMLElement,
+ interactionType
+ });
+ await tester.toggleRowSelection({row: 2});
+ expect(tester.getRows()[2].getAttribute('aria-selected')).toBe('true');
+});
diff --git a/packages/@react-spectrum/s2/test/TableView.test.tsx b/packages/@react-spectrum/s2/test/TableView.test.tsx
index b45165f18a5..c217b1648ea 100644
--- a/packages/@react-spectrum/s2/test/TableView.test.tsx
+++ b/packages/@react-spectrum/s2/test/TableView.test.tsx
@@ -271,16 +271,16 @@ describe('TableView', () => {
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
+ let groups = tableTester.getRowGroups();
expect(groups).toHaveLength(3);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.getRows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
@@ -289,7 +289,7 @@ describe('TableView', () => {
await user.click(footerRows[0]);
expect(onSelectionChange).not.toHaveBeenCalled();
- await user.click(tableTester.rows[0]);
+ await user.click(tableTester.getRows()[0]);
expect(onSelectionChange).toHaveBeenCalled();
});
});
diff --git a/packages/@react-spectrum/s2/test/TreeView.browser.test.tsx b/packages/@react-spectrum/s2/test/TreeView.browser.test.tsx
new file mode 100644
index 00000000000..fa3de611c1d
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/TreeView.browser.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {Collection} from 'react-aria/Collection';
+import {expect, it} from 'vitest';
+import React from 'react';
+import {render} from './utils/render';
+import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
+import {TreeView, TreeViewItem, TreeViewItemContent} from '../src/TreeView';
+import {User} from '@react-aria/test-utils';
+
+let items = [
+ {
+ id: 1,
+ title: 'Documents',
+ type: 'directory',
+ children: [
+ {
+ id: 2,
+ title: 'Project',
+ type: 'directory',
+ children: [
+ {id: 3, title: 'Weekly Report', type: 'file', children: []},
+ {id: 4, title: 'Budget', type: 'file', children: []}
+ ]
+ }
+ ]
+ },
+ {
+ id: 5,
+ title: 'Photos',
+ type: 'directory',
+ children: [
+ {id: 6, title: 'Image 1', type: 'file', children: []},
+ {id: 7, title: 'Image 2', type: 'file', children: []}
+ ]
+ }
+];
+
+function TreeViewExample() {
+ return (
+
+ {function renderItem(item) {
+ return (
+
+ {item.title}
+ {renderItem}
+
+ );
+ }}
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('expands and selects a row via $interactionType', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('Tree', {
+ root: container.querySelector('[role=treegrid]') as HTMLElement,
+ interactionType
+ });
+ await tester.toggleRowExpansion({row: 'Photos'});
+ await tester.toggleRowSelection({row: 'Image 2'});
+ let selectedRow = tester.findRow({indexOrText: 'Image 2'});
+ expect(selectedRow!.getAttribute('aria-selected')).toBe('true');
+});
diff --git a/packages/@react-spectrum/test-utils/README.md b/packages/@react-spectrum/test-utils/README.md
index 0030138c3bf..21451959b5b 100644
--- a/packages/@react-spectrum/test-utils/README.md
+++ b/packages/@react-spectrum/test-utils/README.md
@@ -1,3 +1,74 @@
# @react-spectrum/test-utils
This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details.
+
+See the [React Spectrum testing docs](https://react-spectrum.adobe.com/testing#react-spectrum-test-utils) for usage.
+
+`@react-spectrum/test-utils` re-exports the same test utils available in `@react-aria/test-utils`, including the ARIA pattern testers. These testers are a set of testing utilities that aim to make writing unit tests easier for consumers of React Spectrum.
+
+In addition to the re-exports, this package provides `simulateMobile` and `simulateDesktop` helpers for switching between mobile and desktop component variants in your tests (Jest only).
+
+> **Requirements:** This library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). You need to be on React 18+ for these utilities to work.
+
+## Installation
+
+```
+npm install @react-spectrum/test-utils --dev
+```
+
+## Setup
+
+Initialize a `User` object at the top of your test file, and use it to create an ARIA pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
+
+```ts
+// YourTest.test.ts
+import {screen} from '@testing-library/react';
+import {User} from '@react-spectrum/test-utils';
+
+// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers.
+// 'interactionType' specifies what mode of interaction should be simulated by the tester
+// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press)
+let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime});
+// ...
+
+it('my test case', async function () {
+ // Render your test component/app
+ render();
+ // Initialize the table tester via providing the 'Table' pattern name and the root element of said table
+ let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')});
+
+ // ...
+});
+```
+
+## User API
+
+```ts
+class User {
+ constructor(opts?: {
+ interactionType?: 'mouse' | 'keyboard' | 'touch',
+ advanceTimer?: (time?: number) => void | Promise
+ });
+
+ createTester(patternName, opts): PatternTester;
+}
+```
+
+- `interactionType` — default modality used by testers created from this `User`. Individual testers can override this via `setInteractionType` or per-method options.
+- `advanceTimer` — used by testers to advance timers for interactions like long press. Pass `jest.advanceTimersByTime` (or your test framework's equivalent) when using fake timers.
+- `createTester(patternName, opts)` — returns a tester for the given ARIA pattern. `opts.root` is the root element of the component under test.
+
+## Patterns
+
+Below is a list of the ARIA patterns supported by `createTester`. See the accompanying component testing docs pages on the [React Spectrum docs site](https://react-spectrum.adobe.com/testing#react-spectrum-test-utils) for sample usage of each tester in a test suite.
+
+- [CheckboxGroup](https://react-spectrum.adobe.com/CheckboxGroup/testing)
+- [ComboBox](https://react-spectrum.adobe.com/ComboBox/testing)
+- [Dialog](https://react-spectrum.adobe.com/Dialog/testing)
+- [ListView](https://react-spectrum.adobe.com/ListView/testing)
+- [Menu](https://react-spectrum.adobe.com/Menu/testing)
+- [Picker](https://react-spectrum.adobe.com/Picker/testing)
+- [RadioGroup](https://react-spectrum.adobe.com/RadioGroup/testing)
+- [TableView](https://react-spectrum.adobe.com/TableView/testing)
+- [Tabs](https://react-spectrum.adobe.com/Tabs/testing)
+- [TreeView](https://react-spectrum.adobe.com/TreeView/testing)
diff --git a/packages/@react-spectrum/test-utils/package.json b/packages/@react-spectrum/test-utils/package.json
index ccfd2a3ce13..dc5fffa7a73 100644
--- a/packages/@react-spectrum/test-utils/package.json
+++ b/packages/@react-spectrum/test-utils/package.json
@@ -39,7 +39,7 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
},
"peerDependencies": {
- "@testing-library/react": "^16.0.0",
+ "@testing-library/dom": "^10.0.0",
"@testing-library/user-event": "^14.0.0",
"jest": "^29.5.0",
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
diff --git a/packages/dev/docs/pages/react-aria/testing.mdx b/packages/dev/docs/pages/react-aria/testing.mdx
index f571ef54227..6a199d06a46 100644
--- a/packages/dev/docs/pages/react-aria/testing.mdx
+++ b/packages/dev/docs/pages/react-aria/testing.mdx
@@ -162,7 +162,7 @@ the resulting state of the component.
yarn add --dev @react-aria/test-utils
```
-Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need
+Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need
to be on React 18+ in order for these utilities to work.
### Setup
diff --git a/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx b/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx
index 0a1ba3fb395..b4200b66c6b 100644
--- a/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing CheckboxGroup with React Aria test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create an `CheckboxGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -45,15 +45,15 @@ it('CheckboxGroup can select multiple checkboxes', async function () {
);
let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByTestId('test-checkboxgroup')});
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(0);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(0);
await checkboxGroupTester.toggleCheckbox({checkbox: 0});
- expect(checkboxGroupTester.checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.getCheckboxes()[0]).toBeChecked();
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4});
- expect(checkboxGroupTester.checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.getCheckboxes()[4]).toBeChecked();
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(2);
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx b/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx
index 8b99587290b..6b38124fbf4 100644
--- a/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing ComboBox with React Aria test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `ComboBox` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -46,12 +46,12 @@ it('ComboBox can select an option via keyboard', async function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'});
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeInTheDocument();
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
- let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ let options = comboboxTester.getOptions();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.getCombobox().value).toBe('One');
+ expect(comboboxTester.getListbox()).not.toBeInTheDocument();
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx b/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx
index 09c849d4e51..105094eebb9 100644
--- a/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx
@@ -28,7 +28,7 @@ GridList supports long press interactions on its items in certain configurations
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `GridList` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -52,17 +52,17 @@ it('GridList can select a row via keyboard', async function () {
);
let gridListTester = testUtilUser.createTester('GridList', {root: getByTestId('test-gridlist'), interactionType: 'keyboard'});
- let row = gridListTester.rows[0];
+ let row = gridListTester.getRows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/ListBox/testing.mdx b/packages/dev/s2-docs/pages/react-aria/ListBox/testing.mdx
index 175da6f0d2b..d6b74125a52 100644
--- a/packages/dev/s2-docs/pages/react-aria/ListBox/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/ListBox/testing.mdx
@@ -28,7 +28,7 @@ ListBox supports long press interactions on its options in certain configuration
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `ListBox` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -53,7 +53,7 @@ it('ListBox can select an option via keyboard', async function () {
let listboxTester = testUtilUser.createTester('ListBox', {root: getByTestId('test-listbox'), interactionType: 'keyboard'});
await listboxTester.toggleOptionSelection({option: 4});
- expect(listboxTester.options()[4]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[4]).toHaveAttribute('aria-selected', 'true');
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx
index a95052226e2..4075e27607e 100644
--- a/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx
@@ -28,7 +28,7 @@ Menu supports long press interactions in certain configurations. See the followi
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Menu` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -54,16 +54,16 @@ it('Menu can open its submenu via keyboard', async function () {
let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'});
await menuTester.open();
- expect(menuTester.menu).toBeInTheDocument();
- let submenuTriggers = menuTester.submenuTriggers;
+ expect(menuTester.getMenu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.getSubmenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.getMenu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.getOptions()[0]});
+ expect(submenuTester.getMenu()).not.toBeInTheDocument();
+ expect(menuTester.getMenu()).not.toBeInTheDocument();
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/Modal/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Modal/testing.mdx
new file mode 100644
index 00000000000..9c57c7a791d
--- /dev/null
+++ b/packages/dev/s2-docs/pages/react-aria/Modal/testing.mdx
@@ -0,0 +1,75 @@
+import {Layout} from '../../../src/Layout';
+export default Layout;
+
+import {InlineAlert, Heading, Content} from '@react-spectrum/s2';
+import testUtilDocs from 'docs:@react-aria/test-utils';
+import {InstallCommand} from '../../../src/InstallCommand';
+import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ';
+
+export const isSubpage = true;
+export const hideFromSearch = true;
+export const tags = ['testing', 'modal', 'dialog', 'test-utils'];
+export const description = 'Testing Modal with React Aria test utils';
+
+# Testing Modal
+
+## Test utils
+
+`@react-aria/test-utils` offers common dialog interaction testing utilities. Install it with your preferred package manager.
+
+
+
+
+ Requirements
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+
+
+Initialize a `User` object at the top of your test file, and use it to create a `Dialog` pattern tester in your test cases. Pass `overlayType: 'modal'` to indicate the dialog is rendered inside a `Modal`. The tester has methods that you can call within your test to query for the dialog or simulate common interactions.
+
+```ts
+// Modal.test.ts
+import {render} from '@testing-library/react';
+import {User} from '@react-aria/test-utils';
+
+let testUtilUser = new User({
+ interactionType: 'mouse'
+});
+// ...
+
+it('Modal can be opened and closed', async function () {
+ // Render your test component/app and initialize the dialog tester
+ let {getByRole} = render(
+
+
+
+
+
+
+ );
+ let button = getByRole('button');
+ let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'});
+
+ await dialogTester.open();
+ let dialog = dialogTester.getDialog();
+ expect(dialog).toBeVisible();
+
+ await dialogTester.close();
+ expect(dialog).not.toBeInTheDocument();
+});
+```
+
+## API
+
+### User
+
+
+
+### DialogTester
+
+
+
+## Testing FAQ
+
+
diff --git a/packages/dev/s2-docs/pages/react-aria/Popover/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Popover/testing.mdx
new file mode 100644
index 00000000000..e9d8adf4fdd
--- /dev/null
+++ b/packages/dev/s2-docs/pages/react-aria/Popover/testing.mdx
@@ -0,0 +1,75 @@
+import {Layout} from '../../../src/Layout';
+export default Layout;
+
+import {InlineAlert, Heading, Content} from '@react-spectrum/s2';
+import testUtilDocs from 'docs:@react-aria/test-utils';
+import {InstallCommand} from '../../../src/InstallCommand';
+import {PatternTestingFAQ} from '../../../src/PatternTestingFAQ';
+
+export const isSubpage = true;
+export const hideFromSearch = true;
+export const tags = ['testing', 'popover', 'dialog', 'test-utils'];
+export const description = 'Testing Popover with React Aria test utils';
+
+# Testing Popover
+
+## Test utils
+
+`@react-aria/test-utils` offers common dialog interaction testing utilities. Install it with your preferred package manager.
+
+
+
+
+ Requirements
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+
+
+Initialize a `User` object at the top of your test file, and use it to create a `Dialog` pattern tester in your test cases. Pass `overlayType: 'popover'` to indicate the dialog is rendered inside a `Popover`. The tester has methods that you can call within your test to query for the dialog or simulate common interactions.
+
+```ts
+// Popover.test.ts
+import {render} from '@testing-library/react';
+import {User} from '@react-aria/test-utils';
+
+let testUtilUser = new User({
+ interactionType: 'mouse'
+});
+// ...
+
+it('Popover can be opened and closed', async function () {
+ // Render your test component/app and initialize the dialog tester
+ let {getByRole} = render(
+
+
+
+
+
+
+ );
+ let button = getByRole('button');
+ let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'popover'});
+
+ await dialogTester.open();
+ let dialog = dialogTester.getDialog();
+ expect(dialog).toBeVisible();
+
+ await dialogTester.close();
+ expect(dialog).not.toBeInTheDocument();
+});
+```
+
+## API
+
+### User
+
+
+
+### DialogTester
+
+
+
+## Testing FAQ
+
+
diff --git a/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx b/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx
index bc818613d62..5f3f7e4f597 100644
--- a/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing RadioGroup with React Aria test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `RadioGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -46,14 +46,14 @@ it('RadioGroup can switch the selected radio', async function () {
);
let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')});
- let radios = radioGroupTester.radios;
- expect(radioGroupTester.selectedRadio).toBeFalsy();
+ let radios = radioGroupTester.getRadios();
+ expect(radioGroupTester.getSelectedRadio()).toBeFalsy();
await radioGroupTester.triggerRadio({radio: radios[0]});
- expect(radioGroupTester.selectedRadio).toBe(radios[0]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[0]);
await radioGroupTester.triggerRadio({radio: radios[1]});
- expect(radioGroupTester.selectedRadio).toBe(radios[1]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[1]);
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx
index 016c20d29a8..9226d627389 100644
--- a/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing Select with React Aria test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Select` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -44,10 +44,10 @@ it('Select can select an option via keyboard', async function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an item');
- await selectTester.selectOption({option: 'Cat'});
+ await selectTester.toggleOptionSelection({option: 'Cat'});
expect(trigger).toHaveTextContent('Cat');
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx
index 7760545b56d..e3aad95f314 100644
--- a/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx
@@ -28,7 +28,7 @@ Table supports long press interactions on its rows in certain configurations. Se
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Table` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -52,22 +52,22 @@ it('Table can toggle row selection', async function () {
);
let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')});
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.getSelectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.getRows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx
index 451c22b9e1f..f7f850eebcb 100644
--- a/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing Tabs with React Aria test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Tabs` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -45,11 +45,11 @@ it('Tabs can change selection via keyboard', async function () {
);
let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'});
- let tabs = tabsTester.tabs;
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ let tabs = tabsTester.getTabs();
+ expect(tabsTester.getSelectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[1]);
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx b/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx
index 73deef1ee1c..38222434fd4 100644
--- a/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx
@@ -28,7 +28,7 @@ Tree supports long press interactions on its rows in certain configurations. See
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Tree` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -53,19 +53,19 @@ it('Tree can select and expand an item via keyboard', async function () {
let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'});
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(within(treeTester.getRows()[1]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).not.toBeChecked();
- await treeTester.toggleRowExpansion({index: 0});
- expect(treeTester.rows[0]).toHaveAttribute('aria-expanded', 'true');
+ await treeTester.toggleRowExpansion({row: 0});
+ expect(treeTester.getRows()[0]).toHaveAttribute('aria-expanded', 'true');
});
```
diff --git a/packages/dev/s2-docs/pages/react-aria/testing.mdx b/packages/dev/s2-docs/pages/react-aria/testing.mdx
index 2bf5b3cdee3..86d9cf75d78 100644
--- a/packages/dev/s2-docs/pages/react-aria/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/testing.mdx
@@ -175,7 +175,7 @@ or for users who have built their own components following the respective ARIA p
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
@@ -216,6 +216,7 @@ the testers in your test suite.
- [CheckboxGroup](./CheckboxGroup/testing)
- [ComboBox](./ComboBox/testing)
+- Dialog via [Modal](./Modal/testing) / [Popover](./Popover/testing)
- [GridList](./GridList/testing)
- [ListBox](./ListBox/testing)
- [Menu](./Menu/testing)
diff --git a/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx b/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx
index 6542894cd46..8a0687a88b2 100644
--- a/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing CheckboxGroup with React Spectrum test utils
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `CheckboxGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -45,15 +45,15 @@ it('CheckboxGroup can select multiple checkboxes', async function () {
);
let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByTestId('test-checkboxgroup')});
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(0);
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(0);
await checkboxGroupTester.toggleCheckbox({checkbox: 0});
- expect(checkboxGroupTester.checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.getCheckboxes()[0]).toBeChecked();
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4});
- expect(checkboxGroupTester.checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.getCheckboxes()[4]).toBeChecked();
+ expect(checkboxGroupTester.getSelectedCheckboxes()).toHaveLength(2);
});
```
diff --git a/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx b/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx
index 39378b98ed3..ecb839780fc 100644
--- a/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing ComboBox with React Spectrum test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `ComboBox` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -46,12 +46,12 @@ it('ComboBox can select an option via keyboard', async function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'});
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeInTheDocument();
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
- let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ let options = comboboxTester.getOptions();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.getCombobox().value).toBe('One');
+ expect(comboboxTester.getListbox()).not.toBeInTheDocument();
});
```
diff --git a/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx b/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx
index 8cbd1a0ab6f..dff0972fe67 100644
--- a/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing Dialog with React Spectrum test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Dialog` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -50,7 +50,7 @@ it('Dialog can be opened and closed', async function () {
let button = getByRole('button');
let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
expect(dialog).toBeVisible();
await dialogTester.close();
expect(dialog).not.toBeInTheDocument();
diff --git a/packages/dev/s2-docs/pages/s2/ListView/testing.mdx b/packages/dev/s2-docs/pages/s2/ListView/testing.mdx
index 74486c588d0..826093eedae 100644
--- a/packages/dev/s2-docs/pages/s2/ListView/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/ListView/testing.mdx
@@ -28,7 +28,7 @@ ListView supports long press interactions on its rows in certain configurations.
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `GridList` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -52,17 +52,17 @@ it('ListView can toggle row selection', async function () {
);
let gridListTester = testUtilUser.createTester('GridList', {root: getByTestId('test-listview'), interactionType: 'keyboard'});
- let row = gridListTester.rows[0];
+ let row = gridListTester.getRows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/dev/s2-docs/pages/s2/Menu/testing.mdx b/packages/dev/s2-docs/pages/s2/Menu/testing.mdx
index c0c20c65f1b..5dc208c7f99 100644
--- a/packages/dev/s2-docs/pages/s2/Menu/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Menu/testing.mdx
@@ -28,7 +28,7 @@ Menu supports long press interactions in certain configurations. See the followi
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Menu` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -54,16 +54,16 @@ it('Menu can open its submenu via keyboard', async function () {
let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'});
await menuTester.open();
- expect(menuTester.menu).toBeInTheDocument();
- let submenuTriggers = menuTester.submenuTriggers;
+ expect(menuTester.getMenu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.getSubmenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.getMenu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.getOptions()[0]});
+ expect(submenuTester.getMenu()).not.toBeInTheDocument();
+ expect(menuTester.getMenu()).not.toBeInTheDocument();
});
```
diff --git a/packages/dev/s2-docs/pages/s2/Picker/testing.mdx b/packages/dev/s2-docs/pages/s2/Picker/testing.mdx
index 8df691dc74f..20b164c607b 100644
--- a/packages/dev/s2-docs/pages/s2/Picker/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Picker/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing Picker with React Spectrum test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Picker` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -44,10 +44,10 @@ it('Picker can select an option via keyboard', async function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an item');
- await selectTester.selectOption({option: 'Cat'});
+ await selectTester.toggleOptionSelection({option: 'Cat'});
expect(trigger).toHaveTextContent('Cat');
});
```
diff --git a/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx b/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx
index 6023e0ad243..37ff1f66a2b 100644
--- a/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing RadioGroup with React Spectrum test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `RadioGroup` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -46,14 +46,14 @@ it('RadioGroup can switch the selected radio', async function () {
);
let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')});
- let radios = radioGroupTester.radios;
- expect(radioGroupTester.selectedRadio).toBeFalsy();
+ let radios = radioGroupTester.getRadios();
+ expect(radioGroupTester.getSelectedRadio()).toBeFalsy();
await radioGroupTester.triggerRadio({radio: radios[0]});
- expect(radioGroupTester.selectedRadio).toBe(radios[0]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[0]);
await radioGroupTester.triggerRadio({radio: radios[1]});
- expect(radioGroupTester.selectedRadio).toBe(radios[1]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[1]);
});
```
diff --git a/packages/dev/s2-docs/pages/s2/TableView/testing.mdx b/packages/dev/s2-docs/pages/s2/TableView/testing.mdx
index 5ca41895842..72d4ff5300e 100644
--- a/packages/dev/s2-docs/pages/s2/TableView/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/TableView/testing.mdx
@@ -28,7 +28,7 @@ TableView supports long press interactions on its rows in certain configurations
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `TableView` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -52,22 +52,22 @@ it('TableView can toggle row selection', async function () {
);
let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')});
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.getSelectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.getRows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx b/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx
index 6bf054ffbe6..9cb4f1419d3 100644
--- a/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx
@@ -21,7 +21,7 @@ export const description = 'Testing Tabs with React Spectrum test utils';
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `Tabs` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -45,11 +45,11 @@ it('Tabs can change selection via keyboard', async function () {
);
let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'});
- let tabs = tabsTester.tabs;
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ let tabs = tabsTester.getTabs();
+ expect(tabsTester.getSelectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[1]);
});
```
diff --git a/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx b/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx
index 2a65163d56b..8500ef118f6 100644
--- a/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx
@@ -28,7 +28,7 @@ TreeView supports long press interactions on its rows in certain configurations.
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
Initialize a `User` object at the top of your test file, and use it to create a `TreeView` pattern tester in your test cases. The tester has methods that you can call within your test to query for specific subcomponents or simulate common interactions.
@@ -53,19 +53,19 @@ it('TreeView can select an item via keyboard', async function () {
let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'});
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(within(treeTester.getRows()[1]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).not.toBeChecked();
- await treeTester.toggleRowExpansion({index: 0});
- expect(treeTester.rows[0]).toHaveAttribute('aria-expanded', 'true');
+ await treeTester.toggleRowExpansion({row: 0});
+ expect(treeTester.getRows()[0]).toHaveAttribute('aria-expanded', 'true');
});
```
diff --git a/packages/dev/s2-docs/pages/s2/testing.mdx b/packages/dev/s2-docs/pages/s2/testing.mdx
index 7bcc5736ce6..36c59ba2de5 100644
--- a/packages/dev/s2-docs/pages/s2/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/testing.mdx
@@ -170,7 +170,7 @@ the ARIA pattern testers. These testers are set of testing utilities that aims t
Requirements
- Please note that this library uses [@testing-library/react@16](https://www.npmjs.com/package/@testing-library/react) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
+ Please note that this library uses [@testing-library/dom@10](https://www.npmjs.com/package/@testing-library/dom) and [@testing-library/user-event@14](https://www.npmjs.com/package/@testing-library/user-event). This means that you need to be on React 18+ in order for these utilities to work.
diff --git a/packages/dev/s2-docs/scripts/generateAgentSkills.mjs b/packages/dev/s2-docs/scripts/generateAgentSkills.mjs
index 81121573528..832dc03bbd9 100644
--- a/packages/dev/s2-docs/scripts/generateAgentSkills.mjs
+++ b/packages/dev/s2-docs/scripts/generateAgentSkills.mjs
@@ -81,7 +81,8 @@ const CUSTOM_SKILL_CONTENT = {
path.join(
REPO_ROOT,
'packages/dev/s2-docs/skills/react-spectrum-s2/implementation-guidance.md'
- )
+ ),
+ path.join(REPO_ROOT, 'packages/dev/s2-docs/skills/react-spectrum-s2/test-utils-guidance.md')
],
guideEntries: [
{
@@ -95,6 +96,11 @@ const CUSTOM_SKILL_CONTENT = {
'How to choose the right S2 component when requirements do not name one explicitly.'
}
]
+ },
+ 'react-aria': {
+ embeddedMarkdownPaths: [
+ path.join(REPO_ROOT, 'packages/dev/s2-docs/skills/react-aria/test-utils-guidance.md')
+ ]
}
};
@@ -375,7 +381,8 @@ function generateDocsSkillMd(skillConfig, categories, isS2) {
const customSkillNotesMarkdown = getCustomSkillNotesMarkdown(skillConfig.name);
const embeddedCustomMarkdown = readCustomEmbeddedMarkdown(skillConfig.name, {
'{{guidesBase}}': 'references/guides/',
- '{{componentsBase}}': 'references/components/'
+ '{{componentsBase}}': 'references/components/',
+ '{{testingBase}}': 'references/testing/'
});
let content = generateFrontmatter(skillConfig);
diff --git a/packages/dev/s2-docs/skills/react-aria/test-utils-guidance.md b/packages/dev/s2-docs/skills/react-aria/test-utils-guidance.md
new file mode 100644
index 00000000000..db6575b713b
--- /dev/null
+++ b/packages/dev/s2-docs/skills/react-aria/test-utils-guidance.md
@@ -0,0 +1,115 @@
+## Test utilities
+
+`@react-aria/test-utils` provides ARIA pattern testers that simulate mouse, keyboard, and touch interactions for components built with React Aria Components.
+
+### Installation
+
+```bash
+npm install @react-aria/test-utils --save-dev
+```
+
+### Core pattern
+
+External consumers should import from `@react-aria/test-utils`. Tests inside the `packages/` monorepo should import everything from `@react-spectrum/test-utils-internal`, which re-exports `User` and all other test utilities:
+```ts
+import {act, render, User} from '@react-spectrum/test-utils-internal';
+```
+
+Initialize a `User` once per test file. Call `createTester` to get a tester for a specific ARIA pattern, then call tester methods to simulate interactions.
+
+```ts
+import {User} from '@react-aria/test-utils';
+
+// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers.
+// 'interactionType' specifies what mode of interaction should be simulated by the tester
+// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press)
+let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime});
+
+it('my test case', async function () {
+ // Render your test component/app
+ let {getByTestId} = render();
+ // Initialize the table tester via providing the 'Table' pattern name and the root element of said table
+ let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test_table')});
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
+
+ await tableTester.toggleSelectAll();
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
+ ...
+});
+```
+
+Set `interactionType` to `'mouse'`, `'keyboard'`, or `'touch'`. Override per tester via `createTester(..., {interactionType})` or per method call.
+
+When using fake timers, pass `advanceTimer: jest.advanceTimersByTime` and flush timers after each test:
+
+```ts
+afterEach(() => {
+ act(() => jest.runAllTimers());
+});
+```
+
+### Tips and Tricks
+- The testers typically offers these things: a way to simulate common user interactions for the given component via a specified user modality (e.g. using mouse vs keyboard to toggle a menu), a way to get the various common elements that make up the component (e.g. the rows in a table), and a way to query the state of the component (e.g. get the selected rows in a table). Prefer using the testers for these use cases so that the user doesn't need to know what specific roles/elements/etc to target in their tests.
+- You can still simulate interactions manually in your test alongside the utilities provided by the tester. This can come in handy if you find that the tester doesn't cover a specific user flow or if one of its utilities isn't quite working as expected. After simulating your interaction, you can still
+use the tester to query for the component's state or trigger a different interaction utility.
+- Mouse drag interactions, simulated scrolling, and other mock reliant interactions are not available in these test utils since they depend heavily on how the user mocks things like clientHeight/Width/etc in their tests. These interactions need to be simulated manually by the user.
+- Some testers may support the notion of "long press" for certain interactions (e.g. long pressing a button to trigger its menu). To simulate this, you will need mock PointerEvent globally (see the installPointerEvent util) and provide a way to advance timers to the User via `advanceTimer`.
+- These test utils are compatible with not only JSDOM unit tests but browser tests as well (e.g. vitest-browser-react).
+- Methods that accept a target (`option`, `row`, `column`, `checkbox`, `radio`, `tab`) take a `number` (index), `string` (text content), or `HTMLElement`. Use the tester's own query methods (e.g. `getRows()`, `getOptions()`) to obtain an `HTMLElement` when you need one.
+- Link navigation assertions must be simulated manually. The testers do not assert navigation side effects.
+
+### When not to use the testers
+
+Skip the testers and write manual interactions for the following cases:
+
+- When testing a Menu or Dialog rendered without a trigger, or when testing interactive elements embedded inside rows or cells (e.g. an ActionMenu inside a TreeView row). The testers assume a trigger exists and do not reach into row/cell content.
+- tests that verify exact focus order, arrow key cycling, or specific modifier key behavior. Use `fireEvent.keyDown` or `userEvent.keyboard` directly so the test is actually testing the desired keyboard flow.
+- when `isOpen` or `defaultOpen` is set, `open()` will no-op but the tester's `root` must still resolve to the trigger element. Use `getByLabelText` or `getByTestId` rather than `getByRole('button')` to avoid ambiguity when multiple buttons are in the DOM.
+- testing `isDismissible`, `isKeyboardDismissDisabled`, or outside-click behavior. Use `userEvent.click(document.body)` or `user.keyboard('[Escape]')` directly and assert the expected state afterwards.
+- when a Dialog closes via an action button (not the explicit close/dismiss button) you should instead click that button manually, then use `dialogTester.getDialog()` to assert whether the dialog is still present.
+
+### Draggable handle components
+
+Components with draggable handles (Slider, ColorArea, ColorSlider, ColorWheel) need `getBoundingClientRect` mocked so move calculations work:
+
+```ts
+import {installMouseEvent} from '@react-spectrum/test-utils';
+installMouseEvent();
+
+beforeAll(() => {
+ jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(
+ () => ({top: 0, left: 0, width: 100, height: 10, bottom: 10, right: 100})
+ );
+});
+```
+
+### Available testers
+
+| Pattern name | Component | Key methods |
+|---|---|---|
+| `'CheckboxGroup'` | CheckboxGroup | `getCheckboxGroup()`, `getCheckboxes()`, `getSelectedCheckboxes()`, `toggleCheckbox({checkbox})` |
+| `'ComboBox'` | ComboBox | `getCombobox()`, `getListbox()`, `getOptions()`, `open()`, `toggleOptionSelection({option})` |
+| `'Dialog'` | Modal, Popover | `getTrigger()`, `getDialog()`, `open()`, `close()` — pass `overlayType: 'modal'` or `'popover'` to `createTester` |
+| `'GridList'` | GridList | `getGridlist()`, `getRows()`, `getSelectedRows()`, `toggleRowSelection({row})`, `triggerRowAction({row})` |
+| `'ListBox'` | ListBox | `getListbox()`, `getOptions()`, `getSelectedOptions()`, `toggleOptionSelection({option})`, `triggerOptionAction({option})` |
+| `'Menu'` | Menu | `getTrigger()`, `getMenu()`, `getOptions()`, `open()`, `toggleOptionSelection({option})`, `openSubmenu({submenuTrigger})`, `close()` |
+| `'RadioGroup'` | RadioGroup | `getRadioGroup()`, `getRadios()`, `getSelectedRadio()`, `triggerRadio({radio})` |
+| `'Select'` | Select | `getTrigger()`, `getListbox()`, `getOptions()`, `toggleOptionSelection({option})` |
+| `'Table'` | Table | `getTable()`, `getRows()`, `getFooterRows()`, `getColumns()`, `getSelectedRows()`, `toggleRowSelection({row})`, `toggleSort({column})`, `triggerRowAction({row})` |
+| `'Tabs'` | Tabs | `getTablist()`, `getTabs()`, `getTabpanels()`, `getSelectedTab()`, `triggerTab({tab})` |
+| `'Tree'` | Tree | `getTree()`, `getRows()`, `getSelectedRows()`, `toggleRowSelection({row})`, `toggleRowExpansion({row})`, `triggerRowAction({row})` |
+
+### Per-component reference
+
+- [CheckboxGroup]({{testingBase}}CheckboxGroup/testing.md)
+- [ComboBox]({{testingBase}}ComboBox/testing.md)
+- [GridList]({{testingBase}}GridList/testing.md)
+- [ListBox]({{testingBase}}ListBox/testing.md)
+- [Menu]({{testingBase}}Menu/testing.md)
+- [Modal]({{testingBase}}Modal/testing.md)
+- [Popover]({{testingBase}}Popover/testing.md)
+- [RadioGroup]({{testingBase}}RadioGroup/testing.md)
+- [Select]({{testingBase}}Select/testing.md)
+- [Table]({{testingBase}}Table/testing.md)
+- [Tabs]({{testingBase}}Tabs/testing.md)
+- [Tree]({{testingBase}}Tree/testing.md)
diff --git a/packages/dev/s2-docs/skills/react-spectrum-s2/test-utils-guidance.md b/packages/dev/s2-docs/skills/react-spectrum-s2/test-utils-guidance.md
new file mode 100644
index 00000000000..74dad90b995
--- /dev/null
+++ b/packages/dev/s2-docs/skills/react-spectrum-s2/test-utils-guidance.md
@@ -0,0 +1,130 @@
+## Test utilities
+
+`@react-spectrum/test-utils` provides ARIA pattern testers that simulate mouse, keyboard, and touch interactions for components built with React Spectrum S2.
+
+### Installation
+
+```bash
+npm install @react-spectrum/test-utils --save-dev
+```
+
+### Core pattern
+
+External consumers import from `@react-spectrum/test-utils`. Tests inside the `packages/` monorepo should import everything from `@react-spectrum/test-utils-internal`, which re-exports `User` and all other test utilities:
+ ```ts
+import {act, render, User} from '@react-spectrum/test-utils-internal';
+```
+
+Initialize a `User` once per test file. Call `createTester` to get a tester for a specific ARIA pattern, then call tester methods to simulate interactions.
+
+```ts
+import {User} from '@react-spectrum/test-utils';
+
+// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers.
+// 'interactionType' specifies what mode of interaction should be simulated by the tester
+// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press)
+let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime});
+
+it('my test case', async function () {
+ // Render your test component/app
+ let {getByTestId} = render();
+ // Initialize the table tester via providing the 'Table' pattern name and the root element of said table
+ let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test_table')});
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
+
+ await tableTester.toggleSelectAll();
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
+ ...
+});
+```
+
+Set `interactionType` to `'mouse'`, `'keyboard'`, or `'touch'`. Override per tester via `createTester(..., {interactionType})` or per method call.
+
+When using fake timers, pass `advanceTimer: jest.advanceTimersByTime` and flush timers after each test:
+
+```ts
+afterEach(() => {
+ act(() => jest.runAllTimers());
+});
+```
+
+### Tips and Tricks
+- The testers typically offers these things: a way to simulate common user interactions for the given component via a specified user modality (e.g. using mouse vs keyboard to toggle a menu), a way to get the various common elements that make up the component (e.g. the rows in a table), and a way to query the state of the component (e.g. get the selected rows in a table). Prefer using the testers for these use cases so that the user doesn't need to know what specific roles/elements/etc to target in their tests.
+- You can still simulate interactions manually in your test alongside the utilities provided by the tester. This can come in handy if you find that the tester doesn't cover a specific user flow or if one of its utilities isn't quite working as expected. After simulating your interaction, you can still use the tester to query for the component's state or trigger a different interaction utility.
+- Mouse drag interactions and other mock reliant interactions are not available in these test utils since they depended heavily on how the user mocked various things in their test. These must still be simulated manually by the user.
+- Some testers may support the notion of "long press" for certain interactions (e.g. long pressing a button to trigger its menu). To simulate this, you will need to mock PointerEvent globally (see the `installPointerEvent` util) and provide a way to advance timers to the User via `advanceTimer`.
+- These test utils are compatible with not only JSDOM unit tests but browser tests as well (e.g. vitest-browser-react).
+- Methods that accept a target (`option`, `row`, `column`, `checkbox`, `radio`, `tab`) take a `number` (index), `string` (text content), or `HTMLElement`. Use the tester's own query methods (e.g. `getRows()`, `getOptions()`) to obtain an `HTMLElement` when you need one.
+- Link navigation assertions must be simulated manually. The testers do not assert navigation side effects.
+
+### When not to use the testers
+
+Skip the testers and write manual interactions for the following cases:
+
+- When testing a Menu or Dialog rendered without a trigger, or when testing interactive elements embedded inside rows or cells (e.g. an ActionMenu inside a TreeView row). The testers assume a trigger exists and do not reach into row/cell content.
+- tests that verify exact focus order, arrow key cycling, or specific modifier key behavior. Use `fireEvent.keyDown` or `userEvent.keyboard` directly so the test is actually testing the desired keyboard flow.
+- when `isOpen` or `defaultOpen` is set, `open()` will no-op but the tester's `root` must still resolve to the trigger element. Use `getByLabelText` or `getByTestId` rather than `getByRole('button')` to avoid ambiguity when multiple buttons are in the DOM.
+- testing `isDismissible`, `isKeyboardDismissDisabled`, or outside-click behavior. Use `userEvent.click(document.body)` or `user.keyboard('[Escape]')` directly and assert the expected state afterwards.
+- when a Dialog closes via an action button (not the explicit close/dismiss button) you should instead click that button manually, then use `dialogTester.getDialog()` to assert whether the dialog is still present.
+
+### Draggable handle components
+
+Components with draggable handles (Slider, ColorArea, ColorSlider, ColorWheel) need `getBoundingClientRect` mocked so move calculations work:
+
+```ts
+import {installMouseEvent} from '@react-spectrum/test-utils';
+installMouseEvent();
+
+beforeAll(() => {
+ jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(
+ () => ({top: 0, left: 0, width: 100, height: 10, bottom: 10, right: 100})
+ );
+});
+```
+
+### Available testers
+
+The pattern name passed to `createTester` is the ARIA pattern name — not the S2 component name.
+
+| Pattern name | S2 component | Key methods |
+|---|---|---|
+| `'CheckboxGroup'` | CheckboxGroup | `getCheckboxGroup()`, `getCheckboxes()`, `getSelectedCheckboxes()`, `toggleCheckbox({checkbox})` |
+| `'ComboBox'` | ComboBox | `getCombobox()`, `getListbox()`, `getOptions()`, `open()`, `toggleOptionSelection({option})` |
+| `'Dialog'` | Dialog | `getTrigger()`, `getDialog()`, `open()`, `close()` — pass `overlayType: 'modal'` or `'popover'` to `createTester` |
+| `'GridList'` | ListView | `getGridlist()`, `getRows()`, `getSelectedRows()`, `toggleRowSelection({row})`, `triggerRowAction({row})` |
+| `'Menu'` | Menu | `getTrigger()`, `getMenu()`, `getOptions()`, `open()`, `toggleOptionSelection({option})`, `openSubmenu({submenuTrigger})`, `close()` |
+| `'RadioGroup'` | RadioGroup | `getRadioGroup()`, `getRadios()`, `getSelectedRadio()`, `triggerRadio({radio})` |
+| `'Select'` | Picker | `getTrigger()`, `getListbox()`, `getOptions()`, `toggleOptionSelection({option})` |
+| `'Table'` | TableView | `getTable()`, `getRows()`, `getFooterRows()`, `getColumns()`, `getSelectedRows()`, `toggleRowSelection({row})`, `toggleSort({column})`, `triggerRowAction({row})` |
+| `'Tabs'` | Tabs | `getTablist()`, `getTabs()`, `getTabpanels()`, `getSelectedTab()`, `triggerTab({tab})` |
+| `'Tree'` | TreeView | `getTree()`, `getRows()`, `getSelectedRows()`, `toggleRowSelection({row})`, `toggleRowExpansion({row})`, `triggerRowAction({row})` |
+
+#### Dialog `overlayType` reference
+
+Pass `overlayType` to `createTester` so the tester knows how the overlay is mounted:
+
+| S2 component | `overlayType` |
+|---|---|
+| `Dialog` | `'modal'` |
+| `AlertDialog` | `'modal'` |
+| `CustomDialog` | `'modal'` |
+| Popover-based dialogs | `'popover'` |
+
+```ts
+let dialogTester = testUtilUser.createTester('Dialog', {root: tree.getByRole('button'), overlayType: 'modal'});
+await dialogTester.open();
+expect(dialogTester.getDialog()).toBeVisible();
+```
+
+### Per-component reference
+
+- [CheckboxGroup]({{testingBase}}CheckboxGroup/testing.md)
+- [ComboBox]({{testingBase}}ComboBox/testing.md)
+- [Dialog]({{testingBase}}Dialog/testing.md)
+- [ListView]({{testingBase}}ListView/testing.md)
+- [Menu]({{testingBase}}Menu/testing.md)
+- [Picker]({{testingBase}}Picker/testing.md)
+- [RadioGroup]({{testingBase}}RadioGroup/testing.md)
+- [TableView]({{testingBase}}TableView/testing.md)
+- [Tabs]({{testingBase}}Tabs/testing.md)
+- [TreeView]({{testingBase}}TreeView/testing.md)
diff --git a/packages/react-aria-components/docs/ComboBox.mdx b/packages/react-aria-components/docs/ComboBox.mdx
index 30b302628f1..0751df477b3 100644
--- a/packages/react-aria-components/docs/ComboBox.mdx
+++ b/packages/react-aria-components/docs/ComboBox.mdx
@@ -1599,12 +1599,12 @@ it('ComboBox can select an option via keyboard', async function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'});
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeInTheDocument();
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
- let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ let options = comboboxTester.getOptions();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.getCombobox().value).toBe('One');
+ expect(comboboxTester.getListbox()).not.toBeInTheDocument();
});
```
diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx
index 904d5aa7806..debf6d418ab 100644
--- a/packages/react-aria-components/docs/GridList.mdx
+++ b/packages/react-aria-components/docs/GridList.mdx
@@ -2107,17 +2107,17 @@ it('GridList can select a row via keyboard', async function () {
);
let gridListTester = testUtilUser.createTester('GridList', {root: getByTestId('test-gridlist'), interactionType: 'keyboard'});
- let row = gridListTester.rows[0];
+ let row = gridListTester.getRows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/react-aria-components/docs/ListBox.mdx b/packages/react-aria-components/docs/ListBox.mdx
index 2467be7be0c..8537fbae916 100644
--- a/packages/react-aria-components/docs/ListBox.mdx
+++ b/packages/react-aria-components/docs/ListBox.mdx
@@ -2206,7 +2206,7 @@ it('ListBox can select an option via keyboard', async function () {
let listboxTester = testUtilUser.createTester('ListBox', {root: getByTestId('test-listbox'), interactionType: 'keyboard'});
await listboxTester.toggleOptionSelection({option: 4});
- expect(listboxTester.options()[4]).toHaveAttribute('aria-selected', 'true');
+ expect(listboxTester.getOptions()[4]).toHaveAttribute('aria-selected', 'true');
});
```
diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx
index a9d8f923f5d..98e0cc5232b 100644
--- a/packages/react-aria-components/docs/Menu.mdx
+++ b/packages/react-aria-components/docs/Menu.mdx
@@ -1247,16 +1247,16 @@ it('Menu can open its submenu via keyboard', async function () {
let menuTester = testUtilUser.createTester('Menu', {root: getByTestId('test-menutrigger'), interactionType: 'keyboard'});
await menuTester.open();
- expect(menuTester.menu).toBeInTheDocument();
- let submenuTriggers = menuTester.submenuTriggers;
+ expect(menuTester.getMenu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.getSubmenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.getMenu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.getOptions()[0]});
+ expect(submenuTester.getMenu()).not.toBeInTheDocument();
+ expect(menuTester.getMenu()).not.toBeInTheDocument();
});
```
diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx
index 1ef1318eef6..921e113a509 100644
--- a/packages/react-aria-components/docs/Select.mdx
+++ b/packages/react-aria-components/docs/Select.mdx
@@ -1364,10 +1364,10 @@ it('Select can select an option via keyboard', async function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('test-select'), interactionType: 'keyboard'});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an item');
- await selectTester.selectOption({option: 'Cat'});
+ await selectTester.toggleOptionSelection({option: 'Cat'});
expect(trigger).toHaveTextContent('Cat');
});
```
diff --git a/packages/react-aria-components/docs/Table.mdx b/packages/react-aria-components/docs/Table.mdx
index 6dc90486c98..6d96b08ae72 100644
--- a/packages/react-aria-components/docs/Table.mdx
+++ b/packages/react-aria-components/docs/Table.mdx
@@ -2695,22 +2695,22 @@ it('Table can toggle row selection', async function () {
);
let tableTester = testUtilUser.createTester('Table', {root: getByTestId('test-table')});
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.getSelectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.getRows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.getSelectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
});
```
diff --git a/packages/react-aria-components/docs/Tabs.mdx b/packages/react-aria-components/docs/Tabs.mdx
index 32226da769e..b77ec14d504 100644
--- a/packages/react-aria-components/docs/Tabs.mdx
+++ b/packages/react-aria-components/docs/Tabs.mdx
@@ -835,11 +835,11 @@ it('Tabs can change selection via keyboard', async function () {
);
let tabsTester = testUtilUser.createTester('Tabs', {root: getByTestId('test-tabs'), interactionType: 'keyboard'});
- let tabs = tabsTester.tabs;
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ let tabs = tabsTester.getTabs();
+ expect(tabsTester.getSelectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[1]);
});
```
diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx
index 8f6668bcab3..9ac5198ade1 100644
--- a/packages/react-aria-components/docs/Tree.mdx
+++ b/packages/react-aria-components/docs/Tree.mdx
@@ -2193,16 +2193,16 @@ it('Tree can select a item via keyboard', async function () {
let treeTester = testUtilUser.createTester('Tree', {root: getByTestId('test-tree'), interactionType: 'keyboard'});
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(2);
+ expect(within(treeTester.getRows()[1]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 0});
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).not.toBeChecked();
});
```
diff --git a/packages/react-aria-components/test/AriaMenu.test-util.tsx b/packages/react-aria-components/test/AriaMenu.test-util.tsx
index 0d19040bb7f..2500c7dde4e 100644
--- a/packages/react-aria-components/test/AriaMenu.test-util.tsx
+++ b/packages/react-aria-components/test/AriaMenu.test-util.tsx
@@ -58,6 +58,8 @@ interface AriaMenuTestProps extends AriaBaseTestProps {
longPress?: (props?: {name: string}) => ReturnType;
// Menu must have two levels of submenus
submenus?: (props?: {name: string}) => ReturnType;
+ // Menu must have a disabled submenu trigger
+ disabledSubmenuTrigger?: (props?: {name: string}) => ReturnType;
};
}
export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): void => {
@@ -97,7 +99,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
it('has default behavior (button renders, menu is closed)', function () {
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.getTrigger()!;
expect(triggerButton).toBeTruthy();
expect(triggerButton).toHaveAttribute('aria-haspopup', 'true');
@@ -105,7 +107,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let buttonText = within(triggerButton).getByText(triggerText);
expect(buttonText).toBeTruthy();
- expect(menuTester.menu).toBeFalsy();
+ expect(menuTester.getMenu()).toBeFalsy();
expect(triggerButton).toHaveAttribute('aria-expanded', 'false');
expect(triggerButton).toHaveAttribute('type', 'button');
@@ -114,14 +116,14 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
it('toggles the menu display on button click', async function () {
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.getTrigger()!;
await menuTester.open();
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu!;
+ let menu = menuTester.getMenu()!;
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
expect(menu).toHaveFocus();
@@ -140,7 +142,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
it('will not close the menu when mousing over the trigger again without lifting press', function () {
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.getTrigger()!;
fireEvent.mouseEnter(triggerButton);
fireEvent.mouseDown(triggerButton, {detail: 1});
@@ -155,14 +157,14 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
it('closes the menu on click outside', async function () {
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.getTrigger()!;
await menuTester.open();
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu!;
+ let menu = menuTester.getMenu()!;
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
expect(menu).toHaveFocus();
@@ -187,16 +189,16 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
menuTester.setInteractionType('keyboard');
- let triggerButton = menuTester.trigger;
+ let triggerButton = menuTester.getTrigger();
await menuTester.open();
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeTruthy();
- let options = menuTester.options();
+ let options = menuTester.getOptions();
expect(options[0]).toHaveFocus();
await menuTester.close();
@@ -212,16 +214,16 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
menuTester.setInteractionType('keyboard');
- let triggerButton = menuTester.trigger;
+ let triggerButton = menuTester.getTrigger();
await menuTester.open({direction: 'down'});
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeTruthy();
- let options = menuTester.options();
+ let options = menuTester.getOptions();
expect(options[0]).toHaveFocus();
await menuTester.close();
@@ -237,16 +239,16 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
menuTester.setInteractionType('keyboard');
- let triggerButton = menuTester.trigger;
+ let triggerButton = menuTester.getTrigger();
await menuTester.open({direction: 'up'});
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeTruthy();
- let options = menuTester.options();
+ let options = menuTester.getOptions();
expect(options[options.length - 1]).toHaveFocus();
await menuTester.close();
@@ -262,18 +264,18 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let tree = renderers.standard();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
menuTester.setInteractionType('keyboard');
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.getTrigger()!;
await menuTester.open();
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
- let options = menuTester.options();
+ let options = menuTester.getOptions();
expect(options[0]).toHaveFocus();
await user.keyboard('[ArrowUp]');
@@ -296,7 +298,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
await user.keyboard('[Space]');
act(() => {
@@ -308,7 +310,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
act(() => {
jest.runAllTimers();
});
- menu = menuTester.menu;
+ menu = menuTester.getMenu();
await user.keyboard('[Enter]');
act(() => {
@@ -327,7 +329,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
let buttons = tree.getAllByLabelText('Dismiss');
expect(buttons.length).toBe(2);
@@ -346,20 +348,20 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
it('selects an option via mouse', async function () {
let tree = renderers.singleSelection!();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.getTrigger()!;
await menuTester.open();
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
- let options = menuTester.options();
+ let options = menuTester.getOptions();
- await menuTester.selectOption({option: options[1], menuSelectionMode: 'single'});
+ await menuTester.toggleOptionSelection({option: options[1], menuSelectionMode: 'single'});
act(() => {
jest.runAllTimers();
@@ -371,7 +373,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- options = menuTester.options();
+ options = menuTester.getOptions();
expect(options[0]).toHaveAttribute('aria-checked', 'false');
expect(options[1]).toHaveAttribute('aria-checked', 'true');
});
@@ -383,21 +385,21 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
root: tree.container,
interactionType: 'keyboard'
});
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.getTrigger()!;
await menuTester.open();
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu!;
+ let menu = menuTester.getMenu()!;
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
- let options = menuTester.options();
+ let options = menuTester.getOptions();
expect(options[0]).toHaveFocus();
- await menuTester.selectOption({option: options[1], menuSelectionMode: 'single'});
+ await menuTester.toggleOptionSelection({option: options[1], menuSelectionMode: 'single'});
act(() => {
jest.runAllTimers();
@@ -409,7 +411,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- options = menuTester.options();
+ options = menuTester.getOptions();
expect(options[0]).toHaveAttribute('aria-checked', 'false');
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[1]).toHaveFocus();
@@ -421,7 +423,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- options = menuTester.options();
+ options = menuTester.getOptions();
expect(options[0]).toHaveAttribute('aria-checked', 'false');
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[1]).toHaveFocus();
@@ -431,18 +433,18 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let tree = renderers.singleSelection!();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
menuTester.setInteractionType('keyboard');
- let triggerButton = menuTester.trigger;
+ let triggerButton = menuTester.getTrigger();
await menuTester.open();
act(() => {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
- let options = menuTester.options();
+ let options = menuTester.getOptions();
expect(options[0]).toHaveFocus();
await user.keyboard('[ArrowDown]');
@@ -468,11 +470,11 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
fireEvent.keyDown(document.activeElement!, {key: 'Enter', repeat: true});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeInTheDocument();
await user.keyboard('{/Enter}');
expect(
- menuTester.options().filter(option => option.getAttribute('aria-checked') === 'true')
+ menuTester.getOptions().filter(option => option.getAttribute('aria-checked') === 'true')
.length
).toBe(0);
});
@@ -490,11 +492,17 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- let menu = menuTester.menu;
- let options = menuTester.options();
+ let menu = menuTester.getMenu();
+ let options = menuTester.getOptions();
- await menuTester.selectOption({option: options[2], menuSelectionMode: 'multiple'});
- await menuTester.selectOption({option: options[1], menuSelectionMode: 'multiple'});
+ await menuTester.toggleOptionSelection({
+ option: options[2],
+ menuSelectionMode: 'multiple'
+ });
+ await menuTester.toggleOptionSelection({
+ option: options[1],
+ menuSelectionMode: 'multiple'
+ });
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[2]).toHaveAttribute('aria-checked', 'true');
@@ -515,8 +523,8 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- menu = menuTester.menu;
- options = menuTester.options();
+ menu = menuTester.getMenu();
+ options = menuTester.getOptions();
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[2]).toHaveAttribute('aria-checked', 'true');
expect(options[2]).toHaveFocus();
@@ -532,8 +540,8 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- let menu = menuTester.menu;
- let options = menuTester.options();
+ let menu = menuTester.getMenu();
+ let options = menuTester.getOptions();
expect(options[0]).toHaveFocus();
await user.keyboard('[ArrowDown]');
@@ -560,8 +568,8 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- menu = menuTester.menu;
- options = menuTester.options();
+ menu = menuTester.getMenu();
+ options = menuTester.getOptions();
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[2]).toHaveAttribute('aria-checked', 'true');
expect(options[1]).toHaveFocus();
@@ -577,8 +585,8 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- let menu = menuTester.menu;
- let options = menuTester.options();
+ let menu = menuTester.getMenu();
+ let options = menuTester.getOptions();
expect(options[0]).toHaveFocus();
await user.keyboard('[ArrowDown]');
@@ -594,8 +602,8 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
- menu = menuTester.menu;
- options = menuTester.options();
+ menu = menuTester.getMenu();
+ options = menuTester.getOptions();
expect(options[0]).toHaveAttribute('aria-checked', 'false');
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[2]).toHaveAttribute('aria-checked', 'false');
@@ -613,11 +621,11 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
fireEvent.keyDown(document.activeElement!, {key: 'Enter', repeat: true});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
expect(menu).toBeInTheDocument();
await user.keyboard('{/Enter}');
expect(
- menuTester.options().filter(option => option.getAttribute('aria-checked') === 'true')
+ menuTester.getOptions().filter(option => option.getAttribute('aria-checked') === 'true')
.length
).toBe(0);
});
@@ -629,7 +637,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
it('does not trigger', async function () {
let tree = renderers.disabledTrigger!();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
- let triggerButton = menuTester.trigger;
+ let triggerButton = menuTester.getTrigger();
await menuTester.open();
act(() => {
@@ -680,9 +688,9 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
interactionType
});
await menuTester.open();
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
- let submenuTrigger = menuTester.submenuTriggers[0]!;
+ let submenuTrigger = menuTester.getSubmenuTriggers()[0]!;
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'false');
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
@@ -690,17 +698,17 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.getMenu();
expect(submenu).toBeInTheDocument();
- await submenuUtil.selectOption({
+ await submenuUtil.toggleOptionSelection({
option: submenuUtil
- .options()
+ .getOptions()
.filter(item => item.getAttribute('aria-haspopup') == null)[0]
});
expect(menu).not.toBeInTheDocument();
expect(submenu).not.toBeInTheDocument();
- expect(document.activeElement).toBe(menuTester.trigger);
+ expect(document.activeElement).toBe(menuTester.getTrigger());
});
it('should support nested submenu triggers', async () => {
@@ -712,36 +720,51 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
interactionType
});
await menuTester.open();
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
- let submenuTrigger = menuTester.submenuTriggers[0];
+ let submenuTrigger = menuTester.getSubmenuTriggers()[0];
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'false');
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.getMenu();
expect(submenu).toBeInTheDocument();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.getSubmenuTriggers()[0];
expect(nestedSubmenuTrigger).toHaveAttribute('aria-expanded', 'false');
let nestedSubmenuUtil = (await submenuUtil.openSubmenu({
submenuTrigger: nestedSubmenuTrigger
}))!;
expect(nestedSubmenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let nestedSubmenu = nestedSubmenuUtil.menu;
+ let nestedSubmenu = nestedSubmenuUtil.getMenu();
expect(nestedSubmenu).toBeInTheDocument();
- await nestedSubmenuUtil.selectOption({
+ await nestedSubmenuUtil.toggleOptionSelection({
option: nestedSubmenuUtil
- .options()
+ .getOptions()
.filter(item => item.getAttribute('aria-haspopup') == null)[0]
});
expect(menu).not.toBeInTheDocument();
expect(submenu).not.toBeInTheDocument();
expect(nestedSubmenu).not.toBeInTheDocument();
- expect(document.activeElement).toBe(menuTester.trigger);
+ expect(document.activeElement).toBe(menuTester.getTrigger());
});
+
+ if (renderers.disabledSubmenuTrigger) {
+ it('doesnt open a submenu if its trigger is disabled', async () => {
+ let tree = renderers.disabledSubmenuTrigger!();
+ let menuTester = testUtilUser.createTester('Menu', {
+ user,
+ root: tree.container,
+ interactionType
+ });
+ await expect(menuTester.openSubmenu({submenuTrigger: 'Share…'})).rejects.toThrow();
+ expect(menuTester.getMenu()).toBeInTheDocument();
+ expect(menuTester.getSubmenuTriggers()[0]).toHaveAttribute('aria-disabled');
+ expect(tree.getAllByRole('menu', {hidden: true})).toHaveLength(1);
+ });
+ }
});
describe('submenu specific interaction tests', function () {
@@ -750,9 +773,9 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
- let submenuTrigger = menuTester.submenuTriggers[0];
+ let submenuTrigger = menuTester.getSubmenuTriggers()[0];
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'false');
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
@@ -760,10 +783,10 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.getMenu();
expect(submenu).toBeInTheDocument();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.getSubmenuTriggers()[0];
expect(nestedSubmenuTrigger).toHaveAttribute('aria-expanded', 'false');
let nestedSubmenuUtil = (await submenuUtil.openSubmenu({
@@ -773,7 +796,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
jest.runAllTimers();
});
expect(nestedSubmenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let nestedSubmenu = nestedSubmenuUtil.menu;
+ let nestedSubmenu = nestedSubmenuUtil.getMenu();
expect(submenu).toBeInTheDocument();
await user.click(document.body);
@@ -794,23 +817,23 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
interactionType: 'keyboard'
});
await menuTester.open();
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
- let submenuTrigger = menuTester.submenuTriggers[0];
+ let submenuTrigger = menuTester.getSubmenuTriggers()[0];
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
act(() => {
jest.runAllTimers();
});
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.getMenu();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.getSubmenuTriggers()[0];
let nestedSubmenuUtil = (await submenuUtil.openSubmenu({
submenuTrigger: nestedSubmenuTrigger
}))!;
act(() => {
jest.runAllTimers();
});
- let nestedSubmenu = nestedSubmenuUtil.menu;
+ let nestedSubmenu = nestedSubmenuUtil.getMenu();
await user.keyboard('[Escape]');
act(() => {
@@ -829,24 +852,24 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let tree = renderers.submenus!();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- let menu = menuTester.menu;
- let submenuTrigger = menuTester.submenuTriggers[0];
+ let menu = menuTester.getMenu();
+ let submenuTrigger = menuTester.getSubmenuTriggers()[0];
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
act(() => {
jest.runAllTimers();
});
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.getMenu();
expect(submenu).toBeInTheDocument();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.getSubmenuTriggers()[0];
let nestedSubmenuUtil = (await submenuUtil.openSubmenu({
submenuTrigger: nestedSubmenuTrigger
}))!;
act(() => {
jest.runAllTimers();
});
- let nestedSubmenu = nestedSubmenuUtil.menu;
+ let nestedSubmenu = nestedSubmenuUtil.getMenu();
expect(submenu).toBeInTheDocument();
- await user.hover(menuTester.options()[0]);
+ await user.hover(menuTester.getOptions()[0]);
act(() => {
jest.runAllTimers();
});
@@ -859,12 +882,12 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let tree = renderers.submenus!();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- await user.hover(menuTester.submenuTriggers[0]);
+ await user.hover(menuTester.getSubmenuTriggers()[0]);
act(() => {
jest.runAllTimers();
});
- expect(menuTester.submenuTriggers[0]).toHaveAttribute('aria-expanded', 'true');
- expect(document.activeElement).toBe(menuTester.submenuTriggers[0]);
+ expect(menuTester.getSubmenuTriggers()[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(document.activeElement).toBe(menuTester.getSubmenuTriggers()[0]);
// It should also allow the user to move focus into the submenu via ArrowRight
await user.keyboard('{ArrowRight}');
diff --git a/packages/react-aria-components/test/AriaTree.test-util.tsx b/packages/react-aria-components/test/AriaTree.test-util.tsx
index 90c0d41745c..46dba784d11 100644
--- a/packages/react-aria-components/test/AriaTree.test-util.tsx
+++ b/packages/react-aria-components/test/AriaTree.test-util.tsx
@@ -76,16 +76,16 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
it('should have the base set of aria and data attributes', () => {
let root = renderers.standard!();
let treeTester = testUtilUser.createTester('Tree', {user, root: root.container});
- let tree = treeTester.tree;
+ let tree = treeTester.getTree();
expect(tree).toHaveAttribute('aria-label');
- for (let row of treeTester.rows) {
+ for (let row of treeTester.getRows()) {
expect(row).toHaveAttribute('aria-level');
expect(row).toHaveAttribute('aria-posinset');
expect(row).toHaveAttribute('aria-setsize');
}
- expect(treeTester.rows[0]).not.toHaveAttribute('aria-expanded');
- expect(treeTester.rows[1]).toHaveAttribute('aria-expanded', 'false');
+ expect(treeTester.getRows()[0]).not.toHaveAttribute('aria-expanded');
+ expect(treeTester.getRows()[1]).toHaveAttribute('aria-expanded', 'false');
});
describeInteractions('interaction', function ({interactionType}) {
@@ -99,7 +99,7 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
await treeTester.toggleRowExpansion({row: 1});
await treeTester.toggleRowExpansion({row: 2});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
let rowNoChild = rows[0];
expect(rowNoChild).toHaveAttribute('aria-label');
expect(rowNoChild).not.toHaveAttribute('aria-expanded');
@@ -158,7 +158,7 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
interactionType
});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows[0]).toHaveAttribute('aria-selected', 'false');
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
// disabled rows should not be selectable
@@ -168,23 +168,23 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
await treeTester.toggleRowSelection({row: 0});
expect(rows[0]).toHaveAttribute('aria-selected', 'true');
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
expect(rows[0]).toHaveAttribute('aria-selected', 'false');
expect(rows[1]).toHaveAttribute('aria-selected', 'true');
- expect(treeTester.selectedRows).toHaveLength(1);
- expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked();
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.getSelectedRows()).toHaveLength(1);
+ expect(within(treeTester.getRows()[0]).getByRole('checkbox')).not.toBeChecked();
+ expect(within(treeTester.getRows()[1]).getByRole('checkbox')).toBeChecked();
- await treeTester.toggleRowSelection({row: 2});
+ await expect(treeTester.toggleRowSelection({row: 2})).rejects.toThrow();
expect(rows[0]).toHaveAttribute('aria-selected', 'false');
expect(rows[1]).toHaveAttribute('aria-selected', 'true');
expect(rows[2]).not.toHaveAttribute('aria-selected');
await treeTester.toggleRowExpansion({row: 1});
- rows = treeTester.rows;
+ rows = treeTester.getRows();
// row 2 is now the subrow of row 1 because we expanded it
expect(rows[2]).toHaveAttribute('aria-selected', 'false');
@@ -196,7 +196,7 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
// collapse and re-expand to make sure the selection persists
await treeTester.toggleRowExpansion({row: 1});
await treeTester.toggleRowExpansion({row: 1});
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows[2]).toHaveAttribute('aria-selected', 'true');
await treeTester.toggleRowSelection({row: 2});
@@ -207,7 +207,7 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
await treeTester.toggleRowExpansion({row: 1});
// items inside a disabled item can be selected
await treeTester.toggleRowExpansion({row: 2});
- rows = treeTester.rows;
+ rows = treeTester.getRows();
await treeTester.toggleRowSelection({row: 3});
expect(rows[3]).toHaveAttribute('aria-selected', 'true');
@@ -227,13 +227,12 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
interactionType
});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows[2]).toHaveAttribute('aria-expanded', 'false');
- await treeTester.toggleRowExpansion({row: 2});
+ await expect(treeTester.toggleRowExpansion({row: 2})).rejects.toThrow();
+ await expect(treeTester.toggleRowSelection({row: 2})).rejects.toThrow();
expect(rows[2]).toHaveAttribute('aria-expanded', 'false');
-
- await treeTester.toggleRowSelection({row: 2});
expect(rows[2]).not.toHaveAttribute('aria-selected');
});
});
diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js
index d740b2ccdde..dfa993ecef8 100644
--- a/packages/react-aria-components/test/ComboBox.test.js
+++ b/packages/react-aria-components/test/ComboBox.test.js
@@ -156,11 +156,11 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('p');
- let groups = comboboxTester.sections;
+ let groups = comboboxTester.getSections();
expect(groups).toHaveLength(1);
expect(groups[0]).toHaveAttribute('aria-labelledby');
expect(document.getElementById(groups[0].getAttribute('aria-labelledby'))).toHaveTextContent(
@@ -196,11 +196,11 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('p');
- let groups = comboboxTester.sections;
+ let groups = comboboxTester.getSections();
expect(groups).toHaveLength(1);
expect(groups[0]).toHaveAttribute('aria-labelledby');
expect(document.getElementById(groups[0].getAttribute('aria-labelledby'))).toHaveTextContent(
@@ -232,10 +232,10 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('c');
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
expect(options).toHaveLength(1);
});
@@ -280,6 +280,33 @@ describe('ComboBox', () => {
expect(document.querySelector('input[type=hidden]')).toBeNull();
});
+ it('should support selecting an option via keyboard', async () => {
+ let onSelectionChange = jest.fn();
+ let tree = render(
+
+
+
+
+
+
+ Cat
+ Dog
+ Kangaroo
+
+
+
+ );
+ let comboboxTester = testUtilUser.createTester('ComboBox', {
+ root: tree.container,
+ interactionType: 'keyboard'
+ });
+
+ await comboboxTester.toggleOptionSelection({option: 'Dog'});
+ expect(onSelectionChange).toHaveBeenCalledWith('2');
+ expect(comboboxTester.getCombobox()).toHaveValue('Dog');
+ expect(comboboxTester.getListbox()).toBeNull();
+ });
+
it.each(['click', 'tab'])(
'should not fire extra onSelectionChange calls after focus moves away in fully controlled mode via %s',
async focusMove => {
@@ -324,14 +351,14 @@ describe('ComboBox', () => {
let tree = render();
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- await comboboxTester.selectOption({option: 'Dog'});
+ await comboboxTester.toggleOptionSelection({option: 'Dog'});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
if (focusMove === 'click') {
await user.click(tree.getByRole('button', {name: 'Next'}));
} else {
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.tab();
}
@@ -361,12 +388,12 @@ describe('ComboBox', () => {
);
const comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- const combobox = comboboxTester.combobox;
+ const combobox = comboboxTester.getCombobox();
expect(combobox).toHaveValue('Dog');
await comboboxTester.open();
- const options = comboboxTester.options();
+ const options = comboboxTester.getOptions();
await user.click(options[0]);
expect(combobox).toHaveValue('Cat');
@@ -402,7 +429,7 @@ describe('ComboBox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.getCombobox();
expect(combobox).toHaveAttribute('required');
expect(combobox).not.toHaveAttribute('aria-required');
@@ -423,11 +450,11 @@ describe('ComboBox', () => {
expect(comboboxWrapper).toHaveAttribute('data-invalid');
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('C');
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
await user.click(options[0]);
expect(combobox).toHaveAttribute('aria-describedby');
@@ -501,11 +528,11 @@ describe('ComboBox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.getListbox()).toBeFalsy();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
- expect(comboboxTester.options()).toHaveLength(7);
+ expect(comboboxTester.getOptions()).toHaveLength(7);
});
it('should clear contexts inside popover', async () => {
@@ -557,13 +584,13 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('p');
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
expect(options).toHaveLength(1);
- expect(comboboxTester.listbox).toBeTruthy();
+ expect(comboboxTester.getListbox()).toBeTruthy();
expect(options[0]).toHaveTextContent('No results');
});
@@ -603,12 +630,12 @@ describe('ComboBox', () => {
let tree = render();
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.getCombobox().focus();
});
await user.keyboard('L');
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('Create "L"');
@@ -618,14 +645,14 @@ describe('ComboBox', () => {
await user.click(options[0]);
}
expect(onAction).toHaveBeenCalledTimes(1);
- expect(comboboxTester.combobox).toHaveValue('');
+ expect(comboboxTester.getCombobox()).toHaveValue('');
// Repeat with an option selected.
- await comboboxTester.selectOption({option: 'Cat'});
+ await comboboxTester.toggleOptionSelection({option: 'Cat'});
await user.keyboard('s');
- options = comboboxTester.options();
+ options = comboboxTester.getOptions();
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('Create "Cats"');
@@ -635,7 +662,7 @@ describe('ComboBox', () => {
await user.click(options[0]);
}
expect(onAction).toHaveBeenCalledTimes(2);
- expect(comboboxTester.combobox).toHaveValue('Cat');
+ expect(comboboxTester.getCombobox()).toHaveValue('Cat');
});
it('should not close the combobox when clicking on a section header', async () => {
@@ -690,7 +717,7 @@ describe('ComboBox', () => {
expect(listbox).toBeVisible();
// Verify we can still interact with options
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
expect(options.length).toBeGreaterThan(0);
// Click an option
@@ -701,7 +728,7 @@ describe('ComboBox', () => {
// Verify the combobox is closed and the value is updated
expect(tree.queryByRole('listbox')).toBeNull();
- expect(comboboxTester.combobox).toHaveValue('Apple');
+ expect(comboboxTester.getCombobox()).toHaveValue('Apple');
});
it('should support multiple selection', async () => {
@@ -721,26 +748,26 @@ describe('ComboBox', () => {
let value = container.querySelector('.react-aria-ComboBoxValue');
expect(value).toHaveTextContent('No items selected');
- expect(comboboxTester.combobox.getAttribute('aria-describedby')).toContain(value.id);
+ expect(comboboxTester.getCombobox().getAttribute('aria-describedby')).toContain(value.id);
- expect(comboboxTester.combobox).toHaveValue('');
+ expect(comboboxTester.getCombobox()).toHaveValue('');
await comboboxTester.open();
- let listbox = comboboxTester.listbox;
+ let listbox = comboboxTester.getListbox();
expect(listbox).toHaveAttribute('aria-multiselectable', 'true');
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
expect(options).toHaveLength(3);
- await user.click(options[0]);
+ await comboboxTester.toggleOptionSelection({option: options[0]});
expect(options[0]).toHaveAttribute('aria-selected', 'true');
- expect(comboboxTester.combobox).toHaveValue('');
- expect(comboboxTester.listbox).toBeInTheDocument();
+ expect(comboboxTester.getCombobox()).toHaveValue('');
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
expect(value).toHaveTextContent('Cat');
- await user.click(options[1]);
+ await comboboxTester.toggleOptionSelection({option: options[1]});
expect(options[1]).toHaveAttribute('aria-selected', 'true');
- expect(comboboxTester.combobox).toHaveValue('');
- expect(comboboxTester.listbox).toBeInTheDocument();
+ expect(comboboxTester.getCombobox()).toHaveValue('');
+ expect(comboboxTester.getListbox()).toBeInTheDocument();
expect(value).toHaveTextContent('Cat and Dog');
await comboboxTester.close();
@@ -751,21 +778,52 @@ describe('ComboBox', () => {
expect(formData.getAll('combobox')).toEqual(['1', '2']);
await user.click(document.querySelector('input[type="reset"]'));
- expect(comboboxTester.combobox).toHaveValue('');
+ expect(comboboxTester.getCombobox()).toHaveValue('');
formData = new FormData(getByTestId('form'));
expect(formData.getAll('combobox')).toEqual(['']);
});
+ it('should support deselection if multiple selection is enabled', async () => {
+ let onChange = jest.fn();
+ let {container} = render(
+
+ );
+ let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
+
+ await comboboxTester.toggleOptionSelection({option: 'Cat'});
+ await comboboxTester.toggleOptionSelection({option: 'Dog'});
+ expect(comboboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(comboboxTester.getOptions()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['1', '2']);
+
+ await comboboxTester.toggleOptionSelection({option: 'Cat'});
+ expect(comboboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(comboboxTester.getOptions()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['2']);
+
+ await comboboxTester.toggleOptionSelection({option: 'Dog'});
+ expect(comboboxTester.getOptions()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(comboboxTester.getOptions()[1]).toHaveAttribute('aria-selected', 'false');
+ expect(onChange).toHaveBeenLastCalledWith([]);
+
+ await comboboxTester.close();
+ });
+
it('should support controlled multi-selection', async () => {
let {container} = render(
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
- expect(comboboxTester.combobox).toHaveValue('');
+ expect(comboboxTester.getCombobox()).toHaveValue('');
await comboboxTester.open();
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
expect(options[0]).toHaveAttribute('aria-selected', 'false');
expect(options[1]).toHaveAttribute('aria-selected', 'true');
expect(options[2]).toHaveAttribute('aria-selected', 'true');
@@ -786,7 +844,7 @@ describe('ComboBox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.getCombobox();
expect(combobox).toHaveValue('C');
await comboboxTester.open();
@@ -815,10 +873,10 @@ describe('ComboBox', () => {
await user.tab();
await user.keyboard('Test');
- expect(comboboxTester.combobox).toHaveValue('Test');
+ expect(comboboxTester.getCombobox()).toHaveValue('Test');
await user.tab();
- expect(comboboxTester.combobox).toHaveValue('Test');
+ expect(comboboxTester.getCombobox()).toHaveValue('Test');
});
it.each(['{Escape}', '{Enter}', '{Tab}'])(
@@ -839,12 +897,12 @@ describe('ComboBox', () => {
await user.tab();
await user.keyboard('Den');
- expect(comboboxTester.combobox).toHaveValue('Den');
+ expect(comboboxTester.getCombobox()).toHaveValue('Den');
await user.keyboard(key);
expect(queryByRole('listbox')).toBeNull();
- expect(comboboxTester.combobox).toHaveValue('Den');
+ expect(comboboxTester.getCombobox()).toHaveValue('Den');
expect(value).toHaveTextContent('Cat and Dog');
}
);
@@ -904,7 +962,7 @@ describe('ComboBox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.getCombobox();
expect(combobox).toHaveAttribute('required');
expect(combobox.validity.valid).toBe(false);
@@ -916,7 +974,7 @@ describe('ComboBox', () => {
expect(container.querySelector('.react-aria-ComboBox')).toHaveAttribute('data-invalid');
await comboboxTester.open();
- let options = comboboxTester.options();
+ let options = comboboxTester.getOptions();
await user.click(options[0]);
act(() => combobox.blur());
@@ -930,7 +988,7 @@ describe('ComboBox', () => {
expect(hiddenInputs[0]).toHaveAttribute('value', '1');
await comboboxTester.open();
- options = comboboxTester.options();
+ options = comboboxTester.getOptions();
await user.click(options[0]);
act(() => combobox.blur());
expect(combobox).toHaveAttribute('required');
@@ -951,8 +1009,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.getListbox()).toBeVisible();
+ expect(comboboxTester.getCombobox()).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();
@@ -960,8 +1018,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.getListbox()).toBeVisible();
+ expect(comboboxTester.getCombobox()).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(0);
});
@@ -973,7 +1031,7 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
+ expect(comboboxTester.getListbox()).toBeVisible();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();
@@ -981,8 +1039,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeNull();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.getListbox()).toBeNull();
+ expect(comboboxTester.getCombobox()).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();
@@ -990,8 +1048,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.getListbox()).toBeVisible();
+ expect(comboboxTester.getCombobox()).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
});
diff --git a/packages/react-aria-components/test/Dialog.browser.test.tsx b/packages/react-aria-components/test/Dialog.browser.test.tsx
new file mode 100644
index 00000000000..5b5b622accd
--- /dev/null
+++ b/packages/react-aria-components/test/Dialog.browser.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {Button} from '../src/Button';
+import {Dialog, DialogTrigger} from '../src/Dialog';
+import {expect, it} from 'vitest';
+import {Heading} from '../src/Heading';
+import {Modal} from '../src/Modal';
+import React from 'react';
+import {render} from 'vitest-browser-react';
+import {User} from '@react-aria/test-utils';
+
+function DialogExample() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('opens via $interactionType and closes', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('Dialog', {
+ root: container.querySelector('button') as HTMLElement,
+ interactionType
+ });
+
+ await tester.open();
+ expect(tester.getDialog()).not.toBeNull();
+ expect(tester.getDialog()!.contains(document.activeElement)).toBe(true);
+
+ await tester.close();
+ expect(tester.getDialog()).toBeNull();
+});
diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js
index 7864e5cd002..9621e7c90d8 100644
--- a/packages/react-aria-components/test/Dialog.test.js
+++ b/packages/react-aria-components/test/Dialog.test.js
@@ -70,7 +70,7 @@ describe('Dialog', () => {
let button = getByRole('button');
let dialogTester = testUtilUser.createTester('Dialog', {root: button, overlayType: 'modal'});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
expect(dialog).toHaveAttribute('role', 'alertdialog');
let heading = getByRole('heading');
expect(dialog).toHaveAttribute('aria-labelledby', heading.id);
@@ -174,7 +174,7 @@ describe('Dialog', () => {
await dialogTester.open();
expect(button).toHaveAttribute('data-pressed');
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.getDialog();
let heading = getByRole('heading');
expect(dialog).toHaveAttribute('aria-labelledby', heading.id);
expect(dialog).toHaveAttribute('data-test', 'dialog');
diff --git a/packages/react-aria-components/test/GridList.browser.test.tsx b/packages/react-aria-components/test/GridList.browser.test.tsx
new file mode 100644
index 00000000000..d7b944e1f73
--- /dev/null
+++ b/packages/react-aria-components/test/GridList.browser.test.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {expect, it} from 'vitest';
+import {GridList, GridListItem} from '../src/GridList';
+import React from 'react';
+import {render} from 'vitest-browser-react';
+import {User} from '@react-aria/test-utils';
+
+function Grid() {
+ return (
+
+ 0,0
+ 0,1
+ 0,2
+ 1,0
+ 1,1
+ 1,2
+ 2,0
+ 2,1
+ 2,2
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('selects a row via $interactionType in real browser grid layout', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let gridlist = container.querySelector('[role=grid]') as HTMLElement;
+ let tester = testUtilUser.createTester('GridList', {
+ root: gridlist,
+ layout: 'grid',
+ interactionType
+ });
+
+ let rows = tester.getRows();
+ await tester.toggleRowSelection({row: rows[5]});
+ expect(rows[5].getAttribute('aria-selected')).toBe('true');
+ expect(document.activeElement).toBe(rows[5]);
+
+ await tester.toggleRowSelection({row: rows[0]});
+ expect(rows[0].getAttribute('aria-selected')).toBe('true');
+ expect(document.activeElement).toBe(rows[0]);
+
+ await tester.toggleRowSelection({row: rows[8]});
+ expect(rows[8].getAttribute('aria-selected')).toBe('true');
+ expect(document.activeElement).toBe(rows[8]);
+});
diff --git a/packages/react-aria-components/test/GridList.test.js b/packages/react-aria-components/test/GridList.test.js
index 27b381c1eef..e9b99905cd2 100644
--- a/packages/react-aria-components/test/GridList.test.js
+++ b/packages/react-aria-components/test/GridList.test.js
@@ -145,9 +145,9 @@ describe('GridList', () => {
let {getByRole} = renderGridList();
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
- expect(gridListTester.gridlist).toHaveAttribute('class', 'react-aria-GridList');
+ expect(gridListTester.getGridlist()).toHaveAttribute('class', 'react-aria-GridList');
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.getRows()) {
expect(row).toHaveAttribute('class', 'react-aria-GridListItem');
}
});
@@ -341,7 +341,7 @@ describe('GridList', () => {
);
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
- let row = gridListTester.rows[0];
+ let row = gridListTester.getRows()[0];
expect(row).not.toHaveAttribute('aria-selected', 'true');
expect(row).not.toHaveClass('selected');
expect(within(row).getByRole('checkbox')).not.toBeChecked();
@@ -361,21 +361,21 @@ describe('GridList', () => {
let {getByRole} = renderGridList({selectionMode: 'multiple', escapeKeyBehavior: 'none'});
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
- let row = gridListTester.rows[0];
+ let row = gridListTester.getRows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
await gridListTester.toggleRowSelection({row: 0});
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 1});
- expect(gridListTester.selectedRows).toHaveLength(2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(2);
await user.keyboard('{Escape}');
- expect(gridListTester.selectedRows).toHaveLength(2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(2);
});
- it('should support disabled state', () => {
- let {getAllByRole} = renderGridList(
+ it('should support disabled state', async () => {
+ let {getAllByRole, getByRole} = renderGridList(
{selectionMode: 'multiple', disabledKeys: ['cat']},
{className: ({isDisabled}) => (isDisabled ? 'disabled' : '')}
);
@@ -385,6 +385,10 @@ describe('GridList', () => {
expect(row).toHaveClass('disabled');
expect(within(row).getByRole('checkbox')).toBeDisabled();
+
+ let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
+ await expect(gridListTester.toggleRowSelection({row: 0})).rejects.toThrow();
+ await expect(gridListTester.triggerRowAction({row: 0})).rejects.toThrow();
});
it('should support isDisabled prop on items', async () => {
@@ -399,7 +403,7 @@ describe('GridList', () => {
);
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows[1]).toHaveAttribute('aria-disabled', 'true');
expect(within(rows[1]).getByRole('button')).toBeDisabled();
@@ -720,9 +724,9 @@ describe('GridList', () => {
root: getByRole('grid'),
interactionType: type
});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -742,8 +746,8 @@ describe('GridList', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(gridListTester.selectedRows).toHaveLength(1);
- expect(gridListTester.selectedRows[0]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row2);
let row1 = rows[1];
await gridListTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
@@ -757,8 +761,8 @@ describe('GridList', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['dog']));
- expect(gridListTester.selectedRows).toHaveLength(1);
- expect(gridListTester.selectedRows[0]).toBe(row1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row1);
await gridListTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
expect(row1).toHaveAttribute('aria-selected', 'false');
@@ -771,7 +775,7 @@ describe('GridList', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set([]));
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.getSelectedRows()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
@@ -789,9 +793,9 @@ describe('GridList', () => {
root: getByRole('grid'),
interactionType: type
});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -809,13 +813,13 @@ describe('GridList', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['cat', 'kangaroo'])
);
- expect(gridListTester.selectedRows).toHaveLength(2);
- expect(gridListTester.selectedRows[1]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(2);
+ expect(gridListTester.getSelectedRows()[1]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(gridListTester.selectedRows).toHaveLength(1);
- expect(gridListTester.selectedRows[0]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row2);
}
let row1 = rows[1];
@@ -829,17 +833,17 @@ describe('GridList', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['cat', 'dog', 'kangaroo'])
);
- expect(gridListTester.selectedRows).toHaveLength(3);
- expect(gridListTester.selectedRows[1]).toBe(row1);
- expect(gridListTester.selectedRows[2]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(3);
+ expect(gridListTester.getSelectedRows()[1]).toBe(row1);
+ expect(gridListTester.getSelectedRows()[2]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['dog', 'kangaroo'])
);
- expect(gridListTester.selectedRows).toHaveLength(2);
- expect(gridListTester.selectedRows[0]).toBe(row1);
- expect(gridListTester.selectedRows[1]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(2);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row1);
+ expect(gridListTester.getSelectedRows()[1]).toBe(row2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -853,13 +857,13 @@ describe('GridList', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['cat', 'kangaroo'])
);
- expect(gridListTester.selectedRows).toHaveLength(2);
- expect(gridListTester.selectedRows[1]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(2);
+ expect(gridListTester.getSelectedRows()[1]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(gridListTester.selectedRows).toHaveLength(1);
- expect(gridListTester.selectedRows[0]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row2);
}
});
@@ -878,9 +882,9 @@ describe('GridList', () => {
root: getByRole('grid'),
interactionType: type
});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -899,8 +903,8 @@ describe('GridList', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(gridListTester.selectedRows).toHaveLength(1);
- expect(gridListTester.selectedRows[0]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row2);
let row1 = rows[1];
await gridListTester.toggleRowSelection({row: row1});
@@ -915,8 +919,8 @@ describe('GridList', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['dog']));
- expect(gridListTester.selectedRows).toHaveLength(1);
- expect(gridListTester.selectedRows[0]).toBe(row1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row1);
// pressing without modifier keys won't deselect the row
await gridListTester.toggleRowSelection({row: row1});
@@ -927,7 +931,7 @@ describe('GridList', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(row1).toHaveAttribute('aria-selected', 'true');
@@ -936,16 +940,16 @@ describe('GridList', () => {
expect(row2).toHaveAttribute('data-selected', 'true');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['dog', 'kangaroo']));
- expect(gridListTester.selectedRows).toHaveLength(2);
- expect(gridListTester.selectedRows[0]).toBe(row1);
+ expect(gridListTester.getSelectedRows()).toHaveLength(2);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row1);
await gridListTester.toggleRowSelection({row: row1});
expect(row1).toHaveAttribute('aria-selected', 'false');
expect(row1).not.toHaveAttribute('data-selected');
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls[2][0])).toEqual(new Set(['kangaroo']));
- expect(gridListTester.selectedRows).toHaveLength(1);
- expect(gridListTester.selectedRows[0]).toBe(row2);
+ expect(gridListTester.getSelectedRows()).toHaveLength(1);
+ expect(gridListTester.getSelectedRows()[0]).toBe(row2);
}
});
});
@@ -1562,7 +1566,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows).toHaveLength(4);
let loaderRow = rows[3];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -1575,7 +1579,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows).toHaveLength(3);
expect(tree.queryByText('Loading...')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
@@ -1585,7 +1589,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows).toHaveLength(1);
expect(rows[0]).toHaveTextContent('empty state');
expect(tree.queryByText('Loading...')).toBeFalsy();
@@ -1593,7 +1597,7 @@ describe('GridList', () => {
// Even if the gridlist is empty, providing isLoading will render the loader
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.getRows();
expect(rows).toHaveLength(2);
expect(rows[1]).toHaveTextContent('empty state');
expect(tree.queryByText('Loading...')).toBeTruthy();
@@ -1677,7 +1681,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows).toHaveLength(8);
let loaderRow = rows[7];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -1696,35 +1700,35 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows).toHaveLength(7);
- expect(within(gridListTester.gridlist).queryByText('Loading...')).toBeFalsy();
+ expect(within(gridListTester.getGridlist()).queryByText('Loading...')).toBeFalsy();
- let sentinel = within(gridListTester.gridlist).getByTestId('loadMoreSentinel');
+ let sentinel = within(gridListTester.getGridlist()).getByTestId('loadMoreSentinel');
let sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('1250px');
expect(sentinelParentStyles.height).toBe('0px');
expect(sentinel.parentElement).toHaveAttribute('inert');
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.getRows();
expect(rows).toHaveLength(1);
let emptyStateRow = rows[0];
expect(emptyStateRow).toHaveTextContent('empty state');
- expect(within(gridListTester.gridlist).queryByText('Loading...')).toBeFalsy();
+ expect(within(gridListTester.getGridlist()).queryByText('Loading...')).toBeFalsy();
- sentinel = within(gridListTester.gridlist).getByTestId('loadMoreSentinel');
+ sentinel = within(gridListTester.getGridlist()).getByTestId('loadMoreSentinel');
sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('0px');
expect(sentinelParentStyles.height).toBe('0px');
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.getRows();
expect(rows).toHaveLength(1);
emptyStateRow = rows[0];
expect(emptyStateRow).toHaveTextContent('loading');
- sentinel = within(gridListTester.gridlist).getByTestId('loadMoreSentinel');
+ sentinel = within(gridListTester.getGridlist()).getByTestId('loadMoreSentinel');
sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('0px');
expect(sentinelParentStyles.height).toBe('0px');
@@ -1734,7 +1738,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.getRows();
expect(rows).toHaveLength(1);
let loaderRow = rows[0];
@@ -1745,15 +1749,15 @@ describe('GridList', () => {
}
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.getRows();
expect(rows).toHaveLength(7);
- expect(within(gridListTester.gridlist).queryByText('loading')).toBeFalsy();
+ expect(within(gridListTester.getGridlist()).queryByText('loading')).toBeFalsy();
for (let [index, row] of rows.entries()) {
expect(row).toHaveAttribute('aria-rowindex', `${index + 1}`);
}
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.getRows();
expect(rows).toHaveLength(8);
loaderRow = rows[7];
expect(loaderRow).not.toHaveAttribute('aria-rowindex');
diff --git a/packages/react-aria-components/test/ListBox.browser.test.tsx b/packages/react-aria-components/test/ListBox.browser.test.tsx
new file mode 100644
index 00000000000..f8b99c311c9
--- /dev/null
+++ b/packages/react-aria-components/test/ListBox.browser.test.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {expect, it} from 'vitest';
+import {ListBox, ListBoxItem} from '../src/ListBox';
+import React from 'react';
+import {render} from 'vitest-browser-react';
+import {User} from '@react-aria/test-utils';
+
+function GridListBox() {
+ return (
+
+ 0,0
+ 0,1
+ 0,2
+ 1,0
+ 1,1
+ 1,2
+ 2,0
+ 2,1
+ 2,2
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`(
+ 'selects an option via $interactionType in real browser grid layout',
+ async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let listbox = container.querySelector('[role=listbox]') as HTMLElement;
+ let tester = testUtilUser.createTester('ListBox', {
+ root: listbox,
+ layout: 'grid',
+ interactionType
+ });
+
+ let options = tester.getOptions();
+ await tester.toggleOptionSelection({option: options[5]});
+ expect(options[5].getAttribute('aria-selected')).toBe('true');
+ expect(document.activeElement).toBe(options[5]);
+
+ await tester.toggleOptionSelection({option: options[0]});
+ expect(options[0].getAttribute('aria-selected')).toBe('true');
+ expect(document.activeElement).toBe(options[0]);
+
+ await tester.toggleOptionSelection({option: options[8]});
+ expect(options[8].getAttribute('aria-selected')).toBe('true');
+ expect(document.activeElement).toBe(options[8]);
+ }
+);
diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js
index 979f6b0a75d..415e2b3352f 100644
--- a/packages/react-aria-components/test/ListBox.test.js
+++ b/packages/react-aria-components/test/ListBox.test.js
@@ -111,13 +111,13 @@ describe('ListBox', () => {
);
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- expect(listboxTester.listbox).toHaveAttribute('data-rac');
- let sections = listboxTester.sections;
+ expect(listboxTester.getListbox()).toHaveAttribute('data-rac');
+ let sections = listboxTester.getSections();
for (let section of sections) {
expect(section).toHaveAttribute('data-rac');
}
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
for (let option of options) {
expect(option).toHaveAttribute('data-rac');
}
@@ -587,7 +587,7 @@ describe('ListBox', () => {
);
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- let option = listboxTester.options()[0];
+ let option = listboxTester.getOptions()[0];
expect(option).not.toHaveAttribute('aria-selected', 'true');
expect(option).not.toHaveClass('selected');
@@ -649,7 +649,7 @@ describe('ListBox', () => {
);
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
await listboxTester.triggerOptionAction({option: options[0], interactionType});
expect(onAction).toHaveBeenCalledTimes(1);
});
@@ -669,14 +669,14 @@ describe('ListBox', () => {
);
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
await listboxTester.triggerOptionAction({option: options[0]});
- let selectedOptions = listboxTester.selectedOptions;
+ let selectedOptions = listboxTester.getSelectedOptions();
expect(selectedOptions).toHaveLength(1);
expect(onAction).not.toHaveBeenCalled();
await listboxTester.triggerOptionAction({option: options[1], needsDoubleClick: true});
- selectedOptions = listboxTester.selectedOptions;
+ selectedOptions = listboxTester.getSelectedOptions();
expect(selectedOptions).toHaveLength(1);
expect(onAction).toHaveBeenCalledTimes(1);
});
@@ -695,7 +695,7 @@ describe('ListBox', () => {
root: getByRole('listbox'),
interactionType: type
});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
expect(onSelectionChange).toHaveBeenCalledTimes(0);
let option2 = options[2];
@@ -712,8 +712,8 @@ describe('ListBox', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option2);
let option1 = options[1];
await listboxTester.toggleOptionSelection({option: option1, selectionBehavior: 'replace'});
@@ -727,8 +727,8 @@ describe('ListBox', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['dog']));
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(option1);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option1);
await listboxTester.toggleOptionSelection({option: option1, selectionBehavior: 'replace'});
expect(option1).toHaveAttribute('aria-selected', 'false');
@@ -741,7 +741,7 @@ describe('ListBox', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set([]));
- expect(listboxTester.selectedOptions).toHaveLength(0);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
@@ -754,7 +754,7 @@ describe('ListBox', () => {
root: getByRole('listbox'),
interactionType: type
});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
let option2 = options[2];
await listboxTester.toggleOptionSelection({
@@ -769,13 +769,13 @@ describe('ListBox', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['cat', 'kangaroo'])
);
- expect(listboxTester.selectedOptions).toHaveLength(2);
- expect(listboxTester.selectedOptions[1]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(2);
+ expect(listboxTester.getSelectedOptions()[1]).toBe(option2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option2);
}
let option1 = options[1];
@@ -789,17 +789,17 @@ describe('ListBox', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['cat', 'dog', 'kangaroo'])
);
- expect(listboxTester.selectedOptions).toHaveLength(3);
- expect(listboxTester.selectedOptions[1]).toBe(option1);
- expect(listboxTester.selectedOptions[2]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(3);
+ expect(listboxTester.getSelectedOptions()[1]).toBe(option1);
+ expect(listboxTester.getSelectedOptions()[2]).toBe(option2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['dog', 'kangaroo'])
);
- expect(listboxTester.selectedOptions).toHaveLength(2);
- expect(listboxTester.selectedOptions[0]).toBe(option1);
- expect(listboxTester.selectedOptions[1]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(2);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option1);
+ expect(listboxTester.getSelectedOptions()[1]).toBe(option2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -813,13 +813,13 @@ describe('ListBox', () => {
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(
new Set(['cat', 'kangaroo'])
);
- expect(listboxTester.selectedOptions).toHaveLength(2);
- expect(listboxTester.selectedOptions[1]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(2);
+ expect(listboxTester.getSelectedOptions()[1]).toBe(option2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option2);
}
});
@@ -833,7 +833,7 @@ describe('ListBox', () => {
root: getByRole('listbox'),
interactionType: type
});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
let option2 = options[2];
await listboxTester.toggleOptionSelection({option: 'Kangaroo'});
@@ -846,8 +846,8 @@ describe('ListBox', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['kangaroo']));
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option2);
let option1 = options[1];
await listboxTester.toggleOptionSelection({option: option1});
@@ -862,8 +862,8 @@ describe('ListBox', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['dog']));
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(option1);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option1);
// pressing without modifier keys won't deselect the row
await listboxTester.toggleOptionSelection({option: option1});
expect(option1).toHaveAttribute('aria-selected', 'true');
@@ -873,7 +873,7 @@ describe('ListBox', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(listboxTester.selectedOptions).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(option1).toHaveAttribute('aria-selected', 'true');
@@ -882,16 +882,16 @@ describe('ListBox', () => {
expect(option2).toHaveAttribute('data-selected', 'true');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['dog', 'kangaroo']));
- expect(listboxTester.selectedOptions).toHaveLength(2);
- expect(listboxTester.selectedOptions[0]).toBe(option1);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(2);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option1);
await listboxTester.toggleOptionSelection({option: option1});
expect(option1).toHaveAttribute('aria-selected', 'false');
expect(option1).not.toHaveAttribute('data-selected');
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls[2][0])).toEqual(new Set(['kangaroo']));
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(option2);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(option2);
}
});
});
@@ -918,21 +918,21 @@ describe('ListBox', () => {
interactionType: 'touch'
});
- await listboxTester.toggleOptionSelection({option: listboxTester.options()[0]});
- expect(listboxTester.selectedOptions).toHaveLength(0);
+ await listboxTester.toggleOptionSelection({option: listboxTester.getOptions()[0]});
+ expect(listboxTester.getSelectedOptions()).toHaveLength(0);
expect(onAction).toHaveBeenCalledTimes(1);
await listboxTester.toggleOptionSelection({
- option: listboxTester.options()[0],
+ option: listboxTester.getOptions()[0],
needsLongPress: true
});
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(listboxTester.options()[0]);
+ expect(listboxTester.getSelectedOptions()).toHaveLength(1);
+ expect(listboxTester.getSelectedOptions()[0]).toBe(listboxTester.getOptions()[0]);
expect(onAction).toHaveBeenCalledTimes(1);
- await listboxTester.toggleOptionSelection({option: listboxTester.options()[1]});
- expect(listboxTester.selectedOptions).toHaveLength(2);
- expect(listboxTester.selectedOptions[1]).toBe(listboxTester.options()[1]);
+ await listboxTester.toggleOptionSelection({option: listboxTester.getOptions()[1]});
+ expect(listboxTester.getSelectedOptions()).toHaveLength(2);
+ expect(listboxTester.getSelectedOptions()[1]).toBe(listboxTester.getOptions()[1]);
});
});
@@ -1182,6 +1182,66 @@ describe('ListBox', () => {
expect(document.activeElement).toBe(options[0]); // 1,1
});
+ it('should support keyboard navigation across grid layout via the test util', async () => {
+ /**
+ * The following ListBox is roughly in this shape:
+ *
+ * ## | 1,1 | 2,1 | 3,1 |
+ *
+ * ## | 1,2 | 2,2 | 3,2 |
+ *
+ * ## | 1,3 | 3,2 | 3,3 |
+ */
+ let {getByRole} = render(
+
+ 1,1
+ 1,2
+ 1,3
+ 2,1
+ 2,2
+ 2,3
+ 3,1
+ 3,2
+ 3,3
+
+ );
+
+ jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function () {
+ if (this.getAttribute('role') === 'listbox') {
+ return {top: 0, left: 0, bottom: 200, right: 300, width: 300, height: 200};
+ } else {
+ let index = [...this.parentElement.children].indexOf(this);
+ return {
+ top: (index % 3) * 40,
+ left: Math.floor(index / 3) * 100,
+ bottom: (index % 3) * 40 + 40,
+ right: Math.floor(index / 3) * 100 + 100,
+ width: 100,
+ height: 40
+ };
+ }
+ });
+
+ let listboxTester = testUtilUser.createTester('ListBox', {
+ root: getByRole('listbox'),
+ interactionType: 'keyboard',
+ layout: 'grid'
+ });
+ let options = listboxTester.getOptions();
+
+ await listboxTester.toggleOptionSelection({option: options[5]});
+ expect(options[5]).toHaveAttribute('aria-selected', 'true');
+ expect(document.activeElement).toBe(options[5]);
+
+ await listboxTester.toggleOptionSelection({option: '1,1'});
+ expect(options[0]).toHaveAttribute('aria-selected', 'true');
+ expect(document.activeElement).toBe(options[0]);
+
+ await listboxTester.toggleOptionSelection({option: 8});
+ expect(options[8]).toHaveAttribute('aria-selected', 'true');
+ expect(document.activeElement).toBe(options[8]);
+ });
+
it('should support onScroll', () => {
let onScroll = jest.fn();
let {getByRole} = renderListbox({onScroll});
@@ -1263,7 +1323,7 @@ describe('ListBox', () => {
let {getByRole} = renderListbox({selectionMode: 'multiple', escapeKeyBehavior: 'none'});
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- let option = listboxTester.options()[0];
+ let option = listboxTester.getOptions()[0];
expect(option).not.toHaveAttribute('aria-selected', 'true');
expect(option).not.toHaveClass('selected');
@@ -1978,7 +2038,7 @@ describe('ListBox', () => {
let tree = render();
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
expect(options).toHaveLength(4);
let loaderRow = options[3];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -1991,7 +2051,7 @@ describe('ListBox', () => {
let tree = render();
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
expect(options).toHaveLength(3);
expect(tree.queryByText('Loading...')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
@@ -2001,7 +2061,7 @@ describe('ListBox', () => {
let tree = render();
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
expect(options).toHaveLength(1);
expect(options[0]).toHaveTextContent('empty state');
expect(tree.queryByText('Loading...')).toBeFalsy();
@@ -2009,7 +2069,7 @@ describe('ListBox', () => {
// Even if the listbox is empty, providing isLoading will render the loader
tree.rerender();
- options = listboxTester.options();
+ options = listboxTester.getOptions();
expect(options).toHaveLength(2);
expect(options[1]).toHaveTextContent('empty state');
expect(tree.queryByText('Loading...')).toBeTruthy();
@@ -2096,7 +2156,7 @@ describe('ListBox', () => {
it('should always render the sentinel even when virtualized', () => {
let tree = render();
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
expect(options).toHaveLength(8);
let loaderRow = options[7];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -2119,31 +2179,31 @@ describe('ListBox', () => {
it.skip('should not reserve room for the loader if isLoading is false', () => {
let tree = render();
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let options = listboxTester.options();
+ let options = listboxTester.getOptions();
expect(options).toHaveLength(7);
- expect(within(listboxTester.listbox).queryByText('Loading...')).toBeFalsy();
+ expect(within(listboxTester.getListbox()).queryByText('Loading...')).toBeFalsy();
- let sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
+ let sentinel = within(listboxTester.getListbox()).getByTestId('loadMoreSentinel');
let sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('1250px');
expect(sentinelParentStyles.height).toBe('0px');
expect(sentinel.parentElement).toHaveAttribute('inert');
tree.rerender();
- options = listboxTester.options();
+ options = listboxTester.getOptions();
expect(options).toHaveLength(1);
let emptyStateRow = options[0];
expect(emptyStateRow).toHaveTextContent('empty state');
- expect(within(listboxTester.listbox).queryByText('Loading...')).toBeFalsy();
+ expect(within(listboxTester.getListbox()).queryByText('Loading...')).toBeFalsy();
- sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
+ sentinel = within(listboxTester.getListbox()).getByTestId('loadMoreSentinel');
sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('0px');
expect(sentinelParentStyles.height).toBe('0px');
// Setting isLoading will render the loader even if the list is empty.
tree.rerender();
- options = listboxTester.options();
+ options = listboxTester.getOptions();
expect(options).toHaveLength(2);
emptyStateRow = options[1];
expect(emptyStateRow).toHaveTextContent('empty state');
@@ -2151,7 +2211,7 @@ describe('ListBox', () => {
let loadingRow = options[0];
expect(loadingRow).toHaveTextContent('Loading...');
- sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
+ sentinel = within(listboxTester.getListbox()).getByTestId('loadMoreSentinel');
sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('0px');
expect(sentinelParentStyles.height).toBe('30px');
diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx
index 44841bccad2..3efe8997db2 100644
--- a/packages/react-aria-components/test/Menu.test.tsx
+++ b/packages/react-aria-components/test/Menu.test.tsx
@@ -1207,18 +1207,18 @@ describe('Menu', () => {
interactionType: 'keyboard'
});
- expect(menuTester.trigger).not.toHaveAttribute('data-pressed');
+ expect(menuTester.getTrigger()).not.toHaveAttribute('data-pressed');
await menuTester.open();
- expect(menuTester.trigger).toHaveAttribute('data-pressed');
+ expect(menuTester.getTrigger()).toHaveAttribute('data-pressed');
- expect(menuTester.options()).toHaveLength(5);
- expect(menuTester.menu).toBeInTheDocument();
+ expect(menuTester.getOptions()).toHaveLength(5);
+ expect(menuTester.getMenu()).toBeInTheDocument();
- let popover = menuTester.menu?.closest('.react-aria-Popover');
+ let popover = menuTester.getMenu()?.closest('.react-aria-Popover');
expect(popover).toBeInTheDocument();
expect(popover).toHaveAttribute('data-trigger', 'MenuTrigger');
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.getSubmenuTriggers()[0];
expect(triggerItem).toHaveTextContent('Share…');
expect(triggerItem).toHaveAttribute('aria-haspopup', 'menu');
expect(triggerItem).toHaveAttribute('aria-expanded', 'false');
@@ -1234,26 +1234,26 @@ describe('Menu', () => {
expect(triggerItem).toHaveAttribute('data-hovered', 'true');
expect(triggerItem).toHaveAttribute('aria-expanded', 'true');
expect(triggerItem).toHaveAttribute('data-open', 'true');
- expect(submenuTester?.menu).toBeInTheDocument();
- expect(submenuTester?.options()).toHaveLength(3);
+ expect(submenuTester?.getMenu()).toBeInTheDocument();
+ expect(submenuTester?.getOptions()).toHaveLength(3);
// Open the nested submenu
let nestedSubmenu = await submenuTester?.openSubmenu({submenuTrigger: 'Email…'});
act(() => {
jest.runAllTimers();
});
- expect(nestedSubmenu?.menu).toBeInTheDocument();
- expect(document.activeElement).toBe(nestedSubmenu?.options()[0]);
+ expect(nestedSubmenu?.getMenu()).toBeInTheDocument();
+ expect(document.activeElement).toBe(nestedSubmenu?.getOptions()[0]);
await user.keyboard('{Escape}');
act(() => {
jest.runAllTimers();
});
- expect(nestedSubmenu?.menu).not.toBeInTheDocument();
- expect(submenuTester?.menu).toBeInTheDocument();
- expect(menuTester.menu).toBeInTheDocument();
- expect(document.activeElement).toBe(nestedSubmenu?.trigger);
+ expect(nestedSubmenu?.getMenu()).not.toBeInTheDocument();
+ expect(submenuTester?.getMenu()).toBeInTheDocument();
+ expect(menuTester.getMenu()).toBeInTheDocument();
+ expect(document.activeElement).toBe(nestedSubmenu?.getTrigger());
});
it('should not close the menu when clicking on a element within the submenu tree', async () => {
let onAction = jest.fn();
@@ -1378,7 +1378,7 @@ describe('Menu', () => {
await menuTester.open();
expect(button).toHaveAttribute('data-pressed');
- let groups = menuTester.sections;
+ let groups = menuTester.getSections();
expect(groups).toHaveLength(2);
expect(groups[0]).toHaveClass('react-aria-MenuSection');
@@ -1394,24 +1394,24 @@ describe('Menu', () => {
'Settings'
);
- let menu = menuTester.menu!;
+ let menu = menuTester.getMenu()!;
expect(getAllByRole('menuitem')).toHaveLength(7);
let popover = menu.closest('.react-aria-Popover');
expect(popover).toBeInTheDocument();
expect(popover).toHaveAttribute('data-trigger', 'MenuTrigger');
- let submenuTriggers = menuTester.submenuTriggers;
+ let submenuTriggers = menuTester.getSubmenuTriggers();
expect(submenuTriggers).toHaveLength(1);
// Open the submenu
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger: 'Share…'}))!;
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.getMenu();
expect(submenu).toBeInTheDocument();
- let submenuItems = submenuUtil.options();
+ let submenuItems = submenuUtil.getOptions();
expect(submenuItems).toHaveLength(6);
- let groupsInSubmenu = submenuUtil.sections;
+ let groupsInSubmenu = submenuUtil.getSections();
expect(groupsInSubmenu).toHaveLength(2);
expect(groupsInSubmenu[0]).toHaveClass('react-aria-MenuSection');
@@ -1474,17 +1474,17 @@ describe('Menu', () => {
);
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')});
- expect(menuTester.trigger).not.toHaveAttribute('data-pressed');
+ expect(menuTester.getTrigger()).not.toHaveAttribute('data-pressed');
await menuTester.open();
- expect(menuTester.trigger).toHaveAttribute('data-pressed');
- expect(menuTester.options()).toHaveLength(5);
+ expect(menuTester.getTrigger()).toHaveAttribute('data-pressed');
+ expect(menuTester.getOptions()).toHaveLength(5);
- let popover = menuTester.menu?.closest('.react-aria-Popover');
+ let popover = menuTester.getMenu()?.closest('.react-aria-Popover');
expect(popover).toBeInTheDocument();
expect(popover).toHaveAttribute('data-trigger', 'MenuTrigger');
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.getSubmenuTriggers()[0];
expect(triggerItem).toHaveTextContent('Share…');
expect(triggerItem).toHaveAttribute('aria-haspopup', 'menu');
expect(triggerItem).toHaveAttribute('aria-expanded', 'false');
@@ -1561,7 +1561,7 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')});
await menuTester.open();
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.getSubmenuTriggers()[0];
expect(triggerItem).toHaveTextContent('Share…');
expect(triggerItem).toHaveAttribute('aria-haspopup', 'menu');
@@ -1570,9 +1570,9 @@ describe('Menu', () => {
act(() => {
jest.runAllTimers();
});
- expect(subDialogTester?.menu).toBeInTheDocument();
+ expect(subDialogTester?.getMenu()).toBeInTheDocument();
- let subDialogTriggerItem = subDialogTester?.submenuTriggers[0];
+ let subDialogTriggerItem = subDialogTester?.getSubmenuTriggers()[0];
expect(subDialogTriggerItem).toHaveTextContent('Nested Subdialog');
expect(subDialogTriggerItem).toHaveAttribute('aria-haspopup', 'menu');
@@ -1648,16 +1648,16 @@ describe('Menu', () => {
await menuTester.open();
// Open the subdialog
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.getSubmenuTriggers()[0];
let subDialogTester = await menuTester.openSubmenu({submenuTrigger: triggerItem});
act(() => {
jest.runAllTimers();
});
- expect(subDialogTester?.menu).toBeInTheDocument();
+ expect(subDialogTester?.getMenu()).toBeInTheDocument();
// Open the nested subdialog
- let subDialogTriggerItem = subDialogTester?.submenuTriggers[0];
+ let subDialogTriggerItem = subDialogTester?.getSubmenuTriggers()[0];
await subDialogTester?.openSubmenu({submenuTrigger: subDialogTriggerItem!});
act(() => {
jest.runAllTimers();
@@ -1671,7 +1671,7 @@ describe('Menu', () => {
});
subdialogs = queryAllByRole('dialog');
expect(subdialogs).toHaveLength(0);
- expect(menuTester.menu).not.toBeInTheDocument();
+ expect(menuTester.getMenu()).not.toBeInTheDocument();
});
// TODO: add test where clicking in a parent subdialog should close the nested subdialog when we fix that use case
@@ -1765,7 +1765,7 @@ describe('Menu', () => {
jest.runAllTimers();
});
- let menu = menuTester.menu;
+ let menu = menuTester.getMenu();
let activeElement = document.activeElement;
await user.tab();
@@ -1799,7 +1799,7 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- await menuTester.selectOption({option: 'Cat'});
+ await menuTester.toggleOptionSelection({option: 'Cat'});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onPressStart).toHaveBeenCalledTimes(1);
@@ -1828,7 +1828,7 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- await menuTester.selectOption({option: 'Cat', closesOnSelect: false});
+ await menuTester.toggleOptionSelection({option: 'Cat', closesOnSelect: false});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onPressStart).toHaveBeenCalledTimes(1);
@@ -1853,11 +1853,8 @@ describe('Menu', () => {
);
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
- await user.pointer({target: menuTester.trigger, keys: '[MouseLeft>]'});
- await user.pointer({
- target: menuTester.findOption({optionIndexOrText: 'Cat'}),
- keys: '[/MouseLeft]'
- });
+ await user.pointer({target: menuTester.getTrigger(), keys: '[MouseLeft>]'});
+ await user.pointer({target: menuTester.findOption({indexOrText: 'Cat'}), keys: '[/MouseLeft]'});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onPressStart).not.toHaveBeenCalled();
@@ -1883,7 +1880,7 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- await menuTester.selectOption({option: 'Cat', interactionType: 'keyboard'});
+ await menuTester.toggleOptionSelection({option: 'Cat', interactionType: 'keyboard'});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onPressStart).toHaveBeenCalledTimes(1);
@@ -1914,7 +1911,7 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- await menuTester.selectOption({
+ await menuTester.toggleOptionSelection({
option: 'Cat',
interactionType: 'keyboard',
closesOnSelect: false
@@ -1988,7 +1985,7 @@ describe('Menu', () => {
});
await menuTester.open();
await findByRole('menu');
- await menuTester.selectOption({option: 0});
+ await menuTester.toggleOptionSelection({option: 0});
expect(
await findByText('Contact your administrator for permissions to delete.')
).toBeInTheDocument();
@@ -2000,7 +1997,7 @@ describe('Menu', () => {
act(() => {
jest.runAllTimers();
});
- expect(document.activeElement).toBe(menuTester.options()[0]);
+ expect(document.activeElement).toBe(menuTester.getOptions()[0]);
});
});
});
@@ -2172,6 +2169,27 @@ AriaMenuTests({
+ ),
+ disabledSubmenuTrigger: () =>
+ render(
+
+
+
+
+
+
)
}
});
diff --git a/packages/react-aria-components/test/RadioGroup.test.js b/packages/react-aria-components/test/RadioGroup.test.js
index ca27333d12d..357564454ca 100644
--- a/packages/react-aria-components/test/RadioGroup.test.js
+++ b/packages/react-aria-components/test/RadioGroup.test.js
@@ -356,17 +356,17 @@ describe.each(['RadioGroup', 'RadioField'])('%s', comp => {
{buttonClassName: ({isSelected}) => (isSelected ? 'selected' : '')}
);
let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')});
- let radios = radioGroupTester.radios;
+ let radios = radioGroupTester.getRadios();
let label = radios[0].closest('label');
- expect(radioGroupTester.selectedRadio).toBeFalsy();
+ expect(radioGroupTester.getSelectedRadio()).toBeFalsy();
expect(label).not.toHaveAttribute('data-selected');
expect(findRoot(label)).not.toHaveAttribute('data-selected');
expect(label).not.toHaveClass('selected');
await radioGroupTester.triggerRadio({radio: radios[0]});
expect(onChange).toHaveBeenLastCalledWith('a');
- expect(radioGroupTester.selectedRadio).toBe(radios[0]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[0]);
expect(label).toHaveAttribute('data-selected', 'true');
expect(findRoot(label)).toHaveAttribute('data-selected', 'true');
expect(label).toHaveClass('selected');
@@ -374,7 +374,7 @@ describe.each(['RadioGroup', 'RadioField'])('%s', comp => {
await radioGroupTester.triggerRadio({radio: radios[1]});
expect(onChange).toHaveBeenLastCalledWith('b');
expect(radios[0]).not.toBeChecked();
- expect(radioGroupTester.selectedRadio).toBe(radios[1]);
+ expect(radioGroupTester.getSelectedRadio()).toBe(radios[1]);
expect(label).not.toHaveAttribute('data-selected');
expect(findRoot(label)).not.toHaveAttribute('data-selected');
expect(label).not.toHaveClass('selected');
@@ -433,9 +433,9 @@ describe.each(['RadioGroup', 'RadioField'])('%s', comp => {
});
let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')});
- expect(radioGroupTester.radiogroup).toHaveAttribute('aria-orientation', 'horizontal');
- expect(radioGroupTester.radiogroup).toHaveClass('horizontal');
- let radios = radioGroupTester.radios;
+ expect(radioGroupTester.getRadioGroup()).toHaveAttribute('aria-orientation', 'horizontal');
+ expect(radioGroupTester.getRadioGroup()).toHaveClass('horizontal');
+ let radios = radioGroupTester.getRadios();
await radioGroupTester.triggerRadio({radio: radios[0]});
expect(radios[0]).toBeChecked();
diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js
index 8289ae8a476..df4906037d2 100644
--- a/packages/react-aria-components/test/Select.test.js
+++ b/packages/react-aria-components/test/Select.test.js
@@ -54,7 +54,7 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an item');
expect(trigger).not.toHaveAttribute('data-pressed');
@@ -83,12 +83,12 @@ describe('Select', () => {
await selectTester.open();
expect(trigger).toHaveAttribute('data-pressed', 'true');
- let listbox = selectTester.listbox;
+ let listbox = selectTester.getListbox();
expect(listbox).toHaveAttribute('class', 'react-aria-ListBox');
expect(listbox.closest('.react-aria-Popover')).toBeInTheDocument();
expect(listbox.closest('.react-aria-Popover')).toHaveAttribute('data-trigger', 'Select');
- let options = selectTester.options();
+ let options = selectTester.getOptions();
expect(options).toHaveLength(3);
await user.click(options[1]);
@@ -103,7 +103,7 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger.closest('.react-aria-Select')).toHaveAttribute('slot', 'test');
expect(trigger).toHaveAttribute('aria-label', 'test');
});
@@ -145,7 +145,7 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Cat');
});
@@ -172,14 +172,14 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('1 - Cat');
});
it('supports placeholder', () => {
let {getByTestId} = render();
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an animal');
});
@@ -230,7 +230,7 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('open');
await selectTester.open();
@@ -255,12 +255,12 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
await selectTester.open();
expect(trigger).toHaveAttribute('data-pressed', 'true');
- await selectTester.selectOption({option: 'Dog', closesOnSelect: false});
+ await selectTester.toggleOptionSelection({option: 'Dog', closesOnSelect: false});
expect(trigger).toHaveTextContent('Dog');
expect(trigger).toHaveAttribute('data-pressed', 'true');
});
@@ -283,12 +283,12 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
await selectTester.open();
expect(trigger).toHaveAttribute('data-pressed', 'true');
- await selectTester.selectOption({option: 'Dog', closesOnSelect: true});
+ await selectTester.toggleOptionSelection({option: 'Dog', closesOnSelect: true});
expect(trigger).toHaveTextContent('Dog');
expect(trigger).not.toHaveAttribute('data-pressed', 'true');
});
@@ -329,7 +329,7 @@ describe('Select', () => {
let wrapper = getByTestId('test-select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
let select = wrapper;
let input = document.querySelector('[name=select]');
expect(input).toHaveAttribute('required');
@@ -348,8 +348,8 @@ describe('Select', () => {
expect(select).toHaveAttribute('data-invalid');
expect(document.activeElement).toBe(trigger);
- await selectTester.selectOption({option: 'Cat'});
- expect(selectTester.trigger).not.toHaveAttribute('aria-describedby');
+ await selectTester.toggleOptionSelection({option: 'Cat'});
+ expect(selectTester.getTrigger()).not.toHaveAttribute('aria-describedby');
expect(select).not.toHaveAttribute('data-invalid');
});
@@ -461,7 +461,7 @@ describe('Select', () => {
await user.tab();
await user.tab();
- expect(document.activeElement).toBe(selectTester.trigger);
+ expect(document.activeElement).toBe(selectTester.getTrigger());
await user.tab();
expect(document.activeElement).toBe(clearButton);
@@ -473,13 +473,13 @@ describe('Select', () => {
expect(document.activeElement).toBe(clearButton);
await user.tab({shift: true});
- expect(document.activeElement).toBe(selectTester.trigger);
+ expect(document.activeElement).toBe(selectTester.getTrigger());
await user.tab({shift: true});
expect(document.activeElement).toBe(beforeInput);
await user.tab();
- await selectTester.selectOption({option: 'Dog'});
+ await selectTester.toggleOptionSelection({option: 'Dog'});
expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenLastCalledWith('dog');
@@ -497,11 +497,11 @@ describe('Select', () => {
root: wrapper,
interactionType: 'keyboard'
});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an item');
expect(trigger).not.toHaveAttribute('data-pressed');
- await selectTester.selectOption({option: 'Kangaroo'});
+ await selectTester.toggleOptionSelection({option: 'Kangaroo'});
expect(trigger).toHaveTextContent('Kangaroo');
});
@@ -545,7 +545,7 @@ describe('Select', () => {
root: wrapper,
interactionType: 'keyboard'
});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Northern Territory');
expect(trigger).not.toHaveAttribute('data-pressed');
});
@@ -556,7 +556,7 @@ describe('Select', () => {
let selectTester = testUtilUser.createTester('Select', {
root: getByTestId('select')
});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(document.activeElement).toBe(trigger);
});
@@ -614,7 +614,7 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- await user.click(selectTester.trigger);
+ await user.click(selectTester.getTrigger());
let popover = queryByTestId('popover');
expect(popover).toBeFalsy();
@@ -636,7 +636,7 @@ describe('Select', () => {
);
- await user.click(selectTester.trigger);
+ await user.click(selectTester.getTrigger());
popover = queryByTestId('popover');
expect(popover).toBeFalsy();
});
@@ -674,11 +674,11 @@ describe('Select', () => {
const {getByTestId} = render();
const wrapper = getByTestId('select');
const selectTester = testUtilUser.createTester('Select', {root: wrapper});
- const trigger = selectTester.trigger;
+ const trigger = selectTester.getTrigger();
const submit = getByTestId('submit');
expect(trigger).toHaveTextContent('Select an item');
- await selectTester.selectOption({option: 'Cat'});
+ await selectTester.toggleOptionSelection({option: 'Cat'});
expect(trigger).toHaveTextContent('Cat');
await user.click(submit);
expect(onSubmit).toHaveBeenCalledTimes(1);
@@ -699,19 +699,19 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an item');
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.getListbox();
expect(listbox).toHaveAttribute('aria-multiselectable', 'true');
- let options = selectTester.options();
+ let options = selectTester.getOptions();
expect(options).toHaveLength(3);
- await user.click(options[0]);
- await user.click(options[1]);
+ await selectTester.toggleOptionSelection({option: options[0]});
+ await selectTester.toggleOptionSelection({option: options[1]});
expect(trigger).toHaveTextContent('Cat and Dog');
await selectTester.close();
@@ -722,6 +722,30 @@ describe('Select', () => {
expect(formData.getAll('select')).toEqual(['cat', 'dog']);
});
+ it('should support deselection if multiple selection is enabled', async () => {
+ let onChange = jest.fn();
+ let {getByTestId} = render();
+ let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
+
+ await selectTester.toggleOptionSelection({option: 'Cat'});
+ await selectTester.toggleOptionSelection({option: 'Dog'});
+ expect(selectTester.getOptions()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(selectTester.getOptions()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['cat', 'dog']);
+
+ await selectTester.toggleOptionSelection({option: 'Cat'});
+ expect(selectTester.getOptions()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(selectTester.getOptions()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['dog']);
+
+ await selectTester.toggleOptionSelection({option: 'Dog'});
+ expect(selectTester.getOptions()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(selectTester.getOptions()[1]).toHaveAttribute('aria-selected', 'false');
+ expect(onChange).toHaveBeenLastCalledWith([]);
+
+ await selectTester.close();
+ });
+
it('should support multiple selection form integration with many items', async () => {
let items = [];
for (let i = 0; i < 320; i++) {
@@ -748,7 +772,7 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Select an item');
let submit = getByTestId('submit');
@@ -759,7 +783,7 @@ describe('Select', () => {
await selectTester.open();
- let options = selectTester.options();
+ let options = selectTester.getOptions();
await user.click(options[0]);
await user.click(options[1]);
await selectTester.close();
@@ -779,12 +803,12 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Dog and Kangaroo');
await selectTester.open();
- let options = selectTester.options();
+ let options = selectTester.getOptions();
expect(options[0]).toHaveAttribute('aria-selected', 'false');
expect(options[1]).toHaveAttribute('aria-selected', 'true');
expect(options[2]).toHaveAttribute('aria-selected', 'true');
@@ -815,10 +839,10 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.getTrigger();
expect(trigger).toHaveTextContent('Cat');
- await selectTester.selectOption({option: 'Dog'});
+ await selectTester.toggleOptionSelection({option: 'Dog'});
expect(trigger).toHaveTextContent('2 selected items');
});
diff --git a/packages/react-aria-components/test/Table.test.js b/packages/react-aria-components/test/Table.test.js
index 1ab33d14ad9..6aa69f336cc 100644
--- a/packages/react-aria-components/test/Table.test.js
+++ b/packages/react-aria-components/test/Table.test.js
@@ -304,27 +304,27 @@ describe('Table', () => {
it('should render with default classes', () => {
let {getByRole} = renderTable();
let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')});
- let table = tableTester.table;
+ let table = tableTester.getTable();
expect(table).toHaveAttribute('class', 'react-aria-Table');
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
expect(row).toHaveAttribute('class', 'react-aria-Row');
}
- let rowGroups = tableTester.rowGroups;
+ let rowGroups = tableTester.getRowGroups();
expect(rowGroups).toHaveLength(2);
expect(rowGroups[0]).toHaveAttribute('class', 'react-aria-TableHeader');
expect(rowGroups[1]).toHaveAttribute('class', 'react-aria-TableBody');
- for (let cell of tableTester.columns) {
+ for (let cell of tableTester.getColumns()) {
expect(cell).toHaveAttribute('class', 'react-aria-Column');
}
- for (let cell of tableTester.rowHeaders) {
+ for (let cell of tableTester.getRowHeaders()) {
expect(cell).toHaveAttribute('class', 'react-aria-Cell');
}
- for (let cell of tableTester.cells()) {
+ for (let cell of tableTester.getCells()) {
expect(cell).toHaveAttribute('class', 'react-aria-Cell');
}
});
@@ -753,22 +753,23 @@ describe('Table', () => {
});
it('should support disabled state', async () => {
- let {getAllByRole} = renderTable({
+ let {getByRole} = renderTable({
tableProps: {selectionMode: 'multiple', disabledKeys: ['2'], disabledBehavior: 'all'},
rowProps: {className: ({isDisabled}) => (isDisabled ? 'disabled' : '')}
});
- let rows = getAllByRole('row');
- let row = rows[2];
-
- expect(row).toHaveAttribute('aria-disabled', 'true');
- expect(row).toHaveClass('disabled');
- expect(within(row).getByRole('checkbox')).toBeDisabled();
+ let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')});
+ let disabledRow = tableTester.getRows()[1];
+ expect(disabledRow).toHaveAttribute('aria-disabled', 'true');
+ expect(disabledRow).toHaveClass('disabled');
+ expect(within(disabledRow).getByRole('checkbox')).toBeDisabled();
await user.tab();
- expect(document.activeElement).toBe(rows[1]);
+ expect(document.activeElement).toBe(tableTester.getRows()[0]);
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});
- expect(document.activeElement).toBe(rows[3]);
+ expect(document.activeElement).toBe(tableTester.getRows()[2]);
+ await expect(tableTester.toggleRowSelection({row: 1})).rejects.toThrow();
+ await expect(tableTester.triggerRowAction({row: 1})).rejects.toThrow();
});
it('should support isDisabled prop on rows', async () => {
@@ -845,7 +846,7 @@ describe('Table', () => {
let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')});
- let columns = tableTester.columns;
+ let columns = tableTester.getColumns();
expect(columns[0]).toHaveAttribute('aria-sort', 'ascending');
expect(columns[0]).toHaveTextContent('▲');
expect(columns[1]).toHaveAttribute('aria-sort', 'none');
@@ -883,7 +884,7 @@ describe('Table', () => {
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowRight}');
- let gridRows = tableTester.rows;
+ let gridRows = tableTester.getRows();
expect(gridRows).toHaveLength(4);
let cell = within(gridRows[1]).getAllByRole('rowheader')[0];
expect(cell).toHaveTextContent('Program Files');
@@ -891,7 +892,7 @@ describe('Table', () => {
rerender();
- gridRows = tableTester.rows;
+ gridRows = tableTester.getRows();
expect(gridRows).toHaveLength(3);
cell = within(gridRows[1]).getAllByRole('rowheader')[0];
expect(cell).toHaveTextContent('bootmgr');
@@ -1696,8 +1697,8 @@ describe('Table', () => {
const DndTableExample = stories.DndTableExample;
let {getAllByRole} = render();
let tableTester = testUtilUser.createTester('Table', {root: getAllByRole('grid')[1]});
- expect(tableTester.rows).toHaveLength(7);
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getRows()).toHaveLength(7);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
await user.tab();
await user.keyboard('{ArrowRight}');
await user.keyboard('{Enter}');
@@ -1715,8 +1716,8 @@ describe('Table', () => {
// run onInsert promise in DnDTableExample first, otherwise updateFocusAfterDrop doesn't run properly
await act(async () => {});
act(() => jest.runAllTimers());
- expect(tableTester.rows).toHaveLength(8);
- expect(tableTester.selectedRows).toHaveLength(1);
+ expect(tableTester.getRows()).toHaveLength(8);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
});
});
@@ -2362,7 +2363,7 @@ describe('Table', () => {
it('should render the loading element when loading', async () => {
let tree = render();
let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
expect(rows).toHaveLength(12);
let loaderRow = rows[11];
expect(loaderRow).toHaveTextContent('spinner');
@@ -2374,7 +2375,7 @@ describe('Table', () => {
it('should render the sentinel but not the loading indicator when not loading', async () => {
let tree = render();
let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
expect(rows).toHaveLength(11);
expect(tree.queryByText('spinner')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
@@ -2383,24 +2384,24 @@ describe('Table', () => {
it('should properly render the renderEmptyState if table is empty', async () => {
let tree = render();
let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
expect(rows).toHaveLength(2);
expect(rows[1]).toHaveTextContent('No results');
expect(tree.queryByText('spinner')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
- let body = tableTester.rowGroups[1];
+ let body = tableTester.getRowGroups()[1];
expect(body).toHaveAttribute('data-empty', 'true');
let selectAll = tree.getAllByRole('checkbox')[0];
expect(selectAll).toBeDisabled();
// Even if the table is empty, providing isLoading will render the loader
tree.rerender();
- rows = tableTester.rows;
+ rows = tableTester.getRows();
expect(rows).toHaveLength(3);
expect(rows[2]).toHaveTextContent('No results');
expect(tree.queryByText('spinner')).toBeTruthy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
- body = tableTester.rowGroups[1];
+ body = tableTester.getRowGroups()[1];
expect(body).toHaveAttribute('data-empty', 'true');
selectAll = tree.getAllByRole('checkbox')[0];
expect(selectAll).toBeDisabled();
@@ -2587,7 +2588,7 @@ describe('Table', () => {
it('should always render the sentinel even when virtualized', () => {
let tree = render();
let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
expect(rows).toHaveLength(7);
let loaderRow = rows[6];
expect(loaderRow).toHaveTextContent('spinner');
@@ -2605,34 +2606,34 @@ describe('Table', () => {
it('should not reserve room for the loader if isLoading is false', () => {
let tree = render();
let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
expect(rows).toHaveLength(6);
- expect(within(tableTester.table).queryByText('spinner')).toBeFalsy();
+ expect(within(tableTester.getTable()).queryByText('spinner')).toBeFalsy();
- let sentinel = within(tableTester.table).getByTestId('loadMoreSentinel');
+ let sentinel = within(tableTester.getTable()).getByTestId('loadMoreSentinel');
let sentinelVirtWrapperStyles = sentinel.closest('[role="presentation"]').style;
expect(sentinelVirtWrapperStyles.top).toBe('1250px');
expect(sentinelVirtWrapperStyles.height).toBe('0px');
expect(sentinel.closest('[inert]')).toBeTruthy();
tree.rerender();
- rows = tableTester.rows;
+ rows = tableTester.getRows();
expect(rows).toHaveLength(1);
let emptyStateRow = rows[0];
expect(emptyStateRow).toHaveTextContent('No results');
- expect(within(tableTester.table).queryByText('spinner')).toBeFalsy();
- sentinel = within(tableTester.table).getByTestId('loadMoreSentinel', {hidden: true});
+ expect(within(tableTester.getTable()).queryByText('spinner')).toBeFalsy();
+ sentinel = within(tableTester.getTable()).getByTestId('loadMoreSentinel', {hidden: true});
sentinelVirtWrapperStyles = sentinel.closest('[role="presentation"]').style;
expect(sentinelVirtWrapperStyles.top).toBe('0px');
expect(sentinelVirtWrapperStyles.height).toBe('0px');
tree.rerender();
- rows = tableTester.rows;
+ rows = tableTester.getRows();
expect(rows).toHaveLength(1);
emptyStateRow = rows[0];
expect(emptyStateRow).toHaveTextContent('loading');
- sentinel = within(tableTester.table).getByTestId('loadMoreSentinel', {hidden: true});
+ sentinel = within(tableTester.getTable()).getByTestId('loadMoreSentinel', {hidden: true});
sentinelVirtWrapperStyles = sentinel.closest('[role="presentation"]').style;
expect(sentinelVirtWrapperStyles.top).toBe('0px');
expect(sentinelVirtWrapperStyles.height).toBe('0px');
@@ -2641,7 +2642,7 @@ describe('Table', () => {
it('should have the correct row indicies after loading more items', async () => {
let tree = render();
let tableTester = testUtilUser.createTester('Table', {root: tree.getByRole('grid')});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
expect(rows).toHaveLength(1);
let loaderRow = rows[0];
@@ -2650,15 +2651,15 @@ describe('Table', () => {
expect(loaderRow).not.toHaveAttribute('aria-rowindex');
tree.rerender();
- rows = tableTester.rows;
+ rows = tableTester.getRows();
expect(rows).toHaveLength(6);
- expect(within(tableTester.table).queryByText('spinner')).toBeFalsy();
+ expect(within(tableTester.getTable()).queryByText('spinner')).toBeFalsy();
for (let [index, row] of rows.entries()) {
expect(row).toHaveAttribute('aria-rowindex', `${index + 2}`);
}
tree.rerender();
- rows = tableTester.rows;
+ rows = tableTester.getRows();
expect(rows).toHaveLength(7);
loaderRow = rows[6];
expect(loaderRow).not.toHaveAttribute('aria-rowindex');
@@ -2872,9 +2873,9 @@ describe('Table', () => {
root: getByRole('grid'),
interactionType: type
});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -2894,8 +2895,8 @@ describe('Table', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['3']));
- expect(tableTester.selectedRows).toHaveLength(1);
- expect(tableTester.selectedRows[0]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
+ expect(tableTester.getSelectedRows()[0]).toBe(row2);
let row1 = rows[1];
await tableTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
@@ -2909,8 +2910,8 @@ describe('Table', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['2']));
- expect(tableTester.selectedRows).toHaveLength(1);
- expect(tableTester.selectedRows[0]).toBe(row1);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
+ expect(tableTester.getSelectedRows()[0]).toBe(row1);
await tableTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
expect(row1).toHaveAttribute('aria-selected', 'false');
@@ -2923,7 +2924,7 @@ describe('Table', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set([]));
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.getSelectedRows()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
@@ -2939,9 +2940,9 @@ describe('Table', () => {
root: getByRole('grid'),
interactionType: type
});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -2957,13 +2958,13 @@ describe('Table', () => {
// Called twice because initial focus will select the first keyboard focused row, meaning we have two items selected
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['1', '3']));
- expect(tableTester.selectedRows).toHaveLength(2);
- expect(tableTester.selectedRows[1]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(2);
+ expect(tableTester.getSelectedRows()[1]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['3']));
- expect(tableTester.selectedRows).toHaveLength(1);
- expect(tableTester.selectedRows[0]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
+ expect(tableTester.getSelectedRows()[0]).toBe(row2);
}
let row1 = rows[1];
@@ -2975,15 +2976,15 @@ describe('Table', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['1', '2', '3']));
- expect(tableTester.selectedRows).toHaveLength(3);
- expect(tableTester.selectedRows[1]).toBe(row1);
- expect(tableTester.selectedRows[2]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(3);
+ expect(tableTester.getSelectedRows()[1]).toBe(row1);
+ expect(tableTester.getSelectedRows()[2]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['2', '3']));
- expect(tableTester.selectedRows).toHaveLength(2);
- expect(tableTester.selectedRows[0]).toBe(row1);
- expect(tableTester.selectedRows[1]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(2);
+ expect(tableTester.getSelectedRows()[0]).toBe(row1);
+ expect(tableTester.getSelectedRows()[1]).toBe(row2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -2995,13 +2996,13 @@ describe('Table', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(4);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['1', '3']));
- expect(tableTester.selectedRows).toHaveLength(2);
- expect(tableTester.selectedRows[1]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(2);
+ expect(tableTester.getSelectedRows()[1]).toBe(row2);
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['3']));
- expect(tableTester.selectedRows).toHaveLength(1);
- expect(tableTester.selectedRows[0]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
+ expect(tableTester.getSelectedRows()[0]).toBe(row2);
}
});
@@ -3018,9 +3019,9 @@ describe('Table', () => {
root: getByRole('grid'),
interactionType: type
});
- let rows = tableTester.rows;
+ let rows = tableTester.getRows();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -3039,8 +3040,8 @@ describe('Table', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(1);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['3']));
- expect(tableTester.selectedRows).toHaveLength(1);
- expect(tableTester.selectedRows[0]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
+ expect(tableTester.getSelectedRows()[0]).toBe(row2);
let row1 = rows[1];
await tableTester.toggleRowSelection({row: row1});
@@ -3055,8 +3056,8 @@ describe('Table', () => {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
expect(new Set(onSelectionChange.mock.calls.at(-1)[0])).toEqual(new Set(['2']));
- expect(tableTester.selectedRows).toHaveLength(1);
- expect(tableTester.selectedRows[0]).toBe(row1);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
+ expect(tableTester.getSelectedRows()[0]).toBe(row1);
// pressing without modifier keys won't deselect the row
await tableTester.toggleRowSelection({row: row1});
@@ -3067,7 +3068,7 @@ describe('Table', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(tableTester.selectedRows).toHaveLength(1);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(row1).toHaveAttribute('aria-selected', 'true');
@@ -3076,16 +3077,16 @@ describe('Table', () => {
expect(row2).toHaveAttribute('data-selected', 'true');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['2', '3']));
- expect(tableTester.selectedRows).toHaveLength(2);
- expect(tableTester.selectedRows[0]).toBe(row1);
+ expect(tableTester.getSelectedRows()).toHaveLength(2);
+ expect(tableTester.getSelectedRows()[0]).toBe(row1);
await tableTester.toggleRowSelection({row: row1});
expect(row1).toHaveAttribute('aria-selected', 'false');
expect(row1).not.toHaveAttribute('data-selected');
expect(onSelectionChange).toHaveBeenCalledTimes(3);
expect(new Set(onSelectionChange.mock.calls[2][0])).toEqual(new Set(['3']));
- expect(tableTester.selectedRows).toHaveLength(1);
- expect(tableTester.selectedRows[0]).toBe(row2);
+ expect(tableTester.getSelectedRows()).toHaveLength(1);
+ expect(tableTester.getSelectedRows()[0]).toBe(row2);
}
});
});
@@ -3235,20 +3236,20 @@ describe('Table', () => {
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
+ let groups = tableTester.getRowGroups();
expect(groups).toHaveLength(3);
expect(groups[0].tagName).toBe('THEAD');
expect(groups[1].tagName).toBe('TBODY');
expect(groups[2].tagName).toBe('TFOOT');
- expect(tableTester.rows).toHaveLength(8);
+ expect(tableTester.getRows()).toHaveLength(8);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.getRows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
@@ -3313,17 +3314,17 @@ describe('Table', () => {
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
+ let groups = tableTester.getRowGroups();
expect(groups).toHaveLength(3);
- expect(tableTester.rows).toHaveLength(8);
+ expect(tableTester.getRows()).toHaveLength(8);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.getRows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
@@ -3367,29 +3368,42 @@ describe('Table', () => {
))}
+
+
+ | Total |
+ Blah |
+
+
);
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
- expect(groups).toHaveLength(4);
+ let groups = tableTester.getRowGroups();
+ expect(groups).toHaveLength(5);
expect(groups[0].tagName).toBe('THEAD');
expect(groups[1].tagName).toBe('TBODY');
expect(groups[2].tagName).toBe('TBODY');
expect(groups[3].tagName).toBe('TBODY');
- expect(tableTester.rows).toHaveLength(10);
+ expect(tableTester.getRows()).toHaveLength(11);
+
+ expect(tableTester.getRows({element: groups[1]})).toHaveLength(3);
+ expect(tableTester.getRows({element: groups[2]})).toHaveLength(3);
+ expect(tableTester.getRows({element: groups[3]})).toHaveLength(4);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.getRows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.getRows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
+
+ expect(tableTester.getFooterRows()).toHaveLength(1);
+ expect(tableTester.getFooterRows()[0]).toHaveTextContent('Blah');
});
});
diff --git a/packages/react-aria-components/test/Tabs.browser.test.tsx b/packages/react-aria-components/test/Tabs.browser.test.tsx
new file mode 100644
index 00000000000..8a778b0f008
--- /dev/null
+++ b/packages/react-aria-components/test/Tabs.browser.test.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {expect, it} from 'vitest';
+import React from 'react';
+import {render} from 'vitest-browser-react';
+import {Tab, TabList, TabPanel, Tabs} from '../src/Tabs';
+import {User} from '@react-aria/test-utils';
+
+function TabsExample() {
+ return (
+
+
+ One
+ Two
+ Three
+
+ Panel One
+ Panel Two
+ Panel Three
+
+ );
+}
+
+it.each`
+ interactionType
+ ${'mouse'}
+ ${'keyboard'}
+`('triggers a tab via $interactionType', async ({interactionType}) => {
+ let testUtilUser = new User();
+ let {container} = await render();
+
+ let tester = testUtilUser.createTester('Tabs', {
+ root: container.querySelector('[role=tablist]') as HTMLElement,
+ interactionType
+ });
+ let tabs = tester.getTabs();
+ await tester.triggerTab({tab: tabs[1]});
+ expect(tester.getSelectedTab()).toBe(tabs[1]);
+});
diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js
index 3500b2fa62e..881b7b2b14b 100644
--- a/packages/react-aria-components/test/Tabs.test.js
+++ b/packages/react-aria-components/test/Tabs.test.js
@@ -73,16 +73,16 @@ describe('Tabs', () => {
let {getByTestId} = renderTabs();
let tabs = getByTestId('tabs-wrapper');
let tabsTester = testUtilUser.createTester('Tabs', {root: tabs});
- let tablist = tabsTester.tablist;
+ let tablist = tabsTester.getTablist();
expect(tabs).toBeInTheDocument();
expect(tablist).toHaveAttribute('class', 'react-aria-TabList');
expect(tablist).toHaveAttribute('aria-label', 'Test');
- for (let tab of tabsTester.tabs) {
+ for (let tab of tabsTester.getTabs()) {
expect(tab).toHaveAttribute('class', 'react-aria-Tab');
}
- expect(tabsTester.tabpanels[0]).toHaveAttribute('class', 'react-aria-TabPanel');
+ expect(tabsTester.getTabpanels()[0]).toHaveAttribute('class', 'react-aria-TabPanel');
});
it('should render tabs with custom classes', () => {
@@ -399,8 +399,8 @@ describe('Tabs', () => {
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')});
await user.tab();
- expect(tabsTester.selectedTab).toBe(tabsTester.tabs[0]);
- expect(document.activeElement).toBe(tabsTester.tabpanels[0]);
+ expect(tabsTester.getSelectedTab()).toBe(tabsTester.getTabs()[0]);
+ expect(document.activeElement).toBe(tabsTester.getTabpanels()[0]);
});
it('should support selected state', async () => {
@@ -411,7 +411,7 @@ describe('Tabs', () => {
{className: ({isSelected}) => (isSelected ? 'selected' : '')}
);
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.getTabs();
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
expect(tabs[0]).toHaveClass('selected');
@@ -445,15 +445,15 @@ describe('Tabs', () => {
);
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')});
- expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('first-element');
+ expect(tabsTester.getActiveTabpanel().getAttribute('id')).toContain('first-element');
await tabsTester.triggerTab({tab: 1});
expect(onSelectionChange).toHaveBeenCalled();
- expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('second-element');
+ expect(tabsTester.getActiveTabpanel().getAttribute('id')).toContain('second-element');
await tabsTester.triggerTab({tab: 2});
expect(onSelectionChange).toHaveBeenCalled();
- expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('third-element');
+ expect(tabsTester.getActiveTabpanel().getAttribute('id')).toContain('third-element');
});
it('should support orientation', () => {
@@ -482,23 +482,23 @@ describe('Tabs', () => {
root: getByRole('tablist'),
interactionType
});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.getTabs();
await tabsTester.triggerTab({tab: 0});
- expect(tabsTester.selectedTab).toBe(tabs[0]);
- expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[0].id);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[0]);
+ expect(tabsTester.getActiveTabpanel().getAttribute('aria-labelledby')).toBe(tabs[0].id);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
- expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[1].id);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[1]);
+ expect(tabsTester.getActiveTabpanel().getAttribute('aria-labelledby')).toBe(tabs[1].id);
await tabsTester.triggerTab({tab: 2});
- expect(tabsTester.selectedTab).toBe(tabs[2]);
- expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[2].id);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[2]);
+ expect(tabsTester.getActiveTabpanel().getAttribute('aria-labelledby')).toBe(tabs[2].id);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
- expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[1].id);
+ expect(tabsTester.getSelectedTab()).toBe(tabs[1]);
+ expect(tabsTester.getActiveTabpanel().getAttribute('aria-labelledby')).toBe(tabs[1].id);
}
);
@@ -563,7 +563,7 @@ describe('Tabs', () => {
interactionType: 'keyboard'
});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.getTabs();
await tabsTester.triggerTab({tab: 0});
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
@@ -997,12 +997,12 @@ describe('Tabs', () => {
);
let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')});
- expect(tester.tabs.length).toBe(2);
+ expect(tester.getTabs().length).toBe(2);
let trigger = tree.getByRole('button');
let menu = testUtilUser.createTester('Menu', {root: trigger});
await menu.open();
- expect(menu.options()).toHaveLength(2);
+ expect(menu.getOptions()).toHaveLength(2);
await menu.close();
});
@@ -1034,12 +1034,12 @@ describe('Tabs', () => {
);
let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')});
- expect(tester.tabs.length).toBe(2);
+ expect(tester.getTabs().length).toBe(2);
let trigger = tree.getByRole('button');
let menu = testUtilUser.createTester('Select', {root: trigger});
await menu.open();
- expect(menu.options()).toHaveLength(3);
+ expect(menu.getOptions()).toHaveLength(3);
await menu.close();
});
@@ -1069,14 +1069,14 @@ describe('Tabs', () => {
);
let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')});
- expect(tester.tabs.length).toBe(2);
+ expect(tester.getTabs().length).toBe(2);
let menu = testUtilUser.createTester('ComboBox', {
interactionType: 'keyboard',
root: tree.container.querySelector('.react-aria-ComboBox')
});
await menu.open();
- expect(menu.options()).toHaveLength(3);
+ expect(menu.getOptions()).toHaveLength(3);
await menu.close();
});
});
diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx
index 5fc5a09f824..f0c922c1013 100644
--- a/packages/react-aria-components/test/Tree.test.tsx
+++ b/packages/react-aria-components/test/Tree.test.tsx
@@ -1782,7 +1782,7 @@ describe('Tree', () => {
let tree = render();
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows).toHaveLength(3);
let loaderRow = rows[2];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -1794,7 +1794,7 @@ describe('Tree', () => {
tree.rerender(
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(8);
let newLoaderRow = rows[4];
expect(newLoaderRow).toHaveTextContent('Loading...');
@@ -1809,7 +1809,7 @@ describe('Tree', () => {
let tree = render();
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows).toHaveLength(2);
expect(tree.queryByText('Loading...')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
@@ -1967,7 +1967,7 @@ describe('Tree', () => {
/>
);
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows).toHaveLength(8);
let rootLoaderRow = rows[7];
expect(rootLoaderRow).toHaveTextContent('Loading...');
@@ -1993,7 +1993,7 @@ describe('Tree', () => {
/>
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(9);
rootLoaderRow = rows[8];
rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -2024,7 +2024,7 @@ describe('Tree', () => {
/>
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(10);
rootLoaderRow = rows[9];
rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -2062,7 +2062,7 @@ describe('Tree', () => {
/>
);
- rows = treeTester.rows;
+ rows = treeTester.getRows();
expect(rows).toHaveLength(11);
rootLoaderRow = rows[10];
rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -2107,7 +2107,7 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows).toHaveLength(9);
let rootLoaderRow = rows[8];
let rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -2141,7 +2141,7 @@ describe('Tree', () => {
it.skip('should restore focus to the tree if the loader is keyboard focused when loading finishes', async () => {
let tree = render();
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.getRows();
expect(rows).toHaveLength(8);
let rootLoaderRow = rows[7];
expect(rootLoaderRow).toHaveTextContent('Loading...');
@@ -2152,7 +2152,7 @@ describe('Tree', () => {
tree.rerender();
- expect(document.activeElement).toBe(treeTester.tree);
+ expect(document.activeElement).toBe(treeTester.getTree());
});
});
});
@@ -2510,9 +2510,9 @@ describe('Tree', () => {
let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
- expect(firstTreeTester.rows).toHaveLength(2);
+ expect(firstTreeTester.getRows()).toHaveLength(2);
// has the empty state row
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.getRows()).toHaveLength(1);
await user.tab();
// selects and drops first row onto second tree
await user.keyboard('{ArrowRight}');
@@ -2527,13 +2527,13 @@ describe('Tree', () => {
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
});
act(() => jest.runAllTimers());
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.getRows()).toHaveLength(1);
// expands tree row children
await user.keyboard('{ArrowRight}');
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowRight}');
- expect(secondTreeTester.selectedRows).toHaveLength(9);
+ expect(secondTreeTester.getSelectedRows()).toHaveLength(9);
});
it('should focus the parent row when dropped on if it isnt expanded', async () => {
@@ -2542,9 +2542,9 @@ describe('Tree', () => {
let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
- expect(firstTreeTester.rows).toHaveLength(2);
+ expect(firstTreeTester.getRows()).toHaveLength(2);
// has the empty state row
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.getRows()).toHaveLength(1);
await user.tab();
// selects and drops first row onto second tree
await user.keyboard('{ArrowRight}');
@@ -2558,12 +2558,12 @@ describe('Tree', () => {
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
});
act(() => jest.runAllTimers());
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.getRows()).toHaveLength(1);
await user.keyboard('{ArrowRight}');
- expect(secondTreeTester.rows).toHaveLength(6);
+ expect(secondTreeTester.getRows()).toHaveLength(6);
// tab back to the first tree and drop a new row onto one of the 2nd tree's child rows as it is expanded
await user.tab({shift: true});
- expect(document.activeElement).toBe(firstTreeTester.rows[0]);
+ expect(document.activeElement).toBe(firstTreeTester.getRows()[0]);
await user.keyboard('{ArrowRight}');
await user.keyboard('{Enter}');
act(() => jest.runAllTimers());
@@ -2577,7 +2577,7 @@ describe('Tree', () => {
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
});
act(() => jest.runAllTimers());
- expect(document.activeElement).toBe(secondTreeTester.rows[2]);
+ expect(document.activeElement).toBe(secondTreeTester.getRows()[2]);
});
it('should focus the dropped row when dropped on a parent that is expanded', async () => {
@@ -2586,9 +2586,9 @@ describe('Tree', () => {
let firstTreeTester = testUtilUser.createTester('Tree', {root: trees[0]});
let secondTreeTester = testUtilUser.createTester('Tree', {root: trees[1]});
- expect(firstTreeTester.rows).toHaveLength(2);
+ expect(firstTreeTester.getRows()).toHaveLength(2);
// has the empty state row
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.getRows()).toHaveLength(1);
await user.tab();
// selects and drops first row onto second tree
await user.keyboard('{ArrowRight}');
@@ -2603,16 +2603,16 @@ describe('Tree', () => {
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
});
act(() => jest.runAllTimers());
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.getRows()).toHaveLength(1);
// expands tree row children
await user.keyboard('{ArrowRight}');
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowRight}');
- expect(secondTreeTester.rows).toHaveLength(9);
+ expect(secondTreeTester.getRows()).toHaveLength(9);
// tab back to the first tree and drop a new row onto one of the 2nd tree's child rows as it is expanded
await user.tab({shift: true});
- expect(document.activeElement).toBe(firstTreeTester.rows[0]);
+ expect(document.activeElement).toBe(firstTreeTester.getRows()[0]);
await user.keyboard('{ArrowRight}');
await user.keyboard('{Enter}');
@@ -2628,7 +2628,7 @@ describe('Tree', () => {
});
act(() => jest.runAllTimers());
expect(document.activeElement).toHaveTextContent('Projects');
- expect(document.activeElement).toBe(secondTreeTester.rows[3]);
+ expect(document.activeElement).toBe(secondTreeTester.getRows()[3]);
});
});
diff --git a/packages/react-aria-components/test/Treeble.test.js b/packages/react-aria-components/test/Treeble.test.js
index 555559672e6..c551171ab67 100644
--- a/packages/react-aria-components/test/Treeble.test.js
+++ b/packages/react-aria-components/test/Treeble.test.js
@@ -15,6 +15,7 @@ import {Cell as AriaCell, Column, Row, Table, TableBody, TableHeader} from '../s
import {Button} from '../src/Button';
import {Collection} from 'react-aria/Collection';
import {composeRenderProps} from '../src/utils';
+import {I18nProvider} from 'react-aria/I18nProvider';
import React from 'react';
import {useDragAndDrop} from '../src/useDragAndDrop';
import {User} from '@react-aria/test-utils';
@@ -203,124 +204,142 @@ describe('Treeble', () => {
let tree = render();
let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')});
- expect(tester.table).toHaveAttribute('role', 'treegrid');
-
- expect(tester.rows).toHaveLength(4);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'false');
- expect(tester.rows[0]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[0]).not.toHaveAttribute('data-expanded');
- expect(tester.rows[0]).toHaveAttribute('data-has-child-items', 'true');
- expect(tester.rows[0]).toHaveAttribute('data-level', '1');
- expect(tester.rows[0]).toHaveAttribute('style', '--table-row-level: 1;');
- expect(tester.rowHeaders[0]).toHaveTextContent('Games');
- expect(tester.rowHeaders[0]).toHaveAttribute('data-tree-column');
- for (let cell of tester.cells()) {
+ expect(tester.getTable()).toHaveAttribute('role', 'treegrid');
+
+ expect(tester.getRows()).toHaveLength(4);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'false');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[0]).not.toHaveAttribute('data-expanded');
+ expect(tester.getRows()[0]).toHaveAttribute('data-has-child-items', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('data-level', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('style', '--table-row-level: 1;');
+ expect(tester.getRowHeaders()[0]).toHaveTextContent('Games');
+ expect(tester.getRowHeaders()[0]).toHaveAttribute('data-tree-column');
+ for (let cell of tester.getCells()) {
expect(cell).not.toHaveAttribute('data-tree-column');
}
- for (let cell of tester.cells({element: tester.rows[0]})) {
+ for (let cell of tester.getCells({element: tester.getRows()[0]})) {
expect(cell).not.toHaveAttribute('data-expanded');
expect(cell).toHaveAttribute('data-has-child-items', 'true');
expect(cell).toHaveAttribute('data-level', '1');
}
- let button = within(tester.rowHeaders[0]).getByRole('button');
+ let button = within(tester.getRowHeaders()[0]).getByRole('button');
expect(button).toHaveAttribute('aria-label', 'Expand');
- expect(button).toHaveAttribute('aria-labelledby', `${button.id} ${tester.rowHeaders[0].id}`);
+ expect(button).toHaveAttribute(
+ 'aria-labelledby',
+ `${button.id} ${tester.getRowHeaders()[0].id}`
+ );
expect(button).toHaveAttribute('tabindex', '-1');
- expect(tester.rows[1]).toHaveAttribute('aria-expanded', 'false');
- expect(tester.rows[1]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[1]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[1]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[1]).not.toHaveAttribute('data-expanded');
- expect(tester.rows[1]).toHaveAttribute('data-has-child-items', 'true');
- expect(tester.rows[1]).toHaveAttribute('data-level', '1');
- expect(tester.rows[1]).toHaveAttribute('style', '--table-row-level: 1;');
- expect(tester.rowHeaders[1]).toHaveTextContent('Applications');
-
- expect(tester.rows[2]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[2]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[2]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[2]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[2]).not.toHaveAttribute('data-expanded');
- expect(tester.rows[2]).not.toHaveAttribute('data-has-child-items');
- expect(tester.rows[2]).toHaveAttribute('data-level', '1');
- expect(tester.rows[2]).toHaveAttribute('style', '--table-row-level: 1;');
- expect(tester.rowHeaders[2]).toHaveTextContent('2024 Financial Report');
-
- expect(tester.rows[3]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[3]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[3]).toHaveAttribute('aria-posinset', '4');
- expect(tester.rows[3]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[3]).not.toHaveAttribute('data-expanded');
- expect(tester.rows[3]).not.toHaveAttribute('data-has-child-items');
- expect(tester.rows[3]).toHaveAttribute('data-level', '1');
- expect(tester.rows[3]).toHaveAttribute('style', '--table-row-level: 1;');
- expect(tester.rowHeaders[3]).toHaveTextContent('Job Posting');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-expanded', 'false');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[1]).not.toHaveAttribute('data-expanded');
+ expect(tester.getRows()[1]).toHaveAttribute('data-has-child-items', 'true');
+ expect(tester.getRows()[1]).toHaveAttribute('data-level', '1');
+ expect(tester.getRows()[1]).toHaveAttribute('style', '--table-row-level: 1;');
+ expect(tester.getRowHeaders()[1]).toHaveTextContent('Applications');
+
+ expect(tester.getRows()[2]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[2]).not.toHaveAttribute('data-expanded');
+ expect(tester.getRows()[2]).not.toHaveAttribute('data-has-child-items');
+ expect(tester.getRows()[2]).toHaveAttribute('data-level', '1');
+ expect(tester.getRows()[2]).toHaveAttribute('style', '--table-row-level: 1;');
+ expect(tester.getRowHeaders()[2]).toHaveTextContent('2024 Financial Report');
+
+ expect(tester.getRows()[3]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-posinset', '4');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[3]).not.toHaveAttribute('data-expanded');
+ expect(tester.getRows()[3]).not.toHaveAttribute('data-has-child-items');
+ expect(tester.getRows()[3]).toHaveAttribute('data-level', '1');
+ expect(tester.getRows()[3]).toHaveAttribute('style', '--table-row-level: 1;');
+ expect(tester.getRowHeaders()[3]).toHaveTextContent('Job Posting');
});
- it.each(['mouse', 'touch', 'keyboard'])('should expand a row with %s', async interactionType => {
- let tree = render();
- let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')});
+ it.each`
+ interactionType | locale | direction
+ ${'mouse'} | ${'en-US'} | ${'ltr'}
+ ${'touch'} | ${'en-US'} | ${'ltr'}
+ ${'keyboard'} | ${'en-US'} | ${'ltr'}
+ ${'mouse'} | ${'ar-AE'} | ${'rtl'}
+ ${'touch'} | ${'ar-AE'} | ${'rtl'}
+ ${'keyboard'} | ${'ar-AE'} | ${'rtl'}
+ `(
+ 'should expand a row with $interactionType ($direction)',
+ async ({interactionType, locale, direction}) => {
+ let tree = render(
+
+
+
+ );
+ let tester = utils.createTester('Table', {root: tree.getByTestId('treeble'), direction});
+
+ await tester.toggleRowExpansion({row: 0, interactionType});
+
+ expect(tester.getRows()).toHaveLength(7);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[0]).toHaveAttribute('data-expanded', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('data-has-child-items', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('data-level', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('style', '--table-row-level: 1;');
+ expect(tester.getRowHeaders()[0]).toHaveTextContent('Games');
+ for (let cell of tester.getCells({element: tester.getRows()[0]})) {
+ expect(cell).toHaveAttribute('data-expanded');
+ expect(cell).toHaveAttribute('data-has-child-items', 'true');
+ expect(cell).toHaveAttribute('data-level', '1');
+ }
- await tester.toggleRowExpansion({row: 0, interactionType});
-
- expect(tester.rows).toHaveLength(7);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'true');
- expect(tester.rows[0]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[0]).toHaveAttribute('data-expanded', 'true');
- expect(tester.rows[0]).toHaveAttribute('data-has-child-items', 'true');
- expect(tester.rows[0]).toHaveAttribute('data-level', '1');
- expect(tester.rows[0]).toHaveAttribute('style', '--table-row-level: 1;');
- expect(tester.rowHeaders[0]).toHaveTextContent('Games');
- for (let cell of tester.cells({element: tester.rows[0]})) {
- expect(cell).toHaveAttribute('data-expanded');
- expect(cell).toHaveAttribute('data-has-child-items', 'true');
- expect(cell).toHaveAttribute('data-level', '1');
+ expect(tester.getRows()[1]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRows()[1]).toHaveAttribute('style', '--table-row-level: 2;');
+ expect(tester.getRowHeaders()[1]).toHaveTextContent('Mario Kart');
+
+ expect(tester.getRows()[2]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRows()[2]).toHaveAttribute('style', '--table-row-level: 2;');
+ expect(tester.getRowHeaders()[2]).toHaveTextContent('Tetris');
+
+ expect(tester.getRows()[3]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRows()[3]).toHaveAttribute('style', '--table-row-level: 2;');
+ expect(tester.getRowHeaders()[3]).toHaveTextContent('Pac-Man');
+
+ expect(tester.getRows()[4]).toHaveAttribute('aria-expanded', 'false');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[4]).toHaveAttribute('style', '--table-row-level: 1;');
+ expect(tester.getRowHeaders()[4]).toHaveTextContent('Applications');
+
+ expect(tester.getRows()[5]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[5]).toHaveAttribute('style', '--table-row-level: 1;');
+ expect(tester.getRowHeaders()[5]).toHaveTextContent('2024 Financial Report');
+
+ await tester.toggleRowExpansion({row: 0, interactionType});
+ expect(tester.getRows()).toHaveLength(4);
}
-
- expect(tester.rows[1]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[1]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[1]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[1]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rows[1]).toHaveAttribute('style', '--table-row-level: 2;');
- expect(tester.rowHeaders[1]).toHaveTextContent('Mario Kart');
-
- expect(tester.rows[2]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[2]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rows[2]).toHaveAttribute('style', '--table-row-level: 2;');
- expect(tester.rowHeaders[2]).toHaveTextContent('Tetris');
-
- expect(tester.rows[3]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[3]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[3]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[3]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rows[3]).toHaveAttribute('style', '--table-row-level: 2;');
- expect(tester.rowHeaders[3]).toHaveTextContent('Pac-Man');
-
- expect(tester.rows[4]).toHaveAttribute('aria-expanded', 'false');
- expect(tester.rows[4]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[4]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[4]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[4]).toHaveAttribute('style', '--table-row-level: 1;');
- expect(tester.rowHeaders[4]).toHaveTextContent('Applications');
-
- expect(tester.rows[5]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[5]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[5]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[5]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[5]).toHaveAttribute('style', '--table-row-level: 1;');
- expect(tester.rowHeaders[5]).toHaveTextContent('2024 Financial Report');
-
- await tester.toggleRowExpansion({row: 0, interactionType});
- expect(tester.rows).toHaveLength(4);
- });
+ );
it('should support defaultExpandedKeys', async () => {
let onExpandedChange = jest.fn();
@@ -329,109 +348,109 @@ describe('Treeble', () => {
);
let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')});
- expect(tester.rows).toHaveLength(7);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'true');
- expect(tester.rows[0]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-setsize', '4');
-
- expect(tester.rowHeaders[0]).toHaveTextContent('Games');
-
- expect(tester.rows[1]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[1]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[1]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[1]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[1]).toHaveTextContent('Mario Kart');
-
- expect(tester.rows[2]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[2]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[2]).toHaveTextContent('Tetris');
-
- expect(tester.rows[3]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[3]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[3]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[3]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[3]).toHaveTextContent('Pac-Man');
-
- expect(tester.rows[4]).toHaveAttribute('aria-expanded', 'false');
- expect(tester.rows[4]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[4]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[4]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rowHeaders[4]).toHaveTextContent('Applications');
-
- expect(tester.rows[5]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[5]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[5]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[5]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rowHeaders[5]).toHaveTextContent('2024 Financial Report');
+ expect(tester.getRows()).toHaveLength(7);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-setsize', '4');
+
+ expect(tester.getRowHeaders()[0]).toHaveTextContent('Games');
+
+ expect(tester.getRows()[1]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[1]).toHaveTextContent('Mario Kart');
+
+ expect(tester.getRows()[2]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[2]).toHaveTextContent('Tetris');
+
+ expect(tester.getRows()[3]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[3]).toHaveTextContent('Pac-Man');
+
+ expect(tester.getRows()[4]).toHaveAttribute('aria-expanded', 'false');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRowHeaders()[4]).toHaveTextContent('Applications');
+
+ expect(tester.getRows()[5]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRowHeaders()[5]).toHaveTextContent('2024 Financial Report');
await tester.toggleRowExpansion({row: 4});
expect(onExpandedChange).toHaveBeenCalledTimes(1);
expect(onExpandedChange).toHaveBeenCalledWith(new Set(['games', 'apps']));
- expect(tester.rows).toHaveLength(10);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'true');
- expect(tester.rows[0]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rows[0]).toHaveAttribute('data-expanded', 'true');
- expect(tester.rows[0]).toHaveAttribute('data-has-child-items', 'true');
- expect(tester.rows[0]).toHaveAttribute('data-level', '1');
- expect(tester.rowHeaders[0]).toHaveTextContent('Games');
-
- expect(tester.rows[1]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[1]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[1]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[1]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[1]).toHaveTextContent('Mario Kart');
-
- expect(tester.rows[2]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[2]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[2]).toHaveTextContent('Tetris');
-
- expect(tester.rows[3]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[3]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[3]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[3]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[3]).toHaveTextContent('Pac-Man');
-
- expect(tester.rows[4]).toHaveAttribute('aria-expanded', 'true');
- expect(tester.rows[4]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[4]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[4]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rowHeaders[4]).toHaveTextContent('Applications');
-
- expect(tester.rows[5]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[5]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[5]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[5]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[5]).toHaveTextContent('Photoshop');
-
- expect(tester.rows[6]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[6]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[6]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[6]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[6]).toHaveTextContent('Premiere');
-
- expect(tester.rows[7]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[7]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[7]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[7]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[7]).toHaveTextContent('Lightroom');
-
- expect(tester.rows[8]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[8]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[8]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[8]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rowHeaders[8]).toHaveTextContent('2024 Financial Report');
+ expect(tester.getRows()).toHaveLength(10);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRows()[0]).toHaveAttribute('data-expanded', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('data-has-child-items', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('data-level', '1');
+ expect(tester.getRowHeaders()[0]).toHaveTextContent('Games');
+
+ expect(tester.getRows()[1]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[1]).toHaveTextContent('Mario Kart');
+
+ expect(tester.getRows()[2]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[2]).toHaveTextContent('Tetris');
+
+ expect(tester.getRows()[3]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[3]).toHaveTextContent('Pac-Man');
+
+ expect(tester.getRows()[4]).toHaveAttribute('aria-expanded', 'true');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRowHeaders()[4]).toHaveTextContent('Applications');
+
+ expect(tester.getRows()[5]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[5]).toHaveTextContent('Photoshop');
+
+ expect(tester.getRows()[6]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[6]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[6]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[6]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[6]).toHaveTextContent('Premiere');
+
+ expect(tester.getRows()[7]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[7]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[7]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[7]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[7]).toHaveTextContent('Lightroom');
+
+ expect(tester.getRows()[8]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[8]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[8]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[8]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRowHeaders()[8]).toHaveTextContent('2024 Financial Report');
await tester.toggleRowExpansion({row: 4});
- expect(tester.rows).toHaveLength(7);
+ expect(tester.getRows()).toHaveLength(7);
expect(onExpandedChange).toHaveBeenCalledTimes(2);
expect(onExpandedChange).toHaveBeenLastCalledWith(new Set(['games']));
@@ -442,49 +461,49 @@ describe('Treeble', () => {
let tree = render();
let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')});
- expect(tester.rows).toHaveLength(7);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'true');
- expect(tester.rows[0]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[0]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rowHeaders[0]).toHaveTextContent('Games');
-
- expect(tester.rows[1]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[1]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[1]).toHaveAttribute('aria-posinset', '1');
- expect(tester.rows[1]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[1]).toHaveTextContent('Mario Kart');
-
- expect(tester.rows[2]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[2]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[2]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[2]).toHaveTextContent('Tetris');
-
- expect(tester.rows[3]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[3]).toHaveAttribute('aria-level', '2');
- expect(tester.rows[3]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[3]).toHaveAttribute('aria-setsize', '3');
- expect(tester.rowHeaders[3]).toHaveTextContent('Pac-Man');
-
- expect(tester.rows[4]).toHaveAttribute('aria-expanded', 'false');
- expect(tester.rows[4]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[4]).toHaveAttribute('aria-posinset', '2');
- expect(tester.rows[4]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rowHeaders[4]).toHaveTextContent('Applications');
-
- expect(tester.rows[5]).not.toHaveAttribute('aria-expanded');
- expect(tester.rows[5]).toHaveAttribute('aria-level', '1');
- expect(tester.rows[5]).toHaveAttribute('aria-posinset', '3');
- expect(tester.rows[5]).toHaveAttribute('aria-setsize', '4');
- expect(tester.rowHeaders[5]).toHaveTextContent('2024 Financial Report');
+ expect(tester.getRows()).toHaveLength(7);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[0]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRowHeaders()[0]).toHaveTextContent('Games');
+
+ expect(tester.getRows()[1]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-posinset', '1');
+ expect(tester.getRows()[1]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[1]).toHaveTextContent('Mario Kart');
+
+ expect(tester.getRows()[2]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[2]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[2]).toHaveTextContent('Tetris');
+
+ expect(tester.getRows()[3]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-level', '2');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[3]).toHaveAttribute('aria-setsize', '3');
+ expect(tester.getRowHeaders()[3]).toHaveTextContent('Pac-Man');
+
+ expect(tester.getRows()[4]).toHaveAttribute('aria-expanded', 'false');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-posinset', '2');
+ expect(tester.getRows()[4]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRowHeaders()[4]).toHaveTextContent('Applications');
+
+ expect(tester.getRows()[5]).not.toHaveAttribute('aria-expanded');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-level', '1');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-posinset', '3');
+ expect(tester.getRows()[5]).toHaveAttribute('aria-setsize', '4');
+ expect(tester.getRowHeaders()[5]).toHaveTextContent('2024 Financial Report');
await tester.toggleRowExpansion({row: 4});
expect(onExpandedChange).toHaveBeenCalledTimes(1);
expect(onExpandedChange).toHaveBeenCalledWith(new Set(['games', 'apps']));
- expect(tester.rows).toHaveLength(7); // controlled
+ expect(tester.getRows()).toHaveLength(7); // controlled
});
it('supports keyboard navigation of flattened rows', async () => {
@@ -493,16 +512,16 @@ describe('Treeble', () => {
await user.tab();
- for (let i = 0; i < tester.rows.length; i++) {
- expect(document.activeElement).toBe(tester.rows[i]);
+ for (let i = 0; i < tester.getRows().length; i++) {
+ expect(document.activeElement).toBe(tester.getRows()[i]);
await user.keyboard('{ArrowDown}');
}
await user.keyboard('{Home}');
- expect(document.activeElement).toBe(tester.rows[0]);
+ expect(document.activeElement).toBe(tester.getRows()[0]);
await user.keyboard('{End}');
- expect(document.activeElement).toBe(tester.rows[tester.rows.length - 1]);
+ expect(document.activeElement).toBe(tester.getRows()[tester.getRows().length - 1]);
});
it('supports keyboard navigation of cells', async () => {
@@ -510,25 +529,25 @@ describe('Treeble', () => {
let tester = utils.createTester('Table', {root: tree.getByTestId('treeble')});
await user.tab();
- expect(document.activeElement).toBe(tester.rows[0]);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'false');
+ expect(document.activeElement).toBe(tester.getRows()[0]);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'false');
await user.keyboard('{ArrowRight}');
- expect(document.activeElement).toBe(tester.rows[0]);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(document.activeElement).toBe(tester.getRows()[0]);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'true');
- let cells = [tester.rowHeaders[0], ...tester.cells({element: tester.rows[0]})];
+ let cells = [tester.getRowHeaders()[0], ...tester.getCells({element: tester.getRows()[0]})];
for (let cell of cells) {
await user.keyboard('{ArrowRight}');
expect(document.activeElement).toBe(cell);
}
await user.keyboard('{ArrowRight}');
- expect(document.activeElement).toBe(tester.rows[0]);
+ expect(document.activeElement).toBe(tester.getRows()[0]);
await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(tester.rows[0]);
- expect(tester.rows[0]).toHaveAttribute('aria-expanded', 'false');
+ expect(document.activeElement).toBe(tester.getRows()[0]);
+ expect(tester.getRows()[0]).toHaveAttribute('aria-expanded', 'false');
for (let cell of cells.reverse()) {
await user.keyboard('{ArrowLeft}');
@@ -536,7 +555,7 @@ describe('Treeble', () => {
}
await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(tester.rows[0]);
+ expect(document.activeElement).toBe(tester.getRows()[0]);
});
it('supports selection', async () => {
@@ -552,7 +571,7 @@ describe('Treeble', () => {
await tester.toggleRowSelection({row: 0});
await user.keyboard('{Shift>}');
- await user.click(tester.rows[2]);
+ await user.click(tester.getRows()[2]);
await user.keyboard('{/Shift}');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
@@ -612,7 +631,7 @@ describe('Treeble', () => {
await user.keyboard('{Enter}');
act(() => jest.runAllTimers());
- expect(tester.rowHeaders.map(r => r.textContent)).toEqual([
+ expect(tester.getRowHeaders().map(r => r.textContent)).toEqual([
'>Documents',
'>Project',
'Image 2',
diff --git a/packages/react-aria-components/test/VirtualizedMenu.test.tsx b/packages/react-aria-components/test/VirtualizedMenu.test.tsx
index 21c87e67b41..053469968de 100644
--- a/packages/react-aria-components/test/VirtualizedMenu.test.tsx
+++ b/packages/react-aria-components/test/VirtualizedMenu.test.tsx
@@ -71,7 +71,7 @@ describe('virtualized menu', () => {
tester.setInteractionType('mouse');
await tester.open();
let items = getAllByRole('menuitem');
- let menu = tester.menu;
+ let menu = tester.getMenu();
expect(menu).toBeInTheDocument();
expect(items[0]).toHaveAttribute('aria-posinset', '1');
expect(items[0]).toHaveAttribute('aria-setsize', '50');
diff --git a/vitest.browser.config.ts b/vitest.browser.config.ts
index 3d582fb3446..96a154b7e1b 100644
--- a/vitest.browser.config.ts
+++ b/vitest.browser.config.ts
@@ -21,12 +21,12 @@ import svgr from 'vite-plugin-svgr';
const s2Dir = path.resolve(__dirname, 'packages/@react-spectrum/s2');
-// Handles ../intl/*.json imports
+// Handles ../intl/*.json and ../intl//*.json imports.
function intlJsonPlugin(): Plugin {
return {
name: 'intl-json-loader',
async resolveId(source, importer) {
- if (source.includes('/intl/*.json') && importer) {
+ if (/\/intl\/.*\*\.json$/.test(source) && importer) {
const dir = path.dirname(importer);
const intlDir = path.resolve(dir, source.replace('*.json', ''));
return `virtual:intl-messages:${intlDir}`;
diff --git a/yarn.lock b/yarn.lock
index 139d5124625..def4ad819e5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7191,7 +7191,7 @@ __metadata:
react: "npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
react-dom: "npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
peerDependencies:
- "@testing-library/react": ^16.0.0
+ "@testing-library/dom": ^10.0.0
"@testing-library/user-event": ^14.0.0
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
@@ -8280,7 +8280,7 @@ __metadata:
react: "npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
react-dom: "npm:^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
peerDependencies:
- "@testing-library/react": ^16.0.0
+ "@testing-library/dom": ^10.0.0
"@testing-library/user-event": ^14.0.0
jest: ^29.5.0
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1