- 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)
378 lines
12 KiB
Dart
378 lines
12 KiB
Dart
import 'dart:collection';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:habo/generated/l10n.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/category_repository.dart';
|
|
import 'package:habo/repositories/event_repository.dart';
|
|
import 'package:habo/repositories/habit_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(() async {
|
|
// Initialize localization for tests
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
await S.load(const Locale('en'));
|
|
registerFallbackValue(Habit(
|
|
habitData: HabitData(
|
|
position: 0,
|
|
title: 'Fallback',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
),
|
|
));
|
|
registerFallbackValue(TimeOfDay.now());
|
|
registerFallbackValue(Colors.grey);
|
|
});
|
|
|
|
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(
|
|
id: 1,
|
|
position: 0,
|
|
title: 'Test Habit',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
),
|
|
),
|
|
];
|
|
|
|
when(() => mockHabitRepository.getAllHabits())
|
|
.thenAnswer((_) async => mockHabits);
|
|
|
|
// Act
|
|
await habitsManager.initModel();
|
|
|
|
// Assert
|
|
verify(() => mockHabitRepository.getAllHabits()).called(1);
|
|
});
|
|
|
|
group('CRUD Operations', () {
|
|
test('should add habit', () async {
|
|
// Arrange
|
|
|
|
when(() => mockHabitRepository.createHabit(any()))
|
|
.thenAnswer((_) async => 1);
|
|
|
|
// Act
|
|
habitsManager.addHabit(
|
|
'Test Habit',
|
|
false,
|
|
'Test cue',
|
|
'Test routine',
|
|
'Test reward',
|
|
false,
|
|
false,
|
|
false,
|
|
const TimeOfDay(hour: 9, minute: 0),
|
|
'Test sanction',
|
|
false,
|
|
'Test accountant',
|
|
);
|
|
|
|
// Assert
|
|
verify(() => mockHabitRepository.createHabit(any())).called(1);
|
|
});
|
|
|
|
test('should edit habit', () async {
|
|
// Setup
|
|
final testHabit = Habit(
|
|
habitData: HabitData(
|
|
id: 1,
|
|
position: 0,
|
|
title: 'Test Habit',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
),
|
|
);
|
|
|
|
// Add habit to internal state
|
|
habitsManager.allHabits.add(testHabit);
|
|
when(() => mockHabitRepository.updateHabit(any()))
|
|
.thenAnswer((_) async {});
|
|
|
|
// Act
|
|
habitsManager.editHabit(testHabit.habitData);
|
|
|
|
// Assert
|
|
verify(() => mockHabitRepository.updateHabit(any())).called(1);
|
|
});
|
|
|
|
test('should delete habit', () async {
|
|
// Setup
|
|
final testHabit = Habit(
|
|
habitData: HabitData(
|
|
id: 1,
|
|
position: 0,
|
|
title: 'Test Habit',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
),
|
|
);
|
|
|
|
// Add habit to internal state
|
|
habitsManager.allHabits.add(testHabit);
|
|
when(() => mockHabitRepository.deleteHabit(any()))
|
|
.thenAnswer((_) async {});
|
|
when(() => mockUIFeedbackService.showMessageWithAction(
|
|
message: any(named: 'message'),
|
|
actionLabel: any(named: 'actionLabel'),
|
|
onActionPressed: any(named: 'onActionPressed'),
|
|
backgroundColor: any(named: 'backgroundColor'),
|
|
)).thenReturn(null);
|
|
|
|
// Act
|
|
habitsManager.deleteHabit(1);
|
|
|
|
// Assert - verify internal state changes immediately
|
|
expect(habitsManager.allHabits.length, 0);
|
|
expect(habitsManager.toDelete.length, 1);
|
|
});
|
|
});
|
|
|
|
group('Archive Operations', () {
|
|
test('should archive habit', () async {
|
|
// Setup
|
|
final testHabit = Habit(
|
|
habitData: HabitData(
|
|
id: 1,
|
|
position: 0,
|
|
title: 'Test Habit',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
archived: false,
|
|
),
|
|
);
|
|
|
|
// Add habit to internal state
|
|
habitsManager.allHabits.add(testHabit);
|
|
when(() => mockHabitRepository.updateHabit(any()))
|
|
.thenAnswer((_) async {});
|
|
when(() => mockNotificationService.disableHabitNotification(any()))
|
|
.thenReturn(null);
|
|
when(() => mockUIFeedbackService.showMessageWithAction(
|
|
message: any(named: 'message'),
|
|
actionLabel: any(named: 'actionLabel'),
|
|
onActionPressed: any(named: 'onActionPressed'),
|
|
backgroundColor: any(named: 'backgroundColor'),
|
|
)).thenReturn(null);
|
|
|
|
// Act
|
|
habitsManager.archiveHabit(1);
|
|
|
|
// Assert
|
|
expect(testHabit.habitData.archived, true);
|
|
verify(() => mockHabitRepository.updateHabit(any())).called(1);
|
|
verify(() => mockNotificationService.disableHabitNotification(1))
|
|
.called(1);
|
|
verify(() => mockUIFeedbackService.showMessageWithAction(
|
|
message: any(named: 'message'),
|
|
actionLabel: any(named: 'actionLabel'),
|
|
onActionPressed: any(named: 'onActionPressed'),
|
|
backgroundColor: any(named: 'backgroundColor'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('should unarchive habit', () async {
|
|
// Setup
|
|
final testHabit = Habit(
|
|
habitData: HabitData(
|
|
id: 1,
|
|
position: 0,
|
|
title: 'Test Habit',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: true,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
archived: true,
|
|
),
|
|
);
|
|
|
|
// Add habit to internal state
|
|
habitsManager.allHabits.add(testHabit);
|
|
when(() => mockHabitRepository.updateHabit(any()))
|
|
.thenAnswer((_) async {});
|
|
when(() => mockNotificationService.setHabitNotification(
|
|
any(), any(), any(), any())).thenReturn(null);
|
|
when(() => mockUIFeedbackService.showSuccess(any())).thenReturn(null);
|
|
|
|
// Act
|
|
habitsManager.unarchiveHabit(1);
|
|
|
|
// Assert
|
|
expect(testHabit.habitData.archived, false);
|
|
verify(() => mockHabitRepository.updateHabit(any())).called(1);
|
|
verify(() => mockNotificationService.setHabitNotification(
|
|
1, any(), 'Habo', 'Test Habit')).called(1);
|
|
verify(() => mockUIFeedbackService.showSuccess(any())).called(1);
|
|
});
|
|
|
|
test('should filter active habits correctly', () {
|
|
// Setup
|
|
final activeHabit = Habit(
|
|
habitData: HabitData(
|
|
id: 1,
|
|
position: 0,
|
|
title: 'Active Habit',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
archived: false,
|
|
),
|
|
);
|
|
|
|
final archivedHabit = Habit(
|
|
habitData: HabitData(
|
|
id: 2,
|
|
position: 1,
|
|
title: 'Archived Habit',
|
|
twoDayRule: false,
|
|
cue: '',
|
|
routine: '',
|
|
reward: '',
|
|
showReward: false,
|
|
advanced: false,
|
|
notification: false,
|
|
notTime: const TimeOfDay(hour: 9, minute: 0),
|
|
events: SplayTreeMap<DateTime, List>(),
|
|
sanction: '',
|
|
showSanction: false,
|
|
accountant: '',
|
|
archived: true,
|
|
),
|
|
);
|
|
|
|
// Add habits to internal state
|
|
habitsManager.allHabits.addAll([activeHabit, archivedHabit]);
|
|
|
|
// Act & Assert
|
|
final activeHabits = habitsManager.activeHabits;
|
|
final archivedHabits = habitsManager.archivedHabits;
|
|
|
|
expect(activeHabits.length, 1);
|
|
expect(activeHabits.first.habitData.title, 'Active Habit');
|
|
expect(archivedHabits.length, 1);
|
|
expect(archivedHabits.first.habitData.title, 'Archived Habit');
|
|
});
|
|
});
|
|
});
|
|
}
|