diff --git a/packages/@adobe/react-spectrum/docs/combobox/ComboBox.mdx b/packages/@adobe/react-spectrum/docs/combobox/ComboBox.mdx
index 7c4e54b76fe..c2abb6b8429 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.listbox()).toBeInTheDocument();
let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.combobox().value).toBe('One');
+ expect(comboboxTester.listbox()).not.toBeInTheDocument();
});
```
diff --git a/packages/@adobe/react-spectrum/docs/list/ListView.mdx b/packages/@adobe/react-spectrum/docs/list/ListView.mdx
index f774d1e866d..50d0a6258c0 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.rows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.selectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).toHaveLength(0);
});
```
diff --git a/packages/@adobe/react-spectrum/docs/menu/MenuTrigger.mdx b/packages/@adobe/react-spectrum/docs/menu/MenuTrigger.mdx
index eb3d403692f..7cf702ba1c8 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.menu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.submenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.menu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.options()[0]});
+ expect(submenuTester.menu()).not.toBeInTheDocument();
+ expect(menuTester.menu()).not.toBeInTheDocument();
});
```
diff --git a/packages/@adobe/react-spectrum/docs/picker/Picker.mdx b/packages/@adobe/react-spectrum/docs/picker/Picker.mdx
index f522760486d..14a2b0a06c2 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.trigger();
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..43bc410349d 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.selectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.selectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.rows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.selectedRows()).toHaveLength(0);
});
```
diff --git a/packages/@adobe/react-spectrum/docs/tabs/Tabs.mdx b/packages/@adobe/react-spectrum/docs/tabs/Tabs.mdx
index 1d28a009f2b..a0916431483 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.tabs();
+ expect(tabsTester.selectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.selectedTab()).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..07df8cd8b9d 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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.selectedRows()).toHaveLength(2);
+ expect(within(treeTester.rows()[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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[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 c69c85c8e1d..e3eaecccaf7 100644
--- a/packages/@adobe/react-spectrum/test/checkbox/CheckboxGroup.test.js
+++ b/packages/@adobe/react-spectrum/test/checkbox/CheckboxGroup.test.js
@@ -800,29 +800,29 @@ describe('CheckboxGroup', () => {
);
let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByRole('group')});
- expect(checkboxGroupTester.checkboxgroup).toHaveAttribute('role');
- let checkboxes = checkboxGroupTester.checkboxes;
+ expect(checkboxGroupTester.checkboxgroup()).toHaveAttribute('role');
+ let checkboxes = checkboxGroupTester.checkboxes();
await checkboxGroupTester.toggleCheckbox({checkbox: checkboxes[0]});
expect(checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.selectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4, interactionType: 'keyboard'});
expect(checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.selectedCheckboxes()).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.selectedCheckboxes()).toHaveLength(3);
await checkboxGroupTester.toggleCheckbox({checkbox: 'Soccer', interactionType: 'keyboard'});
expect(checkboxes[0]).not.toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.selectedCheckboxes()).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.selectedCheckboxes()).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 651b7d5e05b..c39522037cc 100644
--- a/packages/@adobe/react-spectrum/test/combobox/ComboBox.test.js
+++ b/packages/@adobe/react-spectrum/test/combobox/ComboBox.test.js
@@ -315,23 +315,23 @@ describe('ComboBox', function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('One');
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onFocus).not.toHaveBeenCalled();
comboboxTester.setInteractionType('keyboard');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
});
@@ -341,11 +341,11 @@ describe('ComboBox', function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('One');
- expect(comboboxTester.listbox).toBeFalsy();
- expect(comboboxTester.combobox.value).toBe('Blargh');
+ expect(comboboxTester.listbox()).toBeFalsy();
+ expect(comboboxTester.combobox().value).toBe('Blargh');
expect(onOpenChange).not.toHaveBeenCalled();
expect(onFocus).toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
@@ -353,14 +353,14 @@ describe('ComboBox', function () {
comboboxTester.setInteractionType('keyboard');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
expect(onInputChange).not.toHaveBeenCalled();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
expect(onOpenChange).not.toHaveBeenCalled();
});
@@ -368,7 +368,7 @@ describe('ComboBox', function () {
let tree = renderComboBox();
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.combobox();
expect(combobox).not.toHaveAttribute('aria-controls');
expect(combobox).not.toHaveAttribute('aria-activedescendant');
expect(combobox).toHaveAttribute('aria-autocomplete', 'list');
@@ -386,7 +386,7 @@ describe('ComboBox', function () {
expect(combobox.value).toBe('On');
expect(items[0]).toHaveTextContent('One');
- expect(combobox).toHaveAttribute('aria-controls', comboboxTester.listbox.id);
+ expect(combobox).toHaveAttribute('aria-controls', comboboxTester.listbox().id);
expect(combobox).not.toHaveAttribute('aria-activedescendant');
await user.keyboard('{ArrowDown}');
@@ -394,7 +394,7 @@ describe('ComboBox', function () {
jest.runAllTimers();
});
- expect(combobox).toHaveAttribute('aria-activedescendant', comboboxTester.focusedOption.id);
+ expect(combobox).toHaveAttribute('aria-activedescendant', comboboxTester.focusedOption().id);
});
describe('refs', function () {
@@ -426,11 +426,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.trigger();
+ let combobox = comboboxTester.combobox();
await comboboxTester.open({triggerBehavior: 'focus'});
- let listbox = comboboxTester.listbox;
+ let listbox = comboboxTester.listbox();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true, 'focus');
await testComboBoxOpen(combobox, button, listbox);
@@ -440,11 +440,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.trigger();
+ let combobox = comboboxTester.combobox();
await comboboxTester.open({triggerBehavior: 'manual'});
- let listbox = comboboxTester.listbox;
+ let listbox = comboboxTester.listbox();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true, 'focus');
await testComboBoxOpen(combobox, button, listbox);
@@ -458,18 +458,18 @@ describe('ComboBox', function () {
let combobox = getByRole('combobox');
let comboboxTester = testUtilUser.createTester('ComboBox', {root: combobox, trigger: button});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeInTheDocument();
- expect(document.activeElement).toBe(comboboxTester.combobox);
+ expect(comboboxTester.listbox()).toBeInTheDocument();
+ expect(document.activeElement).toBe(comboboxTester.combobox());
- await user.click(comboboxTester.trigger);
+ await user.click(comboboxTester.trigger());
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
});
it('doesn\'t focus first item if there are items loaded', async function () {
@@ -492,21 +492,21 @@ describe('ComboBox', function () {
let tree = renderComboBox({});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.combobox();
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.combobox());
+ expect(comboboxTester.listbox()).toBeInTheDocument();
- let button = comboboxTester.trigger;
+ let button = comboboxTester.trigger();
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.listbox()).toBeFalsy();
});
it('it doesn\'t reset the focused item when re-opening the menu', async function () {
@@ -514,15 +514,15 @@ describe('ComboBox', function () {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
await comboboxTester.open();
- expect(comboboxTester.combobox).not.toHaveAttribute('aria-activedescendant');
+ expect(comboboxTester.combobox()).not.toHaveAttribute('aria-activedescendant');
let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
+ await comboboxTester.toggleOptionSelection({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
+ expect(comboboxTester.combobox().value).toBe('One');
await comboboxTester.open();
- expect(comboboxTester.combobox).toHaveAttribute('aria-activedescendant', options[0].id);
+ expect(comboboxTester.combobox()).toHaveAttribute('aria-activedescendant', options[0].id);
});
it('shows all items', async function () {
@@ -865,7 +865,7 @@ describe('ComboBox', function () {
let tree = renderComboBox({defaultSelectedKey: '2'});
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.combobox();
expect(combobox.value).toBe('Two');
act(() => combobox.focus());
@@ -876,8 +876,8 @@ describe('ComboBox', function () {
expect(combobox.value).toBe('Tw');
expect(comboboxTester.options().length).toBe(1);
- await comboboxTester.selectOption({option: 'Two'});
- expect(comboboxTester.listbox).toBeFalsy();
+ await comboboxTester.toggleOptionSelection({option: 'Two'});
+ expect(comboboxTester.listbox()).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 e52b04f4ed5..97008fd2f39 100644
--- a/packages/@adobe/react-spectrum/test/dialog/DialogTrigger.test.js
+++ b/packages/@adobe/react-spectrum/test/dialog/DialogTrigger.test.js
@@ -78,7 +78,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.dialog();
expect(dialog).toBeVisible();
let modal = getByTestId('modal');
@@ -126,7 +126,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.dialog();
expect(dialog).toBeVisible();
let popover = getByTestId('popover');
@@ -276,7 +276,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.dialog();
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 c55e8ba5211..93432c17de4 100644
--- a/packages/@adobe/react-spectrum/test/list/ListView.test.js
+++ b/packages/@adobe/react-spectrum/test/list/ListView.test.js
@@ -177,7 +177,7 @@ describe('ListView', function () {
expect(grid).toHaveAttribute('aria-rowcount', '3');
expect(grid).toHaveAttribute('aria-colcount', '1');
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows).toHaveLength(3);
expect(rows[0]).toHaveAttribute('aria-rowindex', '1');
expect(rows[1]).toHaveAttribute('aria-rowindex', '2');
@@ -866,14 +866,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.rows();
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.selectedRows()).toHaveLength(1);
await user.keyboard('{Control>}a{/Control}');
act(() => jest.runAllTimers());
@@ -881,7 +881,7 @@ describe('ListView', function () {
onSelectionChange.mockClear();
expect(announce).toHaveBeenLastCalledWith('All items selected.');
expect(announce).toHaveBeenCalledTimes(2);
- expect(gridListTester.selectedRows).toHaveLength(3);
+ expect(gridListTester.selectedRows()).toHaveLength(3);
fireEvent.keyDown(rows[0], {key: 'Escape'});
fireEvent.keyUp(rows[0], {key: 'Escape'});
@@ -889,7 +889,7 @@ describe('ListView', function () {
onSelectionChange.mockClear();
expect(announce).toHaveBeenLastCalledWith('No items selected.');
expect(announce).toHaveBeenCalledTimes(3);
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).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 4985b2e6ad2..c943b0aa2da 100644
--- a/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
+++ b/packages/@adobe/react-spectrum/test/listbox/ListBox.test.js
@@ -94,11 +94,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.listbox();
expect(listbox).toBeTruthy();
expect(listbox).toHaveAttribute('aria-labelledby', 'label');
- let sections = listboxTester.sections;
+ let sections = listboxTester.sections();
expect(sections.length).toBe(withSection.length);
for (let section of sections) {
@@ -126,11 +126,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 () {
@@ -194,7 +194,7 @@ describe('ListBox', function () {
let tree = renderComponent({onSelectionChange, defaultSelectedKeys: ['Blah'], autoFocus: 'first', selectionMode: 'single'});
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
- let selectedOptions = listboxTester.selectedOptions;
+ let selectedOptions = listboxTester.selectedOptions();
expect(selectedOptions).toHaveLength(1);
expect(selectedOptions[0]).toBe(document.activeElement);
expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true');
@@ -206,7 +206,7 @@ describe('ListBox', function () {
// Select a different listbox item via enter
await listboxTester.toggleOptionSelection({option: 4, interactionType: 'keyboard'});
- selectedOptions = listboxTester.selectedOptions;
+ selectedOptions = listboxTester.selectedOptions();
expect(selectedOptions[0]).toHaveAttribute('aria-selected', 'true');
itemText = within(selectedOptions[0]).getByText('Bleh');
expect(itemText).toBeTruthy();
@@ -819,8 +819,8 @@ 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;
+ let item = listboxTester.findOption({indexOrText: 'Foo 1'});
+ let listboxSections = listboxTester.sections();
expect(listboxTester.options({element: listboxSections[0]})).toContain(item);
expect(listboxSections[0]).toBe(getByLabelText('Section 1'));
@@ -837,8 +837,8 @@ describe('ListBox', function () {
rerender();
listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- item = listboxTester.findOption({optionIndexOrText: 'Foo 1'});
- listboxSections = listboxTester.sections;
+ item = listboxTester.findOption({indexOrText: 'Foo 1'});
+ listboxSections = listboxTester.sections();
expect(listboxTester.options({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 8d60edc93f2..126a693db00 100644
--- a/packages/@adobe/react-spectrum/test/menu/MenuTrigger.test.js
+++ b/packages/@adobe/react-spectrum/test/menu/MenuTrigger.test.js
@@ -124,7 +124,7 @@ describe('MenuTrigger', function () {
await triggerEvent(triggerButton);
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeInTheDocument();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
@@ -237,7 +237,7 @@ describe('MenuTrigger', function () {
expect(onSelect).toBeCalledTimes(0);
}
- await menuTester.selectOption({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false});
+ await menuTester.toggleOptionSelection({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false});
if (Component === MenuTrigger) {
expect(onSelectionChange).toBeCalledTimes(1);
@@ -245,13 +245,13 @@ describe('MenuTrigger', function () {
expect(onSelect).toBeCalledTimes(1);
}
- expect(menuTester.menu).toBeInTheDocument();
+ expect(menuTester.menu()).toBeInTheDocument();
if (Component === MenuTrigger) {
- expect(menuTester.trigger).toHaveAttribute('aria-expanded', 'true');
+ expect(menuTester.trigger()).toHaveAttribute('aria-expanded', 'true');
expect(onOpenChange).toBeCalledTimes(1);
} else {
- expect(menuTester.trigger).toHaveAttribute('aria-expanded');
+ expect(menuTester.trigger()).toHaveAttribute('aria-expanded');
expect(onOpen).toBeCalledTimes(1);
expect(onClose).toBeCalledTimes(0);
}
@@ -269,10 +269,10 @@ describe('MenuTrigger', function () {
expect(onOpenChange).toBeCalledTimes(1);
expect(onSelectionChange).toBeCalledTimes(0);
menuTester.setInteractionType('keyboard');
- await menuTester.selectOption({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false});
+ await menuTester.toggleOptionSelection({option: 'Foo', menuSelectionMode: 'single', closesOnSelect: false});
- expect(menuTester.menu).toBeInTheDocument();
- expect(menuTester.trigger).toHaveAttribute('aria-expanded', 'true');
+ expect(menuTester.menu()).toBeInTheDocument();
+ expect(menuTester.trigger()).toHaveAttribute('aria-expanded', 'true');
expect(onOpenChange).toBeCalledTimes(1);
});
@@ -288,15 +288,15 @@ describe('MenuTrigger', function () {
expect(onOpenChange).toBeCalledTimes(1);
expect(onSelectionChange).toBeCalledTimes(0);
- await menuTester.selectOption({option: 'Foo', menuSelectionMode: 'multiple', keyboardActivation: 'Space'});
+ 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({option: 'Bar', menuSelectionMode: 'multiple', keyboardActivation: 'Space'});
+ await menuTester.toggleOptionSelection({option: 'Bar', menuSelectionMode: 'multiple', keyboardActivation: 'Space'});
expect(onSelectionChange).toBeCalledTimes(2);
expect(onSelectionChange.mock.calls[1][0].has('Bar')).toBeTruthy();
await menuTester.close();
- expect(menuTester.menu).not.toBeInTheDocument();
+ expect(menuTester.menu()).not.toBeInTheDocument();
expect(onOpenChange).toBeCalledTimes(2);
});
@@ -875,7 +875,7 @@ describe('MenuTrigger', function () {
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
await user.tab();
act(() => {jest.runAllTimers();});
diff --git a/packages/@adobe/react-spectrum/test/picker/Picker.test.js b/packages/@adobe/react-spectrum/test/picker/Picker.test.js
index 16ff41e2960..ecc09a2fa5e 100644
--- a/packages/@adobe/react-spectrum/test/picker/Picker.test.js
+++ b/packages/@adobe/react-spectrum/test/picker/Picker.test.js
@@ -94,10 +94,10 @@ describe('Picker', function () {
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
expect(queryByRole('listbox')).toBeNull();
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
@@ -204,11 +204,11 @@ 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.trigger();
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
@@ -238,7 +238,7 @@ describe('Picker', function () {
expect(queryByRole('listbox')).toBeNull();
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
// 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())
@@ -246,7 +246,7 @@ describe('Picker', function () {
fireEvent.keyUp(picker, {key: 'ArrowDown'});
act(() => jest.runAllTimers());
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
@@ -527,10 +527,10 @@ describe('Picker', function () {
expect(queryByRole('listbox')).toBeNull();
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
expect(listbox).toBeVisible();
expect(onOpenChange).toBeCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true);
@@ -937,11 +937,11 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
let items = selectTester.options();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('One');
@@ -950,7 +950,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');
@@ -969,11 +969,11 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
let items = selectTester.options();
expect(items.length).toBe(3);
expect(items[0]).toHaveTextContent('Empty');
@@ -982,19 +982,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);
@@ -1060,7 +1060,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
selectTester.setInteractionType('keyboard');
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
@@ -1071,7 +1071,7 @@ describe('Picker', function () {
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');
@@ -1136,13 +1136,13 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
expect(picker).toHaveTextContent('Select…');
expect(onOpenChangeSpy).toHaveBeenCalledTimes(0);
await selectTester.open();
expect(onOpenChangeSpy).toHaveBeenCalledTimes(1);
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
let label = getAllByText('Test')[0];
expect(listbox).toBeVisible();
expect(listbox).toHaveAttribute('aria-labelledby', label.id);
@@ -1152,7 +1152,7 @@ describe('Picker', function () {
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);
@@ -1340,15 +1340,15 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
expect(picker).toHaveTextContent('Select…');
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
let items = selectTester.options();
expect(items.length).toBe(6);
- let groups = selectTester.sections;
+ let groups = selectTester.sections();
expect(groups).toHaveLength(2);
expect(groups[0]).toHaveAttribute('aria-labelledby', getByText('Section 1').id);
@@ -1396,7 +1396,7 @@ describe('Picker', function () {
// Open again
await selectTester.open();
- listbox = selectTester.listbox;
+ listbox = selectTester.listbox();
items = selectTester.options();
expect(items.length).toBe(6);
@@ -1502,14 +1502,14 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
expect(picker).toHaveTextContent('Two');
await selectTester.open();
let items = selectTester.options();
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);
@@ -2080,7 +2080,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
let input = document.querySelector('[name=picker]');
expect(input).toHaveAttribute('required');
expect(picker).not.toHaveAttribute('aria-describedby');
@@ -2092,7 +2092,7 @@ describe('Picker', function () {
expect(document.getElementById(picker.getAttribute('aria-describedby'))).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');
});
@@ -2109,7 +2109,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByRole('button')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
let input = document.querySelector('[name=picker]');
expect(picker).not.toHaveAttribute('aria-describedby');
expect(input.validity.valid).toBe(false);
@@ -2120,7 +2120,7 @@ describe('Picker', function () {
expect(document.getElementById(picker.getAttribute('aria-describedby'))).toHaveTextContent('Invalid value');
expect(document.activeElement).toBe(picker);
- await selectTester.selectOption({option: 'One'});
+ await selectTester.toggleOptionSelection({option: 'One'});
expect(picker).not.toHaveAttribute('aria-describedby');
});
@@ -2150,7 +2150,7 @@ describe('Picker', function () {
let {getByTestId} = render();
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
let input = document.querySelector('[name=picker]');
expect(picker).not.toHaveAttribute('aria-describedby');
@@ -2160,7 +2160,7 @@ describe('Picker', function () {
expect(document.getElementById(picker.getAttribute('aria-describedby'))).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);
});
@@ -2200,7 +2200,7 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
let input = document.querySelector('[name=picker]');
expect(input).toHaveAttribute('required');
expect(picker).not.toHaveAttribute('aria-describedby');
@@ -2211,7 +2211,7 @@ describe('Picker', function () {
expect(picker).toHaveAttribute('aria-describedby');
expect(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'));
@@ -2233,13 +2233,13 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
let input = document.querySelector('[name=picker]');
expect(picker).toHaveAttribute('aria-describedby');
expect(document.getElementById(picker.getAttribute('aria-describedby'))).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');
});
@@ -2256,11 +2256,11 @@ describe('Picker', function () {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('picker')});
- let picker = selectTester.trigger;
+ let picker = selectTester.trigger();
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 efccdb90617..3a4526b3dc8 100644
--- a/packages/@adobe/react-spectrum/test/picker/TempUtilTest.test.js
+++ b/packages/@adobe/react-spectrum/test/picker/TempUtilTest.test.js
@@ -113,8 +113,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.trigger()).toHaveTextContent('Three');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('three');
});
@@ -139,8 +139,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.trigger()).toHaveTextContent('Cat');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('cat');
});
@@ -218,8 +218,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.trigger()).toHaveTextContent('Three');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('three');
});
@@ -244,8 +244,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.trigger()).toHaveTextContent('Cat');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange).toHaveBeenLastCalledWith('cat');
});
@@ -271,7 +271,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.listbox());
});
});
});
diff --git a/packages/@adobe/react-spectrum/test/radio/Radio.test.js b/packages/@adobe/react-spectrum/test/radio/Radio.test.js
index 56923ed40b4..d5a191402e3 100644
--- a/packages/@adobe/react-spectrum/test/radio/Radio.test.js
+++ b/packages/@adobe/react-spectrum/test/radio/Radio.test.js
@@ -971,21 +971,21 @@ describe('Radios', function () {
);
let direction = props.locale === 'ar-AE' ? 'rtl' : 'ltr';
let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup'), direction});
- let radios = radioGroupTester.radios;
+ let radios = radioGroupTester.radios();
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 0ca9c6d2438..946906a1eb7 100644
--- a/packages/@adobe/react-spectrum/test/table/TableTests.js
+++ b/packages/@adobe/react-spectrum/test/table/TableTests.js
@@ -2243,15 +2243,15 @@ export let tableTests = () => {
await tableTester.toggleRowSelection({row: 0});
- expect(tableTester.selectedRows).toHaveLength(1);
+ expect(tableTester.selectedRows()).toHaveLength(1);
expect(onSelectionChange).toHaveBeenCalledTimes(1);
await tableTester.toggleRowSelection({row: 1});
- expect(tableTester.selectedRows).toHaveLength(2);
+ expect(tableTester.selectedRows()).toHaveLength(2);
expect(onSelectionChange).toHaveBeenCalledTimes(2);
await user.keyboard('{Escape}');
- expect(tableTester.selectedRows).toHaveLength(2);
+ expect(tableTester.selectedRows()).toHaveLength(2);
expect(onSelectionChange).toHaveBeenCalledTimes(2);
});
@@ -2588,12 +2588,12 @@ export let tableTests = () => {
checkSelectAll(tree, 'unchecked');
- let rows = tableTester.rows;
+ let rows = tableTester.rows();
checkRowSelection(rows.slice(1), false);
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.selectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(tableTester.rows.length);
+ expect(tableTester.selectedRows()).toHaveLength(tableTester.rows().length);
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onSelectionChange.mock.calls[0][0]).toEqual('all');
checkRowSelection(rows.slice(1), true);
@@ -4564,7 +4564,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.columns();
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 fa5a9a177f7..8d477e6a661 100644
--- a/packages/@adobe/react-spectrum/test/tabs/Tabs.test.js
+++ b/packages/@adobe/react-spectrum/test/tabs/Tabs.test.js
@@ -79,11 +79,11 @@ describe('Tabs', function () {
let container = renderComponent();
let tabsTester = testUtilUser.createTester('Tabs', {root: container.getByRole('tablist')});
- let tablist = tabsTester.tablist;
+ let tablist = tabsTester.tablist();
expect(tablist).toBeTruthy();
expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.tabs();
expect(tabs.length).toBe(3);
for (let tab of tabs) {
@@ -91,15 +91,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.selectedTab());
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.activeTabpanel());
+ expect(tabsTester.tabpanels()).toHaveLength(1);
}
}
});
@@ -146,7 +146,7 @@ describe('Tabs', function () {
let onKeyDown = jest.fn();
let container = renderComponent({orientation: 'horizontal', providerProps: {locale: 'ar-AE'}});
let tabsTester = testUtilUser.createTester('Tabs', {root: container.getByRole('tablist'), interactionType: 'keyboard', direction: 'rtl'});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.tabs();
window.addEventListener('keydown', onKeyDown);
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
@@ -1334,23 +1334,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.tablist()).toHaveAttribute('aria-orientation', props.orientation);
+ let tabs = tabsTester.tabs();
await tabsTester.triggerTab({tab: tabs[0]});
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ expect(tabsTester.selectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 4, interactionType: 'keyboard'});
- expect(tabsTester.selectedTab).toBe(tabs[4]);
- let tab4 = tabsTester.findTab({tabIndexOrText: 3});
+ expect(tabsTester.selectedTab()).toBe(tabs[4]);
+ let tab4 = tabsTester.findTab({indexOrText: 3});
await tabsTester.triggerTab({tab: tab4, interactionType: 'keyboard'});
- expect(tabsTester.selectedTab).toBe(tabs[3]);
+ expect(tabsTester.selectedTab()).toBe(tabs[3]);
await tabsTester.triggerTab({tab: 'Tab 1', interactionType: 'mouse'});
- expect(tabsTester.selectedTab).toBe(tabs[0]);
+ expect(tabsTester.selectedTab()).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.selectedTab()).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 1bc3e78eb21..20231da76f1 100644
--- a/packages/@adobe/react-spectrum/test/tree/TreeView.test.tsx
+++ b/packages/@adobe/react-spectrum/test/tree/TreeView.test.tsx
@@ -367,7 +367,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.rows();
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)
@@ -433,10 +433,10 @@ describe('Tree', () => {
it('should not render checkboxes for selection with selectionStyle=highlight', async () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- expect(treeTester.tree).toHaveAttribute('aria-multiselectable', 'true');
- let rows = treeTester.rows;
+ expect(treeTester.tree()).toHaveAttribute('aria-multiselectable', 'true');
+ let rows = treeTester.rows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -450,8 +450,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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row2);
let row1 = rows[1];
await treeTester.toggleRowSelection({row: row1});
@@ -461,29 +461,29 @@ 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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row1);
});
it('should prevent Esc from clearing selection if escapeKeyBehavior is "none"', async () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
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.selectedRows()).toHaveLength(1);
let row2 = rows[2];
await treeTester.toggleRowSelection({row: row2});
expect(onSelectionChange).toHaveBeenCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set(['Projects', 'Projects-1']));
- expect(treeTester.selectedRows).toHaveLength(2);
+ expect(treeTester.selectedRows()).toHaveLength(2);
await user.keyboard('{Escape}');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
- expect(treeTester.selectedRows).toHaveLength(2);
+ expect(treeTester.selectedRows()).toHaveLength(2);
});
it('should render a chevron for an expandable row marked with hasChildItems', () => {
@@ -664,7 +664,7 @@ describe('Tree', () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
await treeTester.triggerRowAction({row: rows[0]});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenLastCalledWith('Photos');
@@ -673,7 +673,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);
@@ -704,9 +704,9 @@ describe('Tree', () => {
it('should perform selection for highlight mode with single selection', async () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid'), interactionType: type as 'keyboard' | 'mouse' | 'touch'});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -725,8 +725,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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row2);
let row1 = rows[1];
await treeTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
@@ -740,8 +740,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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row1);
await treeTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
expect(row1).toHaveAttribute('aria-selected', 'false');
@@ -754,15 +754,15 @@ 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.selectedRows()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid'), interactionType: type as 'keyboard' | 'mouse' | 'touch'});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -778,13 +778,13 @@ describe('Tree', () => {
// 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(['Photos', 'Projects-1']));
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(treeTester.selectedRows[1]).toBe(row2);
+ expect(treeTester.selectedRows()).toHaveLength(2);
+ expect(treeTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row2);
}
let row1 = rows[1];
@@ -796,15 +796,15 @@ describe('Tree', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
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.selectedRows()).toHaveLength(3);
+ expect(treeTester.selectedRows()[1]).toBe(row1);
+ expect(treeTester.selectedRows()[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.selectedRows()).toHaveLength(2);
+ expect(treeTester.selectedRows()[0]).toBe(row1);
+ expect(treeTester.selectedRows()[1]).toBe(row2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -816,22 +816,22 @@ describe('Tree', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(4);
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.selectedRows()).toHaveLength(2);
+ expect(treeTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row2);
}
});
it('should perform replace selection in highlight mode when not using modifier keys', async () => {
let {getByRole} = render();
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid'), interactionType: type as 'keyboard' | 'mouse' | 'touch'});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
- for (let row of treeTester.rows) {
+ for (let row of treeTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -850,8 +850,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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row2);
let row1 = rows[1];
await treeTester.toggleRowSelection({row: row1});
@@ -866,8 +866,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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row1);
// pressing without modifier keys won't deselect the row
await treeTester.toggleRowSelection({row: row1});
@@ -878,7 +878,7 @@ describe('Tree', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(treeTester.selectedRows).toHaveLength(1);
+ expect(treeTester.selectedRows()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(row1).toHaveAttribute('aria-selected', 'true');
@@ -887,16 +887,16 @@ describe('Tree', () => {
expect(row2).toHaveAttribute('data-selected', 'true');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
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.selectedRows()).toHaveLength(2);
+ expect(treeTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(treeTester.selectedRows()[0]).toBe(row2);
}
});
});
@@ -1111,7 +1111,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.rows();
expect(rows).toHaveLength(20);
await user.tab();
@@ -1136,7 +1136,7 @@ describe('Tree', () => {
expect(onExpandedChange).toHaveBeenCalledTimes(1);
// Note that the children of the parent row will still be in the "expanded" array
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.rows();
expect(rows).toHaveLength(9);
await treeTester.toggleRowExpansion({row: rows[0], interactionType: type as 'mouse' | 'keyboard'});
@@ -1149,7 +1149,7 @@ describe('Tree', () => {
expect(rows[0]).toHaveAttribute('data-has-child-items', 'true');
expect(onExpandedChange).toHaveBeenCalledTimes(2);
expect(new Set(onExpandedChange.mock.calls[1][0])).toEqual(new Set(['Projects', 'Project-2', 'Project-5', 'Reports', 'Reports-1', 'Reports-1A', 'Reports-1AB']));
- rows = treeTester.rows;
+ rows = treeTester.rows();
expect(rows).toHaveLength(20);
await user.keyboard('{ArrowDown}');
@@ -1179,7 +1179,7 @@ describe('Tree', () => {
expect(rows[0]).toHaveAttribute('data-has-child-items', 'true');
expect(onExpandedChange).toHaveBeenCalledTimes(3);
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.rows();
expect(rows).toHaveLength(17);
// Check behavior of onExpandedChange when a nested row is already closed and the parent is collapsed
@@ -1189,7 +1189,7 @@ describe('Tree', () => {
expect(document.activeElement).toBe(rows[0]);
expect(onExpandedChange).toHaveBeenCalledTimes(4);
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.rows();
expect(rows).toHaveLength(9);
// Check that the nested collapsed row is still closed when the parent is reexpanded
@@ -1197,7 +1197,7 @@ describe('Tree', () => {
expect(document.activeElement).toBe(rows[0]);
expect(onExpandedChange).toHaveBeenCalledTimes(5);
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.rows();
expect(rows).toHaveLength(17);
});
@@ -1435,12 +1435,12 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {user, root: getByRole('treegrid')});
- let tree = treeTester.tree;
+ let tree = treeTester.tree();
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.rows()[0];
expect(row).toHaveAttribute('aria-level', '1');
expect(row).not.toHaveAttribute('aria-posinset');
expect(row).not.toHaveAttribute('aria-setsize');
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 4fe8937ca34..5c74676d327 100644
--- a/packages/@react-aria/test-utils/package.json
+++ b/packages/@react-aria/test-utils/package.json
@@ -26,7 +26,7 @@
"@swc/helpers": "^0.5.0"
},
"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..f034af349de
--- /dev/null
+++ b/packages/@react-aria/test-utils/src/act.ts
@@ -0,0 +1,30 @@
+/*
+ * 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: any) => {
+ // only wrap in act if in test environment, breaks vite browser test if test utils are used otherwise
+ // @ts-ignore
+ if (typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined') {
+ return actImpl(fn);
+ }
+ return fn();
+}) as typeof actImpl;
diff --git a/packages/@react-aria/test-utils/src/checkboxgroup.ts b/packages/@react-aria/test-utils/src/checkboxgroup.ts
index 54e436a5cab..268a4364896 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,16 +54,16 @@ export class CheckboxGroupTester {
/**
* Returns a checkbox matching the specified index or text content.
*/
- findCheckbox(opts: {checkboxIndexOrText: number | string}): HTMLElement {
+ findCheckbox(opts: {indexOrText: number | string}): HTMLElement {
let {
- checkboxIndexOrText
+ 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.checkboxes()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ let label = within(this.checkboxgroup()).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) {
@@ -83,7 +84,7 @@ export class CheckboxGroupTester {
private async keyboardNavigateToCheckbox(opts: {checkbox: HTMLElement}) {
let {checkbox} = opts;
- let checkboxes = this.checkboxes;
+ let checkboxes = this.checkboxes();
checkboxes = checkboxes.filter(checkbox => !(checkbox.hasAttribute('disabled') || checkbox.getAttribute('aria-disabled') === 'true'));
if (checkboxes.length === 0) {
throw new Error('Checkbox group doesnt have any non-disabled checkboxes. Please double check your checkbox group.');
@@ -94,7 +95,7 @@ export class CheckboxGroupTester {
throw new Error('Checkbox provided is not in the checkbox group.');
}
- if (!this.checkboxgroup.contains(document.activeElement)) {
+ if (!this.checkboxgroup().contains(document.activeElement)) {
act(() => checkboxes[0].focus());
}
@@ -118,13 +119,13 @@ export class CheckboxGroupTester {
} = 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') {
@@ -138,21 +139,21 @@ export class CheckboxGroupTester {
/**
* Returns the checkboxgroup.
*/
- get checkboxgroup(): HTMLElement {
+ checkboxgroup(): HTMLElement {
return this._checkboxgroup;
}
/**
* Returns the checkboxes.
*/
- get checkboxes(): HTMLElement[] {
- return within(this.checkboxgroup).queryAllByRole('checkbox');
+ checkboxes(): HTMLElement[] {
+ return within(this.checkboxgroup()).queryAllByRole('checkbox');
}
/**
* Returns the currently selected checkboxes in the checkboxgroup if any.
*/
- get selectedCheckboxes(): HTMLElement[] {
- return this.checkboxes.filter(checkbox => (checkbox as HTMLInputElement).checked || checkbox.getAttribute('aria-checked') === 'true');
+ selectedCheckboxes(): HTMLElement[] {
+ return this.checkboxes().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 d95ac6f5711..3fa21ba17b1 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,7 +31,12 @@ interface ComboBoxSelectOpts extends ComboBoxOpenOpts {
/**
* The index, text, or node of the option to select. Option nodes can be sourced via `options()`.
*/
- option: number | string | HTMLElement
+ 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 {
@@ -81,8 +88,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.trigger();
+ let combobox = this.combobox();
let isDisabled = trigger!.hasAttribute('disabled');
if (interactionType === 'mouse') {
@@ -91,8 +98,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}');
}
@@ -124,67 +131,99 @@ export class ComboBoxTester {
/**
* Returns an option matching the specified index or text content.
*/
- findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
let {
- optionIndexOrText
+ indexOrText
} = opts;
let option;
let options = this.options();
- let listbox = this.listbox;
+ 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;
+ 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.combobox();
+ let options = this.options();
+ 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.
+ * 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.combobox().getAttribute('aria-controls')) {
await this.open({triggerBehavior});
}
- let listbox = this.listbox;
+ let listbox = this.listbox();
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;
+ }
+ });
}
}
@@ -192,9 +231,9 @@ export class ComboBoxTester {
* Closes the combobox dropdown.
*/
async close(): Promise {
- let listbox = this.listbox;
+ let listbox = this.listbox();
if (listbox) {
- act(() => this.combobox.focus());
+ act(() => this.combobox().focus());
await this.user.keyboard('[Escape]');
await waitFor(() => {
@@ -210,30 +249,30 @@ export class ComboBoxTester {
/**
* Returns the combobox.
*/
- get combobox(): HTMLElement {
+ combobox(): HTMLElement {
return this._combobox;
}
/**
* Returns the combobox trigger button.
*/
- get trigger(): HTMLElement {
+ trigger(): HTMLElement {
return this._trigger;
}
/**
* Returns the combobox's listbox if present.
*/
- get listbox(): HTMLElement | null {
- let listBoxId = this.combobox.getAttribute('aria-controls');
+ listbox(): HTMLElement | null {
+ let listBoxId = this.combobox().getAttribute('aria-controls');
return listBoxId ? document.getElementById(listBoxId) || null : null;
}
/**
* Returns the combobox's sections if present.
*/
- get sections(): HTMLElement[] {
- let listbox = this.listbox;
+ sections(): HTMLElement[] {
+ let listbox = this.listbox();
return listbox ? within(listbox).queryAllByRole('group') : [];
}
@@ -241,7 +280,7 @@ 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;
+ let {element = this.listbox()} = opts;
let options = [];
if (element) {
options = within(element).queryAllByRole('option');
@@ -253,8 +292,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');
+ focusedOption(): HTMLElement | null {
+ let focusedOptionId = this.combobox().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 213c86c2b1a..8e68bdd985c 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 {
/**
@@ -33,13 +34,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;
}
/**
@@ -56,7 +61,7 @@ export class DialogTester {
let {
interactionType = this._interactionType
} = opts;
- let trigger = this.trigger;
+ let trigger = this.trigger();
if (!trigger.hasAttribute('disabled')) {
if (interactionType === 'mouse') {
await this.user.click(trigger);
@@ -126,7 +131,7 @@ export class DialogTester {
/**
* Returns the dialog's trigger.
*/
- get trigger(): HTMLElement {
+ trigger(): HTMLElement {
if (!this._trigger) {
throw new Error('No trigger element found for dialog.');
}
@@ -137,7 +142,7 @@ export class DialogTester {
/**
* Returns the dialog if present.
*/
- get dialog(): HTMLElement | null {
+ dialog(): 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 d5d1c21e082..33dd74dfb45 100644
--- a/packages/@react-aria/test-utils/src/gridlist.ts
+++ b/packages/@react-aria/test-utils/src/gridlist.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 {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 +22,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,53 +52,73 @@ export class GridListTester {
/**
* Returns a row matching the specified index or text content.
*/
- findRow(opts: {rowIndexOrText: number | string}): HTMLElement {
+ findRow(opts: {indexOrText: number | string}): HTMLElement {
let {
- rowIndexOrText
+ 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.rows()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ row = (within(this.gridlist()!).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.rows();
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 (document.activeElement !== this._gridlist && !this._gridlist.contains(document.activeElement)) {
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}]` : ''}`);
} else if (this._gridlist.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);
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}]`);
@@ -111,19 +142,17 @@ 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')) {
- return;
+ if (rowCheckbox?.getAttribute('disabled') === '' || row?.getAttribute('aria-disabled') === 'true') {
+ 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
@@ -144,12 +173,8 @@ 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, pointerOpts: {pointerType: 'touch'}});
+ await triggerLongPress({element: cell, advanceTimer: this._advanceTimer!, pointerOpts: {pointerType: 'touch'}});
} else {
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
await this.user.keyboard(`[${metaKey}>]`);
@@ -162,8 +187,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.
*/
@@ -175,20 +198,20 @@ export class GridListTester {
} = 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 {
@@ -199,29 +222,29 @@ export class GridListTester {
/**
* Returns the gridlist.
*/
- get gridlist(): HTMLElement {
+ gridlist(): HTMLElement {
return this._gridlist;
}
/**
* Returns the gridlist's rows if any.
*/
- get rows(): HTMLElement[] {
- return within(this?.gridlist).queryAllByRole('row');
+ rows(): HTMLElement[] {
+ return within(this.gridlist()).queryAllByRole('row');
}
/**
* Returns the gridlist's selected rows if any.
*/
- get selectedRows(): HTMLElement[] {
- return this.rows.filter(row => row.getAttribute('aria-selected') === 'true');
+ selectedRows(): HTMLElement[] {
+ return this.rows().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;
+ let {element = this.gridlist()} = 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 cac8d9d78bc..f3102eaeb71 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 {
/**
@@ -55,13 +56,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;
+ }
+ }
}
/**
@@ -71,29 +80,26 @@ 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 {
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
let {
- optionIndexOrText
+ indexOrText
} = opts;
let option;
let options = this.options();
- if (typeof optionIndexOrText === 'number') {
- option = options[optionIndexOrText];
- } else if (typeof optionIndexOrText === 'string') {
- option = (within(this.listbox!).getByText(optionIndexOrText).closest('[role=option]'))! as HTMLElement;
+ if (typeof indexOrText === 'number') {
+ option = options[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ option = (within(this.listbox()!).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();
@@ -113,12 +119,32 @@ 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;
+ // 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 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}]`);
@@ -141,11 +167,11 @@ 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') {
@@ -163,11 +189,7 @@ 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, pointerOpts: {pointerType: 'touch'}});
+ await triggerLongPress({element: option, advanceTimer: this._advanceTimer!, pointerOpts: {pointerType: 'touch'}});
} else {
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
await this.user.keyboard(`[${metaKey}>]`);
@@ -191,11 +213,11 @@ export class ListBoxTester {
} = 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) {
@@ -215,7 +237,7 @@ export class ListBoxTester {
/**
* Returns the listbox.
*/
- get listbox(): HTMLElement {
+ listbox(): HTMLElement {
return this._listbox;
}
@@ -235,14 +257,14 @@ export class ListBoxTester {
/**
* Returns the listbox's selected options if any.
*/
- get selectedOptions(): HTMLElement[] {
+ selectedOptions(): HTMLElement[] {
return this.options().filter(row => row.getAttribute('aria-selected') === 'true');
}
/**
* Returns the listbox's sections if any.
*/
- get sections(): HTMLElement[] {
+ sections(): 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 87af5c11fd0..19e2b76fa8a 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 {
/**
@@ -76,13 +77,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;
@@ -96,8 +101,6 @@ 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.
*/
@@ -107,15 +110,12 @@ export class MenuTester {
interactionType = this._interactionType,
direction
} = opts;
- let trigger = this.trigger;
+ let trigger = this.trigger();
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, pointerOpts: {pointerType}});
+ await triggerLongPress({element: trigger, advanceTimer: this._advanceTimer!, pointerOpts: {pointerType}});
} else if (interactionType === 'mouse') {
await this.user.click(trigger);
} else {
@@ -156,31 +156,29 @@ export class MenuTester {
/**
* Returns a option matching the specified index or text content.
*/
- findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
let {
- optionIndexOrText
+ indexOrText
} = opts;
let option;
let options = this.options();
- let menu = this.menu;
+ let menu = this.menu();
- if (typeof optionIndexOrText === 'number') {
- option = options[optionIndexOrText];
- } else if (typeof optionIndexOrText === 'string' && menu != null) {
- option = (within(menu!).getByText(optionIndexOrText).closest('[role=menuitem], [role=menuitemradio], [role=menuitemcheckbox]'))! as HTMLElement;
+ if (typeof indexOrText === 'number') {
+ option = options[indexOrText];
+ } else if (typeof indexOrText === 'string' && menu != null) {
+ option = (within(menu!).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.
+ * 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,
@@ -189,13 +187,13 @@ export class MenuTester {
interactionType = this._interactionType,
keyboardActivation = 'Enter'
} = opts;
- let trigger = this.trigger;
+ let trigger = this.trigger();
if (!trigger.getAttribute('aria-controls') && !trigger.hasAttribute('aria-expanded')) {
await this.open({needsLongPress});
}
- let menu = this.menu;
+ let menu = this.menu();
if (!menu) {
throw new Error('Menu not found.');
@@ -203,11 +201,11 @@ 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') {
@@ -262,7 +260,7 @@ export class MenuTester {
// close. In React 16, focus actually makes it all the way to the root menu's submenu trigger so we need check the root menu
if (this._isSubmenu) {
await waitFor(() => {
- if (document.activeElement === this.trigger || this._rootMenu?.contains(document.activeElement)) {
+ if (document.activeElement === this.trigger() || this._rootMenu?.contains(document.activeElement)) {
throw new Error('Expected focus after selecting an submenu option to move away from the original submenu trigger.');
} else {
return true;
@@ -284,59 +282,58 @@ 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.trigger();
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 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();
- }
+ let menu = this.menu();
+ 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;
+ }
- 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.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();
+ }
- 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}) {
@@ -347,7 +344,7 @@ export class MenuTester {
if (targetIndex === -1) {
throw new Error('Option provided is not in the menu');
}
- if (document.activeElement === this.menu) {
+ if (document.activeElement === this.menu()) {
await this.user.keyboard('[ArrowDown]');
}
let currIndex = options.indexOf(document.activeElement as HTMLElement);
@@ -365,13 +362,13 @@ export class MenuTester {
* Closes the menu.
*/
async close(): Promise {
- let menu = this.menu;
+ let menu = this.menu();
if (menu) {
act(() => menu.focus());
await this.user.keyboard('[Escape]');
await waitFor(() => {
- if (document.activeElement !== this.trigger) {
+ if (document.activeElement !== this.trigger()) {
throw new Error(`Expected the document.activeElement after closing the menu to be the menu trigger but got ${document.activeElement}`);
} else {
return true;
@@ -387,7 +384,7 @@ export class MenuTester {
/**
* Returns the menu's trigger.
*/
- get trigger(): HTMLElement {
+ trigger(): HTMLElement {
if (!this._trigger) {
throw new Error('No trigger element found for menu.');
}
@@ -398,16 +395,16 @@ export class MenuTester {
/**
* Returns the menu if present.
*/
- get menu(): HTMLElement | null {
- let menuId = this.trigger.getAttribute('aria-controls');
+ menu(): HTMLElement | null {
+ let menuId = this.trigger().getAttribute('aria-controls');
return menuId ? document.getElementById(menuId) : null;
}
/**
* Returns the menu's sections if any.
*/
- get sections(): HTMLElement[] {
- let menu = this.menu;
+ sections(): HTMLElement[] {
+ let menu = this.menu();
if (menu) {
return within(menu).queryAllByRole('group');
} else {
@@ -419,25 +416,20 @@ 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');
- }
- }
+ let {element = this.menu()} = 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[] {
+ submenuTriggers(): HTMLElement[] {
let options = this.options();
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 6c1d0e38c9e..33c04f2b82e 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 {
/**
@@ -54,16 +55,16 @@ export class RadioGroupTester {
/**
* Returns a radio matching the specified index or text content.
*/
- findRadio(opts: {radioIndexOrText: number | string}): HTMLElement {
+ findRadio(opts: {indexOrText: number | string}): HTMLElement {
let {
- radioIndexOrText
+ 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.radios()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ let label = within(this.radiogroup()).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');
@@ -83,7 +84,7 @@ export class RadioGroupTester {
private async keyboardNavigateToRadio(opts: {radio: HTMLElement, orientation?: Orientation}) {
let {radio, orientation = 'vertical'} = opts;
- let radios = this.radios;
+ let radios = this.radios();
radios = radios.filter(radio => !(radio.hasAttribute('disabled') || radio.getAttribute('aria-disabled') === 'true'));
if (radios.length === 0) {
throw new Error('Radio group doesnt have any non-disabled radios. Please double check your radio group.');
@@ -94,8 +95,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.radiogroup().contains(document.activeElement)) {
+ let selectedRadio = this.selectedRadio();
if (selectedRadio != null) {
act(() => selectedRadio.focus());
} else {
@@ -136,13 +137,13 @@ export class RadioGroupTester {
} = 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 +157,21 @@ export class RadioGroupTester {
/**
* Returns the radiogroup.
*/
- get radiogroup(): HTMLElement {
+ radiogroup(): HTMLElement {
return this._radiogroup;
}
/**
* Returns the radios.
*/
- get radios(): HTMLElement[] {
- return within(this.radiogroup).queryAllByRole('radio');
+ radios(): HTMLElement[] {
+ return within(this.radiogroup()).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;
+ selectedRadio(): HTMLElement | null {
+ return this.radios().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 8399363b2bb..7f2cec20097 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 {
/**
@@ -68,7 +70,7 @@ export class SelectTester {
let {
interactionType = this._interactionType
} = opts;
- let trigger = this.trigger;
+ let trigger = this.trigger();
let isDisabled = trigger.hasAttribute('disabled');
if (interactionType === 'mouse') {
@@ -101,7 +103,7 @@ export class SelectTester {
* Closes the select.
*/
async close(): Promise {
- let listbox = this.listbox;
+ let listbox = this.listbox();
if (listbox) {
act(() => listbox.focus());
await this.user.keyboard('[Escape]');
@@ -123,19 +125,19 @@ export class SelectTester {
/**
* Returns a option matching the specified index or text content.
*/
- findOption(opts: {optionIndexOrText: number | string}): HTMLElement {
+ findOption(opts: {indexOrText: number | string}): HTMLElement {
let {
- optionIndexOrText
+ indexOrText
} = opts;
let option;
let options = this.options();
- let listbox = this.listbox;
+ 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;
+ 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,7 +150,7 @@ export class SelectTester {
if (targetIndex === -1) {
throw new Error('Option provided is not in the listbox');
}
- if (document.activeElement === this.listbox) {
+ if (document.activeElement === this.listbox()) {
await this.user.keyboard('[ArrowDown]');
}
let currIndex = options.indexOf(document.activeElement as HTMLElement);
@@ -163,68 +165,65 @@ 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.
+ * 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.trigger();
if (!trigger.getAttribute('aria-controls')) {
await this.open();
}
- let listbox = this.listbox;
+ let listbox = this.listbox();
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') {
+ return;
+ }
- 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)) {
- throw new Error('Expected select element listbox to not be in the document after selecting an option');
+ 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)) {
+ throw new Error('Expected select element listbox to not be in the document after selecting an option');
}
}
}
@@ -233,7 +232,7 @@ 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;
+ let {element = this.listbox()} = opts;
let options = [];
if (element) {
options = within(element).queryAllByRole('option');
@@ -245,23 +244,23 @@ export class SelectTester {
/**
* Returns the select's trigger.
*/
- get trigger(): HTMLElement {
+ trigger(): HTMLElement {
return this._trigger;
}
/**
* Returns the select's listbox if present.
*/
- get listbox(): HTMLElement | null {
- let listBoxId = this.trigger.getAttribute('aria-controls');
+ listbox(): HTMLElement | null {
+ let listBoxId = this.trigger().getAttribute('aria-controls');
return listBoxId ? document.getElementById(listBoxId) : null;
}
/**
* Returns the select's sections if present.
*/
- get sections(): HTMLElement[] {
- let listbox = this.listbox;
+ sections(): HTMLElement[] {
+ let listbox = this.listbox();
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 e5820e01dac..b61288d303e 100644
--- a/packages/@react-aria/test-utils/src/table.ts
+++ b/packages/@react-aria/test-utils/src/table.ts
@@ -10,9 +10,10 @@
* governing permissions and limitations under the License.
*/
-import {act, waitFor, within} from '@testing-library/react';
-import {BaseGridRowInteractionOpts, GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types';
-import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
+import {act} from './act';
+import {BaseGridRowInteractionOpts, Direction, GridRowActionOpts, TableTesterOpts, ToggleGridRowOpts, UserOpts} from './types';
+import {formatTargetNode, getAltKey, getMetaKey, pressElement, triggerLongPress} from './utils';
+import {waitFor, within} from '@testing-library/dom';
interface TableToggleRowOpts extends ToggleGridRowOpts {}
interface TableToggleExpansionOpts extends BaseGridRowInteractionOpts {}
@@ -38,14 +39,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;
+ }
+ }
}
/**
@@ -55,11 +65,10 @@ 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.rows();
let targetIndex = rows.indexOf(row);
if (targetIndex === -1) {
throw new Error('Row provided is not in the table');
@@ -74,17 +83,19 @@ export class TableTester {
await this.user.keyboard('[ArrowDown]');
}
+ let rowGroups = this.rowGroups();
// 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
- if (this.rowGroups[1].contains(document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
+ let focusPrevKey = this._direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
+ if (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);
@@ -116,19 +127,23 @@ 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, selectionOnNav: selectionBehavior === 'replace' ? 'none' : 'default'});
if (selectionBehavior === 'replace') {
@@ -145,12 +160,8 @@ 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, pointerOpts: {pointerType: 'touch'}});
+ await triggerLongPress({element: cell, advanceTimer: this._advanceTimer!, pointerOpts: {pointerType: 'touch'}});
} else {
if (selectionBehavior === 'replace' && interactionType !== 'touch') {
await this.user.keyboard(`[${metaKey}>]`);
@@ -171,38 +182,38 @@ export class TableTester {
row,
interactionType = this._interactionType
} = opts;
- if (!this.table.contains(document.activeElement)) {
+ if (!this.table().contains(document.activeElement)) {
await act(async () => {
- this.table.focus();
+ this.table().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}]`);
}
}
};
@@ -218,9 +229,9 @@ export class TableTester {
let columnheader;
if (typeof column === 'number') {
- columnheader = this.columns[column];
+ columnheader = this.columns()[column];
} else if (typeof column === 'string') {
- columnheader = within(this.rowGroups[0]).getByText(column);
+ columnheader = within(this.rowGroups()[0]).getByText(column);
while (columnheader && !/columnheader/.test(columnheader.getAttribute('role'))) {
columnheader = columnheader.parentElement;
}
@@ -277,12 +288,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(() => {
@@ -309,9 +316,9 @@ export class TableTester {
let columnheader;
if (typeof column === 'number') {
- columnheader = this.columns[column];
+ columnheader = this.columns()[column];
} else if (typeof column === 'string') {
- columnheader = within(this.rowGroups[0]).getByText(column);
+ columnheader = within(this.rowGroups()[0]).getByText(column);
while (columnheader && !/columnheader/.test(columnheader.getAttribute('role'))) {
columnheader = columnheader.parentElement;
}
@@ -363,12 +370,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(() => {
@@ -394,11 +397,15 @@ export class TableTester {
} = 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) {
@@ -411,9 +418,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
/**
@@ -423,11 +427,15 @@ export class TableTester {
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.table();
+ if (document.activeElement !== table && !table.contains(document.activeElement)) {
+ act(() => table.focus());
+ }
+ await this.user.keyboard(`[${metaKey}>]a[/${metaKey}]`);
} else {
+ let checkbox = within(this.table()).getByLabelText('Select All');
await pressElement(this.user, checkbox, interactionType);
}
}
@@ -435,18 +443,18 @@ export class TableTester {
/**
* Returns a row matching the specified index or text content.
*/
- findRow(opts: {rowIndexOrText: number | string}): HTMLElement {
+ findRow(opts: {indexOrText: number | string}): HTMLElement {
let {
- rowIndexOrText
+ 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.rows();
+ let bodyRowGroup = this.rowGroups()[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;
}
@@ -463,7 +471,7 @@ export class TableTester {
text
} = opts;
- let cell = within(this.table).getByText(text);
+ let cell = within(this.table()).getByText(text);
if (cell) {
while (cell && !/gridcell|rowheader|columnheader/.test(cell.getAttribute('role') || '')) {
if (cell.parentElement) {
@@ -480,14 +488,14 @@ export class TableTester {
/**
* Returns the table.
*/
- get table(): HTMLElement {
+ table(): HTMLElement {
return this._table;
}
/**
* Returns the row groups within the table.
*/
- get rowGroups(): HTMLElement[] {
+ rowGroups(): HTMLElement[] {
let table = this._table;
return table ? within(table).queryAllByRole('rowgroup') : [];
}
@@ -495,37 +503,45 @@ export class TableTester {
/**
* Returns the columns within the table.
*/
- get columns(): HTMLElement[] {
- let headerRowGroup = this.rowGroups[0];
+ columns(): HTMLElement[] {
+ let headerRowGroup = this.rowGroups()[0];
return headerRowGroup ? within(headerRowGroup).queryAllByRole('columnheader') : [];
}
/**
* Returns the rows within the table if any.
*/
- get rows(): HTMLElement[] {
- return this.rowGroups.slice(1).flatMap(rowGroup => within(rowGroup).queryAllByRole('row'));
+ rows(): HTMLElement[] {
+ return this.rowGroups().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');
+ selectedRows(): HTMLElement[] {
+ return this.rows().filter(row => row.getAttribute('aria-selected') === 'true');
+ }
+
+ /**
+ * Returns the footer rows within the table if any.
+ */
+ footerRows(): HTMLElement[] {
+ let footerRowGroup = this.rowGroups()[2];
+ return footerRowGroup ? within(footerRowGroup).queryAllByRole('row') : [];
}
/**
* Returns the row headers within the table if any.
*/
- get rowHeaders(): HTMLElement[] {
- return within(this.table).queryAllByRole('rowheader');
+ rowHeaders(): HTMLElement[] {
+ return within(this.table()).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;
+ let {element = this.table()} = 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 c26da3e7656..eb83c61cbef 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 {
/**
@@ -55,30 +56,28 @@ 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 {
+ findTab(opts: {indexOrText: number | string}): HTMLElement {
let {
- tabIndexOrText
+ 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.tabs();
+ 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.tabs();
tabs = tabs.filter(tab => !(tab.hasAttribute('disabled') || tab.getAttribute('aria-disabled') === 'true'));
if (tabs.length === 0) {
throw new Error('Tablist doesnt have any non-disabled tabs. Please double check your tabs implementation.');
@@ -90,7 +89,7 @@ export class TabsTester {
}
if (!this._tablist.contains(document.activeElement)) {
- let selectedTab = this.selectedTab;
+ let selectedTab = this.selectedTab();
if (selectedTab != null) {
act(() => selectedTab.focus());
} else {
@@ -132,13 +131,13 @@ export class TabsTester {
} = 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') {
@@ -159,16 +158,16 @@ export class TabsTester {
/**
* Returns the tablist.
*/
- get tablist(): HTMLElement {
+ tablist(): HTMLElement {
return this._tablist;
}
/**
* Returns the tabpanels.
*/
- get tabpanels(): HTMLElement[] {
+ tabpanels(): HTMLElement[] {
let tabpanels = [] as HTMLElement[];
- for (let tab of this.tabs) {
+ for (let tab of this.tabs()) {
let controlId = tab.getAttribute('aria-controls');
let panel = controlId != null ? document.getElementById(controlId) : null;
if (panel != null) {
@@ -182,22 +181,22 @@ export class TabsTester {
/**
* Returns the tabs in the tablist.
*/
- get tabs(): HTMLElement[] {
- return within(this.tablist).queryAllByRole('tab');
+ tabs(): HTMLElement[] {
+ return within(this.tablist()).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;
+ selectedTab(): HTMLElement | null {
+ return this.tabs().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');
+ activeTabpanel(): HTMLElement | null {
+ let activeTabpanelId = this.selectedTab()?.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 cadcf52b72a..6df52215274 100644
--- a/packages/@react-aria/test-utils/src/tree.ts
+++ b/packages/@react-aria/test-utils/src/tree.ts
@@ -10,31 +10,34 @@
* governing permissions and limitations under the License.
*/
-import {act, within} from '@testing-library/react';
-import {BaseGridRowInteractionOpts, GridRowActionOpts, ToggleGridRowOpts, TreeTesterOpts, UserOpts} from './types';
-import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
+import {act} from './act';
+import {BaseGridRowInteractionOpts, Direction, GridRowActionOpts, ToggleGridRowOpts, TreeTesterOpts, UserOpts} from './types';
+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;
+ }
}
}
@@ -48,40 +51,40 @@ export class TreeTester {
/**
* Returns a row matching the specified index or text content.
*/
- findRow(opts: {rowIndexOrText: number | string}): HTMLElement {
+ findRow(opts: {indexOrText: number | string}): HTMLElement {
let {
- rowIndexOrText
+ 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.rows()[indexOrText];
+ } else if (typeof indexOrText === 'string') {
+ row = (within(this.tree()!).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.rows();
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.tree()) {
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
} else if (this._tree.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);
@@ -118,19 +121,17 @@ 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')) {
- return;
+ if (rowCheckbox?.getAttribute('disabled') === '' || row?.getAttribute('aria-disabled') === 'true') {
+ 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
@@ -151,14 +152,9 @@ 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, pointerOpts: {pointerType: 'touch'}});
+ await triggerLongPress({element: cell, 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}>]`);
}
@@ -178,38 +174,35 @@ export class TreeTester {
row,
interactionType = this._interactionType
} = opts;
- if (!this.tree.contains(document.activeElement)) {
- await act(async () => {
- this.tree.focus();
- });
+ if (!this.tree().contains(document.activeElement)) {
+ act(() => this.tree().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}]`);
}
}
};
@@ -225,22 +218,20 @@ export class TreeTester {
} = 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 {
@@ -251,29 +242,29 @@ export class TreeTester {
/**
* Returns the tree.
*/
- get tree(): HTMLElement {
+ tree(): HTMLElement {
return this._tree;
}
/**
* Returns the tree's rows if any.
*/
- get rows(): HTMLElement[] {
- return within(this?.tree).queryAllByRole('row');
+ rows(): HTMLElement[] {
+ return within(this.tree()).queryAllByRole('row');
}
/**
* Returns the tree's selected rows if any.
*/
- get selectedRows(): HTMLElement[] {
- return this.rows.filter(row => row.getAttribute('aria-selected') === 'true');
+ selectedRows(): HTMLElement[] {
+ return this.rows().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;
+ let {element = this.tree()} = 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 5ecbc80b52e..348054f1cd9 100644
--- a/packages/@react-aria/test-utils/src/types.ts
+++ b/packages/@react-aria/test-utils/src/types.ts
@@ -36,7 +36,12 @@ export interface BaseTesterOpts extends UserOpts {
/** @private */
user?: any,
/** The base element for the given tester (e.g. the table, menu trigger button, etc). */
- root: HTMLElement
+ root: HTMLElement,
+ /**
+ * The horizontal layout direction, typically affected by locale.
+ * @default 'ltr'
+ */
+ direction?: Direction
}
export interface CheckboxGroupTesterOpts extends BaseTesterOpts {}
@@ -65,13 +70,20 @@ 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 {
@@ -89,13 +101,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 {
/**
@@ -105,27 +111,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 {
/**
@@ -148,12 +138,11 @@ 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 during user actions, useful when performing multi-row selection in a "selectionBehavior: '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).
+ * See the "Selection Behavior" section of the appropriate React Aria Component docs for more information (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 69%
rename from packages/@react-aria/test-utils/src/events.ts
rename to packages/@react-aria/test-utils/src/utils.ts
index e603111d3ea..df34de5eda4 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.outerHTML;
+ }
+ return String(value);
+}
+
/**
* Simulates a "long press" event on a element.
* @param opts - Options for the long press.
@@ -54,38 +62,52 @@ export function getMetaKey(): 'MetaLeft' | 'ControlLeft' {
* @param opts.pointeropts - Options to pass to the simulated event. Defaults to mouse. See https://testing-library.com/docs/dom-testing-library/api-events/#fireevent for more info.
*/
export async function triggerLongPress(opts: {element: HTMLElement, 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}]});
- shouldFocus = fireEvent.mouseDown(element, pointerOpts);
+ act(() => {
+ shouldFocus = fireEvent.touchEnd(element, {targetTouches: [{identifier: pointerOpts.pointerId, clientX: pointerOpts.clientX, clientY: pointerOpts.clientY}]});
+ 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
@@ -94,9 +116,6 @@ export async function pressElement(user: {click: (element: Element) => Promise 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..baa972110a5
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/CheckboxGroup.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 {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.checkboxes();
+ await tester.toggleCheckbox({checkbox: checkboxes[2]});
+ expect(tester.selectedCheckboxes()).toContain(checkboxes[2]);
+});
diff --git a/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx b/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx
index b42afec1e93..600522535f6 100644
--- a/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx
+++ b/packages/@react-spectrum/s2/test/CheckboxGroup.test.tsx
@@ -73,28 +73,28 @@ describe('CheckboxGroup', () => {
);
let checkboxGroupTester = testUtilUser.createTester('CheckboxGroup', {root: getByRole('group')});
- expect(checkboxGroupTester.checkboxgroup).toHaveAttribute('role');
- let checkboxes = checkboxGroupTester.checkboxes;
+ expect(checkboxGroupTester.checkboxgroup()).toHaveAttribute('role');
+ let checkboxes = checkboxGroupTester.checkboxes();
await checkboxGroupTester.toggleCheckbox({checkbox: checkboxes[0]});
expect(checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.selectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4, interactionType: 'keyboard'});
expect(checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.selectedCheckboxes()).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.selectedCheckboxes()).toHaveLength(3);
await checkboxGroupTester.toggleCheckbox({checkbox: 'Soccer', interactionType: 'keyboard'});
expect(checkboxes[0]).not.toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.selectedCheckboxes()).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.selectedCheckboxes()).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..42aae0a31ba
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/Combobox.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 {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.combobox()).toHaveValue('Dog');
+});
diff --git a/packages/@react-spectrum/s2/test/Combobox.test.tsx b/packages/@react-spectrum/s2/test/Combobox.test.tsx
index f925a5a137d..a3ce6886d70 100644
--- a/packages/@react-spectrum/s2/test/Combobox.test.tsx
+++ b/packages/@react-spectrum/s2/test/Combobox.test.tsx
@@ -66,15 +66,15 @@ describe('Combobox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
let options = comboboxTester.options();
expect(options).toHaveLength(1);
- expect(comboboxTester.listbox).toBeTruthy();
+ expect(comboboxTester.listbox()).toBeTruthy();
expect(options[0]).toHaveTextContent('No results');
- expect(within(comboboxTester.listbox!).getByTestId('loadMoreSentinel')).toBeInTheDocument();
+ expect(within(comboboxTester.listbox()!).getByTestId('loadMoreSentinel')).toBeInTheDocument();
});
it('should only call loadMore whenever intersection is detected', async () => {
@@ -95,7 +95,7 @@ describe('Combobox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
@@ -140,7 +140,7 @@ describe('Combobox', () => {
await comboboxTester.open();
expect(announce).toHaveBeenLastCalledWith('5 options available.');
- expect(within(comboboxTester.listbox!).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
+ expect(within(comboboxTester.listbox()!).getByRole('progressbar', {hidden: true})).toBeInTheDocument();
await user.keyboard('C');
expect(announce).toHaveBeenLastCalledWith('2 options available.');
@@ -202,7 +202,7 @@ describe('Combobox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.getByTestId('testcombobox')});
let buttons = tree.getAllByRole('button');
expect(buttons).toHaveLength(2);
- expect(buttons[1]).toBe(comboboxTester.trigger);
+ expect(buttons[1]).toBe(comboboxTester.trigger());
await user.click(buttons[0]);
@@ -241,22 +241,22 @@ describe('Combobox', () => {
let dialogTester = testUtilUser.createTester('Dialog', {root: tree.container, interactionType: 'mouse'});
await dialogTester.open();
- expect(dialogTester.dialog).toBeVisible();
+ expect(dialogTester.dialog()).toBeVisible();
act(() => {
jest.runAllTimers();
});
- let comboboxTester = testUtilUser.createTester('ComboBox', {root: dialogTester.dialog!, interactionType: 'mouse'});
+ let comboboxTester = testUtilUser.createTester('ComboBox', {root: dialogTester.dialog()!, interactionType: 'mouse'});
await comboboxTester.open();
- expect(comboboxTester.listbox).toBeVisible();
+ expect(comboboxTester.listbox()).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.listbox()).toBeNull());
await user.click(backdrop!);
- expect(dialogTester.dialog).toBeNull();
+ expect(dialogTester.dialog()).toBeNull();
});
});
diff --git a/packages/@react-spectrum/s2/test/EditableTableView.test.tsx b/packages/@react-spectrum/s2/test/EditableTableView.test.tsx
index 270bd1668b5..ff03c3de026 100644
--- a/packages/@react-spectrum/s2/test/EditableTableView.test.tsx
+++ b/packages/@react-spectrum/s2/test/EditableTableView.test.tsx
@@ -257,7 +257,7 @@ describe('TableView', () => {
let dialogTrigger = document.activeElement! as HTMLElement;
let dialogTester = testUtilUser.createTester('Dialog', {root: dialogTrigger, interactionType: 'keyboard', overlayType: 'modal'});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.dialog();
expect(dialog).toBeVisible();
let input = within(dialog!).getByRole('textbox');
@@ -270,7 +270,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}');
@@ -279,13 +279,13 @@ describe('TableView', () => {
dialogTrigger = document.activeElement! as HTMLElement;
dialogTester = testUtilUser.createTester('Dialog', {root: dialogTrigger, interactionType: 'keyboard', overlayType: 'modal'});
await dialogTester.open();
- dialog = dialogTester.dialog;
+ dialog = dialogTester.dialog();
// 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.trigger()).toHaveFocus();
+ await selectTester.toggleOptionSelection({option: 'Steven'});
act(() => {jest.runAllTimers();});
await user.tab();
await user.tab();
@@ -295,13 +295,13 @@ describe('TableView', () => {
act(() => {jest.runAllTimers();});
expect(dialog).not.toBeInTheDocument();
- expect(within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByText('Steven')).toBeInTheDocument();
+ expect(within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByText('Steven')).toBeInTheDocument();
await user.tab();
expect(getByRole('button', {name: 'After'})).toHaveFocus();
await user.tab({shift: true});
- expect(within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByRole('button', {name: 'Edit farmer'})).toHaveFocus();
+ expect(within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByRole('button', {name: 'Edit farmer'})).toHaveFocus();
});
it('should perform validation when editing text in a cell', async () => {
@@ -338,7 +338,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 () => {
@@ -366,7 +366,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
@@ -393,7 +393,7 @@ describe('TableView', () => {
act(() => {jest.runAllTimers();});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
});
@@ -419,7 +419,7 @@ describe('TableView', () => {
act(() => {jest.runAllTimers();});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
});
});
@@ -446,7 +446,7 @@ describe('TableView', () => {
act(() => {jest.advanceTimersByTime(5000);});
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();
@@ -479,7 +479,7 @@ describe('TableView', () => {
act(() => {jest.advanceTimersByTime(5000);});
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();
@@ -629,7 +629,7 @@ describe('TableView', () => {
let dialogTrigger = document.activeElement! as HTMLElement;
let dialogTester = testUtilUser.createTester('Dialog', {root: dialogTrigger, interactionType: 'keyboard', overlayType: 'modal'});
await dialogTester.open();
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.dialog();
expect(dialog).toBeVisible();
let input = within(dialog!).getByRole('textbox');
@@ -642,7 +642,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}');
@@ -651,13 +651,13 @@ describe('TableView', () => {
dialogTrigger = document.activeElement! as HTMLElement;
dialogTester = testUtilUser.createTester('Dialog', {root: dialogTrigger, interactionType: 'keyboard', overlayType: 'modal'});
await dialogTester.open();
- dialog = dialogTester.dialog;
+ dialog = dialogTester.dialog();
// 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.trigger()).toHaveFocus();
+ await selectTester.toggleOptionSelection({option: 'Steven'});
act(() => {jest.runAllTimers();});
await user.tab();
await user.tab();
@@ -667,13 +667,13 @@ describe('TableView', () => {
act(() => {jest.runAllTimers();});
expect(dialog).not.toBeInTheDocument();
- expect(within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByText('Steven')).toBeInTheDocument();
+ expect(within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByText('Steven')).toBeInTheDocument();
await user.tab();
expect(getByRole('button', {name: 'After'})).toHaveFocus();
await user.tab({shift: true});
- expect(within(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).getByRole('button', {name: 'Edit farmer'})).toHaveFocus();
+ expect(within(tableTester.findRow({indexOrText: 'Apples Crisp'})).getByRole('button', {name: 'Edit farmer'})).toHaveFocus();
});
it('should perform validation when editing text in a cell', async () => {
@@ -710,7 +710,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 () => {
@@ -738,7 +738,7 @@ describe('TableView', () => {
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
@@ -765,7 +765,7 @@ describe('TableView', () => {
act(() => {jest.runAllTimers();});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples'})).toBeInTheDocument();
expect(onCancel).toHaveBeenCalled();
});
});
@@ -791,7 +791,7 @@ describe('TableView', () => {
act(() => {jest.runAllTimers();});
expect(dialog).not.toBeInTheDocument();
- expect(tableTester.findRow({rowIndexOrText: 'Apples Crisp'})).toBeInTheDocument();
+ expect(tableTester.findRow({indexOrText: 'Apples Crisp'})).toBeInTheDocument();
});
});
@@ -818,7 +818,7 @@ describe('TableView', () => {
act(() => {jest.advanceTimersByTime(5000);});
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();
@@ -851,7 +851,7 @@ describe('TableView', () => {
act(() => {jest.advanceTimersByTime(5000);});
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..cc6bc1f208d
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/Menu.browser.test.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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/Picker.browser.test.tsx b/packages/@react-spectrum/s2/test/Picker.browser.test.tsx
new file mode 100644
index 00000000000..1ecb93eac85
--- /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.trigger()).toHaveTextContent('Dog');
+});
diff --git a/packages/@react-spectrum/s2/test/Picker.test.tsx b/packages/@react-spectrum/s2/test/Picker.test.tsx
index 5c62d97a82f..7605d066b79 100644
--- a/packages/@react-spectrum/s2/test/Picker.test.tsx
+++ b/packages/@react-spectrum/s2/test/Picker.test.tsx
@@ -68,7 +68,7 @@ describe('Picker', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: tree.container});
- expect(selectTester.listbox).toBeFalsy();
+ expect(selectTester.listbox()).toBeFalsy();
selectTester.setInteractionType('mouse');
await selectTester.open();
@@ -157,8 +157,8 @@ describe('Picker', () => {
let selectTester = testUtilUser.createTester('Select', {root: tree.container, 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
@@ -194,8 +194,8 @@ describe('Picker', () => {
let selectTester = testUtilUser.createTester('Select', {root: tree.container, 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('Picker\'s value should not have interactive children for accessibility.');
@@ -230,7 +230,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.trigger());
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..6b684dea170
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/RadioGroup.browser.test.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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.radios();
+ await tester.triggerRadio({radio: radios[1]});
+ expect(tester.selectedRadio()).toBe(radios[1]);
+});
diff --git a/packages/@react-spectrum/s2/test/RadioGroup.test.tsx b/packages/@react-spectrum/s2/test/RadioGroup.test.tsx
index 9a3dfbb8693..3b688114adb 100644
--- a/packages/@react-spectrum/s2/test/RadioGroup.test.tsx
+++ b/packages/@react-spectrum/s2/test/RadioGroup.test.tsx
@@ -45,22 +45,22 @@ describe('RadioGroup', () => {
);
let direction = props.locale === 'ar-AE' ? 'rtl' : 'ltr' as Direction;
let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup'), direction});
- expect(radioGroupTester.radiogroup).toHaveAttribute('aria-orientation', props.orientation);
- let radios = radioGroupTester.radios;
+ expect(radioGroupTester.radiogroup()).toHaveAttribute('aria-orientation', props.orientation);
+ let radios = radioGroupTester.radios();
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();
@@ -70,9 +70,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.selectedRadio()).toBe(radios[0]);
} else {
- expect(radioGroupTester.selectedRadio).toBe(radios[3]);
+ expect(radioGroupTester.selectedRadio()).toBe(radios[3]);
}
});
});
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..8452c40b675
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/TableView.browser.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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.rows()[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 a14c2267f25..e4903145460 100644
--- a/packages/@react-spectrum/s2/test/TableView.test.tsx
+++ b/packages/@react-spectrum/s2/test/TableView.test.tsx
@@ -238,16 +238,16 @@ describe('TableView', () => {
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
+ let groups = tableTester.rowGroups();
expect(groups).toHaveLength(3);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.rows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
@@ -256,7 +256,7 @@ describe('TableView', () => {
await user.click(footerRows[0]);
expect(onSelectionChange).not.toHaveBeenCalled();
- await user.click(tableTester.rows[0]);
+ await user.click(tableTester.rows()[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..8b57ed38a2d
--- /dev/null
+++ b/packages/@react-spectrum/s2/test/TreeView.browser.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 094d707072c..e879f589885 100644
--- a/packages/@react-spectrum/test-utils/package.json
+++ b/packages/@react-spectrum/test-utils/package.json
@@ -29,7 +29,7 @@
"@swc/helpers": "^0.5.0"
},
"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..c37a7a86775 100644
--- a/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/CheckboxGroup/testing.mdx
@@ -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.selectedCheckboxes()).toHaveLength(0);
await checkboxGroupTester.toggleCheckbox({checkbox: 0});
- expect(checkboxGroupTester.checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.checkboxes()[0]).toBeChecked();
+ expect(checkboxGroupTester.selectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4});
- expect(checkboxGroupTester.checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.checkboxes()[4]).toBeChecked();
+ expect(checkboxGroupTester.selectedCheckboxes()).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..69ce7b771dd 100644
--- a/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/ComboBox/testing.mdx
@@ -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.listbox()).toBeInTheDocument();
let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.combobox().value).toBe('One');
+ expect(comboboxTester.listbox()).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..eb52db3db08 100644
--- a/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/GridList/testing.mdx
@@ -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.rows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.selectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).toHaveLength(0);
});
```
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..d8c7808ce2c 100644
--- a/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Menu/testing.mdx
@@ -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.menu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.submenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.menu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.options()[0]});
+ expect(submenuTester.menu()).not.toBeInTheDocument();
+ expect(menuTester.menu()).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..ab0ed857995
--- /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/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.
+
+
+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.dialog();
+ 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..07c5c9ccdb0
--- /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/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.
+
+
+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.dialog();
+ 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..9a24afffb5d 100644
--- a/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/RadioGroup/testing.mdx
@@ -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.radios();
+ expect(radioGroupTester.selectedRadio()).toBeFalsy();
await radioGroupTester.triggerRadio({radio: radios[0]});
- expect(radioGroupTester.selectedRadio).toBe(radios[0]);
+ expect(radioGroupTester.selectedRadio()).toBe(radios[0]);
await radioGroupTester.triggerRadio({radio: radios[1]});
- expect(radioGroupTester.selectedRadio).toBe(radios[1]);
+ expect(radioGroupTester.selectedRadio()).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..22c3dd8d13a 100644
--- a/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Select/testing.mdx
@@ -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.trigger();
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..27134d2a2dc 100644
--- a/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Table/testing.mdx
@@ -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.selectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.selectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.rows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.selectedRows()).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..4e42e640982 100644
--- a/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Tabs/testing.mdx
@@ -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.tabs();
+ expect(tabsTester.selectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.selectedTab()).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..fe522e43e08 100644
--- a/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/Tree/testing.mdx
@@ -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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.selectedRows()).toHaveLength(2);
+ expect(within(treeTester.rows()[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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[0]).getByRole('checkbox')).not.toBeChecked();
await treeTester.toggleRowExpansion({index: 0});
- expect(treeTester.rows[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(treeTester.rows()[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..39c67238b08 100644
--- a/packages/dev/s2-docs/pages/react-aria/testing.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/testing.mdx
@@ -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..f51c8ff79ce 100644
--- a/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/CheckboxGroup/testing.mdx
@@ -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.selectedCheckboxes()).toHaveLength(0);
await checkboxGroupTester.toggleCheckbox({checkbox: 0});
- expect(checkboxGroupTester.checkboxes[0]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(1);
+ expect(checkboxGroupTester.checkboxes()[0]).toBeChecked();
+ expect(checkboxGroupTester.selectedCheckboxes()).toHaveLength(1);
await checkboxGroupTester.toggleCheckbox({checkbox: 4});
- expect(checkboxGroupTester.checkboxes[4]).toBeChecked();
- expect(checkboxGroupTester.selectedCheckboxes).toHaveLength(2);
+ expect(checkboxGroupTester.checkboxes()[4]).toBeChecked();
+ expect(checkboxGroupTester.selectedCheckboxes()).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..169ac21bb31 100644
--- a/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/ComboBox/testing.mdx
@@ -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.listbox()).toBeInTheDocument();
let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.combobox().value).toBe('One');
+ expect(comboboxTester.listbox()).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..0937be15176 100644
--- a/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Dialog/testing.mdx
@@ -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.dialog();
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..bd17ca71bc7 100644
--- a/packages/dev/s2-docs/pages/s2/ListView/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/ListView/testing.mdx
@@ -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.rows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.selectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).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..267565de0dd 100644
--- a/packages/dev/s2-docs/pages/s2/Menu/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Menu/testing.mdx
@@ -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.menu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.submenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.menu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.options()[0]});
+ expect(submenuTester.menu()).not.toBeInTheDocument();
+ expect(menuTester.menu()).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..3724fd49e3f 100644
--- a/packages/dev/s2-docs/pages/s2/Picker/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Picker/testing.mdx
@@ -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.trigger();
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..9967cd8b74f 100644
--- a/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/RadioGroup/testing.mdx
@@ -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.radios();
+ expect(radioGroupTester.selectedRadio()).toBeFalsy();
await radioGroupTester.triggerRadio({radio: radios[0]});
- expect(radioGroupTester.selectedRadio).toBe(radios[0]);
+ expect(radioGroupTester.selectedRadio()).toBe(radios[0]);
await radioGroupTester.triggerRadio({radio: radios[1]});
- expect(radioGroupTester.selectedRadio).toBe(radios[1]);
+ expect(radioGroupTester.selectedRadio()).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..069779e7c8c 100644
--- a/packages/dev/s2-docs/pages/s2/TableView/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/TableView/testing.mdx
@@ -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.selectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.selectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.rows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.selectedRows()).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..5a866e7c206 100644
--- a/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/Tabs/testing.mdx
@@ -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.tabs();
+ expect(tabsTester.selectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.selectedTab()).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..ac999c52d94 100644
--- a/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx
+++ b/packages/dev/s2-docs/pages/s2/TreeView/testing.mdx
@@ -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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.selectedRows()).toHaveLength(2);
+ expect(within(treeTester.rows()[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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[0]).getByRole('checkbox')).not.toBeChecked();
await treeTester.toggleRowExpansion({index: 0});
- expect(treeTester.rows[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(treeTester.rows()[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 ac572c307a2..fb97922f5e0 100644
--- a/packages/dev/s2-docs/scripts/generateAgentSkills.mjs
+++ b/packages/dev/s2-docs/scripts/generateAgentSkills.mjs
@@ -83,6 +83,10 @@ 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: [
@@ -97,6 +101,14 @@ 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'
+ )
+ ]
}
};
@@ -384,7 +396,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..37cc79b8484
--- /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.selectedRows()).toHaveLength(0);
+
+ await tableTester.toggleSelectAll();
+ expect(tableTester.selectedRows()).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. `rows()`, `options()`) 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.dialog()` 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 | `checkboxGroup()`, `checkboxes()`, `selectedCheckboxes()`, `toggleCheckbox({checkbox})` |
+| `'ComboBox'` | ComboBox | `combobox()`, `listbox()`, `options()`, `open()`, `toggleOptionSelection({option})` |
+| `'Dialog'` | Modal, Popover | `trigger()`, `dialog()`, `open()`, `close()` — pass `overlayType: 'modal'` or `'popover'` to `createTester` |
+| `'GridList'` | GridList | `gridlist()`, `rows()`, `selectedRows()`, `toggleRowSelection({row})`, `triggerRowAction({row})` |
+| `'ListBox'` | ListBox | `listbox()`, `options()`, `selectedOptions()`, `toggleOptionSelection({option})`, `triggerOptionAction({option})` |
+| `'Menu'` | Menu | `trigger()`, `menu()`, `options()`, `open()`, `toggleOptionSelection({option})`, `openSubmenu({submenuTrigger})`, `close()` |
+| `'RadioGroup'` | RadioGroup | `radiogroup()`, `radios()`, `selectedRadio()`, `triggerRadio({radio})` |
+| `'Select'` | Select | `trigger()`, `listbox()`, `options()`, `toggleOptionSelection({option})` |
+| `'Table'` | Table | `table()`, `rows()`, `footerRows()`, `columns()`, `selectedRows()`, `toggleRowSelection({row})`, `toggleSort({column})`, `triggerRowAction({row})` |
+| `'Tabs'` | Tabs | `tablist()`, `tabs()`, `tabpanels()`, `selectedTab()`, `triggerTab({tab})` |
+| `'Tree'` | Tree | `tree()`, `rows()`, `selectedRows()`, `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..1be6cc86411
--- /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.selectedRows()).toHaveLength(0);
+
+ await tableTester.toggleSelectAll();
+ expect(tableTester.selectedRows()).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. `rows()`, `options()`) 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.dialog()` 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 | `checkboxGroup()`, `checkboxes()`, `selectedCheckboxes()`, `toggleCheckbox({checkbox})` |
+| `'ComboBox'` | ComboBox | `combobox()`, `listbox()`, `options()`, `open()`, `toggleOptionSelection({option})` |
+| `'Dialog'` | Dialog | `trigger()`, `dialog()`, `open()`, `close()` — pass `overlayType: 'modal'` or `'popover'` to `createTester` |
+| `'GridList'` | ListView | `gridlist()`, `rows()`, `selectedRows()`, `toggleRowSelection({row})`, `triggerRowAction({row})` |
+| `'Menu'` | Menu | `trigger()`, `menu()`, `options()`, `open()`, `toggleOptionSelection({option})`, `openSubmenu({submenuTrigger})`, `close()` |
+| `'RadioGroup'` | RadioGroup | `radiogroup()`, `radios()`, `selectedRadio()`, `triggerRadio({radio})` |
+| `'Select'` | Picker | `trigger()`, `listbox()`, `options()`, `toggleOptionSelection({option})` |
+| `'Table'` | TableView | `table()`, `rows()`, `footerRows()`, `columns()`, `selectedRows()`, `toggleRowSelection({row})`, `toggleSort({column})`, `triggerRowAction({row})` |
+| `'Tabs'` | Tabs | `tablist()`, `tabs()`, `tabpanels()`, `selectedTab()`, `triggerTab({tab})` |
+| `'Tree'` | TreeView | `tree()`, `rows()`, `selectedRows()`, `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.dialog()).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..cc1bdda1654 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.listbox()).toBeInTheDocument();
let options = comboboxTester.options();
- await comboboxTester.selectOption({option: options[0]});
- expect(comboboxTester.combobox.value).toBe('One');
- expect(comboboxTester.listbox).not.toBeInTheDocument();
+ await comboboxTester.toggleOptionSelection({option: options[0]});
+ expect(comboboxTester.combobox().value).toBe('One');
+ expect(comboboxTester.listbox()).not.toBeInTheDocument();
});
```
diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx
index 904d5aa7806..075023ebe67 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.rows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).toHaveLength(0);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.selectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 0});
expect(within(row).getByRole('checkbox')).not.toBeChecked();
- expect(gridListTester.selectedRows).toHaveLength(0);
+ expect(gridListTester.selectedRows()).toHaveLength(0);
});
```
diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx
index a9d8f923f5d..f588e7d940e 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.menu()).toBeInTheDocument();
+ let submenuTriggers = menuTester.submenuTriggers();
expect(submenuTriggers).toHaveLength(1);
let submenuTester = await menuTester.openSubmenu({submenuTrigger: 'Share…'});
- expect(submenuTester.menu).toBeInTheDocument();
+ expect(submenuTester.menu()).toBeInTheDocument();
- await submenuTester.selectOption({option: submenuTester.options()[0]});
- expect(submenuTester.menu).not.toBeInTheDocument();
- expect(menuTester.menu).not.toBeInTheDocument();
+ await submenuTester.toggleOptionSelection({option: submenuTester.options()[0]});
+ expect(submenuTester.menu()).not.toBeInTheDocument();
+ expect(menuTester.menu()).not.toBeInTheDocument();
});
```
diff --git a/packages/react-aria-components/docs/Select.mdx b/packages/react-aria-components/docs/Select.mdx
index 1ef1318eef6..d1eb5b7012c 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.trigger();
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..4acbd1d7371 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.selectedRows()).toHaveLength(0);
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
await tableTester.toggleRowSelection({row: 2});
- expect(tableTester.selectedRows).toHaveLength(9);
- let checkbox = within(tableTester.rows[2]).getByRole('checkbox');
+ expect(tableTester.selectedRows()).toHaveLength(9);
+ let checkbox = within(tableTester.rows()[2]).getByRole('checkbox');
expect(checkbox).not.toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(10);
+ expect(tableTester.selectedRows()).toHaveLength(10);
expect(checkbox).toBeChecked();
await tableTester.toggleSelectAll();
- expect(tableTester.selectedRows).toHaveLength(0);
+ expect(tableTester.selectedRows()).toHaveLength(0);
});
```
diff --git a/packages/react-aria-components/docs/Tabs.mdx b/packages/react-aria-components/docs/Tabs.mdx
index 32226da769e..12d1a4f1765 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.tabs();
+ expect(tabsTester.selectedTab()).toBe(tabs[0]);
await tabsTester.triggerTab({tab: 1});
- expect(tabsTester.selectedTab).toBe(tabs[1]);
+ expect(tabsTester.selectedTab()).toBe(tabs[1]);
});
```
diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx
index 8f6668bcab3..a8d12f9dde3 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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[0]).getByRole('checkbox')).toBeChecked();
await treeTester.toggleRowSelection({row: 1});
- expect(treeTester.selectedRows).toHaveLength(2);
- expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked();
+ expect(treeTester.selectedRows()).toHaveLength(2);
+ expect(within(treeTester.rows()[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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[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 087821d8941..c36fa1a9007 100644
--- a/packages/react-aria-components/test/AriaMenu.test-util.tsx
+++ b/packages/react-aria-components/test/AriaMenu.test-util.tsx
@@ -57,7 +57,9 @@ interface AriaMenuTestProps extends AriaBaseTestProps {
// Menu should only open on long press
longPress?: (props?: {name: string}) => ReturnType,
// Menu must have two levels of submenus
- submenus?: (props?: {name: string}) => ReturnType
+ 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 => {
@@ -98,7 +100,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.trigger()!;
expect(triggerButton).toBeTruthy();
expect(triggerButton).toHaveAttribute('aria-haspopup', 'true');
@@ -106,7 +108,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let buttonText = within(triggerButton).getByText(triggerText);
expect(buttonText).toBeTruthy();
- expect(menuTester.menu).toBeFalsy();
+ expect(menuTester.menu()).toBeFalsy();
expect(triggerButton).toHaveAttribute('aria-expanded', 'false');
expect(triggerButton).toHaveAttribute('type', 'button');
@@ -115,12 +117,12 @@ 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.trigger()!;
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu!;
+ let menu = menuTester.menu()!;
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
expect(menu).toHaveFocus();
@@ -137,7 +139,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.trigger()!;
fireEvent.mouseEnter(triggerButton);
fireEvent.mouseDown(triggerButton, {detail: 1});
@@ -152,12 +154,12 @@ 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.trigger()!;
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu!;
+ let menu = menuTester.menu()!;
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
expect(menu).toHaveFocus();
@@ -180,12 +182,12 @@ 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.trigger();
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeTruthy();
let options = menuTester.options();
expect(options[0]).toHaveFocus();
@@ -201,12 +203,12 @@ 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.trigger();
await menuTester.open({direction: 'down'});
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeTruthy();
let options = menuTester.options();
expect(options[0]).toHaveFocus();
@@ -222,12 +224,12 @@ 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.trigger();
await menuTester.open({direction: 'up'});
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeTruthy();
let options = menuTester.options();
expect(options[options.length - 1]).toHaveFocus();
@@ -243,12 +245,12 @@ 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.trigger()!;
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
@@ -273,7 +275,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
await user.keyboard('[Space]');
act(() => {jest.runAllTimers();});
@@ -281,7 +283,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- menu = menuTester.menu;
+ menu = menuTester.menu();
await user.keyboard('[Enter]');
act(() => {jest.runAllTimers();});
@@ -296,7 +298,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
let buttons = tree.getAllByLabelText('Dismiss');
expect(buttons.length).toBe(2);
@@ -313,18 +315,18 @@ 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.trigger()!;
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
let options = menuTester.options();
- await menuTester.selectOption({option: options[1], menuSelectionMode: 'single'});
+ await menuTester.toggleOptionSelection({option: options[1], menuSelectionMode: 'single'});
act(() => {jest.runAllTimers();});
expect(menu).not.toBeInTheDocument();
@@ -340,19 +342,19 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
it('selects an option via keyboard and autoFocuses it next time the menu is opened via keyboard, does not clear if menu is closed with Esc', async function () {
let tree = (renderers.singleSelection!)();
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container, interactionType: 'keyboard'});
- let triggerButton = menuTester.trigger!;
+ let triggerButton = menuTester.trigger()!;
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu!;
+ let menu = menuTester.menu()!;
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
let options = menuTester.options();
expect(options[0]).toHaveFocus();
- await menuTester.selectOption({option: options[1], menuSelectionMode: 'single'});
+ await menuTester.toggleOptionSelection({option: options[1], menuSelectionMode: 'single'});
act(() => {jest.runAllTimers();});
expect(menu).not.toBeInTheDocument();
@@ -380,12 +382,12 @@ 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.trigger();
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeTruthy();
expect(menu).toHaveAttribute('aria-labelledby', triggerButton.id);
@@ -411,7 +413,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
act(() => {jest.runAllTimers();});
fireEvent.keyDown(document.activeElement!, {key: 'Enter', repeat: true});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeInTheDocument();
await user.keyboard('{/Enter}');
expect(menuTester.options().filter(option => option.getAttribute('aria-checked') === 'true').length).toBe(0);
@@ -428,11 +430,11 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
let options = menuTester.options();
- 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');
@@ -447,7 +449,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- menu = menuTester.menu;
+ menu = menuTester.menu();
options = menuTester.options();
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[2]).toHaveAttribute('aria-checked', 'true');
@@ -462,7 +464,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
let options = menuTester.options();
expect(options[0]).toHaveFocus();
@@ -484,7 +486,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- menu = menuTester.menu;
+ menu = menuTester.menu();
options = menuTester.options();
expect(options[1]).toHaveAttribute('aria-checked', 'true');
expect(options[2]).toHaveAttribute('aria-checked', 'true');
@@ -499,7 +501,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
let options = menuTester.options();
expect(options[0]).toHaveFocus();
@@ -512,7 +514,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
await menuTester.open();
act(() => {jest.runAllTimers();});
- menu = menuTester.menu;
+ menu = menuTester.menu();
options = menuTester.options();
expect(options[0]).toHaveAttribute('aria-checked', 'false');
expect(options[1]).toHaveAttribute('aria-checked', 'true');
@@ -529,7 +531,7 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
act(() => {jest.runAllTimers();});
fireEvent.keyDown(document.activeElement!, {key: 'Enter', repeat: true});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
expect(menu).toBeInTheDocument();
await user.keyboard('{/Enter}');
expect(menuTester.options().filter(option => option.getAttribute('aria-checked') === 'true').length).toBe(0);
@@ -542,7 +544,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.trigger();
await menuTester.open();
act(() => {jest.runAllTimers();});
@@ -587,21 +589,21 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container, interactionType});
await menuTester.open();
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
- let submenuTrigger = menuTester.submenuTriggers[0]!;
+ let submenuTrigger = menuTester.submenuTriggers()[0]!;
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'false');
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
act(() => {jest.runAllTimers();});
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.menu();
expect(submenu).toBeInTheDocument();
- await submenuUtil.selectOption({option: submenuUtil.options().filter(item => item.getAttribute('aria-haspopup') == null)[0]});
+ await submenuUtil.toggleOptionSelection({option: submenuUtil.options().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.trigger());
});
it('should support nested submenu triggers', async () => {
@@ -609,30 +611,41 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container, interactionType});
await menuTester.open();
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
- let submenuTrigger = menuTester.submenuTriggers[0];
+ let submenuTrigger = menuTester.submenuTriggers()[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.menu();
expect(submenu).toBeInTheDocument();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.submenuTriggers()[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.menu();
expect(nestedSubmenu).toBeInTheDocument();
- await nestedSubmenuUtil.selectOption({option: nestedSubmenuUtil.options().filter(item => item.getAttribute('aria-haspopup') == null)[0]});
+ await nestedSubmenuUtil.toggleOptionSelection({option: nestedSubmenuUtil.options().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.trigger());
});
+
+ 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.menu()).toBeInTheDocument();
+ expect(menuTester.submenuTriggers()[0]).toHaveAttribute('aria-disabled');
+ expect(tree.getAllByRole('menu', {hidden: true})).toHaveLength(1);
+ });
+ }
});
describe('submenu specific interaction tests', function () {
@@ -641,24 +654,24 @@ 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.menu();
- let submenuTrigger = menuTester.submenuTriggers[0];
+ let submenuTrigger = menuTester.submenuTriggers()[0];
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'false');
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
act(() => {jest.runAllTimers();});
expect(submenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.menu();
expect(submenu).toBeInTheDocument();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.submenuTriggers()[0];
expect(nestedSubmenuTrigger).toHaveAttribute('aria-expanded', 'false');
let nestedSubmenuUtil = (await submenuUtil.openSubmenu({submenuTrigger: nestedSubmenuTrigger}))!;
act(() => {jest.runAllTimers();});
expect(nestedSubmenuTrigger).toHaveAttribute('aria-expanded', 'true');
- let nestedSubmenu = nestedSubmenuUtil.menu;
+ let nestedSubmenu = nestedSubmenuUtil.menu();
expect(submenu).toBeInTheDocument();
await user.click(document.body);
@@ -673,17 +686,17 @@ export const AriaMenuTests = ({renderers, setup, prefix}: AriaMenuTestProps): vo
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container, interactionType: 'keyboard'});
await menuTester.open();
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
- let submenuTrigger = menuTester.submenuTriggers[0];
+ let submenuTrigger = menuTester.submenuTriggers()[0];
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
act(() => {jest.runAllTimers();});
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.menu();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.submenuTriggers()[0];
let nestedSubmenuUtil = (await submenuUtil.openSubmenu({submenuTrigger: nestedSubmenuTrigger}))!;
act(() => {jest.runAllTimers();});
- let nestedSubmenu = nestedSubmenuUtil.menu;
+ let nestedSubmenu = nestedSubmenuUtil.menu();
await user.keyboard('[Escape]');
act(() => {jest.runAllTimers();});
@@ -698,16 +711,16 @@ 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.menu();
+ let submenuTrigger = menuTester.submenuTriggers()[0];
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger}))!;
act(() => {jest.runAllTimers();});
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.menu();
expect(submenu).toBeInTheDocument();
- let nestedSubmenuTrigger = submenuUtil.submenuTriggers[0];
+ let nestedSubmenuTrigger = submenuUtil.submenuTriggers()[0];
let nestedSubmenuUtil = (await submenuUtil.openSubmenu({submenuTrigger: nestedSubmenuTrigger}))!;
act(() => {jest.runAllTimers();});
- let nestedSubmenu = nestedSubmenuUtil.menu;
+ let nestedSubmenu = nestedSubmenuUtil.menu();
expect(submenu).toBeInTheDocument();
await user.hover(menuTester.options()[0]);
act(() => {jest.runAllTimers();});
@@ -720,10 +733,10 @@ 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.submenuTriggers()[0]);
act(() => {jest.runAllTimers();});
- expect(menuTester.submenuTriggers[0]).toHaveAttribute('aria-expanded', 'true');
- expect(document.activeElement).toBe(menuTester.submenuTriggers[0]);
+ expect(menuTester.submenuTriggers()[0]).toHaveAttribute('aria-expanded', 'true');
+ expect(document.activeElement).toBe(menuTester.submenuTriggers()[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 f695141b107..a08f9d41005 100644
--- a/packages/react-aria-components/test/AriaTree.test-util.tsx
+++ b/packages/react-aria-components/test/AriaTree.test-util.tsx
@@ -75,16 +75,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.tree();
expect(tree).toHaveAttribute('aria-label');
- for (let row of treeTester.rows) {
+ for (let row of treeTester.rows()) {
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.rows()[0]).not.toHaveAttribute('aria-expanded');
+ expect(treeTester.rows()[1]).toHaveAttribute('aria-expanded', 'false');
});
describeInteractions('interaction', function ({interactionType}) {
@@ -94,7 +94,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.rows();
let rowNoChild = rows[0];
expect(rowNoChild).toHaveAttribute('aria-label');
expect(rowNoChild).not.toHaveAttribute('aria-expanded');
@@ -149,7 +149,7 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
let tree = (renderers.singleSelection!)();
let treeTester = testUtilUser.createTester('Tree', {user, root: tree.container, interactionType});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
expect(rows[0]).toHaveAttribute('aria-selected', 'false');
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
// disabled rows should not be selectable
@@ -159,23 +159,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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[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.selectedRows()).toHaveLength(1);
+ expect(within(treeTester.rows()[0]).getByRole('checkbox')).not.toBeChecked();
+ expect(within(treeTester.rows()[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.rows();
// row 2 is now the subrow of row 1 because we expanded it
expect(rows[2]).toHaveAttribute('aria-selected', 'false');
@@ -187,7 +187,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.rows();
expect(rows[2]).toHaveAttribute('aria-selected', 'true');
await treeTester.toggleRowSelection({row: 2});
@@ -198,7 +198,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.rows();
await treeTester.toggleRowSelection({row: 3});
expect(rows[3]).toHaveAttribute('aria-selected', 'true');
@@ -214,13 +214,12 @@ export const AriaTreeTests = ({renderers, setup, prefix}: AriaTreeTestProps): vo
let tree = (renderers.allInteractionsDisabled!)();
let treeTester = testUtilUser.createTester('Tree', {user, root: tree.container, interactionType});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
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 af75cba5644..3133a346776 100644
--- a/packages/react-aria-components/test/ComboBox.test.js
+++ b/packages/react-aria-components/test/ComboBox.test.js
@@ -152,11 +152,11 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('p');
- let groups = comboboxTester.sections;
+ let groups = comboboxTester.sections();
expect(groups).toHaveLength(1);
expect(groups[0]).toHaveAttribute('aria-labelledby');
expect(document.getElementById(groups[0].getAttribute('aria-labelledby'))).toHaveTextContent('Fruit');
@@ -190,11 +190,11 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('p');
- let groups = comboboxTester.sections;
+ let groups = comboboxTester.sections();
expect(groups).toHaveLength(1);
expect(groups[0]).toHaveAttribute('aria-labelledby');
expect(document.getElementById(groups[0].getAttribute('aria-labelledby'))).toHaveTextContent('Fruit');
@@ -226,7 +226,7 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('c');
let options = comboboxTester.options();
@@ -274,6 +274,30 @@ 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.combobox()).toHaveValue('Dog');
+ expect(comboboxTester.listbox()).toBeNull();
+ });
+
it.each(['click', 'tab'])('should not fire extra onSelectionChange calls after focus moves away in fully controlled mode via %s', async (focusMove) => {
let onSelectionChange = jest.fn();
let keyToText = {
@@ -316,14 +340,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.combobox().focus();
});
await user.tab();
}
@@ -352,7 +376,7 @@ describe('ComboBox', () => {
);
const comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- const combobox = comboboxTester.combobox;
+ const combobox = comboboxTester.combobox();
expect(combobox).toHaveValue('Dog');
await comboboxTester.open();
@@ -395,7 +419,7 @@ describe('ComboBox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.combobox();
expect(combobox).toHaveAttribute('required');
expect(combobox).not.toHaveAttribute('aria-required');
@@ -412,7 +436,7 @@ describe('ComboBox', () => {
expect(comboboxWrapper).toHaveAttribute('data-invalid');
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('C');
@@ -491,7 +515,7 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
- expect(comboboxTester.listbox).toBeFalsy();
+ expect(comboboxTester.listbox()).toBeFalsy();
comboboxTester.setInteractionType('mouse');
await comboboxTester.open();
@@ -547,13 +571,13 @@ describe('ComboBox', () => {
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('p');
let options = comboboxTester.options();
expect(options).toHaveLength(1);
- expect(comboboxTester.listbox).toBeTruthy();
+ expect(comboboxTester.listbox()).toBeTruthy();
expect(options[0]).toHaveTextContent('No results');
});
@@ -596,7 +620,7 @@ describe('ComboBox', () => {
let tree = render();
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
act(() => {
- comboboxTester.combobox.focus();
+ comboboxTester.combobox().focus();
});
await user.keyboard('L');
@@ -611,10 +635,10 @@ describe('ComboBox', () => {
await user.click(options[0]);
}
expect(onAction).toHaveBeenCalledTimes(1);
- expect(comboboxTester.combobox).toHaveValue('');
+ expect(comboboxTester.combobox()).toHaveValue('');
// Repeat with an option selected.
- await comboboxTester.selectOption({option: 'Cat'});
+ await comboboxTester.toggleOptionSelection({option: 'Cat'});
await user.keyboard('s');
@@ -628,7 +652,7 @@ describe('ComboBox', () => {
await user.click(options[0]);
}
expect(onAction).toHaveBeenCalledTimes(2);
- expect(comboboxTester.combobox).toHaveValue('Cat');
+ expect(comboboxTester.combobox()).toHaveValue('Cat');
});
it('should not close the combobox when clicking on a section header', async () => {
@@ -694,7 +718,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.combobox()).toHaveValue('Apple');
});
it('should support multiple selection', async () => {
@@ -709,26 +733,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.combobox().getAttribute('aria-describedby')).toContain(value.id);
- expect(comboboxTester.combobox).toHaveValue('');
+ expect(comboboxTester.combobox()).toHaveValue('');
await comboboxTester.open();
- let listbox = comboboxTester.listbox;
+ let listbox = comboboxTester.listbox();
expect(listbox).toHaveAttribute('aria-multiselectable', 'true');
let options = comboboxTester.options();
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.combobox()).toHaveValue('');
+ expect(comboboxTester.listbox()).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.combobox()).toHaveValue('');
+ expect(comboboxTester.listbox()).toBeInTheDocument();
expect(value).toHaveTextContent('Cat and Dog');
await comboboxTester.close();
@@ -739,16 +763,42 @@ describe('ComboBox', () => {
expect(formData.getAll('combobox')).toEqual(['1', '2']);
await user.click(document.querySelector('input[type="reset"]'));
- expect(comboboxTester.combobox).toHaveValue('');
+ expect(comboboxTester.combobox()).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.options()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(comboboxTester.options()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['1', '2']);
+
+ await comboboxTester.toggleOptionSelection({option: 'Cat'});
+ expect(comboboxTester.options()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(comboboxTester.options()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['2']);
+
+ await comboboxTester.toggleOptionSelection({option: 'Dog'});
+ expect(comboboxTester.options()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(comboboxTester.options()[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.combobox()).toHaveValue('');
await comboboxTester.open();
let options = comboboxTester.options();
@@ -763,7 +813,7 @@ describe('ComboBox', () => {
let {container} = render();
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.combobox();
expect(combobox).toHaveValue('C');
await comboboxTester.open();
@@ -792,10 +842,10 @@ describe('ComboBox', () => {
await user.tab();
await user.keyboard('Test');
- expect(comboboxTester.combobox).toHaveValue('Test');
+ expect(comboboxTester.combobox()).toHaveValue('Test');
await user.tab();
- expect(comboboxTester.combobox).toHaveValue('Test');
+ expect(comboboxTester.combobox()).toHaveValue('Test');
});
it('should allow the user to deselect items with keyboard when multiselect (uncontrolled)', async () => {
@@ -849,7 +899,7 @@ describe('ComboBox', () => {
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: container});
- let combobox = comboboxTester.combobox;
+ let combobox = comboboxTester.combobox();
expect(combobox).toHaveAttribute('required');
expect(combobox.validity.valid).toBe(false);
@@ -857,11 +907,11 @@ describe('ComboBox', () => {
act(() => {getByTestId('form').checkValidity();});
expect(combobox).toHaveAttribute('aria-describedby');
expect(container.querySelector('.react-aria-ComboBox')).toHaveAttribute('data-invalid');
-
+
await comboboxTester.open();
let options = comboboxTester.options();
await user.click(options[0]);
-
+
act(() => combobox.blur());
expect(combobox).not.toHaveAttribute('required');
expect(combobox.validity.valid).toBe(true);
@@ -894,8 +944,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.listbox()).toBeVisible();
+ expect(comboboxTester.combobox()).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();
@@ -903,8 +953,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.listbox()).toBeVisible();
+ expect(comboboxTester.combobox()).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(0);
});
@@ -916,7 +966,7 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
+ expect(comboboxTester.listbox()).toBeVisible();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();
@@ -924,8 +974,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeNull();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.listbox()).toBeNull();
+ expect(comboboxTester.combobox()).toHaveFocus();
expect(onOpenChange).toHaveBeenCalledTimes(1);
onOpenChange.mockClear();
@@ -933,8 +983,8 @@ describe('ComboBox', () => {
act(() => {
jest.runAllTimers();
});
- expect(comboboxTester.listbox).toBeVisible();
- expect(comboboxTester.combobox).toHaveFocus();
+ expect(comboboxTester.listbox()).toBeVisible();
+ expect(comboboxTester.combobox()).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..1d86e5353e6
--- /dev/null
+++ b/packages/react-aria-components/test/Dialog.browser.test.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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.dialog()).not.toBeNull();
+ expect(tester.dialog()!.contains(document.activeElement)).toBe(true);
+
+ await tester.close();
+ expect(tester.dialog()).toBeNull();
+});
diff --git a/packages/react-aria-components/test/Dialog.test.js b/packages/react-aria-components/test/Dialog.test.js
index 69375f92fd3..680160620bb 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.dialog();
expect(dialog).toHaveAttribute('role', 'alertdialog');
let heading = getByRole('heading');
expect(dialog).toHaveAttribute('aria-labelledby', heading.id);
@@ -172,7 +172,7 @@ describe('Dialog', () => {
await dialogTester.open();
expect(button).toHaveAttribute('data-pressed');
- let dialog = dialogTester.dialog;
+ let dialog = dialogTester.dialog();
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..f19bf219da0
--- /dev/null
+++ b/packages/react-aria-components/test/GridList.browser.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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.rows();
+ 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 27a5d5d9b46..9ebf0bb5771 100644
--- a/packages/react-aria-components/test/GridList.test.js
+++ b/packages/react-aria-components/test/GridList.test.js
@@ -100,9 +100,9 @@ describe('GridList', () => {
let {getByRole} = renderGridList();
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
- expect(gridListTester.gridlist).toHaveAttribute('class', 'react-aria-GridList');
+ expect(gridListTester.gridlist()).toHaveAttribute('class', 'react-aria-GridList');
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.rows()) {
expect(row).toHaveAttribute('class', 'react-aria-GridListItem');
}
});
@@ -268,7 +268,7 @@ describe('GridList', () => {
let {getByRole} = renderGridList({selectionMode: 'multiple'}, {checkboxComponent: comp, className: ({isSelected}) => isSelected ? 'selected' : ''});
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
- let row = gridListTester.rows[0];
+ let row = gridListTester.rows()[0];
expect(row).not.toHaveAttribute('aria-selected', 'true');
expect(row).not.toHaveClass('selected');
expect(within(row).getByRole('checkbox')).not.toBeChecked();
@@ -288,27 +288,31 @@ 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.rows()[0];
expect(within(row).getByRole('checkbox')).not.toBeChecked();
await gridListTester.toggleRowSelection({row: 0});
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.selectedRows()).toHaveLength(1);
await gridListTester.toggleRowSelection({row: 1});
- expect(gridListTester.selectedRows).toHaveLength(2);
+ expect(gridListTester.selectedRows()).toHaveLength(2);
await user.keyboard('{Escape}');
- expect(gridListTester.selectedRows).toHaveLength(2);
+ expect(gridListTester.selectedRows()).toHaveLength(2);
});
- it('should support disabled state', () => {
- let {getAllByRole} = renderGridList({selectionMode: 'multiple', disabledKeys: ['cat']}, {className: ({isDisabled}) => isDisabled ? 'disabled' : ''});
+ it('should support disabled state', async () => {
+ let {getAllByRole, getByRole} = renderGridList({selectionMode: 'multiple', disabledKeys: ['cat']}, {className: ({isDisabled}) => isDisabled ? 'disabled' : ''});
let row = getAllByRole('row')[0];
expect(row).toHaveAttribute('aria-disabled', 'true');
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 () => {
@@ -321,7 +325,7 @@ describe('GridList', () => {
);
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows[1]).toHaveAttribute('aria-disabled', 'true');
expect(within(rows[1]).getByRole('button')).toBeDisabled();
@@ -580,9 +584,9 @@ describe('GridList', () => {
it('should perform selection with single selection', async () => {
let {getByRole} = render();
let gridListTester = testUtilUser.createTester('GridList', {user, root: getByRole('grid'), interactionType: type});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -602,8 +606,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.selectedRows()).toHaveLength(1);
+ expect(gridListTester.selectedRows()[0]).toBe(row2);
let row1 = rows[1];
await gridListTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
@@ -617,8 +621,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.selectedRows()).toHaveLength(1);
+ expect(gridListTester.selectedRows()[0]).toBe(row1);
await gridListTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
expect(row1).toHaveAttribute('aria-selected', 'false');
@@ -631,15 +635,15 @@ 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.selectedRows()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
let {getByRole} = render();
let gridListTester = testUtilUser.createTester('GridList', {user, root: getByRole('grid'), interactionType: type});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -655,13 +659,13 @@ describe('GridList', () => {
// 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(['cat', 'kangaroo']));
- expect(gridListTester.selectedRows).toHaveLength(2);
- expect(gridListTester.selectedRows[1]).toBe(row2);
+ expect(gridListTester.selectedRows()).toHaveLength(2);
+ expect(gridListTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(gridListTester.selectedRows()[0]).toBe(row2);
}
let row1 = rows[1];
@@ -673,15 +677,15 @@ describe('GridList', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
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.selectedRows()).toHaveLength(3);
+ expect(gridListTester.selectedRows()[1]).toBe(row1);
+ expect(gridListTester.selectedRows()[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.selectedRows()).toHaveLength(2);
+ expect(gridListTester.selectedRows()[0]).toBe(row1);
+ expect(gridListTester.selectedRows()[1]).toBe(row2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -693,22 +697,22 @@ describe('GridList', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(4);
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.selectedRows()).toHaveLength(2);
+ expect(gridListTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(gridListTester.selectedRows()[0]).toBe(row2);
}
});
it('should perform replace selection in highlight mode when not using modifier keys', async () => {
let {getByRole} = render();
let gridListTester = testUtilUser.createTester('GridList', {user, root: getByRole('grid'), interactionType: type});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
- for (let row of gridListTester.rows) {
+ for (let row of gridListTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -727,8 +731,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.selectedRows()).toHaveLength(1);
+ expect(gridListTester.selectedRows()[0]).toBe(row2);
let row1 = rows[1];
await gridListTester.toggleRowSelection({row: row1});
@@ -743,8 +747,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.selectedRows()).toHaveLength(1);
+ expect(gridListTester.selectedRows()[0]).toBe(row1);
// pressing without modifier keys won't deselect the row
await gridListTester.toggleRowSelection({row: row1});
@@ -755,7 +759,7 @@ describe('GridList', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(gridListTester.selectedRows).toHaveLength(1);
+ expect(gridListTester.selectedRows()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(row1).toHaveAttribute('aria-selected', 'true');
@@ -764,16 +768,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.selectedRows()).toHaveLength(2);
+ expect(gridListTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(gridListTester.selectedRows()[0]).toBe(row2);
}
});
});
@@ -1291,7 +1295,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows).toHaveLength(4);
let loaderRow = rows[3];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -1304,7 +1308,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows).toHaveLength(3);
expect(tree.queryByText('Loading...')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
@@ -1314,7 +1318,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows).toHaveLength(1);
expect(rows[0]).toHaveTextContent('empty state');
expect(tree.queryByText('Loading...')).toBeFalsy();
@@ -1322,7 +1326,7 @@ describe('GridList', () => {
// Even if the gridlist is empty, providing isLoading will render the loader
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.rows();
expect(rows).toHaveLength(2);
expect(rows[1]).toHaveTextContent('empty state');
expect(tree.queryByText('Loading...')).toBeTruthy();
@@ -1398,7 +1402,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows).toHaveLength(8);
let loaderRow = rows[7];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -1417,35 +1421,35 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows).toHaveLength(7);
- expect(within(gridListTester.gridlist).queryByText('Loading...')).toBeFalsy();
+ expect(within(gridListTester.gridlist()).queryByText('Loading...')).toBeFalsy();
- let sentinel = within(gridListTester.gridlist).getByTestId('loadMoreSentinel');
+ let sentinel = within(gridListTester.gridlist()).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.rows();
expect(rows).toHaveLength(1);
let emptyStateRow = rows[0];
expect(emptyStateRow).toHaveTextContent('empty state');
- expect(within(gridListTester.gridlist).queryByText('Loading...')).toBeFalsy();
+ expect(within(gridListTester.gridlist()).queryByText('Loading...')).toBeFalsy();
- sentinel = within(gridListTester.gridlist).getByTestId('loadMoreSentinel');
+ sentinel = within(gridListTester.gridlist()).getByTestId('loadMoreSentinel');
sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('0px');
expect(sentinelParentStyles.height).toBe('0px');
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.rows();
expect(rows).toHaveLength(1);
emptyStateRow = rows[0];
expect(emptyStateRow).toHaveTextContent('loading');
- sentinel = within(gridListTester.gridlist).getByTestId('loadMoreSentinel');
+ sentinel = within(gridListTester.gridlist()).getByTestId('loadMoreSentinel');
sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('0px');
expect(sentinelParentStyles.height).toBe('0px');
@@ -1455,7 +1459,7 @@ describe('GridList', () => {
let tree = render();
let gridListTester = testUtilUser.createTester('GridList', {root: tree.getByRole('grid')});
- let rows = gridListTester.rows;
+ let rows = gridListTester.rows();
expect(rows).toHaveLength(1);
let loaderRow = rows[0];
@@ -1466,15 +1470,15 @@ describe('GridList', () => {
}
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.rows();
expect(rows).toHaveLength(7);
- expect(within(gridListTester.gridlist).queryByText('loading')).toBeFalsy();
+ expect(within(gridListTester.gridlist()).queryByText('loading')).toBeFalsy();
for (let [index, row] of rows.entries()) {
expect(row).toHaveAttribute('aria-rowindex', `${index + 1}`);
}
tree.rerender();
- rows = gridListTester.rows;
+ rows = gridListTester.rows();
expect(rows).toHaveLength(8);
loaderRow = rows[7];
expect(loaderRow).not.toHaveAttribute('aria-rowindex');
@@ -1503,7 +1507,7 @@ describe('GridList', () => {
let {getByRole} = renderGridList({}, {onAction, onPressStart, onPressEnd, onPress, onClick});
let gridListTester = testUtilUser.createTester('GridList', {root: getByRole('grid')});
await gridListTester.triggerRowAction({row: 1, interactionType});
-
+
expect(onAction).toHaveBeenCalledTimes(1);
expect(onPressStart).toHaveBeenCalledTimes(1);
expect(onPressEnd).toHaveBeenCalledTimes(1);
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..ef296b2192b
--- /dev/null
+++ b/packages/react-aria-components/test/ListBox.browser.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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.options();
+ 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 49f4aa98c9b..a23013f6bfd 100644
--- a/packages/react-aria-components/test/ListBox.test.js
+++ b/packages/react-aria-components/test/ListBox.test.js
@@ -93,8 +93,8 @@ describe('ListBox', () => {
);
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox')});
- expect(listboxTester.listbox).toHaveAttribute('data-rac');
- let sections = listboxTester.sections;
+ expect(listboxTester.listbox()).toHaveAttribute('data-rac');
+ let sections = listboxTester.sections();
for (let section of sections) {
expect(section).toHaveAttribute('data-rac');
}
@@ -592,12 +592,12 @@ describe('ListBox', () => {
let options = listboxTester.options();
await listboxTester.triggerOptionAction({option: options[0]});
- let selectedOptions = listboxTester.selectedOptions;
+ let selectedOptions = listboxTester.selectedOptions();
expect(selectedOptions).toHaveLength(1);
expect(onAction).not.toHaveBeenCalled();
await listboxTester.triggerOptionAction({option: options[1], needsDoubleClick: true});
- selectedOptions = listboxTester.selectedOptions;
+ selectedOptions = listboxTester.selectedOptions();
expect(selectedOptions).toHaveLength(1);
expect(onAction).toHaveBeenCalledTimes(1);
});
@@ -623,8 +623,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.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(option2);
let option1 = options[1];
await listboxTester.toggleOptionSelection({option: option1, selectionBehavior: 'replace'});
@@ -638,8 +638,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.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(option1);
await listboxTester.toggleOptionSelection({option: option1, selectionBehavior: 'replace'});
expect(option1).toHaveAttribute('aria-selected', 'false');
@@ -652,7 +652,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.selectedOptions()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
@@ -668,13 +668,13 @@ describe('ListBox', () => {
// 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(['cat', 'kangaroo']));
- expect(listboxTester.selectedOptions).toHaveLength(2);
- expect(listboxTester.selectedOptions[1]).toBe(option2);
+ expect(listboxTester.selectedOptions()).toHaveLength(2);
+ expect(listboxTester.selectedOptions()[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.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(option2);
}
let option1 = options[1];
@@ -686,15 +686,15 @@ describe('ListBox', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(3);
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.selectedOptions()).toHaveLength(3);
+ expect(listboxTester.selectedOptions()[1]).toBe(option1);
+ expect(listboxTester.selectedOptions()[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.selectedOptions()).toHaveLength(2);
+ expect(listboxTester.selectedOptions()[0]).toBe(option1);
+ expect(listboxTester.selectedOptions()[1]).toBe(option2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -706,13 +706,13 @@ describe('ListBox', () => {
if (type === 'keyboard') {
expect(onSelectionChange).toHaveBeenCalledTimes(4);
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.selectedOptions()).toHaveLength(2);
+ expect(listboxTester.selectedOptions()[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.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(option2);
}
});
@@ -732,8 +732,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.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(option2);
let option1 = options[1];
await listboxTester.toggleOptionSelection({option: option1});
@@ -748,8 +748,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.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(option1);
// pressing without modifier keys won't deselect the row
await listboxTester.toggleOptionSelection({option: option1});
expect(option1).toHaveAttribute('aria-selected', 'true');
@@ -759,7 +759,7 @@ describe('ListBox', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(listboxTester.selectedOptions).toHaveLength(1);
+ expect(listboxTester.selectedOptions()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(option1).toHaveAttribute('aria-selected', 'true');
@@ -768,16 +768,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.selectedOptions()).toHaveLength(2);
+ expect(listboxTester.selectedOptions()[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.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(option2);
}
});
});
@@ -797,17 +797,17 @@ describe('ListBox', () => {
let listboxTester = testUtilUser.createTester('ListBox', {root: getByRole('listbox'), advanceTimer: jest.advanceTimersByTime, interactionType: 'touch'});
await listboxTester.toggleOptionSelection({option: listboxTester.options()[0]});
- expect(listboxTester.selectedOptions).toHaveLength(0);
+ expect(listboxTester.selectedOptions()).toHaveLength(0);
expect(onAction).toHaveBeenCalledTimes(1);
await listboxTester.toggleOptionSelection({option: listboxTester.options()[0], needsLongPress: true});
- expect(listboxTester.selectedOptions).toHaveLength(1);
- expect(listboxTester.selectedOptions[0]).toBe(listboxTester.options()[0]);
+ expect(listboxTester.selectedOptions()).toHaveLength(1);
+ expect(listboxTester.selectedOptions()[0]).toBe(listboxTester.options()[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]);
+ expect(listboxTester.selectedOptions()).toHaveLength(2);
+ expect(listboxTester.selectedOptions()[1]).toBe(listboxTester.options()[1]);
});
});
@@ -1029,6 +1029,57 @@ 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.options();
+
+ 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});
@@ -1882,9 +1933,9 @@ describe('ListBox', () => {
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
let options = listboxTester.options();
expect(options).toHaveLength(7);
- expect(within(listboxTester.listbox).queryByText('Loading...')).toBeFalsy();
+ expect(within(listboxTester.listbox()).queryByText('Loading...')).toBeFalsy();
- let sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
+ let sentinel = within(listboxTester.listbox()).getByTestId('loadMoreSentinel');
let sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('1250px');
expect(sentinelParentStyles.height).toBe('0px');
@@ -1895,9 +1946,9 @@ describe('ListBox', () => {
expect(options).toHaveLength(1);
let emptyStateRow = options[0];
expect(emptyStateRow).toHaveTextContent('empty state');
- expect(within(listboxTester.listbox).queryByText('Loading...')).toBeFalsy();
+ expect(within(listboxTester.listbox()).queryByText('Loading...')).toBeFalsy();
- sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
+ sentinel = within(listboxTester.listbox()).getByTestId('loadMoreSentinel');
sentinelParentStyles = sentinel.parentElement.parentElement.style;
expect(sentinelParentStyles.top).toBe('0px');
expect(sentinelParentStyles.height).toBe('0px');
@@ -1912,7 +1963,7 @@ describe('ListBox', () => {
let loadingRow = options[0];
expect(loadingRow).toHaveTextContent('Loading...');
- sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
+ sentinel = within(listboxTester.listbox()).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 798ca2d6eed..f99c1e4320a 100644
--- a/packages/react-aria-components/test/Menu.test.tsx
+++ b/packages/react-aria-components/test/Menu.test.tsx
@@ -1081,18 +1081,18 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button'), interactionType: 'keyboard'});
- expect(menuTester.trigger).not.toHaveAttribute('data-pressed');
+ expect(menuTester.trigger()).not.toHaveAttribute('data-pressed');
await menuTester.open();
- expect(menuTester.trigger).toHaveAttribute('data-pressed');
+ expect(menuTester.trigger()).toHaveAttribute('data-pressed');
expect(menuTester.options()).toHaveLength(5);
- expect(menuTester.menu).toBeInTheDocument();
+ expect(menuTester.menu()).toBeInTheDocument();
- let popover = menuTester.menu?.closest('.react-aria-Popover');
+ let popover = menuTester.menu()?.closest('.react-aria-Popover');
expect(popover).toBeInTheDocument();
expect(popover).toHaveAttribute('data-trigger', 'MenuTrigger');
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.submenuTriggers()[0];
expect(triggerItem).toHaveTextContent('Share…');
expect(triggerItem).toHaveAttribute('aria-haspopup', 'menu');
expect(triggerItem).toHaveAttribute('aria-expanded', 'false');
@@ -1106,22 +1106,22 @@ 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?.menu()).toBeInTheDocument();
expect(submenuTester?.options()).toHaveLength(3);
// Open the nested submenu
let nestedSubmenu = await submenuTester?.openSubmenu({submenuTrigger: 'Email…'});
act(() => {jest.runAllTimers();});
- expect(nestedSubmenu?.menu).toBeInTheDocument();
+ expect(nestedSubmenu?.menu()).toBeInTheDocument();
expect(document.activeElement).toBe(nestedSubmenu?.options()[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?.menu()).not.toBeInTheDocument();
+ expect(submenuTester?.menu()).toBeInTheDocument();
+ expect(menuTester.menu()).toBeInTheDocument();
+ expect(document.activeElement).toBe(nestedSubmenu?.trigger());
});
it('should not close the menu when clicking on a element within the submenu tree', async () => {
let onAction = jest.fn();
@@ -1242,7 +1242,7 @@ describe('Menu', () => {
await menuTester.open();
expect(button).toHaveAttribute('data-pressed');
- let groups = menuTester.sections;
+ let groups = menuTester.sections();
expect(groups).toHaveLength(2);
expect(groups[0]).toHaveClass('react-aria-MenuSection');
@@ -1254,24 +1254,24 @@ describe('Menu', () => {
expect(groups[1]).toHaveAttribute('aria-labelledby');
expect(document.getElementById(groups[1].getAttribute('aria-labelledby')!)).toHaveTextContent('Settings');
- let menu = menuTester.menu!;
+ let menu = menuTester.menu()!;
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.submenuTriggers();
expect(submenuTriggers).toHaveLength(1);
// Open the submenu
let submenuUtil = (await menuTester.openSubmenu({submenuTrigger: 'Share…'}))!;
- let submenu = submenuUtil.menu;
+ let submenu = submenuUtil.menu();
expect(submenu).toBeInTheDocument();
let submenuItems = submenuUtil.options();
expect(submenuItems).toHaveLength(6);
- let groupsInSubmenu = submenuUtil.sections;
+ let groupsInSubmenu = submenuUtil.sections();
expect(groupsInSubmenu).toHaveLength(2);
expect(groupsInSubmenu[0]).toHaveClass('react-aria-MenuSection');
@@ -1328,17 +1328,17 @@ describe('Menu', () => {
);
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')});
- expect(menuTester.trigger).not.toHaveAttribute('data-pressed');
+ expect(menuTester.trigger()).not.toHaveAttribute('data-pressed');
await menuTester.open();
- expect(menuTester.trigger).toHaveAttribute('data-pressed');
+ expect(menuTester.trigger()).toHaveAttribute('data-pressed');
expect(menuTester.options()).toHaveLength(5);
- let popover = menuTester.menu?.closest('.react-aria-Popover');
+ let popover = menuTester.menu()?.closest('.react-aria-Popover');
expect(popover).toBeInTheDocument();
expect(popover).toHaveAttribute('data-trigger', 'MenuTrigger');
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.submenuTriggers()[0];
expect(triggerItem).toHaveTextContent('Share…');
expect(triggerItem).toHaveAttribute('aria-haspopup', 'menu');
expect(triggerItem).toHaveAttribute('aria-expanded', 'false');
@@ -1415,16 +1415,16 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')});
await menuTester.open();
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.submenuTriggers()[0];
expect(triggerItem).toHaveTextContent('Share…');
expect(triggerItem).toHaveAttribute('aria-haspopup', 'menu');
// Open the subdialog
let subDialogTester = await menuTester.openSubmenu({submenuTrigger: triggerItem});
act(() => {jest.runAllTimers();});
- expect(subDialogTester?.menu).toBeInTheDocument();
+ expect(subDialogTester?.menu()).toBeInTheDocument();
- let subDialogTriggerItem = subDialogTester?.submenuTriggers[0];
+ let subDialogTriggerItem = subDialogTester?.submenuTriggers()[0];
expect(subDialogTriggerItem).toHaveTextContent('Nested Subdialog');
expect(subDialogTriggerItem).toHaveAttribute('aria-haspopup', 'menu');
@@ -1496,14 +1496,14 @@ describe('Menu', () => {
await menuTester.open();
// Open the subdialog
- let triggerItem = menuTester.submenuTriggers[0];
+ let triggerItem = menuTester.submenuTriggers()[0];
let subDialogTester = await menuTester.openSubmenu({submenuTrigger: triggerItem});
act(() => {jest.runAllTimers();});
- expect(subDialogTester?.menu).toBeInTheDocument();
+ expect(subDialogTester?.menu()).toBeInTheDocument();
// Open the nested subdialog
- let subDialogTriggerItem = subDialogTester?.submenuTriggers[0];
+ let subDialogTriggerItem = subDialogTester?.submenuTriggers()[0];
await subDialogTester?.openSubmenu({submenuTrigger: subDialogTriggerItem!});
act(() => {jest.runAllTimers();});
let subdialogs = getAllByRole('dialog');
@@ -1513,7 +1513,7 @@ describe('Menu', () => {
act(() => {jest.runAllTimers();});
subdialogs = queryAllByRole('dialog');
expect(subdialogs).toHaveLength(0);
- expect(menuTester.menu).not.toBeInTheDocument();
+ expect(menuTester.menu()).not.toBeInTheDocument();
});
// TODO: add test where clicking in a parent subdialog should close the nested subdialog when we fix that use case
@@ -1605,7 +1605,7 @@ describe('Menu', () => {
await menuTester.open();
act(() => {jest.runAllTimers();});
- let menu = menuTester.menu;
+ let menu = menuTester.menu();
let activeElement = document.activeElement;
await user.tab();
@@ -1633,7 +1633,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);
@@ -1660,7 +1660,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);
@@ -1685,8 +1685,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.trigger(), keys: '[MouseLeft>]'});
+ await user.pointer({target: menuTester.findOption({indexOrText: 'Cat'}), keys: '[/MouseLeft]'});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onPressStart).not.toHaveBeenCalled();
@@ -1712,7 +1712,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);
@@ -1741,7 +1741,7 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {user, root: tree.container});
await menuTester.open();
- await menuTester.selectOption({option: 'Cat', interactionType: 'keyboard', closesOnSelect: false});
+ await menuTester.toggleOptionSelection({option: 'Cat', interactionType: 'keyboard', closesOnSelect: false});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onPressStart).toHaveBeenCalledTimes(1);
@@ -1804,7 +1804,7 @@ describe('Menu', () => {
let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button'), interactionType: 'keyboard'});
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();
expect(onAction).not.toHaveBeenCalled();
let dialogs = getAllByRole('dialog');
@@ -1980,6 +1980,26 @@ AriaMenuTests({
+ ),
+ disabledSubmenuTrigger: () => render(
+
+
+
+
+
+
)
}
});
diff --git a/packages/react-aria-components/test/RadioGroup.test.js b/packages/react-aria-components/test/RadioGroup.test.js
index f12aece756d..e8d3e16bb71 100644
--- a/packages/react-aria-components/test/RadioGroup.test.js
+++ b/packages/react-aria-components/test/RadioGroup.test.js
@@ -290,17 +290,17 @@ describe.each(['RadioGroup', 'RadioField'])('%s', (comp) => {
let onChange = jest.fn();
let {getByRole} = renderGroup({onChange}, {buttonClassName: ({isSelected}) => isSelected ? 'selected' : ''});
let radioGroupTester = testUtilUser.createTester('RadioGroup', {root: getByRole('radiogroup')});
- let radios = radioGroupTester.radios;
+ let radios = radioGroupTester.radios();
let label = radios[0].closest('label');
- expect(radioGroupTester.selectedRadio).toBeFalsy();
+ expect(radioGroupTester.selectedRadio()).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.selectedRadio()).toBe(radios[0]);
expect(label).toHaveAttribute('data-selected', 'true');
expect(findRoot(label)).toHaveAttribute('data-selected', 'true');
expect(label).toHaveClass('selected');
@@ -308,7 +308,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.selectedRadio()).toBe(radios[1]);
expect(label).not.toHaveAttribute('data-selected');
expect(findRoot(label)).not.toHaveAttribute('data-selected');
expect(label).not.toHaveClass('selected');
@@ -363,9 +363,9 @@ describe.each(['RadioGroup', 'RadioField'])('%s', (comp) => {
let {getByRole} = renderGroup({onChange, orientation: 'horizontal', className: ({orientation}) => orientation});
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.radiogroup()).toHaveAttribute('aria-orientation', 'horizontal');
+ expect(radioGroupTester.radiogroup()).toHaveClass('horizontal');
+ let radios = radioGroupTester.radios();
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 b88094f5d3b..17d831b1f29 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.trigger();
expect(trigger).toHaveTextContent('Select an item');
expect(trigger).not.toHaveAttribute('data-pressed');
@@ -75,7 +75,7 @@ describe('Select', () => {
await selectTester.open();
expect(trigger).toHaveAttribute('data-pressed', 'true');
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
expect(listbox).toHaveAttribute('class', 'react-aria-ListBox');
expect(listbox.closest('.react-aria-Popover')).toBeInTheDocument();
expect(listbox.closest('.react-aria-Popover')).toHaveAttribute('data-trigger', 'Select');
@@ -95,7 +95,7 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(trigger.closest('.react-aria-Select')).toHaveAttribute('slot', 'test');
expect(trigger).toHaveAttribute('aria-label', 'test');
});
@@ -135,7 +135,7 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(trigger).toHaveTextContent('Cat');
});
@@ -164,14 +164,14 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
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.trigger();
expect(trigger).toHaveTextContent('Select an animal');
});
@@ -222,7 +222,7 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(trigger).toHaveTextContent('open');
await selectTester.open();
@@ -247,12 +247,12 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
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');
});
@@ -275,12 +275,12 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
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');
});
@@ -323,7 +323,7 @@ describe('Select', () => {
let wrapper = getByTestId('test-select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
let select = wrapper;
let input = document.querySelector('[name=select]');
expect(input).toHaveAttribute('required');
@@ -338,8 +338,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.trigger()).not.toHaveAttribute('aria-describedby');
expect(select).not.toHaveAttribute('data-invalid');
});
@@ -444,7 +444,7 @@ describe('Select', () => {
await user.tab();
await user.tab();
- expect(document.activeElement).toBe(selectTester.trigger);
+ expect(document.activeElement).toBe(selectTester.trigger());
await user.tab();
expect(document.activeElement).toBe(clearButton);
@@ -456,13 +456,13 @@ describe('Select', () => {
expect(document.activeElement).toBe(clearButton);
await user.tab({shift: true});
- expect(document.activeElement).toBe(selectTester.trigger);
+ expect(document.activeElement).toBe(selectTester.trigger());
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');
@@ -479,11 +479,11 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper, interactionType: 'keyboard'});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
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');
});
@@ -524,7 +524,7 @@ describe('Select', () => {
await user.tab();
await user.keyboard('Northern Terr');
let selectTester = testUtilUser.createTester('Select', {root: wrapper, interactionType: 'keyboard'});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(trigger).toHaveTextContent('Northern Territory');
expect(trigger).not.toHaveAttribute('data-pressed');
});
@@ -535,7 +535,7 @@ describe('Select', () => {
let selectTester = testUtilUser.createTester('Select', {
root: getByTestId('select')
});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(document.activeElement).toBe(trigger);
});
@@ -595,7 +595,7 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- await user.click(selectTester.trigger);
+ await user.click(selectTester.trigger());
let popover = queryByTestId('popover');
expect(popover).toBeFalsy();
@@ -619,7 +619,7 @@ describe('Select', () => {
);
- await user.click(selectTester.trigger);
+ await user.click(selectTester.trigger());
popover = queryByTestId('popover');
expect(popover).toBeFalsy();
});
@@ -658,11 +658,11 @@ describe('Select', () => {
const {getByTestId} = render();
const wrapper = getByTestId('select');
const selectTester = testUtilUser.createTester('Select', {root: wrapper});
- const trigger = selectTester.trigger;
+ const trigger = selectTester.trigger();
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);
@@ -683,19 +683,19 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(trigger).toHaveTextContent('Select an item');
await selectTester.open();
- let listbox = selectTester.listbox;
+ let listbox = selectTester.listbox();
expect(listbox).toHaveAttribute('aria-multiselectable', 'true');
let options = selectTester.options();
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();
@@ -706,6 +706,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.options()[0]).toHaveAttribute('aria-selected', 'true');
+ expect(selectTester.options()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['cat', 'dog']);
+
+ await selectTester.toggleOptionSelection({option: 'Cat'});
+ expect(selectTester.options()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(selectTester.options()[1]).toHaveAttribute('aria-selected', 'true');
+ expect(onChange).toHaveBeenLastCalledWith(['dog']);
+
+ await selectTester.toggleOptionSelection({option: 'Dog'});
+ expect(selectTester.options()[0]).toHaveAttribute('aria-selected', 'false');
+ expect(selectTester.options()[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++) {
@@ -732,7 +756,7 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(trigger).toHaveTextContent('Select an item');
let submit = getByTestId('submit');
@@ -763,7 +787,7 @@ describe('Select', () => {
let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
expect(trigger).toHaveTextContent('Dog and Kangaroo');
await selectTester.open();
@@ -799,10 +823,10 @@ describe('Select', () => {
);
let selectTester = testUtilUser.createTester('Select', {root: getByTestId('select')});
- let trigger = selectTester.trigger;
+ let trigger = selectTester.trigger();
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 b5e8bdea8f8..c3b7a3a38f0 100644
--- a/packages/react-aria-components/test/Table.test.js
+++ b/packages/react-aria-components/test/Table.test.js
@@ -273,23 +273,23 @@ 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.table();
expect(table).toHaveAttribute('class', 'react-aria-Table');
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
expect(row).toHaveAttribute('class', 'react-aria-Row');
}
- let rowGroups = tableTester.rowGroups;
+ let rowGroups = tableTester.rowGroups();
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.columns()) {
expect(cell).toHaveAttribute('class', 'react-aria-Column');
}
- for (let cell of tableTester.rowHeaders) {
+ for (let cell of tableTester.rowHeaders()) {
expect(cell).toHaveAttribute('class', 'react-aria-Cell');
}
@@ -708,22 +708,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.rows()[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.rows()[0]);
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});
- expect(document.activeElement).toBe(rows[3]);
+ expect(document.activeElement).toBe(tableTester.rows()[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 () => {
@@ -800,7 +801,7 @@ describe('Table', () => {
let tableTester = testUtilUser.createTester('Table', {root: getByRole('grid')});
- let columns = tableTester.columns;
+ let columns = tableTester.columns();
expect(columns[0]).toHaveAttribute('aria-sort', 'ascending');
expect(columns[0]).toHaveTextContent('▲');
expect(columns[1]).toHaveAttribute('aria-sort', 'none');
@@ -840,7 +841,7 @@ describe('Table', () => {
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowRight}');
- let gridRows = tableTester.rows;
+ let gridRows = tableTester.rows();
expect(gridRows).toHaveLength(4);
let cell = within(gridRows[1]).getAllByRole('rowheader')[0];
expect(cell).toHaveTextContent('Program Files');
@@ -848,7 +849,7 @@ describe('Table', () => {
rerender();
- gridRows = tableTester.rows;
+ gridRows = tableTester.rows();
expect(gridRows).toHaveLength(3);
cell = within(gridRows[1]).getAllByRole('rowheader')[0];
expect(cell).toHaveTextContent('bootmgr');
@@ -1563,8 +1564,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.rows()).toHaveLength(7);
+ expect(tableTester.selectedRows()).toHaveLength(0);
await user.tab();
await user.keyboard('{ArrowRight}');
await user.keyboard('{Enter}');
@@ -1579,8 +1580,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.rows()).toHaveLength(8);
+ expect(tableTester.selectedRows()).toHaveLength(1);
});
});
@@ -2161,7 +2162,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.rows();
expect(rows).toHaveLength(12);
let loaderRow = rows[11];
expect(loaderRow).toHaveTextContent('spinner');
@@ -2173,7 +2174,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.rows();
expect(rows).toHaveLength(11);
expect(tree.queryByText('spinner')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
@@ -2182,24 +2183,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.rows();
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.rowGroups()[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.rows();
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.rowGroups()[1];
expect(body).toHaveAttribute('data-empty', 'true');
selectAll = tree.getAllByRole('checkbox')[0];
expect(selectAll).toBeDisabled();
@@ -2358,7 +2359,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.rows();
expect(rows).toHaveLength(7);
let loaderRow = rows[6];
expect(loaderRow).toHaveTextContent('spinner');
@@ -2376,34 +2377,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.rows();
expect(rows).toHaveLength(6);
- expect(within(tableTester.table).queryByText('spinner')).toBeFalsy();
+ expect(within(tableTester.table()).queryByText('spinner')).toBeFalsy();
- let sentinel = within(tableTester.table).getByTestId('loadMoreSentinel');
+ let sentinel = within(tableTester.table()).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.rows();
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.table()).queryByText('spinner')).toBeFalsy();
+ sentinel = within(tableTester.table()).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.rows();
expect(rows).toHaveLength(1);
emptyStateRow = rows[0];
expect(emptyStateRow).toHaveTextContent('loading');
- sentinel = within(tableTester.table).getByTestId('loadMoreSentinel', {hidden: true});
+ sentinel = within(tableTester.table()).getByTestId('loadMoreSentinel', {hidden: true});
sentinelVirtWrapperStyles = sentinel.closest('[role="presentation"]').style;
expect(sentinelVirtWrapperStyles.top).toBe('0px');
expect(sentinelVirtWrapperStyles.height).toBe('0px');
@@ -2412,7 +2413,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.rows();
expect(rows).toHaveLength(1);
let loaderRow = rows[0];
@@ -2421,15 +2422,15 @@ describe('Table', () => {
expect(loaderRow).not.toHaveAttribute('aria-rowindex');
tree.rerender();
- rows = tableTester.rows;
+ rows = tableTester.rows();
expect(rows).toHaveLength(6);
- expect(within(tableTester.table).queryByText('spinner')).toBeFalsy();
+ expect(within(tableTester.table()).queryByText('spinner')).toBeFalsy();
for (let [index, row] of rows.entries()) {
expect(row).toHaveAttribute('aria-rowindex', `${index + 2}`);
}
tree.rerender();
- rows = tableTester.rows;
+ rows = tableTester.rows();
expect(rows).toHaveLength(7);
loaderRow = rows[6];
expect(loaderRow).not.toHaveAttribute('aria-rowindex');
@@ -2631,9 +2632,9 @@ describe('Table', () => {
}
});
let tableTester = testUtilUser.createTester('Table', {user, root: getByRole('grid'), interactionType: type});
- let rows = tableTester.rows;
+ let rows = tableTester.rows();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -2653,8 +2654,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.selectedRows()).toHaveLength(1);
+ expect(tableTester.selectedRows()[0]).toBe(row2);
let row1 = rows[1];
await tableTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
@@ -2668,8 +2669,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.selectedRows()).toHaveLength(1);
+ expect(tableTester.selectedRows()[0]).toBe(row1);
await tableTester.toggleRowSelection({row: row1, selectionBehavior: 'replace'});
expect(row1).toHaveAttribute('aria-selected', 'false');
@@ -2682,7 +2683,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.selectedRows()).toHaveLength(0);
});
it('should perform toggle selection in highlight mode when using modifier keys', async () => {
@@ -2694,9 +2695,9 @@ describe('Table', () => {
}
});
let tableTester = testUtilUser.createTester('Table', {user, root: getByRole('grid'), interactionType: type});
- let rows = tableTester.rows;
+ let rows = tableTester.rows();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -2712,13 +2713,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.selectedRows()).toHaveLength(2);
+ expect(tableTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(tableTester.selectedRows()[0]).toBe(row2);
}
let row1 = rows[1];
@@ -2730,15 +2731,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.selectedRows()).toHaveLength(3);
+ expect(tableTester.selectedRows()[1]).toBe(row1);
+ expect(tableTester.selectedRows()[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.selectedRows()).toHaveLength(2);
+ expect(tableTester.selectedRows()[0]).toBe(row1);
+ expect(tableTester.selectedRows()[1]).toBe(row2);
}
// With modifier key, you should be able to deselect on press of the same row
@@ -2750,13 +2751,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.selectedRows()).toHaveLength(2);
+ expect(tableTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(tableTester.selectedRows()[0]).toBe(row2);
}
});
@@ -2769,9 +2770,9 @@ describe('Table', () => {
}
});
let tableTester = testUtilUser.createTester('Table', {user, root: getByRole('grid'), interactionType: type});
- let rows = tableTester.rows;
+ let rows = tableTester.rows();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
let checkbox = within(row).queryByRole('checkbox');
expect(checkbox).toBeNull();
expect(row).toHaveAttribute('aria-selected', 'false');
@@ -2790,8 +2791,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.selectedRows()).toHaveLength(1);
+ expect(tableTester.selectedRows()[0]).toBe(row2);
let row1 = rows[1];
await tableTester.toggleRowSelection({row: row1});
@@ -2806,8 +2807,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.selectedRows()).toHaveLength(1);
+ expect(tableTester.selectedRows()[0]).toBe(row1);
// pressing without modifier keys won't deselect the row
await tableTester.toggleRowSelection({row: row1});
@@ -2818,7 +2819,7 @@ describe('Table', () => {
} else {
expect(onSelectionChange).toHaveBeenCalledTimes(2);
}
- expect(tableTester.selectedRows).toHaveLength(1);
+ expect(tableTester.selectedRows()).toHaveLength(1);
} else {
// touch always behaves as toggle
expect(row1).toHaveAttribute('aria-selected', 'true');
@@ -2827,16 +2828,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.selectedRows()).toHaveLength(2);
+ expect(tableTester.selectedRows()[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.selectedRows()).toHaveLength(1);
+ expect(tableTester.selectedRows()[0]).toBe(row2);
}
});
});
@@ -2967,20 +2968,20 @@ describe('Table', () => {
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
+ let groups = tableTester.rowGroups();
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.rows()).toHaveLength(8);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.rows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
@@ -3031,17 +3032,17 @@ describe('Table', () => {
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
+ let groups = tableTester.rowGroups();
expect(groups).toHaveLength(3);
- expect(tableTester.rows).toHaveLength(8);
+ expect(tableTester.rows()).toHaveLength(8);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.rows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
@@ -3091,21 +3092,21 @@ describe('Table', () => {
let tableTester = testUtilUser.createTester('Table', {root});
- let groups = tableTester.rowGroups;
+ let groups = tableTester.rowGroups();
expect(groups).toHaveLength(4);
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.rows()).toHaveLength(10);
await user.tab();
- for (let row of tableTester.rows) {
+ for (let row of tableTester.rows()) {
expect(document.activeElement).toBe(row);
await user.keyboard('{ArrowDown}');
}
- for (let row of tableTester.rows.toReversed().slice(1)) {
+ for (let row of tableTester.rows().toReversed().slice(1)) {
await user.keyboard('{ArrowUp}');
expect(document.activeElement).toBe(row);
}
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..2199b7deb6e
--- /dev/null
+++ b/packages/react-aria-components/test/Tabs.browser.test.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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.tabs();
+ await tester.triggerTab({tab: tabs[1]});
+ expect(tester.selectedTab()).toBe(tabs[1]);
+});
diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js
index 7982487d543..061ebfd6d71 100644
--- a/packages/react-aria-components/test/Tabs.test.js
+++ b/packages/react-aria-components/test/Tabs.test.js
@@ -46,16 +46,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.tablist();
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.tabs()) {
expect(tab).toHaveAttribute('class', 'react-aria-Tab');
}
- expect(tabsTester.tabpanels[0]).toHaveAttribute('class', 'react-aria-TabPanel');
+ expect(tabsTester.tabpanels()[0]).toHaveAttribute('class', 'react-aria-TabPanel');
});
it('should render tabs with custom classes', () => {
@@ -318,15 +318,15 @@ 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.selectedTab()).toBe(tabsTester.tabs()[0]);
+ expect(document.activeElement).toBe(tabsTester.tabpanels()[0]);
});
it('should support selected state', async () => {
let onSelectionChange = jest.fn();
let {getByRole} = renderTabs({onSelectionChange}, {}, {className: ({isSelected}) => isSelected ? 'selected' : ''});
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.tabs();
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
expect(tabs[0]).toHaveClass('selected');
@@ -360,15 +360,15 @@ describe('Tabs', () => {
);
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist')});
- expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('first-element');
+ expect(tabsTester.activeTabpanel().getAttribute('id')).toContain('first-element');
await tabsTester.triggerTab({tab: 1});
expect(onSelectionChange).toHaveBeenCalled();
- expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('second-element');
+ expect(tabsTester.activeTabpanel().getAttribute('id')).toContain('second-element');
await tabsTester.triggerTab({tab: 2});
expect(onSelectionChange).toHaveBeenCalled();
- expect(tabsTester.activeTabpanel.getAttribute('id')).toContain('third-element');
+ expect(tabsTester.activeTabpanel().getAttribute('id')).toContain('third-element');
});
it('should support orientation', () => {
@@ -392,23 +392,23 @@ describe('Tabs', () => {
`('should support changing the selected tab regardless of interaction type, interactionType: $interactionType ', async ({interactionType}) => {
let {getByRole} = renderTabs({orientation: 'vertical'});
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist'), interactionType});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.tabs();
await tabsTester.triggerTab({tab: 0});
- expect(tabsTester.selectedTab).toBe(tabs[0]);
- expect(tabsTester.activeTabpanel.getAttribute('aria-labelledby')).toBe(tabs[0].id);
+ expect(tabsTester.selectedTab()).toBe(tabs[0]);
+ expect(tabsTester.activeTabpanel().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.selectedTab()).toBe(tabs[1]);
+ expect(tabsTester.activeTabpanel().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.selectedTab()).toBe(tabs[2]);
+ expect(tabsTester.activeTabpanel().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.selectedTab()).toBe(tabs[1]);
+ expect(tabsTester.activeTabpanel().getAttribute('aria-labelledby')).toBe(tabs[1].id);
});
it('should support refs', () => {
@@ -461,7 +461,7 @@ describe('Tabs', () => {
let {getByRole} = renderTabs({keyboardActivation: 'manual', onSelectionChange, defaultSelectedKey: 'a'});
let tabsTester = testUtilUser.createTester('Tabs', {root: getByRole('tablist'), interactionType: 'keyboard'});
- let tabs = tabsTester.tabs;
+ let tabs = tabsTester.tabs();
await tabsTester.triggerTab({tab: 0});
expect(tabs[0]).toHaveAttribute('aria-selected', 'true');
diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx
index 1da7313e8f9..cc432c75812 100644
--- a/packages/react-aria-components/test/Tree.test.tsx
+++ b/packages/react-aria-components/test/Tree.test.tsx
@@ -1446,7 +1446,7 @@ describe('Tree', () => {
let tree = render();
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
expect(rows).toHaveLength(3);
let loaderRow = rows[2];
expect(loaderRow).toHaveTextContent('Loading...');
@@ -1456,7 +1456,7 @@ describe('Tree', () => {
// Should render the second sentinel if the row is expanded
tree.rerender();
- rows = treeTester.rows;
+ rows = treeTester.rows();
expect(rows).toHaveLength(8);
let newLoaderRow = rows[4];
expect(newLoaderRow).toHaveTextContent('Loading...');
@@ -1471,7 +1471,7 @@ describe('Tree', () => {
let tree = render();
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
expect(rows).toHaveLength(2);
expect(tree.queryByText('Loading...')).toBeFalsy();
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
@@ -1621,7 +1621,7 @@ describe('Tree', () => {
documentsIsLoading />
);
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
expect(rows).toHaveLength(8);
let rootLoaderRow = rows[7];
expect(rootLoaderRow).toHaveTextContent('Loading...');
@@ -1646,7 +1646,7 @@ describe('Tree', () => {
documentsIsLoading />
);
- rows = treeTester.rows;
+ rows = treeTester.rows();
expect(rows).toHaveLength(9);
rootLoaderRow = rows[8];
rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -1676,7 +1676,7 @@ describe('Tree', () => {
documentsIsLoading />
);
- rows = treeTester.rows;
+ rows = treeTester.rows();
expect(rows).toHaveLength(10);
rootLoaderRow = rows[9];
rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -1713,7 +1713,7 @@ describe('Tree', () => {
documentsIsLoading />
);
- rows = treeTester.rows;
+ rows = treeTester.rows();
expect(rows).toHaveLength(11);
rootLoaderRow = rows[10];
rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -1757,7 +1757,7 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
expect(rows).toHaveLength(9);
let rootLoaderRow = rows[8];
let rootLoaderParentStyles = rootLoaderRow.parentElement!.style;
@@ -1793,7 +1793,7 @@ describe('Tree', () => {
);
let treeTester = testUtilUser.createTester('Tree', {root: tree.getByRole('treegrid')});
- let rows = treeTester.rows;
+ let rows = treeTester.rows();
expect(rows).toHaveLength(8);
let rootLoaderRow = rows[7];
expect(rootLoaderRow).toHaveTextContent('Loading...');
@@ -1806,7 +1806,7 @@ describe('Tree', () => {
);
- expect(document.activeElement).toBe(treeTester.tree);
+ expect(document.activeElement).toBe(treeTester.tree());
});
});
});
@@ -2099,9 +2099,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.rows()).toHaveLength(2);
// has the empty state row
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.rows()).toHaveLength(1);
await user.tab();
// selects and drops first row onto second tree
await user.keyboard('{ArrowRight}');
@@ -2116,13 +2116,13 @@ describe('Tree', () => {
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
});
act(() => jest.runAllTimers());
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.rows()).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.selectedRows()).toHaveLength(9);
});
it('should focus the parent row when dropped on if it isnt expanded', async () => {
@@ -2131,9 +2131,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.rows()).toHaveLength(2);
// has the empty state row
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.rows()).toHaveLength(1);
await user.tab();
// selects and drops first row onto second tree
await user.keyboard('{ArrowRight}');
@@ -2147,12 +2147,12 @@ describe('Tree', () => {
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
});
act(() => jest.runAllTimers());
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.rows()).toHaveLength(1);
await user.keyboard('{ArrowRight}');
- expect(secondTreeTester.rows).toHaveLength(6);
+ expect(secondTreeTester.rows()).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.rows()[0]);
await user.keyboard('{ArrowRight}');
await user.keyboard('{Enter}');
act(() => jest.runAllTimers());
@@ -2166,7 +2166,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.rows()[2]);
});
it('should focus the dropped row when dropped on a parent that is expanded', async () => {
@@ -2175,9 +2175,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.rows()).toHaveLength(2);
// has the empty state row
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.rows()).toHaveLength(1);
await user.tab();
// selects and drops first row onto second tree
await user.keyboard('{ArrowRight}');
@@ -2192,16 +2192,16 @@ describe('Tree', () => {
fireEvent.keyUp(document.activeElement as Element, {key: 'Enter'});
});
act(() => jest.runAllTimers());
- expect(secondTreeTester.rows).toHaveLength(1);
+ expect(secondTreeTester.rows()).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.rows()).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.rows()[0]);
await user.keyboard('{ArrowRight}');
await user.keyboard('{Enter}');
@@ -2217,7 +2217,7 @@ describe('Tree', () => {
});
act(() => jest.runAllTimers());
expect(document.activeElement).toHaveTextContent('Projects');
- expect(document.activeElement).toBe(secondTreeTester.rows[3]);
+ expect(document.activeElement).toBe(secondTreeTester.rows()[3]);
});
});
diff --git a/packages/react-aria-components/test/Treeble.test.js b/packages/react-aria-components/test/Treeble.test.js
index 404e2f6847c..c36cdc24667 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';
@@ -181,123 +182,135 @@ 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');
+ 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(cell).not.toHaveAttribute('data-tree-column');
}
- for (let cell of tester.cells({element: tester.rows[0]})) {
+ for (let cell of tester.cells({element: tester.rows()[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.rowHeaders()[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.rowHeaders()[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.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');
});
- 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.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(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.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');
+ 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);
+ expect(tester.rows()).toHaveLength(4);
});
it('should support defaultExpandedKeys', async () => {
@@ -305,109 +318,109 @@ 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.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');
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.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');
await tester.toggleRowExpansion({row: 4});
- expect(tester.rows).toHaveLength(7);
+ expect(tester.rows()).toHaveLength(7);
expect(onExpandedChange).toHaveBeenCalledTimes(2);
expect(onExpandedChange).toHaveBeenLastCalledWith(new Set(['games']));
@@ -418,49 +431,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.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');
await tester.toggleRowExpansion({row: 4});
expect(onExpandedChange).toHaveBeenCalledTimes(1);
expect(onExpandedChange).toHaveBeenCalledWith(new Set(['games', 'apps']));
- expect(tester.rows).toHaveLength(7); // controlled
+ expect(tester.rows()).toHaveLength(7); // controlled
});
it('supports keyboard navigation of flattened rows', async () => {
@@ -469,16 +482,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.rows().length; i++) {
+ expect(document.activeElement).toBe(tester.rows()[i]);
await user.keyboard('{ArrowDown}');
}
await user.keyboard('{Home}');
- expect(document.activeElement).toBe(tester.rows[0]);
+ expect(document.activeElement).toBe(tester.rows()[0]);
await user.keyboard('{End}');
- expect(document.activeElement).toBe(tester.rows[tester.rows.length - 1]);
+ expect(document.activeElement).toBe(tester.rows()[tester.rows().length - 1]);
});
it('supports keyboard navigation of cells', async () => {
@@ -486,25 +499,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.rows()[0]);
+ expect(tester.rows()[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.rows()[0]);
+ expect(tester.rows()[0]).toHaveAttribute('aria-expanded', 'true');
- let cells = [tester.rowHeaders[0], ...tester.cells({element: tester.rows[0]})];
+ let cells = [tester.rowHeaders()[0], ...tester.cells({element: tester.rows()[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.rows()[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.rows()[0]);
+ expect(tester.rows()[0]).toHaveAttribute('aria-expanded', 'false');
for (let cell of cells.reverse()) {
await user.keyboard('{ArrowLeft}');
@@ -512,7 +525,7 @@ describe('Treeble', () => {
}
await user.keyboard('{ArrowLeft}');
- expect(document.activeElement).toBe(tester.rows[0]);
+ expect(document.activeElement).toBe(tester.rows()[0]);
});
it('supports selection', async () => {
@@ -522,7 +535,7 @@ describe('Treeble', () => {
await tester.toggleRowSelection({row: 0});
await user.keyboard('{Shift>}');
- await user.click(tester.rows[2]);
+ await user.click(tester.rows()[2]);
await user.keyboard('{/Shift}');
expect(onSelectionChange).toHaveBeenCalledTimes(2);
@@ -582,7 +595,7 @@ describe('Treeble', () => {
await user.keyboard('{Enter}');
act(() => jest.runAllTimers());
- expect(tester.rowHeaders.map(r => r.textContent)).toEqual([
+ expect(tester.rowHeaders().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 d3892dbc449..81d8703780a 100644
--- a/packages/react-aria-components/test/VirtualizedMenu.test.tsx
+++ b/packages/react-aria-components/test/VirtualizedMenu.test.tsx
@@ -73,7 +73,7 @@ describe('virtualized menu', () => {
tester.setInteractionType('mouse');
await tester.open();
let items = getAllByRole('menuitem');
- let menu = tester.menu;
+ let menu = tester.menu();
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 1b89e68fde5..784361923c1 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 0663c15e716..bd779b9cd28 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6938,7 +6938,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
@@ -8027,7 +8027,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