timetracker/test/features/timer/timer_notifier_test.dart
2026-08-03 21:51:48 +02:00

158 lines
5.5 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:timetrack/core/database/app_database.dart' as appdb;
import 'package:timetrack/core/database/daos/projects_dao.dart';
import 'package:timetrack/features/entries/data/drift_entries_repository.dart';
import 'package:timetrack/features/entries/data/entries_repository.dart';
import 'package:timetrack/features/entries/domain/time_entry.dart';
import 'package:timetrack/features/projects/domain/project.dart';
import 'package:timetrack/features/timer/domain/timer_notifier.dart';
import 'package:timetrack/features/timer/domain/timer_state.dart';
// Mocks
class MockTimeEntriesRepository extends Mock implements TimeEntriesRepository {}
class MockAppDatabase extends Mock implements appdb.AppDatabase {}
class MockProjectsDao extends Mock implements ProjectsDao {}
class FakeTimeEntry extends Fake implements TimeEntry {}
void main() {
setUpAll(() {
registerFallbackValue(FakeTimeEntry());
});
late MockTimeEntriesRepository mockRepo;
late MockAppDatabase mockDb;
late MockProjectsDao mockProjectsDao;
final testProject = Project(
id: 1,
name: 'Test',
colorValue: 0xFF2563EB,
createdAt: DateTime(2026),
);
final activeEntry = TimeEntry(
id: 42,
projectId: 1,
startTime: DateTime(2026, 7, 1, 9),
tags: const [],
createdAt: DateTime(2026, 7, 1, 9),
);
setUp(() {
mockRepo = MockTimeEntriesRepository();
mockDb = MockAppDatabase();
mockProjectsDao = MockProjectsDao();
when(() => mockDb.projectsDao).thenReturn(mockProjectsDao);
// Default: no project found (safe fallback for restore)
when(() => mockProjectsDao.getById(any()))
.thenAnswer((_) async => null);
});
ProviderContainer buildContainer({TimeEntry? activeEntryOverride}) {
when(() => mockRepo.getActiveEntry())
.thenAnswer((_) async => activeEntryOverride);
return ProviderContainer(
overrides: [
timeEntriesRepositoryProvider.overrideWithValue(mockRepo),
appdb.appDatabaseProvider.overrideWithValue(mockDb),
],
);
}
group('TimerNotifier', () {
test('build starts idle when no active entry', () async {
final container = buildContainer(activeEntryOverride: null);
addTearDown(container.dispose);
await Future<void>.delayed(const Duration(milliseconds: 50));
final state = container.read(timerNotifierProvider);
expect(state, isA<TimerIdle>());
});
test('build restores running state from DB active entry', () async {
// Override the default null-returning stub for id=1
when(() => mockProjectsDao.getById(1))
.thenAnswer((_) async => appdb.Project(
id: 1,
name: 'Test',
colorValue: 0xFF2563EB,
createdAt: DateTime(2026),
));
final container = buildContainer(activeEntryOverride: activeEntry);
addTearDown(container.dispose);
// Trigger provider creation and keep it alive with a listener
container.listen(timerNotifierProvider, (prev, next) {});
await Future<void>.delayed(const Duration(milliseconds: 500));
final state = container.read(timerNotifierProvider);
expect(state, isA<TimerRunning>());
expect((state as TimerRunning).entryId, equals(42));
});
test('start transitions to running state', () async {
final container = buildContainer(activeEntryOverride: null);
addTearDown(container.dispose);
await Future<void>.delayed(const Duration(milliseconds: 50));
when(() => mockRepo.create(any())).thenAnswer((_) async => 99);
await container
.read(timerNotifierProvider.notifier)
.start(testProject);
final state = container.read(timerNotifierProvider);
expect(state, isA<TimerRunning>());
expect((state as TimerRunning).project.name, equals('Test'));
});
test('stop transitions to idle and saves entry', () async {
final container = buildContainer(activeEntryOverride: null);
addTearDown(container.dispose);
await Future<void>.delayed(const Duration(milliseconds: 50));
when(() => mockRepo.create(any())).thenAnswer((_) async => 99);
// After start(), getActiveEntry should return the running entry
var callCount = 0;
when(() => mockRepo.getActiveEntry()).thenAnswer((_) async {
callCount++;
if (callCount == 1) return null; // initial restore check
return activeEntry;
});
when(() => mockRepo.update(any())).thenAnswer((_) async {});
await container
.read(timerNotifierProvider.notifier)
.start(testProject);
await container.read(timerNotifierProvider.notifier).stop();
final state = container.read(timerNotifierProvider);
expect(state, isA<TimerIdle>());
});
test('discard transitions to idle and deletes entry', () async {
final container = buildContainer(activeEntryOverride: null);
addTearDown(container.dispose);
await Future<void>.delayed(const Duration(milliseconds: 50));
when(() => mockRepo.create(any())).thenAnswer((_) async => 99);
when(() => mockRepo.delete(any())).thenAnswer((_) async {});
await container
.read(timerNotifierProvider.notifier)
.start(testProject);
await container.read(timerNotifierProvider.notifier).discard();
final state = container.read(timerNotifierProvider);
expect(state, isA<TimerIdle>());
verify(() => mockRepo.delete(99)).called(1);
});
});
}