From 0f3d2a1f13f4e0fb6363fbb29c9b8f6c7580cf65 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:46:36 +0100 Subject: [PATCH] test granting and revoking pro features --- integration_test/mocks/iap_products_mock.dart | 45 +++++++ integration_test/purchases_test.dart | 122 ++++++++++++++++++ .../utils/widget_tester_actions.dart | 10 +- 3 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 integration_test/mocks/iap_products_mock.dart create mode 100644 integration_test/purchases_test.dart diff --git a/integration_test/mocks/iap_products_mock.dart b/integration_test/mocks/iap_products_mock.dart new file mode 100644 index 0000000..1a67c69 --- /dev/null +++ b/integration_test/mocks/iap_products_mock.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; + +@visibleForTesting +class MockIAPProductsProvider extends StatefulWidget { + final bool initialyPurchased; + final Widget child; + + const MockIAPProductsProvider({this.initialyPurchased = true, required this.child, super.key}); + + static MockIAPProductsProviderState of(BuildContext context) => MockIAPProductsProvider.maybeOf(context)!; + + static MockIAPProductsProviderState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + State createState() => MockIAPProductsProviderState(); +} + +class MockIAPProductsProviderState extends State { + late bool _purchased = widget.initialyPurchased; + @override + Widget build(BuildContext context) { + return IAPProducts( + products: List.from([ + IAPProduct( + storeId: IAPProductType.paidFeatures.storeId, + status: _purchased ? IAPProductStatus.purchased : IAPProductStatus.purchasable, + ) + ]), + child: widget.child, + ); + } + + void buy() { + _purchased = true; + setState(() {}); + } + + void clearPurchases() { + _purchased = false; + setState(() {}); + } +} diff --git a/integration_test/purchases_test.dart b/integration_test/purchases_test.dart new file mode 100644 index 0000000..dc7fbe5 --- /dev/null +++ b/integration_test/purchases_test.dart @@ -0,0 +1,122 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart'; +import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; +import 'package:lightmeter/screens/settings/screen_settings.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../integration_test/utils/widget_tester_actions.dart'; +import 'mocks/iap_products_mock.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + void mockSharedPrefs() { + // ignore: invalid_use_of_visible_for_testing_member + SharedPreferences.setMockInitialValues({ + /// Metering values + UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index, + UserPreferencesService.showEv100Key: true, + UserPreferencesService.meteringScreenLayoutKey: json.encode( + { + MeteringScreenLayoutFeature.equipmentProfiles: true, + MeteringScreenLayoutFeature.extremeExposurePairs: true, + MeteringScreenLayoutFeature.filmPicker: true, + }.toJson(), + ), + }); + } + + setUpAll(() { + mockSharedPrefs(); + }); + + /// Generates several screenshots with the light theme + testWidgets( + 'Purchase & refund premium features', + (tester) async { + await tester.pumpApplication(productStatus: IAPProductStatus.purchasable); + await tester.takePhoto(); + + /// Expect the bare minimum free functionallity + _expectProMeteringScreen(enabled: false); + + /// Check, that premium settings are disabled + await tester.tap(find.byTooltip(S.current.tooltipOpenSettings)); + await tester.pumpAndSettle(); + await _expectProSettingsScreen(tester, enabled: false); + + /// Make purchase + (tester.state(find.byType(MockIAPProductsProvider)) as MockIAPProductsProviderState).buy(); + await tester.pumpAndSettle(); + + /// Check, that premium settings are enabled + await _expectProSettingsScreen(tester, enabled: true); + + /// Expect, that all the premium controls are now available to user + (tester.state(find.byType(Navigator)) as NavigatorState).pop(); + await tester.pumpAndSettle(Dimens.durationML); + _expectProMeteringScreen(enabled: true); + + /// Refund + (tester.state(find.byType(MockIAPProductsProvider)) as MockIAPProductsProviderState).clearPurchases(); + await tester.pumpAndSettle(); + + /// Expect the bare minimum free functionallity + _expectProMeteringScreen(enabled: false); + + /// Check, that premium settings are disabled + await tester.tap(find.byTooltip(S.current.tooltipOpenSettings)); + await tester.pumpAndSettle(); + await _expectProSettingsScreen(tester, enabled: false); + }, + ); +} + +void _expectProMeteringScreen({required bool enabled}) { + expect(find.byType(EquipmentProfilePicker), enabled ? findsOneWidget : findsNothing); + expect(find.byType(ExtremeExposurePairsContainer), findsOneWidget); + expect(find.byType(FilmPicker), enabled ? findsOneWidget : findsNothing); + expect(find.byType(IsoValuePicker), findsOneWidget); + expect(find.byType(NdValuePicker), findsOneWidget); + expect( + find.descendant( + of: find.byType(MeteringMeasureButton), + matching: find.byWidgetPredicate((widget) => widget is Text && widget.data!.contains('\u2081\u2080\u2080')), + ), + enabled ? findsOneWidget : findsNothing, + ); +} + +Future _expectProSettingsScreen(WidgetTester tester, {required bool enabled}) async { + void expectDisabled(String title, bool disabled) { + find.ancestor( + of: find.text(title), + matching: find.byWidgetPredicate((widget) => widget is Disable && widget.disable == disabled), + ); + } + + expectDisabled(S.current.showEv100, !enabled); + expectDisabled(S.current.equipmentProfiles, !enabled); + expectDisabled(S.current.filmsInUse, !enabled); + expectDisabled(S.current.cameraFeatures, !enabled); + await tester.tapDescendantTextOf(S.current.meteringScreenLayout); + expectDisabled(S.current.meteringScreenLayoutHintEquipmentProfiles, !enabled); + expectDisabled(S.current.meteringScreenFeatureExtremeExposurePairs, false); // must be always enabled + expectDisabled(S.current.meteringScreenFeatureFilmPicker, !enabled); + await tester.tapCancelButton(); +} diff --git a/integration_test/utils/widget_tester_actions.dart b/integration_test/utils/widget_tester_actions.dart index 72bc328..bde2aa6 100644 --- a/integration_test/utils/widget_tester_actions.dart +++ b/integration_test/utils/widget_tester_actions.dart @@ -9,6 +9,7 @@ import 'package:lightmeter/screens/metering/components/bottom_controls/component import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import '../mocks/iap_products_mock.dart'; import '../mocks/paid_features_mock.dart'; import 'platform_channel_mock.dart'; @@ -19,13 +20,8 @@ extension WidgetTesterCommonActions on WidgetTester { Film selectedFilm = const Film.other(), }) async { await pumpWidget( - IAPProducts( - products: [ - IAPProduct( - storeId: IAPProductType.paidFeatures.storeId, - status: productStatus, - ), - ], + MockIAPProductsProvider( + initialyPurchased: productStatus == IAPProductStatus.purchased, child: ApplicationWrapper( const Environment.dev(), child: MockIAPProviders(