fixed tests

This commit is contained in:
Vadim 2024-11-06 15:28:16 +01:00
parent 4d02e71736
commit fce8d0bb6a
16 changed files with 265 additions and 320 deletions

View file

@ -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<SettingsScreen>(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<void> setProfileName(String name) async {
Future<void> selectProfileName(String name) async {
await enterText(find.byType(TextField), name);
await pump();
await tapSaveButton();

View file

@ -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<EquipmentProfile>? equipmentProfiles;
final Map<String, EquipmentProfile> equipmentProfiles;
final String selectedEquipmentProfileId;
final Map<String, SelectableFilm<Film>> predefinedFilms;
final Map<String, SelectableFilm<FilmExponential>> customFilms;
@ -18,14 +18,15 @@ class MockIAPProviders extends StatefulWidget {
final Widget child;
MockIAPProviders({
this.equipmentProfiles = const [],
Map<String, EquipmentProfile>? equipmentProfiles,
this.selectedEquipmentProfileId = '',
Map<String, SelectableFilm<Film>>? predefinedFilms,
Map<String, SelectableFilm<FilmExponential>>? 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<MockIAPProviders> {
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<MockIAPProviders> {
@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<EquipmentProfile> {
Map<String, EquipmentProfile> toProfilesMap() => Map.fromEntries(map((e) => MapEntry(e.id, e)));
}
extension FilmMapper on List<Film> {
Map<String, ({T film, bool isUsed})> toFilmsMap<T extends Film>({bool isUsed = true}) =>
Map.fromEntries(map((e) => MapEntry(e.id, (film: e as T, isUsed: isUsed))));

View file

@ -20,7 +20,7 @@ const mockPhotoEv100 = 8.3;
extension WidgetTesterCommonActions on WidgetTester {
Future<void> pumpApplication({
IAPProductStatus productStatus = IAPProductStatus.purchased,
List<EquipmentProfile>? equipmentProfiles,
Map<String, EquipmentProfile>? equipmentProfiles,
String selectedEquipmentProfileId = '',
Map<String, SelectableFilm<Film>>? predefinedFilms,
Map<String, SelectableFilm<FilmExponential>>? customFilms,

View file

@ -72,7 +72,7 @@ class _ApplicationWrapperState extends State<ApplicationWrapper> {
volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: RemoteConfigProvider(
remoteConfigService: remoteConfigService,
child: EquipmentProfileProvider(
child: EquipmentProfilesProvider(
storageService: equipmentProfilesStorageService,
onInitialized: equipmentProfilesStorageServiceCompleter.complete,
child: FilmsProvider(

View file

@ -1,10 +1,10 @@
enum NavigationRoutes {
meteringScreen,
settingsScreen,
filmsListScreen,
filmEditScreen,
equipmentProfilesListScreen,
equipmentProfileEditScreen,
filmsListScreen,
filmEditScreen,
proFeaturesScreen,
timerScreen,
}

View file

@ -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<EquipmentProfileProviderState>()!;
}
@override
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
}
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
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<EquipmentProfileProvider> {
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<EquipmentProfilesProviderState>()!;
}
@override
State<EquipmentProfilesProvider> createState() => EquipmentProfilesProviderState();
}
class EquipmentProfilesProviderState extends State<EquipmentProfilesProvider> {
final Map<String, EquipmentProfile> _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<EquipmentProfileProvider> {
@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<EquipmentProfileProvider> {
widget.onInitialized?.call();
}
void setProfile(EquipmentProfile data) {
if (_selectedId != data.id) {
setState(() {
_selectedId = data.id;
});
widget.storageService.selectedEquipmentProfileId = _selectedProfile.id;
}
}
Future<void> addProfile(EquipmentProfile profile) async {
await widget.storageService.addProfile(profile);
_customProfiles[profile.id] = profile;
@ -98,32 +86,58 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
Future<void> 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<EquipmentProfile> {
enum _EquipmentProfilesModelAspect {
profiles,
selected,
}
class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> {
final Map<String, EquipmentProfile> 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<EquipmentProfile> of(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.list)!.values;
final model =
InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: _EquipmentProfilesModelAspect.profiles)!;
return List.from([EquipmentProfilesProvider.defaultProfile, ...model.profiles.values]);
}
static EquipmentProfile selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<EquipmentProfiles>(
context,
aspect: SelectableAspect.selected,
)!
return InheritedModel.inheritFrom<EquipmentProfiles>(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));
}
}

View file

@ -6,22 +6,13 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:uuid/uuid.dart';
class EquipmentProfileEditBloc extends Bloc<EquipmentProfileEditEvent, EquipmentProfileEditState> {
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<EquipmentProfileEditEvent, Equipment
)
: EquipmentProfileEditBloc._(
profilesProvider,
_defaultProfile,
EquipmentProfilesProvider.defaultProfile,
isEdit,
);

View file

@ -20,7 +20,7 @@ class EquipmentProfileEditFlow extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => EquipmentProfileEditBloc(
EquipmentProfileProvider.of(context),
EquipmentProfilesProvider.of(context),
profile: args.profile,
isEdit: args.profile != null,
),

View file

@ -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,

View file

@ -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());

View file

@ -108,7 +108,6 @@ class _GoldenTestApplicationMockState extends State<GoldenTestApplicationMock> {
],
child: _MockApplicationWrapper(
child: MockIAPProviders(
equipmentProfiles: mockEquipmentProfiles,
selectedEquipmentProfileId: mockEquipmentProfiles.first.id,
selectedFilmId: mockFilms.first.id,
child: Builder(

View file

@ -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<EquipmentProfile>())).thenAnswer((_) async {});
when(
() => storageService.updateProfile(
id: any<String>(named: 'id'),
name: any<String>(named: 'name'),
),
).thenAnswer((_) async {});
when(() => storageService.deleteProfile(any<String>())).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<EquipmentProfile>())).called(1);
verifyNever(() => storageService.selectedEquipmentProfileId = '');
verify(() => storageService.equipmentProfiles = any<List<EquipmentProfile>>()).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<List<EquipmentProfile>>()).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<List<EquipmentProfile>>()).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<List<EquipmentProfile>>()).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<List<EquipmentProfile>>()).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<List<EquipmentProfile>>());
},
);
});
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));
}
}

