fixed mocks

This commit is contained in:
Vadim 2025-08-08 16:28:11 +02:00
parent 430d7abfb0
commit fa437a4240
14 changed files with 148 additions and 189 deletions

View file

@ -1,34 +1,13 @@
enum IAPProductStatus {
purchasable,
pending,
purchased,
}
enum IAPProductType { paidFeatures }
class IAPProduct {
final String storeId;
final IAPProductStatus status;
final PurchaseType type;
final String price;
const IAPProduct({
IAPProduct({
required this.storeId,
this.status = IAPProductStatus.purchasable,
required this.type,
required this.price,
});
IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct(
storeId: storeId,
status: status ?? this.status,
price: price,
);
}
extension IAPProductTypeExtension on IAPProductType {
String get storeId {
switch (this) {
case IAPProductType.paidFeatures:
return "";
}
}
}
enum PurchaseType { monthly, yearly, lifetime }

View file

@ -21,45 +21,71 @@ class IAPProductsProviderState extends State<IAPProductsProvider> {
Widget build(BuildContext context) {
return IAPProducts(
isPro: true,
lifetime: IAPProduct(
storeId: '',
type: PurchaseType.lifetime,
price: '0.0\$',
),
yearly: IAPProduct(
storeId: '',
type: PurchaseType.yearly,
price: '0.0\$',
),
monthly: IAPProduct(
storeId: '',
type: PurchaseType.monthly,
price: '0.0\$',
),
child: widget.child,
);
}
Future<void> buy(IAPProductType type) async {}
Future<List<IAPProduct>> fetchProducts() async {
return [];
}
Future<void> restorePurchases() async {}
Future<bool> buyPro(IAPProduct product) async {
return false;
}
Future<bool> restorePurchases() async {
return false;
}
Future<bool> checkIsPro() async {
return false;
}
}
class IAPProducts extends InheritedModel<IAPProductType> {
final List<IAPProduct> products;
class IAPProducts extends InheritedWidget {
final IAPProduct? lifetime;
final IAPProduct? yearly;
final IAPProduct? monthly;
final bool _isPro;
const IAPProducts({
required this.products,
this.lifetime,
this.yearly,
this.monthly,
required bool isPro,
required super.child,
super.key,
});
}) : _isPro = isPro;
static IAPProduct? productOf(BuildContext context, IAPProductType type) {
final IAPProducts? result = InheritedModel.inheritFrom<IAPProducts>(context, aspect: type);
return result!._findProduct(type);
static IAPProducts of(BuildContext context) {
return context.getInheritedWidgetOfExactType<IAPProducts>()!;
}
static bool isPurchased(BuildContext context, IAPProductType type) {
final IAPProducts? result = InheritedModel.inheritFrom<IAPProducts>(context, aspect: type);
return result!._findProduct(type)?.status == IAPProductStatus.purchased;
static bool isPro(BuildContext context, {bool listen = true}) {
return (listen
? context.dependOnInheritedWidgetOfExactType<IAPProducts>()
: context.getInheritedWidgetOfExactType<IAPProducts>())
?._isPro ==
true;
}
bool get hasSubscriptions => yearly != null || monthly != null;
@override
bool updateShouldNotify(IAPProducts oldWidget) => true;
@override
bool updateShouldNotifyDependent(IAPProducts oldWidget, Set<IAPProductType> dependencies) => true;
IAPProduct? _findProduct(IAPProductType type) {
try {
return products.firstWhere((element) => element.storeId == type.storeId);
} catch (_) {
return null;
}
}
bool updateShouldNotify(IAPProducts oldWidget) => oldWidget._isPro != _isPro;
}

View file

@ -15,7 +15,6 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
import 'package:lightmeter/screens/settings/screen_settings.dart';
import 'package:lightmeter/utils/platform_utils.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -42,7 +41,7 @@ void testPurchases(String description) {
UserPreferencesService.seenChangelogVersionKey: await const PlatformUtils().version,
});
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
await tester.pumpApplication(isPro: false);
await tester.takePhoto();
/// Expect the bare minimum free functionallity

View file

@ -59,7 +59,7 @@ class Application extends StatelessWidget {
EquipmentProfileEditFlow(args: context.routeArgs<EquipmentProfileEditArgs>()),
NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(),
NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs<FilmEditArgs>()),
NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(),
NavigationRoutes.proFeaturesScreen.name: (_) => const LightmeterProScreen(),
NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs<TimerFlowArgs>()),
NavigationRoutes.logbookPhotosListScreen.name: (_) => const LogbookPhotosScreen(),
NavigationRoutes.logbookPhotoEditScreen.name: (context) =>

View file

