diff --git a/integration_test/e2e_test.dart b/integration_test/e2e_test.dart index 88edb94..70560b4 100644 --- a/integration_test/e2e_test.dart +++ b/integration_test/e2e_test.dart @@ -48,7 +48,7 @@ void testE2E(String description) { description, (tester) async { await tester.pumpApplication( - equipmentProfiles: [], + equipmentProfiles: {}, predefinedFilms: mockFilms.toFilmsMap(isUsed: true), customFilms: {}, ); @@ -58,7 +58,7 @@ void testE2E(String description) { await tester.tapDescendantTextOf(S.current.equipmentProfiles); await tester.tap(find.byIcon(Icons.add_outlined).first); await tester.pumpAndSettle(); - await tester.setProfileName(mockEquipmentProfiles[0].name); + await tester.selectProfileName(mockEquipmentProfiles[0].name); await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name); await tester.setIsoValues(0, mockEquipmentProfiles[0].isoValues); await tester.setNdValues(0, mockEquipmentProfiles[0].ndValues); @@ -72,7 +72,7 @@ void testE2E(String description) { /// Create Praktica + Jupiter profile from Zenitar profile await tester.tap(find.byIcon(Icons.copy_outlined).first); await tester.pumpAndSettle(); - await tester.setProfileName(mockEquipmentProfiles[1].name); + await tester.selectProfileName(mockEquipmentProfiles[1].name); await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name); await tester.setApertureValues(1, mockEquipmentProfiles[1].apertureValues); await tester.setZoomValue(1, mockEquipmentProfiles[1].lensZoom); @@ -152,7 +152,7 @@ extension EquipmentProfileActions on WidgetTester { await pump(Dimens.durationM); } - Future setProfileName(String name) async { + Future selectProfileName(String name) async { await enterText(find.byType(TextField), name); await pump(); await tapSaveButton(); diff --git a/integration_test/mocks/paid_features_mock.dart b/integration_test/mocks/paid_features_mock.dart index 3748c96..04d3ba4 100644 --- a/integration_test/mocks/paid_features_mock.dart +++ b/integration_test/mocks/paid_features_mock.dart @@ -5,12 +5,12 @@ 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 _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {} class _MockFilmsStorageService extends Mock implements FilmsStorageService {} class MockIAPProviders extends StatefulWidget { - final List? equipmentProfiles; + final Map equipmentProfiles; final String selectedEquipmentProfileId; final Map> predefinedFilms; final Map> customFilms; @@ -18,14 +18,15 @@ class MockIAPProviders extends StatefulWidget { final Widget child; MockIAPProviders({ - this.equipmentProfiles = const [], + Map? equipmentProfiles, this.selectedEquipmentProfileId = '', Map>? predefinedFilms, Map>? customFilms, String? selectedFilmId, required this.child, super.key, - }) : predefinedFilms = predefinedFilms ?? mockFilms.toFilmsMap(), + }) : equipmentProfiles = equipmentProfiles ?? mockEquipmentProfiles.toProfilesMap(), + predefinedFilms = predefinedFilms ?? mockFilms.toFilmsMap(), customFilms = customFilms ?? mockFilms.toFilmsMap(), selectedFilmId = selectedFilmId ?? const FilmStub().id; @@ -34,15 +35,18 @@ class MockIAPProviders extends StatefulWidget { } class _MockIAPProvidersState extends State { - late final _MockIAPStorageService mockIAPStorageService; + late final _MockEquipmentProfilesStorageService mockEquipmentProfilesStorageService; late final _MockFilmsStorageService mockFilmsStorageService; @override void initState() { super.initState(); - mockIAPStorageService = _MockIAPStorageService(); - when(() => mockIAPStorageService.equipmentProfiles).thenReturn(widget.equipmentProfiles ?? mockEquipmentProfiles); - when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(widget.selectedEquipmentProfileId); + mockEquipmentProfilesStorageService = _MockEquipmentProfilesStorageService(); + when(() => mockEquipmentProfilesStorageService.init()).thenAnswer((_) async {}); + when(() => mockEquipmentProfilesStorageService.getProfiles()) + .thenAnswer((_) => Future.value(widget.equipmentProfiles)); + when(() => mockEquipmentProfilesStorageService.selectedEquipmentProfileId) + .thenReturn(widget.selectedEquipmentProfileId); mockFilmsStorageService = _MockFilmsStorageService(); when(() => mockFilmsStorageService.init()).thenAnswer((_) async {}); @@ -53,10 +57,10 @@ class _MockIAPProvidersState extends State { @override Widget build(BuildContext context) { - return EquipmentProfileProvider( - storageService: mockIAPStorageService, + return EquipmentProfilesProvider( + storageService: mockEquipmentProfilesStorageService, child: FilmsProvider( - filmsStorageService: mockFilmsStorageService, + storageService: mockFilmsStorageService, child: widget.child, ), ); @@ -142,6 +146,10 @@ const mockFilms = [ _FilmMultiplying(id: '4', name: 'Mock film 4', iso: 1200, reciprocityMultiplier: 1.5), ]; +extension EquipmentProfileMapper on List { + Map toProfilesMap() => Map.fromEntries(map((e) => MapEntry(e.id, e))); +} + extension FilmMapper on List { Map toFilmsMap({bool isUsed = true}) => Map.fromEntries(map((e) => MapEntry(e.id, (film: e as T, isUsed: isUsed)))); diff --git a/integration_test/utils/widget_tester_actions.dart b/integration_test/utils/widget_tester_actions.dart index 8ff2bb4..be385f0 100644 --- a/integration_test/utils/widget_tester_actions.dart +++ b/integration_test/utils/widget_tester_actions.dart @@ -20,7 +20,7 @@ const mockPhotoEv100 = 8.3; extension WidgetTesterCommonActions on WidgetTester { Future pumpApplication({ IAPProductStatus productStatus = IAPProductStatus.purchased, - List? equipmentProfiles, + Map? equipmentProfiles, String selectedEquipmentProfileId = '', Map>? predefinedFilms, Map>? customFilms, diff --git a/lib/application_wrapper.dart b/lib/application_wrapper.dart index 2a8256d..e3bd928 100644 --- a/lib/application_wrapper.dart +++ b/lib/application_wrapper.dart @@ -72,7 +72,7 @@ class _ApplicationWrapperState extends State { volumeEventsService: const VolumeEventsService(LocalPlatform()), child: RemoteConfigProvider( remoteConfigService: remoteConfigService, - child: EquipmentProfileProvider( + child: EquipmentProfilesProvider( storageService: equipmentProfilesStorageService, onInitialized: equipmentProfilesStorageServiceCompleter.complete, child: FilmsProvider( diff --git a/lib/navigation/routes.dart b/lib/navigation/routes.dart index f449be3..420d90f 100644 --- a/lib/navigation/routes.dart +++ b/lib/navigation/routes.dart @@ -1,10 +1,10 @@ enum NavigationRoutes { meteringScreen, settingsScreen, - filmsListScreen, - filmEditScreen, equipmentProfilesListScreen, equipmentProfileEditScreen, + filmsListScreen, + filmEditScreen, proFeaturesScreen, timerScreen, } diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart index 4bb638f..b09bb3c 100644 --- a/lib/providers/equipment_profile_provider.dart +++ b/lib/providers/equipment_profile_provider.dart @@ -1,31 +1,11 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:lightmeter/utils/context_utils.dart'; -import 'package:lightmeter/utils/selectable_provider.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -class EquipmentProfileProvider extends StatefulWidget { - final EquipmentProfilesStorageService storageService; - final VoidCallback? onInitialized; - final Widget child; - - const EquipmentProfileProvider({ - required this.storageService, - this.onInitialized, - required this.child, - super.key, - }); - - static EquipmentProfileProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => EquipmentProfileProviderState(); -} - -class EquipmentProfileProviderState extends State { - static const EquipmentProfile _defaultProfile = EquipmentProfile( +class EquipmentProfilesProvider extends StatefulWidget { + static const EquipmentProfile defaultProfile = EquipmentProfile( id: '', name: '', apertureValues: ApertureValue.values, @@ -34,10 +14,30 @@ class EquipmentProfileProviderState extends State { isoValues: IsoValue.values, ); + final EquipmentProfilesStorageService storageService; + final VoidCallback? onInitialized; + final Widget child; + + const EquipmentProfilesProvider({ + required this.storageService, + this.onInitialized, + required this.child, + super.key, + }); + + static EquipmentProfilesProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => EquipmentProfilesProviderState(); +} + +class EquipmentProfilesProviderState extends State { final Map _customProfiles = {}; String _selectedId = ''; - EquipmentProfile get _selectedProfile => _customProfiles[_selectedId] ?? _defaultProfile; + EquipmentProfile get _selectedProfile => _customProfiles[_selectedId] ?? EquipmentProfilesProvider.defaultProfile; @override void initState() { @@ -48,11 +48,8 @@ class EquipmentProfileProviderState extends State { @override Widget build(BuildContext context) { return EquipmentProfiles( - values: [ - _defaultProfile, - if (context.isPro) ..._customProfiles.values, - ], - selected: context.isPro ? _selectedProfile : _defaultProfile, + profiles: context.isPro ? _customProfiles : {}, + selected: context.isPro ? _selectedProfile : EquipmentProfilesProvider.defaultProfile, child: widget.child, ); } @@ -64,15 +61,6 @@ class EquipmentProfileProviderState extends State { widget.onInitialized?.call(); } - void setProfile(EquipmentProfile data) { - if (_selectedId != data.id) { - setState(() { - _selectedId = data.id; - }); - widget.storageService.selectedEquipmentProfileId = _selectedProfile.id; - } - } - Future addProfile(EquipmentProfile profile) async { await widget.storageService.addProfile(profile); _customProfiles[profile.id] = profile; @@ -98,32 +86,58 @@ class EquipmentProfileProviderState extends State { Future deleteProfile(EquipmentProfile profile) async { await widget.storageService.deleteProfile(profile.id); if (profile.id == _selectedId) { - _selectedId = _defaultProfile.id; - widget.storageService.selectedEquipmentProfileId = _defaultProfile.id; + _selectedId = EquipmentProfilesProvider.defaultProfile.id; + widget.storageService.selectedEquipmentProfileId = EquipmentProfilesProvider.defaultProfile.id; } _customProfiles.remove(profile.id); setState(() {}); } + + void selectProfile(EquipmentProfile data) { + if (_selectedId != data.id) { + setState(() { + _selectedId = data.id; + }); + widget.storageService.selectedEquipmentProfileId = _selectedProfile.id; + } + } } -class EquipmentProfiles extends SelectableInheritedModel { +enum _EquipmentProfilesModelAspect { + profiles, + selected, +} + +class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> { + final Map profiles; + final EquipmentProfile selected; + const EquipmentProfiles({ - super.key, - required super.values, - required super.selected, + required this.profiles, + required this.selected, required super.child, }); - /// [_defaultProfile] + profiles created by the user + /// _default + profiles create by the user static List of(BuildContext context) { - return InheritedModel.inheritFrom(context, aspect: SelectableAspect.list)!.values; + final model = + InheritedModel.inheritFrom(context, aspect: _EquipmentProfilesModelAspect.profiles)!; + return List.from([EquipmentProfilesProvider.defaultProfile, ...model.profiles.values]); } static EquipmentProfile selectedOf(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: SelectableAspect.selected, - )! + return InheritedModel.inheritFrom(context, aspect: _EquipmentProfilesModelAspect.selected)! .selected; } + + @override + bool updateShouldNotify(EquipmentProfiles _) => true; + + @override + bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set<_EquipmentProfilesModelAspect> dependencies) { + return (dependencies.contains(_EquipmentProfilesModelAspect.selected) && oldWidget.selected != selected) || + ((dependencies.contains(_EquipmentProfilesModelAspect.profiles) || + dependencies.contains(_EquipmentProfilesModelAspect.profiles)) && + const DeepCollectionEquality().equals(oldWidget.profiles, profiles)); + } } diff --git a/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart b/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart index 3bac10f..d151b1c 100644 --- a/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart +++ b/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart @@ -6,22 +6,13 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:uuid/uuid.dart'; class EquipmentProfileEditBloc extends Bloc { - static const EquipmentProfile _defaultProfile = EquipmentProfile( - id: '', - name: '', - apertureValues: ApertureValue.values, - ndValues: NdValue.values, - shutterSpeedValues: ShutterSpeedValue.values, - isoValues: IsoValue.values, - ); - - final EquipmentProfileProviderState profilesProvider; + final EquipmentProfilesProviderState profilesProvider; final EquipmentProfile _originalEquipmentProfile; EquipmentProfile _newEquipmentProfile; final bool _isEdit; factory EquipmentProfileEditBloc( - EquipmentProfileProviderState profilesProvider, { + EquipmentProfilesProviderState profilesProvider, { required EquipmentProfile? profile, required bool isEdit, }) => @@ -33,7 +24,7 @@ class EquipmentProfileEditBloc extends Bloc EquipmentProfileEditBloc( - EquipmentProfileProvider.of(context), + EquipmentProfilesProvider.of(context), profile: args.profile, isEdit: args.profile != null, ), diff --git a/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart index d82cdf4..4493103 100644 --- a/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart +++ b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart @@ -16,7 +16,7 @@ class EquipmentProfilePicker extends StatelessWidget { selectedValue: EquipmentProfiles.selectedOf(context), values: EquipmentProfiles.of(context), itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name), - onChanged: EquipmentProfileProvider.of(context).setProfile, + onChanged: EquipmentProfilesProvider.of(context).selectProfile, closedChild: ReadingValueContainer.singleValue( value: ReadingValue( label: S.of(context).equipmentProfile, diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart index f7c412b..14bf0fb 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart @@ -36,7 +36,7 @@ class MeteringScreenLayoutListTile extends StatelessWidget { }, onSave: (value) { if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) { - EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); + EquipmentProfilesProvider.of(context).selectProfile(EquipmentProfiles.of(context).first); } if (!value[MeteringScreenLayoutFeature.filmPicker]!) { FilmsProvider.of(context).selectFilm(const FilmStub()); diff --git a/test/application_mock.dart b/test/application_mock.dart index ff9a9ef..8475389 100644 --- a/test/application_mock.dart +++ b/test/application_mock.dart @@ -108,7 +108,6 @@ class _GoldenTestApplicationMockState extends State { ], child: _MockApplicationWrapper( child: MockIAPProviders( - equipmentProfiles: mockEquipmentProfiles, selectedEquipmentProfileId: mockEquipmentProfiles.first.id, selectedFilmId: mockFilms.first.id, child: Builder( diff --git a/test/providers/equipment_profile_provider_test.dart b/test/providers/equipment_profile_provider_test.dart index 15472dc..d9418a8 100644 --- a/test/providers/equipment_profile_provider_test.dart +++ b/test/providers/equipment_profile_provider_test.dart @@ -5,14 +5,29 @@ 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 {} +import '../../integration_test/mocks/paid_features_mock.dart'; + +class _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late _MockIAPStorageService storageService; + late _MockEquipmentProfilesStorageService storageService; setUpAll(() { - storageService = _MockIAPStorageService(); + storageService = _MockEquipmentProfilesStorageService(); + }); + + setUp(() { + registerFallbackValue(_customProfiles.first); + when(() => storageService.addProfile(any())).thenAnswer((_) async {}); + when( + () => storageService.updateProfile( + id: any(named: 'id'), + name: any(named: 'name'), + ), + ).thenAnswer((_) async {}); + when(() => storageService.deleteProfile(any())).thenAnswer((_) async {}); + when(() => storageService.getProfiles()).thenAnswer((_) => Future.value(_customProfiles.toProfilesMap())); }); tearDown(() { @@ -29,35 +44,36 @@ void main() { price: '0.0\$', ), ], - child: EquipmentProfileProvider( + child: EquipmentProfilesProvider( storageService: storageService, child: const _Application(), ), ), ); + await tester.pumpAndSettle(); } void expectEquipmentProfilesCount(int count) { - expect(find.text('Equipment profiles count: $count'), findsOneWidget); + expect(find.text(_EquipmentProfilesCount.text(count)), findsOneWidget); } void expectSelectedEquipmentProfileName(String name) { - expect(find.text('Selected equipment profile: $name'), findsOneWidget); + expect(find.text(_SelectedEquipmentProfile.text(name)), findsOneWidget); } group( - 'EquipmentProfileProvider dependency on IAPProductStatus', + 'EquipmentProfilesProvider dependency on IAPProductStatus', () { setUp(() { when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id); - when(() => storageService.equipmentProfiles).thenReturn(_customProfiles); + when(() => storageService.getProfiles()).thenAnswer((_) => Future.value(_customProfiles.toProfilesMap())); }); testWidgets( 'IAPProductStatus.purchased - show all saved profiles', (tester) async { await pumpTestWidget(tester, IAPProductStatus.purchased); - expectEquipmentProfilesCount(3); + expectEquipmentProfilesCount(_customProfiles.length + 1); expectSelectedEquipmentProfileName(_customProfiles.first.name); }, ); @@ -82,203 +98,105 @@ void main() { }, ); - group('EquipmentProfileProvider CRUD', () { - testWidgets( - 'Add', - (tester) async { - when(() => storageService.equipmentProfiles).thenReturn([]); - when(() => storageService.selectedEquipmentProfileId).thenReturn(''); + testWidgets( + 'EquipmentProfilesProvider CRUD', + (tester) async { + when(() => storageService.getProfiles()).thenAnswer((_) async => {}); + when(() => storageService.selectedEquipmentProfileId).thenReturn(''); - await pumpTestWidget(tester, IAPProductStatus.purchased); - expectEquipmentProfilesCount(1); - expectSelectedEquipmentProfileName(''); + await pumpTestWidget(tester, IAPProductStatus.purchased); + expectEquipmentProfilesCount(1); + expectSelectedEquipmentProfileName(''); - await tester.tap(find.byKey(_Application.addProfileButtonKey)); - await tester.pump(); - expectEquipmentProfilesCount(2); - expectSelectedEquipmentProfileName(''); + /// Add first profile and verify + await tester.equipmentProfilesProvider.addProfile(_customProfiles.first); + await tester.pump(); + expectEquipmentProfilesCount(2); + expectSelectedEquipmentProfileName(''); + verify(() => storageService.addProfile(any())).called(1); - verifyNever(() => storageService.selectedEquipmentProfileId = ''); - verify(() => storageService.equipmentProfiles = any>()).called(1); - }, - ); + /// Add the other profiles and select the 1st one + for (final profile in _customProfiles.skip(1)) { + await tester.equipmentProfilesProvider.addProfile(profile); + } + tester.equipmentProfilesProvider.selectProfile(_customProfiles.first); + await tester.pump(); + expectEquipmentProfilesCount(1 + _customProfiles.length); + expectSelectedEquipmentProfileName(_customProfiles.first.name); - testWidgets( - 'Add from', - (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); - when(() => storageService.selectedEquipmentProfileId).thenReturn(''); + /// Edit the selected profile + final updatedName = "${_customProfiles.first} updated"; + await tester.equipmentProfilesProvider.updateProfile(_customProfiles.first.copyWith(name: updatedName)); + await tester.pump(); + expectEquipmentProfilesCount(1 + _customProfiles.length); + expectSelectedEquipmentProfileName(updatedName); + verify(() => storageService.updateProfile(id: _customProfiles.first.id, name: updatedName)).called(1); - await pumpTestWidget(tester, IAPProductStatus.purchased); - expectEquipmentProfilesCount(3); - expectSelectedEquipmentProfileName(''); + /// Delete a non-selected profile + await tester.equipmentProfilesProvider.deleteProfile(_customProfiles.last); + await tester.pump(); + expectEquipmentProfilesCount(1 + _customProfiles.length - 1); + expectSelectedEquipmentProfileName(updatedName); + verifyNever(() => storageService.selectedEquipmentProfileId = ''); + verify(() => storageService.deleteProfile(_customProfiles.last.id)).called(1); - await tester.tap(find.byKey(_Application.addFromProfileButtonKey(_customProfiles[0].id))); - await tester.pump(); - expectEquipmentProfilesCount(4); - expectSelectedEquipmentProfileName(''); + /// Delete the selected profile + await tester.equipmentProfilesProvider.deleteProfile(_customProfiles.first); + await tester.pump(); + expectEquipmentProfilesCount(1 + _customProfiles.length - 2); + expectSelectedEquipmentProfileName(''); + verify(() => storageService.selectedEquipmentProfileId = '').called(1); + verify(() => storageService.deleteProfile(_customProfiles.first.id)).called(1); + }, + ); +} - verifyNever(() => storageService.selectedEquipmentProfileId = ''); - verify(() => storageService.equipmentProfiles = any>()).called(1); - }, - ); - - testWidgets( - 'Edit selected', - (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); - when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id); - - await pumpTestWidget(tester, IAPProductStatus.purchased); - - /// Change the name & limit ISO values of the both added profiles - await tester.tap(find.byKey(_Application.updateProfileButtonKey(_customProfiles[0].id))); - await tester.pumpAndSettle(); - expectEquipmentProfilesCount(3); - expectSelectedEquipmentProfileName("${_customProfiles[0].name} updated"); - - verifyNever(() => storageService.selectedEquipmentProfileId = _customProfiles[0].id); - verify(() => storageService.equipmentProfiles = any>()).called(1); - }, - ); - - testWidgets( - 'Delete selected', - (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); - when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id); - - await pumpTestWidget(tester, IAPProductStatus.purchased); - expectEquipmentProfilesCount(3); - expectSelectedEquipmentProfileName(_customProfiles[0].name); - - /// Delete the selected profile - await tester.tap(find.byKey(_Application.deleteProfileButtonKey(_customProfiles[0].id))); - await tester.pumpAndSettle(); - expectEquipmentProfilesCount(2); - expectSelectedEquipmentProfileName(''); - - verify(() => storageService.selectedEquipmentProfileId = '').called(1); - verify(() => storageService.equipmentProfiles = any>()).called(1); - }, - ); - - testWidgets( - 'Delete not selected', - (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); - when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id); - - await pumpTestWidget(tester, IAPProductStatus.purchased); - expectEquipmentProfilesCount(3); - expectSelectedEquipmentProfileName(_customProfiles[0].name); - - /// Delete the not selected profile - await tester.tap(find.byKey(_Application.deleteProfileButtonKey(_customProfiles[1].id))); - await tester.pumpAndSettle(); - expectEquipmentProfilesCount(2); - expectSelectedEquipmentProfileName(_customProfiles[0].name); - - verifyNever(() => storageService.selectedEquipmentProfileId = ''); - verify(() => storageService.equipmentProfiles = any>()).called(1); - }, - ); - - testWidgets( - 'Select', - (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); - when(() => storageService.selectedEquipmentProfileId).thenReturn(''); - - await pumpTestWidget(tester, IAPProductStatus.purchased); - expectEquipmentProfilesCount(3); - expectSelectedEquipmentProfileName(''); - - /// Select the 1st custom profile - await tester.tap(find.byKey(_Application.setProfileButtonKey(_customProfiles[0].id))); - await tester.pumpAndSettle(); - expectEquipmentProfilesCount(3); - expectSelectedEquipmentProfileName(_customProfiles[0].name); - - verify(() => storageService.selectedEquipmentProfileId = _customProfiles[0].id).called(1); - verifyNever(() => storageService.equipmentProfiles = any>()); - }, - ); - }); +extension on WidgetTester { + EquipmentProfilesProviderState get equipmentProfilesProvider { + final BuildContext context = element(find.byType(_Application)); + return EquipmentProfilesProvider.of(context); + } } class _Application extends StatelessWidget { const _Application(); - static ValueKey get addProfileButtonKey => const ValueKey('addProfileButtonKey'); - static ValueKey addFromProfileButtonKey(String id) => ValueKey('addFromProfileButtonKey$id'); - static ValueKey setProfileButtonKey(String id) => ValueKey('setProfileButtonKey$id'); - static ValueKey updateProfileButtonKey(String id) => ValueKey('updateProfileButtonKey$id'); - static ValueKey deleteProfileButtonKey(String id) => ValueKey('deleteProfileButtonKey$id'); - @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( home: Scaffold( - appBar: AppBar(title: const Text('IAPProviders test')), body: Center( child: Column( children: [ - Text("Equipment profiles count: ${EquipmentProfiles.of(context).length}"), - Text("Selected equipment profile: ${EquipmentProfiles.selectedOf(context).name}"), - ElevatedButton( - key: addProfileButtonKey, - onPressed: () { - EquipmentProfileProvider.of(context).addProfile('Test added'); - }, - child: const Text("Add"), - ), - ...EquipmentProfiles.of(context).map((e) => _equipmentProfilesCrudRow(context, e)), + _EquipmentProfilesCount(), + _SelectedEquipmentProfile(), ], ), ), ), ); } +} - Widget _equipmentProfilesCrudRow(BuildContext context, EquipmentProfile profile) { - return Row( - children: [ - ElevatedButton( - key: setProfileButtonKey(profile.id), - onPressed: () { - EquipmentProfileProvider.of(context).setProfile(profile); - }, - child: const Text("Set"), - ), - ElevatedButton( - key: addFromProfileButtonKey(profile.id), - onPressed: () { - EquipmentProfileProvider.of(context).addProfile('Test from ${profile.name}', profile); - }, - child: const Text("Add from"), - ), - ElevatedButton( - key: updateProfileButtonKey(profile.id), - onPressed: () { - EquipmentProfileProvider.of(context).updateProfile( - profile.copyWith( - name: '${profile.name} updated', - isoValues: _customProfiles.first.isoValues, - ), - ); - }, - child: const Text("Update"), - ), - ElevatedButton( - key: deleteProfileButtonKey(profile.id), - onPressed: () { - EquipmentProfileProvider.of(context).deleteProfile(profile); - }, - child: const Text("Delete"), - ), - ], - ); +class _EquipmentProfilesCount extends StatelessWidget { + static String text(int count) => "Profiles count: $count"; + + const _EquipmentProfilesCount(); + + @override + Widget build(BuildContext context) { + return Text(text(EquipmentProfiles.of(context).length)); + } +} + +class _SelectedEquipmentProfile extends StatelessWidget { + static String text(String name) => "Selected profile: $name}"; + + const _SelectedEquipmentProfile(); + + @override + Widget build(BuildContext context) { + return Text(text(EquipmentProfiles.selectedOf(context).name)); } } diff --git a/test/providers/films_provider_test.dart b/test/providers/films_provider_test.dart index d9de6a3..258dd37 100644 --- a/test/providers/films_provider_test.dart +++ b/test/providers/films_provider_test.dart @@ -9,25 +9,24 @@ class _MockFilmsStorageService extends Mock implements FilmsStorageService {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late _MockFilmsStorageService mockFilmsStorageService; + late _MockFilmsStorageService storageService; setUpAll(() { - mockFilmsStorageService = _MockFilmsStorageService(); + storageService = _MockFilmsStorageService(); }); setUp(() { registerFallbackValue(mockCustomFilms.first); - when(() => mockFilmsStorageService.toggleFilm(any(), any())).thenAnswer((_) async {}); - when(() => mockFilmsStorageService.addFilm(any())).thenAnswer((_) async {}); - when(() => mockFilmsStorageService.updateFilm(any())).thenAnswer((_) async {}); - when(() => mockFilmsStorageService.deleteFilm(any())).thenAnswer((_) async {}); - when(() => mockFilmsStorageService.getPredefinedFilms()) - .thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap())); - when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap())); + when(() => storageService.toggleFilm(any(), any())).thenAnswer((_) async {}); + when(() => storageService.addFilm(any())).thenAnswer((_) async {}); + when(() => storageService.updateFilm(any())).thenAnswer((_) async {}); + when(() => storageService.deleteFilm(any())).thenAnswer((_) async {}); + when(() => storageService.getPredefinedFilms()).thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap())); + when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap())); }); tearDown(() { - reset(mockFilmsStorageService); + reset(storageService); }); Future pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async { @@ -41,7 +40,7 @@ void main() { ), ], child: FilmsProvider( - filmsStorageService: mockFilmsStorageService, + storageService: storageService, child: const _Application(), ), ), @@ -69,11 +68,10 @@ void main() { 'FilmsProvider dependency on IAPProductStatus', () { setUp(() { - when(() => mockFilmsStorageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id); - when(() => mockFilmsStorageService.getPredefinedFilms()) + when(() => storageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id); + when(() => storageService.getPredefinedFilms()) .thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap())); - when(() => mockFilmsStorageService.getCustomFilms()) - .thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap())); + when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap())); }); testWidgets( @@ -117,7 +115,7 @@ void main() { testWidgets( 'toggle predefined film', (tester) async { - when(() => mockFilmsStorageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id); + when(() => storageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id); await pumpTestWidget(tester, IAPProductStatus.purchased); expectPredefinedFilmsCount(mockPredefinedFilms.length); expectCustomFilmsCount(mockCustomFilms.length); @@ -131,15 +129,15 @@ void main() { expectFilmsInUseCount(mockPredefinedFilms.length - 1 + mockCustomFilms.length + 1); expectSelectedFilmName(''); - verify(() => mockFilmsStorageService.toggleFilm(mockPredefinedFilms.first, false)).called(1); - verify(() => mockFilmsStorageService.selectedFilmId = '').called(1); + verify(() => storageService.toggleFilm(mockPredefinedFilms.first, false)).called(1); + verify(() => storageService.selectedFilmId = '').called(1); }, ); testWidgets( 'toggle custom film', (tester) async { - when(() => mockFilmsStorageService.selectedFilmId).thenReturn(mockCustomFilms.first.id); + when(() => storageService.selectedFilmId).thenReturn(mockCustomFilms.first.id); await pumpTestWidget(tester, IAPProductStatus.purchased); expectPredefinedFilmsCount(mockPredefinedFilms.length); expectCustomFilmsCount(mockCustomFilms.length); @@ -153,8 +151,8 @@ void main() { expectFilmsInUseCount(mockPredefinedFilms.length - 1 + mockCustomFilms.length + 1); expectSelectedFilmName(''); - verify(() => mockFilmsStorageService.toggleFilm(mockCustomFilms.first, false)).called(1); - verify(() => mockFilmsStorageService.selectedFilmId = '').called(1); + verify(() => storageService.toggleFilm(mockCustomFilms.first, false)).called(1); + verify(() => storageService.selectedFilmId = '').called(1); }, ); }, @@ -163,7 +161,7 @@ void main() { testWidgets( 'selectFilm', (tester) async { - when(() => mockFilmsStorageService.selectedFilmId).thenReturn(''); + when(() => storageService.selectedFilmId).thenReturn(''); await pumpTestWidget(tester, IAPProductStatus.purchased); expectSelectedFilmName(''); @@ -175,16 +173,16 @@ void main() { await tester.pump(); expectSelectedFilmName(mockCustomFilms.first.name); - verify(() => mockFilmsStorageService.selectedFilmId = mockPredefinedFilms.first.id).called(1); - verify(() => mockFilmsStorageService.selectedFilmId = mockCustomFilms.first.id).called(1); + verify(() => storageService.selectedFilmId = mockPredefinedFilms.first.id).called(1); + verify(() => storageService.selectedFilmId = mockCustomFilms.first.id).called(1); }, ); testWidgets( 'Custom film CRUD', (tester) async { - when(() => mockFilmsStorageService.selectedFilmId).thenReturn(''); - when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer((_) => Future.value({})); + when(() => storageService.selectedFilmId).thenReturn(''); + when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value({})); await pumpTestWidget(tester, IAPProductStatus.purchased); expectPredefinedFilmsCount(mockPredefinedFilms.length); expectCustomFilmsCount(0); @@ -199,16 +197,16 @@ void main() { expectCustomFilmsCount(1); expectFilmsInUseCount(mockPredefinedFilms.length + 1 + 1); expectSelectedFilmName(mockCustomFilms.first.name); - verify(() => mockFilmsStorageService.addFilm(mockCustomFilms.first)).called(1); - verify(() => mockFilmsStorageService.toggleFilm(mockCustomFilms.first, true)).called(1); - verify(() => mockFilmsStorageService.selectedFilmId = mockCustomFilms.first.id).called(1); + verify(() => storageService.addFilm(mockCustomFilms.first)).called(1); + verify(() => storageService.toggleFilm(mockCustomFilms.first, true)).called(1); + verify(() => storageService.selectedFilmId = mockCustomFilms.first.id).called(1); const editedFilmName = 'Edited custom film 2x'; final editedFilm = mockCustomFilms.first.copyWith(name: editedFilmName); await tester.filmsProvider.updateCustomFilm(editedFilm); await tester.pump(); expectSelectedFilmName(editedFilm.name); - verify(() => mockFilmsStorageService.updateFilm(editedFilm)).called(1); + verify(() => storageService.updateFilm(editedFilm)).called(1); await tester.filmsProvider.deleteCustomFilm(editedFilm); await tester.pump(); @@ -216,8 +214,8 @@ void main() { expectCustomFilmsCount(0); expectFilmsInUseCount(mockPredefinedFilms.length + 0 + 1); expectSelectedFilmName(''); - verify(() => mockFilmsStorageService.deleteFilm(editedFilm)).called(1); - verify(() => mockFilmsStorageService.selectedFilmId = '').called(1); + verify(() => storageService.deleteFilm(editedFilm)).called(1); + verify(() => storageService.selectedFilmId = '').called(1); }, ); } diff --git a/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart b/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart index 8c7d96b..0218bb2 100644 --- a/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart +++ b/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart @@ -7,18 +7,19 @@ import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../../../../integration_test/mocks/paid_features_mock.dart'; import '../../../../../application_mock.dart'; import 'utils.dart'; -class _MockIAPStorageService extends Mock implements IAPStorageService {} +class _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {} void main() { - late final _MockIAPStorageService mockIAPStorageService; + late final _MockEquipmentProfilesStorageService storageService; setUpAll(() { - mockIAPStorageService = _MockIAPStorageService(); - when(() => mockIAPStorageService.equipmentProfiles).thenReturn(_mockEquipmentProfiles); - when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(''); + storageService = _MockEquipmentProfilesStorageService(); + when(() => storageService.getProfiles()).thenAnswer((_) async => _mockEquipmentProfiles.toProfilesMap()); + when(() => storageService.selectedEquipmentProfileId).thenReturn(''); }); Future pumpApplication(WidgetTester tester) async { @@ -31,8 +32,8 @@ void main() { price: '0.0\$', ), ], - child: EquipmentProfileProvider( - storageService: mockIAPStorageService, + child: EquipmentProfilesProvider( + storageService: storageService, child: const WidgetTestApplicationMock( child: Row(children: [Expanded(child: EquipmentProfilePicker())]), ), @@ -59,7 +60,7 @@ void main() { testWidgets( 'None', (tester) async { - when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(''); + when(() => storageService.selectedEquipmentProfileId).thenReturn(''); await pumpApplication(tester); expectReadingValueContainerText(S.current.none); await tester.openAnimatedPicker(); @@ -70,7 +71,7 @@ void main() { testWidgets( 'Praktica + Zenitar', (tester) async { - when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(_mockEquipmentProfiles.first.id); + when(() => storageService.selectedEquipmentProfileId).thenReturn(_mockEquipmentProfiles.first.id); await pumpApplication(tester); expectReadingValueContainerText(_mockEquipmentProfiles.first.name); await tester.openAnimatedPicker(); diff --git a/test/screens/metering/components/shared/readings_container/film_picker_test.dart b/test/screens/metering/components/shared/readings_container/film_picker_test.dart index a5bf3ee..1ea3399 100644 --- a/test/screens/metering/components/shared/readings_container/film_picker_test.dart +++ b/test/screens/metering/components/shared/readings_container/film_picker_test.dart @@ -36,7 +36,7 @@ void main() { ), ], child: FilmsProvider( - filmsStorageService: mockFilmsStorageService, + storageService: mockFilmsStorageService, child: const WidgetTestApplicationMock( child: Row( children: [ diff --git a/test/screens/metering/utils/listener_equipment_profiles_test.dart b/test/screens/metering/utils/listener_equipment_profiles_test.dart index 38e2a14..d5aa0ce 100644 --- a/test/screens/metering/utils/listener_equipment_profiles_test.dart +++ b/test/screens/metering/utils/listener_equipment_profiles_test.dart @@ -6,16 +6,29 @@ import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../../integration_test/mocks/paid_features_mock.dart'; import '../../../function_mock.dart'; -class _MockIAPStorageService extends Mock implements IAPStorageService {} +class _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final storageService = _MockIAPStorageService(); - final equipmentProfileProviderKey = GlobalKey(); + final storageService = _MockEquipmentProfilesStorageService(); final onDidChangeDependencies = MockValueChanged(); + setUp(() { + registerFallbackValue(_customProfiles.first); + when(() => storageService.addProfile(any())).thenAnswer((_) async {}); + when( + () => storageService.updateProfile( + id: any(named: 'id'), + name: any(named: 'name'), + ), + ).thenAnswer((_) async {}); + when(() => storageService.deleteProfile(any())).thenAnswer((_) async {}); + when(() => storageService.getProfiles()).thenAnswer((_) => Future.value(_customProfiles.toProfilesMap())); + }); + tearDown(() { reset(onDidChangeDependencies); reset(storageService); @@ -31,8 +44,7 @@ void main() { price: '0.0\$', ), ], - child: EquipmentProfileProvider( - key: equipmentProfileProviderKey, + child: EquipmentProfilesProvider( storageService: storageService, child: MaterialApp( home: EquipmentProfileListener( @@ -48,11 +60,10 @@ void main() { testWidgets( 'Trigger `onDidChangeDependencies` by selecting a new profile', (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); when(() => storageService.selectedEquipmentProfileId).thenReturn(''); await pumpTestWidget(tester); - equipmentProfileProviderKey.currentState!.setProfile(_customProfiles[0]); + tester.equipmentProfilesProvider.selectProfile(_customProfiles[0]); await tester.pump(); verify(() => onDidChangeDependencies.onChanged(_customProfiles[0])).called(1); }, @@ -61,18 +72,17 @@ void main() { testWidgets( 'Trigger `onDidChangeDependencies` by updating the selected profile', (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id); await pumpTestWidget(tester); final updatedProfile1 = _customProfiles[0].copyWith(name: 'Test 1 updated'); - equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile1); + await tester.equipmentProfilesProvider.updateProfile(updatedProfile1); await tester.pump(); verify(() => onDidChangeDependencies.onChanged(updatedProfile1)).called(1); /// Verify that updating the not selected profile doesn't trigger the callback final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated'); - equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile2); + await tester.equipmentProfilesProvider.updateProfile(updatedProfile2); await tester.pump(); verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2)); }, @@ -81,18 +91,24 @@ void main() { testWidgets( "Don't trigger `onDidChangeDependencies` by updating the unselected profile", (tester) async { - when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id); await pumpTestWidget(tester); final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated'); - equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile2); + await tester.equipmentProfilesProvider.updateProfile(updatedProfile2); await tester.pump(); verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2)); }, ); } +extension on WidgetTester { + EquipmentProfilesProviderState get equipmentProfilesProvider { + final BuildContext context = element(find.byType(MaterialApp)); + return EquipmentProfilesProvider.of(context); + } +} + final List _customProfiles = [ const EquipmentProfile( id: '1',