Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * Comprehensive tests for formula reference adjustment on row/col insert/delete.
3 *
4 * Issue #508: Formula refs must be adjusted when rows/columns are structurally
5 * modified. This includes absolute refs ($A$1) which are only "absolute" for
6 * copy/paste, not for structural changes.
7 */
8
9import { describe, it, expect } from 'vitest';
10import {
11 adjustFormulaRefs,
12 insertRow,
13 deleteRow,
14 insertColumn,
15 deleteColumn,
16} from '../src/sheets/row-col-ops.js';
17
18// ============================================================
19// Helper: simple in-memory cell map for integration tests
20// ============================================================
21
22interface MockCellData {
23 v: unknown;
24 f: string;
25 s: unknown;
26}
27
28function createMockCellMap(initial: Record<string, MockCellData> = {}) {
29 const store = new Map<string, MockCell>();
30
31 class MockCell {
32 private data = new Map<string, unknown>();
33 constructor(cellData?: MockCellData) {
34 if (cellData) {
35 this.data.set('v', cellData.v);
36 this.data.set('f', cellData.f);
37 this.data.set('s', cellData.s ?? '');
38 }
39 }
40 get(key: string) { return this.data.get(key); }
41 set(key: string, value: unknown) { this.data.set(key, value); }
42 }
43
44 for (const [id, data] of Object.entries(initial)) {
45 store.set(id, new MockCell(data));
46 }
47
48 const cellMap = {
49 get(key: string) { return store.get(key); },
50 set(key: string, value: unknown) { store.set(key, value as MockCell); },
51 has(key: string) { return store.has(key); },
52 delete(key: string) { store.delete(key); },
53 forEach(cb: (value: unknown, key: string) => void) { store.forEach(cb); },
54 };
55
56 function getCells() { return cellMap; }
57
58 function setCellData(id: string, data: { v?: unknown; f?: string; s?: unknown }) {
59 let cell: MockCell;
60 if (store.has(id)) {
61 cell = store.get(id)!;
62 } else {
63 cell = new MockCell();
64 store.set(id, cell);
65 }
66 if (data.v !== undefined) cell.set('v', data.v);
67 if (data.f !== undefined) cell.set('f', data.f);
68 if (data.s !== undefined) cell.set('s', typeof data.s === 'object' ? JSON.stringify(data.s) : data.s);
69 }
70
71 function getCellData(id: string): MockCellData | null {
72 const cell = store.get(id);
73 if (!cell) return null;
74 return {
75 v: cell.get('v') ?? '',
76 f: (cell.get('f') as string) ?? '',
77 s: cell.get('s') ?? '',
78 };
79 }
80
81 return { getCells, setCellData, getCellData, store };
82}
83
84// ============================================================
85// Row insertion — relative refs
86// ============================================================
87
88describe('Row insert — relative refs', () => {
89 it('shifts ref at insert point down by 1', () => {
90 expect(adjustFormulaRefs('A5', { type: 'row', index: 5, delta: 1 })).toBe('A6');
91 });
92
93 it('shifts ref below insert point down by 1', () => {
94 expect(adjustFormulaRefs('A10', { type: 'row', index: 5, delta: 1 })).toBe('A11');
95 });
96
97 it('does not shift ref above insert point', () => {
98 expect(adjustFormulaRefs('A2', { type: 'row', index: 5, delta: 1 })).toBe('A2');
99 });
100
101 it('shifts multiple refs in a formula', () => {
102 expect(adjustFormulaRefs('A5+B7+C3', { type: 'row', index: 5, delta: 1 }))
103 .toBe('A6+B8+C3');
104 });
105
106 it('shifts refs in SUM with range', () => {
107 expect(adjustFormulaRefs('SUM(A5:A10)', { type: 'row', index: 5, delta: 1 }))
108 .toBe('SUM(A6:A11)');
109 });
110
111 it('insert at row 1 shifts everything', () => {
112 expect(adjustFormulaRefs('A1+B1', { type: 'row', index: 1, delta: 1 }))
113 .toBe('A2+B2');
114 });
115});
116
117// ============================================================
118// Row insertion — absolute refs ($)
119// ============================================================
120
121describe('Row insert — absolute refs', () => {
122 it('shifts A$5 down when inserting at row 5 (absolute row, structural change)', () => {
123 expect(adjustFormulaRefs('A$5', { type: 'row', index: 5, delta: 1 })).toBe('A$6');
124 });
125
126 it('shifts A$5 down when inserting above (row 3)', () => {
127 expect(adjustFormulaRefs('A$5', { type: 'row', index: 3, delta: 1 })).toBe('A$6');
128 });
129
130 it('does not shift A$5 when inserting below (row 10)', () => {
131 expect(adjustFormulaRefs('A$5', { type: 'row', index: 10, delta: 1 })).toBe('A$5');
132 });
133
134 it('shifts $A5 down (absolute col, relative row)', () => {
135 expect(adjustFormulaRefs('$A5', { type: 'row', index: 3, delta: 1 })).toBe('$A6');
136 });
137
138 it('shifts $A$5 down (fully absolute ref)', () => {
139 expect(adjustFormulaRefs('$A$5', { type: 'row', index: 3, delta: 1 })).toBe('$A$6');
140 });
141
142 it('preserves $ signs in the output', () => {
143 expect(adjustFormulaRefs('$B$10', { type: 'row', index: 5, delta: 1 })).toBe('$B$11');
144 });
145
146 it('shifts mixed refs correctly in one formula', () => {
147 expect(adjustFormulaRefs('A1+$B$5+C$3', { type: 'row', index: 3, delta: 1 }))
148 .toBe('A1+$B$6+C$4');
149 });
150});
151
152// ============================================================
153// Row deletion — relative and absolute refs
154// ============================================================
155
156describe('Row delete — relative refs', () => {
157 it('ref to deleted row becomes #REF!', () => {
158 expect(adjustFormulaRefs('A5', { type: 'row', index: 5, delta: -1 })).toBe('#REF!');
159 });
160
161 it('ref below deleted row shifts up', () => {
162 expect(adjustFormulaRefs('A10', { type: 'row', index: 5, delta: -1 })).toBe('A9');
163 });
164
165 it('ref above deleted row unchanged', () => {
166 expect(adjustFormulaRefs('A2', { type: 'row', index: 5, delta: -1 })).toBe('A2');
167 });
168
169 it('multiple refs: deleted becomes #REF!, others shift', () => {
170 expect(adjustFormulaRefs('A5+A10+A2', { type: 'row', index: 5, delta: -1 }))
171 .toBe('#REF!+A9+A2');
172 });
173});
174
175describe('Row delete — absolute refs', () => {
176 it('A$5 to deleted row becomes #REF!', () => {
177 expect(adjustFormulaRefs('A$5', { type: 'row', index: 5, delta: -1 })).toBe('#REF!');
178 });
179
180 it('$A$5 to deleted row becomes #REF!', () => {
181 expect(adjustFormulaRefs('$A$5', { type: 'row', index: 5, delta: -1 })).toBe('#REF!');
182 });
183
184 it('$A$10 shifts up when row 5 deleted', () => {
185 expect(adjustFormulaRefs('$A$10', { type: 'row', index: 5, delta: -1 })).toBe('$A$9');
186 });
187
188 it('A$10 shifts up when row 5 deleted, preserving $ on row', () => {
189 expect(adjustFormulaRefs('A$10', { type: 'row', index: 5, delta: -1 })).toBe('A$9');
190 });
191
192 it('$A$3 unchanged when row 5 deleted (above deleted row)', () => {
193 expect(adjustFormulaRefs('$A$3', { type: 'row', index: 5, delta: -1 })).toBe('$A$3');
194 });
195});
196
197// ============================================================
198// Column insertion — relative and absolute refs
199// ============================================================
200
201describe('Column insert — relative refs', () => {
202 it('shifts ref at insert col right', () => {
203 expect(adjustFormulaRefs('C1', { type: 'col', index: 3, delta: 1 })).toBe('D1');
204 });
205
206 it('shifts ref right of insert col right', () => {
207 expect(adjustFormulaRefs('E1', { type: 'col', index: 3, delta: 1 })).toBe('F1');
208 });
209
210 it('does not shift ref left of insert col', () => {
211 expect(adjustFormulaRefs('A1', { type: 'col', index: 3, delta: 1 })).toBe('A1');
212 });
213});
214
215describe('Column insert — absolute refs', () => {
216 it('shifts $C1 right on structural insert (absolute col)', () => {
217 expect(adjustFormulaRefs('$C1', { type: 'col', index: 3, delta: 1 })).toBe('$D1');
218 });
219
220 it('shifts C$1 right (relative col, absolute row)', () => {
221 expect(adjustFormulaRefs('C$1', { type: 'col', index: 3, delta: 1 })).toBe('D$1');
222 });
223
224 it('shifts $C$1 right (fully absolute)', () => {
225 expect(adjustFormulaRefs('$C$1', { type: 'col', index: 3, delta: 1 })).toBe('$D$1');
226 });
227
228 it('does not shift $B1 when insert is at col 3 (left of insert)', () => {
229 expect(adjustFormulaRefs('$B1', { type: 'col', index: 3, delta: 1 })).toBe('$B1');
230 });
231
232 it('shifts $E$5 right when insert is at col 3', () => {
233 expect(adjustFormulaRefs('$E$5', { type: 'col', index: 3, delta: 1 })).toBe('$F$5');
234 });
235});
236
237// ============================================================
238// Column deletion — relative and absolute refs
239// ============================================================
240
241describe('Column delete — relative refs', () => {
242 it('ref to deleted col becomes #REF!', () => {
243 expect(adjustFormulaRefs('C1', { type: 'col', index: 3, delta: -1 })).toBe('#REF!');
244 });
245
246 it('ref right of deleted col shifts left', () => {
247 expect(adjustFormulaRefs('E1', { type: 'col', index: 3, delta: -1 })).toBe('D1');
248 });
249
250 it('ref left of deleted col unchanged', () => {
251 expect(adjustFormulaRefs('A1', { type: 'col', index: 3, delta: -1 })).toBe('A1');
252 });
253});
254
255describe('Column delete — absolute refs', () => {
256 it('$C1 to deleted col becomes #REF!', () => {
257 expect(adjustFormulaRefs('$C1', { type: 'col', index: 3, delta: -1 })).toBe('#REF!');
258 });
259
260 it('$C$1 to deleted col becomes #REF!', () => {
261 expect(adjustFormulaRefs('$C$1', { type: 'col', index: 3, delta: -1 })).toBe('#REF!');
262 });
263
264 it('$E$1 shifts left when col 3 deleted', () => {
265 expect(adjustFormulaRefs('$E$1', { type: 'col', index: 3, delta: -1 })).toBe('$D$1');
266 });
267
268 it('$B$1 unchanged when col 3 deleted (left of deleted)', () => {
269 expect(adjustFormulaRefs('$B$1', { type: 'col', index: 3, delta: -1 })).toBe('$B$1');
270 });
271});
272
273// ============================================================
274// Range refs (A1:B5)
275// ============================================================
276
277describe('Range refs — row operations', () => {
278 it('insert inside range expands the range', () => {
279 expect(adjustFormulaRefs('SUM(A1:A10)', { type: 'row', index: 5, delta: 1 }))
280 .toBe('SUM(A1:A11)');
281 });
282
283 it('insert above range shifts both ends', () => {
284 expect(adjustFormulaRefs('SUM(A5:A10)', { type: 'row', index: 2, delta: 1 }))
285 .toBe('SUM(A6:A11)');
286 });
287
288 it('insert below range does not change it', () => {
289 expect(adjustFormulaRefs('SUM(A1:A5)', { type: 'row', index: 10, delta: 1 }))
290 .toBe('SUM(A1:A5)');
291 });
292
293 it('delete row at range endpoint: endpoint becomes #REF!', () => {
294 expect(adjustFormulaRefs('SUM(A1:A10)', { type: 'row', index: 10, delta: -1 }))
295 .toBe('SUM(A1:#REF!)');
296 });
297
298 it('delete row above range shifts both endpoints up', () => {
299 expect(adjustFormulaRefs('SUM(A5:A10)', { type: 'row', index: 2, delta: -1 }))
300 .toBe('SUM(A4:A9)');
301 });
302
303 it('delete row inside range shifts lower endpoint', () => {
304 expect(adjustFormulaRefs('SUM(A3:A10)', { type: 'row', index: 5, delta: -1 }))
305 .toBe('SUM(A3:A9)');
306 });
307
308 it('multi-column range adjusts on row insert', () => {
309 expect(adjustFormulaRefs('SUM(A1:C10)', { type: 'row', index: 5, delta: 1 }))
310 .toBe('SUM(A1:C11)');
311 });
312});
313
314describe('Range refs — column operations', () => {
315 it('insert inside column range shifts right endpoint', () => {
316 expect(adjustFormulaRefs('SUM(A1:E1)', { type: 'col', index: 3, delta: 1 }))
317 .toBe('SUM(A1:F1)');
318 });
319
320 it('insert left of range shifts both endpoints', () => {
321 expect(adjustFormulaRefs('SUM(C1:E1)', { type: 'col', index: 2, delta: 1 }))
322 .toBe('SUM(D1:F1)');
323 });
324
325 it('delete col at range endpoint becomes #REF!', () => {
326 expect(adjustFormulaRefs('SUM(C1:E1)', { type: 'col', index: 3, delta: -1 }))
327 .toBe('SUM(#REF!:D1)');
328 });
329});
330
331describe('Range refs with absolute markers', () => {
332 it('shifts $A$1:$A$10 range on row insert', () => {
333 expect(adjustFormulaRefs('SUM($A$1:$A$10)', { type: 'row', index: 5, delta: 1 }))
334 .toBe('SUM($A$1:$A$11)');
335 });
336
337 it('shifts $A$5:$C$10 on row insert above range', () => {
338 expect(adjustFormulaRefs('SUM($A$5:$C$10)', { type: 'row', index: 2, delta: 1 }))
339 .toBe('SUM($A$6:$C$11)');
340 });
341
342 it('shifts $C$1:$E$1 on column insert', () => {
343 expect(adjustFormulaRefs('SUM($C$1:$E$1)', { type: 'col', index: 3, delta: 1 }))
344 .toBe('SUM($D$1:$F$1)');
345 });
346
347 it('absolute range endpoint on deleted row becomes #REF!', () => {
348 expect(adjustFormulaRefs('SUM($A$5:$A$5)', { type: 'row', index: 5, delta: -1 }))
349 .toBe('SUM(#REF!:#REF!)');
350 });
351});
352
353// ============================================================
354// Cross-sheet refs — should NOT be adjusted
355// ============================================================
356
357describe('Cross-sheet refs are not adjusted', () => {
358 it('unquoted cross-sheet ref unchanged on row insert', () => {
359 expect(adjustFormulaRefs('Sheet2!A5', { type: 'row', index: 3, delta: 1 }))
360 .toBe('Sheet2!A5');
361 });
362
363 it('quoted cross-sheet ref unchanged on row insert', () => {
364 expect(adjustFormulaRefs("'My Sheet'!A5", { type: 'row', index: 3, delta: 1 }))
365 .toBe("'My Sheet'!A5");
366 });
367
368 it('cross-sheet ref unchanged but local ref shifts', () => {
369 expect(adjustFormulaRefs('Sheet2!A5+A5', { type: 'row', index: 3, delta: 1 }))
370 .toBe('Sheet2!A5+A6');
371 });
372
373 it('cross-sheet ref unchanged on column delete', () => {
374 expect(adjustFormulaRefs('Sheet1!C1+D1', { type: 'col', index: 3, delta: -1 }))
375 .toBe('Sheet1!C1+C1');
376 });
377});
378
379// ============================================================
380// Complex formula scenarios
381// ============================================================
382
383describe('Complex formulas', () => {
384 it('IF formula with multiple refs', () => {
385 expect(adjustFormulaRefs('IF(A5>0,B5,C5)', { type: 'row', index: 3, delta: 1 }))
386 .toBe('IF(A6>0,B6,C6)');
387 });
388
389 it('nested SUM + single ref', () => {
390 expect(adjustFormulaRefs('SUM(A1:A10)+B5', { type: 'row', index: 5, delta: -1 }))
391 .toBe('SUM(A1:A9)+#REF!');
392 });
393
394 it('VLOOKUP formula adjusts table range', () => {
395 expect(adjustFormulaRefs('VLOOKUP(A1,B1:D10,3)', { type: 'row', index: 5, delta: 1 }))
396 .toBe('VLOOKUP(A1,B1:D11,3)');
397 });
398
399 it('INDEX/MATCH formula adjusts all refs', () => {
400 expect(adjustFormulaRefs('INDEX(B1:B10,MATCH(A1,A1:A10,0))', { type: 'row', index: 5, delta: 1 }))
401 .toBe('INDEX(B1:B11,MATCH(A1,A1:A11,0))');
402 });
403
404 it('formula with no cell refs unchanged', () => {
405 expect(adjustFormulaRefs('42+10', { type: 'row', index: 1, delta: 1 })).toBe('42+10');
406 });
407
408 it('empty formula unchanged', () => {
409 expect(adjustFormulaRefs('', { type: 'row', index: 1, delta: 1 })).toBe('');
410 });
411
412 it('formula with concatenation operator', () => {
413 expect(adjustFormulaRefs('A5&" - "&B5', { type: 'row', index: 3, delta: 1 }))
414 .toBe('A6&" - "&B6');
415 });
416
417 it('multiple deletions produce multiple #REF!', () => {
418 expect(adjustFormulaRefs('A3+A4+A5', { type: 'row', index: 3, delta: -1 }))
419 .toBe('#REF!+A3+A4');
420 });
421});
422
423// ============================================================
424// Integration: insertRow with absolute refs in formulas
425// ============================================================
426
427describe('insertRow — absolute ref integration', () => {
428 it('adjusts absolute refs in formulas of non-shifted cells', () => {
429 const { getCells, setCellData, getCellData } = createMockCellMap({
430 'A1': { v: '', f: '$A$3+$B$4', s: '' },
431 'A3': { v: 100, f: '', s: '' },
432 'B4': { v: 200, f: '', s: '' },
433 });
434
435 insertRow(getCells, setCellData, 2, 2);
436
437 expect(getCellData('A1')?.f).toBe('$A$4+$B$5');
438 });
439
440 it('adjusts absolute refs in formulas of shifted cells', () => {
441 const { getCells, setCellData, getCellData } = createMockCellMap({
442 'A3': { v: '', f: '$A$1+$B$2', s: '' },
443 'A1': { v: 10, f: '', s: '' },
444 'B2': { v: 20, f: '', s: '' },
445 });
446
447 insertRow(getCells, setCellData, 3, 2);
448
449 expect(getCellData('A4')?.f).toBe('$A$1+$B$2');
450 });
451
452 it('adjusts mixed absolute/relative refs on insert', () => {
453 const { getCells, setCellData, getCellData } = createMockCellMap({
454 'A1': { v: '', f: 'A5+$B$5+C$5', s: '' },
455 'A5': { v: 10, f: '', s: '' },
456 'B5': { v: 20, f: '', s: '' },
457 'C5': { v: 30, f: '', s: '' },
458 });
459
460 insertRow(getCells, setCellData, 3, 3);
461
462 expect(getCellData('A1')?.f).toBe('A6+$B$6+C$6');
463 });
464});
465
466// ============================================================
467// Integration: deleteRow with absolute refs in formulas
468// ============================================================
469
470describe('deleteRow — absolute ref integration', () => {
471 it('absolute ref to deleted row becomes #REF!', () => {
472 const { getCells, setCellData, getCellData } = createMockCellMap({
473 'A1': { v: '', f: '$A$3*2', s: '' },
474 'A3': { v: 100, f: '', s: '' },
475 });
476
477 deleteRow(getCells, setCellData, 3, 1);
478
479 expect(getCellData('A1')?.f).toBe('#REF!*2');
480 });
481
482 it('absolute ref below deleted row shifts up', () => {
483 const { getCells, setCellData, getCellData } = createMockCellMap({
484 'A1': { v: '', f: '$A$5', s: '' },
485 'A3': { v: 'delete me', f: '', s: '' },
486 'A5': { v: 50, f: '', s: '' },
487 });
488
489 deleteRow(getCells, setCellData, 3, 1);
490
491 expect(getCellData('A1')?.f).toBe('$A$4');
492 });
493});
494
495// ============================================================
496// Integration: insertColumn with absolute refs in formulas
497// ============================================================
498
499describe('insertColumn — absolute ref integration', () => {
500 it('adjusts absolute column refs in non-shifted cells', () => {
501 const { getCells, setCellData, getCellData } = createMockCellMap({
502 'A1': { v: '', f: '$C$1+$D$1', s: '' },
503 'C1': { v: 10, f: '', s: '' },
504 'D1': { v: 20, f: '', s: '' },
505 });
506
507 insertColumn(getCells, setCellData, 2, 1);
508
509 expect(getCellData('A1')?.f).toBe('$D$1+$E$1');
510 });
511
512 it('mixed refs adjusted on column insert', () => {
513 const { getCells, setCellData, getCellData } = createMockCellMap({
514 'A1': { v: '', f: 'C1+$C$1+$C1+C$1', s: '' },
515 'C1': { v: 10, f: '', s: '' },
516 });
517
518 insertColumn(getCells, setCellData, 3, 1);
519
520 expect(getCellData('A1')?.f).toBe('D1+$D$1+$D1+D$1');
521 });
522});
523
524// ============================================================
525// Integration: deleteColumn with absolute refs in formulas
526// ============================================================
527
528describe('deleteColumn — absolute ref integration', () => {
529 it('absolute ref to deleted column becomes #REF!', () => {
530 const { getCells, setCellData, getCellData } = createMockCellMap({
531 'A1': { v: '', f: '$C$1+$D$1', s: '' },
532 'C1': { v: 10, f: '', s: '' },
533 'D1': { v: 20, f: '', s: '' },
534 });
535
536 deleteColumn(getCells, setCellData, 3, 1);
537
538 expect(getCellData('A1')?.f).toBe('#REF!+$C$1');
539 });
540
541 it('absolute ref right of deleted col shifts left', () => {
542 const { getCells, setCellData, getCellData } = createMockCellMap({
543 'A1': { v: '', f: '$E$1', s: '' },
544 'B1': { v: 'delete me', f: '', s: '' },
545 'E1': { v: 50, f: '', s: '' },
546 });
547
548 deleteColumn(getCells, setCellData, 2, 1);
549
550 expect(getCellData('A1')?.f).toBe('$D$1');
551 });
552});
553
554// ============================================================
555// Edge cases
556// ============================================================
557
558describe('Edge cases', () => {
559 it('row 1 insert at row 1 shifts all refs', () => {
560 expect(adjustFormulaRefs('A1+B1+C1', { type: 'row', index: 1, delta: 1 }))
561 .toBe('A2+B2+C2');
562 });
563
564 it('delete row 1 shifts everything up', () => {
565 expect(adjustFormulaRefs('A2+A3', { type: 'row', index: 1, delta: -1 }))
566 .toBe('A1+A2');
567 });
568
569 it('column A insert at col 1 shifts all column refs right', () => {
570 expect(adjustFormulaRefs('A1+B1+C1', { type: 'col', index: 1, delta: 1 }))
571 .toBe('B1+C1+D1');
572 });
573
574 it('delete column A shifts all refs left', () => {
575 expect(adjustFormulaRefs('B1+C1', { type: 'col', index: 1, delta: -1 }))
576 .toBe('A1+B1');
577 });
578
579 it('large row number shifts correctly', () => {
580 expect(adjustFormulaRefs('A999', { type: 'row', index: 500, delta: 1 })).toBe('A1000');
581 });
582
583 it('multi-letter column (AA) shifts correctly', () => {
584 expect(adjustFormulaRefs('AA1', { type: 'col', index: 27, delta: 1 })).toBe('AB1');
585 });
586
587 it('formula with only operators and numbers is unchanged', () => {
588 expect(adjustFormulaRefs('1+2*3/4', { type: 'row', index: 1, delta: 1 })).toBe('1+2*3/4');
589 });
590
591 it('sequential insert/delete operations', () => {
592 let formula = 'A1+A5+A10';
593
594 formula = adjustFormulaRefs(formula, { type: 'row', index: 3, delta: 1 });
595 expect(formula).toBe('A1+A6+A11');
596
597 formula = adjustFormulaRefs(formula, { type: 'row', index: 6, delta: -1 });
598 expect(formula).toBe('A1+#REF!+A10');
599 });
600
601 it('preserves all $ combinations through row adjustment', () => {
602 const result = adjustFormulaRefs('A5+$A5+A$5+$A$5', { type: 'row', index: 3, delta: 1 });
603 expect(result).toBe('A6+$A6+A$6+$A$6');
604 });
605
606 it('preserves all $ combinations through column adjustment', () => {
607 const result = adjustFormulaRefs('C1+$C1+C$1+$C$1', { type: 'col', index: 3, delta: 1 });
608 expect(result).toBe('D1+$D1+D$1+$D$1');
609 });
610});