Skip to content

Commit cca9be1

Browse files
authored
isLoading ComboBox tests and useAsyncList loadingState update (#1481)
1 parent 25ea367 commit cca9be1

File tree

4 files changed

+382
-29
lines changed

4 files changed

+382
-29
lines changed

packages/@react-spectrum/combobox/stories/ComboBox.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ function AsyncLoadingExample() {
526526
items={list.items}
527527
inputValue={list.filterText}
528528
onInputChange={list.setFilterText}
529-
loadingState={list.state}
529+
loadingState={list.loadingState}
530530
onLoadMore={list.loadMore}
531531
onOpenChange={action('onOpenChange')}>
532532
{item => <Item key={item.name}>{item.name}</Item>}

packages/@react-spectrum/combobox/test/ComboBox.test.js

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import scaleMedium from '@adobe/spectrum-css-temp/vars/spectrum-medium-unique.cs
2121
import themeLight from '@adobe/spectrum-css-temp/vars/spectrum-light-unique.css';
2222
import {triggerPress} from '@react-spectrum/test-utils';
2323
import {typeText} from '@react-spectrum/test-utils';
24+
import {useAsyncList} from '@react-stately/data';
2425
import userEvent from '@testing-library/user-event';
2526

2627
let theme = {
@@ -78,6 +79,47 @@ function renderSectionComboBox(props = {}) {
7879
);
7980
}
8081

82+
let initialFilterItems = [
83+
{name: 'Aardvark', id: '1'},
84+
{name: 'Kangaroo', id: '2'},
85+
{name: 'Snake', id: '3'}
86+
];
87+
88+
let secondCallFilterItems = [
89+
{name: 'Aardvark', id: '1'}
90+
];
91+
92+
function getFilterItems() {
93+
return Promise.resolve({
94+
items: initialFilterItems
95+
});
96+
}
97+
98+
function mockSecondCall() {
99+
return Promise.resolve({
100+
items: secondCallFilterItems
101+
});
102+
}
103+
104+
let load;
105+
let AsyncComboBox = () => {
106+
let list = useAsyncList({
107+
load: load
108+
});
109+
110+
return (
111+
<ComboBox
112+
items={list.items}
113+
label="Combobox"
114+
inputValue={list.filterText}
115+
onInputChange={list.setFilterText}
116+
loadingState={list.loadingState}
117+
onLoadMore={list.loadMore}>
118+
{(item) => <Item>{item.name}</Item>}
119+
</ComboBox>
120+
);
121+
};
122+
81123
function testComboBoxOpen(combobox, button, listbox, focusedItemIndex) {
82124
let buttonId = button.id;
83125
let comboboxLabelledBy = combobox.getAttribute('aria-labelledby');
@@ -130,6 +172,14 @@ describe('ComboBox', function () {
130172
jest.useFakeTimers();
131173
});
132174

175+
beforeEach(() => {
176+
load = jest
177+
.fn()
178+
.mockImplementationOnce(getFilterItems)
179+
.mockImplementationOnce(mockSecondCall)
180+
.mockImplementationOnce(mockSecondCall);
181+
});
182+
133183
afterEach(() => {
134184
jest.clearAllMocks();
135185
act(() => jest.runAllTimers());
@@ -2584,6 +2634,137 @@ describe('ComboBox', function () {
25842634
expect(combobox).toHaveAttribute('aria-invalid', 'true');
25852635
});
25862636

2637+
describe('loadingState', function () {
2638+
it.each`
2639+
LoadingState | ValidationState
2640+
${'loading'} | ${null}
2641+
${'filtering'} | ${null}
2642+
${'loading'} | ${'invalid'}
2643+
${'filtering'} | ${'invalid'}
2644+
`('should render the loading swirl in the input field when loadingState="$LoadingState" and validationState="$ValidationState"', ({LoadingState, ValidationState}) => {
2645+
let {getByRole} = renderComboBox({loadingState: LoadingState, validationState: ValidationState});
2646+
let button = getByRole('button');
2647+
let progressSpinner = getByRole('progressbar');
2648+
2649+
expect(progressSpinner).toBeTruthy();
2650+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading...');
2651+
2652+
let combobox = getByRole('combobox');
2653+
if (ValidationState) {
2654+
expect(combobox).toHaveAttribute('aria-invalid', 'true');
2655+
}
2656+
2657+
// validation icon should no be present
2658+
expect(() => within(combobox).getByRole('img', {hidden: true})).toThrow();
2659+
2660+
act(() => {
2661+
triggerPress(button);
2662+
jest.runAllTimers();
2663+
});
2664+
2665+
let listbox = getByRole('listbox');
2666+
expect(listbox).toBeVisible();
2667+
expect(() => within(listbox).getByRole('progressbar')).toThrow();
2668+
});
2669+
2670+
it('should render the loading swirl in the listbox when loadingState="loadingMore"', function () {
2671+
let {getByRole} = renderComboBox({loadingState: 'loadingMore'});
2672+
let button = getByRole('button');
2673+
2674+
expect(() => getByRole('progressbar')).toThrow();
2675+
2676+
act(() => {
2677+
triggerPress(button);
2678+
jest.runAllTimers();
2679+
});
2680+
2681+
let listbox = getByRole('listbox');
2682+
expect(listbox).toBeVisible();
2683+
2684+
let progressSpinner = within(listbox).getByRole('progressbar');
2685+
expect(progressSpinner).toBeTruthy();
2686+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading more…');
2687+
});
2688+
});
2689+
2690+
describe('async loading', function () {
2691+
it('async combobox works with useAsyncList', async () => {
2692+
let {getByRole} = render(
2693+
<Provider theme={theme}>
2694+
<AsyncComboBox />
2695+
</Provider>
2696+
);
2697+
2698+
let combobox = getByRole('combobox');
2699+
let button = getByRole('button');
2700+
let progressSpinner = getByRole('progressbar');
2701+
expect(progressSpinner).toBeTruthy();
2702+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading...');
2703+
2704+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
2705+
expect(load).toHaveBeenLastCalledWith(
2706+
expect.objectContaining({
2707+
'filterText': ''
2708+
})
2709+
);
2710+
2711+
act(() => {
2712+
triggerPress(button);
2713+
jest.runAllTimers();
2714+
});
2715+
2716+
expect(() => getByRole('progressbar')).toThrow();
2717+
2718+
let listbox = getByRole('listbox');
2719+
expect(listbox).toBeTruthy();
2720+
let items = within(listbox).getAllByRole('option');
2721+
expect(items).toHaveLength(3);
2722+
expect(items[0]).toHaveTextContent('Aardvark');
2723+
expect(items[1]).toHaveTextContent('Kangaroo');
2724+
expect(items[2]).toHaveTextContent('Snake');
2725+
2726+
act(() => {
2727+
fireEvent.change(combobox, {target: {value: 'aard'}});
2728+
jest.runAllTimers();
2729+
});
2730+
2731+
progressSpinner = getByRole('progressbar');
2732+
expect(progressSpinner).toBeTruthy();
2733+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading...');
2734+
2735+
await waitFor(() => expect(load).toHaveBeenCalledTimes(2));
2736+
expect(load).toHaveBeenLastCalledWith(
2737+
expect.objectContaining({
2738+
'filterText': 'aard'
2739+
})
2740+
);
2741+
expect(() => getByRole('progressbar')).toThrow();
2742+
2743+
items = within(listbox).getAllByRole('option');
2744+
expect(items).toHaveLength(1);
2745+
expect(items[0]).toHaveTextContent('Aardvark');
2746+
2747+
act(() => {
2748+
triggerPress(items[0]);
2749+
jest.runAllTimers();
2750+
});
2751+
2752+
progressSpinner = getByRole('progressbar');
2753+
expect(progressSpinner).toBeTruthy();
2754+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading...');
2755+
2756+
await waitFor(() => expect(load).toHaveBeenCalledTimes(3));
2757+
expect(load).toHaveBeenLastCalledWith(
2758+
expect.objectContaining({
2759+
'filterText': 'Aardvark'
2760+
})
2761+
);
2762+
2763+
expect(() => getByRole('progressbar')).toThrow();
2764+
expect(combobox.value).toBe('Aardvark');
2765+
});
2766+
});
2767+
25872768
describe('mobile combobox', function () {
25882769
beforeEach(() => {
25892770
jest.spyOn(window.screen, 'width', 'get').mockImplementation(() => 600);
@@ -3272,6 +3453,177 @@ describe('ComboBox', function () {
32723453
expect(document.activeElement).toBe(getByRole('button'));
32733454
});
32743455
});
3456+
3457+
describe('isLoading', function () {
3458+
it.each`
3459+
LoadingState | ValidationState
3460+
${'loading'} | ${null}
3461+
${'filtering'} | ${null}
3462+
${'loading'} | ${'invalid'}
3463+
${'filtering'} | ${'invalid'}
3464+
`('should render the loading swirl in the tray input field when loadingState="$LoadingState" and validationState="$ValidationState"', ({LoadingState, ValidationState}) => {
3465+
let {getByRole, getByTestId} = renderComboBox({loadingState: LoadingState, validationState: ValidationState, defaultInputValue: 'O'});
3466+
let button = getByRole('button');
3467+
let progressSpinner = getByRole('progressbar');
3468+
3469+
expect(progressSpinner).toBeTruthy();
3470+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading...');
3471+
3472+
act(() => {
3473+
triggerPress(button);
3474+
jest.runAllTimers();
3475+
});
3476+
3477+
let tray = getByTestId('tray');
3478+
expect(tray).toBeVisible();
3479+
3480+
let trayProgressSpinner = within(tray).getByRole('progressbar');
3481+
expect(trayProgressSpinner).toBeTruthy();
3482+
3483+
if (LoadingState === 'loading') {
3484+
expect(trayProgressSpinner).toHaveAttribute('aria-label', 'Loading more…');
3485+
} else {
3486+
expect(trayProgressSpinner).toHaveAttribute('aria-label', 'Loading...');
3487+
}
3488+
3489+
3490+
let clearButton = within(tray).getByLabelText('Clear');
3491+
expect(clearButton).toBeTruthy();
3492+
3493+
let listbox = getByRole('listbox');
3494+
3495+
if (LoadingState === 'loading') {
3496+
expect(within(listbox).getByRole('progressbar')).toBeTruthy();
3497+
} else {
3498+
expect(() => within(listbox).getByRole('progressbar')).toThrow();
3499+
}
3500+
3501+
if (ValidationState) {
3502+
let trayInput = within(tray).getByRole('searchbox');
3503+
expect(trayInput).toHaveAttribute('aria-invalid', 'true');
3504+
}
3505+
3506+
if (ValidationState && LoadingState === 'loading') {
3507+
// validation icon should be present along with the clear button
3508+
expect(within(tray).getAllByRole('img', {hidden: true})).toHaveLength(2);
3509+
} else {
3510+
// validation icon should not be present, only img is the clear button
3511+
expect(within(tray).getAllByRole('img', {hidden: true})).toHaveLength(1);
3512+
}
3513+
});
3514+
3515+
it('should render the loading swirl in the listbox when loadingState="loadingMore"', function () {
3516+
let {getByRole, getByTestId} = renderComboBox({loadingState: 'loadingMore', validationState: 'invalid'});
3517+
let button = getByRole('button');
3518+
3519+
expect(() => getByRole('progressbar')).toThrow();
3520+
3521+
act(() => {
3522+
triggerPress(button);
3523+
jest.runAllTimers();
3524+
});
3525+
3526+
let tray = getByTestId('tray');
3527+
expect(tray).toBeVisible();
3528+
3529+
let allProgressSpinners = within(tray).getAllByRole('progressbar');
3530+
expect(allProgressSpinners.length).toBe(1);
3531+
3532+
let validationIcon = within(tray).getByRole('img', {hidden: true});
3533+
expect(validationIcon).toBeTruthy();
3534+
3535+
let trayInput = within(tray).getByRole('searchbox');
3536+
expect(trayInput).toHaveAttribute('aria-invalid', 'true');
3537+
3538+
let listbox = getByRole('listbox');
3539+
let progressSpinner = within(listbox).getByRole('progressbar');
3540+
expect(progressSpinner).toBeTruthy();
3541+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading more…');
3542+
});
3543+
});
3544+
3545+
describe('mobile async loading', function () {
3546+
it('async combobox works with useAsyncList', async () => {
3547+
let {getByRole, getByTestId, getByText} = render(
3548+
<Provider theme={theme}>
3549+
<AsyncComboBox />
3550+
</Provider>
3551+
);
3552+
3553+
let button = getByRole('button');
3554+
let progressSpinner = getByRole('progressbar');
3555+
expect(progressSpinner).toBeTruthy();
3556+
3557+
await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
3558+
expect(load).toHaveBeenLastCalledWith(
3559+
expect.objectContaining({
3560+
'filterText': ''
3561+
})
3562+
);
3563+
3564+
act(() => {
3565+
triggerPress(button);
3566+
jest.runAllTimers();
3567+
});
3568+
3569+
expect(() => getByRole('progressbar')).toThrow();
3570+
3571+
let tray = getByTestId('tray');
3572+
expect(tray).toBeVisible();
3573+
expect(() => within(tray).getByRole('progressbar')).toThrow();
3574+
3575+
let listbox = getByRole('listbox');
3576+
expect(() => within(listbox).getByRole('progressbar')).toThrow;
3577+
let items = within(listbox).getAllByRole('option');
3578+
expect(items).toHaveLength(3);
3579+
expect(items[0]).toHaveTextContent('Aardvark');
3580+
expect(items[1]).toHaveTextContent('Kangaroo');
3581+
expect(items[2]).toHaveTextContent('Snake');
3582+
3583+
let trayInput = within(tray).getByRole('searchbox');
3584+
act(() => {
3585+
trayInput.focus();
3586+
fireEvent.change(trayInput, {target: {value: 'aard'}});
3587+
jest.runAllTimers();
3588+
});
3589+
3590+
let trayInputProgress = within(tray).getByRole('progressbar');
3591+
expect(trayInputProgress).toBeTruthy();
3592+
expect(() => within(listbox).getByRole('progressbar')).toThrow;
3593+
3594+
await waitFor(() => expect(load).toHaveBeenCalledTimes(2));
3595+
expect(load).toHaveBeenLastCalledWith(
3596+
expect.objectContaining({
3597+
'filterText': 'aard'
3598+
})
3599+
);
3600+
expect(() => within(tray).getByRole('progressbar')).toThrow();
3601+
3602+
items = within(listbox).getAllByRole('option');
3603+
expect(items).toHaveLength(1);
3604+
expect(items[0]).toHaveTextContent('Aardvark');
3605+
3606+
act(() => {
3607+
triggerPress(items[0]);
3608+
jest.runAllTimers();
3609+
});
3610+
3611+
progressSpinner = getByRole('progressbar');
3612+
expect(progressSpinner).toBeTruthy();
3613+
expect(progressSpinner).toHaveAttribute('aria-label', 'Loading...');
3614+
3615+
await waitFor(() => expect(load).toHaveBeenCalledTimes(3));
3616+
expect(load).toHaveBeenLastCalledWith(
3617+
expect.objectContaining({
3618+
'filterText': 'Aardvark'
3619+
})
3620+
);
3621+
3622+
expect(() => getByRole('progressbar')).toThrow();
3623+
expect(() => getByTestId('tray')).toThrow();
3624+
expect(button).toHaveAttribute('aria-labelledby', `${getByText('Combobox').id} ${getByText('Aardvark').id}`);
3625+
});
3626+
});
32753627
});
32763628

32773629
describe('accessibility', function () {

0 commit comments

Comments
 (0)