View file

@ -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<Film>(), any<bool>())).thenAnswer((_) async {});
when(() => mockFilmsStorageService.addFilm(any<FilmExponential>())).thenAnswer((_) async {});
when(() => mockFilmsStorageService.updateFilm(any<FilmExponential>())).thenAnswer((_) async {});
when(() => mockFilmsStorageService.deleteFilm(any<FilmExponential>())).thenAnswer((_) async {});
when(() => mockFilmsStorageService.getPredefinedFilms())
.thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap()));
when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap()));
when(() => storageService.toggleFilm(any<Film>(), any<bool>())).thenAnswer((_) async {});
when(() => storageService.addFilm(any<FilmExponential>())).thenAnswer((_) async {});
when(() => storageService.updateFilm(any<FilmExponential>())).thenAnswer((_) async {});
when(() => storageService.deleteFilm(any<FilmExponential>())).thenAnswer((_) async {});
when(() => storageService.getPredefinedFilms()).thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap()));
when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap()));
});
tearDown(() {
reset(mockFilmsStorageService);
reset(storageService);
});
Future<void> 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);
},
);
}

View file

@ -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<void> 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<EquipmentProfilePicker>();
@ -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<EquipmentProfilePicker>();

View file

@ -36,7 +36,7 @@ void main() {
),
],
child: FilmsProvider(
filmsStorageService: mockFilmsStorageService,
storageService: mockFilmsStorageService,
child: const WidgetTestApplicationMock(
child: Row(
children: [

View file

@ -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<EquipmentProfileProviderState>();
final storageService = _MockEquipmentProfilesStorageService();
final onDidChangeDependencies = MockValueChanged<EquipmentProfile>();
setUp(() {
registerFallbackValue(_customProfiles.first);
when(() => storageService.addProfile(any<EquipmentProfile>())).thenAnswer((_) async {});
when(
() => storageService.updateProfile(
id: any<String>(named: 'id'),
name: any<String>(named: 'name'),
),
).thenAnswer((_) async {});
when(() => storageService.deleteProfile(any<String>())).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<EquipmentProfile> _customProfiles = [
const EquipmentProfile(
id: '1',