diff --git a/iap/lib/src/data/models/iap_product.dart b/iap/lib/src/data/models/iap_product.dart index ba24c17..2e3aaff 100644 --- a/iap/lib/src/data/models/iap_product.dart +++ b/iap/lib/src/data/models/iap_product.dart @@ -6,8 +6,26 @@ enum IAPProductStatus { enum IAPProductType { paidFeatures } -abstract class IAPProduct { - const IAPProduct._(); +class IAPProduct { + final String storeId; + final IAPProductStatus status; - IAPProductStatus get status => IAPProductStatus.purchasable; + IAPProduct({ + required this.storeId, + this.status = IAPProductStatus.purchasable, + }); + + IAPProduct copyWith({IAPProductStatus? status}) => IAPProduct( + storeId: storeId, + status: status ?? this.status, + ); +} + +extension IAPProductTypeExtension on IAPProductType { + String get storeId { + switch (this) { + case IAPProductType.paidFeatures: + return ""; + } + } } diff --git a/integration_test/generate_screenshots.dart b/integration_test/generate_screenshots.dart new file mode 100644 index 0000000..5b7ad5d --- /dev/null +++ b/integration_test/generate_screenshots.dart @@ -0,0 +1,207 @@ +import 'dart:io'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/basic.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; +import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/data/models/theme_type.dart'; +import 'package:lightmeter/data/models/volume_action.dart'; +import 'package:lightmeter/data/permissions_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; +import 'package:lightmeter/environment.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/bloc_metering.dart'; +import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.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/iso_picker/widget_picker_iso.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart'; +import 'package:lightmeter/screens/metering/flow_metering.dart'; +import 'package:lightmeter/screens/metering/screen_metering.dart'; +import 'package:lightmeter/screens/metering/state_metering.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; +import 'package:lightmeter/screens/settings/screen_settings.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import 'mocks/application_mock.dart'; + +class _MockUserPreferencesService extends Mock implements UserPreferencesService {} + +class _MockCaffeineService extends Mock implements CaffeineService {} + +class _MockHapticsService extends Mock implements HapticsService {} + +class _MockPermissionsService extends Mock implements PermissionsService {} + +class _MockLightSensorService extends Mock implements LightSensorService {} + +class _MockVolumeEventsService extends Mock implements VolumeEventsService {} + +class _MockMeteringBloc extends Mock implements MeteringBloc {} + +void main() { + late _MockUserPreferencesService mockUserPreferencesService; + late _MockCaffeineService mockCaffeineService; + late _MockHapticsService mockHapticsService; + late _MockPermissionsService mockPermissionsService; + late _MockLightSensorService mockLightSensorService; + late _MockVolumeEventsService mockVolumeEventsService; + + final binding = IntegrationTestWidgetsFlutterBinding(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late _MockMeteringBloc meteringBloc; + + const meteringBlocState = MeteringDataState( + ev100: 7.7, + iso: IsoValue(400, StopType.full), + nd: NdValue(0), + isMetering: false, + ); + + setUpAll(() { + mockUserPreferencesService = _MockUserPreferencesService(); + + when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.sensor); + when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third); + when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en); + when(() => mockUserPreferencesService.caffeine).thenReturn(true); + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter); + when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0); + when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0); + when(() => mockUserPreferencesService.iso).thenReturn(const IsoValue(400, StopType.full)); + when(() => mockUserPreferencesService.ndFilter).thenReturn(NdValue.values.first); + when(() => mockUserPreferencesService.haptics).thenReturn(true); + when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({ + MeteringScreenLayoutFeature.equipmentProfiles: true, + MeteringScreenLayoutFeature.extremeExposurePairs: true, + MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: false, + }); + when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light); + when(() => mockUserPreferencesService.dynamicColor).thenReturn(false); + + mockCaffeineService = _MockCaffeineService(); + when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true); + + mockHapticsService = _MockHapticsService(); + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {}); + when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {}); + + mockPermissionsService = _MockPermissionsService(); + when(() => mockPermissionsService.requestCameraPermission()) + .thenAnswer((_) async => PermissionStatus.granted); + when(() => mockPermissionsService.checkCameraPermission()) + .thenAnswer((_) async => PermissionStatus.granted); + + mockLightSensorService = _MockLightSensorService(); + when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true); + when(() => mockLightSensorService.luxStream()).thenAnswer((_) => Stream.fromIterable([100])); + + mockVolumeEventsService = _MockVolumeEventsService(); + when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true); + when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false); + when(() => mockVolumeEventsService.volumeButtonsEventStream()) + .thenAnswer((_) => const Stream.empty()); + + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {}); + + meteringBloc = _MockMeteringBloc(); + when(() => meteringBloc.state).thenReturn(meteringBlocState); + whenListen(meteringBloc, Stream.fromIterable([meteringBlocState])); + when(() => meteringBloc.close()).thenAnswer((_) async {}); + }); + + group( + '', + () { + const initColor = 0xff2196f3; + setUp(() { + when(() => mockUserPreferencesService.primaryColor).thenReturn(const Color(initColor)); + }); + + testWidgets( + '$initColor', + (tester) async { + await tester.pumpWidget( + ApplicationMock( + const Environment.prod().copyWith(hasLightSensor: true), + userPreferencesService: mockUserPreferencesService, + caffeineService: mockCaffeineService, + hapticsService: mockHapticsService, + permissionsService: mockPermissionsService, + lightSensorService: mockLightSensorService, + volumeEventsService: mockVolumeEventsService, + ), + ); + await tester.pumpAndSettle(); + + //await tester.takeScreenshot(binding, '$initColor-metering-reflected'); + + await tester.tap(find.byType(MeteringMeasureButton)); + await tester.tap(find.byType(MeteringMeasureButton)); + await tester.takeScreenshot(binding, '$initColor-metering-incident'); + + await tester.tap(find.byType(IsoValuePicker)); + await tester.pumpAndSettle(Dimens.durationL); + expect(find.byType(IsoValuePicker), findsOneWidget); + await tester.takeScreenshot(binding, '$initColor-metering-iso_picker'); + final cancelButton = find.byWidgetPredicate( + (widget) => + widget is TextButton && + widget.child is Text && + (widget.child as Text?)?.data == (S.current.cancel), + ); + expect(cancelButton, findsOneWidget); + await tester.tap(cancelButton); + // await tester.tap( + // find.descendant( + // of: find.byWidgetPredicate( + // (widget) => + // widget is AnimatedDialog && widget.openedChild is DialogPicker, + // ), + // matching: find.byType(TextButton), + // ), + // ); + await tester.pumpAndSettle(Dimens.durationML); + await tester.pumpAndSettle(); + + expect(find.byTooltip(S.current.tooltipOpenSettings), findsOneWidget); + await tester.tap(find.byTooltip(S.current.tooltipOpenSettings)); + await tester.pumpAndSettle(); + // print("============ TAP ============"); + // expect(find.byType(SettingsScreen), findsOneWidget); + // await tester.takeScreenshot(binding, '$initColor-settings'); + // await tester.takeScreenshot(binding, '$initColor-settings-metering_screen_layout'); + // await tester.takeScreenshot(binding, '$initColor-equipment_profiles'); + // await tester.takeScreenshot(binding, '$initColor-equipment_profiles-iso_picker'); + }, + ); + }, + ); +} + +extension on WidgetTester { + Future takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async { + if (Platform.isAndroid) { + await binding.convertFlutterSurfaceToImage(); + await pumpAndSettle(); + } + await binding.takeScreenshot(name); + } +} diff --git a/integration_test/generate_screenshots.sh b/integration_test/generate_screenshots.sh new file mode 100644 index 0000000..4c62a11 --- /dev/null +++ b/integration_test/generate_screenshots.sh @@ -0,0 +1,9 @@ +flutter drive \ + --dart-define="cameraPreviewAspectRatio=240/320" \ + --driver=test_driver/screenshot_driver.dart \ + --target=integration_test/generate_screenshots.dart \ + --profile \ + --flavor=dev \ + --no-dds \ + --endless-trace-buffer \ + --purge-persistent-cache diff --git a/integration_test/mocks/application_mock.dart b/integration_test/mocks/application_mock.dart new file mode 100644 index 0000000..15a7a84 --- /dev/null +++ b/integration_test/mocks/application_mock.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/data/permissions_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; +import 'package:lightmeter/environment.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; +import 'package:lightmeter/screens/metering/flow_metering.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class _MockSharedPreferences extends Mock implements SharedPreferences {} + +class ApplicationMock extends StatelessWidget { + final Environment env; + final CaffeineService caffeineService; + final HapticsService hapticsService; + final LightSensorService lightSensorService; + final PermissionsService permissionsService; + final UserPreferencesService userPreferencesService; + final VolumeEventsService volumeEventsService; + + const ApplicationMock( + this.env, { + required this.caffeineService, + required this.hapticsService, + required this.lightSensorService, + required this.permissionsService, + required this.userPreferencesService, + required this.volumeEventsService, + super.key, + }); + + @override + Widget build(BuildContext context) { + return IAPProviders( + sharedPreferences: _MockSharedPreferences(), + child: IAPProducts( + products: [ + IAPProduct( + storeId: IAPProductType.paidFeatures.storeId, + status: IAPProductStatus.purchased, + ) + ], + child: EquipmentProfiles( + selected: _equipmentProfiles[1], + values: _equipmentProfiles, + child: Films( + selected: Film('Ilford HP+', 400), + values: [Film.other(), Film('Ilford HP+', 400)], + filmsInUse: [Film.other(), Film('Ilford HP+', 400)], + child: ServicesProvider( + caffeineService: caffeineService, + environment: env, + hapticsService: hapticsService, + lightSensorService: lightSensorService, + permissionsService: permissionsService, + userPreferencesService: userPreferencesService, + volumeEventsService: volumeEventsService, + child: UserPreferencesProvider( + child: Builder( + builder: (context) { + final theme = UserPreferencesProvider.themeOf(context); + final systemIconsBrightness = + ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: systemIconsBrightness == Brightness.light + ? Brightness.dark + : Brightness.light, + statusBarIconBrightness: systemIconsBrightness, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: systemIconsBrightness, + ), + child: MaterialApp( + theme: theme, + locale: Locale(UserPreferencesProvider.localeOf(context).intlName), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: child!, + ), + initialRoute: "metering", + routes: { + "metering": (context) => const MeteringFlow(), + "settings": (context) => const SettingsFlow(), + }, + ), + ); + }, + ), + ), + ), + ), + ), + ), + ); + } +} + +final _equipmentProfiles = [ + const EquipmentProfile( + id: '', + name: '', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ), + EquipmentProfile( + id: '1', + name: 'Praktikca + Zenitar', + apertureValues: ApertureValue.values.sublist( + ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)), + ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)), + ), + ndValues: NdValue.values.sublist(0, 3), + shutterSpeedValues: ShutterSpeedValue.values.sublist( + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)), + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)), + ), + isoValues: const [ + IsoValue(50, StopType.full), + IsoValue(100, StopType.full), + IsoValue(200, StopType.full), + IsoValue(250, StopType.third), + IsoValue(400, StopType.full), + IsoValue(500, StopType.third), + IsoValue(800, StopType.full), + IsoValue(1600, StopType.full), + IsoValue(3200, StopType.full), + ], + ), + const EquipmentProfile( + id: '2', + name: 'Praktica + Jupiter', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ), +]; diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index f1d11fb..cbb043b 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -15,6 +15,7 @@ import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart'; import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; diff --git a/screenshots/4280391411-metering-incident.png b/screenshots/4280391411-metering-incident.png new file mode 100644 index 0000000..f17ddab Binary files /dev/null and b/screenshots/4280391411-metering-incident.png differ diff --git a/screenshots/4280391411-metering-iso_picker.png b/screenshots/4280391411-metering-iso_picker.png new file mode 100644 index 0000000..2fdafe7 Binary files /dev/null and b/screenshots/4280391411-metering-iso_picker.png differ