diff --git a/integration_test/extract_benchmarks.sh b/integration_test/extract_benchmarks.sh new file mode 100644 index 0000000..5a914a0 --- /dev/null +++ b/integration_test/extract_benchmarks.sh @@ -0,0 +1,51 @@ +timeline_name="$1" +csv_filename="$2" + +if [[ -n "$timeline_name" ]]; then + if [[ -z "$csv_filename" ]]; then + csv_filename="${timeline_name}" + fi + + echo "====== Extracting & merging ${timeline_name} timelines ======" + echo "" >>${csv_filename}.csv + echo "${timeline_name}" >>${csv_filename}.csv + + extent_micros="timeExtentMicros" + timelines=$(find ./build -maxdepth 1 -name "${timeline_name}*.timeline.json" -print) + for i in "${timelines[@]}"; do + benchextract $i + extent_micros+="$(grep -A0 -h '"timeExtentMicros":' $i | grep -o " [0-9]*")" + done + echo $extent_micros | tr ' ' ',' >>${csv_filename}.csv + + metrics=( + "average_frame_build_time_millis" + "90th_percentile_frame_build_time_millis" + "99th_percentile_frame_build_time_millis" + "worst_frame_build_time_millis" + "average_rasterizer_time_millis" + "90th_percentile_rasterizer_time_millis" + "99th_percentile_rasterizer_time_millis" + "worst_frame_rasterizer_time_millis" + ) + + timeline_summaries=$(find ./build -maxdepth 1 -name "${timeline_name}*.timeline_summary.json" -print) + for i in "${timeline_summaries[@]}"; do + metrics[0]+="$(grep -A0 -h '"average_frame_build_time_millis":' $i | grep -o " [0-9.]*")" + metrics[1]+="$(grep -A0 -h '"90th_percentile_frame_build_time_millis":' $i | grep -o " [0-9.]*")" + metrics[2]+="$(grep -A0 -h '"99th_percentile_frame_build_time_millis":' $i | grep -o " [0-9.]*")" + metrics[3]+="$(grep -A0 -h '"worst_frame_build_time_millis":' $i | grep -o " [0-9.]*")" + metrics[4]+="$(grep -A0 -h '"average_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")" + metrics[5]+="$(grep -A0 -h '"90th_percentile_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")" + metrics[6]+="$(grep -A0 -h '"99th_percentile_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")" + metrics[7]+="$(grep -A0 -h '"worst_frame_rasterizer_time_millis":' $i | grep -o " [0-9.]*")" + done + for metric in "${metrics[@]}"; do + echo $metric | tr ' ' ',' >>${csv_filename}.csv + done + + benchmarks=$(find ./build -maxdepth 1 -name "${timeline_name}*.timeline.benchmark" -print) + benchmerge $benchmarks +else + echo "Provide the timeline name" +fi diff --git a/integration_test/generate_screenshots.dart b/integration_test/generate_screenshots.dart new file mode 100644 index 0000000..7f8172d --- /dev/null +++ b/integration_test/generate_screenshots.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:lightmeter/application.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/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/res/theme.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/dialog_picker/widget_picker_dialog.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; +import 'package:lightmeter/screens/settings/screen_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:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'utils/widget_tester_extension.dart'; + +class _MockSharedPreferences extends Mock implements SharedPreferences {} + +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 {} + +//https://stackoverflow.com/a/67186625/13167574 +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(); + + setUpAll(() { + mockUserPreferencesService = _MockUserPreferencesService(); + when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera); + 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.isKeepScreenOn()).thenAnswer((_) async => false); + when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true); + when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false); + + 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 {}); + }); + + Future pumpApplication(WidgetTester tester) async { + await tester.pumpWidget( + IAPProviders( + sharedPreferences: _MockSharedPreferences(), + child: EquipmentProfiles( + selected: _mockEquipmentProfiles[0], + values: _mockEquipmentProfiles, + child: Films( + selected: const Film('Ilford HP5+', 400), + values: const [Film.other(), Film('Ilford HP5+', 400)], + filmsInUse: const [Film.other(), Film('Ilford HP5+', 400)], + child: ServicesProvider( + environment: const Environment.prod().copyWith(hasLightSensor: true), + userPreferencesService: mockUserPreferencesService, + caffeineService: mockCaffeineService, + hapticsService: mockHapticsService, + permissionsService: mockPermissionsService, + lightSensorService: mockLightSensorService, + volumeEventsService: mockVolumeEventsService, + child: const UserPreferencesProvider(child: Application()), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + } + + /// Generates several screenshots with the light theme + /// and the additionally the first one with the dark theme + void generateScreenshots(Color color) { + testWidgets('${color.value}_light', (tester) async { + when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light); + when(() => mockUserPreferencesService.primaryColor).thenReturn(color); + await pumpApplication(tester); + + await tester.takePhoto(); + await tester.takeScreenshot(binding, '${color.value}_metering_reflected'); + + await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor)); + await tester.pumpAndSettle(); + await tester.tap(find.byType(MeteringMeasureButton)); + await tester.tap(find.byType(MeteringMeasureButton)); + await tester.takeScreenshot(binding, '${color.value}_metering_incident'); + + expect(find.byType(IsoValuePicker), findsOneWidget); + await tester.tap(find.byType(IsoValuePicker)); + await tester.pumpAndSettle(Dimens.durationL); + expect(find.byType(DialogPicker), findsOneWidget); + await tester.takeScreenshot(binding, '${color.value}_metering_iso_picker'); + + await tester.tapCancelButton(); + expect(find.byType(DialogPicker), findsNothing); + await tester.openSettings(); + await tester.takeScreenshot(binding, '${color.value}_settings'); + + await tester.tapListTile(S.current.meteringScreenLayout); + await tester.takeScreenshot(binding, '${color.value}_settings_metering_screen_layout'); + + await tester.tapCancelButton(); + await tester.tapListTile(S.current.equipmentProfiles); + expect(find.byType(EquipmentProfilesScreen), findsOneWidget); + await tester.tap(find.byType(EquipmentProfileContainer).first); + await tester.pumpAndSettle(); + await tester.takeScreenshot(binding, '${color.value}-equipment_profiles'); + + await tester.tap(find.byIcon(Icons.iso).first); + await tester.pumpAndSettle(); + await tester.takeScreenshot(binding, '${color.value}_equipment_profiles_iso_picker'); + }); + + testWidgets( + '${color.value}_dark', + (tester) async { + when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.dark); + when(() => mockUserPreferencesService.primaryColor).thenReturn(color); + await pumpApplication(tester); + + await tester.takePhoto(); + await tester.takeScreenshot(binding, '${color.value}_metering_reflected_dark'); + + await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor)); + await tester.pumpAndSettle(); + await tester.tap(find.byType(MeteringMeasureButton)); + await tester.tap(find.byType(MeteringMeasureButton)); + await tester.takeScreenshot(binding, '${color.value}_metering_incident_dark'); + }, + ); + } + + generateScreenshots(primaryColorsList[5]); + generateScreenshots(primaryColorsList[3]); + generateScreenshots(primaryColorsList[9]); +} + +final _mockEquipmentProfiles = [ + const EquipmentProfile( + id: '', + name: '', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ), + EquipmentProfile( + id: '1', + name: 'Praktica + Zenitar', + apertureValues: ApertureValue.values.sublist( + ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)), + ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1, + ), + 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)) + 1, + ), + 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/integration_test/generate_timelines.sh b/integration_test/generate_timelines.sh new file mode 100644 index 0000000..042826e --- /dev/null +++ b/integration_test/generate_timelines.sh @@ -0,0 +1,10 @@ +flutter drive \ + --dart-define="cameraPreviewAspectRatio=240/320" \ + --dart-define="cameraStubImage=assets/camera_stub_image.jpg" \ + --driver=test_driver/performance_driver.dart \ + --target=integration_test/metering_test.dart \ + --profile \ + --flavor=dev \ + --no-dds \ + --endless-trace-buffer \ + --purge-persistent-cache diff --git a/integration_test/metering_test.dart b/integration_test/metering_test.dart new file mode 100644 index 0000000..ddf7182 --- /dev/null +++ b/integration_test/metering_test.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:lightmeter/application.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/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/res/theme.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/dialog_picker/widget_picker_dialog.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; +import 'package:lightmeter/screens/settings/screen_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:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'utils/widget_tester_extension.dart'; + +class _MockSharedPreferences extends Mock implements SharedPreferences {} + +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 {} + +//https://stackoverflow.com/a/67186625/13167574 +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(); + + setUpAll(() { + mockUserPreferencesService = _MockUserPreferencesService(); + when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera); + 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.primaryColor).thenReturn(primaryColorsList[5]); + when(() => mockUserPreferencesService.dynamicColor).thenReturn(false); + + mockCaffeineService = _MockCaffeineService(); + when(() => mockCaffeineService.isKeepScreenOn()).thenAnswer((_) async => false); + when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true); + when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false); + + 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 {}); + }); + + Future pumpApplication(WidgetTester tester) async { + await tester.pumpWidget( + IAPProviders( + sharedPreferences: _MockSharedPreferences(), + child: EquipmentProfiles( + selected: _mockEquipmentProfiles[0], + values: _mockEquipmentProfiles, + child: Films( + selected: const Film('Ilford HP5+', 400), + values: const [Film.other(), Film('Ilford HP5+', 400)], + filmsInUse: const [Film.other(), Film('Ilford HP5+', 400)], + child: ServicesProvider( + environment: const Environment.prod().copyWith(hasLightSensor: true), + userPreferencesService: mockUserPreferencesService, + caffeineService: mockCaffeineService, + hapticsService: mockHapticsService, + permissionsService: mockPermissionsService, + lightSensorService: mockLightSensorService, + volumeEventsService: mockVolumeEventsService, + child: const UserPreferencesProvider(child: Application()), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + } + + testWidgets('Pickers test', (tester) async { + await pumpApplication(tester); + + await tester.takePhoto(); + + expect(find.byType(IsoValuePicker), findsOneWidget); + await binding.traceActionNamed( + () async { + await tester.tap(find.byType(IsoValuePicker)); + await tester.pumpAndSettle(Dimens.durationL); + }, + "open_iso_picker", + ); + expect(find.byType(DialogPicker), findsOneWidget); + await tester.tapCancelButton(); + expect(find.byType(DialogPicker), findsNothing); + }); +} + +extension on IntegrationTestWidgetsFlutterBinding { + Future traceActionNamed(Future Function() action, String timelineName) async { + final nowString = DateTime.now().toIso8601String().replaceAll(':', '-'); + await traceAction(action, reportKey: "${timelineName}_$nowString"); + } +} + +final _mockEquipmentProfiles = [ + const EquipmentProfile( + id: '', + name: '', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ), + EquipmentProfile( + id: '1', + name: 'Praktica + Zenitar', + apertureValues: ApertureValue.values.sublist( + ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)), + ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1, + ), + 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)) + 1, + ), + 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/integration_test/mocks/application_mock.dart b/integration_test/mocks/application_mock.dart deleted file mode 100644 index be2de3b..0000000 --- a/integration_test/mocks/application_mock.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:lightmeter/data/models/theme_type.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:mocktail/mocktail.dart'; - -class _MockUserPreferencesService extends Mock implements UserPreferencesService {} - -class ApplicationMock extends StatefulWidget { - final Widget child; - - const ApplicationMock({required this.child, super.key}); - - @override - State createState() => _ApplicationMockState(); -} - -class _ApplicationMockState extends State { - late final _MockUserPreferencesService userPreferencesService; - - @override - void initState() { - super.initState(); - userPreferencesService = _MockUserPreferencesService(); - when(() => userPreferencesService.themeType).thenReturn(ThemeType.light); - when(() => userPreferencesService.primaryColor) - .thenReturn(ThemeProvider.primaryColorsList.first); - when(() => userPreferencesService.dynamicColor).thenReturn(false); - } - - @override - Widget build(BuildContext context) { - return InheritedWidgetBase( - data: userPreferencesService, - child: ThemeProvider( - child: Builder( - builder: (context) { - return MaterialApp( - theme: context.listen(), - 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!, - ), - home: widget.child, - ); - }, - ), - ), - ); - } -} diff --git a/integration_test/utils/widget_tester_extension.dart b/integration_test/utils/widget_tester_extension.dart new file mode 100644 index 0000000..57d652a --- /dev/null +++ b/integration_test/utils/widget_tester_extension.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; +import 'package:lightmeter/screens/settings/screen_settings.dart'; + +extension WidgetTesterFinder on WidgetTester { + Future takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name) async { + if (Platform.isAndroid) { + await binding.convertFlutterSurfaceToImage(); + await pumpAndSettle(); + } + await binding.takeScreenshot(name); + await pumpAndSettle(); + } + + Future takePhoto() async { + await tap(find.byType(MeteringMeasureButton)); + await pump(const Duration(seconds: 2)); // wait for circular progress indicator + await pump(const Duration(seconds: 1)); // wait for circular progress indicator + await pumpAndSettle(); + } + + Future tapCancelButton() async { + final cancelButton = find.byWidgetPredicate( + (widget) => + widget is TextButton && + widget.child is Text && + (widget.child as Text?)?.data == S.current.cancel, + ); + expect(cancelButton, findsOneWidget); + await tap(cancelButton); + await pumpAndSettle(); + } + + Future tapListTile(String title) async { + final listTile = find.byWidgetPredicate( + (widget) => + widget is ListTile && widget.title is Text && (widget.title as Text?)?.data == title, + ); + expect(listTile, findsOneWidget); + await tap(listTile); + await pumpAndSettle(); + } + + Future openSettings() async { + final settingsButton = find.byTooltip(S.current.tooltipOpenSettings); + expect(settingsButton, findsOneWidget); + await tap(settingsButton); + await pumpAndSettle(); + expect(find.byType(SettingsScreen), findsOneWidget); + } +} diff --git a/integration_test/widget_dialog_animated_test.dart b/integration_test/widget_dialog_animated_test.dart deleted file mode 100644 index 1f5666a..0000000 --- a/integration_test/widget_dialog_animated_test.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:lightmeter/data/models/film.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; -import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; - -import 'mocks/application_mock.dart'; - -void main() { - runApp(const ApplicationMock(child: AnimatedPickerTest())); -} - -void main2() { - final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('AnimatedDialogPicker test', () { - testWidgets('Tap on `ReadingValueContainer`, verify opened', (tester) async { - await tester.pumpWidget(const ApplicationMock(child: AnimatedPickerTest())); - expect(find.text('Film'), findsOneWidget); - expect(find.text('None'), findsOneWidget); - - await binding.traceAction( - () async { - await tester.tap(find.byType(AnimatedDialogPicker)); - await tester.pumpAndSettle(Dimens.durationL); - }, - reportKey: 'dialog_opening_timeline', - ); - - expect(find.text('Film'), findsNWidgets(3)); - expect(find.text('None'), findsNWidgets(3)); - }); - }); -} - -class AnimatedPickerTest extends StatefulWidget { - const AnimatedPickerTest({super.key}); - - @override - State createState() => _AnimatedPickerTestState(); -} - -class _AnimatedPickerTestState extends State { - Film _selectedFilm = Film.values.first; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: _FilmPicker( - values: Film.values, - selectedValue: _selectedFilm, - onChanged: (value) { - setState(() { - _selectedFilm = value; - }); - }, - ), - ), - ); - } -} - -class _FilmPicker extends StatelessWidget { - final List values; - final Film selectedValue; - final ValueChanged onChanged; - - const _FilmPicker({ - required this.values, - required this.selectedValue, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return AnimatedDialogPicker( - icon: Icons.camera_roll, - title: "Film", - selectedValue: selectedValue, - values: values, - itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? 'None' : value.name), - onChanged: onChanged, - closedChild: ReadingValueContainer.singleValue( - value: ReadingValue( - label: "Film", - value: selectedValue.name.isEmpty ? 'None' : selectedValue.name, - ), - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 6ebe8a3..6a81741 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: dev_dependencies: bloc_test: 9.1.3 - build_runner: ^2.1.7 + build_runner: 2.4.6 flutter_driver: sdk: flutter flutter_launcher_icons: 0.11.0 diff --git a/screenshots/4280391411-equipment_profiles.png b/screenshots/4280391411-equipment_profiles.png new file mode 100644 index 0000000..e90ec31 Binary files /dev/null and b/screenshots/4280391411-equipment_profiles.png differ diff --git a/screenshots/4280391411_equipment_profiles_iso_picker.png b/screenshots/4280391411_equipment_profiles_iso_picker.png new file mode 100644 index 0000000..b0d6d7c Binary files /dev/null and b/screenshots/4280391411_equipment_profiles_iso_picker.png differ diff --git a/screenshots/4280391411_metering_incident.png b/screenshots/4280391411_metering_incident.png new file mode 100644 index 0000000..ef6761e Binary files /dev/null and b/screenshots/4280391411_metering_incident.png differ diff --git a/screenshots/4280391411_metering_incident_dark.png b/screenshots/4280391411_metering_incident_dark.png new file mode 100644 index 0000000..91fa83a Binary files /dev/null and b/screenshots/4280391411_metering_incident_dark.png differ diff --git a/screenshots/4280391411_metering_iso_picker.png b/screenshots/4280391411_metering_iso_picker.png new file mode 100644 index 0000000..ddb5600 Binary files /dev/null and b/screenshots/4280391411_metering_iso_picker.png differ diff --git a/screenshots/4280391411_metering_reflected.png b/screenshots/4280391411_metering_reflected.png new file mode 100644 index 0000000..131f1c0 Binary files /dev/null and b/screenshots/4280391411_metering_reflected.png differ diff --git a/screenshots/4280391411_metering_reflected_dark.png b/screenshots/4280391411_metering_reflected_dark.png new file mode 100644 index 0000000..a8798f4 Binary files /dev/null and b/screenshots/4280391411_metering_reflected_dark.png differ diff --git a/screenshots/4280391411_settings.png b/screenshots/4280391411_settings.png new file mode 100644 index 0000000..7affafc Binary files /dev/null and b/screenshots/4280391411_settings.png differ diff --git a/screenshots/4280391411_settings_metering_screen_layout.png b/screenshots/4280391411_settings_metering_screen_layout.png new file mode 100644 index 0000000..1f3e830 Binary files /dev/null and b/screenshots/4280391411_settings_metering_screen_layout.png differ diff --git a/screenshots/4283215696-equipment_profiles.png b/screenshots/4283215696-equipment_profiles.png new file mode 100644 index 0000000..5b378be Binary files /dev/null and b/screenshots/4283215696-equipment_profiles.png differ diff --git a/screenshots/4283215696_equipment_profiles_iso_picker.png b/screenshots/4283215696_equipment_profiles_iso_picker.png new file mode 100644 index 0000000..1bb5e9b Binary files /dev/null and b/screenshots/4283215696_equipment_profiles_iso_picker.png differ diff --git a/screenshots/4283215696_metering_incident.png b/screenshots/4283215696_metering_incident.png new file mode 100644 index 0000000..a436e8f Binary files /dev/null and b/screenshots/4283215696_metering_incident.png differ diff --git a/screenshots/4283215696_metering_incident_dark.png b/screenshots/4283215696_metering_incident_dark.png new file mode 100644 index 0000000..8c6126e Binary files /dev/null and b/screenshots/4283215696_metering_incident_dark.png differ diff --git a/screenshots/4283215696_metering_iso_picker.png b/screenshots/4283215696_metering_iso_picker.png new file mode 100644 index 0000000..c839f4d Binary files /dev/null and b/screenshots/4283215696_metering_iso_picker.png differ diff --git a/screenshots/4283215696_metering_reflected.png b/screenshots/4283215696_metering_reflected.png new file mode 100644 index 0000000..1150a67 Binary files /dev/null and b/screenshots/4283215696_metering_reflected.png differ diff --git a/screenshots/4283215696_metering_reflected_dark.png b/screenshots/4283215696_metering_reflected_dark.png new file mode 100644 index 0000000..54931a2 Binary files /dev/null and b/screenshots/4283215696_metering_reflected_dark.png differ diff --git a/screenshots/4283215696_settings.png b/screenshots/4283215696_settings.png new file mode 100644 index 0000000..54bb433 Binary files /dev/null and b/screenshots/4283215696_settings.png differ diff --git a/screenshots/4283215696_settings_metering_screen_layout.png b/screenshots/4283215696_settings_metering_screen_layout.png new file mode 100644 index 0000000..7590bdf Binary files /dev/null and b/screenshots/4283215696_settings_metering_screen_layout.png differ diff --git a/screenshots/4284955319-equipment_profiles.png b/screenshots/4284955319-equipment_profiles.png new file mode 100644 index 0000000..061c989 Binary files /dev/null and b/screenshots/4284955319-equipment_profiles.png differ diff --git a/screenshots/4284955319_equipment_profiles_iso_picker.png b/screenshots/4284955319_equipment_profiles_iso_picker.png new file mode 100644 index 0000000..4e7f4bd Binary files /dev/null and b/screenshots/4284955319_equipment_profiles_iso_picker.png differ diff --git a/screenshots/4284955319_metering_incident.png b/screenshots/4284955319_metering_incident.png new file mode 100644 index 0000000..7b9cd29 Binary files /dev/null and b/screenshots/4284955319_metering_incident.png differ diff --git a/screenshots/4284955319_metering_incident_dark.png b/screenshots/4284955319_metering_incident_dark.png new file mode 100644 index 0000000..633e119 Binary files /dev/null and b/screenshots/4284955319_metering_incident_dark.png differ diff --git a/screenshots/4284955319_metering_iso_picker.png b/screenshots/4284955319_metering_iso_picker.png new file mode 100644 index 0000000..4a22584 Binary files /dev/null and b/screenshots/4284955319_metering_iso_picker.png differ diff --git a/screenshots/4284955319_metering_reflected.png b/screenshots/4284955319_metering_reflected.png new file mode 100644 index 0000000..5d485f9 Binary files /dev/null and b/screenshots/4284955319_metering_reflected.png differ diff --git a/screenshots/4284955319_metering_reflected_dark.png b/screenshots/4284955319_metering_reflected_dark.png new file mode 100644 index 0000000..ef208b0 Binary files /dev/null and b/screenshots/4284955319_metering_reflected_dark.png differ diff --git a/screenshots/4284955319_settings.png b/screenshots/4284955319_settings.png new file mode 100644 index 0000000..b3ac870 Binary files /dev/null and b/screenshots/4284955319_settings.png differ diff --git a/screenshots/4284955319_settings_metering_screen_layout.png b/screenshots/4284955319_settings_metering_screen_layout.png new file mode 100644 index 0000000..89286c2 Binary files /dev/null and b/screenshots/4284955319_settings_metering_screen_layout.png differ diff --git a/test_driver/performance_driver.dart b/test_driver/performance_driver.dart index d63b117..57c24df 100644 --- a/test_driver/performance_driver.dart +++ b/test_driver/performance_driver.dart @@ -1,26 +1,25 @@ import 'package:flutter_driver/flutter_driver.dart' as driver; import 'package:integration_test/integration_test_driver.dart'; -Future main() { - return integrationDriver( +import 'utils/android_camera_permission.dart'; + +Future main() async { + await grandCameraPermission(); + await integrationDriver( responseDataCallback: (data) async { if (data != null) { - final timeline = - driver.Timeline.fromJson(data['dialog_opening_timeline'] as Map); + for (final String timelineName in data.keys) { + final timeline = driver.Timeline.fromJson(data[timelineName] as Map); + final summary = driver.TimelineSummary.summarize(timeline); - // Convert the Timeline into a TimelineSummary that's easier to - // read and understand. - final summary = driver.TimelineSummary.summarize(timeline); - - // Then, write the entire timeline to disk in a json format. - // This file can be opened in the Chrome browser's tracing tools - // found by navigating to chrome://tracing. - // Optionally, save the summary to disk by setting includeSummary - // to true - await summary.writeTimelineToFile( - 'dialog_opening_timeline(fade)', - pretty: true, - ); + // Write the entire timeline and summary to disk in a json format. + // This file can be opened in the Chrome browser's tracing tools + // found by navigating to chrome://tracing. + await summary.writeTimelineToFile( + timelineName, + pretty: true, + ); + } } }, ); diff --git a/test_driver/screenshot_driver.dart b/test_driver/screenshot_driver.dart new file mode 100644 index 0000000..2a7525d --- /dev/null +++ b/test_driver/screenshot_driver.dart @@ -0,0 +1,20 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:integration_test/integration_test_driver_extended.dart'; + +import 'utils/android_camera_permission.dart'; + +Future main() async { + try { + await grandCameraPermission(); + await integrationDriver( + onScreenshot: (name, bytes, [args]) async { + final File image = await File('screenshots/$name.png').create(recursive: true); + image.writeAsBytesSync(bytes); + return true; + }, + ); + } catch (e) { + log('Error occured: $e'); + } +} diff --git a/test_driver/utils/android_camera_permission.dart b/test_driver/utils/android_camera_permission.dart new file mode 100644 index 0000000..b194248 --- /dev/null +++ b/test_driver/utils/android_camera_permission.dart @@ -0,0 +1,34 @@ +import 'dart:developer'; +import 'dart:io'; + +Future grandCameraPermission() async { + try { + final bool adbExists = Process.runSync('which', ['adb']).exitCode == 0; + if (!adbExists) { + log(r'This test needs ADB to exist on the $PATH. Skipping...'); + exit(0); + } + final deviceId = await Process.run('adb', ["-s", 'shell', 'devices']).then((value) { + if (value.stdout is String) { + return RegExp(r"(?:List of devices attached\n)([A-Z0-9]*)(?:\sdevice\n)") + .firstMatch(value.stdout as String)! + .group(1); + } + }); + if (deviceId == null) { + log('This test needs at least one device connected'); + exit(0); + } + await Process.run('adb', [ + "-s", + deviceId, // https://github.com/flutter/flutter/issues/86295#issuecomment-1192766368 + 'shell', + 'pm', + 'grant', + 'com.vodemn.lightmeter.dev', + 'android.permission.CAMERA' + ]); + } catch (e) { + log('Error occured: $e'); + } +}