|
| 1 | +import { describe, it, expect } from 'vitest' |
| 2 | +import { adjustSelectionIndices } from './adjust-selection-indices' |
| 3 | + |
| 4 | +describe('adjustSelectionIndices', () => { |
| 5 | + describe('no-ops', () => { |
| 6 | + it('returns empty for empty selection', () => { |
| 7 | + expect(adjustSelectionIndices([], [1], [2])).toEqual([]) |
| 8 | + }) |
| 9 | + |
| 10 | + it('returns unchanged indices when no removes or adds', () => { |
| 11 | + expect(sorted(adjustSelectionIndices([0, 2, 4], [], []))).toEqual([0, 2, 4]) |
| 12 | + }) |
| 13 | + }) |
| 14 | + |
| 15 | + describe('add only', () => { |
| 16 | + it('shifts selected index when add is before it', () => { |
| 17 | + // Old [a,b,c], new [X,a,b,c] → adds=[0] |
| 18 | + expect(sorted(adjustSelectionIndices([1], [], [0]))).toEqual([2]) |
| 19 | + }) |
| 20 | + |
| 21 | + it('does not shift selected index when add is after it', () => { |
| 22 | + // Old [a,b], new [a,b,X] → adds=[2] |
| 23 | + expect(sorted(adjustSelectionIndices([0], [], [2]))).toEqual([0]) |
| 24 | + }) |
| 25 | + |
| 26 | + it('shifts correctly when add is between selected indices', () => { |
| 27 | + // Old [a,b,c], new [a,X,b,c] → adds=[1]. Selected {0,2} |
| 28 | + expect(sorted(adjustSelectionIndices([0, 2], [], [1]))).toEqual([0, 3]) |
| 29 | + }) |
| 30 | + |
| 31 | + it('handles multiple adds', () => { |
| 32 | + // Old [a,b,c], new [X,a,Y,b,c] → adds=[0,2]. Selected {1} |
| 33 | + expect(sorted(adjustSelectionIndices([1], [], [0, 2]))).toEqual([3]) |
| 34 | + }) |
| 35 | + |
| 36 | + it('handles add at end of listing', () => { |
| 37 | + // Old [a,b], new [a,b,c] → adds=[2]. Selected {0,1} |
| 38 | + expect(sorted(adjustSelectionIndices([0, 1], [], [2]))).toEqual([0, 1]) |
| 39 | + }) |
| 40 | + |
| 41 | + it('handles add at beginning of listing', () => { |
| 42 | + // Old [b,c], new [a,b,c] → adds=[0]. Selected {0,1} |
| 43 | + expect(sorted(adjustSelectionIndices([0, 1], [], [0]))).toEqual([1, 2]) |
| 44 | + }) |
| 45 | + }) |
| 46 | + |
| 47 | + describe('remove only', () => { |
| 48 | + it('shifts selected index when unselected item is removed before it', () => { |
| 49 | + // Old [a,b,c], new [b,c] → removes=[0]. Selected {2} |
| 50 | + expect(sorted(adjustSelectionIndices([2], [0], []))).toEqual([1]) |
| 51 | + }) |
| 52 | + |
| 53 | + it('deselects removed index and shifts others', () => { |
| 54 | + // Old [a,b,c,d], new [a,c,d] → removes=[1]. Selected {1,3} |
| 55 | + expect(sorted(adjustSelectionIndices([1, 3], [1], []))).toEqual([2]) |
| 56 | + }) |
| 57 | + |
| 58 | + it('does not shift when remove is after selected index', () => { |
| 59 | + // Old [a,b,c], new [a,b] → removes=[2]. Selected {0} |
| 60 | + expect(sorted(adjustSelectionIndices([0], [2], []))).toEqual([0]) |
| 61 | + }) |
| 62 | + |
| 63 | + it('handles multiple removes', () => { |
| 64 | + // Old [a,b,c,d,e], new [a,d] → removes=[1,2,4]. Selected {0,3} |
| 65 | + expect(sorted(adjustSelectionIndices([0, 3], [1, 2, 4], []))).toEqual([0, 1]) |
| 66 | + }) |
| 67 | + |
| 68 | + it('returns empty when all selected indices are removed', () => { |
| 69 | + expect(adjustSelectionIndices([1, 3], [1, 3], [])).toEqual([]) |
| 70 | + }) |
| 71 | + |
| 72 | + it('handles remove at beginning of listing', () => { |
| 73 | + // Old [a,b,c], new [b,c] → removes=[0]. Selected {0,1,2} |
| 74 | + expect(sorted(adjustSelectionIndices([0, 1, 2], [0], []))).toEqual([0, 1]) |
| 75 | + }) |
| 76 | + |
| 77 | + it('handles remove at end of listing', () => { |
| 78 | + // Old [a,b,c], new [a,b] → removes=[2]. Selected {0,1,2} |
| 79 | + expect(sorted(adjustSelectionIndices([0, 1, 2], [2], []))).toEqual([0, 1]) |
| 80 | + }) |
| 81 | + }) |
| 82 | + |
| 83 | + describe('mixed adds and removes', () => { |
| 84 | + it('handles simultaneous add and remove', () => { |
| 85 | + // Old [a,b,c], new [a,X,c] → removes=[1], adds=[1]. Selected {2} |
| 86 | + // Interim: s=2, removedBefore=1 → interim=1. Adds=[1]: 1 <= 1+0 → offset=1. Result: 1+1=2 |
| 87 | + expect(sorted(adjustSelectionIndices([2], [1], [1]))).toEqual([2]) |
| 88 | + }) |
| 89 | + |
| 90 | + it('deselects when selected item is removed and new item is added at same position', () => { |
| 91 | + // Old [a,b,c], new [a,X,c] → removes=[1], adds=[1]. Selected {1} (b is removed) |
| 92 | + // b was selected, b is removed → deselected. X is a new item, not auto-selected. |
| 93 | + expect(adjustSelectionIndices([1], [1], [1])).toEqual([]) |
| 94 | + }) |
| 95 | + |
| 96 | + it('handles add before and remove after selected', () => { |
| 97 | + // Old [a,b,c], new [X,a,b] → removes=[2], adds=[0]. Selected {1} |
| 98 | + expect(sorted(adjustSelectionIndices([1], [2], [0]))).toEqual([2]) |
| 99 | + }) |
| 100 | + }) |
| 101 | + |
| 102 | + describe('non-contiguous selection', () => { |
| 103 | + it('handles selection with gaps', () => { |
| 104 | + // Old [a,b,c,d,e,f,g,h,i,j], selected {1,5,9} |
| 105 | + // new listing removes nothing, adds [3] → [a,b,c,X,d,e,f,g,h,i,j] |
| 106 | + // Interim: [1,5,9]. Add 3: 3 <= 1? no → emit 1. 3 <= 5? yes, offset=1 → emit 6. 9+1=10 → emit 10. |
| 107 | + expect(sorted(adjustSelectionIndices([1, 5, 9], [], [3]))).toEqual([1, 6, 10]) |
| 108 | + }) |
| 109 | + }) |
| 110 | + |
| 111 | + describe('large selection', () => { |
| 112 | + it('handles 1000 selected items with small diff correctly and fast', () => { |
| 113 | + // 1000 items selected (0..999), remove indices 100, 500, 900; add at new positions 50, 600 |
| 114 | + const selected = Array.from({ length: 1000 }, (_, i) => i) |
| 115 | + const removes = [100, 500, 900] |
| 116 | + const adds = [50, 600] |
| 117 | + |
| 118 | + const start = performance.now() |
| 119 | + const result = adjustSelectionIndices(selected, removes, adds) |
| 120 | + const elapsed = performance.now() - start |
| 121 | + |
| 122 | + expect(result.length).toBe(997) // 1000 - 3 removed |
| 123 | + expect(elapsed).toBeLessThan(50) // should be well under 50ms |
| 124 | + |
| 125 | + // Spot-check: index 0 should become 1 (add at 50 > 0, so offset = 0 initially... let's check) |
| 126 | + // Actually index 0: interim=0, add 50 <= 0? no → result 0. That's before any add. |
| 127 | + expect(result).toContain(0) |
| 128 | + // Index 999 had removes [100,500,900] before it → 3 removed, interim = 996 |
| 129 | + // Adds [50,600]: 50 <= 996? yes offset=1. 600 <= 997? yes offset=2. Result = 998. |
| 130 | + expect(result).toContain(998) |
| 131 | + }) |
| 132 | + }) |
| 133 | + |
| 134 | + describe('verified examples from spec', () => { |
| 135 | + it('example 1: Old [a,b,c,d,e], new [a,b,X,c,e]', () => { |
| 136 | + // removes=[3] (d), adds=[2] (X). Selected {2,3} → {3} |
| 137 | + const result = sorted(adjustSelectionIndices([2, 3], [3], [2])) |
| 138 | + expect(result).toEqual([3]) |
| 139 | + }) |
| 140 | + |
| 141 | + it('example 2: Old [a,b,c,d,e], new [a,X,b,c,d,e]', () => { |
| 142 | + // removes=[], adds=[1]. Selected {0,3} → {0,4} |
| 143 | + const result = sorted(adjustSelectionIndices([0, 3], [], [1])) |
| 144 | + expect(result).toEqual([0, 4]) |
| 145 | + }) |
| 146 | + |
| 147 | + it('example 3: Old [a,b,c,d,e,f], new [X,a,c,d,Y,f]', () => { |
| 148 | + // removes=[1,4] (b,e), adds=[0,4] (X,Y). Selected {1,3,5} → {3,5} |
| 149 | + const result = sorted(adjustSelectionIndices([1, 3, 5], [1, 4], [0, 4])) |
| 150 | + expect(result).toEqual([3, 5]) |
| 151 | + }) |
| 152 | + }) |
| 153 | +}) |
| 154 | + |
| 155 | +function sorted(arr: number[]): number[] { |
| 156 | + return [...arr].sort((a, b) => a - b) |
| 157 | +} |
0 commit comments