m3_lightmeter/test/providers/logbook_photos_provider_test.dart
Vadim 7ad47c0636
ML-203 Logging EXIF data (#239)
* typos

* added `LogbookPhotosProvider`

* implemented `LogbookScreen`

* implemented `LogbookPhotoEditScreen`

* added photo update

* save geolocation

* added `CameraSettingsSection`

* adjusted logbook grid

* added hero animation

* fixed logbook list updates

* added empty logbook state

* added `saveLogbookPhotos` option

* fixed updating photos

* made `DialogPicker` content scrollable

* added tests for `LogbookPhotosProvider`

* made image preview full-width

* made note field multiline

* wip

* migrated to new iap service

* fixed unit tests

* typo

* fixed arb formatting

* stub logbook photos for tests

* implemented integration test for logbook

* moved date to title

* redundant bottom padding

* added logbook photo screen to screenshots generator

* Update settings.gradle

* aligned iap stub with iap release

* sync

* made logbook iap

* debug screenshots

* Update runner.dart

* fixed dialog picker of optional values

* added bottom padding to logbook edit screen

* fixed tests

* Create camera_stub_image.jpg

* Update films_provider_test.dart

* rename

* Update pubspec.yaml

* added logbook to pro features
2025-07-29 12:38:48 +02:00

261 lines
7.6 KiB
Dart

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';
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(() {
when(() => storageService.addPhoto(any<LogbookPhoto>())).thenAnswer((_) async {});
when(
() => storageService.updatePhoto(
id: any<String>(named: 'id'),
note: any<String>(named: 'note'),
apertureValue: any<Optional<ApertureValue>?>(named: 'apertureValue'),
shutterSpeedValue: any<Optional<ShutterSpeedValue>?>(named: 'shutterSpeedValue'),
),
).thenAnswer((_) async {});
when(() => storageService.deletePhoto(any<String>())).thenAnswer((_) async {});
when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos));
when(() => geolocationService.getCurrentPosition()).thenAnswer((_) => Future.value());
});
tearDown(() {
reset(storageService);
});
Future<void> pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: productStatus,
price: '0.0\$',
),
],
child: LogbookPhotosProvider(
storageService: storageService,
geolocationService: geolocationService,
child: const _Application(),
),
),
);
await tester.pumpAndSettle();
}
void expectLogbookPhotosCount(int count) {
expect(find.text(_LogbookPhotosCount.text(count)), findsOneWidget);
}
void expectLogbookPhotosEnabled(bool enabled) {
expect(find.text(_LogbookPhotosEnabled.text(enabled)), findsOneWidget);
}
group(
'LogbookPhotosProvider dependency on IAPProductStatus',
() {
setUp(() {
when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos));
});
testWidgets(
'IAPProductStatus.purchased - show all saved photos',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.purchased);
expectLogbookPhotosCount(_customPhotos.length);
expectLogbookPhotosEnabled(true);
},
);
testWidgets(
'IAPProductStatus.purchasable - show empty list',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.purchasable);
expectLogbookPhotosCount(0);
expectLogbookPhotosEnabled(true);
},
);
testWidgets(
'IAPProductStatus.pending - show empty list',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.pending);
expectLogbookPhotosCount(0);
expectLogbookPhotosEnabled(true);
},
);
},
);
testWidgets(
'saveLogbookPhotos',
(tester) async {
when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos));
await pumpTestWidget(tester, IAPProductStatus.purchased);
expectLogbookPhotosCount(_customPhotos.length);
expectLogbookPhotosEnabled(true);
tester.logbookPhotosProvider.saveLogbookPhotos(false);
await tester.pump();
expectLogbookPhotosCount(_customPhotos.length);
expectLogbookPhotosEnabled(false);
tester.logbookPhotosProvider.saveLogbookPhotos(true);
await tester.pump();
expectLogbookPhotosCount(_customPhotos.length);
expectLogbookPhotosEnabled(true);
},
);
testWidgets(
'LogbookPhotosProvider CRUD',
(tester) async {
when(() => storageService.getPhotos()).thenAnswer((_) async => []);
await pumpTestWidget(tester, IAPProductStatus.purchased);
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 = photo!.copyWith(note: 'Updated note');
await tester.logbookPhotosProvider.updateLogbookPhoto(updatedPhoto);
await tester.pump();
expectLogbookPhotosCount(1); // No photos loaded initially
verify(
() => storageService.updatePhoto(
id: updatedPhoto.id,
note: 'Updated note',
),
).called(1);
/// Delete a photo
await tester.logbookPhotosProvider.deleteLogbookPhoto(updatedPhoto);
await tester.pump();
expectLogbookPhotosCount(0);
verify(() => storageService.deletePhoto(updatedPhoto.id)).called(1);
},
);
testWidgets(
'addPhotoIfPossible when disabled',
(tester) async {
when(() => storageService.getPhotos()).thenAnswer((_) async => []);
await pumpTestWidget(tester, IAPProductStatus.purchased);
// Disable logbook photos
tester.logbookPhotosProvider.saveLogbookPhotos(false);
await tester.pump();
// Try to add photo when disabled
await tester.logbookPhotosProvider.addPhotoIfPossible(
'assets/camera_stub_image.jpg',
ev100: 12.0,
iso: 100,
nd: 0,
);
await tester.pump();
// Should not add photo when disabled
expectLogbookPhotosCount(0);
verifyNever(() => storageService.addPhoto(any<LogbookPhoto>()));
},
);
}
extension on WidgetTester {
LogbookPhotosProviderState get logbookPhotosProvider {
final BuildContext context = element(find.byType(_Application));
return LogbookPhotosProvider.of(context);
}
}
class _Application extends StatelessWidget {
const _Application();
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
_LogbookPhotosCount(),
_LogbookPhotosEnabled(),
],
),
),
),
);
}
}
class _LogbookPhotosCount extends StatelessWidget {
static String text(int count) => "Photos count: $count";
const _LogbookPhotosCount();
@override
Widget build(BuildContext context) {
return Text(text(LogbookPhotos.of(context).length));
}
}
class _LogbookPhotosEnabled extends StatelessWidget {
static String text(bool enabled) => "Photos enabled: $enabled";
const _LogbookPhotosEnabled();
@override
Widget build(BuildContext context) {
return Text(text(LogbookPhotos.isEnabledOf(context)));
}
}
final List<LogbookPhoto> _customPhotos = [
LogbookPhoto(
id: '1',
name: 'assets/camera_stub_image.jpg',
timestamp: DateTime(2024, 1, 1, 12),
ev: 12.0,
iso: 100,
nd: 0,
note: 'Test photo 1',
),
LogbookPhoto(
id: '2',
name: 'assets/camera_stub_image.jpg',
timestamp: DateTime(2024, 1, 2, 12),
ev: 13.0,
iso: 200,
nd: 1,
note: 'Test photo 2',
),
];