diff --git a/integration_test/metering_screen_layout_test.dart b/integration_test/metering_screen_layout_test.dart index 512cd78..3775870 100644 --- a/integration_test/metering_screen_layout_test.dart +++ b/integration_test/metering_screen_layout_test.dart @@ -6,21 +6,16 @@ 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/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart'; -import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.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/screen_metering.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:meta/meta.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../integration_test/utils/widget_tester_actions.dart'; import 'mocks/paid_features_mock.dart'; - -const _mockPhotoEv100 = 8.3; +import 'utils/expectations.dart'; @isTestGroup void testToggleLayoutFeatures(String description) { @@ -46,11 +41,11 @@ void testToggleLayoutFeatures(String description) { (tester) async { await tester.pumpApplication(selectedEquipmentProfileId: mockEquipmentProfiles.first.id); await tester.takePhoto(); - _expectPickerTitle(mockEquipmentProfiles.first.name); - _expectExtremeExposurePairs('f/1.8 - 1/100', 'f/16 - 1/1.3'); - _expectExposurePairsListItem(tester, 'f/1.8', '1/100'); - await tester.scrollToTheLastExposurePair(mockEquipmentProfiles.first); - _expectExposurePairsListItem(tester, 'f/16', '1/1.3'); + expectPickerTitle(mockEquipmentProfiles.first.name); + expectExtremeExposurePairs('f/1.8 - 1/100', 'f/16 - 1/1.3'); + expectExposurePairsListItem(tester, 'f/1.8', '1/100'); + await tester.scrollToTheLastExposurePair(equipmentProfile: mockEquipmentProfiles.first); + expectExposurePairsListItem(tester, 'f/16', '1/1.3'); // Disable layout feature await tester.toggleLayoutFeature(S.current.meteringScreenLayoutHintEquipmentProfiles); @@ -60,12 +55,12 @@ void testToggleLayoutFeatures(String description) { reason: 'Equipment profile picker must be hidden from the metering screen when the corresponding layout feature is disabled.', ); - _expectExtremeExposurePairs( + expectExtremeExposurePairs( 'f/1.0 - 1/320', 'f/45 - 6"', reason: 'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset', ); - _expectExposurePairsListItem( + expectExposurePairsListItem( tester, 'f/1.0', '1/320', @@ -73,7 +68,7 @@ void testToggleLayoutFeatures(String description) { 'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset.', ); await tester.scrollToTheLastExposurePair(); - _expectExposurePairsListItem( + expectExposurePairsListItem( tester, 'f/45', '6"', @@ -83,7 +78,7 @@ void testToggleLayoutFeatures(String description) { // Enable layout feature await tester.toggleLayoutFeature(S.current.meteringScreenLayoutHintEquipmentProfiles); - _expectPickerTitle( + expectPickerTitle( S.current.none, reason: 'Equipment profile must remain unselected when the corresponding layout feature is re-enabled.', ); @@ -95,10 +90,10 @@ void testToggleLayoutFeatures(String description) { (tester) async { await tester.pumpApplication(); await tester.takePhoto(); - _expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 6"'); - _expectExposurePairsListItem(tester, 'f/1.0', '1/320'); + expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 6"'); + expectExposurePairsListItem(tester, 'f/1.0', '1/320'); await tester.scrollToTheLastExposurePair(); - _expectExposurePairsListItem(tester, 'f/45', '6"'); + expectExposurePairsListItem(tester, 'f/45', '6"'); // Disable layout feature await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs); @@ -108,7 +103,7 @@ void testToggleLayoutFeatures(String description) { reason: 'Extreme exposure pairs container must be hidden from the metering screen when the corresponding layout feature is disabled.', ); - _expectExposurePairsListItem( + expectExposurePairsListItem( tester, 'f/1.0', '1/320', @@ -116,7 +111,7 @@ void testToggleLayoutFeatures(String description) { 'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.', ); await tester.scrollToTheLastExposurePair(); - _expectExposurePairsListItem( + expectExposurePairsListItem( tester, 'f/45', '6"', @@ -126,7 +121,7 @@ void testToggleLayoutFeatures(String description) { // Enable layout feature await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs); - _expectExtremeExposurePairs( + expectExtremeExposurePairs( 'f/1.0 - 1/320', 'f/45 - 6"', reason: @@ -140,11 +135,11 @@ void testToggleLayoutFeatures(String description) { (tester) async { await tester.pumpApplication(selectedFilm: mockFilms.first); await tester.takePhoto(); - _expectPickerTitle(mockFilms.first.name); - _expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 12"'); - _expectExposurePairsListItem(tester, 'f/1.0', '1/320'); + expectPickerTitle(mockFilms.first.name); + expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 12"'); + expectExposurePairsListItem(tester, 'f/1.0', '1/320'); await tester.scrollToTheLastExposurePair(); - _expectExposurePairsListItem(tester, 'f/45', '12"'); + expectExposurePairsListItem(tester, 'f/45', '12"'); // Disable layout feature await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker); @@ -154,19 +149,19 @@ void testToggleLayoutFeatures(String description) { reason: 'Film picker must be hidden from the metering screen when the corresponding layout feature is disabled.', ); - _expectExtremeExposurePairs( + expectExtremeExposurePairs( 'f/1.0 - 1/320', 'f/45 - 6"', reason: 'Shutter speed must not be affected by reciprocity when film is discarded.', ); - _expectExposurePairsListItem( + expectExposurePairsListItem( tester, 'f/1.0', '1/320', reason: 'Shutter speed must not be affected by reciprocity when film is discarded.', ); await tester.scrollToTheLastExposurePair(); - _expectExposurePairsListItem( + expectExposurePairsListItem( tester, 'f/45', '6"', @@ -175,7 +170,7 @@ void testToggleLayoutFeatures(String description) { // Enable layout feature await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker); - _expectPickerTitle( + expectPickerTitle( S.current.none, reason: 'Film must remain unselected when the corresponding layout feature is re-enabled.', ); @@ -193,46 +188,4 @@ extension on WidgetTester { await tapSaveButton(); await navigatorPop(); } - - Future scrollToTheLastExposurePair([EquipmentProfile equipmentProfile = defaultEquipmentProfile]) async { - final exposurePairs = MeteringContainerBuidler.buildExposureValues( - _mockPhotoEv100, - StopType.third, - equipmentProfile, - ); - await scrollUntilVisible( - find.byWidgetPredicate((widget) => widget is Row && widget.key == ValueKey(exposurePairs.length - 1)), - 56, - scrollable: find.descendant(of: find.byType(ExposurePairsList), matching: find.byType(Scrollable)), - ); - } -} - -void _expectPickerTitle(String title, {String? reason}) { - expect(find.descendant(of: find.byType(T), matching: find.text(title)), findsOneWidget, reason: reason); -} - -void _expectExtremeExposurePairs(String fastest, String slowest, {String? reason}) { - final pickerFinder = find.byType(ExtremeExposurePairsContainer); - expect(find.descendant(of: pickerFinder, matching: find.text(fastest)), findsOneWidget, reason: reason); - expect(find.descendant(of: pickerFinder, matching: find.text(slowest)), findsOneWidget, reason: reason); -} - -void _expectExposurePairsListItem(WidgetTester tester, String aperture, String shutterSpeed, {String? reason}) { - Key? findKey>(String value) => tester - .widget( - find.ancestor( - of: find.ancestor( - of: find.text(value), - matching: find.byType(ExposurePairsListItem), - ), - matching: find.descendant(of: find.byType(ExposurePairsList), matching: find.byType(Row)), - ), - ) - .key; - expect( - findKey(aperture), - findKey(shutterSpeed), - reason: reason, - ); } diff --git a/integration_test/metering_screen_pickers_test.dart b/integration_test/metering_screen_pickers_test.dart new file mode 100644 index 0000000..89a2d32 --- /dev/null +++ b/integration_test/metering_screen_pickers_test.dart @@ -0,0 +1,264 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/data/models/exposure_pair.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/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; +import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart'; +import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.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/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart'; +import 'package:lightmeter/screens/metering/screen_metering.dart'; +import 'package:lightmeter/screens/settings/screen_settings.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:meta/meta.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../integration_test/utils/widget_tester_actions.dart'; +import 'mocks/paid_features_mock.dart'; + +const mockPhotoFastestAperture = ApertureValue(1, StopType.full); +const mockPhotoSlowestAperture = ApertureValue(45, StopType.full); +const mockPhotoFastestShutterSpeed = ShutterSpeedValue(320, true, StopType.third); +const mockPhotoSlowestShutterSpeed = ShutterSpeedValue(6, false, StopType.third); +const mockPhotoFastestExposurePair = ExposurePair(mockPhotoFastestAperture, mockPhotoFastestShutterSpeed); +const mockPhotoSlowestExposurePair = ExposurePair(mockPhotoSlowestAperture, mockPhotoSlowestShutterSpeed); + +class MeteringValuesExpectation { + final String fastestExposurePair; + final String slowestExposurePair; + final double ev; + + const MeteringValuesExpectation( + this.fastestExposurePair, + this.slowestExposurePair, + this.ev, + ); +} + +@isTestGroup +void testMeteringScreenPickers(String description) { + group( + description, + () { + setUp(() { + SharedPreferences.setMockInitialValues({ + /// Metering values + UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index, + UserPreferencesService.meteringScreenLayoutKey: json.encode( + { + MeteringScreenLayoutFeature.equipmentProfiles: true, + MeteringScreenLayoutFeature.extremeExposurePairs: true, + MeteringScreenLayoutFeature.filmPicker: true, + }.toJson(), + ), + }); + }); + + group( + 'Select film', + () { + testMeteringPicker( + 'with the same ISO', + expectBefore: MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + mockPhotoSlowestExposurePair.toString(), + mockPhotoEv100, + ), + valueToSelect: mockFilms[0].name, + expectAfter: MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + '$mockPhotoSlowestAperture - ${mockFilms[0].reciprocityFailure(mockPhotoSlowestShutterSpeed)}', + mockPhotoEv100, + ), + ); + + testMeteringPicker( + 'with greater ISO', + expectBefore: MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + mockPhotoSlowestExposurePair.toString(), + mockPhotoEv100, + ), + valueToSelect: mockFilms[1].name, + expectAfter: MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + '$mockPhotoSlowestAperture - ${mockFilms[1].reciprocityFailure(mockPhotoSlowestShutterSpeed)}', + mockPhotoEv100, + ), + ); + }, + ); + + testMeteringPicker( + 'Select ISO +1 EV', + expectBefore: MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + mockPhotoSlowestExposurePair.toString(), + mockPhotoEv100, + ), + valueToSelect: '400', + expectAfter: MeteringValuesExpectation( + '$mockPhotoFastestAperture - 1/1250', + '$mockPhotoSlowestAperture - 1.6"', + mockPhotoEv100 + 2, + ), + ); + + testMeteringPicker( + 'Select ND -1 EV', + expectBefore: MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + mockPhotoSlowestExposurePair.toString(), + mockPhotoEv100, + ), + valueToSelect: '2', + expectAfter: MeteringValuesExpectation( + '$mockPhotoFastestAperture - 1/160', + '$mockPhotoSlowestAperture - 13"', + mockPhotoEv100 - 1, + ), + ); + + testWidgets( + description, + (tester) async { + Future selectAndExpect( + String valueToSelect, + MeteringValuesExpectation expectation, { + String? reason, + }) async { + /// Verify, that EV is recalculated with a new setting + await tester.openPickerAndSelect(valueToSelect); + _expectPickerTitle

(valueToSelect); + expectExposurePairsContainer(expectation.fastestExposurePair, expectation.slowestExposurePair); + expectMeasureButton(expectation.ev); + + /// Make sure, that the selected setting is applied in the subsequent measurements + await tester.takePhoto(); + await tester.takePhoto(); + expectExposurePairsContainer(expectation.fastestExposurePair, expectation.slowestExposurePair); + expectMeasureButton(expectation.ev); + } + + await tester.pumpApplication(); + await tester.takePhoto(); + + await selectAndExpect( + '400', + MeteringValuesExpectation( + '$mockPhotoFastestAperture - 1/1250', + '$mockPhotoSlowestAperture - 1.6"', + mockPhotoEv100 + 2, + ), + reason: 'Selecting ISO value must change EV value and therefore exposure pairs.', + ); + + await selectAndExpect( + '2', + MeteringValuesExpectation( + '$mockPhotoFastestAperture - 1/640', + '$mockPhotoSlowestAperture - 3"', + mockPhotoEv100 - 1, + ), + reason: 'Selecting ND value must change EV value and therefore exposure pairs.', + ); + + await selectAndExpect( + mockFilms[0].name, + MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + '$mockPhotoSlowestAperture - ${mockFilms[0].reciprocityFailure(mockPhotoSlowestShutterSpeed)}', + mockPhotoEv100, + ), + reason: 'Selecting with the same ISO must change nothing exept exposure pairs due to reciprocity.', + ); + await selectAndExpect( + mockFilms[1].name, + MeteringValuesExpectation( + mockPhotoFastestExposurePair.toString(), + '$mockPhotoSlowestAperture - ${mockFilms[0].reciprocityFailure(mockPhotoSlowestShutterSpeed)}', + mockPhotoEv100, + ), + reason: + 'Selecting with a different ISO must must indicate push/pull and can change nothing exept exposure pairs due to reciprocity.', + ); + }, + ); + }, + ); +} + +/// Runs the picker test +/// +/// 1. Takes photo and verifies `expectBefore` values +/// 2. Opens a picker and select the provided value +/// 3. Verifies `expectAfter` +/// 4. Takes photo and verifies `expectAfter` values +@isTest +void testMeteringPicker( + String description, { + required MeteringValuesExpectation expectBefore, + required String valueToSelect, + required MeteringValuesExpectation expectAfter, + bool? skip, +}) { + testWidgets( + description, + (tester) async { + await tester.pumpApplication(); + + // Verify initial EV + await tester.toggleIncidentMetering(expectBefore.ev); + expectExposurePairsContainer(expectBefore.fastestExposurePair, expectBefore.slowestExposurePair); + expectMeasureButton(expectBefore.ev); + + /// Verify, that EV is recalculated with a new setting + await tester.openPickerAndSelect(valueToSelect); + expectExposurePairsContainer(expectAfter.fastestExposurePair, expectAfter.slowestExposurePair); + expectMeasureButton(expectAfter.ev); + + /// Make sure, that the selected setting is applied in the subsequent measurements + await tester.toggleIncidentMetering(expectBefore.ev); + expectExposurePairsContainer(expectAfter.fastestExposurePair, expectAfter.slowestExposurePair); + expectMeasureButton(expectAfter.ev); + }, + skip: skip, + ); +} + +extension on WidgetTester { + Future openPickerAndSelect(String valueToSelect) async { + await openAnimatedPicker

(); + await tapDescendantTextOf>(valueToSelect); + await tapSelectButton(); + } +} + +void _expectPickerTitle(String title, {String? reason}) { + expect(find.descendant(of: find.byType(T), matching: find.text(title)), findsOneWidget, reason: reason); +} + +void expectExposurePairsContainer(String fastest, String slowest) { + final pickerFinder = find.byType(ExtremeExposurePairsContainer); + expect(pickerFinder, findsOneWidget); + expect(find.descendant(of: pickerFinder, matching: find.text(S.current.fastestExposurePair)), findsOneWidget); + expect(find.descendant(of: pickerFinder, matching: find.text(fastest)), findsOneWidget); + expect(find.descendant(of: pickerFinder, matching: find.text(S.current.slowestExposurePair)), findsOneWidget); + expect(find.descendant(of: pickerFinder, matching: find.text(slowest)), findsOneWidget); +} + +void expectMeasureButton(double ev) { + find.descendant( + of: find.byType(MeteringMeasureButton), + matching: find.text('${ev.toStringAsFixed(1)}\n${S.current.ev}'), + ); +} diff --git a/integration_test/utils/expectations.dart b/integration_test/utils/expectations.dart new file mode 100644 index 0000000..6fb6b10 --- /dev/null +++ b/integration_test/utils/expectations.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart'; +import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +void expectPickerTitle

(String title, {String? reason}) { + expect(find.descendant(of: find.byType(P), matching: find.text(title)), findsOneWidget, reason: reason); +} + +void expectExtremeExposurePairs(String fastest, String slowest, {String? reason}) { + final pickerFinder = find.byType(ExtremeExposurePairsContainer); + expect(find.descendant(of: pickerFinder, matching: find.text(fastest)), findsOneWidget, reason: reason); + expect(find.descendant(of: pickerFinder, matching: find.text(slowest)), findsOneWidget, reason: reason); +} + +void expectExposurePairsListItem(WidgetTester tester, String aperture, String shutterSpeed, {String? reason}) { + Key? findKey>(String value) => tester + .widget( + find.ancestor( + of: find.ancestor( + of: find.text(value), + matching: find.byType(ExposurePairsListItem), + ), + matching: find.descendant(of: find.byType(ExposurePairsList), matching: find.byType(Row)), + ), + ) + .key; + expect( + findKey(aperture), + findKey(shutterSpeed), + reason: reason, + ); +} diff --git a/integration_test/utils/widget_tester_actions.dart b/integration_test/utils/widget_tester_actions.dart index e735919..ec09083 100644 --- a/integration_test/utils/widget_tester_actions.dart +++ b/integration_test/utils/widget_tester_actions.dart @@ -6,6 +6,8 @@ import 'package:lightmeter/environment.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/exposure_pairs_list/widget_list_exposure_pairs.dart'; +import 'package:lightmeter/screens/metering/screen_metering.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -13,6 +15,8 @@ import '../mocks/iap_products_mock.dart'; import '../mocks/paid_features_mock.dart'; import 'platform_channel_mock.dart'; +const mockPhotoEv100 = 8.3; + extension WidgetTesterCommonActions on WidgetTester { Future pumpApplication({ IAPProductStatus productStatus = IAPProductStatus.purchased, @@ -89,3 +93,22 @@ extension WidgetTesterTextButtonActions on WidgetTester { await pumpAndSettle(); } } + +extension WidgetTesterExposurePairsListActions on WidgetTester { + Future scrollToTheLastExposurePair({ + double ev = mockPhotoEv100, + StopType stopType = StopType.third, + EquipmentProfile equipmentProfile = defaultEquipmentProfile, + }) async { + final exposurePairs = MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + equipmentProfile, + ); + await scrollUntilVisible( + find.byWidgetPredicate((widget) => widget is Row && widget.key == ValueKey(exposurePairs.length - 1)), + 56, + scrollable: find.descendant(of: find.byType(ExposurePairsList), matching: find.byType(Scrollable)), + ); + } +}