Compare commits

..

No commits in common. "3c4d959cc68ab48c8f7c1221626e7a7264e9e2c0" and "24804a119e7b7d5cbb694ad5a944069c727cce3d" have entirely different histories.

12 changed files with 709 additions and 107 deletions

2
.gitignore vendored
View file

@ -61,4 +61,4 @@ ios/Runner/GoogleService-Info.plist
coverage/
test/coverage_helper_test.dart
screenshots/*.png
screenshots/

View file

@ -10,19 +10,18 @@ import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/generated/l10n.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/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
import 'package:lightmeter/screens/settings/screen_settings.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:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../integration_test/mocks/paid_features_mock.dart';
import '../integration_test/utils/widget_tester_actions.dart';
import 'utils/platform_channel_mock.dart';
import 'utils/widget_tester_actions.dart';
//https://stackoverflow.com/a/67186625/13167574
/// Just a screenshot generator. No expectations here.
void main() {
final binding = IntegrationTestWidgetsFlutterBinding();
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -30,7 +29,6 @@ void main() {
final Color darkThemeColor = primaryColorsList[3];
void mockSharedPrefs(ThemeType theme, Color color) {
// ignore: invalid_use_of_visible_for_testing_member
SharedPreferences.setMockInitialValues({
/// Metering values
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
@ -76,11 +74,14 @@ void main() {
if (Platform.isAndroid) {
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
await tester.pumpAndSettle();
await tester.toggleIncidentMetering(7.3);
await tester.tap(find.byType(MeteringMeasureButton));
await sendMockIncidentEv(7.3);
await tester.tap(find.byType(MeteringMeasureButton));
await tester.takeScreenshot(binding, '${lightThemeColor.value}_metering_incident');
}
await tester.openAnimatedPicker<IsoValuePicker>();
await tester.tap(find.byType(IsoValuePicker));
await tester.pumpAndSettle(Dimens.durationL);
await tester.takeScreenshot(binding, '${lightThemeColor.value}_metering_iso_picker');
await tester.tapCancelButton();
@ -88,13 +89,12 @@ void main() {
await tester.pumpAndSettle();
await tester.takeScreenshot(binding, '${lightThemeColor.value}_settings');
await tester.tapDescendantTextOf<SettingsScreen>(S.current.meteringScreenLayout);
await tester.tapListTile(S.current.meteringScreenLayout);
await tester.takeScreenshot(binding, '${lightThemeColor.value}_settings_metering_screen_layout');
await tester.tapCancelButton();
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
await tester.pumpAndSettle();
await tester.tapDescendantTextOf<EquipmentProfilesScreen>(mockEquipmentProfiles.first.name);
await tester.tapListTile(S.current.equipmentProfiles);
await tester.tap(find.byType(EquipmentProfileContainer).first);
await tester.pumpAndSettle();
await tester.takeScreenshot(binding, '${lightThemeColor.value}-equipment_profiles');
@ -117,7 +117,9 @@ void main() {
if (Platform.isAndroid) {
await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
await tester.pumpAndSettle();
await tester.toggleIncidentMetering(7.3);
await tester.tap(find.byType(MeteringMeasureButton));
await sendMockIncidentEv(7.3);
await tester.tap(find.byType(MeteringMeasureButton));
await tester.takeScreenshot(binding, '${darkThemeColor.value}_metering_incident');
}
},
@ -133,4 +135,13 @@ extension on WidgetTester {
await binding.takeScreenshot(name);
await pumpAndSettle();
}
Future<void> 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();
}
}

View file

@ -2,7 +2,7 @@ flutter drive \
--dart-define="cameraPreviewAspectRatio=240/320" \
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
--driver=test_driver/screenshot_driver.dart \
--target=screenshots/generate_screenshots.dart \
--target=integration_test/generate_screenshots.dart \
--profile \
--flavor=dev \
--no-dds \

View file

@ -0,0 +1,303 @@
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/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/exposure_pair.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/camera_container/widget_container_camera.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/widget_container_light_sensor.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: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/shared/icon_placeholder/widget_icon_placeholder.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'mocks/paid_features_mock.dart';
import 'utils/expectations.dart';
import 'utils/platform_channel_mock.dart';
import 'utils/widget_tester_actions.dart';
const defaultIsoValue = IsoValue(100, StopType.full);
const mockPhotoEv100 = 8.3;
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);
//https://stackoverflow.com/a/67186625/13167574
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group(
'[Light sensor availability]',
() {
testWidgets(
'Android - has sensor',
(tester) async {
SharedPreferences.setMockInitialValues({UserPreferencesService.evSourceTypeKey: EvSourceType.sensor.index});
setLightSensorAvilability(hasSensor: true);
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
/// Verify that [LightSensorContainer] is shown in correspondance with the saved ev source
expect(find.byType(LightSensorContainer), findsOneWidget);
},
skip: Platform.isIOS,
);
testWidgets(
'Android - no sensor',
(tester) async {
SharedPreferences.setMockInitialValues({UserPreferencesService.evSourceTypeKey: EvSourceType.sensor.index});
setLightSensorAvilability(hasSensor: false);
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
/// Verify that [CameraContainer] is shown instead of [LightSensorContainer]
expect(find.byType(CameraContainer), findsOneWidget);
/// and there is no ability to switch to the incident metering
expect(find.byTooltip(S.current.tooltipUseLightSensor), findsNothing);
},
skip: Platform.isIOS,
);
testWidgets(
'iOS - no sensor',
(tester) async {
SharedPreferences.setMockInitialValues({UserPreferencesService.evSourceTypeKey: EvSourceType.sensor.index});
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
/// verify no button to switch to the incident light mode
expect(find.byType(CameraContainer), findsOneWidget);
/// and there is no ability to switch to the incident metering
expect(find.byTooltip(S.current.tooltipUseLightSensor), findsNothing);
},
skip: Platform.isAndroid,
);
},
);
group(
'[Match extreme exposure pairs & pairs list edge values]',
() {
Future<List<ExposurePair>> scrollToTheLastExposurePair(WidgetTester tester) async {
final exposurePairs = MeteringContainerBuidler.buildExposureValues(
mockPhotoEv100,
StopType.third,
defaultEquipmentProfile,
);
await tester.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)),
);
return exposurePairs;
}
void expectExposurePairsListItem(int index, String aperture, String shutterSpeed) {
final firstPairRow = find.byWidgetPredicate((widget) => widget is Row && widget.key == ValueKey(index));
expect(find.descendant(of: firstPairRow, matching: find.text(aperture)), findsOneWidget);
expect(find.descendant(of: firstPairRow, matching: find.text(shutterSpeed)), findsOneWidget);
}
setUpAll(() {
SharedPreferences.setMockInitialValues({UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index});
});
testWidgets(
'No exposure pairs',
(tester) async {
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
/// Verify that no exposure pairs are shown in [ExtremeExposurePairsContainer]
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(S.current.slowestExposurePair)), findsOneWidget);
expect(find.descendant(of: pickerFinder, matching: find.text('-')), findsNWidgets(2));
/// Verify that the exposure pairs list is empty
expect(
find.descendant(
of: find.byType(ExposurePairsList),
matching: find.byWidgetPredicate(
(widget) =>
widget is IconPlaceholder &&
widget.icon == Icons.not_interested &&
widget.text == S.current.noExposurePairs,
),
),
findsOneWidget,
);
},
);
testWidgets(
'Multiple exposure pairs w/o reciprocity',
(tester) async {
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
await tester.takePhoto();
/// Verify that reciprocity is not applied to the slowest exposure pair in the container
expectExposurePairsContainer('$mockPhotoFastestAperture - 1/320', '$mockPhotoSlowestAperture - 6"');
expectMeasureButton(mockPhotoEv100);
/// Verify that reciprocity is not applied to the slowest exposure pair in the list
expectExposurePairsListItem(0, '$mockPhotoFastestAperture', '1/320');
final exposurePairs = await scrollToTheLastExposurePair(tester);
expectExposurePairsListItem(exposurePairs.length - 1, '$mockPhotoSlowestAperture', '6"');
expectMeasureButton(mockPhotoEv100);
},
);
testWidgets(
'Multiple exposure pairs w/ reciprocity',
(tester) async {
await tester.pumpApplication(selectedFilm: mockFilms.first);
await tester.takePhoto();
/// Verify that reciprocity is applied to the slowest exposure pair in the container
expectExposurePairsContainer('$mockPhotoFastestAperture - 1/320', '$mockPhotoSlowestAperture - 12"');
expectMeasureButton(mockPhotoEv100);
/// Verify that reciprocity is applied to the slowest exposure pair in the list
expectExposurePairsListItem(0, '$mockPhotoFastestAperture', '1/320');
final exposurePairs = await scrollToTheLastExposurePair(tester);
expectExposurePairsListItem(exposurePairs.length - 1, '$mockPhotoSlowestAperture', '12"');
expectMeasureButton(mockPhotoEv100);
},
);
},
);
group(
'[Pickers tests]',
() {
group('Select film', () {
testWidgets(
'with the same ISO',
(tester) async {
await tester.pumpApplication();
await tester.takePhoto();
// Verify that reciprocity failure is applies for the film is not selected
expectAnimatedPickerWith<FilmPicker>(title: S.current.film, value: S.current.none);
expectExposurePairsContainer('$mockPhotoFastestExposurePair', '$mockPhotoSlowestExposurePair');
expectMeasureButton(mockPhotoEv100);
await tester.openAnimatedPicker<FilmPicker>();
await tester.tapDescendantTextOf<DialogPicker<Film>>(mockFilms.first.name);
await tester.tapSelectButton();
/// Verify that exposure pairs are the same, except that the reciprocity failure is applied
expectExposurePairsContainer(
'$mockPhotoFastestExposurePair',
'$mockPhotoSlowestAperture - ${mockFilms.first.reciprocityFailure(mockPhotoSlowestShutterSpeed)}',
);
expectMeasureButton(mockPhotoEv100);
/// Make sure, that the EV is not changed
await tester.takePhoto();
expectExposurePairsContainer(
'$mockPhotoFastestExposurePair',
'$mockPhotoSlowestAperture - ${mockFilms.first.reciprocityFailure(mockPhotoSlowestShutterSpeed)}',
);
expectMeasureButton(mockPhotoEv100);
},
);
testWidgets(
'with greater ISO',
(tester) async {
await tester.pumpApplication();
await tester.takePhoto();
// Verify that reciprocity failure is applies for the film is not selected
expectAnimatedPickerWith<FilmPicker>(title: S.current.film, value: S.current.none);
expectExposurePairsContainer('$mockPhotoFastestExposurePair', '$mockPhotoSlowestExposurePair');
expectMeasureButton(mockPhotoEv100);
await tester.openAnimatedPicker<FilmPicker>();
await tester.tapDescendantTextOf<DialogPicker<Film>>(mockFilms[1].name);
await tester.tapSelectButton();
/// Verify that exposure pairs are the same, except that the reciprocity failure is applied
expectExposurePairsContainer(
'$mockPhotoFastestExposurePair',
'$mockPhotoSlowestAperture - ${mockFilms[1].reciprocityFailure(mockPhotoSlowestShutterSpeed)}',
);
expectMeasureButton(mockPhotoEv100);
/// Make sure, that the EV is not changed
await tester.takePhoto();
expectExposurePairsContainer(
'$mockPhotoFastestExposurePair',
'$mockPhotoSlowestAperture - ${mockFilms[1].reciprocityFailure(mockPhotoSlowestShutterSpeed)}',
);
expectMeasureButton(mockPhotoEv100);
},
);
});
testWidgets(
'Select ISO +1 EV',
(tester) async {
await tester.pumpApplication(productStatus: IAPProductStatus.purchased);
expectExposurePairsContainer('f/1.0 - 1/320', 'f/45 - 13"');
expectMeasureButton(mockPhotoEv100);
await tester.openAnimatedPicker<IsoValuePicker>();
expect(find.byType(DialogPicker<IsoValue>), findsOneWidget);
await tester.tapRadioListTile<IsoValue>('800');
await tester.tapSelectButton();
expectExposurePairsContainer('f/1.0 - 1/320', 'f/45 - 6"');
expectMeasureButton(8.3);
/// Make sure, that current ISO is used in metering
await tester.tap(find.byType(MeteringMeasureButton));
await tester.tap(find.byType(MeteringMeasureButton));
await tester.pumpAndSettle();
expectExposurePairsContainer('f/1.0 - 1/320', 'f/45 - 6"');
expectMeasureButton(8.3);
},
skip: true,
);
testWidgets(
'Select ND -1 EV',
(tester) async {
await tester.pumpApplication(productStatus: IAPProductStatus.purchased);
expectExposurePairsContainer('f/1.0 - 1/320', 'f/45 - 13"');
expectMeasureButton(mockPhotoEv100);
await tester.openAnimatedPicker<NdValuePicker>();
expect(find.byType(DialogPicker<NdValue>), findsOneWidget);
await tester.tapRadioListTile<NdValue>('2');
await tester.tapSelectButton();
expectExposurePairsContainer('f/1.0 - 1/80', 'f/36 - 16"');
expectMeasureButton(6.3);
/// Make sure, that current ISO is used in metering
await tester.tap(find.byType(MeteringMeasureButton));
await tester.tap(find.byType(MeteringMeasureButton));
await tester.pumpAndSettle();
expectExposurePairsContainer('f/1.0 - 1/80', 'f/36 - 16"');
expectMeasureButton(6.3);
},
skip: true,
);
},
);
}

View file

@ -0,0 +1,17 @@
# https://github.com/flutter/flutter/issues/86295#issuecomment-1192766368
devices=$(adb devices)
devicesIds=$(echo $devices | grep -Eo '[A-Z0-9]{2,}')
firstDeviceId=$(echo $devicesIds | cut -d " " -f 1)
# adb -s $firstDeviceId shell pm grant com.vodemn.lightmeter.dev android.permission.CAMERA
flutter drive \
--dart-define="cameraPreviewAspectRatio=240/320" \
--dart-define="cameraStubImage=assets/camera_stub_image.jpg" \
--driver=test_driver/integration_driver.dart \
--target=integration_test/metering_screen_test.dart \
--profile \
--flavor=dev \
--no-dds \
--endless-trace-buffer \
--purge-persistent-cache \
-d $firstDeviceId

View file

@ -0,0 +1,294 @@
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/theme.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/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.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 'mocks/paid_features_mock.dart';
import 'utils/expectations.dart';
import 'utils/widget_tester_actions.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 {}
const _defaultIsoValue = IsoValue(400, StopType.full);
//https://stackoverflow.com/a/67186625/13167574
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
late _MockUserPreferencesService mockUserPreferencesService;
late _MockCaffeineService mockCaffeineService;
late _MockHapticsService mockHapticsService;
late _MockPermissionsService mockPermissionsService;
late _MockLightSensorService mockLightSensorService;
late _MockVolumeEventsService mockVolumeEventsService;
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(_defaultIsoValue);
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: true,
});
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<int>.empty());
when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
});
Future<void> pumpApplication(
WidgetTester tester,
IAPProductStatus purchaseStatus, {
String selectedEquipmentProfileId = '',
Film selectedFilm = const Film.other(),
}) async {
await tester.pumpWidget(
MockIAPProviders(
purchaseStatus: purchaseStatus,
selectedEquipmentProfileId: selectedEquipmentProfileId,
selectedFilm: selectedFilm,
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();
}
group(
'[Metering layout features]',
() {
Future<void> toggleFeatureAndClose(WidgetTester tester, String feature) async {
await tester.openSettings();
expect(find.byType(SettingsScreen), findsOneWidget);
await tester.tap(find.text(S.current.meteringScreenLayout));
await tester.pumpAndSettle();
expect(find.byType(MeteringScreenLayoutFeaturesDialog), findsOneWidget);
await tester.tap(
find.descendant(
of: find.byType(SwitchListTile),
matching: find.text(feature),
),
);
await tester.tapSaveButton();
expect(find.byType(MeteringScreenLayoutFeaturesDialog), findsNothing);
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
}
testWidgets(
'Toggle equipmentProfiles & discard selected',
(tester) async {
await pumpApplication(
tester,
IAPProductStatus.purchased,
selectedEquipmentProfileId: mockEquipmentProfiles[0].id,
);
await tester.toggleIncidentMetering();
expectAnimatedPickerWith<EquipmentProfilePicker>(value: mockEquipmentProfiles[0].name);
expectExposurePairsContainer('f/1.8 - 1/50', 'f/16 - 1.6"');
expectMeasureButton(7.3);
await toggleFeatureAndClose(tester, S.current.meteringScreenLayoutHintEquipmentProfiles);
expect(find.byType(EquipmentProfilePicker), findsNothing);
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 13"');
expectMeasureButton(7.3);
await toggleFeatureAndClose(tester, S.current.meteringScreenLayoutHintEquipmentProfiles);
expectAnimatedPickerWith<EquipmentProfilePicker>(value: S.current.none);
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 13"');
expectMeasureButton(7.3);
},
);
testWidgets(
'Toggle extremeExposurePairs',
(tester) async {
await pumpApplication(tester, IAPProductStatus.purchased);
await tester.toggleIncidentMetering();
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 13"');
expectMeasureButton(7.3);
await toggleFeatureAndClose(tester, S.current.meteringScreenFeatureExtremeExposurePairs);
expect(find.byType(ExtremeExposurePairsContainer), findsNothing);
expectMeasureButton(7.3);
await toggleFeatureAndClose(tester, S.current.meteringScreenFeatureExtremeExposurePairs);
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 13"');
expectMeasureButton(7.3);
},
);
testWidgets(
'Toggle film & discard selected',
(tester) async {
await pumpApplication(
tester,
IAPProductStatus.purchased,
selectedFilm: mockFilms.first,
);
await tester.toggleIncidentMetering();
expectAnimatedPickerWith<FilmPicker>(value: mockFilms.first.name);
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 26"');
expectMeasureButton(7.3);
await toggleFeatureAndClose(tester, S.current.meteringScreenFeatureFilmPicker);
expect(find.byType(FilmPicker), findsNothing);
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 13"');
expectMeasureButton(7.3);
await toggleFeatureAndClose(tester, S.current.meteringScreenFeatureFilmPicker);
expectAnimatedPickerWith<FilmPicker>(value: S.current.none);
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 13"');
expectMeasureButton(7.3);
},
);
testWidgets(
'Toggle histogram',
(tester) async {
await pumpApplication(tester, IAPProductStatus.purchased);
},
skip: true, // TODO(@vodemn)
);
},
);
testWidgets(
'[Films in use] Deselect current',
(tester) async {
await pumpApplication(
tester,
IAPProductStatus.purchased,
selectedFilm: mockFilms[0],
);
// Check that film is selected and reciprocity is applied
await tester.toggleIncidentMetering();
expectAnimatedPickerWith<FilmPicker>(value: mockFilms[0].name);
expectExposurePairsContainer('f/1.0 - 1/160', 'f/45 - 26"');
expectMeasureButton(7.3);
// Deselect the first films
await tester.openSettings();
expect(find.byType(SettingsScreen), findsOneWidget);
await tester.tap(find.text(S.current.filmsInUse));
await tester.pumpAndSettle();
expect(find.byType(DialogFilter<Film>), findsOneWidget);
await tester.tap(
find.descendant(
of: find.byType(CheckboxListTile),
matching: find.text(mockFilms[0].name),
),
);
await tester.tapSaveButton();
expect(find.byType(DialogFilter<Film>), findsNothing);
await tester.tap(find.byIcon(Icons.close));
await tester.pumpAndSettle();
// The previously selected films is no longer in use and therefore is discarded to None
expectAnimatedPickerWith<FilmPicker>(value: S.current.none);
expectMeasureButton(7.3);
// The previously selected films is no longer in use and therefore is not present in the picker
await tester.openAnimatedPicker<FilmPicker>();
expect(find.byType(DialogPicker<Film>), findsOneWidget);
expect(
find.descendant(
of: find.byWidgetPredicate((widget) => widget is RadioListTile<Film> && widget.selected),
matching: find.text(mockFilms[0].name),
),
findsNothing,
);
},
);
}
extension _WidgetTesterActions on WidgetTester {
Future<void> openSettings() async {
expect(find.byTooltip(S.current.tooltipOpenSettings), findsOneWidget);
await tap(find.byTooltip(S.current.tooltipOpenSettings));
await pumpAndSettle();
}
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_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/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
/// Expects exactly one picker of the specified type and verifies `title` or/and `value` if any of the values is not null.
void expectAnimatedPickerWith<T>({String? title, String? value}) {
final pickerFinder = find.byType(T);
expect(pickerFinder, findsOneWidget);
if (title != null) expect(find.descendant(of: pickerFinder, matching: find.text(title)), findsOneWidget);
if (value != null) expect(find.descendant(of: pickerFinder, matching: find.text(value)), findsOneWidget);
}
/// Finds exactly one dialog picker of the provided value type
void expectDialogPicker<T>() {
expect(find.byType(DialogPicker<T>), findsOneWidget);
}
void expectMeasureButton(double ev) {
find.descendant(
of: find.byType(MeteringMeasureButton),
matching: find.text('${ev.toStringAsFixed(1)}\n${S.current.ev}'),
);
}
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 expectRadioListTile<T>(String text, {bool isSelected = false}) {
expect(
find.descendant(of: find.byWidgetPredicate((widget) => widget is RadioListTile<T>), matching: find.text(text)),
findsOneWidget,
);
}

View file

@ -3,12 +3,25 @@ import 'dart:math';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
const _systemFeatureMethodChannel = MethodChannel('system_feature');
const _lightSensorMethodChannel = MethodChannel("light.eventChannel");
Future<void> sendMockLux([int lux = 100]) async {
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
"light.eventChannel",
const StandardMethodCodec().encodeSuccessEnvelope(lux),
(ByteData? data) {},
);
}
Future<void> sendMockIncidentEv(double ev) async {
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
"light.eventChannel",
const StandardMethodCodec().encodeSuccessEnvelope((2.5 * pow(2, ev)).toInt()),
(ByteData? data) {},
);
}
void setLightSensorAvilability({required bool hasSensor}) {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
_systemFeatureMethodChannel,
const MethodChannel('system_feature'),
(methodCall) async {
switch (methodCall.method) {
case "sensor":
@ -19,43 +32,3 @@ void setLightSensorAvilability({required bool hasSensor}) {
},
);
}
void resetLightSensorAvilability() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
_systemFeatureMethodChannel,
null,
);
}
Future<void> sendMockIncidentEv(double ev) => sendMockLux((2.5 * pow(2, ev)).toInt());
Future<void> sendMockLux([int lux = 100]) async {
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
_lightSensorMethodChannel.name,
const StandardMethodCodec().encodeSuccessEnvelope(lux),
(ByteData? data) {},
);
}
void setupLightSensorStreamHandler() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
_lightSensorMethodChannel,
(methodCall) async {
switch (methodCall.method) {
case "listen":
return;
case "cancel":
return;
default:
return null;
}
},
);
}
void resetLightSensorStreamHandler() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
_lightSensorMethodChannel,
null,
);
}

View file

@ -19,13 +19,8 @@ extension WidgetTesterCommonActions on WidgetTester {
Film selectedFilm = const Film.other(),
}) async {
await pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
status: productStatus,
),
],
MockIAPProductsProvider(
productStatus: productStatus,
child: ApplicationWrapper(
const Environment.dev(),
child: MockIAPProviders(
@ -60,6 +55,10 @@ extension WidgetTesterCommonActions on WidgetTester {
}
extension WidgetTesterListTileActions on WidgetTester {
Future<void> tapRadioListTile<T>(String text) async {
await tap(find.descendant(of: find.byType(RadioListTile<T>), matching: find.text(text)));
}
/// Useful for tapping a specific [ListTile] inside a specific screen or dialog
Future<void> tapDescendantTextOf<T>(String text) async {
await tap(find.descendant(of: find.byType(T), matching: find.text(text)));

View file

@ -7,9 +7,9 @@ import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
IAPProducts(
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
child: const ApplicationWrapper(
const MockIAPProductsProvider(
productStatus: IAPProductStatus.purchasable,
child: ApplicationWrapper(
Environment.dev(),
child: Application(),
),

View file

@ -1,37 +0,0 @@
# Screenshots
The easiest way to create several sets of identical screenshots for Android and iOS is to generate them instead of taking them manually. Generating screenshots will save time and effort while also providing a consistent output.
## Context
As a user I want to see the most relevant screenshots in the store, so that I can see the actual state of the app.
## Screenshot cases
- Metering screen
1. Reflected light metering mode*
2. Incident light metering mode* **
3. Opened ISO picker
- Settings screen
1. Just the screen
2. Opened metering screen layout features dialog
- Equipment profiles screen
1. Just the screen
2. Opened equipment profile ISO picker
> *also in dark mode
> **Android only
## Run the generator
```console
sh screenshots/generate_screenshots.sh
```
Screenshots will be stored in the _screenshots/_ folder.

View file

@ -7,7 +7,7 @@ Future<void> main() async {
await grantCameraPermission();
await integrationDriver(
onScreenshot: (name, bytes, [args]) async {
final File image = await File('screenshots/$name.png').create(recursive: true);
final File image = await File('screenshots/TEST_$name.png').create(recursive: true);
image.writeAsBytesSync(bytes);
return true;
},