diff --git a/integration_test/mocks/paid_features_mock.dart b/integration_test/mocks/paid_features_mock.dart index 1553d09..fd71c77 100644 --- a/integration_test/mocks/paid_features_mock.dart +++ b/integration_test/mocks/paid_features_mock.dart @@ -1,14 +1,18 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:lightmeter/data/geolocation_service.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/films_provider.dart'; +import 'package:lightmeter/providers/logbook_photos_provider.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:mocktail/mocktail.dart'; class _MockIapStorageService extends Mock implements IapStorageService {} +class _MockGeolocationService extends Mock implements GeolocationService {} + class MockIAPProviders extends StatefulWidget { final TogglableMap equipmentProfiles; final String selectedEquipmentProfileId; @@ -36,11 +40,15 @@ class MockIAPProviders extends StatefulWidget { class _MockIAPProvidersState extends State { late final _MockIapStorageService mockIapStorageService; + late final mockGeolocationService = _MockGeolocationService(); @override void initState() { super.initState(); registerFallbackValue(defaultEquipmentProfile); + registerFallbackValue(defaultCustomPhotos.first); + registerFallbackValue(ApertureValue.values.first); + registerFallbackValue(ShutterSpeedValue.values.first); mockIapStorageService = _MockIapStorageService(); when(() => mockIapStorageService.init()).thenAnswer((_) async {}); @@ -59,6 +67,22 @@ class _MockIAPProvidersState extends State { when(() => mockIapStorageService.getPredefinedFilms()).thenAnswer((_) => Future.value(widget.predefinedFilms)); when(() => mockIapStorageService.getCustomFilms()).thenAnswer((_) => Future.value(widget.customFilms)); when(() => mockIapStorageService.selectedFilmId).thenReturn(widget.selectedFilmId); + + when(() => mockIapStorageService.getPhotos()).thenAnswer((_) async => []); + when(() => mockIapStorageService.addPhoto(any())).thenAnswer((_) async {}); + when( + () => mockIapStorageService.updatePhoto( + id: any(named: 'id'), + note: any(named: 'note'), + apertureValue: any(named: 'apertureValue'), + removeApertureValue: any(named: 'removeApertureValue'), + shutterSpeedValue: any(named: 'shutterSpeedValue'), + removeShutterSpeedValue: any(named: 'removeShutterSpeedValue'), + ), + ).thenAnswer((_) async {}); + when(() => mockIapStorageService.deletePhoto(any())).thenAnswer((_) async {}); + + when(() => mockGeolocationService.getCurrentPosition()).thenAnswer((_) => Future.value()); } @override @@ -67,7 +91,11 @@ class _MockIAPProvidersState extends State { storageService: mockIapStorageService, child: FilmsProvider( storageService: mockIapStorageService, - child: widget.child, + child: LogbookPhotosProvider( + storageService: mockIapStorageService, + geolocationService: mockGeolocationService, + child: widget.child, + ), ), ); } @@ -193,3 +221,24 @@ class _FilmMultiplying extends FilmExponential { @override int get hashCode => Object.hash(id, name, iso, reciprocityMultiplier, runtimeType); } + +final List defaultCustomPhotos = [ + LogbookPhoto( + id: '1', + name: 'test_photo_1.jpg', + timestamp: DateTime(2024, 1, 1, 12), + ev: 12.0, + iso: 100, + nd: 0, + note: 'Test photo 1', + ), + LogbookPhoto( + id: '2', + name: 'test_photo_2.jpg', + timestamp: DateTime(2024, 1, 2, 12), + ev: 13.0, + iso: 200, + nd: 1, + note: 'Test photo 2', + ), +]; diff --git a/lib/application_wrapper.dart b/lib/application_wrapper.dart index c8c9f4a..3cfbe4b 100644 --- a/lib/application_wrapper.dart +++ b/lib/application_wrapper.dart @@ -84,6 +84,7 @@ class _ApplicationWrapperState extends State { onInitialized: filmsStorageServiceCompleter.complete, child: LogbookPhotosProvider( storageService: iapStorageService, + geolocationService: const GeolocationService(), onInitialized: logbookPhotosStorageServiceCompleter.complete, child: UserPreferencesProvider( hasLightSensor: hasLightSensor, diff --git a/lib/providers/logbook_photos_provider.dart b/lib/providers/logbook_photos_provider.dart index 38a79b5..0761d19 100644 --- a/lib/providers/logbook_photos_provider.dart +++ b/lib/providers/logbook_photos_provider.dart @@ -2,7 +2,8 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/data/geolocation_service.dart'; +import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -10,11 +11,13 @@ import 'package:uuid/v8.dart'; class LogbookPhotosProvider extends StatefulWidget { final IapStorageService storageService; + final GeolocationService geolocationService; final VoidCallback? onInitialized; final Widget child; const LogbookPhotosProvider({ required this.storageService, + required this.geolocationService, this.onInitialized, required this.child, super.key, @@ -64,15 +67,14 @@ class LogbookPhotosProviderState extends State { widget.onInitialized?.call(); } - Future addPhotoIfPossible( + Future addPhotoIfPossible( String path, { required double ev100, required int iso, required int nd, }) async { if (context.isPro && _isEnabled) { - final geolocationService = ServicesProvider.of(context).geolocationService; - final coordinates = await geolocationService.getCurrentPosition(); + final coordinates = await widget.geolocationService.getCurrentPosition(); final photo = LogbookPhoto( id: const UuidV8().generate(), @@ -86,12 +88,14 @@ class LogbookPhotosProviderState extends State { await widget.storageService.addPhoto(photo); _photos[photo.id] = photo; setState(() {}); + return photo; } else { - Directory(path).deleteSync(recursive: true); + _deletePhoto(path); + return null; } } - Future updateProfile(LogbookPhoto photo) async { + Future updateLogbookPhoto(LogbookPhoto photo) async { final oldProfile = _photos[photo.id]!; await widget.storageService.updatePhoto( id: photo.id, @@ -105,12 +109,22 @@ class LogbookPhotosProviderState extends State { setState(() {}); } - Future deleteProfile(LogbookPhoto photo) async { + Future deleteLogbookPhoto(LogbookPhoto photo) async { await widget.storageService.deletePhoto(photo.id); _photos.remove(photo.id); - Directory(photo.name).deleteSync(recursive: true); + _deletePhoto(photo.name); setState(() {}); } + + Future _deletePhoto(String path) async { + if (PlatformConfig.cameraStubImage.isEmpty) { + try { + Directory(path).deleteSync(recursive: true); + } catch (e) { + debugPrint(e.toString()); + } + } + } } enum _LogbookPhotosModelAspect { photosList, isEnabled } diff --git a/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart b/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart index 50006c8..4f5268f 100644 --- a/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart +++ b/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart @@ -79,13 +79,13 @@ class LogbookPhotoEditBloc extends Bloc _onSave(LogbookPhotoSaveEvent _, Emitter emit) async { emit(state.copyWith(isLoading: true)); - await photosProvider.updateProfile(_newPhoto); + await photosProvider.updateLogbookPhoto(_newPhoto); emit(state.copyWith(isLoading: false)); } Future _onDelete(LogbookPhotoDeleteEvent _, Emitter emit) async { emit(state.copyWith(isLoading: true)); - await photosProvider.deleteProfile(_newPhoto); + await photosProvider.deleteLogbookPhoto(_newPhoto); emit(state.copyWith(isLoading: false)); } diff --git a/test/application_mock.dart b/test/application_mock.dart index 7034137..00c99ec 100644 --- a/test/application_mock.dart +++ b/test/application_mock.dart @@ -5,6 +5,7 @@ import 'package:light_sensor/light_sensor.dart'; import 'package:lightmeter/data/analytics/analytics.dart'; import 'package:lightmeter/data/analytics/api/analytics_firebase.dart'; import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/geolocation_service.dart'; import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; @@ -153,6 +154,7 @@ class _MockApplicationWrapper extends StatelessWidget { analytics: const LightmeterAnalytics(api: LightmeterAnalyticsFirebase()), caffeineService: const CaffeineService(), environment: const Environment.dev().copyWith(hasLightSensor: true), + geolocationService: const GeolocationService(), hapticsService: const HapticsService(), lightSensorService: const LightSensorService(LocalPlatform()), permissionsService: const PermissionsService(), diff --git a/test/providers/logbook_photos_provider_test.dart b/test/providers/logbook_photos_provider_test.dart index 7a561c9..579effc 100644 --- a/test/providers/logbook_photos_provider_test.dart +++ b/test/providers/logbook_photos_provider_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/geolocation_service.dart'; import 'package:lightmeter/providers/logbook_photos_provider.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -7,16 +8,22 @@ import 'package:mocktail/mocktail.dart'; class _MockLogbookPhotosStorageService extends Mock implements IapStorageService {} +class _MockGeolocationService extends Mock implements GeolocationService {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); late _MockLogbookPhotosStorageService storageService; + late _MockGeolocationService geolocationService; setUpAll(() { storageService = _MockLogbookPhotosStorageService(); + geolocationService = _MockGeolocationService(); + registerFallbackValue(ApertureValue.values.first); + registerFallbackValue(_customPhotos.first); + registerFallbackValue(ShutterSpeedValue.values.first); }); setUp(() { - registerFallbackValue(_customPhotos.first); when(() => storageService.addPhoto(any())).thenAnswer((_) async {}); when( () => storageService.updatePhoto( @@ -30,6 +37,8 @@ void main() { ).thenAnswer((_) async {}); when(() => storageService.deletePhoto(any())).thenAnswer((_) async {}); when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos)); + + when(() => geolocationService.getCurrentPosition()).thenAnswer((_) => Future.value()); }); tearDown(() { @@ -48,6 +57,7 @@ void main() { ], child: LogbookPhotosProvider( storageService: storageService, + geolocationService: geolocationService, child: const _Application(), ), ), @@ -128,11 +138,19 @@ void main() { expectLogbookPhotosCount(0); expectLogbookPhotosEnabled(true); + /// Create a photo + final photo = await tester.logbookPhotosProvider.addPhotoIfPossible( + _customPhotos.first.name, + ev100: _customPhotos.first.ev, + iso: _customPhotos.first.iso, + nd: _customPhotos.first.nd, + ); + /// Update a photo - final updatedPhoto = _customPhotos.first.copyWith(note: 'Updated note'); - await tester.logbookPhotosProvider.updateProfile(updatedPhoto); + final updatedPhoto = photo!.copyWith(note: 'Updated note'); + await tester.logbookPhotosProvider.updateLogbookPhoto(updatedPhoto); await tester.pump(); - expectLogbookPhotosCount(0); // No photos loaded initially + expectLogbookPhotosCount(1); // No photos loaded initially verify( () => storageService.updatePhoto( id: updatedPhoto.id, @@ -143,10 +161,10 @@ void main() { ).called(1); /// Delete a photo - await tester.logbookPhotosProvider.deleteProfile(_customPhotos.first); + await tester.logbookPhotosProvider.deleteLogbookPhoto(updatedPhoto); await tester.pump(); expectLogbookPhotosCount(0); - verify(() => storageService.deletePhoto(_customPhotos.first.id)).called(1); + verify(() => storageService.deletePhoto(updatedPhoto.id)).called(1); }, ); @@ -162,7 +180,7 @@ void main() { // Try to add photo when disabled await tester.logbookPhotosProvider.addPhotoIfPossible( - 'test_path.jpg', + 'assets/camera_stub_image.jpg', ev100: 12.0, iso: 100, nd: 0, @@ -228,7 +246,7 @@ class _LogbookPhotosEnabled extends StatelessWidget { final List _customPhotos = [ LogbookPhoto( id: '1', - name: 'test_photo_1.jpg', + name: 'assets/camera_stub_image.jpg', timestamp: DateTime(2024, 1, 1, 12), ev: 12.0, iso: 100, @@ -237,7 +255,7 @@ final List _customPhotos = [ ), LogbookPhoto( id: '2', - name: 'test_photo_2.jpg', + name: 'assets/camera_stub_image.jpg', timestamp: DateTime(2024, 1, 2, 12), ev: 13.0, iso: 200, diff --git a/test/screens/metering/bloc_metering_test.dart b/test/screens/metering/bloc_metering_test.dart index 2e94aef..c768667 100644 --- a/test/screens/metering/bloc_metering_test.dart +++ b/test/screens/metering/bloc_metering_test.dart @@ -1,6 +1,8 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/foundation.dart'; import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:lightmeter/providers/logbook_photos_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; @@ -20,11 +22,17 @@ class _MockMeteringCommunicationBloc extends MockBloc implements MeteringCommunicationBloc {} +class _MockLogbookPhotosProviderState extends Mock implements LogbookPhotosProviderState { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) => '_MockLogbookPhotosProviderState'; +} + void main() { late _MockMeteringInteractor meteringInteractor; late _MockVolumeKeysNotifier volumeKeysNotifier; late _MockMeteringCommunicationBloc communicationBloc; late MeteringBloc bloc; + late _MockLogbookPhotosProviderState logbookPhotosProvider; const iso100 = IsoValue(100, StopType.full); setUp(() { @@ -37,11 +45,13 @@ void main() { volumeKeysNotifier = _MockVolumeKeysNotifier(); communicationBloc = _MockMeteringCommunicationBloc(); + logbookPhotosProvider = _MockLogbookPhotosProviderState(); bloc = MeteringBloc( meteringInteractor, volumeKeysNotifier, communicationBloc, + logbookPhotosProvider, ); }); diff --git a/test/screens/settings/goldens/settings_screen.png b/test/screens/settings/goldens/settings_screen.png index 47c39fc..e09202b 100644 Binary files a/test/screens/settings/goldens/settings_screen.png and b/test/screens/settings/goldens/settings_screen.png differ