import type { GridState, Store } from '../state' import { createStore, INITIAL_STATE } from '../state' import type { Column, GridRowId } from '../types' import { findNextEditable } from './find-next-editable' const columns: GridState['columns'] = { ...INITIAL_STATE.columns, ids: ['firstName', 'lastName', 'lastLogin'], entities: { firstName: { id: 'firstName', actualWidth: 100, data: { id: 'firstName', label: 'First Name', cell: { editable: true, }, }, }, lastName: { id: 'lastName', actualWidth: 100, data: { id: 'lastName', label: 'Last Name', cell: { editable: ({ row }) => Number(row.id) % 2 === 0, } as Column['cell'], }, }, lastLogin: { id: 'lastLogin', actualWidth: 100, data: { id: 'lastLogin', label: 'Last login', }, }, }, } const columnsWithSpans: GridState['columns'] = { ...INITIAL_STATE.columns, ids: ['firstName', 'lastName', 'lastLogin'], entities: { firstName: { id: 'firstName', actualWidth: 100, data: { id: 'firstName', label: 'First Name', cell: { editable: true, }, }, }, lastName: { id: 'lastName', actualWidth: 100, data: { id: 'lastName', label: 'Last Name', cell: { colSpan: ({ row }) => row.id === '3' ? ['lastName', 'lastLogin'] : ['firstName', 'lastName'], editable: true, }, }, }, lastLogin: { id: 'lastLogin', actualWidth: 100, data: { id: 'lastLogin', label: 'Last login', cell: { editable: true, }, }, }, }, } const loopAroundEditingColumns: GridState['columns'] = { ...INITIAL_STATE.columns, ids: ['firstName', 'lastName', 'lastLogin'], entities: { firstName: { id: 'firstName', actualWidth: 100, data: { id: 'firstName', label: 'First Name', }, }, lastName: { id: 'lastName', actualWidth: 100, data: { id: 'lastName', label: 'Last Name', cell: { editable: ({ row }) => row.id === '2', } as Column['cell'], }, }, lastLogin: { id: 'lastLogin', actualWidth: 100, data: { id: 'lastLogin', label: 'Last login', }, }, }, } const hiddenColumns: GridState['columns'] = { ...INITIAL_STATE.columns, ids: ['firstName', 'lastName', 'lastLogin'], hiddenIds: ['lastName'], entities: { firstName: { id: 'firstName', actualWidth: 100, data: { id: 'firstName', label: 'First Name', cell: { editable: true, }, }, }, lastName: { id: 'lastName', actualWidth: 100, data: { id: 'lastName', label: 'Last Name', cell: { editable: true, } as Column['cell'], }, }, lastLogin: { id: 'lastLogin', actualWidth: 100, data: { id: 'lastLogin', label: 'Last login', }, }, }, } const rows: GridState['rows'] = { ...INITIAL_STATE.rows, collection: { ...INITIAL_STATE.rows.collection, ids: ['1', '2', '3', '4', '5'], entities: new Map( Object.entries({ 1: { id: '1', firstName: 'Joe', lastName: 'Doe', lastLogin: '123', }, 2: { id: '2', firstName: 'Fran', lastName: 'Bran', lastLogin: '234', }, 3: { id: '3', firstName: 'Jane', lastName: 'Doe', lastLogin: '345', }, 4: { id: '4', firstName: 'Frank', lastName: 'Bran', lastLogin: '456', }, /* id 5 simulates a loading row that should be skipped */ }) ), meta: new Map(), }, } describe('findNextEditable', () => { let store: Store beforeEach(() => { store = createStore() jest.spyOn(store.selectors, 'selectRowIds').mockReturnValue([ '1', '2', '3', '4', '5', ]) }) describe('normal navigation', () => { function configureStoreWithEdit({ rowId, columnId, }: { rowId: string columnId: string }) { store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns, rows, currentEdit: { rowId, columnId }, }) } it('should navigate to the next editable cell on that row', () => { configureStoreWithEdit({ rowId: '2', columnId: 'firstName' }) const next = findNextEditable(store, true) expect(next).toEqual({ columnId: 'lastName', rowId: '2', }) }) it('should navigate to the next editable cell on the next row', () => { configureStoreWithEdit({ rowId: '3', columnId: 'firstName' }) const next = findNextEditable(store, true) expect(next).toEqual({ columnId: 'firstName', rowId: '4', }) }) it('should navigate to the next editable cell on the first row of the grid (when end it hit)', () => { configureStoreWithEdit({ rowId: '4', columnId: 'lastName' }) const next = findNextEditable(store, true) expect(next).toEqual({ columnId: 'firstName', rowId: '1', }) }) it('should navigate to the previous editable cell on that row', () => { configureStoreWithEdit({ rowId: '2', columnId: 'lastName' }) const next = findNextEditable(store, false) expect(next).toEqual({ columnId: 'firstName', rowId: '2', }) }) it('should navigate to the previous editable cell on the previous row', () => { configureStoreWithEdit({ rowId: '3', columnId: 'firstName' }) const next = findNextEditable(store, false) expect(next).toEqual({ columnId: 'lastName', rowId: '2', }) }) it('should navigate to the previous editable cell on the last row of the grid (when start is hit)', () => { configureStoreWithEdit({ rowId: '1', columnId: 'firstName' }) const next = findNextEditable(store, false) expect(next).toEqual({ columnId: 'lastName', rowId: '4', }) }) }) describe('with spanned cells', () => { function configureStoreWithEdit({ rowId, columnId, }: { rowId: string columnId: string }) { store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns: columnsWithSpans, rows, currentEdit: { rowId, columnId }, }) } it('should navigate to the next editable cell when leaving a merged cell', () => { configureStoreWithEdit({ rowId: '1', columnId: 'lastName' }) const next = findNextEditable(store, true) expect(next).toEqual({ columnId: 'lastLogin', rowId: '1', }) }) it('should navigate to the next editable cell skipping merged columns', () => { configureStoreWithEdit({ rowId: '1', columnId: 'lastLogin' }) const next = findNextEditable(store, true) expect(next).toEqual({ columnId: 'lastName', rowId: '2', }) }) it('should navigate to the previous editable cell skipping merged columns', () => { configureStoreWithEdit({ rowId: '4', columnId: 'lastName' }) const prev = findNextEditable(store, false) /* This test merges lastLogin, so lastName is still the correct source cell on this row */ expect(prev).toEqual({ columnId: 'lastName', rowId: '3', }) }) }) describe('with hidden columns', () => { function configureStoreWithEdit({ rowId, columnId, }: { rowId: string columnId: string }) { store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns: hiddenColumns, rows, currentEdit: { rowId, columnId }, }) } it('should skip hidden columns', () => { configureStoreWithEdit({ rowId: '1', columnId: 'firstName' }) expect(findNextEditable(store)).toEqual({ columnId: 'firstName', rowId: '2', }) }) it('should skip hidden columns backwards', () => { configureStoreWithEdit({ rowId: '1', columnId: 'firstName' }) expect(findNextEditable(store, false)).toEqual({ rowId: '4', columnId: 'firstName', }) }) }) describe('edge cases', () => { describe('no current edit', () => { beforeEach(() => { store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns, rows, currentEdit: null, }) }) it('should return null', () => { expect(findNextEditable(store)).toBeNull() }) }) describe('search loops back around', () => { beforeEach(() => { store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns: loopAroundEditingColumns, rows, currentEdit: { rowId: '2', columnId: 'lastName' }, }) }) it('should return null', () => { expect(findNextEditable(store)).toBeNull() }) }) describe('it cannot find row', () => { beforeEach(() => { store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns, rows, currentEdit: { rowId: '6', columnId: 'lastName' }, }) }) it('should return null', () => { expect(findNextEditable(store)).toBeNull() }) }) describe('it cannot find column', () => { beforeEach(() => { store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns, rows, currentEdit: { rowId: '1', columnId: 'bogus' }, }) }) it('should return null', () => { expect(findNextEditable(store)).toBeNull() }) }) describe('it exceeds 100 attempts to find the next cell', () => { beforeEach(() => { const sortedIds: GridRowId[] = [] const largeRows = Array.from({ length: 100 }) .fill(true) .reduce( (memo, v, i) => { const id = `${i + 1}` memo.ids.push(id) sortedIds.push(id) memo.entities.set(id, { id, firstName: "Doesn't", lastName: 'Matter', lastLogin: '123', }) return memo }, { ...INITIAL_STATE.rows.collection, entities: new Map(), } ) jest.mocked(store.selectors.selectRowIds).mockReturnValue( sortedIds ) store.getState = jest.fn().mockReturnValue({ ...INITIAL_STATE, columns: loopAroundEditingColumns, rows: { ...INITIAL_STATE.rows, collection: largeRows, }, currentEdit: { rowId: '2', columnId: 'lastName' }, }) }) it('should return null', () => { expect(findNextEditable(store)).toBeNull() }) }) }) })