import { ACTIONS_COLUMN_ID, SELECTION_COLUMN_ID } from '../constants' import type { Emitter } from '../events' import { createEmitter } from '../events' import type { GridState, Store } from '../state' import { createStore, INITIAL_STATE } from '../state' import type { GridFocusState } from '../state/reducer/focus' import type { GridRangeSelection } from '../types' import type { GridRangeSelectionApi } from './range-selection' import { createRangeSelectionApi } from './range-selection' const getStateMock = (state: Partial = {}): GridState => ({ ...INITIAL_STATE, columns: { ...INITIAL_STATE.columns, ids: ['col1', 'col2', 'col3'], }, rows: { ...INITIAL_STATE.rows, }, ...state, }) describe('range selection api', () => { let store: Store, api: GridRangeSelectionApi, events: Emitter beforeEach(() => { store = createStore() jest.spyOn(store, 'getState').mockReturnValue(getStateMock()) jest.spyOn(store.selectors, 'selectRangeSelection') jest.spyOn(store.selectors, 'selectCurrentFocus') jest.spyOn(store.selectors, 'selectRowIds').mockReturnValue([ 'row1', 'row2', 'row3', ]) jest.spyOn(store.selectors, 'selectColumnIds').mockReturnValue([ SELECTION_COLUMN_ID, 'col1', 'col2', 'col3', ACTIONS_COLUMN_ID, ]) jest.spyOn(store.selectors, 'selectRangeIds').mockReturnValue({ rowIds: ['row2'], columnIds: ['col2'], }) jest.spyOn(store.selectors, 'selectColumnSpanByRowId').mockReturnValue( null ) events = createEmitter() events.emit = jest.fn() api = createRangeSelectionApi(store, events) }) describe('select', () => { it('should emit onRangeSelectionChange with the provided range', () => { const range: GridRangeSelection = { from: { columnId: 'col1', rowId: 'row1' }, to: { columnId: 'col2', rowId: 'row2' }, } api.select(range) expect(events.emit).toHaveBeenCalledWith( 'onRangeSelectionChange', range ) }) it('should emit onRangeSelectionChange with null when range is null', () => { api.select(null) expect(events.emit).toHaveBeenCalledWith( 'onRangeSelectionChange', null ) }) }) describe('move', () => { const mockRangeSelection: GridRangeSelection = { from: { columnId: 'col2', rowId: 'row2' }, to: { columnId: 'col2', rowId: 'row2' }, } const mockCurrentFocus: GridFocusState['focus'] = { area: 'body', columnId: 'col2', rowId: 'row2', subFocus: 'first', } beforeEach(() => { jest.mocked(store.selectors.selectRangeSelection).mockReturnValue( mockRangeSelection ) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( mockCurrentFocus ) }) describe('when no range selection exists', () => { it('should not emit any events', () => { jest.mocked( store.selectors.selectRangeSelection ).mockReturnValue(null) api.move('right') expect(events.emit).not.toHaveBeenCalled() }) }) describe('when range selection "from" does not match current focus', () => { it('should not emit any events when rowId does not match', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col2', rowId: 'row1', // Different rowId subFocus: 'first', } ) api.move('right') expect(events.emit).not.toHaveBeenCalled() }) it('should not emit any events when columnId does not match', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col1', // Different columnId rowId: 'row2', subFocus: 'first', } ) api.move('right') expect(events.emit).not.toHaveBeenCalled() }) }) describe('when moving left', () => { it('should move to the previous column', () => { api.move('left') expect(events.emit).toHaveBeenCalledWith( 'onRangeSelectionChange', { from: mockRangeSelection.from, to: { columnId: 'col1', rowId: 'row2' }, } ) }) it('should not move past the selection column', () => { const rangeAtSelectionBoundary: GridRangeSelection = { from: { columnId: 'col1', rowId: 'row2' }, to: { columnId: 'col1', rowId: 'row2' }, } jest.mocked( store.selectors.selectRangeSelection ).mockReturnValue(rangeAtSelectionBoundary) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col1', rowId: 'row2', subFocus: 'first', } ) api.move('left') expect(events.emit).not.toHaveBeenCalled() }) it('should not move if already at the first column', () => { jest.mocked(store.selectors.selectColumnIds).mockReturnValue([ 'col1', 'col2', 'col3', ]) const rangeAtStart: GridRangeSelection = { from: { columnId: 'col1', rowId: 'row2' }, to: { columnId: 'col1', rowId: 'row2' }, } jest.mocked( store.selectors.selectRangeSelection ).mockReturnValue(rangeAtStart) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col1', rowId: 'row2', subFocus: 'first', } ) api.move('left') expect(events.emit).not.toHaveBeenCalled() }) }) describe('when moving right', () => { it('should move to the next column', () => { api.move('right') expect(events.emit).toHaveBeenCalledWith( 'onRangeSelectionChange', { from: mockRangeSelection.from, to: { columnId: 'col3', rowId: 'row2' }, } ) }) it('should not move past the actions column', () => { const rangeAtActionsBoundary: GridRangeSelection = { from: { columnId: 'col3', rowId: 'row2' }, to: { columnId: 'col3', rowId: 'row2' }, } jest.mocked( store.selectors.selectRangeSelection ).mockReturnValue(rangeAtActionsBoundary) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col3', rowId: 'row2', subFocus: 'first', } ) api.move('right') expect(events.emit).not.toHaveBeenCalled() }) it('should not move if already at the last column', () => { jest.mocked(store.selectors.selectColumnIds).mockReturnValue([ 'col1', 'col2', 'col3', ]) const rangeAtEnd: GridRangeSelection = { from: { columnId: 'col3', rowId: 'row2' }, to: { columnId: 'col3', rowId: 'row2' }, } jest.mocked( store.selectors.selectRangeSelection ).mockReturnValue(rangeAtEnd) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col3', rowId: 'row2', subFocus: 'first', } ) api.move('right') expect(events.emit).not.toHaveBeenCalled() }) }) describe('when moving up', () => { it('should move to the previous row', () => { api.move('up') expect(events.emit).toHaveBeenCalledWith( 'onRangeSelectionChange', { from: mockRangeSelection.from, to: { columnId: 'col2', rowId: 'row1' }, } ) }) it('should not move if already at the first row', () => { const rangeAtTop: GridRangeSelection = { from: { columnId: 'col2', rowId: 'row1' }, to: { columnId: 'col2', rowId: 'row1' }, } jest.mocked( store.selectors.selectRangeSelection ).mockReturnValue(rangeAtTop) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col2', rowId: 'row1', subFocus: 'first', } ) api.move('up') expect(events.emit).not.toHaveBeenCalled() }) }) describe('when moving down', () => { it('should move to the next row', () => { api.move('down') expect(events.emit).toHaveBeenCalledWith( 'onRangeSelectionChange', { from: mockRangeSelection.from, to: { columnId: 'col2', rowId: 'row3' }, } ) }) it('should not move if already at the last row', () => { const rangeAtBottom: GridRangeSelection = { from: { columnId: 'col2', rowId: 'row3' }, to: { columnId: 'col2', rowId: 'row3' }, } jest.mocked( store.selectors.selectRangeSelection ).mockReturnValue(rangeAtBottom) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'col2', rowId: 'row3', subFocus: 'first', } ) api.move('down') expect(events.emit).not.toHaveBeenCalled() }) }) describe('when movement results in no change', () => { it('should not emit any events', () => { // Mock a scenario where the new position would be the same as current jest.mocked(store.selectors.selectColumnIds).mockReturnValue([ 'col2', ]) // Only one column jest.mocked(store.selectors.selectRowIds).mockReturnValue([ 'row2', ]) // Only one row api.move('right') api.move('left') api.move('up') api.move('down') expect(events.emit).not.toHaveBeenCalled() }) }) }) })