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 { CoreAdminContext } from '../../core'; import { TestMemoryRouter } from '../../routing'; import { AuthProvider } from '../../types'; import { ListController } from './ListController'; import { getListControllerProps, sanitizeListRestProps, } from './useListController'; import { Authenticated, CanAccess, DisableAuthentication, } from './useListController.security.stories'; import { Basic, defaultDataProvider } from './useListController.stories'; describe('useListController', () => { const defaultProps = { children: jest.fn(), resource: 'posts', debounce: 200, }; 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, 210)); 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', async () => { 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 await waitFor(() => { 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).toHaveBeenCalledWith( expect.objectContaining({ displayedFilters: {}, filterValues: {}, }) ); rerender( ); // Check that the permanent filter was used in the query await waitFor(() => { expect(getList).toHaveBeenCalledTimes(2); }); expect(getList).toHaveBeenCalledWith( 'posts', expect.objectContaining({ filter: { foo: 2 } }) ); expect(children).toHaveBeenCalledTimes(3); }); }); describe('showFilter', () => { it('Does not remove previously shown filter when adding a new one', async () => { let currentDisplayedFilters; const childFunction = ({ showFilter, displayedFilters }) => { currentDisplayedFilters = displayedFilters; return ( <>