import { describe, it, expect } from 'vitest'; import { parseA1, toA1, cycleReferenceLocks, currentIdentifierAtCaret } from '../refs'; describe('parseA1 / toA1: $-locked (absolute/mixed) references', () => { it('parseA1 reports which axes are $-locked', () => { expect(parseA1('A1')).toEqual({ col: 0, row: 0, colAbs: false, rowAbs: false }); expect(parseA1('$A$1')).toEqual({ col: 0, row: 0, colAbs: true, rowAbs: true }); expect(parseA1('A$1')).toEqual({ col: 0, row: 0, colAbs: false, rowAbs: true }); expect(parseA1('$A1')).toEqual({ col: 0, row: 0, colAbs: true, rowAbs: false }); }); it('toA1 round-trips the lock markers', () => { expect(toA1(0, 0)).toBe('A1'); expect(toA1(0, 0, true, true)).toBe('$A$1'); expect(toA1(0, 0, false, true)).toBe('A$1'); expect(toA1(0, 0, true, false)).toBe('$A1'); }); }); describe('cycleReferenceLocks (F4) -- collapsed caret', () => { it('cycles relative -> absolute -> row-locked -> col-locked -> relative', () => { let f = '=A1'; let pos = 3; for (const expected of ['=$A$1', '=A$1', '=$A1', '=A1']) { const res = cycleReferenceLocks(f, pos, pos)!; expect(res.text).toBe(expected); expect(res.start).toBe(res.end); // a collapsed caret in -> a collapsed caret out f = res.text; pos = res.end; } }); it('toggles only the reference the caret sits in/against', () => { const res = cycleReferenceLocks('=A1+B2', 2, 2)!; // caret inside "A1" expect(res.text).toBe('=$A$1+B2'); const res2 = cycleReferenceLocks('=A1+B2', 6, 6)!; // caret inside "B2" expect(res2.text).toBe('=A1+$B$2'); }); it('picks the right side of a range depending on the caret', () => { const left = cycleReferenceLocks('=A1:B2', 2, 2)!; expect(left.text).toBe('=$A$1:B2'); const right = cycleReferenceLocks('=A1:B2', 6, 6)!; expect(right.text).toBe('=A1:$B$2'); }); it('returns null when the caret is not on a reference, or the text is not a formula', () => { expect(cycleReferenceLocks('=SUM(1;2)', 5, 5)).toBeNull(); expect(cycleReferenceLocks('42', 1, 1)).toBeNull(); }); it('never toggles a table-name qualifier, even one shaped like a cell', () => { // "AB12" reads as a cell reference on its own, but here it's a table name const formula = '=AB12!A1'; expect(cycleReferenceLocks(formula, 2, 2)).toBeNull(); // caret inside "AB12" expect(cycleReferenceLocks(formula, 7, 7)?.text).toBe('=AB12!$A$1'); // caret inside the actual cell }); }); describe('cycleReferenceLocks (F4) -- a real text selection spanning several refs', () => { it('toggles every reference the selection overlaps, each from its own state', () => { // select "A1+$B2" inside "=A1+$B2+C3" -- C3 sits outside the selection. // A1 (relative) -> $A$1; $B2 (already col-locked-only, the cycle's last // stop before relative) -> B2, its next step back to fully relative. const formula = '=A1+$B2+C3'; const start = formula.indexOf('A1'); const end = formula.indexOf('$B2') + '$B2'.length; const res = cycleReferenceLocks(formula, start, end)!; expect(res.text).toBe('=$A$1+B2+C3'); }); it('a selection covering the whole formula adjusts all of it in one press', () => { const res = cycleReferenceLocks('=A1+B2', 1, 7)!; expect(res.text).toBe('=$A$1+$B$2'); }); it('re-cycling the same (now resized) selection keeps advancing every reference', () => { let f = '=A1+B2'; let sel = cycleReferenceLocks(f, 1, 7)!; // -> $A$1+$B$2 f = sel.text; sel = cycleReferenceLocks(f, sel.start, sel.end)!; // -> A$1+B$2 expect(sel.text).toBe('=A$1+B$2'); }); it('a selection touching no reference returns null', () => { expect(cycleReferenceLocks('=SUM(1;2)', 4, 8)).toBeNull(); }); }); describe('currentIdentifierAtCaret -- function-name autocomplete detection', () => { it('right after "=", right after an operator, right after "(" or ";"', () => { expect(currentIdentifierAtCaret('=S', 2)).toEqual({ text: 'S', start: 1 }); expect(currentIdentifierAtCaret('=A1+S', 5)).toEqual({ text: 'S', start: 4 }); expect(currentIdentifierAtCaret('=SUM(A', 6)).toEqual({ text: 'A', start: 5 }); expect(currentIdentifierAtCaret('=SUM(1;A', 8)).toEqual({ text: 'A', start: 7 }); }); it('null outside a formula (no leading "=")', () => { expect(currentIdentifierAtCaret('SUM', 3)).toBeNull(); }); it('null once the word looks like a cell reference (has a digit)', () => { expect(currentIdentifierAtCaret('=SUM(A1', 7)).toBeNull(); expect(currentIdentifierAtCaret('=A1', 3)).toBeNull(); }); it('null right after an already-open, complete function call', () => { expect(currentIdentifierAtCaret('=SUM(', 5)).toBeNull(); }); it('null once the caret has moved past the word (trailing whitespace)', () => { expect(currentIdentifierAtCaret('=SUM ', 5)).toBeNull(); }); it('only looks at text up to the caret, ignoring what follows it', () => { // caret between "SUM" and "X" in "SUMX" -- same result as if "X" weren't there expect(currentIdentifierAtCaret('=SUMX', 4)).toEqual({ text: 'SUM', start: 1 }); }); it('null on a malformed prefix (unterminated string) rather than throwing', () => { expect(currentIdentifierAtCaret('=SUM("x', 7)).toBeNull(); }); });