diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts index 0176587dbda99..e6e0c02e48d35 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts @@ -9,7 +9,7 @@ export type GridDataSourceCacheDefaultConfig = { ttl?: number; }; -function getKey(params: GridGetRowsParams) { +export function getKey(params: GridGetRowsParams) { return JSON.stringify([ params.filterModel, params.sortModel, diff --git a/packages/x-data-grid-pro/src/tests/dataSource.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/dataSource.DataGridPro.test.tsx new file mode 100644 index 0000000000000..8c10d9924034d --- /dev/null +++ b/packages/x-data-grid-pro/src/tests/dataSource.DataGridPro.test.tsx @@ -0,0 +1,282 @@ +import * as React from 'react'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import { act, createRenderer, waitFor, screen, within } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import { + DataGridPro, + DataGridProProps, + GridApi, + GridDataSource, + GridDataSourceCache, + GridGetRowsParams, + GridGetRowsResponse, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { SinonSpy, spy } from 'sinon'; +import { getKey } from '../hooks/features/dataSource/cache'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); +const cache = new Map(); + +const testCache: GridDataSourceCache = { + set: (key, value) => cache.set(getKey(key), value), + get: (key) => cache.get(getKey(key)), + clear: () => cache.clear(), +}; + +describe(' - Data source', () => { + const { render } = createRenderer(); + + let apiRef: React.MutableRefObject; + let fetchRowsSpy: SinonSpy; + let mockServer: ReturnType; + + function TestDataSource(props: Partial & { shouldRequestsFail?: boolean }) { + apiRef = useGridApiRef(); + const { shouldRequestsFail = false, ...rest } = props; + mockServer = useMockServer( + { rowLength: 100, maxColumns: 1 }, + { useCursorPagination: false, minDelay: 0, maxDelay: 0, verbose: false }, + shouldRequestsFail, + ); + fetchRowsSpy = spy(mockServer, 'fetchRows'); + const { fetchRows } = mockServer; + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params: GridGetRowsParams) => { + const urlParams = new URLSearchParams({ + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + start: `${params.start}`, + end: `${params.end}`, + }); + + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + }), + [fetchRows], + ); + + const baselineProps = { + unstable_dataSource: dataSource, + columns: mockServer.columns, + initialState: { pagination: { paginationModel: { page: 0, pageSize: 10 } } }, + disableVirtualization: true, + }; + + return ( +
+ +
+ ); + } + + beforeEach(function beforeTest() { + if (isJSDOM) { + this.skip(); // Needs layout + } + + cache.clear(); + }); + + it('should fetch the data on initial render', async () => { + render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + }); + + it('should re-fetch the data on filter change', async () => { + const { setProps } = render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + setProps({ filterModel: { items: [{ field: 'name', value: 'John', operator: 'contains' }] } }); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + }); + + it('should re-fetch the data on sort change', async () => { + const { setProps } = render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + setProps({ sortModel: [{ field: 'name', sort: 'asc' }] }); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + }); + + it('should re-fetch the data on pagination change', async () => { + const { setProps } = render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + setProps({ paginationModel: { page: 1, pageSize: 10 } }); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + }); + + describe('Cache', () => { + it('should cache the data using the default cache', async () => { + render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + + const dataRow1 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0', + ); + + const cell1 = within(dataRow1).getByRole('gridcell'); + const cell1Content = cell1.innerText; + + act(() => { + apiRef.current.setPage(1); + }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + + const dataRow2 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0' && el !== dataRow1, + ); + const cell2 = within(dataRow2).getByRole('gridcell'); + const cell2Content = cell2.innerText; + expect(cell2Content).not.to.equal(cell1Content); + + act(() => { + apiRef.current.setPage(0); + }); + + expect(fetchRowsSpy.callCount).to.equal(2); + + const dataRow3 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0' && el !== dataRow1 && el !== dataRow2, + ); + const cell3 = within(dataRow3).getByRole('gridcell'); + const cell3Content = cell3.innerText; + expect(cell3Content).to.equal(cell1Content); + }); + + it('should cache the data using the custom cache', async () => { + render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + expect(cache.size).to.equal(1); + }); + + it('should use the cached data when the same query is made again', async () => { + render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + expect(cache.size).to.equal(1); + + const dataRow1 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0', + ); + + const cell1 = within(dataRow1).getByRole('gridcell'); + + const cell1Content = cell1.innerText; + + act(() => { + apiRef.current.setPage(1); + }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + expect(cache.size).to.equal(2); + + const dataRow2 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0' && el !== dataRow1, + ); + + const cell2 = within(dataRow2).getByRole('gridcell'); + + const cell2Content = cell2.innerText; + expect(cell2Content).not.to.equal(cell1Content); + + act(() => { + apiRef.current.setPage(0); + }); + + const dataRow3 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0' && el !== dataRow1 && el !== dataRow2, + ); + + const cell3 = within(dataRow3).getByRole('gridcell'); + + const cell3Content = cell3.innerText; + expect(cell3Content).to.equal(cell1Content); + + expect(fetchRowsSpy.callCount).to.equal(2); + expect(cache.size).to.equal(2); + }); + + it('should allow to disable the default cache', async () => { + // only + render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + + const dataRow1 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0', + ); + + const cell1 = within(dataRow1).getByRole('gridcell'); + + const cell1Content = cell1.innerText; + + act(() => { + apiRef.current.setPage(1); + }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + + const dataRow2 = await screen.findByText( + (_, el) => el?.getAttribute('data-rowindex') === '0' && el !== dataRow1, + ); + + const cell2 = within(dataRow2).getByRole('gridcell'); + + const cell2Content = cell2.innerText; + expect(cell2Content).not.to.equal(cell1Content); + + act(() => { + apiRef.current.setPage(0); + }); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(3); + }); + }); + }); + + describe('Error handling', () => { + it('should call `unstable_onDataSourceError` when the data source returns an error', async () => { + const onDataSourceError = spy(); + render(); + await waitFor(() => { + expect(onDataSourceError.callCount).to.equal(1); + }); + }); + }); +}); diff --git a/packages/x-data-grid-pro/src/tests/dataSourceTreeData.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/dataSourceTreeData.DataGridPro.test.tsx new file mode 100644 index 0000000000000..8ad3581a199fc --- /dev/null +++ b/packages/x-data-grid-pro/src/tests/dataSourceTreeData.DataGridPro.test.tsx @@ -0,0 +1,203 @@ +import * as React from 'react'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import { createRenderer, waitFor, fireEvent, within, act, screen } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import { + DataGridPro, + DataGridProProps, + GRID_ROOT_GROUP_ID, + GridApi, + GridDataSource, + GridGetRowsParams, + GridGroupNode, + useGridApiRef, +} from '@mui/x-data-grid-pro'; +import { SinonSpy, spy } from 'sinon'; +import { raf } from 'test/utils/helperFn'; + +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + +const dataSetOptions = { + dataSet: 'Employee' as const, + rowLength: 100, + maxColumns: 3, + treeData: { maxDepth: 2, groupingField: 'name', averageChildren: 5 }, +}; +const pageSizeOptions = [5, 10, 50]; + +const serverOptions = { minDelay: 0, maxDelay: 0, verbose: false }; + +describe(' - Data source tree data', () => { + const { render } = createRenderer(); + + let apiRef: React.MutableRefObject; + let fetchRowsSpy: SinonSpy; + let mockServer: ReturnType; + + function TestDataSource(props: Partial & { shouldRequestsFail?: boolean }) { + apiRef = useGridApiRef(); + mockServer = useMockServer(dataSetOptions, serverOptions, props.shouldRequestsFail ?? false); + fetchRowsSpy = spy(mockServer, 'fetchRows'); + const { fetchRows, columns } = mockServer; + + const dataSource: GridDataSource = React.useMemo( + () => ({ + getRows: async (params: GridGetRowsParams) => { + const urlParams = new URLSearchParams({ + paginationModel: JSON.stringify(params.paginationModel), + filterModel: JSON.stringify(params.filterModel), + sortModel: JSON.stringify(params.sortModel), + groupKeys: JSON.stringify(params.groupKeys), + }); + + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row[dataSetOptions.treeData.groupingField], + getChildrenCount: (row) => row.descendantCount, + }), + [fetchRows], + ); + + return ( +
+ +
+ ); + } + + beforeEach(function beforeTest() { + if (isJSDOM) { + this.skip(); // Needs layout + } + }); + + it('should fetch the data on initial render', async () => { + render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + }); + + it('should re-fetch the data on filter change', async () => { + const { setProps } = render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + setProps({ filterModel: { items: [{ field: 'name', value: 'John', operator: 'contains' }] } }); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + }); + + it('should re-fetch the data on sort change', async () => { + const { setProps } = render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + setProps({ sortModel: [{ field: 'name', sort: 'asc' }] }); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + }); + + it('should re-fetch the data on pagination change', async () => { + const { setProps } = render(); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + setProps({ paginationModel: { page: 1, pageSize: 10 } }); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + }); + + it('should fetch nested data when clicking on a dropdown', async () => { + render(); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + await raf(); + expect(Object.keys(apiRef.current.state.rows.tree).length).to.equal(10 + 1); + const dataRow1 = await screen.findByText((_, el) => el?.getAttribute('data-rowindex') === '0'); + + const cell11 = within(dataRow1).getAllByRole('gridcell')[0]; + fireEvent.click(within(cell11).getByRole('button')); + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + + const cell11ChildrenCount = Number(cell11.innerText.split('(')[1].split(')')[0]); + expect(Object.keys(apiRef.current.state.rows.tree).length).to.equal( + 10 + 1 + cell11ChildrenCount, + ); + }); + + it('should fetch nested data when calling API method `unstable_dataSource.fetchRows`', async () => { + render(); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + await raf(); + + const tree = apiRef.current.state.rows.tree; + expect(Object.keys(tree).length).to.equal(10 + 1); + const dataRow1 = await screen.findByText((_, el) => el?.getAttribute('data-rowindex') === '0'); + + const cell11 = within(dataRow1).getAllByRole('gridcell')[0]; + const firstChildId = (tree[GRID_ROOT_GROUP_ID] as GridGroupNode).children[0]; + act(() => { + apiRef.current.unstable_dataSource.fetchRows(firstChildId); + }); + await raf(); + + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(2); + }); + + const cell11ChildrenCount = Number(cell11.innerText.split('(')[1].split(')')[0]); + expect(Object.keys(apiRef.current.state.rows.tree).length).to.equal( + 10 + 1 + cell11ChildrenCount, + ); + }); + + it('should lazily fetch nested data when using `defaultGroupingExpansionDepth`', async () => { + render(); + + // Initial fetch + await waitFor(() => { + expect(fetchRowsSpy.callCount).to.equal(1); + }); + + const groupsToFetch = apiRef.current.state.rows.groupsToFetch; + expect(groupsToFetch?.length).to.be.greaterThan(0); + + const tree = apiRef.current.state.rows.tree; + + // All the group nodes belonging to the grid root group should be there for fetching + (tree[GRID_ROOT_GROUP_ID] as GridGroupNode).children.forEach((child) => { + const node = tree[child]; + if (node.type === 'group') { + expect(groupsToFetch).to.include(child); + } + }); + }); +});