test(model): add workbook, dependency and json tests

This commit is contained in:
dualfroz committed 2026-08-28 09:00:00 +02:00
1 parent 47ea8bb501
commit e0d5adb91c
3 files changed
+251

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
import 'package:cellar/cellar.dart';
import 'package:test/test.dart';
double numberOf(CellValue value) => (value as NumberValue).value;
void main() {
group('dependency graph recalculation', () {
test('changing a precedent recomputes a dependent', () {
final sheet = Sheet();
sheet.setValue('A1', '5');
sheet.setValue('B1', '=A1+1');
expect(numberOf(sheet.valueOf('B1')), 6);
sheet.setValue('A1', '10');
expect(numberOf(sheet.valueOf('B1')), 11);
});
test('a chain propagates through several levels', () {
final sheet = Sheet();
sheet.setValue('A1', '2');
sheet.setValue('B1', '=A1*2');
sheet.setValue('C1', '=B1*2');
sheet.setValue('D1', '=C1+1');
expect(numberOf(sheet.valueOf('D1')), 9);
sheet.setValue('A1', '5');
expect(numberOf(sheet.valueOf('B1')), 10);
expect(numberOf(sheet.valueOf('C1')), 20);
expect(numberOf(sheet.valueOf('D1')), 21);
});
test('a single precedent feeds multiple dependents', () {
final sheet = Sheet();
sheet.setValue('A1', '3');
sheet.setValue('B1', '=A1+1');
sheet.setValue('B2', '=A1*10');
sheet.setValue('B3', '=A1-1');
sheet.setValue('A1', '7');
expect(numberOf(sheet.valueOf('B1')), 8);
expect(numberOf(sheet.valueOf('B2')), 70);
expect(numberOf(sheet.valueOf('B3')), 6);
});
test('range dependencies recompute when a member changes', () {
final sheet = Sheet();
sheet.setValue('A1', '1');
sheet.setValue('A2', '2');
sheet.setValue('A3', '3');
sheet.setValue('B1', '=SUM(A1:A3)');
expect(numberOf(sheet.valueOf('B1')), 6);
sheet.setValue('A2', '20');
expect(numberOf(sheet.valueOf('B1')), 24);
});
});
group('cycle detection', () {
test('two cells referencing each other become #CYCLE!', () {
final sheet = Sheet();
sheet.setValue('A1', '=B1');
sheet.setValue('B1', '=A1');
expect(sheet.valueOf('A1'), const ErrorValue(ErrorKind.cycle));
expect(sheet.valueOf('B1'), const ErrorValue(ErrorKind.cycle));
});
test('self reference is a cycle', () {
final sheet = Sheet();
sheet.setValue('A1', '=A1+1');
expect(sheet.valueOf('A1'), const ErrorValue(ErrorKind.cycle));
});
test('a three cell cycle is detected', () {
final sheet = Sheet();
sheet.setValue('A1', '=B1');
sheet.setValue('B1', '=C1');
sheet.setValue('C1', '=A1');
expect(sheet.valueOf('A1'), const ErrorValue(ErrorKind.cycle));
expect(sheet.valueOf('B1'), const ErrorValue(ErrorKind.cycle));
expect(sheet.valueOf('C1'), const ErrorValue(ErrorKind.cycle));
});
test('breaking a cycle restores computation', () {
final sheet = Sheet();
sheet.setValue('A1', '=B1');
sheet.setValue('B1', '=A1');
expect(sheet.valueOf('A1'), const ErrorValue(ErrorKind.cycle));
sheet.setValue('B1', '5');
expect(numberOf(sheet.valueOf('B1')), 5);
expect(numberOf(sheet.valueOf('A1')), 5);
});
});
}
+42
View File
@@ -0,0 +1,42 @@
import 'package:cellar/cellar.dart';
import 'package:test/test.dart';
double num(CellValue value) => (value as NumberValue).value;
void main() {
group('workbook JSON store', () {
test('round trips values, formulas, formats and names', () {
final workbook = Workbook();
final sheet1 = workbook.addSheet('Sheet1');
final data = workbook.addSheet('Data');
sheet1.setValue('A1', '10');
sheet1.setValue('A2', '20');
sheet1.setValue('A3', '=SUM(A1:A2)');
sheet1.setFormat('A3', const NumberFormat.currency('zl', before: false, thousands: false));
data.setValue('B1', '=Sheet1!A3 * 2');
workbook.defineName('Total', 'Sheet1!A3');
final encoded = encodeWorkbookJson(workbook);
final reloaded = decodeWorkbookJson(encoded);
expect(reloaded.sheetNames, ['Sheet1', 'Data']);
expect(num(reloaded.sheet('Sheet1').valueOf('A3')), 30);
expect(reloaded.sheet('Sheet1').displayAt(CellAddress.parse('A3')), '30.00zl');
expect(num(reloaded.sheet('Data').valueOf('B1')), 60);
expect(reloaded.resolveName('Total'), isNotNull);
});
test('reloaded workbook recomputes after an edit', () {
final workbook = Workbook();
final sheet1 = workbook.addSheet('Sheet1');
final data = workbook.addSheet('Data');
sheet1.setValue('A1', '5');
data.setValue('B1', '=Sheet1!A1 + 100');
final reloaded = decodeWorkbookJson(encodeWorkbookJson(workbook));
expect(num(reloaded.sheet('Data').valueOf('B1')), 105);
reloaded.sheet('Sheet1').setValue('A1', '7');
expect(num(reloaded.sheet('Data').valueOf('B1')), 107);
});
});
}
+116
View File
@@ -0,0 +1,116 @@
import 'package:cellar/cellar.dart';
import 'package:test/test.dart';
double numberOf(CellValue value) => (value as NumberValue).value;
void main() {
group('multi-sheet workbook', () {
test('a workbook holds several named sheets', () {
final workbook = Workbook();
workbook.addSheet('Sheet1');
workbook.addSheet('Data');
expect(workbook.sheetCount, 2);
expect(workbook.sheetNames, ['Sheet1', 'Data']);
});
test('cross sheet reference resolves the other sheet', () {
final workbook = Workbook();
final first = workbook.addSheet('Sheet1');
final data = workbook.addSheet('Data');
first.setValue('A1', '10');
first.setValue('A2', '20');
data.setValue('B1', '=Sheet1!A1 + Sheet1!A2');
expect(numberOf(data.valueOf('B1')), 30);
});
test('cross sheet range aggregation', () {
final workbook = Workbook();
final first = workbook.addSheet('Sheet1');
final data = workbook.addSheet('Data');
first.setValue('A1', '1');
first.setValue('A2', '2');
first.setValue('A3', '3');
data.setValue('B1', '=SUM(Sheet1!A1:A3) * 2');
expect(numberOf(data.valueOf('B1')), 12);
});
test('editing a precedent recomputes a dependent on another sheet', () {
final workbook = Workbook();
final first = workbook.addSheet('Sheet1');
final data = workbook.addSheet('Data');
first.setValue('A1', '5');
data.setValue('B1', '=Sheet1!A1 * 3');
expect(numberOf(data.valueOf('B1')), 15);
first.setValue('A1', '9');
expect(numberOf(data.valueOf('B1')), 27);
});
test('quoted sheet names with spaces', () {
final workbook = Workbook();
final first = workbook.addSheet('My Sheet');
final data = workbook.addSheet('Data');
first.setValue('A1', '42');
data.setValue('B1', "='My Sheet'!A1 + 1");
expect(numberOf(data.valueOf('B1')), 43);
});
test('reference to an unknown sheet is #REF!', () {
final workbook = Workbook();
final data = workbook.addSheet('Data');
data.setValue('A1', '=Missing!A1');
expect(data.valueOf('A1'), const ErrorValue(ErrorKind.ref));
});
test('a cycle spanning two sheets is detected', () {
final workbook = Workbook();
final first = workbook.addSheet('Sheet1');
final data = workbook.addSheet('Data');
first.setValue('A1', '=Data!A1');
data.setValue('A1', '=Sheet1!A1');
expect(first.valueOf('A1'), const ErrorValue(ErrorKind.cycle));
expect(data.valueOf('A1'), const ErrorValue(ErrorKind.cycle));
});
});
group('named ranges', () {
test('a named cell resolves to its value', () {
final workbook = Workbook();
final sheet = workbook.addSheet('Sheet1');
sheet.setValue('B2', '99');
workbook.defineName('Answer', 'Sheet1!B2');
sheet.setValue('C1', '=Answer + 1');
expect(numberOf(sheet.valueOf('C1')), 100);
});
test('a named range works inside an aggregate', () {
final workbook = Workbook();
final sheet = workbook.addSheet('Sheet1');
sheet.setValue('A1', '10');
sheet.setValue('A2', '20');
sheet.setValue('A3', '30');
workbook.defineName('Inputs', 'Sheet1!A1:A3');
sheet.setValue('B1', '=SUM(Inputs)');
expect(numberOf(sheet.valueOf('B1')), 60);
});
test('a named range defined after the formula still propagates', () {
final workbook = Workbook();
final sheet = workbook.addSheet('Sheet1');
sheet.setValue('A1', '5');
sheet.setValue('A2', '7');
sheet.setValue('B1', '=SUM(Prices)');
expect(sheet.valueOf('B1'), const ErrorValue(ErrorKind.name));
workbook.defineName('Prices', 'Sheet1!A1:A2');
expect(numberOf(sheet.valueOf('B1')), 12);
sheet.setValue('A1', '50');
expect(numberOf(sheet.valueOf('B1')), 57);
});
test('an unknown bare name is #NAME?', () {
final workbook = Workbook();
final sheet = workbook.addSheet('Sheet1');
sheet.setValue('A1', '=Nope + 1');
expect(sheet.valueOf('A1'), const ErrorValue(ErrorKind.name));
});
});
}