import React, { ReactElement } from 'react'; import { fireEvent } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { Column, IdType, Row, SortingRule } from 'react-table'; import renderWithTheme from '../../../testUtils/renderWithTheme'; import Table, { hasFixedLayout } from '../Table'; import Input from '../../Input'; import { fromUndefinedable, getOrElse } from '../../../fp/Option'; import { pipe } from '../../../fp/function'; type RowShape = { age: number; name: string; status: string; }; const columns: Column[] = [ { Header: 'Name', accessor: 'name' }, { Header: 'Age', accessor: 'age' }, { Header: 'Status', accessor: 'status', disableSortBy: true }, ]; const data = [ { name: 'Hanh Le', age: 20, status: 'Complicated' }, { name: 'Vinh Tony', age: 21, status: 'In relationship' }, { name: 'Hau Dao', age: 19, status: 'Single' }, ]; describe('rendering', () => { it('renders header & data', () => { const { getByText } = renderWithTheme( ); expect(getByText('Name')).toBeInTheDocument(); expect(getByText('Hanh Le')).toBeInTheDocument(); expect(getByText('Vinh Tony')).toBeInTheDocument(); expect(getByText('Hau Dao')).toBeInTheDocument(); expect(getByText('Age')).toBeInTheDocument(); expect(getByText('20')).toBeInTheDocument(); expect(getByText('21')).toBeInTheDocument(); expect(getByText('19')).toBeInTheDocument(); expect(getByText('Status')).toBeInTheDocument(); expect(getByText('Complicated')).toBeInTheDocument(); expect(getByText('In relationship')).toBeInTheDocument(); expect(getByText('Single')).toBeInTheDocument(); }); it('allows to custom cell rendering', () => { const Cell = ({ value }: { value: string }): ReactElement => (
{`Custom: ${value}`}
); const columnsWithCustomCell: Column[] = [ { Header: 'Name', accessor: 'name' }, { Header: 'Age', accessor: 'age' }, { Header: 'Status', accessor: 'status', disableSortBy: true, Cell, }, ]; const { getByText } = renderWithTheme(
); expect(getByText('Name')).toBeInTheDocument(); expect(getByText('Hanh Le')).toBeInTheDocument(); expect(getByText('Vinh Tony')).toBeInTheDocument(); expect(getByText('Hau Dao')).toBeInTheDocument(); expect(getByText('Age')).toBeInTheDocument(); expect(getByText('20')).toBeInTheDocument(); expect(getByText('21')).toBeInTheDocument(); expect(getByText('19')).toBeInTheDocument(); expect(getByText('Status')).toBeInTheDocument(); expect(getByText('Custom: Complicated')).toBeInTheDocument(); expect(getByText('Custom: In relationship')).toBeInTheDocument(); expect(getByText('Custom: Single')).toBeInTheDocument(); }); it('renders empty interface when there is no data', () => { const { getByText } = renderWithTheme(
); expect(getByText('No data to display')).toBeInTheDocument(); }); }); describe('interaction', () => { describe('sorting', () => { it('allows to sort on enabled sorting columns', () => { const MyTable = (): ReactElement => { const [sortBy, setSortBy] = React.useState[]>([]); return (
); }; const { getByText, container } = renderWithTheme(); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Hanh Le', '20', 'Complicated', 'Vinh Tony', '21', 'In relationship', 'Hau Dao', '19', 'Single', ]); // 1st click: Sort by age asc fireEvent.click(getByText('Age')); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Hau Dao', '19', 'Single', 'Hanh Le', '20', 'Complicated', 'Vinh Tony', '21', 'In relationship', ]); // 2nd click: Sort by age desc fireEvent.click(getByText('Age')); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Vinh Tony', '21', 'In relationship', 'Hanh Le', '20', 'Complicated', 'Hau Dao', '19', 'Single', ]); // 3rd click: Reset to original order fireEvent.click(getByText('Age')); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Hanh Le', '20', 'Complicated', 'Vinh Tony', '21', 'In relationship', 'Hau Dao', '19', 'Single', ]); }); it('allows to sort on enabled sorting columns with icons', () => { const MyTable = (): ReactElement => { const [sortBy, setSortBy] = React.useState[]>([]); return (
); }; const { getByTestId, container } = renderWithTheme(); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Hanh Le', '20', 'Complicated', 'Vinh Tony', '21', 'In relationship', 'Hau Dao', '19', 'Single', ]); // 1st click: Sort by age desc fireEvent.click(getByTestId('table__header_age__down')); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Vinh Tony', '21', 'In relationship', 'Hanh Le', '20', 'Complicated', 'Hau Dao', '19', 'Single', ]); // 2nd click: Sort by age asc fireEvent.click(getByTestId('table__header_age__up')); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Hau Dao', '19', 'Single', 'Hanh Le', '20', 'Complicated', 'Vinh Tony', '21', 'In relationship', ]); // 3rd click: Sort by name desc fireEvent.click(getByTestId('table__header_name__down')); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual([ 'Vinh Tony', '21', 'In relationship', 'Hau Dao', '19', 'Single', 'Hanh Le', '20', 'Complicated', ]); }); it('DOES NOT allow to sort on disabled sorting columns', () => { const MyTable = (): ReactElement => { const [sortBy, setSortBy] = React.useState[]>([]); return (
); }; const { getByText, queryByTestId, container } = renderWithTheme( ); const orginalOrder = [ 'Hanh Le', '20', 'Complicated', 'Vinh Tony', '21', 'In relationship', 'Hau Dao', '19', 'Single', ]; expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual(orginalOrder); // Click on disabled sort by column: Status fireEvent.click(getByText('Status')); expect( Array.from(container.querySelectorAll('td')).map(ele => ele.innerHTML) ).toEqual(orginalOrder); // Does not show sort icons expect( queryByTestId('table__header_status__down') ).not.toBeInTheDocument(); expect(queryByTestId('table__header_status__up')).not.toBeInTheDocument(); }); }); describe('pagination', () => { it('allows to control paginated data', () => { const paginatedData = [ [ { name: 'Vy Nguyen', age: 50, status: 'Single' }, { name: 'Hau Dao', age: 51, status: 'Single' }, ], [ { name: 'Tuan Thieu', age: 99, status: 'In relationship' }, { name: 'Son Trinh', age: 69, status: 'Separated' }, ], ]; const MyTable = (): ReactElement => { const [currentPage, setCurrentPage] = React.useState(1); return (
[]) )} columns={columns} pagination={{ current: currentPage, total: paginatedData.length }} onPaginationChange={setCurrentPage} /> ); }; const { getByText, getByTestId } = renderWithTheme(); expect(getByTestId('table__cell_0_name')).toHaveTextContent('Vy Nguyen'); expect(getByTestId('table__cell_0_age')).toHaveTextContent('50'); expect(getByTestId('table__cell_0_status')).toHaveTextContent('Single'); expect(getByTestId('table__cell_1_name')).toHaveTextContent('Hau Dao'); expect(getByTestId('table__cell_1_age')).toHaveTextContent('51'); expect(getByTestId('table__cell_1_status')).toHaveTextContent('Single'); // Click on page 2 fireEvent.click(getByText('2')); expect(getByTestId('table__cell_0_name')).toHaveTextContent('Tuan Thieu'); expect(getByTestId('table__cell_0_age')).toHaveTextContent('99'); expect(getByTestId('table__cell_0_status')).toHaveTextContent( 'In relationship' ); expect(getByTestId('table__cell_1_name')).toHaveTextContent('Son Trinh'); expect(getByTestId('table__cell_1_age')).toHaveTextContent('69'); expect(getByTestId('table__cell_1_status')).toHaveTextContent( 'Separated' ); }); }); describe('filtering', () => { it('allows to filter table data', async () => { const NameFilter = ({ column: { filterValue, setFilter }, }: { column: { filterValue?: string; setFilter: (value: string) => void; }; }): ReactElement => ( { setFilter(e.target.value); }} /> ); const columnsWithFilter: Column[] = [ { Header: 'Name', accessor: 'name', Filter: NameFilter }, { Header: 'Age', accessor: 'age' }, { Header: 'Status', accessor: 'status', }, ]; const MyTable = (): ReactElement => { const [filters, setFilters] = React.useState([ { id: 'name', value: '' }, ]); const [nameFilter] = filters; const filteredData = React.useMemo( () => data.filter( ({ name }) => nameFilter === undefined || nameFilter.value === '' || name.includes(nameFilter.value) ), [nameFilter] ); return (
); }; const { queryByText, getByTestId, getByPlaceholderText, } = renderWithTheme(); expect(queryByText('Hanh Le')).toBeInTheDocument(); expect(queryByText('Vinh Tony')).toBeInTheDocument(); expect(queryByText('Hau Dao')).toBeInTheDocument(); // Open name filter dropdown await act(async () => { fireEvent.click(getByTestId('table__header_name__filter')); }); fireEvent.change(getByPlaceholderText('Search...'), { target: { value: 'Vinh' }, }); expect(queryByText('Hanh Le')).not.toBeInTheDocument(); expect(queryByText('Vinh Tony')).toBeInTheDocument(); expect(queryByText('Hau Dao')).not.toBeInTheDocument(); }); }); describe('row expansion', () => { it('allows to expand rows', () => { const expandedRowRenderer = (rowData: RowShape): ReactElement => ( ); const expansionConfig = { expandedRowRenderer, rowExpandable: (rowData: RowShape): boolean => rowData.age >= 18, }; const MyTable = (): ReactElement => { const [expandedRows, setExpandedRows] = React.useState< Record, boolean> >({ 0: true }); return (
{`${rowData.name} - ${rowData.age}`}
); }; const { queryByText, container } = renderWithTheme(); expect(queryByText('Hanh Le')).toBeInTheDocument(); expect(queryByText('20')).toBeInTheDocument(); expect(queryByText('Hanh Le - 20')).toBeInTheDocument(); expect(queryByText('Vinh Tony')).toBeInTheDocument(); expect(queryByText('21')).toBeInTheDocument(); expect(queryByText('Vinh Tony - 21')).not.toBeInTheDocument(); expect(queryByText('Hau Dao')).toBeInTheDocument(); expect(queryByText('19')).toBeInTheDocument(); expect(queryByText('Hau Dao - 19')).not.toBeInTheDocument(); fireEvent.click(container.querySelectorAll('.hero-icon-add').item(0)); expect(queryByText('Vinh Tony - 21')).toBeInTheDocument(); }); }); describe('row selection', () => { it('allows to select rows', () => { const mockSetSelectedRows = jest.fn(); const MyTable = (): ReactElement => { const [selectedRows, setSelectedRows] = React.useState< Record, boolean> >({}); return (
{ setSelectedRows(value); mockSetSelectedRows(value); }} /> ); }; const { container } = renderWithTheme(); // Click on first checkbox will select all rows fireEvent.click( container.querySelectorAll('input[type=checkbox]').item(0) ); expect(mockSetSelectedRows).toHaveBeenCalledWith({ 0: true, 1: true, 2: true, }); mockSetSelectedRows.mockReset(); // Click on first checkbox again will deselect all rows fireEvent.click( container.querySelectorAll('input[type=checkbox]').item(0) ); expect(mockSetSelectedRows).toHaveBeenCalledWith({}); mockSetSelectedRows.mockReset(); // Click on second checkbox will select the first row fireEvent.click( container.querySelectorAll('input[type=checkbox]').item(1) ); expect(mockSetSelectedRows).toHaveBeenCalledWith({ 0: true, }); }); }); describe('row custom color', () => { it('allow to set custom background color', async () => { const rows = { generateClassName: (row: Row): string | undefined => { if (row.original.age > 19) { return 'custom-bg custom-hover-bg'; } if (row.original.name === 'Hau Dao') { return 'custom-bg-yellow'; } return undefined; }, }; const { getByTestId } = renderWithTheme(
); expect(getByTestId('table__row_0')).toHaveClass( 'custom-bg custom-hover-bg' ); expect(getByTestId('table__row_1')).toHaveClass( 'custom-bg custom-hover-bg' ); expect(getByTestId('table__row_2')).toHaveClass('custom-bg-yellow'); expect(getByTestId('table__row_2')).not.toHaveClass( 'custom-bg custom-hover-bg' ); }); }); }); describe('hasFixedLayout', () => { it('returns true', () => { const headers = [ { Header: 'Name', accessor: 'name', width: '50%' }, { Header: 'Age', accessor: 'age' }, { Header: 'Status', accessor: 'status' }, ]; expect(hasFixedLayout(headers)).toBe(true); }); it('returns false', () => { const headers = [ { Header: 'Name', accessor: 'name' }, { Header: 'Age', accessor: 'age' }, { Header: 'Status', accessor: 'status' }, ]; expect(hasFixedLayout(headers)).toBe(false); }); });