From 8c02dae118f8320add1960533f9c81ecdf506c1c Mon Sep 17 00:00:00 2001 From: m3mo Date: Wed, 4 Feb 2026 14:40:19 +0100 Subject: [PATCH] Add unit tests for DailyTasksViewModel - Test loading tasks with success and error states - Test priority sorting (high > medium > low) - Test filter functionality (all, active, completed) - Test task toggle and delete operations - Test date navigation (previousDay, nextDay) - Use mocktail for repository mocking --- .../daily_tasks_viewmodel_test.dart | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 test/features/tasks/presentation/viewmodels/daily_tasks_viewmodel_test.dart diff --git a/test/features/tasks/presentation/viewmodels/daily_tasks_viewmodel_test.dart b/test/features/tasks/presentation/viewmodels/daily_tasks_viewmodel_test.dart new file mode 100644 index 0000000..b764591 --- /dev/null +++ b/test/features/tasks/presentation/viewmodels/daily_tasks_viewmodel_test.dart @@ -0,0 +1,260 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'package:agenda_tasks/core/errors/failures.dart'; +import 'package:agenda_tasks/core/errors/result.dart'; +import 'package:agenda_tasks/core/logging/app_logger.dart'; +import 'package:agenda_tasks/features/tasks/domain/entities/task_entity.dart'; +import 'package:agenda_tasks/features/tasks/domain/enums/priority.dart'; +import 'package:agenda_tasks/features/tasks/domain/repositories/task_repository.dart'; +import 'package:agenda_tasks/features/tasks/presentation/viewmodels/daily_tasks_viewmodel.dart'; + +// Mock classes +class MockTaskRepository extends Mock implements TaskRepository {} + +class MockAppLogger extends Mock implements AppLogger {} + +void main() { + late DailyTasksViewModel viewModel; + late MockTaskRepository mockRepository; + late MockAppLogger mockLogger; + + setUp(() { + mockRepository = MockTaskRepository(); + mockLogger = MockAppLogger(); + viewModel = DailyTasksViewModel( + repository: mockRepository, + logger: mockLogger, + ); + }); + + group('DailyTasksViewModel', () { + final testDate = DateTime(2024, 1, 15); + + // Helper function to create fresh test data for each test + List createTestTasks() => [ + TaskEntity( + id: '1', + title: 'Task 1', + date: testDate, + priority: Priority.high, + isDone: false, + ), + TaskEntity( + id: '2', + title: 'Task 2', + date: testDate, + priority: Priority.medium, + isDone: true, + ), + TaskEntity( + id: '3', + title: 'Task 3', + date: testDate, + priority: Priority.low, + isDone: false, + ), + ]; + + group('loadTasks', () { + test('should set status to loading then success when tasks loaded', () async { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success(createTestTasks())); + + // Act + final future = viewModel.loadTasks(); + + // Assert - initial state should be loading + expect(viewModel.status, equals(TasksStatus.loading)); + + await future; + + // After completion + expect(viewModel.status, equals(TasksStatus.success)); + expect(viewModel.tasks.length, equals(3)); + }); + + test('should set status to error when loading fails', () async { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Error(const NetworkFailure())); + + // Act + await viewModel.loadTasks(); + + // Assert + expect(viewModel.status, equals(TasksStatus.error)); + expect(viewModel.failure, isA()); + }); + + test('should sort tasks by priority (high > medium > low)', () async { + // Arrange + final unsortedTasks = [ + TaskEntity(id: '1', title: 'Low', date: testDate, priority: Priority.low, isDone: false), + TaskEntity(id: '2', title: 'High', date: testDate, priority: Priority.high, isDone: false), + TaskEntity(id: '3', title: 'Medium', date: testDate, priority: Priority.medium, isDone: false), + ]; + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success(unsortedTasks)); + + // Act + await viewModel.loadTasks(); + + // Assert + expect(viewModel.tasks[0].priority, equals(Priority.high)); + expect(viewModel.tasks[1].priority, equals(Priority.medium)); + expect(viewModel.tasks[2].priority, equals(Priority.low)); + }); + }); + + group('setFilter', () { + setUp(() async { + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success(createTestTasks())); + await viewModel.loadTasks(); + }); + + test('should show all tasks when filter is all', () { + // Act + viewModel.setFilter(TaskFilter.all); + + // Assert + expect(viewModel.filter, equals(TaskFilter.all)); + expect(viewModel.tasks.length, equals(3)); + }); + + test('should show only active tasks when filter is active', () { + // Act + viewModel.setFilter(TaskFilter.active); + + // Assert + expect(viewModel.filter, equals(TaskFilter.active)); + expect(viewModel.tasks.length, equals(2)); + expect(viewModel.tasks.every((t) => !t.isDone), isTrue); + }); + + test('should show only completed tasks when filter is completed', () { + // Act + viewModel.setFilter(TaskFilter.completed); + + // Assert + expect(viewModel.filter, equals(TaskFilter.completed)); + expect(viewModel.tasks.length, equals(1)); + expect(viewModel.tasks.every((t) => t.isDone), isTrue); + }); + }); + + group('toggleTask', () { + test('should update task in list when toggle succeeds', () async { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success(createTestTasks())); + await viewModel.loadTasks(); + + final toggledTask = createTestTasks()[0].copyWith(isDone: true); + when(() => mockRepository.toggleTaskStatus('1')) + .thenAnswer((_) async => Success(toggledTask)); + + // Act + await viewModel.toggleTask('1'); + + // Assert + final updatedTask = viewModel.tasks.firstWhere((t) => t.id == '1'); + expect(updatedTask.isDone, isTrue); + }); + + test('should set failure when toggle fails', () async { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success(createTestTasks())); + await viewModel.loadTasks(); + + when(() => mockRepository.toggleTaskStatus('1')) + .thenAnswer((_) async => Error(const ServerFailure(message: 'Error'))); + + // Act + await viewModel.toggleTask('1'); + + // Assert + expect(viewModel.failure, isA()); + }); + }); + + group('deleteTask', () { + test('should remove task from list when delete succeeds', () async { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success(createTestTasks())); + await viewModel.loadTasks(); + + when(() => mockRepository.deleteTask('1')) + .thenAnswer((_) async => const Success(null)); + + // Act + await viewModel.deleteTask('1'); + + // Assert + expect(viewModel.tasks.any((t) => t.id == '1'), isFalse); + expect(viewModel.totalCount, equals(2)); + }); + }); + + group('date navigation', () { + test('should update selectedDate when setSelectedDate is called', () { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success([])); + + // Act + viewModel.setSelectedDate(testDate); + + // Assert + expect(viewModel.selectedDate.year, equals(2024)); + expect(viewModel.selectedDate.month, equals(1)); + expect(viewModel.selectedDate.day, equals(15)); + }); + + test('previousDay should subtract one day', () { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success([])); + viewModel.setSelectedDate(testDate); + + // Act + viewModel.previousDay(); + + // Assert + expect(viewModel.selectedDate.day, equals(14)); + }); + + test('nextDay should add one day', () { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success([])); + viewModel.setSelectedDate(testDate); + + // Act + viewModel.nextDay(); + + // Assert + expect(viewModel.selectedDate.day, equals(16)); + }); + }); + + group('counts', () { + test('should return correct total and completed counts', () async { + // Arrange + when(() => mockRepository.getTasksByDate(any())) + .thenAnswer((_) async => Success(createTestTasks())); + + // Act + await viewModel.loadTasks(); + + // Assert + expect(viewModel.totalCount, equals(3)); + expect(viewModel.completedCount, equals(1)); + }); + }); + }); +}