feat: initial commit - Habo habit tracking app

- 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)
This commit is contained in:
2026-04-13 15:02:30 +00:00
commit aa69f2a91e
212 changed files with 16694 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
import 'dart:convert';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:habo/habits/habit.dart';
import 'package:habo/model/habit_data.dart';
import 'package:intl/intl.dart';
void main() {
group('Backup Enhancement Tests', () {
group('Timestamp format', () {
test('should use correct timestamp format', () {
final now = DateTime(2023, 12, 25, 15, 30, 45);
final formatted = DateFormat('yyyy-MM-dd_HH-mm-ss').format(now);
expect(formatted, '2023-12-25_15-30-45');
});
test('should handle different times correctly', () {
final morning = DateTime(2023, 1, 1, 9, 0, 0);
final formatted = DateFormat('yyyy-MM-dd_HH-mm-ss').format(morning);
expect(formatted, '2023-01-01_09-00-00');
});
});
group('Backup structure', () {
test('should create backup with correct structure', () async {
final testHabits = [
Habit(
habitData: HabitData(
position: 0,
title: 'Test Habit',
twoDayRule: true,
cue: 'Morning coffee',
routine: '10 pushups',
reward: 'Feel energized',
showReward: true,
advanced: true,
notification: true,
notTime: const TimeOfDay(hour: 8, minute: 0),
events: SplayTreeMap<DateTime, List<dynamic>>(),
sanction: 'No dessert',
showSanction: true,
accountant: 'self',
),
)
];
// Test JSON serialization directly
final jsonData = jsonEncode(testHabits);
expect(jsonData, isNotEmpty);
expect(jsonData, contains('Test Habit'));
expect(jsonData, contains('pushups'));
final restoredHabits = jsonDecode(jsonData);
expect(restoredHabits, isList);
expect(restoredHabits.length, 1);
});
test('should handle empty habits list', () async {
final emptyHabits = <Habit>[];
// Test JSON serialization directly
final jsonData = jsonEncode(emptyHabits);
expect(jsonData, isNotEmpty);
expect(jsonDecode(jsonData), isEmpty);
});
});
});
}

View File

@@ -0,0 +1,377 @@
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');
});
});
});
}

View File

@@ -0,0 +1,157 @@
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:habo/habits/habits_manager.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:habo/habits/habit.dart';
import 'package:mocktail/mocktail.dart';
import 'package:habo/model/habit_data.dart';
import 'package:habo/constants.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: '',
));
registerFallbackValue(const TimeOfDay(hour: 0, minute: 0));
});
setUp(() {
mockHabitRepository = MockHabitRepository();
mockEventRepository = MockEventRepository();
mockCategoryRepository = MockCategoryRepository();
mockBackupService = MockBackupService();
mockNotificationService = MockNotificationService();
mockUIFeedbackService = MockUIFeedbackService();
// Setup mock returns
when(() => mockEventRepository.insertEvent(any(), any(), any()))
.thenAnswer((_) => Future.value());
when(() => mockEventRepository.deleteEvent(any(), any()))
.thenAnswer((_) => Future.value());
when(() => mockEventRepository.getEventsForHabit(any()))
.thenAnswer((_) => Future.value([]));
habitsManager = HabitsManager(
habitRepository: mockHabitRepository,
eventRepository: mockEventRepository,
categoryRepository: mockCategoryRepository,
backupService: mockBackupService,
notificationService: mockNotificationService,
uiFeedbackService: mockUIFeedbackService,
);
});
group('Notification Tests', () {
test('should schedule notifications for habits', () async {
// Arrange
final testHabit = Habit(
habitData: HabitData(
position: 0,
title: 'Test Habit',
twoDayRule: false,
cue: '',
routine: '',
reward: '',
showReward: false,
advanced: false,
events: SplayTreeMap<DateTime, List>(),
notification: true,
notTime: const TimeOfDay(hour: 9, minute: 0),
sanction: '',
showSanction: false,
accountant: '',
),
);
when(() => mockHabitRepository.getAllHabits())
.thenAnswer((_) async => [testHabit]);
// Act
habitsManager.resetNotifications([testHabit]);
// Assert
verify(() => mockNotificationService.resetNotifications(any())).called(1);
});
test('should handle habit event addition', () async {
// Arrange
final today = DateTime.now();
final event = [DayType.check];
// Act
habitsManager.addEvent(1, today, event);
// Assert
verify(() => mockEventRepository.insertEvent(1, today, event)).called(1);
});
test('should handle habit event deletion', () async {
// Arrange
final today = DateTime.now();
// Act
habitsManager.deleteEvent(1, today);
// Assert
verify(() => mockEventRepository.deleteEvent(1, today)).called(1);
});
});
}

View File

@@ -0,0 +1,465 @@
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);
}
});
});
});
});
}

View File

@@ -0,0 +1,224 @@
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:habo/generated/l10n.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;
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,
// Don't pass uiFeedbackService to avoid localization
);
});
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
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);
// Initialize localization for tests
S.load(const Locale('en'));
});
group('HabitsManager with Repository Pattern', () {
test('should initialize with provided repositories', () {
expect(habitsManager, isNotNull);
});
test('should load habits from repository', () async {
// Setup
final testHabit = Habit(
habitData: HabitData(
position: 0,
title: 'Test Habit',
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>(),
sanction: '',
showSanction: false,
accountant: '',
),
);
when(() => mockHabitRepository.getAllHabits())
.thenAnswer((_) async => [testHabit]);
// Act
await habitsManager.initModel();
// Assert
verify(() => mockHabitRepository.getAllHabits()).called(1);
expect(habitsManager.allHabits.length, 1);
expect(habitsManager.allHabits[0].habitData.title, 'Test Habit');
});
test('should add habit through repository', () async {
// Setup
when(() => mockHabitRepository.createHabit(any()))
.thenAnswer((_) async => 1);
when(() => mockHabitRepository.getAllHabits())
.thenAnswer((_) async => []);
// Act
habitsManager.addHabit(
'New 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 update habit through repository', () async {
// Setup
final testHabit = Habit(
habitData: HabitData(
id: 1,
position: 0,
title: 'Original',
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 through repository', () 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 {});
// Mock specific localization strings
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);
});
});
}