Table.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import React from 'react';
  2. import { render, WithRoute } from 'lib/testHelpers';
  3. import Table, {
  4. TableProps,
  5. TimestampCell,
  6. SizeCell,
  7. LinkCell,
  8. TagCell,
  9. } from 'components/common/NewTable';
  10. import { screen, waitFor } from '@testing-library/dom';
  11. import { ColumnDef, Row } from '@tanstack/react-table';
  12. import userEvent from '@testing-library/user-event';
  13. import { formatTimestamp } from 'lib/dateTimeHelpers';
  14. import { act } from '@testing-library/react';
  15. import { ConnectorState, ConsumerGroupState } from 'generated-sources';
  16. const mockedUsedNavigate = jest.fn();
  17. jest.mock('react-router-dom', () => ({
  18. ...jest.requireActual('react-router-dom'),
  19. useNavigate: () => mockedUsedNavigate,
  20. }));
  21. type Datum = typeof data[0];
  22. const data = [
  23. {
  24. timestamp: 1660034383725,
  25. text: 'lorem',
  26. selectable: false,
  27. size: 1234,
  28. tag: ConnectorState.RUNNING,
  29. },
  30. {
  31. timestamp: 1660034399999,
  32. text: 'ipsum',
  33. selectable: true,
  34. size: 3,
  35. tag: ConnectorState.FAILED,
  36. },
  37. {
  38. timestamp: 1660034399922,
  39. text: 'dolor',
  40. selectable: true,
  41. size: 50000,
  42. tag: ConsumerGroupState.EMPTY,
  43. },
  44. {
  45. timestamp: 1660034199922,
  46. text: 'sit',
  47. selectable: false,
  48. size: 1_312_323,
  49. tag: 'some_string',
  50. },
  51. ];
  52. const columns: ColumnDef<Datum>[] = [
  53. {
  54. header: 'DateTime',
  55. accessorKey: 'timestamp',
  56. cell: TimestampCell,
  57. },
  58. {
  59. header: 'Text',
  60. accessorKey: 'text',
  61. cell: ({ getValue }) => (
  62. <LinkCell
  63. value={`${getValue<string | number>()}`}
  64. to={encodeURIComponent(`${getValue<string | number>()}`)}
  65. />
  66. ),
  67. },
  68. {
  69. header: 'Size',
  70. accessorKey: 'size',
  71. cell: SizeCell,
  72. },
  73. {
  74. header: 'Tag',
  75. accessorKey: 'tag',
  76. cell: TagCell,
  77. },
  78. ];
  79. const ExpandedRow: React.FC = () => <div>I am expanded row</div>;
  80. interface Props extends TableProps<Datum> {
  81. path?: string;
  82. }
  83. const renderComponent = (props: Partial<Props> = {}) => {
  84. render(
  85. <WithRoute path="/*">
  86. <Table
  87. columns={columns}
  88. data={data}
  89. renderSubComponent={ExpandedRow}
  90. {...props}
  91. />
  92. </WithRoute>,
  93. { initialEntries: [props.path || ''] }
  94. );
  95. };
  96. describe('Table', () => {
  97. it('renders table', () => {
  98. renderComponent();
  99. expect(screen.getByRole('table')).toBeInTheDocument();
  100. expect(screen.getAllByRole('row').length).toEqual(data.length + 1);
  101. });
  102. it('renders empty table', () => {
  103. renderComponent({ data: [] });
  104. expect(screen.getByRole('table')).toBeInTheDocument();
  105. expect(screen.getAllByRole('row').length).toEqual(2);
  106. expect(screen.getByText('No rows found')).toBeInTheDocument();
  107. });
  108. it('renders empty table with custom message', () => {
  109. const emptyMessage = 'Super custom message';
  110. renderComponent({ data: [], emptyMessage });
  111. expect(screen.getByRole('table')).toBeInTheDocument();
  112. expect(screen.getAllByRole('row').length).toEqual(2);
  113. expect(screen.getByText(emptyMessage)).toBeInTheDocument();
  114. });
  115. it('renders SizeCell', () => {
  116. renderComponent();
  117. expect(screen.getByText('1KB')).toBeInTheDocument();
  118. expect(screen.getByText('3Bytes')).toBeInTheDocument();
  119. expect(screen.getByText('49KB')).toBeInTheDocument();
  120. expect(screen.getByText('1MB')).toBeInTheDocument();
  121. });
  122. it('renders TimestampCell', () => {
  123. renderComponent();
  124. expect(
  125. screen.getByText(formatTimestamp(data[0].timestamp))
  126. ).toBeInTheDocument();
  127. });
  128. describe('LinkCell', () => {
  129. it('renders link', () => {
  130. renderComponent();
  131. expect(screen.getByRole('link', { name: 'lorem' })).toBeInTheDocument();
  132. });
  133. it('link click stops propagation', () => {
  134. const onRowClick = jest.fn();
  135. renderComponent({ onRowClick });
  136. const link = screen.getByRole('link', { name: 'lorem' });
  137. userEvent.click(link);
  138. expect(onRowClick).not.toHaveBeenCalled();
  139. });
  140. });
  141. describe('ExpanderCell', () => {
  142. it('renders button', () => {
  143. renderComponent({ getRowCanExpand: () => true });
  144. const btns = screen.getAllByRole('button', { name: 'Expand row' });
  145. expect(btns.length).toEqual(data.length);
  146. expect(screen.queryByText('I am expanded row')).not.toBeInTheDocument();
  147. userEvent.click(btns[2]);
  148. expect(screen.getByText('I am expanded row')).toBeInTheDocument();
  149. userEvent.click(btns[0]);
  150. expect(screen.getAllByText('I am expanded row').length).toEqual(2);
  151. });
  152. it('does not render button', () => {
  153. renderComponent({ getRowCanExpand: () => false });
  154. expect(
  155. screen.queryByRole('button', { name: 'Expand row' })
  156. ).not.toBeInTheDocument();
  157. expect(screen.queryByText('I am expanded row')).not.toBeInTheDocument();
  158. });
  159. });
  160. it('renders TagCell', () => {
  161. renderComponent();
  162. expect(screen.getByText(data[0].tag)).toBeInTheDocument();
  163. expect(screen.getByText(data[1].tag)).toBeInTheDocument();
  164. expect(screen.getByText(data[2].tag)).toBeInTheDocument();
  165. expect(screen.getByText(data[3].tag)).toBeInTheDocument();
  166. });
  167. describe('Pagination', () => {
  168. it('does not render page buttons', () => {
  169. renderComponent();
  170. expect(
  171. screen.queryByRole('button', { name: 'Next' })
  172. ).not.toBeInTheDocument();
  173. });
  174. it('renders page buttons', async () => {
  175. renderComponent({ path: '?perPage=1' });
  176. // Check it renders header row and only one data row
  177. expect(screen.getAllByRole('row').length).toEqual(2);
  178. expect(screen.getByText('lorem')).toBeInTheDocument();
  179. // Check it renders page buttons
  180. const firstBtn = screen.getByRole('button', { name: '⇤' });
  181. const prevBtn = screen.getByRole('button', { name: '← Previous' });
  182. const nextBtn = screen.getByRole('button', { name: 'Next →' });
  183. const lastBtn = screen.getByRole('button', { name: '⇥' });
  184. expect(firstBtn).toBeInTheDocument();
  185. expect(firstBtn).toBeDisabled();
  186. expect(prevBtn).toBeInTheDocument();
  187. expect(prevBtn).toBeDisabled();
  188. expect(nextBtn).toBeInTheDocument();
  189. expect(nextBtn).toBeEnabled();
  190. expect(lastBtn).toBeInTheDocument();
  191. expect(lastBtn).toBeEnabled();
  192. userEvent.click(nextBtn);
  193. expect(screen.getByText('ipsum')).toBeInTheDocument();
  194. expect(prevBtn).toBeEnabled();
  195. expect(firstBtn).toBeEnabled();
  196. userEvent.click(lastBtn);
  197. expect(screen.getByText('sit')).toBeInTheDocument();
  198. expect(lastBtn).toBeDisabled();
  199. expect(nextBtn).toBeDisabled();
  200. userEvent.click(prevBtn);
  201. expect(screen.getByText('dolor')).toBeInTheDocument();
  202. userEvent.click(firstBtn);
  203. expect(screen.getByText('lorem')).toBeInTheDocument();
  204. });
  205. describe('Go To page', () => {
  206. const getGoToPageInput = () =>
  207. screen.getByRole('spinbutton', { name: 'Go to page:' });
  208. beforeEach(() => {
  209. renderComponent({ path: '?perPage=1' });
  210. });
  211. it('renders Go To page', () => {
  212. const goToPage = getGoToPageInput();
  213. expect(goToPage).toBeInTheDocument();
  214. expect(goToPage).toHaveValue(1);
  215. });
  216. it('updates page on Go To page change', () => {
  217. const goToPage = getGoToPageInput();
  218. userEvent.clear(goToPage);
  219. userEvent.type(goToPage, '2');
  220. expect(goToPage).toHaveValue(2);
  221. expect(screen.getByText('ipsum')).toBeInTheDocument();
  222. });
  223. it('does not update page on Go To page change if page is out of range', () => {
  224. const goToPage = getGoToPageInput();
  225. userEvent.type(goToPage, '5');
  226. expect(goToPage).toHaveValue(15);
  227. expect(screen.getByText('No rows found')).toBeInTheDocument();
  228. });
  229. it('does not update page on Go To page change if page is not a number', () => {
  230. const goToPage = getGoToPageInput();
  231. userEvent.type(goToPage, 'abc');
  232. expect(goToPage).toHaveValue(1);
  233. });
  234. });
  235. });
  236. describe('Sorting', () => {
  237. it('sort rows', async () => {
  238. await act(() =>
  239. renderComponent({
  240. path: '/?sortBy=text&&sortDirection=desc',
  241. enableSorting: true,
  242. })
  243. );
  244. expect(screen.getAllByRole('row').length).toEqual(data.length + 1);
  245. const th = screen.getByRole('columnheader', { name: 'Text' });
  246. expect(th).toBeInTheDocument();
  247. let rows = [];
  248. // Check initial sort order by text column is descending
  249. rows = screen.getAllByRole('row');
  250. expect(rows[4].textContent?.indexOf('dolor')).toBeGreaterThan(-1);
  251. expect(rows[3].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);
  252. expect(rows[2].textContent?.indexOf('lorem')).toBeGreaterThan(-1);
  253. expect(rows[1].textContent?.indexOf('sit')).toBeGreaterThan(-1);
  254. // Disable sorting by text column
  255. await waitFor(() => userEvent.click(th));
  256. rows = screen.getAllByRole('row');
  257. expect(rows[1].textContent?.indexOf('lorem')).toBeGreaterThan(-1);
  258. expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);
  259. expect(rows[3].textContent?.indexOf('dolor')).toBeGreaterThan(-1);
  260. expect(rows[4].textContent?.indexOf('sit')).toBeGreaterThan(-1);
  261. // Sort by text column ascending
  262. await waitFor(() => userEvent.click(th));
  263. rows = screen.getAllByRole('row');
  264. expect(rows[1].textContent?.indexOf('dolor')).toBeGreaterThan(-1);
  265. expect(rows[2].textContent?.indexOf('ipsum')).toBeGreaterThan(-1);
  266. expect(rows[3].textContent?.indexOf('lorem')).toBeGreaterThan(-1);
  267. expect(rows[4].textContent?.indexOf('sit')).toBeGreaterThan(-1);
  268. });
  269. });
  270. describe('Row Selecting', () => {
  271. beforeEach(() => {
  272. renderComponent({
  273. enableRowSelection: (row: Row<Datum>) => row.original.selectable,
  274. batchActionsBar: () => <div>I am Action Bar</div>,
  275. });
  276. });
  277. it('renders selectable rows', () => {
  278. expect(screen.getAllByRole('row').length).toEqual(data.length + 1);
  279. const checkboxes = screen.getAllByRole('checkbox');
  280. expect(checkboxes.length).toEqual(data.length + 1);
  281. expect(checkboxes[1]).toBeDisabled();
  282. expect(checkboxes[2]).toBeEnabled();
  283. expect(checkboxes[3]).toBeEnabled();
  284. expect(checkboxes[4]).toBeDisabled();
  285. });
  286. it('renders action bar', () => {
  287. expect(screen.getAllByRole('row').length).toEqual(data.length + 1);
  288. expect(screen.queryByText('I am Action Bar')).not.toBeInTheDocument();
  289. const checkboxes = screen.getAllByRole('checkbox');
  290. expect(checkboxes.length).toEqual(data.length + 1);
  291. userEvent.click(checkboxes[2]);
  292. expect(screen.getByText('I am Action Bar')).toBeInTheDocument();
  293. });
  294. });
  295. describe('Clickable Row', () => {
  296. const onRowClick = jest.fn();
  297. it('handles onRowClick', () => {
  298. renderComponent({ onRowClick });
  299. const rows = screen.getAllByRole('row');
  300. expect(rows.length).toEqual(data.length + 1);
  301. userEvent.click(rows[1]);
  302. expect(onRowClick).toHaveBeenCalledTimes(1);
  303. });
  304. it('does nothing unless onRowClick is provided', () => {
  305. renderComponent();
  306. const rows = screen.getAllByRole('row');
  307. expect(rows.length).toEqual(data.length + 1);
  308. userEvent.click(rows[1]);
  309. });
  310. it('does not handle onRowClick if enableRowSelection', () => {
  311. renderComponent({ onRowClick, enableRowSelection: true });
  312. const rows = screen.getAllByRole('row');
  313. expect(rows.length).toEqual(data.length + 1);
  314. userEvent.click(rows[1]);
  315. expect(onRowClick).not.toHaveBeenCalled();
  316. });
  317. it('does not handle onRowClick if expandable rows', () => {
  318. renderComponent({ onRowClick, getRowCanExpand: () => true });
  319. const rows = screen.getAllByRole('row');
  320. expect(rows.length).toEqual(data.length + 1);
  321. userEvent.click(rows[1]);
  322. expect(onRowClick).not.toHaveBeenCalled();
  323. });
  324. });
  325. });