Skip to content

Commit 15bc7d1

Browse files
committed
add listbox and table tests
1 parent 1eb421e commit 15bc7d1

File tree

2 files changed

+397
-101
lines changed

2 files changed

+397
-101
lines changed

packages/react-aria-components/test/ListBox.test.js

Lines changed: 247 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {act, fireEvent, installPointerEvent, mockClickDefault, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
13+
import {act, fireEvent, installPointerEvent, mockClickDefault, pointerMap, render, setupIntersectionObserverMock, within} from '@react-spectrum/test-utils-internal';
1414
import {
15-
Button, Dialog,
15+
Button,
16+
Collection,
17+
Dialog,
1618
DialogTrigger,
1719
DropIndicator,
1820
Header, Heading,
@@ -27,6 +29,7 @@ import {
2729
Virtualizer
2830
} from '../';
2931
import React, {useState} from 'react';
32+
import {UNSTABLE_ListBoxLoadingSentinel} from '../src/ListBox';
3033
import {User} from '@react-aria/test-utils';
3134
import userEvent from '@testing-library/user-event';
3235

@@ -899,28 +902,28 @@ describe('ListBox', () => {
899902
fireEvent.keyUp(option, {key: 'Enter'});
900903
act(() => jest.runAllTimers());
901904

902-
let rows = getAllByRole('option');
903-
expect(rows).toHaveLength(4);
904-
expect(rows[0]).toHaveAttribute('class', 'react-aria-DropIndicator');
905-
expect(rows[0]).toHaveAttribute('data-drop-target', 'true');
906-
expect(rows[0]).toHaveAttribute('aria-label', 'Insert before Cat');
907-
expect(rows[0]).toHaveTextContent('Test');
908-
expect(rows[1]).toHaveAttribute('class', 'react-aria-DropIndicator');
909-
expect(rows[1]).not.toHaveAttribute('data-drop-target');
910-
expect(rows[1]).toHaveAttribute('aria-label', 'Insert between Cat and Dog');
911-
expect(rows[2]).toHaveAttribute('class', 'react-aria-DropIndicator');
912-
expect(rows[2]).not.toHaveAttribute('data-drop-target');
913-
expect(rows[2]).toHaveAttribute('aria-label', 'Insert between Dog and Kangaroo');
914-
expect(rows[3]).toHaveAttribute('class', 'react-aria-DropIndicator');
915-
expect(rows[3]).not.toHaveAttribute('data-drop-target');
916-
expect(rows[3]).toHaveAttribute('aria-label', 'Insert after Kangaroo');
905+
let options = getAllByRole('option');
906+
expect(options).toHaveLength(4);
907+
expect(options[0]).toHaveAttribute('class', 'react-aria-DropIndicator');
908+
expect(options[0]).toHaveAttribute('data-drop-target', 'true');
909+
expect(options[0]).toHaveAttribute('aria-label', 'Insert before Cat');
910+
expect(options[0]).toHaveTextContent('Test');
911+
expect(options[1]).toHaveAttribute('class', 'react-aria-DropIndicator');
912+
expect(options[1]).not.toHaveAttribute('data-drop-target');
913+
expect(options[1]).toHaveAttribute('aria-label', 'Insert between Cat and Dog');
914+
expect(options[2]).toHaveAttribute('class', 'react-aria-DropIndicator');
915+
expect(options[2]).not.toHaveAttribute('data-drop-target');
916+
expect(options[2]).toHaveAttribute('aria-label', 'Insert between Dog and Kangaroo');
917+
expect(options[3]).toHaveAttribute('class', 'react-aria-DropIndicator');
918+
expect(options[3]).not.toHaveAttribute('data-drop-target');
919+
expect(options[3]).toHaveAttribute('aria-label', 'Insert after Kangaroo');
917920

918921
fireEvent.keyDown(document.activeElement, {key: 'ArrowDown'});
919922
fireEvent.keyUp(document.activeElement, {key: 'ArrowDown'});
920923

921924
expect(document.activeElement).toHaveAttribute('aria-label', 'Insert between Cat and Dog');
922-
expect(rows[0]).not.toHaveAttribute('data-drop-target', 'true');
923-
expect(rows[1]).toHaveAttribute('data-drop-target', 'true');
925+
expect(options[0]).not.toHaveAttribute('data-drop-target', 'true');
926+
expect(options[1]).toHaveAttribute('data-drop-target', 'true');
924927

925928
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
926929
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
@@ -929,7 +932,7 @@ describe('ListBox', () => {
929932
expect(onReorder).toHaveBeenCalledTimes(1);
930933
});
931934

932-
it('should support dropping on rows', () => {
935+
it('should support dropping on options', () => {
933936
let onItemDrop = jest.fn();
934937
let {getAllByRole} = render(<>
935938
<DraggableListBox />
@@ -942,13 +945,13 @@ describe('ListBox', () => {
942945
act(() => jest.runAllTimers());
943946

944947
let listboxes = getAllByRole('listbox');
945-
let rows = within(listboxes[1]).getAllByRole('option');
946-
expect(rows).toHaveLength(3);
947-
expect(rows[0]).toHaveAttribute('data-drop-target', 'true');
948-
expect(rows[1]).not.toHaveAttribute('data-drop-target');
949-
expect(rows[2]).not.toHaveAttribute('data-drop-target');
948+
let options = within(listboxes[1]).getAllByRole('option');
949+
expect(options).toHaveLength(3);
950+
expect(options[0]).toHaveAttribute('data-drop-target', 'true');
951+
expect(options[1]).not.toHaveAttribute('data-drop-target');
952+
expect(options[2]).not.toHaveAttribute('data-drop-target');
950953

951-
expect(document.activeElement).toBe(rows[0]);
954+
expect(document.activeElement).toBe(options[0]);
952955

953956
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
954957
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
@@ -1297,9 +1300,9 @@ describe('ListBox', () => {
12971300
let {getAllByRole} = renderListbox({selectionMode: 'single', onSelectionChange});
12981301
let items = getAllByRole('option');
12991302

1300-
await user.pointer({target: items[0], keys: '[MouseLeft>]'});
1303+
await user.pointer({target: items[0], keys: '[MouseLeft>]'});
13011304
expect(onSelectionChange).toBeCalledTimes(1);
1302-
1305+
13031306
await user.pointer({target: items[0], keys: '[/MouseLeft]'});
13041307
expect(onSelectionChange).toBeCalledTimes(1);
13051308
});
@@ -1309,9 +1312,9 @@ describe('ListBox', () => {
13091312
let {getAllByRole} = renderListbox({selectionMode: 'single', onSelectionChange, shouldSelectOnPressUp: false});
13101313
let items = getAllByRole('option');
13111314

1312-
await user.pointer({target: items[0], keys: '[MouseLeft>]'});
1315+
await user.pointer({target: items[0], keys: '[MouseLeft>]'});
13131316
expect(onSelectionChange).toBeCalledTimes(1);
1314-
1317+
13151318
await user.pointer({target: items[0], keys: '[/MouseLeft]'});
13161319
expect(onSelectionChange).toBeCalledTimes(1);
13171320
});
@@ -1321,11 +1324,223 @@ describe('ListBox', () => {
13211324
let {getAllByRole} = renderListbox({selectionMode: 'single', onSelectionChange, shouldSelectOnPressUp: true});
13221325
let items = getAllByRole('option');
13231326

1324-
await user.pointer({target: items[0], keys: '[MouseLeft>]'});
1327+
await user.pointer({target: items[0], keys: '[MouseLeft>]'});
13251328
expect(onSelectionChange).toBeCalledTimes(0);
1326-
1329+
13271330
await user.pointer({target: items[0], keys: '[/MouseLeft]'});
13281331
expect(onSelectionChange).toBeCalledTimes(1);
13291332
});
13301333
});
1334+
1335+
describe('async loading', () => {
1336+
let items = [
1337+
{name: 'Foo'},
1338+
{name: 'Bar'},
1339+
{name: 'Baz'}
1340+
];
1341+
let renderEmptyState = () => {
1342+
return (
1343+
<div>empty state</div>
1344+
);
1345+
};
1346+
let AsyncListbox = (props) => {
1347+
let {items, isLoading, onLoadMore, ...listBoxProps} = props;
1348+
return (
1349+
<ListBox
1350+
{...listBoxProps}
1351+
aria-label="async listbox"
1352+
renderEmptyState={() => renderEmptyState()}>
1353+
<Collection items={items}>
1354+
{(item) => (
1355+
<ListBoxItem id={item.name}>{item.name}</ListBoxItem>
1356+
)}
1357+
</Collection>
1358+
<UNSTABLE_ListBoxLoadingSentinel isLoading={isLoading} onLoadMore={onLoadMore}>
1359+
Loading...
1360+
</UNSTABLE_ListBoxLoadingSentinel>
1361+
</ListBox>
1362+
);
1363+
};
1364+
1365+
let onLoadMore = jest.fn();
1366+
let observe = jest.fn();
1367+
afterEach(() => {
1368+
jest.clearAllMocks();
1369+
});
1370+
1371+
it('should render the loading element when loading', async () => {
1372+
let tree = render(<AsyncListbox isLoading items={items} />);
1373+
1374+
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
1375+
let options = listboxTester.options();
1376+
expect(options).toHaveLength(4);
1377+
let loaderRow = options[3];
1378+
expect(loaderRow).toHaveTextContent('Loading...');
1379+
1380+
let sentinel = tree.getByTestId('loadMoreSentinel');
1381+
expect(sentinel.parentElement).toHaveAttribute('inert');
1382+
});
1383+
1384+
it('should render the sentinel but not the loading indicator when not loading', async () => {
1385+
let tree = render(<AsyncListbox items={items} />);
1386+
1387+
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
1388+
let options = listboxTester.options();
1389+
expect(options).toHaveLength(3);
1390+
expect(tree.queryByText('Loading...')).toBeFalsy();
1391+
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
1392+
});
1393+
1394+
it('should properly render the renderEmptyState if listbox is empty', async () => {
1395+
let tree = render(<AsyncListbox items={[]} />);
1396+
1397+
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
1398+
let options = listboxTester.options();
1399+
expect(options).toHaveLength(1);
1400+
expect(options[0]).toHaveTextContent('empty state');
1401+
expect(tree.queryByText('Loading...')).toBeFalsy();
1402+
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
1403+
1404+
// Even if the listbox is empty, providing isLoading will render the loader
1405+
tree.rerender(<AsyncListbox items={[]} isLoading />);
1406+
options = listboxTester.options();
1407+
expect(options).toHaveLength(2);
1408+
expect(options[1]).toHaveTextContent('empty state');
1409+
expect(tree.queryByText('Loading...')).toBeTruthy();
1410+
expect(tree.getByTestId('loadMoreSentinel')).toBeInTheDocument();
1411+
});
1412+
1413+
it('should only fire loadMore when intersection is detected regardless of loading state', async () => {
1414+
let observer = setupIntersectionObserverMock({
1415+
observe
1416+
});
1417+
1418+
let tree = render(<AsyncListbox items={items} onLoadMore={onLoadMore} isLoading />);
1419+
let sentinel = tree.getByTestId('loadMoreSentinel');
1420+
expect(observe).toHaveBeenCalledTimes(2);
1421+
expect(observe).toHaveBeenLastCalledWith(sentinel);
1422+
expect(onLoadMore).toHaveBeenCalledTimes(0);
1423+
1424+
act(() => {observer.instance.triggerCallback([{isIntersecting: true}]);});
1425+
expect(onLoadMore).toHaveBeenCalledTimes(1);
1426+
1427+
tree.rerender(<AsyncListbox items={items} onLoadMore={onLoadMore} />);
1428+
expect(observe).toHaveBeenCalledTimes(3);
1429+
expect(observe).toHaveBeenLastCalledWith(sentinel);
1430+
expect(onLoadMore).toHaveBeenCalledTimes(1);
1431+
1432+
act(() => {observer.instance.triggerCallback([{isIntersecting: true}]);});
1433+
expect(onLoadMore).toHaveBeenCalledTimes(2);
1434+
});
1435+
1436+
describe('virtualized', () => {
1437+
let items = [];
1438+
for (let i = 0; i < 50; i++) {
1439+
items.push({name: 'Foo' + i});
1440+
}
1441+
let clientWidth, clientHeight;
1442+
1443+
beforeAll(() => {
1444+
clientWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 100);
1445+
clientHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 100);
1446+
});
1447+
1448+
beforeEach(() => {
1449+
act(() => {jest.runAllTimers();});
1450+
});
1451+
1452+
afterAll(function () {
1453+
clientWidth.mockReset();
1454+
clientHeight.mockReset();
1455+
});
1456+
1457+
let VirtualizedAsyncListbox = (props) => {
1458+
let {items, isLoading, onLoadMore, ...listBoxProps} = props;
1459+
return (
1460+
<Virtualizer
1461+
layout={ListLayout}
1462+
layoutOptions={{
1463+
rowHeight: 25,
1464+
loaderHeight: 30
1465+
}}>
1466+
<ListBox
1467+
{...listBoxProps}
1468+
aria-label="async virtualized listbox"
1469+
renderEmptyState={() => renderEmptyState()}>
1470+
<Collection items={items}>
1471+
{(item) => (
1472+
<ListBoxItem id={item.name}>{item.name}</ListBoxItem>
1473+
)}
1474+
</Collection>
1475+
<UNSTABLE_ListBoxLoadingSentinel isLoading={isLoading} onLoadMore={onLoadMore}>
1476+
Loading...
1477+
</UNSTABLE_ListBoxLoadingSentinel>
1478+
</ListBox>
1479+
</Virtualizer>
1480+
);
1481+
};
1482+
1483+
it('should always render the sentinel even when virtualized', () => {
1484+
let tree = render(<VirtualizedAsyncListbox isLoading items={items} />);
1485+
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
1486+
let options = listboxTester.options();
1487+
expect(options).toHaveLength(8);
1488+
let loaderRow = options[7];
1489+
expect(loaderRow).toHaveTextContent('Loading...');
1490+
expect(loaderRow).toHaveAttribute('aria-posinset', '51');
1491+
expect(loaderRow).toHaveAttribute('aria-setSize', '51');
1492+
let loaderParentStyles = loaderRow.parentElement.style;
1493+
1494+
// 50 items * 25px = 1250
1495+
expect(loaderParentStyles.top).toBe('1250px');
1496+
expect(loaderParentStyles.height).toBe('30px');
1497+
1498+
let sentinel = within(loaderRow.parentElement).getByTestId('loadMoreSentinel');
1499+
expect(sentinel.parentElement).toHaveAttribute('inert');
1500+
});
1501+
1502+
// TODO: for some reason this tree renders empty if ran with the above test...
1503+
// It thinks that the contextSize is 0 and never updates
1504+
it.skip('should not reserve room for the loader if isLoading is false', () => {
1505+
let tree = render(<VirtualizedAsyncListbox items={items} />);
1506+
let listboxTester = testUtilUser.createTester('ListBox', {root: tree.getByRole('listbox')});
1507+
let options = listboxTester.options();
1508+
expect(options).toHaveLength(7);
1509+
expect(within(listboxTester.listbox).queryByText('Loading...')).toBeFalsy();
1510+
1511+
let sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
1512+
let sentinelParentStyles = sentinel.parentElement.parentElement.style;
1513+
expect(sentinelParentStyles.top).toBe('1250px');
1514+
expect(sentinelParentStyles.height).toBe('0px');
1515+
expect(sentinel.parentElement).toHaveAttribute('inert');
1516+
1517+
tree.rerender(<VirtualizedAsyncListbox items={[]} />);
1518+
options = listboxTester.options();
1519+
expect(options).toHaveLength(1);
1520+
let emptyStateRow = options[0];
1521+
expect(emptyStateRow).toHaveTextContent('empty state');
1522+
expect(within(listboxTester.listbox).queryByText('Loading...')).toBeFalsy();
1523+
1524+
sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
1525+
sentinelParentStyles = sentinel.parentElement.parentElement.style;
1526+
expect(sentinelParentStyles.top).toBe('0px');
1527+
expect(sentinelParentStyles.height).toBe('0px');
1528+
1529+
// Setting isLoading will render the loader even if the list is empty.
1530+
tree.rerender(<VirtualizedAsyncListbox items={[]} isLoading />);
1531+
options = listboxTester.options();
1532+
expect(options).toHaveLength(2);
1533+
emptyStateRow = options[1];
1534+
expect(emptyStateRow).toHaveTextContent('empty state');
1535+
1536+
let loadingRow = options[0];
1537+
expect(loadingRow).toHaveTextContent('Loading...');
1538+
1539+
sentinel = within(listboxTester.listbox).getByTestId('loadMoreSentinel');
1540+
sentinelParentStyles = sentinel.parentElement.parentElement.style;
1541+
expect(sentinelParentStyles.top).toBe('0px');
1542+
expect(sentinelParentStyles.height).toBe('30px');
1543+
});
1544+
});
1545+
});
13311546
});

0 commit comments

Comments
 (0)