diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 11599b2..c1c2b45 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1 +1,2 @@ -exec flutter analyze \ No newline at end of file +flutter analyze +flutter test \ No newline at end of file diff --git a/lib/screens/new_project/participant_tile.dart b/lib/screens/new_project/participant_tile.dart index f99e7b7..a8d8a3f 100644 --- a/lib/screens/new_project/participant_tile.dart +++ b/lib/screens/new_project/participant_tile.dart @@ -5,6 +5,8 @@ import '../../models/participant.dart'; import '../../models/project.dart'; import '../../utils/helper/confirm_box.dart'; +// TODO: make immutable +// ignore: must_be_immutable class ParticipantTile extends StatefulWidget { ParticipantTile({ super.key, diff --git a/lib/screens/project/expenses/new_entry.dart b/lib/screens/project/expenses/new_entry.dart index cca9ae6..7da2d3b 100644 --- a/lib/screens/project/expenses/new_entry.dart +++ b/lib/screens/project/expenses/new_entry.dart @@ -10,7 +10,7 @@ import '../../../models/item.dart'; import '../../../models/participant.dart'; import '../../../models/project.dart'; import '../../../widgets/new_screen.dart'; -import '../../../utils/ext/text_input_formatter.dart'; +import '../../../utils/helper/text_input_formatter.dart'; import '../../../utils/ext/time.dart'; import 'entry_table.dart'; diff --git a/lib/utils/ext/string.dart b/lib/utils/ext/string.dart index d846636..457d01d 100644 --- a/lib/utils/ext/string.dart +++ b/lib/utils/ext/string.dart @@ -1,9 +1,11 @@ extension StringExtension on String { String capitalize() { + if (length == 0) return ''; return '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; } String firstCapitalize() { + if (length == 0) return ''; return '${this[0].toUpperCase()}${substring(1)}'; } } diff --git a/lib/utils/ext/time.dart b/lib/utils/ext/time.dart index d3a8ceb..bdbf779 100644 --- a/lib/utils/ext/time.dart +++ b/lib/utils/ext/time.dart @@ -66,7 +66,7 @@ extension DateExtension on DateTime { int daysTo(DateTime to) { DateTime from = DateTime(year, month, day); - to = DateTime(from.year, from.month, from.day); + to = DateTime(to.year, to.month, to.day); return (to.difference(from).inHours / 24).round(); } diff --git a/lib/utils/ext/text_input_formatter.dart b/lib/utils/helper/text_input_formatter.dart similarity index 73% rename from lib/utils/ext/text_input_formatter.dart rename to lib/utils/helper/text_input_formatter.dart index e5d1e68..39be1fc 100644 --- a/lib/utils/ext/text_input_formatter.dart +++ b/lib/utils/helper/text_input_formatter.dart @@ -1,3 +1,4 @@ + import 'dart:math'; import 'package:flutter/services.dart'; @@ -15,20 +16,6 @@ class DecimalTextInputFormatter extends TextInputFormatter { TextSelection newSelection = newValue.selection; String truncated = newValue.text; - // try { - // return TextEditingValue( - // text: double.parse(newValue.text).toStringAsPrecision(2), - // selection: newSelection, - // composing: TextRange.empty, - // ); - // } catch (e) { - // return TextEditingValue( - // text: oldValue.text, - // selection: oldValue.selection, - // composing: oldValue.composing, - // ); - // } - if (truncated == '.') { truncated = '0.'; newSelection = newValue.selection.copyWith( diff --git a/pubspec.lock b/pubspec.lock index 55db101..eb89ab8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" + source: hosted + version: "61.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" + source: hosted + version: "5.13.0" app_links: dependency: "direct main" description: @@ -89,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" cross_file: dependency: transitive description: @@ -200,6 +224,22 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" http: dependency: transitive description: @@ -208,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.5" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -232,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: @@ -256,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" matcher: dependency: transitive description: @@ -288,6 +352,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: "direct main" description: @@ -384,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" process: dependency: transitive description: @@ -392,6 +488,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" select_form_field: dependency: "direct main" description: @@ -472,11 +576,59 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.5" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -541,6 +693,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + url: "https://pub.dev" + source: hosted + version: "1.22.0" test_api: dependency: transitive description: @@ -549,6 +709,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + url: "https://pub.dev" + source: hosted + version: "0.4.20" tuple: dependency: "direct main" description: @@ -613,6 +781,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + url: "https://pub.dev" + source: hosted + version: "9.4.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9b4d954..00de2d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dev_dependencies: flutter_launcher_icons: ^0.13.1 flutter_lints: ^2.0.0 + mocktail: ^0.3.0 flutter: uses-material-design: true diff --git a/test/utils/ext/datetime_test.dart b/test/utils/ext/datetime_test.dart new file mode 100644 index 0000000..eb4a301 --- /dev/null +++ b/test/utils/ext/datetime_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitr/utils/ext/datetime.dart'; + +void main() { + group('DateTimeExtension', () { + test('operator <', () { + final dateTime1 = DateTime(2023, 6, 15); + final dateTime2 = DateTime(2023, 6, 16); + + expect(dateTime1 < dateTime2, true); + expect(dateTime2 < dateTime1, false); + expect(dateTime1 < dateTime1, false); + }); + + test('operator <=', () { + final dateTime1 = DateTime(2023, 6, 15); + final dateTime2 = DateTime(2023, 6, 16); + + expect(dateTime1 <= dateTime2, true); + expect(dateTime2 <= dateTime1, false); + expect(dateTime1 <= dateTime1, true); + }); + + test('operator >', () { + final dateTime1 = DateTime(2023, 6, 15); + final dateTime2 = DateTime(2023, 6, 16); + + expect(dateTime1 > dateTime2, false); + expect(dateTime2 > dateTime1, true); + expect(dateTime1 > dateTime1, false); + }); + + test('operator >=', () { + final dateTime1 = DateTime(2023, 6, 15); + final dateTime2 = DateTime(2023, 6, 16); + + expect(dateTime1 >= dateTime2, false); + expect(dateTime2 >= dateTime1, true); + expect(dateTime1 >= dateTime1, true); + }); + }); +} diff --git a/test/utils/ext/list_test.dart b/test/utils/ext/list_test.dart new file mode 100644 index 0000000..4061deb --- /dev/null +++ b/test/utils/ext/list_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:splitr/models/data.dart'; +import 'package:splitr/utils/ext/list.dart'; + +class MockData extends Mock implements Data {} + +void main() { + group('ListExtension', () { + test( + 'setPresence() should add the element if presence is true and not already present', + () { + final list = [1, 2, 3]; + + list.setPresence(true, 4); + expect(list, [1, 2, 3, 4]); + + list.setPresence(true, 3); + expect(list, [1, 2, 3, 4]); + }); + + test('setPresence() should remove the element if presence is false', () { + final list = [1, 2, 3]; + + list.setPresence(false, 2); + expect(list, [1, 3]); + + list.setPresence(false, 4); + expect(list, [1, 3]); + }); + }); + + group('DataListExtension', () { + late List dataList; + late MockData data1; + late MockData data2; + late MockData data3; + + setUp(() { + data1 = MockData(); + data2 = MockData(); + data3 = MockData(); + + dataList = [ + data1, + data2, + data3, + ]; + }); + + test('enabled() should return only enabled elements', () { + when(() => data1.deleted).thenAnswer((invocation) => false); + when(() => data2.deleted).thenAnswer((invocation) => true); + when(() => data3.deleted).thenAnswer((invocation) => false); + + final enabledList = dataList.enabled(); + expect(enabledList.toList(), [data1, data3]); + }); + + test('enabled() should return empty when no enabled elements', () { + when(() => data1.deleted).thenAnswer((invocation) => true); + when(() => data2.deleted).thenAnswer((invocation) => true); + when(() => data3.deleted).thenAnswer((invocation) => true); + + final enabledList = dataList.enabled(); + expect(enabledList.toList(), []); + }); + test('enabled() should not change anything when all enabled elements', () { + when(() => data1.deleted).thenAnswer((invocation) => false); + when(() => data2.deleted).thenAnswer((invocation) => false); + when(() => data3.deleted).thenAnswer((invocation) => false); + + final enabledList = dataList.enabled(); + expect(enabledList.toList(), dataList); + }); + }); +} diff --git a/test/utils/ext/set_test.dart b/test/utils/ext/set_test.dart new file mode 100644 index 0000000..f88e1e5 --- /dev/null +++ b/test/utils/ext/set_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:splitr/models/data.dart'; +import 'package:splitr/utils/ext/set.dart'; + +class MockData extends Mock implements Data {} + +void main() { + group('DataSetExtension', () { + late Set dataList; + late MockData data1; + late MockData data2; + late MockData data3; + + setUp(() { + data1 = MockData(); + data2 = MockData(); + data3 = MockData(); + + dataList = { + data1, + data2, + data3, + }; + }); + + test('enabled() should return only enabled elements', () { + when(() => data1.deleted).thenAnswer((invocation) => false); + when(() => data2.deleted).thenAnswer((invocation) => true); + when(() => data3.deleted).thenAnswer((invocation) => false); + + final enabledList = dataList.enabled(); + expect(enabledList.toSet(), {data1, data3}); + }); + + test('enabled() should return empty when no enabled elements', () { + when(() => data1.deleted).thenAnswer((invocation) => true); + when(() => data2.deleted).thenAnswer((invocation) => true); + when(() => data3.deleted).thenAnswer((invocation) => true); + + final enabledList = dataList.enabled(); + expect(enabledList.toSet(), {}); + }); + test('enabled() should not change anything when all enabled elements', () { + when(() => data1.deleted).thenAnswer((invocation) => false); + when(() => data2.deleted).thenAnswer((invocation) => false); + when(() => data3.deleted).thenAnswer((invocation) => false); + + final enabledList = dataList.enabled(); + expect(enabledList.toSet(), dataList); + }); + }); +} diff --git a/test/utils/ext/string_test.dart b/test/utils/ext/string_test.dart new file mode 100644 index 0000000..b15d0d3 --- /dev/null +++ b/test/utils/ext/string_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitr/utils/ext/string.dart'; + +void main() { + group('StringExtension', () { + test( + 'capitalize() should capitalize the first letter and lowercase the remaining letters', + () { + expect('hEllo WoRlD'.capitalize(), 'Hello world'); + }); + + test('capitalize() should handle an empty string', () { + expect(''.capitalize(), ''); + }); + + test( + 'firstCapitalize() should capitalize the first letter without changing the remaining letters', + () { + expect('hello World'.firstCapitalize(), 'Hello World'); + }); + + test('firstCapitalize() should handle an empty string', () { + expect(''.firstCapitalize(), ''); + }); + + test('firstCapitalize() should handle a string with only one character', + () { + expect('a'.firstCapitalize(), 'A'); + }); + }); +} diff --git a/test/utils/ext/time_test.dart b/test/utils/ext/time_test.dart new file mode 100644 index 0000000..67aff17 --- /dev/null +++ b/test/utils/ext/time_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitr/utils/ext/time.dart'; + +void main() { + group('DateExtension', () { + late DateTime now; + + setUp(() { + now = DateTime( + 2023, 6, 15, 12, 0, 0); // Set a specific date and time for testing + }); + + test('toDate() should format the date correctly for current year', () { + final date = DateTime(DateTime.now().year, 5, 25); + final result = date.toDate(); + expect(result, '25th May'); + }); + + test('toDate() should format the date correctly for previous year', () { + final date = DateTime(2022, 5, 25); + final result = date.toDate(); + expect(result, '25th May 2022'); + }); + + test('toPocketTime() should format the date and time correctly', () { + final date = DateTime(2023, 5, 25, 8, 30, 0); + final result = date.toPocketTime(); + expect(result, '2023-05-25 08:30:00'); + }); + + test('getFullDate() should return the full formatted date', () { + final date = DateTime(2023, 5, 25); + final result = date.getFullDate(); + expect(result, '25 May 2023'); + }); + + test('getDay() should return a new DateTime with the same day', () { + final date = DateTime(2023, 5, 25, 8, 30, 0); + final result = date.getDay(); + expect(result, DateTime(2023, 5, 25)); + }); + + test('daysElapsed() should return the correct days elapsed', () { + final date = DateTime.now().subtract(const Duration(days: 3)); + final result = date.daysElapsed(); + expect(result, '3 days ago'); + }); + + test( + 'daysSince() should return the correct number of days since the given date', + () { + final date = DateTime(2023, 5, 10); + final result = now.daysSince(date); + expect(result, 36); + }); + + test('daysTo() should return the correct number of days to the given date', + () { + final date = DateTime(2023, 6, 20); + final result = now.daysTo(date); + expect(result, 5); + }); + + test('timeElapsed() should return the correct time elapsed', () { + final date = DateTime.now().subtract(const Duration(hours: 1)); + final result = date.timeElapsed(); + expect(result, '1 hour ago'); + }); + }); +} diff --git a/test/utils/helper/random_test.dart b/test/utils/helper/random_test.dart new file mode 100644 index 0000000..3824dab --- /dev/null +++ b/test/utils/helper/random_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:splitr/utils/helper/random.dart'; + +void main() { + group('getRandom', () { + test('getRandom() should return a random string of the specified length', + () { + const length = 10; + final result = getRandom(length); + + expect(result.length, length); + }); + + test( + 'getRandom() should only contain characters from the given character set', + () { + const length = 10; + final result = getRandom(length); + + const validCharacters = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'; + for (var i = 0; i < result.length; i++) { + expect(validCharacters.contains(result[i]), isTrue); + } + }); + + test('getRandom() should return different strings for different calls', () { + const length = 10; + final result1 = getRandom(length); + final result2 = getRandom(length); + + expect(result1, isNot(result2)); + }); + }); +}