- Complete MVP with Repository Pattern, SQLite storage - Provider + ChangeNotifier state management - Navigation 2.0 with deep link support - Habit CRUD with twoDayRule, notifications, categories - Backup/Restore via JSON - Statistics with streak tracking - Material You theme support - Biometric lock support - Desktop widget support - 27 languages i18n structure - Comprehensive test suite (87/89 passing)
466 lines
16 KiB
Dart
466 lines
16 KiB
Dart
import 'dart:collection';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:habo/habits/habit.dart';
|
|
import 'package:habo/habits/habits_manager.dart';
|
|
import 'package:habo/model/habit_data.dart';
|
|
import 'package:habo/repositories/habit_repository.dart';
|
|
import 'package:habo/repositories/event_repository.dart';
|
|
import 'package:habo/repositories/category_repository.dart';
|
|
import 'package:habo/services/backup_service.dart';
|
|
import 'package:habo/services/notification_service.dart';
|
|
import 'package:habo/services/ui_feedback_service.dart';
|
|
import 'package:mocktail/mocktail.dart';
|
|
|
|
class MockHabitRepository extends Mock implements HabitRepository {}
|
|
|
|
class MockEventRepository extends Mock implements EventRepository {}
|
|
|
|
class MockCategoryRepository extends Mock implements CategoryRepository {}
|
|
|
|
class MockBackupService extends Mock implements BackupService {}
|
|
|
|
class MockNotificationService extends Mock implements NotificationService {}
|
|
|
|
class MockUIFeedbackService extends Mock implements UIFeedbackService {}
|
|
|
|
void main() {
|
|
late HabitsManager habitsManager;
|
|
late MockHabitRepository mockHabitRepository;
|
|
late MockEventRepository mockEventRepository;
|
|
late MockCategoryRepository mockCategoryRepository;
|
|
late MockBackupService mockBackupService;
|
|
late MockNotificationService mockNotificationService;
|
|
late MockUIFeedbackService mockUIFeedbackService;
|
|
|
|
setUpAll(() {
|
|
registerFallbackValue(Habit(
|
|
habitData: HabitData(
|
|
position: 0,
|
|
title: '',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 0, minute: 0),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
),
|
|
));
|
|
registerFallbackValue(HabitData(
|
|
position: 0,
|
|
title: '',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 0, minute: 0),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
));
|
|
});
|
|
|
|
setUp(() {
|
|
mockHabitRepository = MockHabitRepository();
|
|
mockEventRepository = MockEventRepository();
|
|
mockCategoryRepository = MockCategoryRepository();
|
|
mockBackupService = MockBackupService();
|
|
mockNotificationService = MockNotificationService();
|
|
mockUIFeedbackService = MockUIFeedbackService();
|
|
|
|
habitsManager = HabitsManager(
|
|
habitRepository: mockHabitRepository,
|
|
eventRepository: mockEventRepository,
|
|
categoryRepository: mockCategoryRepository,
|
|
backupService: mockBackupService,
|
|
notificationService: mockNotificationService,
|
|
uiFeedbackService: mockUIFeedbackService,
|
|
);
|
|
});
|
|
|
|
group('HabitsManager Tests', () {
|
|
test('should initialize with provided repositories', () {
|
|
expect(habitsManager, isNotNull);
|
|
});
|
|
|
|
test('should populate allHabits from repository', () async {
|
|
// Arrange
|
|
final mockHabits = [
|
|
Habit(
|
|
habitData: HabitData(
|
|
position: 1,
|
|
title: 'Test Habit 1',
|
|
twoDayRule: false,
|
|
cue: 'Test cue',
|
|
routine: 'Test routine',
|
|
reward: 'Test reward',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List<dynamic>>(),
|
|
sanction: 'Test sanction',
|
|
showSanction: false,
|
|
accountant: 'Test accountant',
|
|
),
|
|
),
|
|
];
|
|
|
|
when(() => mockHabitRepository.getAllHabits())
|
|
.thenAnswer((_) async => mockHabits);
|
|
|
|
// Act
|
|
await habitsManager.initModel();
|
|
|
|
// Assert
|
|
verify(() => mockHabitRepository.getAllHabits()).called(1);
|
|
expect(habitsManager.allHabits.length, 1);
|
|
expect(habitsManager.allHabits[0].habitData.title, 'Test Habit 1');
|
|
});
|
|
|
|
test('should handle empty habits list', () async {
|
|
// Arrange
|
|
when(() => mockHabitRepository.getAllHabits())
|
|
.thenAnswer((_) async => []);
|
|
|
|
// Act
|
|
await habitsManager.initModel();
|
|
|
|
// Assert
|
|
expect(habitsManager.allHabits, isEmpty);
|
|
});
|
|
|
|
group('CRUD Operations', () {
|
|
setUp(() async {
|
|
// Setup initial state with empty habits
|
|
when(() => mockHabitRepository.getAllHabits())
|
|
.thenAnswer((_) async => []);
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
await habitsManager.initModel();
|
|
});
|
|
|
|
group('Create Operations', () {
|
|
test('should add a new habit', () async {
|
|
// Arrange
|
|
const testTitle = 'Test Habit';
|
|
const testCue = 'Test cue';
|
|
const testRoutine = 'Test routine';
|
|
const testReward = 'Test reward';
|
|
const testSanction = 'Test sanction';
|
|
const testAccountant = 'Test accountant';
|
|
const testTime = TimeOfDay(hour: 9, minute: 0);
|
|
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
|
|
// Act
|
|
habitsManager.addHabit(
|
|
testTitle,
|
|
false, // twoDayRule
|
|
testCue,
|
|
testRoutine,
|
|
testReward,
|
|
false, // showReward
|
|
false, // advanced
|
|
false, // notification
|
|
testTime,
|
|
testSanction,
|
|
false, // showSanction
|
|
testAccountant,
|
|
);
|
|
await Future.delayed(
|
|
Duration.zero); // Allow async operations to complete
|
|
|
|
// Assert
|
|
expect(habitsManager.allHabits.length, 1);
|
|
expect(habitsManager.allHabits[0].habitData.title, testTitle);
|
|
expect(habitsManager.allHabits[0].habitData.cue, testCue);
|
|
expect(habitsManager.allHabits[0].habitData.routine, testRoutine);
|
|
verify(() => mockHabitRepository.createHabit(any())).called(1);
|
|
});
|
|
|
|
test('should add habit with correct position', () async {
|
|
// Arrange
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
|
|
// Act - Add first habit
|
|
habitsManager.addHabit('First Habit', false, '', '', '', false, false,
|
|
false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
|
|
// Manually set ID for testing
|
|
habitsManager.allHabits[0].habitData.id = 1;
|
|
|
|
// Act - Add second habit
|
|
habitsManager.addHabit('Second Habit', false, '', '', '', false,
|
|
false, false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
|
|
// Manually set ID for testing
|
|
habitsManager.allHabits[1].habitData.id = 2;
|
|
|
|
// Assert
|
|
expect(habitsManager.allHabits.length, 2);
|
|
expect(habitsManager.allHabits[0].habitData.position, 0);
|
|
expect(habitsManager.allHabits[1].habitData.position, 1);
|
|
});
|
|
});
|
|
|
|
group('Read Operations', () {
|
|
setUp(() async {
|
|
// Add some test habits
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
|
|
habitsManager.addHabit('Habit 1', false, '', '', '', false, false,
|
|
false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
habitsManager.allHabits[0].habitData.id = 1;
|
|
|
|
habitsManager.addHabit('Habit 2', false, '', '', '', false, false,
|
|
false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
habitsManager.allHabits[1].habitData.id = 2;
|
|
});
|
|
|
|
test('should find habit by id', () {
|
|
// Act
|
|
final habit = habitsManager.findHabitById(1);
|
|
|
|
// Assert
|
|
expect(habit, isNotNull);
|
|
expect(habit!.habitData.title, 'Habit 1');
|
|
});
|
|
|
|
test('should return null for non-existent habit id', () {
|
|
// Act
|
|
final habit = habitsManager.findHabitById(999);
|
|
|
|
// Assert
|
|
expect(habit, isNull);
|
|
});
|
|
|
|
test('should get habit name by id', () {
|
|
// Act
|
|
final name = habitsManager.getNameOfHabit(1);
|
|
|
|
// Assert
|
|
expect(name, 'Habit 1');
|
|
});
|
|
|
|
test('should return empty string for non-existent habit name', () {
|
|
// Act
|
|
final name = habitsManager.getNameOfHabit(999);
|
|
|
|
// Assert
|
|
expect(name, '');
|
|
});
|
|
});
|
|
|
|
group('Update Operations', () {
|
|
late Habit testHabit;
|
|
|
|
setUp(() async {
|
|
// Setup a test habit
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
when(() => mockHabitRepository.updateHabit(any()))
|
|
.thenAnswer((_) async {});
|
|
when(() => mockHabitRepository.getAllHabits())
|
|
.thenAnswer((_) async => [testHabit]);
|
|
|
|
habitsManager.addHabit(
|
|
'Original Title',
|
|
false,
|
|
'Original cue',
|
|
'Original routine',
|
|
'Original reward',
|
|
false,
|
|
false,
|
|
false,
|
|
const TimeOfDay(hour: 9, minute: 0),
|
|
'Original sanction',
|
|
false,
|
|
'Original accountant');
|
|
await Future.delayed(Duration.zero);
|
|
|
|
testHabit = habitsManager.allHabits[0];
|
|
testHabit.habitData.id = 1;
|
|
});
|
|
|
|
test('should edit existing habit', () async {
|
|
// Arrange
|
|
final updatedData = HabitData(
|
|
position: testHabit.habitData.position,
|
|
title: 'Updated Title',
|
|
twoDayRule: true,
|
|
cue: 'Updated cue',
|
|
routine: 'Updated routine',
|
|
reward: 'Updated reward',
|
|
showReward: true,
|
|
advanced: true,
|
|
notification: true,
|
|
notTime: const TimeOfDay(hour: 10, minute: 30),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: 'Updated sanction',
|
|
showSanction: true,
|
|
accountant: 'Updated accountant',
|
|
);
|
|
updatedData.id = 1;
|
|
|
|
// Act
|
|
habitsManager.editHabit(updatedData);
|
|
|
|
// Assert
|
|
expect(habitsManager.allHabits.length, 1);
|
|
expect(habitsManager.allHabits[0].habitData.title, 'Updated Title');
|
|
expect(habitsManager.allHabits[0].habitData.twoDayRule, true);
|
|
expect(habitsManager.allHabits[0].habitData.cue, 'Updated cue');
|
|
verify(() => mockHabitRepository.updateHabit(any())).called(1);
|
|
});
|
|
|
|
test('should update habit notification settings', () async {
|
|
// Arrange
|
|
final updatedData = HabitData(
|
|
position: testHabit.habitData.position,
|
|
title: testHabit.habitData.title,
|
|
twoDayRule: testHabit.habitData.twoDayRule,
|
|
cue: testHabit.habitData.cue,
|
|
routine: testHabit.habitData.routine,
|
|
reward: testHabit.habitData.reward,
|
|
showReward: testHabit.habitData.showReward,
|
|
advanced: testHabit.habitData.advanced,
|
|
notification: true, // Changed from false to true
|
|
notTime: const TimeOfDay(hour: 15, minute: 45),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: testHabit.habitData.sanction,
|
|
showSanction: testHabit.habitData.showSanction,
|
|
accountant: testHabit.habitData.accountant,
|
|
);
|
|
updatedData.id = 1;
|
|
|
|
// Act
|
|
habitsManager.editHabit(updatedData);
|
|
|
|
// Assert
|
|
expect(habitsManager.allHabits[0].habitData.notification, true);
|
|
expect(habitsManager.allHabits[0].habitData.notTime.hour, 15);
|
|
expect(habitsManager.allHabits[0].habitData.notTime.minute, 45);
|
|
});
|
|
});
|
|
|
|
group('Delete Operations', () {
|
|
late Habit testHabit;
|
|
|
|
setUp(() async {
|
|
// Setup a test habit
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
when(() => mockHabitRepository.deleteHabit(any()))
|
|
.thenAnswer((_) async {});
|
|
|
|
habitsManager.addHabit('Test Habit', false, '', '', '', false, false,
|
|
false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
|
|
testHabit = habitsManager.allHabits[0];
|
|
testHabit.habitData.id = 1;
|
|
});
|
|
|
|
test('should delete habit', () async {
|
|
// Act - Simulate the core deletion logic
|
|
final habitToDelete = habitsManager.findHabitById(1);
|
|
expect(habitToDelete, isNotNull);
|
|
|
|
habitsManager.allHabits.remove(habitToDelete);
|
|
habitsManager.updateOrder();
|
|
|
|
// Assert
|
|
expect(habitsManager.allHabits.length, 0);
|
|
expect(habitsManager.findHabitById(1), isNull);
|
|
});
|
|
|
|
test('should undo delete habit', () async {
|
|
// Arrange - Simulate deletion
|
|
final deletedHabit = habitsManager.findHabitById(1);
|
|
habitsManager.allHabits.remove(deletedHabit);
|
|
|
|
// Act - Undo
|
|
habitsManager.allHabits.insert(0, deletedHabit!);
|
|
habitsManager.updateOrder();
|
|
|
|
// Assert
|
|
expect(habitsManager.allHabits.length, 1);
|
|
expect(habitsManager.allHabits[0], deletedHabit);
|
|
});
|
|
});
|
|
|
|
group('Utility Methods', () {
|
|
setUp(() async {
|
|
// Add some test habits
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
|
|
habitsManager.addHabit('First Habit', false, '', '', '', false, false,
|
|
false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
habitsManager.allHabits[0].habitData.id = 1;
|
|
|
|
habitsManager.addHabit('Second Habit', false, '', '', '', false,
|
|
false, false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
habitsManager.allHabits[1].habitData.id = 2;
|
|
|
|
habitsManager.addHabit('Third Habit', false, '', '', '', false, false,
|
|
false, const TimeOfDay(hour: 9, minute: 0), '', false, '');
|
|
await Future.delayed(Duration.zero);
|
|
habitsManager.allHabits[2].habitData.id = 3;
|
|
});
|
|
|
|
test('should update habit positions correctly', () async {
|
|
// Act - Simulate deletion of middle habit
|
|
final habitToDelete = habitsManager.findHabitById(2);
|
|
habitsManager.allHabits.remove(habitToDelete);
|
|
habitsManager.updateOrder();
|
|
|
|
// Assert positions are updated
|
|
expect(habitsManager.allHabits.length, 2);
|
|
expect(habitsManager.allHabits[0].habitData.position, 0);
|
|
expect(habitsManager.allHabits[1].habitData.position, 1);
|
|
});
|
|
|
|
test('should maintain correct positions after undo', () async {
|
|
// Arrange
|
|
final deletedHabit = habitsManager.allHabits[1];
|
|
|
|
// Act - Simulate delete and undo
|
|
habitsManager.allHabits.remove(deletedHabit);
|
|
habitsManager.updateOrder();
|
|
|
|
habitsManager.allHabits.insert(1, deletedHabit);
|
|
habitsManager.updateOrder();
|
|
|
|
// Assert positions are correct
|
|
expect(habitsManager.allHabits.length, 3);
|
|
for (int i = 0; i < 3; i++) {
|
|
expect(habitsManager.allHabits[i].habitData.position, i);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|