import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; import { Aggregators, SlickDataView } from '../slick.dataview'; import { SlickGrid } from '../slick.grid'; import type { GridOption, Grouping } from '../models'; import { SortDirectionNumber } from '../models/sortDirectionNumber.enum'; import { SlickHybridSelectionModel } from '../plugins/slick.hybridselectionmodel'; import { SlickEventData } from '../slick.core'; const SortComparers = { numeric: (a: number, b: number, direction = 1) => (a - b) * direction, string: (a: string, b: string, direction = 1) => a.localeCompare(b) * direction, }; class FakeAggregator { init() {} storeResult() {} } describe('SlickDatView core file', () => { let container: HTMLElement; let dv: SlickDataView; beforeEach(() => { container = document.createElement('div'); container.id = 'myGrid'; document.body.appendChild(container); }); afterEach(() => { document.body.textContent = ''; dv?.destroy(); }); it('should be able to instantiate SlickDataView', () => { dv = new SlickDataView({}); expect(dv.getItems()).toEqual([]); }); it('should be able to add items to the DataView', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe' }, { id: 2, firstName: 'Jane', lastName: 'Doe' }, ]; dv = new SlickDataView({}); dv.addItem(mockData[0]); dv.addItem(mockData[1]); expect(dv.getLength()).toBe(2); expect(dv.getItemCount()).toBe(2); expect(dv.getItems()).toEqual(mockData); }); describe('Item Getters', () => { afterEach(() => { dv.destroy(); }); test('retrieve an item from the DataView at specific index by calling getItem()', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.getItemCount()).toBe(2); expect(dv.getLength()).toBe(2); expect(dv.getItem(1)).toEqual({ id: 3, name: 'Jane', age: 24 }); }); describe('getRowByItem()', () => { test('get row number in the grid by its item object by calling getRowByItem()', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.getRowByItem(items[1])).toBe(1); }); it('should return undefined when calling getRowByItem() with an invalid item', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; const newItem = { id: 2, name: 'Bob', age: 30 }; dv.setItems(items); expect(dv.getRowByItem(newItem)).toBeUndefined(); }); }); test('get row number in the grid by its Id by calling getRowById()', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.getRowById(3)).toBe(1); }); test('get an item in the DataView by its Id by calling getItemById()', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.getItemById(3)).toEqual({ id: 3, name: 'Jane', age: 24 }); }); test('retrieve an item from the DataView at specific index by calling getItem()', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.getItem(1)).toEqual({ id: 3, name: 'Jane', age: 24 }); }); it('should return mapping of items with their row indexes', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.mapItemsToRows(items)).toEqual([0, 1]); }); it('should return mapping of item Ids with their row indexes and exclude any Ids not found', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.mapIdsToRows([3, 4, 999])).toEqual([1, 0]); }); it('should return mapping of row indexes with their item Ids and exclude any indexes not found', () => { const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.mapRowsToIds([0, 1, 999])).toEqual([4, 3]); }); it('should work with a custom global item metadata provider provided to the DataView constructor options', () => { dv = new SlickDataView({ globalItemMetadataProvider: { getRowMetadata(item, row) { return `row ${row} with name: ${item.name}`; }, }, }); const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(dv.getItemMetadata(0)).toBe('row 0 with name: John'); expect(dv.getItemMetadata(1)).toBe('row 1 with name: Jane'); }); }); describe('CRUD methods', () => { afterEach(() => { dv.destroy(); }); describe('addItem()', () => { it('should call the method and expect item to be added', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItem = { id: 2, name: 'Bob', age: 30 }; dv.setItems(items); dv.addItem(newItem); expect(dv.getItems().length).toBe(3); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, { id: 2, name: 'Bob', age: 30 }, ]); }); }); describe('deleteItem()', () => { it('should call the method and return undefined when item Map is undefined', () => { expect(dv.deleteItem(99)).toBeUndefined(); }); it('should throw when item Id is not found in the items array', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(() => dv.deleteItem(99)).toThrow('[SlickGrid DataView] Invalid id'); }); test('delete an item from the items array', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); dv.deleteItem(1); expect(dv.getItems()).toEqual([{ id: 0, name: 'John', age: 20 }]); expect(refreshSpy).toHaveBeenCalled(); }); test('delete multiple items with multi-digit indices in correct numeric order', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 0, name: 'Item0' }, { id: 1, name: 'Item1' }, { id: 2, name: 'Item2' }, { id: 3, name: 'Item3' }, { id: 10, name: 'Item10' }, { id: 20, name: 'Item20' }, ]; dv.setItems(items); dv.deleteItems([20, 2, 10]); // Non-sequential, multi-digit indices // Should delete items at indices 2, 10, 20 (Item2, Item10, Item20) expect(dv.getItems()).toEqual([ { id: 0, name: 'Item0' }, { id: 1, name: 'Item1' }, { id: 3, name: 'Item3' }, ]); expect(refreshSpy).toHaveBeenCalled(); }); }); describe('deleteItems()', () => { it('should call the method and return undefined when item Map is undefined', () => { expect(dv.deleteItems([99])).toBeUndefined(); }); it('should throw when item Id is not found in the items array', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(() => dv.deleteItems([99])).toThrow('[SlickGrid DataView] Invalid id'); }); test('delete an item from the items array', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); dv.deleteItems([1]); expect(dv.getItems()).toEqual([{ id: 0, name: 'John', age: 20 }]); expect(refreshSpy).toHaveBeenCalled(); }); }); describe('updateItem()', () => { it('should throw when calling the method with input Ids array does not match items array', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; expect(() => dv.updateItems([0, 1, 99], items)).toThrow('[SlickGrid DataView] Mismatch on the length of ids and items provided to update'); }); it('should update item when calling the method', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItem = { id: 1, name: 'Bob', age: 30 }; dv.setItems(items); dv.updateItem(1, updatedItem); expect(dv.getItems().length).toBe(2); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Bob', age: 30 }, ]); }); }); describe('updateSingleItem()', () => { it('should call the method and return undefined when item Map is undefined', () => { expect(dv.updateSingleItem(99, {})).toBeUndefined(); }); it('should throw when calling the method with an Id that is not found', () => { dv.setItems([]); expect(() => dv.updateSingleItem(99, {})).toThrow('[SlickGrid DataView] Invalid id'); }); it('should call the method and expect item to be updated', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); dv.updateSingleItem(1, { id: 1, name: 'Bob', age: 30 }); expect(dv.getItems().length).toBe(2); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Bob', age: 30 }, ]); expect(dv.getItemByIdx(1)).toEqual({ id: 1, name: 'Bob', age: 30 }); }); it('should call the method and expect item to be updated when passing different Id', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); dv.updateSingleItem(1, { id: 2, name: 'Bob', age: 30 }); expect(dv.getIdxById(2)).toBe(1); dv.updateSingleItem(2, { id: 1, name: 'Bob', age: 30 }); expect(dv.getItems().length).toBe(2); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Bob', age: 30 }, ]); expect(dv.getIdxById(1)).toBe(1); }); test('cannot update item to associate with a non-unique id', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItem = { id: 1, name: 'Bob', age: 30 }; dv.setItems(items); expect(() => dv.updateSingleItem(0, updatedItem)).toThrow('[SlickGrid DataView] Cannot update item to associate with a non-unique id'); }); test('cannot update item to associate with a null id', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItem = { name: 'Bob', age: 30 }; dv.setItems(items); expect(() => dv.updateSingleItem(0, updatedItem)).toThrow('[SlickGrid DataView] Cannot update item to associate with a null id'); }); }); }); describe('batch CRUD methods', () => { beforeEach(() => { dv = new SlickDataView({}); }); afterEach(() => { dv.endUpdate(); // close any batch that weren't closed because of potential error thrown dv.destroy(); }); describe('deleteItems()', () => { it('should throw when calling the method with an index not found in the items array', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); dv.beginUpdate(true); expect(() => dv.deleteItems([99])).toThrow('[SlickGrid DataView] Invalid id'); }); test('delete an item from the items array in bulk', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); dv.beginUpdate(true); dv.deleteItems([1]); dv.endUpdate(); expect(dv.getItems()).toEqual([{ id: 0, name: 'John', age: 20 }]); expect(refreshSpy).toHaveBeenCalled(); }); }); it('should batch items with addItems and begin/end batch update', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.beginUpdate(true); dv.addItems(items); dv.endUpdate(); expect(dv.getIdPropertyName()).toBe('id'); expect(dv.getItems()).toEqual(items); }); it('should batch more items with addItems with begin/end batch update and expect them to be inserted at the end of the dataset', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItems = [ { id: 3, name: 'Smith', age: 30 }, { id: 4, name: 'Ronald', age: 34 }, ]; dv.setItems(items); // original items list dv.beginUpdate(true); dv.addItems(newItems); // batch extra items dv.endUpdate(); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, { id: 3, name: 'Smith', age: 30 }, { id: 4, name: 'Ronald', age: 34 }, ]); }); it('should batch more items with insertItems with begin/end batch update and expect them to be inserted at the beginning of the dataset', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItems = [ { id: 3, name: 'Smith', age: 30 }, { id: 4, name: 'Ronald', age: 34 }, ]; dv.setItems(items); // original items list dv.beginUpdate(true); dv.insertItems(0, newItems); // batch extra items dv.endUpdate(); expect(dv.getItems()).toEqual([ { id: 3, name: 'Smith', age: 30 }, { id: 4, name: 'Ronald', age: 34 }, { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]); dv.deleteItem(3); expect(dv.getItems()).toEqual([ { id: 4, name: 'Ronald', age: 34 }, { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]); }); it('should be able to use different "id" when using setItems()', () => { const items = [ { keyId: 0, name: 'John', age: 20 }, { keyId: 1, name: 'Jane', age: 24 }, ]; dv.beginUpdate(true); dv.setItems(items, 'keyId'); dv.endUpdate(); expect(dv.getIdPropertyName()).toBe('keyId'); expect(dv.getItems()).toEqual(items); }); it('should batch more items with insertItems with begin/end batch update and expect them to be inserted at a certain index dataset', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItems = [ { id: 3, name: 'Smith', age: 30 }, { id: 4, name: 'Ronald', age: 34 }, ]; dv.setItems(items); // original items list dv.beginUpdate(true); dv.insertItems(1, newItems); // batch extra items dv.endUpdate(); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 3, name: 'Smith', age: 30 }, { id: 4, name: 'Ronald', age: 34 }, { id: 1, name: 'Jane', age: 24 }, ]); dv.deleteItems([3, 1]); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 4, name: 'Ronald', age: 34 }, ]); }); it('should throw when trying to delete items with have invalid Ids', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); // original items list expect(() => dv.deleteItems([-1, 1])).toThrow('[SlickGrid DataView] Invalid id'); }); it('should throw when trying to delete items with a batch that have invalid Ids', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); // original items list dv.beginUpdate(true); expect(() => dv.deleteItems([-1, 1])).toThrow('[SlickGrid DataView] Invalid id'); }); it('should call updateItems, without batch, and expect a refresh to be called', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItems = [ { id: 0, name: 'Smith', age: 30 }, { id: 1, name: 'Ronald', age: 34 }, ]; const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems(items); // original items list dv.updateItems( updatedItems.map((item) => item.id), updatedItems ); expect(refreshSpy).toHaveBeenCalled(); expect(dv.getItems()).toEqual([ { id: 0, name: 'Smith', age: 30 }, { id: 1, name: 'Ronald', age: 34 }, ]); dv.deleteItem(1); expect(dv.getItems()).toEqual([{ id: 0, name: 'Smith', age: 30 }]); }); it('should batch updateItems and expect a refresh to be called', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItems = [ { id: 0, name: 'Smith', age: 30 }, { id: 1, name: 'Ronald', age: 34 }, ]; const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems(items); // original items list dv.beginUpdate(true); dv.updateItems( updatedItems.map((item) => item.id), updatedItems ); expect(refreshSpy).toHaveBeenCalled(); expect(dv.getItems()).toEqual([ { id: 0, name: 'Smith', age: 30 }, { id: 1, name: 'Ronald', age: 34 }, ]); dv.deleteItem(1); dv.endUpdate(); expect(dv.getItems()).toEqual([{ id: 0, name: 'Smith', age: 30 }]); }); it('should batch updateItems and expect a refresh to be called', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItems = [ { id: 0, name: 'Smith', age: 30 }, { id: 1, name: 'Ronald', age: 34 }, ]; const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems(items); // original items list dv.beginUpdate(true); dv.updateItems( updatedItems.map((item) => item.id), updatedItems ); expect(refreshSpy).toHaveBeenCalled(); expect(dv.getItems()).toEqual([ { id: 0, name: 'Smith', age: 30 }, { id: 1, name: 'Ronald', age: 34 }, ]); dv.deleteItem(1); dv.endUpdate(); expect(dv.getItems()).toEqual([{ id: 0, name: 'Smith', age: 30 }]); }); it('should throw when batching updateItems with some invalid Ids', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItems = [ { id: 0, name: 'Smith', age: 30 }, { id: 1, name: 'Ronald', age: 34 }, ]; dv.setItems(items); // original items list dv.beginUpdate(true); expect(() => dv.updateItems([-1, 1], updatedItems)).toThrow('[SlickGrid DataView] Invalid id'); }); it('should throw when trying to call setItems() with duplicate Ids', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 0, name: 'Jane', age: 24 }, ]; expect(() => dv.setItems(items)).toThrow(`[SlickGrid DataView] Each data element must implement a unique 'id' property`); }); it('should call insertItem() at a defined index location', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItem = { id: 2, name: 'Smith', age: 30 }; const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems(items); dv.insertItem(1, newItem); expect(refreshSpy).toHaveBeenCalled(); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 2, name: 'Smith', age: 30 }, { id: 1, name: 'Jane', age: 24 }, ]); }); it('should throw when trying to call insertItem() with undefined Id', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItem = { id: undefined, name: 'Smith', age: 30 }; dv.setItems(items); expect(() => dv.insertItem(1, newItem)).toThrow(`[SlickGrid DataView] Each data element must implement a unique 'id' property`); }); it('should throw when trying to call insertItem() with undefined Id', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, { id: undefined, name: 'Smith', age: 30 }, ]; dv.beginUpdate(true); dv.setItems(items); expect(() => dv.endUpdate()).toThrow(`[SlickGrid DataView] Each data element must implement a unique 'id' property`); }); }); describe('Grouping', () => { it('should call setGrouping() and expect grouping to be defined without any accumulator neither totals when Aggregators are omitted', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe' }, { id: 2, firstName: 'Jane', lastName: 'Doe' }, ]; dv = new SlickDataView({}); const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems(mockData); dv.setGrouping({ getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, } as Grouping); expect(dv.getItem(0).rows).toHaveLength(2); expect(dv.getGroups().length).toBe(1); expect(refreshSpy).toHaveBeenCalled(); expect(dv.getGrouping().length).toBe(1); expect(dv.getGrouping()[0]).toMatchObject({ aggregators: [], getter: 'lastName' }); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 2, groupingKey: 'Doe', groups: null, level: 0, rows: mockData, selectChecked: false, title: 'Family: Doe (2 items)', totals: null, value: 'Doe', }); expect(dv.getItem(1)).toEqual(mockData[0]); expect(dv.getItem(2)).toEqual(mockData[1]); expect(dv.getItem(3)).toBeUndefined(); // without Totals // remove Grouping dv.setGrouping([]); expect(dv.getGroups().length).toBe(0); }); it('should call setGrouping() and expect grouping to be defined with compiled accumulator and totals when providing Aggregators', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe' }, { id: 2, firstName: 'Jane', lastName: 'Doe' }, ]; dv = new SlickDataView({}); const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems(mockData); const agg = new Aggregators.Count('lastName'); const agg2 = new FakeAggregator(); dv.setGrouping({ getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, aggregators: [agg, agg2], aggregateCollapsed: false, sortAsc: true, } as Grouping); expect(refreshSpy).toHaveBeenCalled(); expect(dv.getGrouping().length).toBe(1); expect(dv.getGrouping()[0]).toMatchObject({ aggregators: [agg, agg2], getter: 'lastName' }); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 2, groupingKey: 'Doe', groups: null, level: 0, rows: mockData, selectChecked: false, title: 'Family: Doe (2 items)', totals: expect.anything(), value: 'Doe', }); expect(dv.getItem(1)).toEqual(mockData[0]); expect(dv.getItem(2)).toEqual(mockData[1]); expect(dv.getItem(3)).toEqual({ __groupTotals: true, __nonDataRow: true, group: expect.anything(), initialized: true, count: { lastName: 2 }, }); }); it('should call setGrouping() and be able to sort it descending by the Grouping field', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe' }, { id: 2, firstName: 'Jane', lastName: 'Doe' }, { id: 3, firstName: 'Bob', lastName: 'Smith' }, ]; dv = new SlickDataView({}); const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems([...mockData]); dv.setGrouping({ getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, comparer: (a, b) => SortComparers.string(a.value, b.value, SortDirectionNumber.desc), } as Grouping); expect(refreshSpy).toHaveBeenCalled(); expect(dv.getGrouping().length).toBe(1); expect(dv.getGrouping()[0]).toMatchObject({ aggregators: [], getter: 'lastName' }); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith', groups: null, level: 0, rows: [{ id: 3, firstName: 'Bob', lastName: 'Smith' }], selectChecked: false, title: 'Family: Smith (1 items)', totals: null, value: 'Smith', }); expect(dv.getItem(1)).toEqual({ id: 3, firstName: 'Bob', lastName: 'Smith' }); expect(dv.getItem(2)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 2, groupingKey: 'Doe', groups: null, level: 0, rows: [ { id: 1, firstName: 'John', lastName: 'Doe' }, { id: 2, firstName: 'Jane', lastName: 'Doe' }, ], selectChecked: false, title: 'Family: Doe (2 items)', totals: null, value: 'Doe', }); expect(dv.getItem(3)).toEqual({ id: 1, firstName: 'John', lastName: 'Doe' }); expect(dv.getItem(4)).toEqual({ id: 2, firstName: 'Jane', lastName: 'Doe' }); expect(dv.getItem(5)).toBeUndefined(); // without Totals dv.collapseAllGroups(); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 1, count: 1, groupingKey: 'Smith', groups: null, level: 0, rows: [{ id: 3, firstName: 'Bob', lastName: 'Smith' }], selectChecked: false, title: 'Family: Smith (1 items)', totals: null, value: 'Smith', }); expect(dv.getItem(1)).toEqual({ __group: true, __nonDataRow: true, collapsed: 1, count: 2, groupingKey: 'Doe', groups: null, level: 0, rows: [ { id: 1, firstName: 'John', lastName: 'Doe' }, { id: 2, firstName: 'Jane', lastName: 'Doe' }, ], selectChecked: false, title: 'Family: Doe (2 items)', totals: null, value: 'Doe', }); dv.expandAllGroups(); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith', groups: null, level: 0, rows: [{ id: 3, firstName: 'Bob', lastName: 'Smith' }], selectChecked: false, title: 'Family: Smith (1 items)', totals: null, value: 'Smith', }); expect(dv.getItem(1)).toEqual({ id: 3, firstName: 'Bob', lastName: 'Smith' }); }); it('should call setGrouping() then use collapseGroup() and expandGroup()', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, { id: 3, firstName: 'John', lastName: 'Smith', age: 26 }, ]; dv = new SlickDataView({}); const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems([...mockData]); const agg1 = new Aggregators.Count('lastName'); const agg2 = new Aggregators.Sum('age'); dv.setGrouping({ getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, aggregators: [agg1, agg2], lazyTotalsCalculation: false, displayTotalsRow: false, aggregateChildGroups: false, } as Grouping); dv.expandGroup('Smith'); // Groups should be expanded expect(refreshSpy).toHaveBeenCalledTimes(3); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 2, groupingKey: 'Doe', groups: null, level: 0, rows: [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, ], selectChecked: false, title: 'Family: Doe (2 items)', totals: expect.anything(), value: 'Doe', }); expect(dv.getItem(1)).toEqual({ id: 1, firstName: 'John', lastName: 'Doe', age: 30 }); expect(dv.getItem(2)).toEqual({ id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }); expect(dv.getItem(3)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith', groups: null, level: 0, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Family: Smith (1 items)', totals: expect.anything(), value: 'Smith', }); dv.collapseGroup('Smith'); // Groups should now be collapsed expect(refreshSpy).toHaveBeenCalledTimes(4); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 2, groupingKey: 'Doe', groups: null, level: 0, rows: [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, ], selectChecked: false, title: 'Family: Doe (2 items)', totals: expect.anything(), value: 'Doe', }); expect(dv.getItem(1)).not.toEqual({ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }); }); it('should expand and collapse a predefined grouping value', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, { id: 3, firstName: 'John', lastName: 'Smith', age: 26 }, ]; dv = new SlickDataView({}); const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems([...mockData]); const agg1 = new Aggregators.Count('lastName'); const agg2 = new Aggregators.Sum('age'); dv.setGrouping({ getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, comparer: (a, b) => SortComparers.string(a.value, b.value, SortDirectionNumber.desc), aggregators: [agg1, agg2], predefinedValues: ['Smith'], } as Grouping); dv.expandGroup('Smith'); // Groups should be expanded expect(refreshSpy).toHaveBeenCalledTimes(3); expect(dv.getItem(0)).toMatchObject({ collapsed: 0, groupingKey: 'Smith', value: 'Smith' }); expect(dv.getItem(2)).toEqual({ __groupTotals: true, __nonDataRow: true, group: expect.anything(), initialized: true, count: { lastName: 1 }, sum: { age: 26 }, }); expect(dv.getItem(3)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 2, groupingKey: 'Doe', groups: null, level: 0, rows: [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, ], selectChecked: false, title: 'Family: Doe (2 items)', totals: expect.anything(), value: 'Doe', }); dv.collapseGroup('Smith'); // Groups should now be collapsed expect(refreshSpy).toHaveBeenCalledTimes(4); expect(dv.getItem(0)).toMatchObject({ collapsed: 1, groupingKey: 'Smith', value: 'Smith' }); expect(dv.getItem(1)).not.toEqual({ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }); }); it('should call setGrouping() with multiple Groups from array and lazyTotalsCalculation with Group Totals calculation', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, { id: 3, firstName: 'John', lastName: 'Smith', age: 26 }, ]; dv = new SlickDataView({}); const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems([...mockData]); const agg1 = new Aggregators.Count('lastName'); const agg2 = new Aggregators.Sum('age'); dv.setGrouping([ { getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, comparer: (a, b) => SortComparers.string(a.value, b.value, SortDirectionNumber.desc), aggregators: [agg1, agg2], aggregateChildGroups: false, lazyTotalsCalculation: true, aggregateEmpty: true, displayTotalsRow: false, }, { getter: 'age', formatter: (g) => `Age: ${g.value} (${g.count} items)`, comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.desc), aggregators: [agg1, agg2], lazyTotalsCalculation: true, aggregateEmpty: true, displayTotalsRow: true, aggregateCollapsed: false, }, ]); expect(dv.getItemMetadata(99)).toBeNull(); expect(dv.getItemMetadata(2)).toBeNull(); expect(dv.getItemMetadata(0)).toEqual({ columns: { 0: { colspan: '*', editor: null, formatter: expect.anything(), }, }, cssClasses: 'slick-group slick-group-level-0', isGroup: true, focusable: true, formatter: undefined, selectable: false, }); dv.expandAllGroups(0); // Groups should be expanded expect(refreshSpy).toHaveBeenCalledTimes(3); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith', groups: expect.anything(), level: 0, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Family: Smith (1 items)', totals: expect.anything(), value: 'Smith', }); expect(dv.getItem(1)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith:|:26', groups: null, level: 1, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Age: 26 (1 items)', totals: expect.anything(), value: 26, }); expect(dv.getItem(2)).toEqual({ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }); expect(dv.getItem(3)).toEqual({ __groupTotals: true, __nonDataRow: true, count: { lastName: 1 }, group: expect.anything(), initialized: true, sum: { age: 26 }, }); expect(dv.getItemMetadata(3)).toEqual({ cssClasses: 'slick-group-totals slick-group-level-1', editor: null, focusable: false, isGroup: true, formatter: expect.anything(), selectable: false, }); expect(dv.getItem(10)).toEqual({ __groupTotals: true, __nonDataRow: true, count: { lastName: 1 }, group: expect.anything(), initialized: true, sum: { age: 28 }, }); dv.collapseAllGroups(0); // Groups should now be collapsed expect(refreshSpy).toHaveBeenCalledTimes(4); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 1, count: 1, groupingKey: 'Smith', groups: expect.anything(), level: 0, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Family: Smith (1 items)', totals: expect.anything(), value: 'Smith', }); expect(dv.getItem(1)).toEqual({ __group: true, __nonDataRow: true, collapsed: 1, count: 2, groupingKey: 'Doe', groups: [expect.anything(), expect.anything()], level: 0, rows: [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, ], selectChecked: false, title: 'Family: Doe (2 items)', totals: expect.anything(), value: 'Doe', }); expect(dv.getItem(2)).toBeUndefined(); }); it('should call setGrouping() with multiple Groups from array and calculate totals afterward when getting item', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, { id: 3, firstName: 'John', lastName: 'Smith', age: 26 }, ]; dv = new SlickDataView({}); dv.setItems([...mockData]); const refreshSpy = vi.spyOn(dv, 'refresh'); const agg1 = new Aggregators.Count('lastName'); const agg2 = new Aggregators.Sum('age'); dv.setGrouping([ { getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, comparer: (a, b) => SortComparers.string(a.value, b.value, SortDirectionNumber.desc), aggregators: [agg1, agg2], lazyTotalsCalculation: false, displayTotalsRow: false, aggregateChildGroups: false, aggregateCollapsed: true, }, { getter: 'age', formatter: (g) => `Age: ${g.value} (${g.count} items)`, // comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.desc), aggregators: [agg1, agg2], lazyTotalsCalculation: false, displayTotalsRow: true, aggregateChildGroups: false, aggregateCollapsed: true, }, ]); expect(dv.getItemMetadata(99)).toBeNull(); expect(dv.getItemMetadata(2)).toBeNull(); expect(dv.getItemMetadata(0)).toEqual({ columns: { 0: { colspan: '*', editor: null, formatter: expect.anything(), }, }, cssClasses: 'slick-group slick-group-level-0', isGroup: true, focusable: true, formatter: undefined, selectable: false, }); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith', groups: expect.anything(), level: 0, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Family: Smith (1 items)', totals: expect.anything(), value: 'Smith', }); expect(dv.getItem(1)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith:|:26', groups: null, level: 1, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Age: 26 (1 items)', totals: expect.anything(), value: 26, }); expect(dv.getItem(2)).toEqual({ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }); expect(dv.getItem(3)).toMatchObject({ __groupTotals: true, __nonDataRow: true, count: { lastName: 1 }, group: expect.anything(), initialized: true, sum: { age: 26 }, }); dv.expandAllGroups(0); // Groups should be expanded expect(refreshSpy).toHaveBeenCalledTimes(2); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith', groups: expect.anything(), level: 0, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Family: Smith (1 items)', totals: expect.anything(), value: 'Smith', }); }); it('should call setGrouping() with multiple Groups from array with Group Totals calculation', () => { const mockData = [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, { id: 3, firstName: 'John', lastName: 'Smith', age: 26 }, ]; dv = new SlickDataView({}); const refreshSpy = vi.spyOn(dv, 'refresh'); dv.setItems([...mockData]); const agg1 = new Aggregators.Count('lastName'); const agg2 = new Aggregators.Sum('age'); dv.setGrouping([ { getter: 'lastName', formatter: (g) => `Family: ${g.value} (${g.count} items)`, comparer: (a, b) => SortComparers.string(a.value, b.value, SortDirectionNumber.desc), aggregators: [agg1, agg2], lazyTotalsCalculation: false, aggregateChildGroups: true, displayTotalsRow: true, aggregateEmpty: true, }, { getter: 'age', formatter: (g) => `Age: ${g.value} (${g.count} items)`, comparer: (a, b) => SortComparers.numeric(a.value, b.value, SortDirectionNumber.desc), aggregators: [agg1, agg2], lazyTotalsCalculation: false, displayTotalsRow: true, }, ]); expect(dv.getItem(1)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith:|:26', groups: null, level: 1, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Age: 26 (1 items)', totals: expect.anything(), value: 26, }); dv.expandAllGroups(); // Groups should be expanded expect(refreshSpy).toHaveBeenCalledTimes(3); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith', groups: expect.anything(), level: 0, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Family: Smith (1 items)', totals: expect.anything(), value: 'Smith', }); expect(dv.getItem(1)).toEqual({ __group: true, __nonDataRow: true, collapsed: 0, count: 1, groupingKey: 'Smith:|:26', groups: null, level: 1, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Age: 26 (1 items)', totals: expect.anything(), value: 26, }); expect(dv.getItem(2)).toEqual({ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }); expect(dv.getItem(3)).toEqual({ __groupTotals: true, __nonDataRow: true, count: { lastName: 1 }, group: expect.anything(), initialized: true, sum: { age: 26 }, }); dv.collapseAllGroups(); // Groups should now be collapsed expect(refreshSpy).toHaveBeenCalledTimes(4); expect(dv.getItem(0)).toEqual({ __group: true, __nonDataRow: true, collapsed: 1, count: 1, groupingKey: 'Smith', groups: expect.anything(), level: 0, rows: [{ id: 3, firstName: 'John', lastName: 'Smith', age: 26 }], selectChecked: false, title: 'Family: Smith (1 items)', totals: expect.anything(), value: 'Smith', }); expect(dv.getItem(1)).toEqual({ __group: true, __nonDataRow: true, collapsed: 1, count: 2, groupingKey: 'Doe', groups: [expect.anything(), expect.anything()], level: 0, rows: [ { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, { id: 2, firstName: 'Jane', lastName: 'Doe', age: 28 }, ], selectChecked: false, title: 'Family: Doe (2 items)', totals: expect.anything(), value: 'Doe', }); expect(dv.getItem(2)).toBeUndefined(); }); it('should apply grouping correctly when using dataItemColumnValueExtractor for nested object properties', () => { const mockData = [ { id: 1, security: { fullName: 'John Doe' }, role: { name: 'admin' } }, { id: 2, security: { fullName: 'Jane Doe' }, role: { name: 'user' } }, { id: 3, security: { fullName: 'John Smith' }, role: { name: 'admin' } }, ]; dv = new SlickDataView({}); dv.setItems(mockData); // Mock grid with dataItemColumnValueExtractor const mockGrid = { _options: { dataItemColumnValueExtractor: (item: any, col: any) => { if (col.field === 'role' && item.role) { return item.role.name; } if (col.field === 'fullName' && item.security) { return item.security.fullName; } return item[col.field]; }, }, getColumns: () => [ { id: 'security', field: 'fullName', name: 'Full Name' }, { id: 'role', field: 'role', name: 'Role' }, ], } as any; // Set grid on dataview to enable extractor usage (dv as any)._grid = mockGrid; // Group by role using the extractor dv.setGrouping({ getter: 'role', formatter: (g) => `Role: ${g.value} (${g.count} items)`, } as Grouping); // Check that grouping works correctly with extractor expect(dv.getGroups().length).toBe(2); // 'admin' and 'user' groups expect(dv.getGroups()[0].value).toBe('admin'); expect(dv.getGroups()[0].count).toBe(2); expect(dv.getGroups()[1].value).toBe('user'); expect(dv.getGroups()[1].count).toBe(1); // Verify items are grouped correctly const adminGroup = dv.getGroups()[0]; expect(adminGroup.rows.length).toBe(2); expect(adminGroup.rows[0].id).toBe(1); expect(adminGroup.rows[1].id).toBe(3); }); }); describe('Sorting', () => { beforeEach(() => { dv = new SlickDataView({}); }); afterEach(() => { dv.destroy(); vi.clearAllMocks(); }); describe('sortedAddItem()', () => { it('should throw when calling the method without a sort comparer', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItem = { id: 2, name: 'Bob', age: 30 }; dv.setItems(items); expect(() => dv.sortedAddItem(newItem)).toThrow('[SlickGrid DataView] sortedAddItem() requires a sort comparer, use sort()'); }); it('should call the method and expect item to be added and sorted in ascending order when no sort direction is provided', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItem = { id: 2, name: 'Bob', age: 30 }; // const comparer = (a: any, b: any) => SortComparers.numeric(a.id, b.id, SortDirectionNumber.asc); const comparer = (a: any, b: any) => (a.id === b.id ? 0 : a.id > b.id ? 1 : -1); const sortSpy = vi.spyOn(dv, 'sort'); dv.setItems(items); dv.sort(comparer, true); dv.sortedAddItem(newItem); expect(refreshSpy).toHaveBeenCalledTimes(3); expect(sortSpy).toHaveBeenCalledTimes(1); expect(dv.getItems().length).toBe(3); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, { id: 2, name: 'Bob', age: 30 }, ]); dv.reSort(); // calling resort will expect same result expect(refreshSpy).toHaveBeenCalledTimes(4); expect(sortSpy).toHaveBeenCalledTimes(2); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, { id: 2, name: 'Bob', age: 30 }, ]); }); it('should call the method and expect item to be added when called with a descending sort comparer', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const newItem = { id: 2, name: 'Bob', age: 30 }; const comparer = (a: any, b: any) => SortComparers.numeric(a.id, b.id, SortDirectionNumber.asc); dv.setItems(items); dv.sort(comparer, false); dv.sortedAddItem(newItem); dv.sort(comparer, false); expect(dv.getItems().length).toBe(3); expect(dv.getItems()).toEqual([ { id: 2, name: 'Bob', age: 30 }, { id: 1, name: 'Jane', age: 24 }, { id: 0, name: 'John', age: 20 }, ]); dv.reSort(); // calling resort will expect same result expect(dv.getItems()).toEqual([ { id: 2, name: 'Bob', age: 30 }, { id: 1, name: 'Jane', age: 24 }, { id: 0, name: 'John', age: 20 }, ]); }); }); describe('sortedUpdateItem()', () => { it('should call the method and return undefined when item Map is undefined', () => { expect(() => dv.sortedUpdateItem(99, {})).toThrow('[SlickGrid DataView] Invalid or non-matching id undefined'); }); it('should throw when calling the method with input Ids array does not match items array', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const comparer = (a: any, b: any) => SortComparers.numeric(a.id, b.id, SortDirectionNumber.asc); dv.setItems(items); dv.sort(comparer); expect(() => dv.sortedUpdateItem(99, items[0])).toThrow('[SlickGrid DataView] Invalid or non-matching id undefined'); }); it('should throw when calling the method with an input Id that does not match the updated item Id', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const comparer = (a: any, b: any) => SortComparers.numeric(a.id, b.id, SortDirectionNumber.asc); dv.setItems(items); dv.sort(comparer); expect(() => dv.sortedUpdateItem(0, items[1])).toThrow('[SlickGrid DataView] Invalid or non-matching id 0'); }); it('should throw when calling the method without a sort comparer', () => { const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; dv.setItems(items); expect(() => dv.sortedUpdateItem(0, items[0])).toThrow('[SlickGrid DataView] sortedUpdateItem() requires a sort comparer, use sort()'); }); it('should call the method and expect item to be added and sorted in ascending order when no sort direction is provided', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Jane', age: 24 }, ]; const updatedItem = { id: 1, name: 'Bob', age: 30 }; const comparer = (a: any, b: any) => SortComparers.numeric(a.id, b.id, SortDirectionNumber.asc); const sortSpy = vi.spyOn(dv, 'sort'); dv.setItems(items); dv.sort(comparer, true); dv.sortedUpdateItem(1, updatedItem); expect(refreshSpy).toHaveBeenCalledTimes(3); expect(sortSpy).toHaveBeenCalledTimes(1); expect(dv.getItems().length).toBe(2); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Bob', age: 30 }, ]); dv.reSort(); // calling resort will expect same result expect(refreshSpy).toHaveBeenCalledTimes(4); expect(sortSpy).toHaveBeenCalledTimes(2); expect(dv.getItems()).toEqual([ { id: 0, name: 'John', age: 20 }, { id: 1, name: 'Bob', age: 30 }, ]); }); it('should call the method and expect item to be added when called with a descending sort comparer', () => { const items = [ { id: 2, name: 'John', age: 20 }, { id: 0, name: 'Jane', age: 24 }, { id: 1, name: 'Bob', age: 22 }, ]; const updatedItem = { id: 2, name: 'Bobby', age: 30 }; const comparer = () => 1; // just return some static value dv.setItems(items); dv.sort(comparer, false); dv.sortedUpdateItem(2, updatedItem); dv.sort(comparer, false); // expect(dv.getItems().length).toBe(2); expect(dv.getItems()).toEqual([ { id: 2, name: 'Bobby', age: 30 }, { id: 0, name: 'Jane', age: 24 }, { id: 1, name: 'Bob', age: 22 }, ]); }); }); }); describe('Filtering', () => { afterEach(() => { dv.destroy(); vi.clearAllMocks(); }); it('should be able to set a filter and expect items to be filtered', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 1, name: 'Bob', age: 33 }, { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; const filter = (item: any) => item.id >= 2; dv.setItems(items); dv.setFilter(filter); expect(dv.getItemCount()).toBe(3); expect(dv.getFilter()).toBeTruthy(); expect(dv.getFilteredItemCount()).toBe(2); expect(dv.getFilteredItems()).toEqual([ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]); expect(refreshSpy).toHaveBeenCalled(); }); it('should be able to set a filter with Pagination and expect items to be filtered on first page', () => { const refreshSpy = vi.spyOn(dv, 'refresh'); const items = [ { id: 1, name: 'Bob', age: 33 }, { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; const filter = (item: any) => item.id >= 2; dv.setItems(items); dv.setFilter(filter); expect(dv.getItemCount()).toBe(3); expect(dv.getFilter()).toBeTruthy(); expect(dv.getFilteredItemCount()).toBe(2); expect(dv.getFilteredItems()).toEqual([ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]); expect(refreshSpy).toHaveBeenCalled(); }); it('should filter without runtime code generation and preserve access to closures', () => { const minimumId = 2; const items = [ { id: 1, name: 'Bob', age: 33 }, { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; const filter = (item: any) => item.id >= minimumId; const functionSpy = vi.spyOn(globalThis, 'Function'); dv = new SlickDataView(); dv.setItems(items); dv.setFilter(filter); expect(functionSpy).not.toHaveBeenCalled(); expect(dv.getFilter()).toBe(filter); expect(dv.getFilteredItems()).toEqual([ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]); functionSpy.mockRestore(); }); it('should cache successful filters when the filter is expanding', () => { const items = [ { id: 1, name: 'Bob', age: 33 }, { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; const filter = vi.fn((item: any) => item.id >= 2); dv.setItems(items); dv.setRefreshHints({ isFilterExpanding: true }); dv.setFilter(filter); expect(filter).toHaveBeenCalledTimes(3); dv.setRefreshHints({ isFilterExpanding: true }); dv.refresh(); expect(filter).toHaveBeenCalledTimes(4); expect(filter).toHaveBeenLastCalledWith(items[0], undefined); expect(dv.getFilteredItems()).toEqual([items[1], items[2]]); }); it('should be able to set a filter and extra filter arguments and expect items to be filtered', () => { const searchString = 'Ob'; // we'll provide "searchString" as filter args function myFilter(item: any, args: any) { return item.name.toLowerCase().includes(args.searchString?.toLowerCase()); } const items = [ { id: 1, name: 'Bob', age: 33 }, { id: 0, name: 'Hobby', age: 44 }, { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv = new SlickDataView(); dv.setItems(items); dv.setFilterArgs({ searchString }); dv.setFilter(myFilter); expect(dv.getItemCount()).toBe(4); expect(dv.getFilter()).toBeTruthy(); expect(dv.getFilterArgs()).toEqual({ searchString }); expect(dv.getFilteredItemCount()).toBe(2); expect(dv.getFilteredItems()).toEqual([ { id: 1, name: 'Bob', age: 33 }, { id: 0, name: 'Hobby', age: 44 }, ]); }); }); describe('Pagination', () => { afterEach(() => { dv.destroy(); vi.clearAllMocks(); }); it('should be able to set a filter with Pagination and expect items to be filtered on 2nd page', () => { const items = [ { id: 1, name: 'Bob', age: 33 }, { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; const filter = (item: any) => item.id >= 2; dv = new SlickDataView(); const onPagingInfoSpy = vi.spyOn(dv.onPagingInfoChanged, 'notify'); dv.setItems(items); dv.setFilter(filter); dv.setPagingOptions({ dataView: dv, pageNum: 1, pageSize: 1 }); dv.refresh(); expect(dv.getPagingInfo()).toEqual({ dataView: dv, pageNum: 1, pageSize: 1, totalPages: 2, totalRows: 2 }); expect(onPagingInfoSpy).toHaveBeenCalledWith({ dataView: dv, pageNum: 1, pageSize: 1, totalPages: 2, totalRows: 2 }, null, dv); expect(dv.getItemCount()).toBe(3); expect(dv.getFilter()).toBeTruthy(); expect(dv.getFilteredItemCount()).toBe(2); expect(dv.getFilteredItems()).toEqual([ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]); }); it('should be able to set a filter with Pagination and expect items to be filtered on 1st page', () => { const items = [ { id: 1, name: 'Bob', age: 33 }, { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, { id: 5, name: 'Alpha', age: 12 }, { id: 6, name: 'Omega', age: 24 }, ]; function myFilter(item: any) { return item.id >= 2; } dv = new SlickDataView(); const onPagingInfoSpy = vi.spyOn(dv.onPagingInfoChanged, 'notify'); const onRowCountChangeSpy = vi.spyOn(dv.onRowCountChanged, 'notify'); const onRowsChangeSpy = vi.spyOn(dv.onRowsChanged, 'notify'); dv.setPagingOptions({ dataView: dv, pageNum: 1, pageSize: 10 }); dv.setItems(items); dv.setRefreshHints({ isFilterNarrowing: true }); dv.setFilter(myFilter); expect(dv.getPagingInfo()).toEqual({ dataView: dv, pageNum: 0, pageSize: 10, totalPages: 1, totalRows: 4 }); expect(onPagingInfoSpy).toHaveBeenCalledWith({ dataView: dv, pageNum: 0, pageSize: 10, totalPages: 1, totalRows: 4 }, null, dv); expect(dv.getItemCount()).toBe(items.length); expect(dv.getFilter()).toBeTruthy(); expect(dv.getFilteredItemCount()).toBe(4); expect(dv.getFilteredItems()).toEqual([ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, { id: 5, name: 'Alpha', age: 12 }, { id: 6, name: 'Omega', age: 24 }, ]); dv.setRefreshHints({ isFilterExpanding: true }); items[0].id = 11; dv.setPagingOptions({ dataView: dv, pageNum: 2, pageSize: 2 }); // calling it again will reuse the cached filter result dv.setRefreshHints({ isFilterExpanding: true }); dv.refresh(); // change filter without changing pagination & expect pageNum to be recalculated dv.setFilter((item) => item.id >= 10); expect(onPagingInfoSpy).toHaveBeenCalledWith({ dataView: dv, pageNum: 0, pageSize: 2, totalPages: 1, totalRows: 1 }, null, dv); expect(onRowCountChangeSpy).toHaveBeenCalled(); expect(onRowsChangeSpy).toHaveBeenCalled(); // change filter without changing pagination will result in 2 changes but only 1 defined as changed because we ignore diffs from 0-1 dv.setRefreshHints({ ignoreDiffsBefore: 1, ignoreDiffsAfter: 3 }); items[0].id = 8; dv.setFilter(function (item) { return item.id >= 0; }); expect(onPagingInfoSpy).toHaveBeenCalledWith({ dataView: dv, pageNum: 0, pageSize: 2, totalPages: 3, totalRows: 5 }, null, dv); expect(onRowCountChangeSpy).toHaveBeenCalled(); expect(onRowsChangeSpy).toHaveBeenCalled(); }); }); describe('Row Selection', () => { let items: any[] = []; beforeEach(() => { items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, { id: 1, name: 'Bob', age: 20 }, { id: 5, name: 'Arnold', age: 50 }, { id: 0, name: 'Avery', age: 44 }, { id: 2, name: 'Rachel', age: 46 }, { id: 6, name: 'Carole', age: 40 }, { id: 8, name: 'Julie', age: 42 }, { id: 7, name: 'Jason', age: 48 }, { id: 9, name: 'Aaron', age: 23 }, { id: 10, name: 'Ariane', age: 43 }, ]; }); afterEach(() => { dv.destroy(); vi.clearAllMocks(); }); it('should throw when calling syncGridSelection() and selection model is undefined', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; const items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, ]; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); dv.setItems(items); expect(() => dv.syncGridSelection(grid, true)).toThrow('SlickGrid Selection model is not set'); }); it('should enable "preserveHidden" but keep "preserveHiddenOnSelectionChange" disabled and expect to only return current page selection when calling getAll methods', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); dv.setPagingOptions({ dataView: dv, pageNum: 0, pageSize: 4 }); dv.syncGridSelection(grid, true); grid.setSelectedRows([0, 1, 7]); expect(dv.getItemCount()).toBe(11); // full count expect(dv.getLength()).toBe(4); // page count expect(dv.getAllSelectedIds()).toEqual([3, 4]); // only 3, 4 because 7 is not on current page expect(dv.getAllSelectedItems()).toEqual([ { id: 3, name: 'Jane', age: 24 }, { id: 4, name: 'John', age: 20 }, ]); // go to next page & change selection, only current page selection is kept dv.setPagingOptions({ dataView: dv, pageNum: 1, pageSize: 4 }); grid.setSelectedRows([2, 3]); expect(dv.getAllSelectedIds()).toEqual([6, 8]); // only 6, 8 because 7 is not on current page expect(dv.getAllSelectedItems()).toEqual([ { id: 6, name: 'Carole', age: 40 }, { id: 8, name: 'Julie', age: 42 }, ]); }); it('should disable "preserveHidden" and enable "preserveHiddenOnSelectionChange" and expect to return previous page + current page selections when calling getAll methods', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); dv.setPagingOptions({ dataView: dv, pageNum: 0, pageSize: 4 }); dv.syncGridSelection(grid, false, true); grid.setSelectedRows([0, 1, 7]); expect(dv.getItemCount()).toBe(11); // full count expect(dv.getLength()).toBe(4); // page count expect(dv.getAllSelectedIds()).toEqual([3, 4]); expect(dv.getAllSelectedItems()).toEqual([ { id: 3, name: 'Jane', age: 24 }, { id: 4, name: 'John', age: 20 }, ]); // go to next page & change selection, selection also includes previous page(s) dv.setPagingOptions({ dataView: dv, pageNum: 1, pageSize: 4 }); grid.setSelectedRows([2, 3]); expect(dv.getAllSelectedIds()).toEqual([3, 4, 6, 8]); // includes previous page + current page selection expect(dv.getAllSelectedItems()).toEqual([ { id: 3, name: 'Jane', age: 24 }, { id: 4, name: 'John', age: 20 }, { id: 6, name: 'Carole', age: 40 }, { id: 8, name: 'Julie', age: 42 }, ]); }); it('should use setSelectedIds() to add/remove row selection with "preserveHiddenOnSelectionChange" enabled and "preserveHidden" disabled', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const setSelectedRowSpy = vi.spyOn(grid, 'setSelectedRows'); const onSelectedRowIdsSpy = vi.spyOn(dv.onSelectedRowIdsChanged, 'notify'); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); dv.setPagingOptions({ dataView: dv, pageNum: 0, pageSize: 4 }); dv.syncGridSelection(grid, false, true); dv.setSelectedIds([3, 4, 8], { isRowBeingAdded: true, applyRowSelectionToGrid: true }); expect(dv.getItemCount()).toBe(11); // full count expect(dv.getLength()).toBe(4); // page count expect(setSelectedRowSpy).toHaveBeenCalledWith([1, 0]); // 8 is outside of current page so not included expect(dv.getAllSelectedIds()).toEqual([3, 4, 8]); // 8 is also included even though it's not in current page expect(dv.getAllSelectedItems()).toEqual([ { id: 3, name: 'Jane', age: 24 }, { id: 4, name: 'John', age: 20 }, { id: 8, name: 'Julie', age: 42 }, ]); // remove selection dv.setSelectedIds([3], { isRowBeingAdded: false, applyRowSelectionToGrid: false }); expect(onSelectedRowIdsSpy).toHaveBeenCalledWith( { added: false, dataView: dv, filteredIds: [4, 8], grid, ids: [3], rows: [1], selectedRowIds: [4, 8], }, new SlickEventData(), dv ); expect(dv.getAllSelectedIds()).toEqual([4, 8]); expect(dv.getAllSelectedItems()).toEqual([ { id: 4, name: 'John', age: 20 }, { id: 8, name: 'Julie', age: 42 }, ]); }); it('should report all filtered selected IDs when selection changes through the grid', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const onSelectedRowIdsSpy = vi.spyOn(dv.onSelectedRowIdsChanged, 'notify'); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); dv.setPagingOptions({ dataView: dv, pageNum: 0, pageSize: 4 }); dv.syncGridSelection(grid, false, true); dv.setSelectedIds([3, 4, 8], { isRowBeingAdded: true, applyRowSelectionToGrid: true }); onSelectedRowIdsSpy.mockClear(); grid.setSelectedRows([0, 1, 2]); expect(onSelectedRowIdsSpy).toHaveBeenLastCalledWith(expect.objectContaining({ filteredIds: [4, 3, 1, 8] }), expect.anything(), dv); }); it('should normalize an existing unsorted selection when the same IDs are selected again', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); grid.setSelectedRows([0, 1]); dv.syncGridSelection(grid, false, true); dv.setSelectedIds([3, 4], { isRowBeingAdded: true, applyRowSelectionToGrid: false }); expect(dv.getAllSelectedIds()).toEqual([3, 4]); }); it('should reuse pending filtered IDs for bulk grid selection', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const onSelectedRowIdsSpy = vi.spyOn(dv.onSelectedRowIdsChanged, 'notify'); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); dv.setPagingOptions({ dataView: dv, pageNum: 0, pageSize: 4 }); dv.syncGridSelection(grid, false, true); dv.setSelectedIds([3, 4, 8], { isRowBeingAdded: true, shouldTriggerEvent: false, applyRowSelectionToGrid: false }); grid.setSelectedRows([0, 1], 'click.selectAll'); expect(onSelectedRowIdsSpy).toHaveBeenLastCalledWith(expect.objectContaining({ filteredIds: [3, 4, 8] }), expect.anything(), dv); }); it('should ignore pending filtered IDs when bulk grid selection changes the selected IDs', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const onSelectedRowIdsSpy = vi.spyOn(dv.onSelectedRowIdsChanged, 'notify'); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); dv.setPagingOptions({ dataView: dv, pageNum: 0, pageSize: 4 }); dv.syncGridSelection(grid, false, true); dv.setSelectedIds([8], { isRowBeingAdded: true, shouldTriggerEvent: false, applyRowSelectionToGrid: false }); grid.setSelectedRows([0, 1], 'click.selectAll'); expect(onSelectedRowIdsSpy).toHaveBeenLastCalledWith(expect.objectContaining({ filteredIds: [4, 3, 8] }), expect.anything(), dv); }); it('should not expect row selections to be preserved when using "multiSelect:false" and setSelectedIds() even when either preseve is enabled ("preserveHidden" or "preserveHiddenOnSelectionChange")', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: false } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const setSelectedRowSpy = vi.spyOn(grid, 'setSelectedRows'); const onSelectedRowIdsSpy = vi.spyOn(dv.onSelectedRowIdsChanged, 'notify'); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); dv.setItems(items); dv.setPagingOptions({ dataView: dv, pageNum: 0, pageSize: 4 }); dv.syncGridSelection(grid, false, true); dv.setSelectedIds([3, 4, 8], { isRowBeingAdded: true, applyRowSelectionToGrid: true }); expect(dv.getItemCount()).toBe(11); // full count expect(dv.getLength()).toBe(4); // page count expect(setSelectedRowSpy).toHaveBeenCalledWith([1, 0]); // 8 is outside of current page so not included expect(dv.getAllSelectedIds()).toEqual([3, 4]); // 8 is also included even though it's not in current page expect(dv.getAllSelectedItems()).toEqual([ { id: 3, name: 'Jane', age: 24 }, { id: 4, name: 'John', age: 20 }, ]); // remove selection dv.setSelectedIds([3], { isRowBeingAdded: false, applyRowSelectionToGrid: false }); expect(onSelectedRowIdsSpy).toHaveBeenCalledWith( { // filteredIds & selectedRowIds becomes empty because of disabled multiSelect not preserving row selections added: false, dataView: dv, filteredIds: [], grid, ids: [3], rows: [1], selectedRowIds: [], }, new SlickEventData(), dv ); expect(dv.getAllSelectedIds()).toEqual([]); expect(dv.getAllSelectedItems()).toEqual([]); }); }); describe('CSS Style Sync', () => { let items: any[] = []; let hash: any = {}; beforeEach(() => { items = [ { id: 4, name: 'John', age: 20 }, { id: 3, name: 'Jane', age: 24 }, { id: 1, name: 'Bob', age: 20 }, { id: 5, name: 'Arnold', age: 50 }, { id: 0, name: 'Avery', age: 44 }, { id: 2, name: 'Rachel', age: 46 }, { id: 6, name: 'Carole', age: 40 }, { id: 8, name: 'Julie', age: 42 }, { id: 7, name: 'Jason', age: 48 }, { id: 9, name: 'Aaron', age: 23 }, { id: 10, name: 'Ariane', age: 43 }, ]; hash = {}; for (const item of items) { if (item.age >= 30) { hash[item.id] = 'highlight'; } } }); afterEach(() => { dv.destroy(); vi.clearAllMocks(); }); it('should call syncGridCellCssStyles() with CSS style hashes and expect it sync it in the grid when onRowsOrCountChanged event is triggered', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: false } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const setCssStyleSpy = vi.spyOn(grid, 'setCellCssStyles'); dv.setItems(items); grid.setCellCssStyles('age_greater30_highlight', hash); dv.syncGridCellCssStyles(grid, 'age_greater30_highlight'); dv.onRowsOrCountChanged.notify({ currentRowCount: 11, dataView: dv, itemCount: 11, previousRowCount: 11, rowCountChanged: true, rowsChanged: true, rowsDiff: [0], }); expect(setCssStyleSpy).toHaveBeenCalledWith('age_greater30_highlight', hash); }); it('should call syncGridCellCssStyles() with CSS style hashes and expect it sync it in the grid when onCellCssStylesChanged event is triggered', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: false } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const setCssStyleSpy = vi.spyOn(grid, 'setCellCssStyles'); dv.setItems(items); grid.setCellCssStyles('age_greater30_highlight', hash); dv.syncGridCellCssStyles(grid, 'age_greater30_highlight'); grid.onCellCssStylesChanged.notify({ grid, hash, key: 'age_greater30_highlight' }); expect(setCssStyleSpy).toHaveBeenCalledWith('age_greater30_highlight', hash); }); it('should unsubscribe onCellCssStylesChanged & onRowsOrCountChanged when onCellCssStylesChanged event is triggered without a hash', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: false } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const unsubscribeCellCssStyleSpy = vi.spyOn(grid.onCellCssStylesChanged, 'unsubscribe'); const unsubscribeRowOrCountSpy = vi.spyOn(dv.onRowsOrCountChanged, 'unsubscribe'); dv.setItems(items); grid.setCellCssStyles('age_greater30_highlight', hash); dv.syncGridCellCssStyles(grid, 'age_greater30_highlight'); grid.onCellCssStylesChanged.notify({ grid, hash: null as any, key: 'age_greater30_highlight' }); expect(unsubscribeCellCssStyleSpy).toHaveBeenCalled(); expect(unsubscribeRowOrCountSpy).toHaveBeenCalledWith(expect.any(Function)); }); it('should not restore CSS styles removed from the grid when the rows change afterward, and still notify the next onCellCssStylesChanged subscriber', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: false } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); const cssStylesChangedSpy = vi.fn(); dv.setItems(items); dv.syncGridCellCssStyles(grid, 'age_highlight'); grid.onCellCssStylesChanged.subscribe(cssStylesChangedSpy); grid.setCellCssStyles('age_highlight', { 0: { age: 'highlight' } }); grid.removeCellCssStyles('age_highlight'); dv.sort((a, b) => SortComparers.numeric(a.id, b.id), true); expect(grid.getCellCssStyles('age_highlight')).toBeUndefined(); expect(cssStylesChangedSpy).toHaveBeenCalledTimes(2); }); it('should only drop the grid handlers of the DataView when calling destroy()', () => { const columns = [ { id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }, ]; const gridOptions = { enableCellNavigation: true, multiSelect: true } as GridOption; dv = new SlickDataView({}); const grid = new SlickGrid('#myGrid', dv, columns, gridOptions); grid.setSelectionModel(new SlickHybridSelectionModel({ selectActiveRow: false, selectionType: 'row' })); const selectedRowsChangedCount = grid.onSelectedRowsChanged.subscriberCount; const cssStylesChangedCount = grid.onCellCssStylesChanged.subscriberCount; const selectedRowsChangedSpy = vi.fn(); const cssStylesChangedSpy = vi.fn(); const selectedRowIdsChangedSpy = vi.fn(); dv.setItems(items); dv.syncGridSelection(grid, false); dv.syncGridCellCssStyles(grid, 'age_highlight'); dv.onSelectedRowIdsChanged.subscribe(selectedRowIdsChangedSpy); grid.onSelectedRowsChanged.subscribe(selectedRowsChangedSpy); grid.onCellCssStylesChanged.subscribe(cssStylesChangedSpy); dv.destroy(); grid.setSelectedRows([0, 1]); grid.setCellCssStyles('age_highlight', { 0: { age: 'highlight' } }); expect(grid.onSelectedRowsChanged.subscriberCount).toBe(selectedRowsChangedCount + 1); expect(grid.onCellCssStylesChanged.subscriberCount).toBe(cssStylesChangedCount + 1); expect(dv.onRowsOrCountChanged.subscriberCount).toBe(0); expect(selectedRowsChangedSpy).toHaveBeenCalledTimes(1); expect(cssStylesChangedSpy).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ key: 'age_highlight' })); expect(selectedRowIdsChangedSpy).not.toHaveBeenCalled(); }); }); });