import * as React from 'react';
import expect from 'expect';
import {
render,
fireEvent,
waitFor,
screen,
act,
} from '@testing-library/react';
import { testDataProvider } from '../../dataProvider';
import { memoryStore } from '../../store';
import {
useInfiniteListController,
InfiniteListControllerResult,
InfiniteListControllerProps,
} from './useInfiniteListController';
import {
getListControllerProps,
sanitizeListRestProps,
} from './useListController';
import { CoreAdminContext } from '../../core';
import { TestMemoryRouter } from '../../routing';
import {
Basic,
Authenticated,
CanAccess,
DisableAuthentication,
defaultDataProvider,
} from './useInfiniteListController.stories';
import type { AuthProvider } from '../../types';
const InfiniteListController = ({
children,
...props
}: {
children: (params: InfiniteListControllerResult) => React.ReactNode;
} & InfiniteListControllerProps) => {
const controllerProps = useInfiniteListController(props);
return children(controllerProps);
};
describe('useInfiniteListController', () => {
const defaultProps = {
children: jest.fn(),
resource: 'posts',
debounce: 200,
};
describe('onSelectAll', () => {
it('should select all records', async () => {
render();
await waitFor(() => {
expect(screen.getByTestId('selected_ids').textContent).toBe(
'Selected ids: []'
);
});
fireEvent.click(screen.getByText('Select All'));
await waitFor(() => {
expect(screen.getByTestId('selected_ids').textContent).toBe(
'Selected ids: [1,2,3,4,5,6,7]'
);
});
});
it('should select all records even though some records are already selected', async () => {
render();
await waitFor(() => {
expect(screen.getByTestId('selected_ids').textContent).toBe(
'Selected ids: []'
);
});
fireEvent.click(screen.getByText('Select item 1'));
await waitFor(() => {
expect(screen.getByTestId('selected_ids').textContent).toBe(
'Selected ids: [1]'
);
});
fireEvent.click(screen.getByText('Select All'));
await waitFor(() => {
expect(screen.getByTestId('selected_ids').textContent).toBe(
'Selected ids: [1,2,3,4,5,6,7]'
);
});
});
it('should not select more records than the provided limit', async () => {
const dataProvider = defaultDataProvider;
const getList = jest.spyOn(dataProvider, 'getList');
render();
await waitFor(() => {
expect(screen.getByTestId('selected_ids').textContent).toBe(
'Selected ids: []'
);
});
fireEvent.click(screen.getByText('Limited Select All'));
await waitFor(() => {
expect(screen.getByTestId('selected_ids').textContent).toBe(
'Selected ids: [1,2,3]'
);
});
await waitFor(() => {
expect(getList).toHaveBeenCalledWith(
'posts',
expect.objectContaining({
pagination: { page: 1, perPage: 3 },
})
);
});
});
});
describe('queryOptions', () => {
it('should accept custom client query options', async () => {
jest.spyOn(console, 'error').mockImplementationOnce(() => {});
const getList = jest
.fn()
.mockImplementationOnce(() => Promise.reject(new Error()));
const onError = jest.fn();
const dataProvider = testDataProvider({ getList });
render(
{() => }
);
await waitFor(() => {
expect(getList).toHaveBeenCalled();
expect(onError).toHaveBeenCalled();
});
});
it('should accept meta in queryOptions', async () => {
const getList = jest
.fn()
.mockImplementationOnce(() =>
Promise.resolve({ data: [], total: 25 })
);
const dataProvider = testDataProvider({ getList });
render(
{() => }
);
await waitFor(() => {
expect(getList).toHaveBeenCalledWith('posts', {
filter: {},
pagination: { page: 1, perPage: 10 },
sort: { field: 'id', order: 'ASC' },
meta: { foo: 'bar' },
signal: undefined,
});
});
});
it('should not crash when the query is disabled and data is undefined', async () => {
const children = jest.fn().mockReturnValue(children);
const dataProvider = testDataProvider({
getList: () => Promise.resolve({ data: [], total: 0 }),
});
const props = { ...defaultProps, children };
render(
);
await waitFor(() => {
const lastCall = children.mock.calls.at(-1)?.[0];
expect(lastCall.isPending).toBe(true);
expect(lastCall.data).toBeUndefined();
});
});
it('should not reset a persisted page while the query is disabled during the auth check', async () => {
let resolveAuthCheck: () => void;
const authProvider: AuthProvider = {
checkAuth: jest.fn(
() =>
new Promise(resolve => {
resolveAuthCheck = resolve;
})
),
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
};
const getList = jest.fn(() =>
Promise.resolve({
data: [{ id: 1, title: 'A post' }],
total: 100,
})
);
const dataProvider = testDataProvider({ getList });
// params persisted with page 3, e.g. from a previous visit or reload
const store = memoryStore({
'posts.listParams': {
page: 3,
perPage: 10,
sort: 'id',
order: 'ASC',
filter: {},
displayedFilters: {},
},
});
const props = { ...defaultProps, children: jest.fn() };
render(
);
// the list query stays disabled while checkAuth is pending
await waitFor(() => {
expect(authProvider.checkAuth).toHaveBeenCalled();
});
expect(getList).not.toHaveBeenCalled();
resolveAuthCheck!();
// once enabled, the data provider is queried for the persisted
// page 3, not page 1
await waitFor(() => {
expect(getList).toHaveBeenCalledWith(
'posts',
expect.objectContaining({
pagination: { page: 3, perPage: 10 },
})
);
});
expect(getList).not.toHaveBeenCalledWith(
'posts',
expect.objectContaining({
pagination: { page: 1, perPage: 10 },
})
);
expect(store.getItem('posts.listParams').page).toBe(3);
});
});
describe('setFilters', () => {
const childFunction = ({ setFilters, filterValues }) => (
{
setFilters({ q: event.target.value });
}}
/>
);
it('should take only last change in case of a burst of changes (case of inputs being currently edited)', async () => {
const props = {
...defaultProps,
children: childFunction,
};
const store = memoryStore();
const storeSpy = jest.spyOn(store, 'setItem');
render(
Promise.resolve({ data: [], total: 0 }),
})}
store={store}
>
);
const searchInput = screen.getByLabelText('search');
fireEvent.change(searchInput, { target: { value: 'hel' } });
fireEvent.change(searchInput, { target: { value: 'hell' } });
fireEvent.change(searchInput, { target: { value: 'hello' } });
await new Promise(resolve => setTimeout(resolve, 210));
await waitFor(() => {
expect(storeSpy).toHaveBeenCalledTimes(1);
});
expect(storeSpy).toHaveBeenCalledWith('posts.listParams', {
filter: { q: 'hello' },
order: 'ASC',
page: 1,
perPage: 10,
sort: 'id',
});
});
it('should remove empty filters', async () => {
const props = {
...defaultProps,
children: childFunction,
};
const store = memoryStore();
const storeSpy = jest.spyOn(store, 'setItem');
render(
Promise.resolve({ data: [], total: 0 }),
})}
store={store}
>
);
expect(storeSpy).toHaveBeenCalledTimes(1);
const searchInput = screen.getByLabelText('search');
// FIXME: For some reason, triggering the change event with an empty string
// does not call the event handler on childFunction
fireEvent.change(searchInput, { target: { value: '' } });
await new Promise(resolve => setTimeout(resolve, 410));
expect(storeSpy).toHaveBeenCalledTimes(2);
expect(storeSpy).toHaveBeenCalledWith('posts.listParams', {
filter: {},
displayedFilters: { q: true },
order: 'ASC',
page: 1,
perPage: 10,
sort: 'id',
});
});
it('should update data if permanent filters change', () => {
const children = jest.fn().mockReturnValue(children);
const props = {
...defaultProps,
debounce: 200,
children,
};
const getList = jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: [], total: 0 })
);
const dataProvider = testDataProvider({ getList });
const { rerender } = render(
);
// Check that the permanent filter was used in the query
expect(getList).toHaveBeenCalledTimes(1);
expect(getList).toHaveBeenCalledWith(
'posts',
expect.objectContaining({ filter: { foo: 1 } })
);
// Check that the permanent filter is not included in the displayedFilters and filterValues (passed to Filter form and button)
expect(children).toHaveBeenCalledTimes(1);
expect(children).toHaveBeenCalledWith(
expect.objectContaining({
displayedFilters: {},
filterValues: {},
})
);
rerender(
);
// Check that the permanent filter was used in the query
expect(getList).toHaveBeenCalledTimes(2);
expect(getList).toHaveBeenCalledWith(
'posts',
expect.objectContaining({ filter: { foo: 2 } })
);
expect(children).toHaveBeenCalledTimes(2);
});
});
describe('showFilter', () => {
it('Does not remove previously shown filter when adding a new one', async () => {
let currentDisplayedFilters;
const childFunction = ({ showFilter, displayedFilters }) => {
currentDisplayedFilters = displayedFilters;
return (
<>