From c593e243782dd73abf7f0aeedff501d62d653240 Mon Sep 17 00:00:00 2001 From: Kirill Kharitonov Date: Wed, 31 Jan 2024 19:06:01 +0300 Subject: [PATCH] test(Table): added unit tests to Table components --- .../Table/__tests__/Table.hocs.test.ts | 137 ++++++++ src/components/Table/__tests__/Table.test.tsx | 217 +++++++++++- .../__tests__/Table.withTableSettings.test.ts | 246 ++++++++++++++ .../__snapshots__/Table.hocs.test.ts.snap | 300 +++++++++++++++++ .../__snapshots__/Table.test.tsx.snap | 311 ++++++++++++++++++ 5 files changed, 1209 insertions(+), 2 deletions(-) create mode 100644 src/components/Table/__tests__/Table.hocs.test.ts create mode 100644 src/components/Table/__tests__/Table.withTableSettings.test.ts create mode 100644 src/components/Table/__tests__/__snapshots__/Table.hocs.test.ts.snap create mode 100644 src/components/Table/__tests__/__snapshots__/Table.test.tsx.snap diff --git a/src/components/Table/__tests__/Table.hocs.test.ts b/src/components/Table/__tests__/Table.hocs.test.ts new file mode 100644 index 0000000000..d6d858122e --- /dev/null +++ b/src/components/Table/__tests__/Table.hocs.test.ts @@ -0,0 +1,137 @@ +import React from 'react'; + +import {render} from '@testing-library/react'; + +import {Table, type TableProps} from '../Table'; +import { + type WithTableActionsProps, + type WithTableSelectionProps, + type WithTableSettingsProps, + type WithTableSortingProps, + withTableActions, + withTableCopy, + withTableSelection, + withTableSettings, + withTableSorting, +} from '../hoc'; + +interface Model { + disabled: boolean; +} + +function getTextContent(html = '') { + return html.replace(/uniq\d+/g, ''); +} + +describe('Table HOCs tests', () => { + it('withTableActions should match snapshot', () => { + const Table1 = withTableActions(Table); + + type Props = TableProps & WithTableActionsProps; + const props: Props = { + data: [{disabled: false}, {disabled: true}], + columns: [{id: 'name'}], + isRowDisabled: ({disabled}) => disabled, + getRowActions: () => [], + }; + const {container} = render(React.createElement(Table1, props)); + + expect(container).toMatchSnapshot(); + }); + + it('all HOCs should match snapshot', () => { + const Table1 = withTableSorting( + withTableSettings(withTableCopy(withTableActions(withTableSelection(Table)))), + ); + + type Props = TableProps & + WithTableActionsProps & + WithTableSelectionProps & + WithTableSettingsProps & + WithTableSortingProps; + + const props: Props = { + data: [{disabled: false}, {disabled: true}], + columns: [{id: 'name'}], + isRowDisabled: ({disabled}) => disabled, + selectedIds: [], + onSelectionChange: () => {}, + getRowActions: () => [], + updateSettings: () => Promise.resolve(), + settings: [], + }; + const {container} = render(React.createElement(Table1, props)); + + expect(container).toMatchSnapshot(); + }); + + it('using withTableActions and withTableSelection should not depend of order', () => { + const Table1 = withTableActions(withTableSelection(Table)); + const Table2 = withTableSelection(withTableActions(Table)); + + type Props = TableProps & + WithTableActionsProps & + WithTableSelectionProps; + const props: Props = { + data: [{disabled: false}, {disabled: true}], + columns: [{id: 'name'}], + isRowDisabled: ({disabled}) => disabled, + selectedIds: [], + onSelectionChange: () => {}, + getRowActions: () => [], + }; + const {container: container1} = render(React.createElement(Table1, props)); + const {container: container2} = render(React.createElement(Table2, props)); + + expect(getTextContent(container1.outerHTML)).toEqual(getTextContent(container2.outerHTML)); + }); + + it('using withTableActions and withTableSorting should not depend of order', () => { + const Table1 = withTableActions(withTableSorting(Table)); + const Table2 = withTableSorting(withTableActions(Table)); + + type Props = TableProps & WithTableActionsProps & WithTableSortingProps; + const props: Props = { + data: [{disabled: false}, {disabled: true}], + columns: [{id: 'name'}], + isRowDisabled: ({disabled}) => disabled, + getRowActions: () => [], + }; + const {container: container1} = render(React.createElement(Table1, props)); + const {container: container2} = render(React.createElement(Table2, props)); + + expect(getTextContent(container1.outerHTML)).toEqual(getTextContent(container2.outerHTML)); + }); + + it('using all HOCs should not depend of order', () => { + const Table1 = withTableSorting( + withTableSettings( + withTableCopy(withTableActions(withTableSelection(Table))), + ), + ); + const Table2 = withTableSelection( + withTableActions(withTableCopy(withTableSettings(withTableSorting(Table)))), + ); + + type Props = TableProps & + WithTableActionsProps & + WithTableSelectionProps & + WithTableSettingsProps & + WithTableSortingProps; + + const props: Props = { + data: [{disabled: false}, {disabled: true}], + columns: [{id: 'name'}], + isRowDisabled: ({disabled}) => disabled, + selectedIds: [], + onSelectionChange: () => {}, + getRowActions: () => [], + updateSettings: () => Promise.resolve(), + settings: [], + }; + const {container: container1} = render(React.createElement(Table1, props)); + const {container: container2} = render(React.createElement(Table2, props)); + + expect(getTextContent(container1.outerHTML)).toEqual(getTextContent(container2.outerHTML)); + }); +}); diff --git a/src/components/Table/__tests__/Table.test.tsx b/src/components/Table/__tests__/Table.test.tsx index 907c4b7670..db8ed9cf9c 100644 --- a/src/components/Table/__tests__/Table.test.tsx +++ b/src/components/Table/__tests__/Table.test.tsx @@ -3,9 +3,9 @@ import React from 'react'; import {render, screen, within} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import {Table, TableProps} from '../Table'; +import {Table, TableColumnConfig, TableProps} from '../Table'; -import {columns, data} from './utils'; +import {DataItem, columns, data} from './utils'; const qaId = 'table-component'; @@ -19,6 +19,75 @@ describe('Table', () => { expect(rows.length).toBe(data.length + 1); }); + test('should render empty state', () => { + const props: TableProps = {data: [], columns: [{id: 'name'}]}; + const {container} = render(React.createElement>(Table, props)); + + expect(container).toMatchSnapshot(); + }); + + test('should correctly apply `align` prop', () => { + const props: TableProps<{}> = { + data: [{}], + columns: [ + {id: 'name1', align: 'left'}, + {id: 'name2', align: 'right'}, + {id: 'name3', align: 'center'}, + ], + }; + const {container} = render(React.createElement>(Table, props)); + + expect(container).toMatchSnapshot(); + }); + + test('should correctly apply `width` prop', () => { + const props: TableProps<{}> = { + data: [{}], + columns: [ + {id: 'name1', width: 100}, + {id: 'name2', width: 200}, + {id: 'name3', width: 300}, + ], + }; + const {container} = render(React.createElement>(Table, props)); + + expect(container).toMatchSnapshot(); + }); + + test('should correctly apply `primary` prop', () => { + const props: TableProps<{}> = { + data: [{}], + columns: [{id: 'name1', primary: true}, {id: 'name2', primary: false}, {id: 'name3'}], + }; + const {container} = render(React.createElement>(Table, props)); + + expect(container).toMatchSnapshot(); + }); + + test('should correctly add custom row classnames', () => { + const props: TableProps<{}> = { + data: [{}], + columns: [{id: 'name'}], + getRowClassNames: () => ['custom-row', 'custom-row_mod'], + }; + const {container} = render(React.createElement>(Table, props)); + + expect(container).toMatchSnapshot(); + }); + + test('should correctly add disabled classname', () => { + const props: TableProps<{disabled: boolean}> = { + data: [{disabled: false}, {disabled: true}], + columns: [{id: 'name'}], + isRowDisabled: ({disabled}) => disabled, + }; + const {container} = render( + React.createElement>(Table, props), + ); + + expect(container).toMatchSnapshot(); + }); + test.each(new Array<'top' | 'middle'>('top', 'middle'))( 'render with given "%s" vertical align', (align) => { @@ -185,4 +254,148 @@ describe('Table', () => { expect(row.className.includes('yc-table__row_disabled')).toBe(expectedFlag); }); }); + + describe('getRowId static method', () => { + test('should return index by default', () => { + const props: TableProps = {data, columns: []}; + + expect(Table.getRowId(props, data[0])).toBe('0'); + expect(Table.getRowId(props, data[1])).toBe('1'); + expect(Table.getRowId(props, data[2])).toBe('2'); + }); + + test('should use prop as function', () => { + const getRowIdMock = jest.fn((item: DataItem) => '__id__' + item.name); + const props: TableProps = {data, columns: [], getRowId: getRowIdMock}; + const id = Table.getRowId(props, data[0]); + + expect(getRowIdMock).toBeCalled(); + expect(id).toBe('__id__' + data[0].name); + }); + + test('should call function with correct arguments', () => { + const getRowIdMock = jest.fn(); + const props: TableProps = {data, columns: [], getRowId: getRowIdMock}; + + Table.getRowId(props, data[0]); + Table.getRowId(props, data[1]); + Table.getRowId(props, data[2]); + + expect(getRowIdMock.mock.calls.length).toBe(3); + expect(getRowIdMock.mock.calls[0]).toEqual([data[0], 0]); + expect(getRowIdMock.mock.calls[1]).toEqual([data[1], 1]); + expect(getRowIdMock.mock.calls[2]).toEqual([data[2], 2]); + }); + + test('should use prop as object key', () => { + const props: TableProps = {data, columns: [], getRowId: 'name'}; + + expect(Table.getRowId(props, data[0])).toBe('Nomlanga Compton'); + expect(Table.getRowId(props, data[1])).toBe('Paul Hatfield'); + expect(Table.getRowId(props, data[2])).toBe('Phelan Daniel'); + }); + + test('should fallback to index on prop as object key', () => { + const props: TableProps = {data, columns: [], getRowId: 'ts'}; + + expect(Table.getRowId(props, data[0])).toBe('0'); + expect(Table.getRowId(props, data[1])).toBe('1'); + expect(Table.getRowId(props, data[2])).toBe('2'); + }); + }); + + describe('getHeadCellContent static method', () => { + test('should return id prop value by default', () => { + const column: TableColumnConfig = {id: 'name'}; + const {container} = render(Table.getHeadCellContent(column) as React.ReactElement); + + expect(container).toHaveTextContent('name'); + }); + + test('should use name prop as function', () => { + const nameMock = jest.fn(() => '__name__'); + const column: TableColumnConfig = {id: 'name', name: nameMock}; + const {container} = render(Table.getHeadCellContent(column) as React.ReactElement); + + expect(nameMock).toBeCalled(); + expect(container).toHaveTextContent('__name__'); + }); + + test('should call function with correct arguments', () => { + const nameMock = jest.fn(() => '__name__'); + const column: TableColumnConfig = {id: 'name', name: nameMock}; + Table.getHeadCellContent(column); + + expect(nameMock.mock.calls.length).toBe(1); + expect(nameMock.mock.calls[0]).toEqual([]); + }); + + test('should use name prop as string', () => { + const column: TableColumnConfig = {id: 'name', name: '__name__'}; + const {container} = render(Table.getHeadCellContent(column) as React.ReactElement); + + expect(container).toHaveTextContent('__name__'); + }); + }); + + describe('getBodyCellContent static method', () => { + test('should return dash by default', () => { + const column: TableColumnConfig = {id: '__unknown__'}; + const content = Table.getBodyCellContent(column, data[0], 0); + + expect(content).toBe('\u2014'); + }); + + test('should return placeholder on empty value', () => { + const column: TableColumnConfig = {id: 'name', placeholder: '-'}; + const items = [{id: 'asdf'}, {name: null}, {name: undefined}, {name: ''}]; + + expect(Table.getBodyCellContent(column, items[0], 0)).toBe('-'); + expect(Table.getBodyCellContent(column, items[0], 1)).toBe('-'); + expect(Table.getBodyCellContent(column, items[0], 2)).toBe('-'); + expect(Table.getBodyCellContent(column, items[0], 3)).toBe('-'); + }); + + test('should use template prop as function', () => { + const templateMock = jest.fn(() => '__content__'); + const column: TableColumnConfig = {id: 'name', template: templateMock}; + const content = Table.getBodyCellContent(column, data[0], 0); + + expect(templateMock).toBeCalled(); + expect(content).toBe('__content__'); + }); + + test('should call function with correct arguments', () => { + const templateMock = jest.fn(); + const column: TableColumnConfig = {id: 'name', template: templateMock}; + Table.getBodyCellContent(column, data[0], 0); + Table.getBodyCellContent(column, data[1], 1); + Table.getBodyCellContent(column, data[2], 2); + + expect(templateMock.mock.calls.length).toBe(3); + expect(templateMock.mock.calls[0]).toEqual([data[0], 0]); + expect(templateMock.mock.calls[1]).toEqual([data[1], 1]); + expect(templateMock.mock.calls[2]).toEqual([data[2], 2]); + }); + + test('should use template prop as object key', () => { + const column1: TableColumnConfig = {id: 'name', template: 'count'}; + const column2: TableColumnConfig = {id: 'name', template: 'city'}; + + expect(Table.getBodyCellContent(column1, data[0], 0)).toBe(82); + expect(Table.getBodyCellContent(column1, data[1], 1)).toBe(51); + expect(Table.getBodyCellContent(column1, data[2], 2)).toBe(10); + expect(Table.getBodyCellContent(column2, data[0], 0)).toBe('Erli'); + expect(Table.getBodyCellContent(column2, data[1], 1)).toBe('Campitello di Fassa'); + expect(Table.getBodyCellContent(column2, data[2], 2)).toBe('Meugliano'); + }); + + test('should use id prop as object key', () => { + const column: TableColumnConfig = {id: 'name'}; + + expect(Table.getBodyCellContent(column, data[0], 0)).toBe('Nomlanga Compton'); + expect(Table.getBodyCellContent(column, data[1], 1)).toBe('Paul Hatfield'); + expect(Table.getBodyCellContent(column, data[2], 2)).toBe('Phelan Daniel'); + }); + }); }); diff --git a/src/components/Table/__tests__/Table.withTableSettings.test.ts b/src/components/Table/__tests__/Table.withTableSettings.test.ts new file mode 100644 index 0000000000..9008b8562f --- /dev/null +++ b/src/components/Table/__tests__/Table.withTableSettings.test.ts @@ -0,0 +1,246 @@ +import type {TableColumnConfig} from '../Table'; +import {enhanceSystemColumn} from '../hoc/withTableActions/withTableActions'; +import {selectionColumnId} from '../hoc/withTableSelection/withTableSelection'; +import { + TableSettingsData, + filterColumns, + getActualItems, + getColumnStringTitle, +} from '../hoc/withTableSettings/withTableSettings'; + +const columns: TableColumnConfig[] = [ + { + id: 'id', + }, + { + id: 'name', + }, + { + id: 'description', + meta: { + sort: true, + }, + }, +]; + +const columnsWithSystem: TableColumnConfig[] = enhanceSystemColumn( + [ + { + id: selectionColumnId, + name: 'selection', + }, + ...columns, + ], + (systemColumn) => { + systemColumn.template = () => 'template'; + return systemColumn; + }, +); + +function ids(items: Array<{id: string}>) { + return items.map(({id}) => id); +} + +describe('withTableSettings getColumnStringTitle', () => { + it('should use id if name is not defined', () => { + expect(getColumnStringTitle({id: 'name'})).toEqual('name'); + }); + + it('should use name if it is string', () => { + expect(getColumnStringTitle({id: 'name', name: 'First Name'})).toEqual('First Name'); + }); + + it('should use meta._originalName if it is a string', () => { + expect( + getColumnStringTitle({ + id: 'name', + meta: { + _originalName: 'Result', + }, + }), + ).toEqual('Result'); + }); +}); + +describe('withTableSettings getActualItems', () => { + it('should filter system columns', () => { + expect(getActualItems(columnsWithSystem, []).map((column) => column.id)).toEqual( + columns.map((column) => column.id), + ); + }); + + it('should return recently added columns', () => { + const settings: TableSettingsData = [ + { + id: 'id', + isSelected: true, + }, + { + id: 'name', + isSelected: false, + }, + { + id: 'description', + isSelected: true, + }, + ]; + const updatedColumns = [...columns, {id: 'os'}]; + const actualSettings = getActualItems(updatedColumns, settings).map((column) => column.id); + expect(actualSettings).toEqual(updatedColumns.map((column) => column.id)); + }); + + it('should respect selectedByDefault in recently added columns', () => { + const settings: TableSettingsData = [ + { + id: 'id', + isSelected: true, + }, + { + id: 'name', + isSelected: false, + }, + { + id: 'description', + isSelected: true, + }, + ]; + const updatedColumns = [ + ...columns, + { + id: 'os', + meta: { + selectedByDefault: true, + }, + }, + { + id: 'osx', + meta: { + selectedByDefault: false, + }, + }, + ]; + const osSettings = getActualItems(updatedColumns, settings).find(({id}) => id === 'os'); + const osxSettings = getActualItems(updatedColumns, settings).find(({id}) => id === 'osx'); + expect(osSettings).toBeDefined(); + expect(osxSettings).toBeDefined(); + // @ts-ignore + expect(osSettings.isSelected).toBe(true); + // @ts-ignore + expect(osxSettings.isSelected).toBe(false); + }); + + it('should return columns when no settings provided', () => { + expect(ids(getActualItems(columns, []))).toEqual(ids(columns)); + }); + + it('should ignore settings which is not exists in columns', () => { + const settings: TableSettingsData = [ + { + id: 'description', + isSelected: true, + }, + { + id: 'name', + isSelected: true, + }, + { + id: 'id', + isSelected: true, + }, + { + id: 'removed', + isSelected: true, + }, + ]; + expect(ids(getActualItems(columns, settings))).toEqual( + ids([settings[0], settings[1], settings[2]]), + ); + }); +}); + +describe('withTableSettings filterColumns', () => { + it('should return only selected columns from settings', () => { + const settings: TableSettingsData = [ + { + id: 'id', + isSelected: true, + }, + { + id: 'name', + isSelected: false, + }, + { + id: 'description', + isSelected: true, + }, + ]; + expect(filterColumns(columns, settings)).toEqual([columns[0], columns[2]]); + }); + + it('should use columns order from settings', () => { + const settings: TableSettingsData = [ + { + id: 'description', + isSelected: true, + }, + { + id: 'name', + isSelected: true, + }, + { + id: 'id', + isSelected: true, + }, + ]; + expect(filterColumns(columns, settings)).toEqual([columns[2], columns[1], columns[0]]); + }); + + it('should ignore system column for selection', () => { + const enhancedColumns = [ + { + id: '_selection', + name: 'for selection', + }, + ...columns, + ]; + const settings: TableSettingsData = [ + { + id: 'description', + isSelected: true, + }, + { + id: 'name', + isSelected: true, + }, + { + id: 'id', + isSelected: true, + }, + ]; + expect(filterColumns(enhancedColumns, settings)).toEqual([ + enhancedColumns[0], + enhancedColumns[3], + enhancedColumns[2], + enhancedColumns[1], + ]); + }); + + it('should not filter system columns when settings provided', () => { + expect( + filterColumns(columnsWithSystem, [ + { + id: 'id', + isSelected: true, + }, + { + id: 'name', + isSelected: false, + }, + { + id: 'description', + isSelected: false, + }, + ]), + ).toEqual([columnsWithSystem[0], columnsWithSystem[1], columnsWithSystem[4]]); + }); +}); diff --git a/src/components/Table/__tests__/__snapshots__/Table.hocs.test.ts.snap b/src/components/Table/__tests__/__snapshots__/Table.hocs.test.ts.snap new file mode 100644 index 0000000000..8260ca57d9 --- /dev/null +++ b/src/components/Table/__tests__/__snapshots__/Table.hocs.test.ts.snap @@ -0,0 +1,300 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table HOCs tests all HOCs should match snapshot 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + +
+ + + name + +
+
+
+ +
+
+
+
+ + + — + +
+ + + — + +
+
+
+`; + +exports[`Table HOCs tests withTableActions should match snapshot 1`] = ` +
+
+ + + + + + + + + + + + + + +
+ name + +
+ — + +
+ — + +
+
+
+`; diff --git a/src/components/Table/__tests__/__snapshots__/Table.test.tsx.snap b/src/components/Table/__tests__/__snapshots__/Table.test.tsx.snap new file mode 100644 index 0000000000..4bb5d4b58a --- /dev/null +++ b/src/components/Table/__tests__/__snapshots__/Table.test.tsx.snap @@ -0,0 +1,311 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table should correctly add custom row classnames 1`] = ` +
+
+ + + + + + + + + + + +
+ name +
+ — +
+
+
+`; + +exports[`Table should correctly add disabled classname 1`] = ` +
+
+ + + + + + + + + + + + + + +
+ name +
+ — +
+ — +
+
+
+`; + +exports[`Table should correctly apply \`align\` prop 1`] = ` +
+
+ + + + + + + + + + + + + + + +
+ name1 + + name2 + + name3 +
+ — + + — + + — +
+
+
+`; + +exports[`Table should correctly apply \`primary\` prop 1`] = ` +
+
+ + + + + + + + + + + + + + + +
+ name1 + + name2 + + name3 +
+ — + + — + + — +
+
+
+`; + +exports[`Table should correctly apply \`width\` prop 1`] = ` +
+
+ + + + + + + + + + + + + + + +
+ name1 + + name2 + + name3 +
+ — + + — + + — +
+
+
+`; + +exports[`Table should render empty state 1`] = ` +
+
+ + + + + + + + + + + +
+ name +
+ No data +
+
+
+`;