- init
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseFormula } from '../parser';
|
||||
import { evaluate } from '../evaluator';
|
||||
import { FormulaError, isError, type CellValue, type EngineHost } from '../types';
|
||||
|
||||
/** Minimal in-memory host: two tables addressed by name. */
|
||||
function makeHost(
|
||||
data: Record<string, Record<string, CellValue>>,
|
||||
home = 'S1',
|
||||
): EngineHost {
|
||||
const sizes: Record<string, { cols: number; rows: number }> = {
|
||||
S1: { cols: 10, rows: 10 },
|
||||
S2: { cols: 10, rows: 10 },
|
||||
};
|
||||
const idFor = (name: string): string | null => {
|
||||
const n = name.trim().toLowerCase();
|
||||
if (n === 's1') return 'S1';
|
||||
if (n === 's2') return 'S2';
|
||||
return null;
|
||||
};
|
||||
return {
|
||||
homeTableId: home,
|
||||
resolveTableName: idFor,
|
||||
tableSize: (id) => sizes[id] ?? null,
|
||||
getComputed(tableId, col, row) {
|
||||
const cell = String.fromCharCode(65 + col) + (row + 1);
|
||||
const v = data[tableId]?.[cell];
|
||||
return v === undefined ? null : v;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ev = (src: string, host: EngineHost) => evaluate(parseFormula(src), host);
|
||||
|
||||
describe('evaluator arithmetic', () => {
|
||||
const host = makeHost({ S1: { B2: 10, C2: 5, D2: 8 } });
|
||||
|
||||
it('respects precedence and associativity', () => {
|
||||
expect(ev('=(B2+C2)*2-D2/4', host)).toBe(28); // (15)*2 - 2
|
||||
expect(ev('=2^3^2', host)).toBe(512);
|
||||
expect(ev('=-2^2', host)).toBe(-4);
|
||||
expect(ev('=2+3*4', host)).toBe(14);
|
||||
});
|
||||
|
||||
it('treats an empty cell as 0', () => {
|
||||
expect(ev('=B2+Z9', host)).toBe(10);
|
||||
});
|
||||
|
||||
it('division by zero -> #DIV/0!', () => {
|
||||
expect(ev('=1/0', host)).toEqual(new FormulaError('#DIV/0!'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluator functions', () => {
|
||||
const host = makeHost({
|
||||
S1: { B2: 1200, B3: 980, B4: 1550, B5: 1340, B6: 1105 },
|
||||
});
|
||||
|
||||
it('SUM / AVG / MIN / MAX / COUNT over a range', () => {
|
||||
expect(ev('=SUM(B2:B6)', host)).toBe(6175);
|
||||
expect(ev('=AVG(B2:B6)', host)).toBe(1235);
|
||||
expect(ev('=MIN(B2:B6)', host)).toBe(980);
|
||||
expect(ev('=MAX(B2:B6)', host)).toBe(1550);
|
||||
expect(ev('=COUNT(B2:B6)', host)).toBe(5);
|
||||
});
|
||||
|
||||
it('AVERAGE is an alias of AVG', () => {
|
||||
expect(ev('=AVERAGE(B2:B6)', host)).toBe(1235);
|
||||
});
|
||||
|
||||
it('mixes ranges, literals and single cells', () => {
|
||||
expect(ev('=SUM(B2:B3; 100; B4)', host)).toBe(1200 + 980 + 100 + 1550);
|
||||
});
|
||||
|
||||
it('COUNT ignores empty cells', () => {
|
||||
expect(ev('=COUNT(B2:B10)', host)).toBe(5);
|
||||
});
|
||||
|
||||
it('AVG of no numbers -> #DIV/0!', () => {
|
||||
const r = ev('=AVG(Z1:Z5)', host);
|
||||
expect(isError(r) && r.code).toBe('#DIV/0!');
|
||||
});
|
||||
|
||||
it('unknown function -> #NAME?', () => {
|
||||
const r = ev('=FOO(1)', host);
|
||||
expect(isError(r) && r.code).toBe('#NAME?');
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluator cross-sheet & errors', () => {
|
||||
const host = makeHost({
|
||||
S1: { A1: 3 },
|
||||
S2: { A1: 7, B1: 100 },
|
||||
});
|
||||
|
||||
it('resolves Name!Ref and is case-insensitive', () => {
|
||||
expect(ev('=S2!A1 + S2!B1', host)).toBe(107);
|
||||
expect(ev('=s2!a1', host)).toBe(7);
|
||||
});
|
||||
|
||||
it('accepts "::" the same as "!"', () => {
|
||||
expect(ev('=S2::A1 + S2::B1', host)).toBe(107);
|
||||
expect(ev('=s2::a1', host)).toBe(ev('=S2!A1', host));
|
||||
});
|
||||
|
||||
it('unknown sheet -> #REF', () => {
|
||||
const r = ev('=Nix!A1', host);
|
||||
expect(isError(r) && r.code).toBe('#REF');
|
||||
});
|
||||
|
||||
it('propagates an error from a referenced cell', () => {
|
||||
const h = makeHost({ S1: { A1: new FormulaError('#DIV/0!') } });
|
||||
const r = ev('=A1 + 1', h);
|
||||
expect(isError(r) && r.code).toBe('#DIV/0!');
|
||||
const r2 = ev('=SUM(A1; 5)', h);
|
||||
expect(isError(r2) && r2.code).toBe('#DIV/0!');
|
||||
});
|
||||
|
||||
it('a bare range in scalar position -> #VALUE!', () => {
|
||||
const r = ev('=A1:A3 + 1', host);
|
||||
expect(isError(r) && r.code).toBe('#VALUE!');
|
||||
});
|
||||
|
||||
it('text in arithmetic -> #VALUE!, numeric text is coerced', () => {
|
||||
const h = makeHost({ S1: { A1: 'abc', A2: '12' } });
|
||||
expect(isError(ev('=A1*2', h))).toBe(true);
|
||||
expect(ev('=A2*2', h)).toBe(24);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DependencyGraph } from '../graph';
|
||||
|
||||
/**
|
||||
* Helper: build a graph from "cell <- precedent, precedent" lines.
|
||||
* Node names are arbitrary strings here (real code uses `keyOf`).
|
||||
*/
|
||||
function build(edges: Record<string, string[]>): DependencyGraph {
|
||||
const g = new DependencyGraph();
|
||||
for (const [cell, precs] of Object.entries(edges)) g.setPrecedents(cell, precs);
|
||||
return g;
|
||||
}
|
||||
|
||||
describe('DependencyGraph.affected', () => {
|
||||
it('returns dirty cells plus their transitive dependents', () => {
|
||||
// A -> B -> C ; A -> D
|
||||
const g = build({ B: ['A'], C: ['B'], D: ['A'] });
|
||||
expect([...g.affected(['A'])].sort()).toEqual(['A', 'B', 'C', 'D']);
|
||||
expect([...g.affected(['B'])].sort()).toEqual(['B', 'C']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DependencyGraph.topoOrder', () => {
|
||||
it('orders precedents before dependents', () => {
|
||||
const g = build({ B: ['A'], C: ['A', 'B'], D: ['C'] });
|
||||
const { order, circular } = g.topoOrder(g.affected(['A']));
|
||||
expect(circular.size).toBe(0);
|
||||
const pos = (n: string) => order.indexOf(n);
|
||||
expect(pos('A')).toBeLessThan(pos('B'));
|
||||
expect(pos('B')).toBeLessThan(pos('C'));
|
||||
expect(pos('C')).toBeLessThan(pos('D'));
|
||||
});
|
||||
|
||||
it('detects a direct self-reference', () => {
|
||||
const g = build({ A: ['A'] });
|
||||
const { circular } = g.topoOrder(g.affected(['A']));
|
||||
expect([...circular]).toEqual(['A']);
|
||||
});
|
||||
|
||||
it('detects a 2-cycle and flags both nodes', () => {
|
||||
const g = build({ A: ['B'], B: ['A'] });
|
||||
const { order, circular } = g.topoOrder(g.affected(['A']));
|
||||
expect([...circular].sort()).toEqual(['A', 'B']);
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
|
||||
it('detects a longer cycle, leaves acyclic tail evaluable', () => {
|
||||
// cycle A->B->C->A ; plus D reads C (D is not on the cycle)
|
||||
const g = build({ A: ['C'], B: ['A'], C: ['B'], D: ['C'] });
|
||||
const { order, circular } = g.topoOrder(g.affected(['A']));
|
||||
expect([...circular].sort()).toEqual(['A', 'B', 'C']);
|
||||
expect(order).toEqual(['D']);
|
||||
});
|
||||
|
||||
it('rewiring precedents removes stale edges', () => {
|
||||
const g = build({ B: ['A'] });
|
||||
expect([...g.affected(['A'])].sort()).toEqual(['A', 'B']);
|
||||
g.setPrecedents('B', []); // B no longer reads A
|
||||
expect([...g.affected(['A'])].sort()).toEqual(['A']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { tokenize } from '../lexer';
|
||||
|
||||
const types = (s: string) => tokenize(s).map((t) => t.type);
|
||||
const values = (s: string) => tokenize(s).map((t) => t.value);
|
||||
|
||||
describe('lexer', () => {
|
||||
it('tokenizes numbers incl. decimals and exponents', () => {
|
||||
expect(values('1 2.5 .5 1e3 1.2E-3')).toEqual([
|
||||
'1',
|
||||
'2.5',
|
||||
'.5',
|
||||
'1e3',
|
||||
'1.2E-3',
|
||||
'',
|
||||
]);
|
||||
expect(types('42')).toEqual(['NUMBER', 'EOF']);
|
||||
});
|
||||
|
||||
it('tokenizes operators and punctuation', () => {
|
||||
expect(types('+-*/^():;!')).toEqual([
|
||||
'PLUS',
|
||||
'MINUS',
|
||||
'STAR',
|
||||
'SLASH',
|
||||
'CARET',
|
||||
'LPAREN',
|
||||
'RPAREN',
|
||||
'COLON',
|
||||
'SEMICOLON',
|
||||
'BANG',
|
||||
'EOF',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads bare words as IDENT (cell refs, functions, sheet names alike)', () => {
|
||||
expect(types('SUM(A1:B2)')).toEqual([
|
||||
'IDENT',
|
||||
'LPAREN',
|
||||
'IDENT',
|
||||
'COLON',
|
||||
'IDENT',
|
||||
'RPAREN',
|
||||
'EOF',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads single-quoted sheet names with spaces', () => {
|
||||
const t = tokenize("'Kosten Q3'!B2");
|
||||
expect(t[0]).toMatchObject({ type: 'QUOTED', value: 'Kosten Q3' });
|
||||
expect(t[1].type).toBe('BANG');
|
||||
expect(t[2]).toMatchObject({ type: 'IDENT', value: 'B2' });
|
||||
});
|
||||
|
||||
it('handles escaped double-quoted strings', () => {
|
||||
expect(tokenize('"a\\"b"')[0]).toMatchObject({ type: 'STRING', value: 'a"b' });
|
||||
});
|
||||
|
||||
it('skips whitespace', () => {
|
||||
expect(types(' A1\t+\n B2 ')).toEqual(['IDENT', 'PLUS', 'IDENT', 'EOF']);
|
||||
});
|
||||
|
||||
it('throws on an unexpected character', () => {
|
||||
expect(() => tokenize('A1 & B2')).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseFormula, collectRefs, ParseError, type Node } from '../parser';
|
||||
|
||||
/** Compact s-expression rendering of an AST for precedence assertions. */
|
||||
function sexpr(n: Node): string {
|
||||
switch (n.kind) {
|
||||
case 'number':
|
||||
return String(n.value);
|
||||
case 'string':
|
||||
return JSON.stringify(n.value);
|
||||
case 'unary':
|
||||
return `(${n.op} ${sexpr(n.operand)})`;
|
||||
case 'binary':
|
||||
return `(${n.op} ${sexpr(n.left)} ${sexpr(n.right)})`;
|
||||
case 'func':
|
||||
return `(${n.name} ${n.args.map(sexpr).join(' ')})`;
|
||||
case 'cell':
|
||||
return `${n.qualifier ? n.qualifier + '!' : ''}cell(${n.col},${n.row})`;
|
||||
case 'range':
|
||||
return `${n.qualifier ? n.qualifier + '!' : ''}range(${n.start.col},${n.start.row}:${n.end.col},${n.end.row})`;
|
||||
}
|
||||
}
|
||||
const parse = (s: string) => sexpr(parseFormula(s));
|
||||
|
||||
describe('parser precedence & associativity', () => {
|
||||
it('* / bind tighter than + -', () => {
|
||||
expect(parse('=1+2*3')).toBe('(+ 1 (* 2 3))');
|
||||
expect(parse('=1-2/2')).toBe('(- 1 (/ 2 2))');
|
||||
});
|
||||
|
||||
it('+ - are left-associative', () => {
|
||||
expect(parse('=1-2-3')).toBe('(- (- 1 2) 3)');
|
||||
});
|
||||
|
||||
it('^ binds tighter than * / and is right-associative', () => {
|
||||
expect(parse('=2*3^2')).toBe('(* 2 (^ 3 2))');
|
||||
expect(parse('=2^3^2')).toBe('(^ 2 (^ 3 2))');
|
||||
});
|
||||
|
||||
it('unary minus binds weaker than ^ (-2^2 = -(2^2))', () => {
|
||||
expect(parse('=-2^2')).toBe('(- (^ 2 2))');
|
||||
expect(parse('=2^-3')).toBe('(^ 2 (- 3))');
|
||||
});
|
||||
|
||||
it('parentheses override precedence', () => {
|
||||
expect(parse('=(1+2)*3')).toBe('(* (+ 1 2) 3)');
|
||||
expect(parse('=(B2+C2)*2-D2/4')).toBe(
|
||||
'(- (* (+ cell(1,1) cell(2,1)) 2) (/ cell(3,1) 4))',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser references', () => {
|
||||
it('parses local cells and ranges', () => {
|
||||
expect(parse('=A1')).toBe('cell(0,0)');
|
||||
expect(parse('=A1:C5')).toBe('range(0,0:2,4)');
|
||||
});
|
||||
|
||||
it('parses table-qualified refs, bare and quoted', () => {
|
||||
expect(parse('=Umsatz!B2')).toBe('Umsatz!cell(1,1)');
|
||||
expect(parse("='Kosten Q3'!B2:E2")).toBe('Kosten Q3!range(1,1:4,1)');
|
||||
});
|
||||
|
||||
it('accepts the Numbers-style "::" qualifier interchangeably with "!"', () => {
|
||||
expect(parse('=Umsatz::B2')).toBe('Umsatz!cell(1,1)');
|
||||
expect(parse("='Kosten Q3'::B2:E2")).toBe('Kosten Q3!range(1,1:4,1)');
|
||||
expect(parse('=Umsatz::B2')).toBe(parse('=Umsatz!B2'));
|
||||
});
|
||||
|
||||
it('parses function calls with ; separator and mixed args', () => {
|
||||
expect(parse('=SUM(Umsatz!B2:E2; 100; A1)')).toBe(
|
||||
'(SUM Umsatz!range(1,1:4,1) 100 cell(0,0))',
|
||||
);
|
||||
});
|
||||
|
||||
it('collectRefs finds every reference', () => {
|
||||
const refs = collectRefs(parseFormula('=SUM(A1:A3; Kosten!B2) + C4'));
|
||||
expect(refs).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser errors', () => {
|
||||
it('throws ParseError on a dangling operator', () => {
|
||||
expect(() => parseFormula('=A1+')).toThrow(ParseError);
|
||||
});
|
||||
it('throws on an unbalanced paren', () => {
|
||||
expect(() => parseFormula('=SUM(A1')).toThrow(ParseError);
|
||||
});
|
||||
it('throws on a non-cell identifier used as a reference', () => {
|
||||
expect(() => parseFormula('=foo + 1')).toThrow(ParseError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Workbook, type TableModel } from '../workbook';
|
||||
import { FormulaError, isError, keyOf } from '../types';
|
||||
|
||||
function sheet(id: string, name: string, cells: Record<string, string>): TableModel {
|
||||
const inputs = new Map<string, string>();
|
||||
for (const [a1, raw] of Object.entries(cells)) {
|
||||
const m = /^([A-Z]+)([0-9]+)$/.exec(a1)!;
|
||||
const col = m[1].split('').reduce((n, ch) => n * 26 + (ch.charCodeAt(0) - 64), 0) - 1;
|
||||
const row = Number(m[2]) - 1;
|
||||
inputs.set(`${col},${row}`, raw);
|
||||
}
|
||||
return { id, name, cols: 8, rows: 30, inputs };
|
||||
}
|
||||
|
||||
/** Rebuilds the small "Umsatz / Kosten / Übersicht" fixture from the brief. */
|
||||
function fixture(): Workbook {
|
||||
const umsatz = sheet('s-umsatz', 'Umsatz', {
|
||||
A2: 'Nord', B2: '1200', C2: '1450', D2: '1600', E2: '1720',
|
||||
A3: 'Süd', B3: '980', C3: '1100', D3: '1250', E3: '1310',
|
||||
A4: 'Ost', B4: '1550', C4: '1480', D4: '1620', E4: '1700',
|
||||
A5: 'West', B5: '1340', C5: '1390', D5: '1500', E5: '1580',
|
||||
A6: 'Zentral', B6: '1105', C6: '1205', D6: '1298', E6: '1402',
|
||||
B8: '=SUM(B2:B6)', B9: '=AVG(B2:B6)', B10: '=MIN(B2:B6)',
|
||||
B11: '=MAX(B2:B6)', B12: '=COUNT(B2:B6)',
|
||||
H2: '=SUM(B2:E2)', H3: '=SUM(B2:E2)/4', H4: '=E2-B2',
|
||||
});
|
||||
const kosten = sheet('s-kosten', 'Kosten', {
|
||||
A2: 'Nord', B2: '800', C2: '820', D2: '900', E2: '880',
|
||||
});
|
||||
const uebersicht = sheet('s-ueber', 'Übersicht', {
|
||||
B2: '=SUM(Umsatz!B2:E2)', C2: '=SUM(Kosten!B2:E2)',
|
||||
D2: '=B2-C2', E2: '=D2/B2*100',
|
||||
B8: '=SUM(B2:B6)', D8: '=SUM(D2:D6)',
|
||||
});
|
||||
return new Workbook([umsatz, kosten, uebersicht]);
|
||||
}
|
||||
|
||||
const val = (wb: Workbook, sheetId: string, a1: string) => {
|
||||
const m = /^([A-Z]+)([0-9]+)$/.exec(a1)!;
|
||||
const col = m[1].split('').reduce((n, ch) => n * 26 + (ch.charCodeAt(0) - 64), 0) - 1;
|
||||
return wb.cellValue(sheetId, col, Number(m[2]) - 1);
|
||||
};
|
||||
|
||||
describe('Workbook aggregates', () => {
|
||||
const wb = fixture();
|
||||
it('SUM/AVG/MIN/MAX/COUNT of Umsatz!B2:B6', () => {
|
||||
expect(val(wb, 's-umsatz', 'B8')).toBe(6175);
|
||||
expect(val(wb, 's-umsatz', 'B9')).toBe(1235);
|
||||
expect(val(wb, 's-umsatz', 'B10')).toBe(980);
|
||||
expect(val(wb, 's-umsatz', 'B11')).toBe(1550);
|
||||
expect(val(wb, 's-umsatz', 'B12')).toBe(5);
|
||||
});
|
||||
it('row helpers', () => {
|
||||
expect(val(wb, 's-umsatz', 'H2')).toBe(5970); // 1200+1450+1600+1720
|
||||
expect(val(wb, 's-umsatz', 'H3')).toBe(1492.5);
|
||||
expect(val(wb, 's-umsatz', 'H4')).toBe(520);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workbook cross-sheet', () => {
|
||||
it('resolves Name!Range across sheets', () => {
|
||||
const wb = fixture();
|
||||
expect(val(wb, 's-ueber', 'B2')).toBe(5970);
|
||||
expect(val(wb, 's-ueber', 'C2')).toBe(3400);
|
||||
expect(val(wb, 's-ueber', 'D2')).toBe(2570);
|
||||
expect(val(wb, 's-ueber', 'E2')).toBeCloseTo((2570 / 5970) * 100, 6);
|
||||
});
|
||||
|
||||
it('editing Umsatz!B2 propagates to Übersicht B2/D2/E2 and the Gesamt row', () => {
|
||||
const wb = fixture();
|
||||
wb.setCell('s-umsatz', 1, 1, '2200'); // B2: 1200 -> 2200 (+1000)
|
||||
expect(val(wb, 's-umsatz', 'B8')).toBe(7175);
|
||||
expect(val(wb, 's-ueber', 'B2')).toBe(6970);
|
||||
expect(val(wb, 's-ueber', 'D2')).toBe(3570);
|
||||
expect(val(wb, 's-ueber', 'B8')).toBe(6970); // only row 2 populated in fixture
|
||||
expect(val(wb, 's-ueber', 'D8')).toBe(3570);
|
||||
});
|
||||
|
||||
it('is tolerant about sheet-name case and quoting', () => {
|
||||
const wb = fixture();
|
||||
wb.setCell('s-kosten', 0, 8, "='Kosten'!B2"); // A9 = 'Kosten'!B2
|
||||
wb.setCell('s-kosten', 1, 8, '=kosten!b2'); // B9 lower-case
|
||||
expect(val(wb, 's-kosten', 'A9')).toBe(800);
|
||||
expect(val(wb, 's-kosten', 'B9')).toBe(800);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workbook error values', () => {
|
||||
const wb = fixture();
|
||||
it('classifies each error kind', () => {
|
||||
wb.setCell('s-umsatz', 6, 20, '=1/0');
|
||||
wb.setCell('s-umsatz', 6, 21, '=FOO(1)');
|
||||
wb.setCell('s-umsatz', 6, 22, '=A1+');
|
||||
wb.setCell('s-umsatz', 6, 23, '=Nix!A1');
|
||||
expect((val(wb, 's-umsatz', 'G21') as FormulaError).code).toBe('#DIV/0!');
|
||||
expect((val(wb, 's-umsatz', 'G22') as FormulaError).code).toBe('#NAME?');
|
||||
expect((val(wb, 's-umsatz', 'G23') as FormulaError).code).toBe('#ERR');
|
||||
expect((val(wb, 's-umsatz', 'G24') as FormulaError).code).toBe('#REF');
|
||||
});
|
||||
|
||||
it('detects a direct circular reference', () => {
|
||||
const wb2 = fixture();
|
||||
wb2.setCell('s-umsatz', 0, 0, '=A1');
|
||||
expect((val(wb2, 's-umsatz', 'A1') as FormulaError).code).toBe('#CIRC');
|
||||
});
|
||||
|
||||
it('detects a cross-sheet circular reference', () => {
|
||||
const wb2 = fixture();
|
||||
wb2.setCell('s-umsatz', 0, 0, "='Kosten'!Z9");
|
||||
wb2.setCell('s-kosten', 25, 8, '=Umsatz!A1');
|
||||
expect((val(wb2, 's-umsatz', 'A1') as FormulaError).code).toBe('#CIRC');
|
||||
expect((val(wb2, 's-kosten', 'Z9') as FormulaError).code).toBe('#CIRC');
|
||||
});
|
||||
|
||||
it('errors propagate to dependents', () => {
|
||||
const wb2 = fixture();
|
||||
wb2.setCell('s-umsatz', 1, 1, '=1/0');
|
||||
expect(isError(val(wb2, 's-umsatz', 'B8'))).toBe(true);
|
||||
expect(isError(val(wb2, 's-ueber', 'B2'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workbook structural changes', () => {
|
||||
it('deleting a referenced sheet turns dependents into #REF', () => {
|
||||
const wb = fixture();
|
||||
wb.tables = wb.tables.filter((t) => t.id !== 's-kosten');
|
||||
wb.rebuild();
|
||||
expect((val(wb, 's-ueber', 'C2') as FormulaError).code).toBe('#REF');
|
||||
// unrelated cells still fine
|
||||
expect(val(wb, 's-ueber', 'B2')).toBe(5970);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workbook recalculation order', () => {
|
||||
it('evaluates precedents before dependents (chain across sheets)', () => {
|
||||
const wb = fixture();
|
||||
// chain: Umsatz!B2 -> Übersicht!B2 -> Übersicht!D2 -> Übersicht!E2
|
||||
const { order, circular } = wb.debugOrder([keyOf('s-umsatz', 1, 1)]);
|
||||
expect(circular).toEqual([]);
|
||||
const pos = (k: string) => order.indexOf(k);
|
||||
expect(pos(keyOf('s-ueber', 1, 1))).toBeGreaterThan(-1);
|
||||
expect(pos(keyOf('s-ueber', 1, 1))).toBeLessThan(pos(keyOf('s-ueber', 3, 1)));
|
||||
expect(pos(keyOf('s-ueber', 3, 1))).toBeLessThan(pos(keyOf('s-ueber', 4, 1)));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user