import { SELECTION_COLUMN_ID } from '../../constants' import type { ApplyPreferences } from '../actions' import type { GridColumnState } from './column' import reducerConfig, { selectColumn, selectColumnCount, selectTotalColumnWidth, selectColumnPreference, selectIsTreeGrid, selectGroupIds, selectHeaderHierarchy, generateSelectors, selectHiddenIds, selectAreHeadersHidden, selectHeaderHeight, } from './column' const { reducer: columnsReducer } = reducerConfig const getStateMock = ( state: Partial = {} ): GridColumnState => ({ ...reducerConfig.initialState, ids: ['1', '2', '3'], preferences: { hiddenIds: null, sortedIds: ['2', '1', '3'], entities: {}, }, entities: { 1: { id: '1', actualWidth: 150, data: { id: '1', label: '1', width: 150, }, }, 2: { id: '2', actualWidth: 200, data: { id: '2', label: '2', width: 200, minWidth: 125, maxWidth: 300, }, }, 3: { id: '3', actualWidth: 100, data: { id: '3', label: '3', width: 100, }, }, }, ...state, }) describe('columnsReducer', () => { describe('unknown action', () => { it('should not modify the state', () => { const state = getStateMock() const newState = columnsReducer(state, { type: 'nope' } as any) expect(state).toBe(newState) }) }) describe('applyProps', () => { const COLUMN_ONE = { id: 'firstName', label: 'First', sortEmptyAs: 'high', movable: true, } const COLUMN_TWO = { id: 'lastName', label: 'Last', width: 128, sortStrategy: 'natural', movable: true, } const COLUMN_THREE = { id: 'email', label: 'Email' } const GROUP_ONE = { id: 'group1', label: 'Group', movable: true, children: ['firstName', 'lastName'], } const GROUP_TWO = { id: 'group2', label: 'Group 2', movable: true, children: ['group1'], } let state: GridColumnState describe('loading new data', () => { beforeEach(() => { state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO], rows: [], }, }) }) it('should store ids', () => { expect(state.ids).toEqual(['firstName', 'lastName']) }) it('should store an entity for each column', () => { expect(state.entities.firstName.data).toBe(COLUMN_ONE) expect(state.entities.lastName.data).toBe(COLUMN_TWO) }) it('should calculate an actual width', () => { expect(state.entities.firstName.actualWidth).toBe(150) expect(state.entities.lastName.actualWidth).toBe(128) }) it('should set that the grid is a normal grid', () => { expect(state.role).toBe('grid') }) it('should store sort information', () => { expect(state.sortEntities).toEqual({ firstName: { sortStrategy: 'fast', sortEmptyAs: 'high', }, lastName: { sortStrategy: 'natural', }, }) }) }) describe('loading new data (tree)', () => { describe('with default indentation', () => { beforeEach(() => { state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: 'tree', label: 'Tree', tree: true }, COLUMN_ONE, COLUMN_TWO, ], rows: [], }, }) }) it('should set that the grid is a tree grid', () => { expect(state.role).toBe('treegrid') }) it('should set the indentation to large', () => { expect(state.treeIndentSize).toBe('large') }) }) describe('with small indentation', () => { beforeEach(() => { state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: 'tree', label: 'Tree', tree: true, treeIndentSize: 'small', }, COLUMN_ONE, COLUMN_TWO, ], rows: [], }, }) }) it('should set that the grid is a tree grid', () => { expect(state.role).toBe('treegrid') }) it('should set the indentation to large', () => { expect(state.treeIndentSize).toBe('small') }) }) }) describe('updating other prop', () => { let initialState: GridColumnState beforeEach(() => { const columns = [COLUMN_ONE, COLUMN_TWO] const rows = [] as any initialState = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns, rows, }, }) state = columnsReducer(initialState, { type: 'applyProps', payload: { columns, rows, selectionMode: 'none', }, }) }) it('should not process the columns at all', () => { expect(initialState.entities).toBe(state.entities) }) }) describe('column groups', () => { describe('loading new data', () => { beforeEach(() => { state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO], columnGroups: [GROUP_ONE], rows: [], }, }) }) it('should store children and group in entities if grouped columns', () => { expect(state.entities.firstName.data).toBe(COLUMN_ONE) expect(state.entities.lastName.data).toBe(COLUMN_TWO) expect(state.entities.group1.data).toBe(GROUP_ONE) expect(state.groupIds).toEqual(['group1']) }) it('should infer sticky state from parents', () => { state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ COLUMN_ONE, { ...COLUMN_TWO, sticky: 'right' }, ], columnGroups: [ { ...GROUP_ONE, sticky: 'right' }, { ...GROUP_TWO, sticky: 'left' }, ], rows: [], }, }) expect(state.entities[COLUMN_ONE.id].data.sticky).toBe( 'left' ) expect(state.entities[COLUMN_TWO.id].data.sticky).toBe( 'left' ) expect(state.entities[GROUP_ONE.id].data.sticky).toBe( 'left' ) }) it('should throw away preferences if groups are split', () => { state = columnsReducer( getStateMock({ preferences: { hiddenIds: [], sortedIds: ['firstName', 'email', 'lastName'], entities: {}, }, }), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO, COLUMN_THREE], columnGroups: [GROUP_ONE], rows: [], }, } ) expect(state.preferences.sortedIds).toBeNull() }) }) describe('updating group prop', () => { let initialState: GridColumnState beforeEach(() => { const columns = [COLUMN_ONE, COLUMN_TWO] const columnGroups = [GROUP_ONE] const rows = [] as any initialState = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns, rows, columnGroups, }, }) state = columnsReducer(initialState, { type: 'applyProps', payload: { columns, rows, columnGroups: [GROUP_ONE, GROUP_TWO], }, }) }) it('should process new groups', () => { expect(state.groupIds).toEqual(['group1', 'group2']) expect(state.entities.group1.data).toBe(GROUP_ONE) expect(state.entities.group2.data).toBe(GROUP_TWO) }) }) describe('updating other prop', () => { let initialState: GridColumnState beforeEach(() => { const columns = [COLUMN_ONE, COLUMN_TWO] const columnGroups = [GROUP_ONE] const rows = [] as any initialState = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns, rows, columnGroups, }, }) state = columnsReducer(initialState, { type: 'applyProps', payload: { columns, rows, columnGroups, selectionMode: 'none', }, }) }) it('should not process the columns at all', () => { expect(initialState.entities).toBe(state.entities) }) }) }) describe('adding an extra column', () => { let initialState: GridColumnState beforeEach(() => { initialState = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO], rows: [], }, }) state = columnsReducer(initialState, { type: 'applyProps', payload: { columns: [COLUMN_TWO, COLUMN_THREE, COLUMN_ONE], rows: [], }, }) }) it('should not re-create previous columns', () => { expect(initialState.entities.firstName).toBe( state.entities.firstName ) expect(initialState.entities.lastName).toBe( state.entities.lastName ) }) it('should use the updated order', () => { expect(state.ids).toEqual(['lastName', 'email', 'firstName']) }) it('should add the new column', () => { expect(state.entities.email.data).toBe(COLUMN_THREE) }) }) describe('hiding a column', () => { let initialState: GridColumnState beforeEach(() => { initialState = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO], rows: [], }, }) state = columnsReducer(initialState, { type: 'applyProps', payload: { columns: [{ ...COLUMN_ONE, hidden: true }, COLUMN_TWO], rows: [], }, }) }) it('should hide the column', () => { expect(state.entities[COLUMN_ONE.id].data.hidden).toBe(true) expect(state.hiddenIds).toEqual([COLUMN_ONE.id]) }) }) describe('column changes after re-order', () => { it('should filter away removed columns', () => { const initialState = getStateMock() const state = columnsReducer(initialState, { type: 'applyProps', payload: { columns: [ { id: '3', label: '3', width: 150, }, { id: '1', label: '1', width: 150, }, ], rows: [], }, }) expect(state.preferences.sortedIds).toEqual(['1', '3']) }) it('should clear preference sorted ids when extra column is added', () => { const initialState = getStateMock() const state = columnsReducer(initialState, { type: 'applyProps', payload: { columns: [ { id: '1', label: '1', width: 150, }, { id: '2', label: '2', width: 150, }, { id: '3', label: '3', width: 150, }, { id: '4', label: '4', width: 150, }, ], rows: [], }, }) expect(state.preferences.sortedIds).toEqual(null) }) }) }) describe('reorderColumns', () => { it('should update the sort order', () => { const { selectColumnIds } = generateSelectors() const state = columnsReducer(getStateMock(), { type: 'reorderColumns', payload: ['3', '2', '1'], }) expect(selectColumnIds(state)).toEqual(['3', '2', '1']) }) }) describe('resizeColumn', () => { it('should resize the column', () => { const state = columnsReducer(getStateMock(), { type: 'resizeColumn', payload: { id: '1', width: 133, }, }) expect(selectColumn(state, '1')).toHaveProperty('actualWidth', 133) }) it('should store the size as a user preference', () => { const state = columnsReducer(getStateMock(), { type: 'resizeColumn', payload: { id: '1', width: 133, }, }) expect(selectColumnPreference(state, '1')).toHaveProperty( 'width', 133 ) }) it('should not size smaller than default minimum width', () => { const state = columnsReducer(getStateMock(), { type: 'resizeColumn', payload: { id: '1', width: 50, }, }) expect(selectColumn(state, '1')).toHaveProperty('actualWidth', 100) }) it('should not size smaller than configured minimum width', () => { const state = columnsReducer(getStateMock(), { type: 'resizeColumn', payload: { id: '2', width: 50, }, }) expect(selectColumn(state, '2')).toHaveProperty('actualWidth', 125) }) it('should not size larger than configured maximum width', () => { const state = columnsReducer(getStateMock(), { type: 'resizeColumn', payload: { id: '2', width: 500, }, }) expect(selectColumn(state, '2')).toHaveProperty('actualWidth', 300) }) }) describe('applyPreferences', () => { let state: GridColumnState describe('single selection mode', () => { beforeEach(() => { const applyPreferences: ApplyPreferences = { type: 'applyPreferences', payload: { preferences: { columns: [ { id: '3', width: 127 }, { id: '1', width: null }, { id: '2', width: 371 }, ], }, multiSelection: false, }, } state = columnsReducer(getStateMock(), applyPreferences) }) it('should update the sorted ids', () => { const { selectColumnIds } = generateSelectors() expect(selectColumnIds(state)).toEqual(['3', '1', '2']) }) it('should set widths on columns with user widths', () => { expect(selectColumn(state, '3')).toHaveProperty( 'actualWidth', 127 ) }) it('should constrain loaded width to match column configuration', () => { expect(selectColumn(state, '2')).toHaveProperty( 'actualWidth', 300 ) }) it("should not set widths on columns that don't have user widths", () => { expect(selectColumn(state, '1')).toHaveProperty( 'actualWidth', 150 ) }) }) describe('multi selection mode', () => { beforeEach(() => { const applyPreferences: ApplyPreferences = { type: 'applyPreferences', payload: { preferences: { columns: [ { id: '3', width: 127 }, { id: '1', width: null }, { id: '2', width: 371 }, ], }, multiSelection: true, }, } state = columnsReducer(getStateMock(), applyPreferences) }) it('should update the sorted ids', () => { const { selectColumnIds } = generateSelectors() expect(selectColumnIds(state)).toEqual([ SELECTION_COLUMN_ID, '3', '1', '2', ]) }) }) describe('column grouping', () => { it('should clear preferences if groups are split', () => { const initialState = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: '1', label: '1' }, { id: '2', label: '2' }, { id: '3', label: '3' }, { id: '4', label: '4' }, ], columnGroups: [ { id: 'group1', label: 'Group 1', children: ['1', '2'], }, { id: 'group2', label: 'Group 2', children: ['3', '4'], }, ], rows: [], }, }) const applyPreferences: ApplyPreferences = { type: 'applyPreferences', payload: { preferences: { columns: [ { id: '3', width: null }, { id: '1', width: null }, { id: '2', width: null }, { id: '4', width: null }, ], }, multiSelection: true, }, } state = columnsReducer(initialState, applyPreferences) expect(state.preferences.sortedIds).toBeNull() }) }) describe('hidden columns', () => { beforeEach(() => { const applyPreferences: ApplyPreferences = { type: 'applyPreferences', payload: { preferences: { columns: [ { id: '3', width: null }, { id: '1', width: null }, { id: '2', width: null, hidden: true }, ], }, multiSelection: true, }, } state = columnsReducer(getStateMock(), applyPreferences) }) it('should return the hidden columns', () => { expect(state.preferences.hiddenIds).toEqual(['2']) }) }) }) describe('updateColumnVisibility', () => { it('should update the visibility of the column and update preferences', () => { const state = columnsReducer(getStateMock(), { type: 'updateColumnVisibility', payload: { id: '1', visible: false, }, }) expect(state.hiddenIds).toEqual([]) expect(state.preferences.hiddenIds).toEqual(['1']) expect(selectHiddenIds(state)).toEqual(['1']) }) it('should update the visibility of the column and remove from preferences', () => { const state = columnsReducer(getStateMock({ hiddenIds: ['1'] }), { type: 'updateColumnVisibility', payload: { id: '1', visible: true, }, }) expect(state.hiddenIds).toEqual(['1']) expect(state.preferences.hiddenIds).toEqual([]) expect(selectHiddenIds(state)).toEqual([]) }) }) describe('selectColumnCount', () => { it('should return the correct count', () => { const state = getStateMock() expect(selectColumnCount(state)).toBe(3) }) }) describe('group selectors', () => { const COLUMN_ONE = { id: 'firstName', label: 'First', sortEmptyAs: 'high', } const COLUMN_TWO = { id: 'lastName', label: 'Last', width: 128, sortStrategy: 'natural', } const COLUMN_THREE = { id: 'email', label: 'Email' } const TOP_GROUP = { id: 'top-group', label: 'Top group', children: ['group1', 'group2'], } const GROUP_ONE = { id: 'group1', label: 'Group', children: ['firstName', 'lastName'], } const GROUP_TWO = { id: 'group2', label: 'Group 2', children: ['email'], } describe('selectGroupIds', () => { it('should return group ids if any', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO, COLUMN_THREE], rows: [], columnGroups: [GROUP_ONE, GROUP_TWO], }, }) expect(selectGroupIds(state)).toEqual([ GROUP_ONE.id, GROUP_TWO.id, ]) }) it('should return empty if none', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO, COLUMN_THREE], rows: [], }, }) expect(selectGroupIds(state)).toEqual([]) }) }) describe('selectHeaderHierarchy', () => { it('should return if any', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO, COLUMN_THREE], rows: [], columnGroups: [TOP_GROUP, GROUP_ONE, GROUP_TWO], }, }) const result = selectHeaderHierarchy(state) expect(result.groups.length).toBe(3) expect(result.levels).toBe(2) }) it('should return if none', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [COLUMN_ONE, COLUMN_TWO, COLUMN_THREE], rows: [], }, }) expect(selectHeaderHierarchy(state)).toEqual({ levels: 0, groups: [], }) }) }) }) describe('selectTotalColumnWidth', () => { it('should return the total width of all columns', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: 'firstName', label: 'First Name', width: 173, }, { id: 'lastName', label: 'Last Name', }, ], rows: [], }, }) expect(selectTotalColumnWidth(state)).toBe(323) }) it('should return the total width of all columns excluding hidden columns', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: 'firstName', label: 'First Name', width: 173, }, { id: 'lastName', label: 'Last Name', hidden: true, }, ], rows: [], }, }) expect(selectTotalColumnWidth(state)).toBe(173) }) }) describe('selectColumnWidths', () => { it('should return the widths of all columns', () => { const { selectColumnWidths } = generateSelectors() const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: 'firstName', label: 'First Name', width: 173, }, { id: 'lastName', label: 'Last Name', }, ], rows: [], }, }) expect(selectColumnWidths(state)).toEqual([173, 150]) }) it('should return the widths of all columns excluding hidden columns', () => { const { selectColumnWidths } = generateSelectors() const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: 'firstName', label: 'First Name', width: 173, }, { id: 'lastName', label: 'Last Name', hidden: true, }, ], rows: [], }, }) expect(selectColumnWidths(state)).toEqual([173]) }) }) describe('selectColumnPreference', () => { it('should return preferences', () => { const state = columnsReducer(getStateMock(), { type: 'resizeColumn', payload: { id: '1', width: 271, }, }) expect(selectColumnPreference(state, '1')).toEqual({ id: '1', width: 271, hidden: false, }) }) }) describe('selectColumnPreferences', () => { const { selectColumnPreferences } = generateSelectors() it('should return preferences (excluding selection column)', () => { const state = columnsReducer( getStateMock({ preferences: { hiddenIds: null, sortedIds: [SELECTION_COLUMN_ID, '2', '1', '3'], entities: {}, }, }), { type: 'resizeColumn', payload: { id: '1', width: 271, }, } ) expect(selectColumnPreferences(state)).toEqual([ { id: '2', width: null, hidden: false }, { id: '1', width: 271, hidden: false }, { id: '3', width: null, hidden: false }, ]) }) }) describe('selectIsTreeGrid', () => { it('should return false if no columns are configured as a tree', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [{ id: 'name', label: 'Name' }], rows: [], }, }) expect(selectIsTreeGrid(state)).toBe(false) }) it('should return true if a column is configured as a tree', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: '__selection', label: '' }, { id: 'name', label: 'Name', tree: true }, ], rows: [], }, }) expect(selectIsTreeGrid(state)).toBe(true) }) }) describe('selectAreHeadersHidden', () => { it('should return false when headers are not hidden', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [{ id: 'name', label: 'Name' }], rows: [], }, }) expect(selectAreHeadersHidden(state)).toBe(false) }) it('should return true when headers are hidden', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [{ id: 'name', label: 'Name' }], rows: [], hideHeaders: true, }, }) expect(selectAreHeadersHidden(state)).toBe(true) }) }) describe('selectHeaderHeight', () => { it('should return 0 when headers are hidden', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [{ id: 'name', label: 'Name' }], rows: [], hideHeaders: true, }, }) expect(selectHeaderHeight(state)).toBe(0) }) it('should return calculated height when headers are visible', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [{ id: 'name', label: 'Name' }], rows: [], }, }) expect(selectHeaderHeight(state)).toBe(36) }) it('should return calculated height with column groups', () => { const state = columnsReducer(getStateMock(), { type: 'applyProps', payload: { columns: [ { id: 'firstName', label: 'First' }, { id: 'lastName', label: 'Last' }, ], columnGroups: [ { id: 'nameGroup', label: 'Name Group', children: ['firstName', 'lastName'], }, ], rows: [], }, }) expect(selectHeaderHeight(state)).toBe(72) }) }) })