158 lines
5.5 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|