diff --git a/pubspec.lock b/pubspec.lock index 14d42c7..27c534e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + url: "https://pub.dev" + source: hosted + version: "1.8.0" cross_file: dependency: transitive description: @@ -503,7 +511,7 @@ packages: source: hosted version: "2.0.9" flutter_test: - dependency: "direct dev" + dependency: "direct main" description: flutter source: sdk version: "0.0.0" @@ -528,6 +536,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.6.6" + get_test: + dependency: "direct main" + description: + name: get_test + sha256: "558c39cb35fb37bd501f337dc143de60a4314d5ef3b75f4b0551d6741634995b" + url: "https://pub.dev" + source: hosted + version: "4.0.1" glob: dependency: transitive description: @@ -760,6 +776,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + mockito: + dependency: "direct main" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" nm: dependency: transitive description: @@ -768,6 +792,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.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: @@ -1037,6 +1069,22 @@ packages: 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: @@ -1058,6 +1106,30 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + 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: @@ -1154,6 +1226,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct main" + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: @@ -1162,6 +1242,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" time: dependency: transitive description: @@ -1362,6 +1450,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 86c46f4..dafde94 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,10 @@ dependencies: flutter_platform_widgets: ^6.0.2 flutter_slidable: ^3.0.1 flutter_svg: ^2.0.7 + flutter_test: + sdk: flutter get: ^4.6.5 + get_test: ^4.0.1 google_fonts: ^6.1.0 hive: ^2.2.1 hive_flutter: ^1.1.0 @@ -41,6 +44,7 @@ dependencies: in_app_update: ^4.2.3 intl: ^0.18.0 loggy: ^2.0.1+1 + mockito: ^5.4.4 package_info_plus: ^4.0.2 pem: ^2.0.1 permission_handler: @@ -53,6 +57,7 @@ dependencies: sizer: ^2.0.15 sqflite: ^2.3.3+1 syncfusion_flutter_charts: ^23.2.7 + test: ^1.25.2 timezone: ^0.9.2 tuple: ^2.0.0 tutorial_coach_mark: ^1.2.11 @@ -63,8 +68,6 @@ dev_dependencies: build_runner: null flutter_gen_runner: null flutter_lints: 4.0.0 - flutter_test: - sdk: flutter flutter_gen: output: lib/app/utils/gen/ diff --git a/test/taskfunctions/comparator_test.dart b/test/taskfunctions/comparator_test.dart new file mode 100644 index 0000000..38e8712 --- /dev/null +++ b/test/taskfunctions/comparator_test.dart @@ -0,0 +1,66 @@ +import 'package:test/test.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/comparator.dart'; +import 'package:taskwarrior/app/models/json/task.dart'; + +void main() { + final task1 = Task((builder) => builder + ..entry = DateTime(2024, 7, 20) + ..modified = DateTime(2024, 7, 21) + ..start = DateTime(2024, 7, 22) + ..due = DateTime(2024, 7, 23) + ..priority = 'H' + ..project = 'Project A' + ..tags.replace(['tag1', 'tag2']) + ..status = 'pending' + ..uuid = 'uuid1' + ..description = 'Task 1 Description' + ); + + final task2 = Task((builder) => builder + ..entry = DateTime(2024, 7, 19) + ..modified = DateTime(2024, 7, 18) + ..start = DateTime(2024, 7, 21) + ..due = DateTime(2024, 7, 22) + ..priority = 'L' + ..project = 'Project B' + ..tags.replace(['tag1']) + ..status = 'pending' + ..uuid = 'uuid2' + ..description = 'Task 2 Description' + ); + + test('Test compareTasks for Created column', () { + final compare = compareTasks('Created'); + expect(compare(task1, task2), greaterThan(0)); + }); + + test('Test compareTasks for Modified column', () { + final compare = compareTasks('Modified'); + expect(compare(task1, task2), greaterThan(0)); + }); + + test('Test compareTasks for Start Time column', () { + final compare = compareTasks('Start Time'); + expect(compare(task1, task2), greaterThan(0)); + }); + + test('Test compareTasks for Priority column', () { + final compare = compareTasks('Priority'); + expect(compare(task1, task2), greaterThan(0)); + }); + + test('Test compareTasks for Project column', () { + final compare = compareTasks('Project'); + expect(compare(task1, task2), lessThan(0)); + }); + + test('Test compareTasks for Tags column', () { + final compare = compareTasks('Tags'); + expect(compare(task1, task2), greaterThan(0)); + }); + + test('Test compareTasks for Urgency column', () { + final compare = compareTasks('Urgency'); + expect(compare(task1, task2), lessThan(0)); + }); +} diff --git a/test/taskfunctions/datetime_differences_test.dart b/test/taskfunctions/datetime_differences_test.dart new file mode 100644 index 0000000..434771e --- /dev/null +++ b/test/taskfunctions/datetime_differences_test.dart @@ -0,0 +1,96 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/datetime_differences.dart'; + +void main() { + group('DateTime Functions Tests', () { + test('Test age function', () { + DateTime dt = DateTime.now().subtract(const Duration(days: 365)); + String result = age(dt); + expect(result, contains('12mo ')); + + dt = DateTime.now().subtract(const Duration(days: 60)); + result = age(dt); + expect(result, contains('2mo ')); + + dt = DateTime.now().subtract(const Duration(days: 21)); + result = age(dt); + expect(result, contains('3w ')); + + dt = DateTime.now().subtract(const Duration(days: 4)); + result = age(dt); + expect(result, contains('4d ')); + + dt = DateTime.now().subtract(const Duration(hours: 5)); + result = age(dt); + expect(result, contains('5h ')); + + dt = DateTime.now().subtract(const Duration(minutes: 10)); + result = age(dt); + expect(result, contains('10min ')); + + dt = DateTime.now().subtract(const Duration(seconds: 30)); + result = age(dt); + expect(result, contains('30s ')); + }); + + test('Test when function', () { + DateTime dt = DateTime.now().add(const Duration(days: 365)); + String result = when(dt); + expect(result, contains('12mo')); + + dt = DateTime.now().add(const Duration(days: 60)); + result = when(dt); + expect(result, contains('1mo')); + + dt = DateTime.now().add(const Duration(days: 21)); + result = when(dt); + expect(result, contains('2w')); + + dt = DateTime.now().add(const Duration(days: 4)); + result = when(dt); + expect(result, contains('3d')); + + dt = DateTime.now().add(const Duration(hours: 5)); + result = when(dt); + expect(result, contains('4h')); + + dt = DateTime.now().add(const Duration(minutes: 10)); + result = when(dt); + expect(result, contains('9min')); + + dt = DateTime.now().add(const Duration(seconds: 30)); + result = when(dt); + expect(result, contains('29s')); + }); + + test('Test difference function', () { + DateTime dt = DateTime.now().subtract(const Duration(days: 365)); + String result = difference(DateTime.now().difference(dt)); + expect(result, contains('12mo ')); + + dt = DateTime.now().subtract(const Duration(days: 60)); + result = difference(DateTime.now().difference(dt)); + expect(result, contains('2mo ')); + + dt = DateTime.now().subtract(const Duration(days: 21)); + result = difference(DateTime.now().difference(dt)); + expect(result, contains('3w ')); + + dt = DateTime.now().subtract(const Duration(days: 4)); + result = difference(DateTime.now().difference(dt)); + expect(result, contains('4d ')); + + dt = DateTime.now().subtract(const Duration(hours: 5)); + result = difference(DateTime.now().difference(dt)); + expect(result, contains('5h ')); + + dt = DateTime.now().subtract(const Duration(minutes: 10)); + result = difference(DateTime.now().difference(dt)); + expect(result, contains('10min ')); + + dt = DateTime.now().subtract(const Duration(seconds: 30)); + result = difference(DateTime.now().difference(dt)); + expect(result, contains('30s ')); + }); + }); +} diff --git a/test/taskfunctions/draft_test.dart b/test/taskfunctions/draft_test.dart new file mode 100644 index 0000000..debe15a --- /dev/null +++ b/test/taskfunctions/draft_test.dart @@ -0,0 +1,92 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/models/models.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/draft.dart'; + +void main() { + group('Draft Tests', () { + test('Setting status to completed updates end date', () { + Task original = Task( + (b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..start = DateTime.utc(2023, 1, 1) + ..priority = 'H' + ..project = 'Project A' + ..tags.replace(['tag1', 'tag2']) + ..uuid = 'uuid1' + ..description = 'Task 1 Description' + ..entry = DateTime(2024, 7, 20) + ..modified = DateTime(2024, 7, 21) + ..start = DateTime(2024, 7, 22) + ..due = DateTime(2024, 7, 23), + ); + + Draft draft = Draft(original); + + draft.set('status', 'completed'); + + expect(draft.draft.status, 'completed'); + + expect(draft.draft.end, isNotNull); + }); + + test('Setting status to pending does not update end date', () { + Task original = Task( + (b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'completed' + ..start = DateTime.utc(2023, 1, 1) + ..priority = 'H' + ..project = 'Project A' + ..tags.replace(['tag1', 'tag2']) + ..uuid = 'uuid2' + ..description = 'Task 2 Description' + ..entry = DateTime(2024, 7, 20) + ..modified = DateTime(2024, 7, 21) + ..start = DateTime(2024, 7, 22) + ..due = DateTime(2024, 7, 23), + ); + + Draft draft = Draft(original); + + draft.set('status', 'pending'); + + expect(draft.draft.status, 'pending'); + + expect(draft.draft.end, isNull); + }); + + test('Setting other properties updates correctly', () { + Task original = Task( + (b) => b + ..id = 1 + ..description = 'Task 3' + ..status = 'pending' + ..start = DateTime.utc(2023, 1, 1) + ..priority = 'H' + ..project = 'Project A' + ..tags.replace(['tag1', 'tag2']) + ..uuid = 'uuid3' + ..description = 'Task 3 Description' + ..entry = DateTime(2024, 7, 20) + ..modified = DateTime(2024, 7, 21) + ..start = DateTime(2024, 7, 22) + ..due = DateTime(2024, 7, 23), + ); + + Draft draft = Draft(original); + + draft.set('priority', 'L'); + + expect(draft.draft.priority, 'L'); + + expect(draft.draft.id, original.id); + + expect(draft.draft.description, original.description); + + expect(draft.draft.status, original.status); + }); + }); +} diff --git a/test/taskfunctions/patch_test.dart b/test/taskfunctions/patch_test.dart new file mode 100644 index 0000000..f262265 --- /dev/null +++ b/test/taskfunctions/patch_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/models/models.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/patch.dart'; + +void main() { + group('patch function', () { + test('should patch description', () { + Task initialTask = Task( + (b) => b + ..id = 1 + ..description = 'Task 3' + ..status = 'pending' + ..start = DateTime.utc(2023, 1, 1) + ..priority = 'H' + ..project = 'Project A' + ..tags.replace(['tag1', 'tag2']) + ..uuid = 'uuid3' + ..description = 'Task 3 Description' + ..entry = DateTime(2024, 7, 20) + ..modified = DateTime(2024, 7, 21) + ..start = DateTime(2024, 7, 22) + ..due = DateTime(2024, 7, 23), + ); + Map updates = {'description': 'Updated description'}; + + Task patchedTask = patch(initialTask, updates); + + expect(patchedTask.description, 'Updated description'); + }); + + test('should handle unknown keys gracefully', () { + Task initialTask = Task( + (b) => b + ..id = 1 + ..description = 'Task 3' + ..status = 'pending' + ..start = DateTime.utc(2023, 1, 1) + ..priority = 'H' + ..project = 'Project A' + ..tags.replace(['tag1', 'tag2']) + ..uuid = 'uuid3' + ..description = 'Task 3 Description' + ..entry = DateTime(2024, 7, 20) + ..modified = DateTime(2024, 7, 21) + ..start = DateTime(2024, 7, 22) + ..due = DateTime(2024, 7, 23), + ); + Map updates = {'unknownField': 'some value'}; + + Task patchedTask = patch(initialTask, updates); + + expect(patchedTask, initialTask); + }); + }); +} diff --git a/test/taskfunctions/profiles_test.dart b/test/taskfunctions/profiles_test.dart new file mode 100644 index 0000000..c8c34e1 --- /dev/null +++ b/test/taskfunctions/profiles_test.dart @@ -0,0 +1,67 @@ +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/profiles.dart'; + +void main() { + late Directory testDirectory; + late Profiles profiles; + + setUp(() async { + testDirectory = await Directory.systemTemp.createTemp('test_directory'); + profiles = Profiles(testDirectory); + }); + + tearDown(() { + testDirectory.deleteSync(recursive: true); + }); + + test('Add and list profiles', () { + var profileId = profiles.addProfile(); + + expect(profiles.listProfiles(), contains(profileId)); + }); + + test('Set and get alias', () { + var profileId = profiles.addProfile(); + + profiles.setAlias(profile: profileId, alias: 'Test Alias'); + + var alias = profiles.getAlias(profileId); + expect(alias, 'Test Alias'); + }); + + test('Set and get current profile', () { + var profileId = profiles.addProfile(); + + profiles.setCurrentProfile(profileId); + + var currentProfile = profiles.getCurrentProfile(); + expect(currentProfile, profileId); + }); + + test('Delete profile', () { + var profileId = profiles.addProfile(); + + profiles.deleteProfile(profileId); + + expect(profiles.listProfiles(), isNot(contains(profileId))); + }); + + test('Copy configuration to new profile', () { + var profileId = profiles.addProfile(); + + profiles.copyConfigToNewProfile(profileId); + + expect(Directory('${testDirectory.path}/profiles').listSync(), + hasLength(greaterThan(1))); + }); + + test('Get current storage', () { + var profileId = profiles.addProfile(); + profiles.setCurrentProfile(profileId); + + var storage = profiles.getCurrentStorage(); + + expect(storage, isNotNull); + }); +} diff --git a/test/taskfunctions/projects_test.dart b/test/taskfunctions/projects_test.dart new file mode 100644 index 0000000..b25288e --- /dev/null +++ b/test/taskfunctions/projects_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/projects.dart'; + +void main() { + test('sparseDecoratedProjectTree test', () { + var projects1 = { + 'projectA': 5, + 'projectA.subprojectB': 3, + 'projectA.subprojectC': 2, + 'projectA.subprojectC.subsubprojectD': 1, + }; + var result1 = sparseDecoratedProjectTree(projects1); + expect(result1.keys.toSet(), { + 'projectA', + 'projectA.subprojectB', + 'projectA.subprojectC', + 'projectA.subprojectC.subsubprojectD', + }); + expect(result1['projectA']!.children, { + 'projectA.subprojectB', + 'projectA.subprojectC', + }); + expect(result1['projectA.subprojectC']!.children, { + 'projectA.subprojectC.subsubprojectD', + }); + + var projects2 = { + 'projectX': 10, + 'projectX.subprojectY': 5, + 'projectX.subprojectZ': 3, + 'projectX.subprojectZ.subsubprojectA': 2, + 'projectX.subprojectZ.subsubprojectB': 1, + 'projectX.subprojectZ.subsubprojectC': 4, + }; + var result2 = sparseDecoratedProjectTree(projects2); + expect(result2.keys.toSet(), { + 'projectX', + 'projectX.subprojectY', + 'projectX.subprojectZ', + 'projectX.subprojectZ.subsubprojectA', + 'projectX.subprojectZ.subsubprojectB', + 'projectX.subprojectZ.subsubprojectC', + }); + expect(result2['projectX.subprojectZ.subsubprojectA']!.parent, 'projectX.subprojectZ'); + expect(result2['projectX.subprojectZ.subsubprojectC']!.parent, 'projectX.subprojectZ'); + + var projects3 = { + 'rootProject': 0, + }; + var result3 = sparseDecoratedProjectTree(projects3); + expect(result3.keys, ['rootProject']); + expect(result3['rootProject']!.children.isEmpty, true); + expect(result3['rootProject']!.parent, null); + + var projects4 = { + 'projectP': 2, + 'projectP.subprojectQ': 3, + 'projectP.subprojectQ.subsubprojectR': 1, + }; + var result4 = sparseDecoratedProjectTree(projects4); + expect(result4.keys.toSet(), { + 'projectP', + 'projectP.subprojectQ', + 'projectP.subprojectQ.subsubprojectR', + }); + expect(result4['projectP.subprojectQ.subsubprojectR']!.parent, 'projectP.subprojectQ'); + + + }); +} diff --git a/test/taskfunctions/query_test.dart b/test/taskfunctions/query_test.dart new file mode 100644 index 0000000..93396ef --- /dev/null +++ b/test/taskfunctions/query_test.dart @@ -0,0 +1,72 @@ +import 'dart:io'; +import 'package:taskwarrior/app/utils/taskfunctions/query.dart'; +import 'package:test/test.dart'; + +void main() { + late Directory tempDir; + late Query query; + + setUp(() { + tempDir = Directory.systemTemp.createTempSync('query_test'); + query = Query(tempDir); + }); + + tearDown(() { + tempDir.deleteSync(recursive: true); + }); + + test('setSelectedSort and getSelectedSort', () { + const selectedSort = 'priority-'; + query.setSelectedSort(selectedSort); + expect(query.getSelectedSort(), selectedSort); + }); + + test('togglePendingFilter and getPendingFilter', () { + expect(query.getPendingFilter(), true); + query.togglePendingFilter(); + expect(query.getPendingFilter(), false); + query.togglePendingFilter(); + expect(query.getPendingFilter(), true); + }); + + test('toggleWaitingFilter and getWaitingFilter', () { + expect(query.getWaitingFilter(), true); + query.toggleWaitingFilter(); + expect(query.getWaitingFilter(), false); + query.toggleWaitingFilter(); + expect(query.getWaitingFilter(), true); + }); + + test('toggleProjectFilter and projectFilter', () { + const project = 'Work'; + query.toggleProjectFilter(project); + expect(query.projectFilter(), project); + query.toggleProjectFilter(project); + expect(query.projectFilter(), ''); + }); + + test('toggleTagUnion and tagUnion', () { + expect(query.tagUnion(), false); + query.toggleTagUnion(); + expect(query.tagUnion(), true); + query.toggleTagUnion(); + expect(query.tagUnion(), false); + }); + + test('toggleTagFilter and getSelectedTags', () { + const tag1 = 'important'; + const tag2 = 'urgent'; + + query.toggleTagFilter(tag1); + var tags = query.getSelectedTags(); + expect(tags.contains('+$tag1'), true); + + query.toggleTagFilter(tag1); + tags = query.getSelectedTags(); + expect(tags.contains('+$tag1'), false); + + query.toggleTagFilter(tag2); + tags = query.getSelectedTags(); + expect(tags.contains('+$tag2'), true); + }); +} diff --git a/test/taskfunctions/tags_test.dart b/test/taskfunctions/tags_test.dart new file mode 100644 index 0000000..c2067b2 --- /dev/null +++ b/test/taskfunctions/tags_test.dart @@ -0,0 +1,318 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/models/models.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/tags.dart'; + +void main() { + group('tagSet', () { + test('should return all unique tags from tasks', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..tags.replace(['tag1', 'tag2']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..tags.replace(['tag2', 'tag3']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + ]; + + final result = tagSet(tasks); + expect(result, {'tag1', 'tag2', 'tag3'}); + }); + + test('should return an empty set if no tasks', () { + final tasks = []; + + final result = tagSet(tasks); + expect(result, {}); + }); + + test('should return an empty set if tasks have no tags', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + ]; + + final result = tagSet(tasks); + expect(result, {}); + }); + }); + + group('tagFrequencies', () { + test('should count the frequency of each tag correctly', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..tags.replace(['tag1', 'tag2']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..tags.replace(['tag2', 'tag3']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + Task((b) => b + ..id = 3 + ..description = 'Task 3' + ..status = 'pending' + ..tags.replace(['tag2']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project C' + ..uuid = 'uuid3'), + ]; + + final result = tagFrequencies(tasks); + expect(result, {'tag1': 1, 'tag2': 3, 'tag3': 1}); + }); + + test('should return an empty map if no tasks', () { + final tasks = []; + + final result = tagFrequencies(tasks); + expect(result, {}); + }); + + test('should return an empty map if tasks have no tags', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + ]; + + final result = tagFrequencies(tasks); + expect(result, {}); + }); + + test('should count tags correctly with overlapping tasks', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..tags.replace(['tag1', 'tag2']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..tags.replace(['tag1', 'tag2', 'tag3']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + Task((b) => b + ..id = 3 + ..description = 'Task 3' + ..status = 'pending' + ..tags.replace(['tag2', 'tag3']) + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project C' + ..uuid = 'uuid3'), + ]; + + final result = tagFrequencies(tasks); + expect(result, {'tag1': 2, 'tag2': 3, 'tag3': 2}); + }); + }); + + group('tagsLastModified', () { + test('should return the latest modification date for each tag', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..tags.replace(['tag1']) + ..modified = DateTime.utc(2024, 1, 1) + ..start = DateTime.now() + ..entry = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..tags.replace(['tag2']) + ..modified = DateTime.utc(2024, 2, 1) + ..start = DateTime.now() + ..entry = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + Task((b) => b + ..id = 3 + ..description = 'Task 3' + ..status = 'pending' + ..tags.replace(['tag1', 'tag3']) + ..modified = DateTime.utc(2024, 3, 1) + ..start = DateTime.now() + ..entry = DateTime.now() + ..due = DateTime.now() + ..project = 'Project C' + ..uuid = 'uuid3'), + ]; + + final result = tagsLastModified(tasks); + expect(result, { + 'tag1': DateTime.utc(2024, 3, 1), + 'tag2': DateTime.utc(2024, 2, 1), + 'tag3': DateTime.utc(2024, 3, 1), + }); + }); + + test('should return an empty map if no tasks', () { + final tasks = []; + + final result = tagsLastModified(tasks); + expect(result, {}); + }); + + test('should return an empty map if tasks have no tags', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..start = DateTime.now() + ..entry = DateTime.now() + ..modified = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + ]; + + final result = tagsLastModified(tasks); + expect(result, {}); + }); + + test('should return correct modification dates with overlapping tags', () { + final tasks = [ + Task((b) => b + ..id = 1 + ..description = 'Task 1' + ..status = 'pending' + ..tags.replace(['tag1', 'tag2']) + ..modified = DateTime.utc(2024, 1, 1) + ..start = DateTime.now() + ..entry = DateTime.now() + ..due = DateTime.now() + ..project = 'Project A' + ..uuid = 'uuid1'), + Task((b) => b + ..id = 2 + ..description = 'Task 2' + ..status = 'pending' + ..tags.replace(['tag2', 'tag3']) + ..modified = DateTime.utc(2024, 2, 1) + ..start = DateTime.now() + ..entry = DateTime.now() + ..due = DateTime.now() + ..project = 'Project B' + ..uuid = 'uuid2'), + Task((b) => b + ..id = 3 + ..description = 'Task 3' + ..status = 'pending' + ..tags.replace(['tag3']) + ..modified = DateTime.utc(2024, 3, 1) + ..start = DateTime.now() + ..entry = DateTime.now() + ..due = DateTime.now() + ..project = 'Project C' + ..uuid = 'uuid3'), + ]; + + final result = tagsLastModified(tasks); + expect(result, { + 'tag1': DateTime.utc(2024, 1, 1), + 'tag2': DateTime.utc(2024, 2, 1), + 'tag3': DateTime.utc(2024, 3, 1), + }); + }); + }); +} diff --git a/test/taskfunctions/taskparser_test.dart b/test/taskfunctions/taskparser_test.dart new file mode 100644 index 0000000..4f1973b --- /dev/null +++ b/test/taskfunctions/taskparser_test.dart @@ -0,0 +1,64 @@ +import 'package:taskwarrior/app/utils/taskfunctions/taskparser.dart'; +import 'package:test/test.dart'; + +void main() { + group('taskParser', () { + test('parses a task with multiple attributes and tags', () { + const taskString = + '+tag1 status:completed project:work priority:high due:2024-12-31T23:59:59Z This is a complex task'; + final task = taskParser(taskString); + expect(task.description, 'This is a complex task'); + expect(task.status, 'completed'); + expect(task.project, 'work'); + expect(task.priority, 'high'); + expect(task.due, DateTime.parse('2024-12-31T23:59:59Z').toUtc()); + expect(task.wait, isNull); + expect(task.until, isNull); + expect(task.tags, ['tag1']); + }); + + test('parses a task with long description and tags', () { + const taskString = '+longtag1 +longtag2 ' + 'This is a very long description that goes on and on, potentially including special characters and a very long string to test how the parser handles such cases'; + final task = taskParser(taskString); + expect(task.description, + 'This is a very long description that goes on and on, potentially including special characters and a very long string to test how the parser handles such cases'); + expect(task.status, 'pending'); + expect(task.project, isNull); + expect(task.priority, isNull); + expect(task.due, isNull); + expect(task.wait, isNull); + expect(task.until, isNull); + expect(task.tags, ['longtag1', 'longtag2']); + }); + + test('parses a task with mixed attribute formats', () { + const taskString = + 'status:completed prio:high project:work +tag1 This is a task with mixed attribute formats'; + final task = taskParser(taskString); + expect(task.description, 'This is a task with mixed attribute formats'); + expect(task.status, 'completed'); + expect(task.project, 'work'); + expect(task.priority, 'high'); + expect(task.due, isNull); + expect(task.wait, isNull); + expect(task.until, isNull); + expect(task.tags, ['tag1']); + }); + + test('parses a task with overlapping attributes and tags', () { + const taskString = + '+tag1 status:completed project:work +tag2 This is a task with overlapping attributes and tags'; + final task = taskParser(taskString); + expect(task.description, + 'This is a task with overlapping attributes and tags'); + expect(task.status, 'completed'); + expect(task.project, 'work'); + expect(task.priority, isNull); + expect(task.due, isNull); + expect(task.wait, isNull); + expect(task.until, isNull); + expect(task.tags, ['tag1', 'tag2']); + }); + }); +} diff --git a/test/taskfunctions/validate_test.dart b/test/taskfunctions/validate_test.dart new file mode 100644 index 0000000..03c2c5a --- /dev/null +++ b/test/taskfunctions/validate_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/validate.dart'; + +void main() { + test('validateTaskDescription', () { + expect(() => validateTaskDescription('Write test cases'), returnsNormally); + + expect(() => validateTaskDescription(''), throwsFormatException); + + expect(() => validateTaskDescription('Do something\\'), throwsFormatException); + }); + + test('validateTaskProject', () { + expect(() => validateTaskProject('Personal'), returnsNormally); + + expect(() => validateTaskProject('Work\\'), throwsFormatException); + }); + + test('validateTaskTags', () { + expect(() => validateTaskTags('important'), returnsNormally); + + expect(() => validateTaskTags('urgent tasks'), throwsFormatException); + }); +}