@ -57,11 +57,11 @@ class WidgetTestApplicationMock extends StatelessWidget {
}
class GoldenTestApplicationMock extends StatefulWidget {
final IAPProductStatus productStatus;
final bool isPro;
final Widget child;
const GoldenTestApplicationMock({
this.productStatus = IAPProductStatus.purchased,
this.isPro = true,
required this.child,
super.key,
});
@ -99,14 +99,8 @@ class _GoldenTestApplicationMockState extends State<GoldenTestApplicationMock> {
@override
Widget build(BuildContext context) {
return IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: widget.productStatus,
price: '0.0\$',
),
],
return MockIapProducts(
isPro: widget.isPro,
child: _MockApplicationWrapper(
child: MockIAPProviders(
selectedEquipmentProfileId: mockEquipmentProfiles.first.id,
@ -175,3 +169,26 @@ class _MockApplicationWrapper extends StatelessWidget {
);
}
}
class MockIapProducts extends IAPProducts {
MockIapProducts({
required super.isPro,
required super.child,
}) : super(
lifetime: IAPProduct(
storeId: '',
type: PurchaseType.lifetime,
price: '0.0\$',
),
yearly: IAPProduct(
storeId: '',
type: PurchaseType.yearly,
price: '0.0\$',
),
monthly: IAPProduct(
storeId: '',
type: PurchaseType.monthly,
price: '0.0\$',
),
);
}

View file

@ -5,6 +5,8 @@ import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:mocktail/mocktail.dart';
import '../application_mock.dart';
class _MockEquipmentProfilesStorageService extends Mock implements IapStorageService {}
void main() {
@ -33,16 +35,10 @@ void main() {
reset(storageService);
});
Future<void> pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async {
Future<void> pumpTestWidget(WidgetTester tester, bool isPro) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: productStatus,
price: '0.0\$',
),
],
MockIapProducts(
isPro: isPro,
child: EquipmentProfilesProvider(
storageService: storageService,
child: const _Application(),
@ -69,31 +65,23 @@ void main() {
() {
setUp(() {
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id);
when(() => storageService.getEquipmentProfiles()).thenAnswer((_) => Future.value(_customProfiles.toTogglableMap()));
when(() => storageService.getEquipmentProfiles())
.thenAnswer((_) => Future.value(_customProfiles.toTogglableMap()));
});
testWidgets(
'IAPProductStatus.purchased - show all saved profiles',
'Pro - show all saved profiles',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectEquipmentProfilesCount(_customProfiles.length + 1);
expectSelectedEquipmentProfileName(_customProfiles.first.name);
},
);
testWidgets(
'IAPProductStatus.purchasable - show only default',
'Not Pro - show only default',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.purchasable);
expectEquipmentProfilesCount(1);
expectSelectedEquipmentProfileName('');
},
);
testWidgets(
'IAPProductStatus.pending - show only default',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.pending);
await pumpTestWidget(tester, false);
expectEquipmentProfilesCount(1);
expectSelectedEquipmentProfileName('');
},
@ -105,7 +93,7 @@ void main() {
'toggleProfile',
(tester) async {
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id);
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectEquipmentProfilesCount(_customProfiles.length + 1);
expectEquipmentProfilesInUseCount(_customProfiles.length + 1);
expectSelectedEquipmentProfileName(_customProfiles.first.name);
@ -127,7 +115,7 @@ void main() {
when(() => storageService.getEquipmentProfiles()).thenAnswer((_) async => {});
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectEquipmentProfilesCount(1);
expectSelectedEquipmentProfileName('');

View file

@ -5,6 +5,8 @@ import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:mocktail/mocktail.dart';
import '../application_mock.dart';
class _MockFilmsStorageService extends Mock implements IapStorageService {}
void main() {
@ -30,16 +32,10 @@ void main() {
reset(storageService);
});
Future<void> pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async {
Future<void> pumpTestWidget(WidgetTester tester, bool isPro) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: productStatus,
price: '0.0\$',
),
],
MockIapProducts(
isPro: isPro,
child: FilmsProvider(
storageService: storageService,
child: const _Application(),
@ -76,9 +72,9 @@ void main() {
});
testWidgets(
'IAPProductStatus.purchased - show all saved films',
'Pro - show all saved films',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectPredefinedFilmsCount(mockPredefinedFilms.length);
expectCustomFilmsCount(mockCustomFilms.length);
expectFilmsInUseCount(mockPredefinedFilms.length + mockCustomFilms.length + 1);
@ -87,20 +83,9 @@ void main() {
);
testWidgets(
'IAPProductStatus.purchasable - show only default',
'Not Pro - show only default',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.purchasable);
expectPredefinedFilmsCount(0);
expectCustomFilmsCount(0);
expectFilmsInUseCount(1);
expectSelectedFilmName('');
},
);
testWidgets(
'IAPProductStatus.pending - show only default',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.pending);
await pumpTestWidget(tester, false);
expectPredefinedFilmsCount(0);
expectCustomFilmsCount(0);
expectFilmsInUseCount(1);
@ -117,7 +102,7 @@ void main() {
'toggle predefined film',
(tester) async {
when(() => storageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id);
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectPredefinedFilmsCount(mockPredefinedFilms.length);
expectCustomFilmsCount(mockCustomFilms.length);
expectFilmsInUseCount(mockPredefinedFilms.length + mockCustomFilms.length + 1);
@ -139,7 +124,7 @@ void main() {
'toggle custom film',
(tester) async {
when(() => storageService.selectedFilmId).thenReturn(mockCustomFilms.first.id);
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectPredefinedFilmsCount(mockPredefinedFilms.length);
expectCustomFilmsCount(mockCustomFilms.length);
expectFilmsInUseCount(mockPredefinedFilms.length + mockCustomFilms.length + 1);
@ -163,7 +148,7 @@ void main() {
'selectFilm',
(tester) async {
when(() => storageService.selectedFilmId).thenReturn('');
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectSelectedFilmName('');
tester.filmsProvider.selectFilm(mockPredefinedFilms.first);
@ -184,7 +169,7 @@ void main() {
(tester) async {
when(() => storageService.selectedFilmId).thenReturn('');
when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value({}));
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectPredefinedFilmsCount(mockPredefinedFilms.length);
expectCustomFilmsCount(0);
expectFilmsInUseCount(mockPredefinedFilms.length + 0 + 1);

View file

@ -6,6 +6,8 @@ import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:mocktail/mocktail.dart';
import '../application_mock.dart';
class _MockLogbookPhotosStorageService extends Mock implements IapStorageService {}
class _MockGeolocationService extends Mock implements GeolocationService {}
@ -43,16 +45,10 @@ void main() {
reset(storageService);
});
Future<void> pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async {
Future<void> pumpTestWidget(WidgetTester tester, bool isPro) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: productStatus,
price: '0.0\$',
),
],
MockIapProducts(
isPro: isPro,
child: LogbookPhotosProvider(
storageService: storageService,
geolocationService: geolocationService,
@ -79,27 +75,18 @@ void main() {
});
testWidgets(
'IAPProductStatus.purchased - show all saved photos',
'Pro - show all saved photos',
(tester) async {
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectLogbookPhotosCount(_customPhotos.length);
expectLogbookPhotosEnabled(true);
},
);
testWidgets(
'IAPProductStatus.purchasable - show empty list',
'Not Pro - 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);
await pumpTestWidget(tester, false);
expectLogbookPhotosCount(0);
expectLogbookPhotosEnabled(true);
},
@ -111,7 +98,7 @@ void main() {
'saveLogbookPhotos',
(tester) async {
when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos));
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectLogbookPhotosCount(_customPhotos.length);
expectLogbookPhotosEnabled(true);
@ -132,7 +119,7 @@ void main() {
(tester) async {
when(() => storageService.getPhotos()).thenAnswer((_) async => []);
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
expectLogbookPhotosCount(0);
expectLogbookPhotosEnabled(true);
@ -168,7 +155,7 @@ void main() {
'addPhotoIfPossible when disabled',
(tester) async {
when(() => storageService.getPhotos()).thenAnswer((_) async => []);
await pumpTestWidget(tester, IAPProductStatus.purchased);
await pumpTestWidget(tester, true);
// Disable logbook photos
tester.logbookPhotosProvider.saveLogbookPhotos(false);

View file

@ -3,7 +3,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../application_mock.dart';
@ -41,8 +40,8 @@ class _MockLightmeterProFlow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GoldenTestApplicationMock(
productStatus: IAPProductStatus.purchasable,
return const GoldenTestApplicationMock(
isPro: false,
child: LightmeterProScreen(),
);
}

View file

@ -25,14 +25,8 @@ void main() {
Future<void> pumpApplication(WidgetTester tester) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: IAPProductStatus.purchased,
price: '0.0\$',
),
],
MockIapProducts(
isPro: true,
child: EquipmentProfilesProvider(
storageService: storageService,
child: WidgetTestApplicationMock(

View file

@ -27,14 +27,8 @@ void main() {
Future<void> pumpApplication(WidgetTester tester) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: IAPProductStatus.purchased,
price: '0.0\$',
),
],
MockIapProducts(
isPro: true,
child: FilmsProvider(
storageService: mockFilmsStorageService,
child: const WidgetTestApplicationMock(

View file

@ -9,7 +9,6 @@ import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../integration_test/utils/finder_actions.dart';
@ -18,27 +17,27 @@ import '../../application_mock.dart';
import '../../utils/golden_test_set_theme.dart';
class _MeteringScreenConfig {
final IAPProductStatus iapProductStatus;
final bool isPro;
final EvSourceType evSourceType;
_MeteringScreenConfig(
this.iapProductStatus,
this.isPro,
this.evSourceType,
);
@override
String toString() {
final buffer = StringBuffer();
buffer.write(iapProductStatus.toString().split('.')[1]);
buffer.write(isPro ? 'purchased' : 'purchasable');
buffer.write(' - ');
buffer.write(evSourceType.toString().split('.')[1]);
return buffer.toString();
}
}
final _testScenarios = [IAPProductStatus.purchased, IAPProductStatus.purchasable].expand(
(iapProductStatus) => EvSourceType.values.map(
(evSourceType) => _MeteringScreenConfig(iapProductStatus, evSourceType),
final _testScenarios = [true, false].expand(
(isPro) => EvSourceType.values.map(
(evSourceType) => _MeteringScreenConfig(isPro, evSourceType),
),
);
@ -97,7 +96,7 @@ void main() {
for (final scenario in _testScenarios) {
builder.addScenario(
name: scenario.toString(),
widget: _MockMeteringFlow(productStatus: scenario.iapProductStatus),
widget: _MockMeteringFlow(isPro: scenario.isPro),
onCreate: (scenarioWidgetKey) async {
await setEvSource(tester, scenarioWidgetKey, scenario.evSourceType);
if (scenarioWidgetKey.toString().contains('Dark')) {
@ -121,14 +120,14 @@ void main() {
}
class _MockMeteringFlow extends StatelessWidget {
final IAPProductStatus productStatus;
final bool isPro;
const _MockMeteringFlow({required this.productStatus});
const _MockMeteringFlow({required this.isPro});
@override
Widget build(BuildContext context) {
return GoldenTestApplicationMock(
productStatus: productStatus,
isPro: isPro,
child: const MeteringFlow(),
);
}

View file

@ -6,6 +6,7 @@ import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:mocktail/mocktail.dart';
import '../../../application_mock.dart';
import '../../../function_mock.dart';
class _MockEquipmentProfilesStorageService extends Mock implements IapStorageService {}
@ -35,14 +36,8 @@ void main() {
Future<void> pumpTestWidget(WidgetTester tester) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: IAPProductStatus.purchased,
price: '0.0\$',
),
],
MockIapProducts(
isPro: true,
child: EquipmentProfilesProvider(
storageService: storageService,
child: MaterialApp(

View file

@ -8,7 +8,6 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -16,21 +15,19 @@ import '../../application_mock.dart';
import '../../utils/golden_test_set_theme.dart';
class _SettingsScreenConfig {
final IAPProductStatus iapProductStatus;
final bool isPro;
_SettingsScreenConfig(this.iapProductStatus);
_SettingsScreenConfig(this.isPro);
@override
String toString() {
final buffer = StringBuffer();
buffer.write(iapProductStatus.toString().split('.')[1]);
buffer.write(isPro ? 'purchased' : 'purchasable');
return buffer.toString();
}
}
final _testScenarios = [IAPProductStatus.purchased, IAPProductStatus.purchasable].map(
(iapProductStatus) => _SettingsScreenConfig(iapProductStatus),
);
final _testScenarios = [true, false].map((isPro) => _SettingsScreenConfig(isPro));
void main() {
setUpAll(() {
@ -60,7 +57,7 @@ void main() {
for (final scenario in _testScenarios) {
builder.addScenario(
name: scenario.toString(),
widget: _MockSettingsFlow(productStatus: scenario.iapProductStatus),
widget: _MockSettingsFlow(isPro: scenario.isPro),
onCreate: (scenarioWidgetKey) async {
if (scenarioWidgetKey.toString().contains('Dark')) {
await setTheme<SettingsFlow>(tester, scenarioWidgetKey, ThemeType.dark);
@ -78,14 +75,14 @@ void main() {
}
class _MockSettingsFlow extends StatelessWidget {
final IAPProductStatus productStatus;
final bool isPro;
const _MockSettingsFlow({required this.productStatus});
const _MockSettingsFlow({required this.isPro});
@override
Widget build(BuildContext context) {
return GoldenTestApplicationMock(
productStatus: productStatus,
isPro: isPro,
child: const SettingsFlow(),
);
}