From e0d5adb91cef23b5f5db94fd1d1da7c8b2a2c365 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 09:00:00 +0200 Subject: [PATCH] test(model): add workbook, dependency and json tests --- test/dependency_test.dart | 93 ++++++++++++++++++++++++++++ test/workbook_json_test.dart | 42 +++++++++++++ test/workbook_test.dart | 116 +++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 test/dependency_test.dart create mode 100644 test/workbook_json_test.dart create mode 100644 test/workbook_test.dart diff --git a/test/dependency_test.dart b/test/dependency_test.dart new file mode 100644 index 0000000..bfff7d8 --- /dev/null +++ b/test/dependency_test.dart @@ -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); + }); + }); +} diff --git a/test/workbook_json_test.dart b/test/workbook_json_test.dart new file mode 100644 index 0000000..ddb8a76 --- /dev/null +++ b/test/workbook_json_test.dart @@ -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); + }); + }); +} diff --git a/test/workbook_test.dart b/test/workbook_test.dart new file mode 100644 index 0000000..85753e8 --- /dev/null +++ b/test/workbook_test.dart @@ -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)); + }); + }); +}