-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* test granting and revoking pro features * extracted common widget tester actions * test disabling & enabling of the metering screen layout features * added integration tests to CI * added integration tests to PR check * allow matrix jobs to fail * use base64 -d * downgraded iphone version to the supported one * use proper android device name * typo in macos version * upgraded iphone version to the supported one * updated android compileSdkVersion * added google services json restoration * combined all tests in one file * removed ipa signing for ios test * debug prints :) * lints * refined tester extension and expectations * e2e test (wip) * added more expectations to e2e test * changed pickers order a bit in e2e test * added equipment profiles creation to e2e test * added film selection to e2e test * set android emulator API level to 32 * use flutter drive for integration tests * removed app pre-build * try running tests only for one platform * added no-dds to flutter drive * try running only on ios * bumped macos version * increased tests timeout * set IPHONEOS_DEPLOYMENT_TARGET = 12.0 * removed prints * Update Podfile * restore firebase_app_id_file.json * Delete run_integration_tests.sh * run e2e with all tests * reverted pr-check
- Loading branch information
Showing
16 changed files
with
853 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# This workflow uses actions that are not certified by GitHub. | ||
# They are provided by a third-party and are governed by | ||
# separate terms of service, privacy policy, and support | ||
# documentation. | ||
|
||
name: Run integration tests | ||
|
||
on: | ||
workflow_dispatch: | ||
workflow_call: | ||
|
||
jobs: | ||
run-integration-tests: | ||
name: Run integration tests | ||
timeout-minutes: 60 | ||
runs-on: macos-13 | ||
steps: | ||
- uses: actions/checkout@v3 | ||
with: | ||
submodules: recursive | ||
|
||
- name: Override iap package with stub | ||
id: override-iap | ||
run: bash ./.github/scripts/stub_iap.sh | ||
|
||
- name: Restore secrets | ||
run: | | ||
bash .github/scripts/restore_from_base64.sh "${{ secrets.CONSTANTS }}" "lib/constants.dart" | ||
bash .github/scripts/restore_from_base64.sh "${{ secrets.GOOGLE_SERVICES_JSON_IOS }}" "ios/Runner/GoogleService-Info.plist" | ||
bash .github/scripts/restore_from_base64.sh "${{ secrets.FIREBASE_APP_ID_FILE }}" "ios/firebase_app_id_file.json" | ||
- uses: subosito/flutter-action@v2 | ||
with: | ||
channel: "stable" | ||
flutter-version: "3.10.0" | ||
|
||
- name: Prepare app | ||
run: | | ||
flutter --version | ||
flutter pub get | ||
flutter pub run intl_utils:generate | ||
flutter analyze lib --fatal-infos | ||
- name: Launch iOS simulator | ||
uses: futureware-tech/simulator-action@v3 | ||
with: | ||
model: "iPhone 15 Pro" | ||
|
||
- name: Run tests | ||
run: | | ||
flutter drive \ | ||
--target=integration_test/run_all_tests.dart \ | ||
--driver=test_driver/integration_driver.dart \ | ||
--flavor=dev \ | ||
--no-dds \ | ||
--dart-define cameraStubImage=assets/camera_stub_image.jpg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# M3 Lightmeter integration tests | ||
|
||
### List of executed tests: | ||
|
||
- [Purchases test](integration_test/purchases_test.dart) | ||
- [Metering screen layout test](integration_test/metering_screen_layout_test.dart) | ||
- [e2e](integration_test/e2e_test.dart) | ||
|
||
### Run all tests | ||
|
||
```console | ||
flutter drive \ | ||
--target=integration_test/run_all_tests.dart \ | ||
--driver=test_driver/integration_driver.dart \ | ||
--flavor=dev \ | ||
--no-dds \ | ||
--dart-define cameraStubImage=assets/camera_stub_image.jpg | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,302 @@ | ||
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/metering_screen_layout_config.dart'; | ||
import 'package:lightmeter/data/shared_prefs_service.dart'; | ||
import 'package:lightmeter/generated/l10n.dart'; | ||
import 'package:lightmeter/res/dimens.dart'; | ||
import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; | ||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart'; | ||
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/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/settings/components/shared/dialog_filter/widget_dialog_filter.dart'; | ||
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.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'; | ||
import 'utils/expectations.dart'; | ||
|
||
@isTest | ||
void testE2E(String 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(), | ||
), | ||
}); | ||
}); | ||
|
||
testWidgets( | ||
description, | ||
(tester) async { | ||
await tester.pumpApplication(equipmentProfiles: [], films: []); | ||
|
||
/// Create Praktica + Zenitar profile from scratch | ||
await tester.openSettings(); | ||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles); | ||
await tester.tap(find.byIcon(Icons.add).first); | ||
await tester.pumpAndSettle(); | ||
await tester.setProfileName(mockEquipmentProfiles[0].name); | ||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name); | ||
await tester.setIsoValues(0, mockEquipmentProfiles[0].isoValues); | ||
await tester.setNdValues(0, mockEquipmentProfiles[0].ndValues); | ||
await tester.setApertureValues(0, mockEquipmentProfiles[0].apertureValues); | ||
await tester.setShutterSpeedValues(0, mockEquipmentProfiles[0].shutterSpeedValues); | ||
expect(find.text('f/1.7 - f/16'), findsOneWidget); | ||
expect(find.text('1/1000 - 16"'), findsOneWidget); | ||
|
||
/// Create Praktica + Jupiter profile from Zenitar profile | ||
await tester.tap(find.byIcon(Icons.copy).first); | ||
await tester.pumpAndSettle(); | ||
await tester.setProfileName(mockEquipmentProfiles[1].name); | ||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name); | ||
await tester.setApertureValues(1, mockEquipmentProfiles[1].apertureValues); | ||
expect(find.text('f/3.5 - f/22'), findsOneWidget); | ||
expect(find.text('1/1000 - 16"'), findsNWidgets(2)); | ||
await tester.navigatorPop(); | ||
|
||
/// Select some films | ||
await tester.tap(find.text(S.current.filmsInUse)); | ||
await tester.pumpAndSettle(); | ||
await tester.setDialogFilterValues<Film>([mockFilms[0], mockFilms[1]], deselectAll: false); | ||
await tester.navigatorPop(); | ||
|
||
/// Select some initial settings according to the selected gear and film | ||
/// Then take a photo and verify, that exposure pairs range and EV matches the selected settings | ||
await tester.openPickerAndSelect<EquipmentProfilePicker, EquipmentProfile>(mockEquipmentProfiles[0].name); | ||
await tester.openPickerAndSelect<FilmPicker, Film>(mockFilms[0].name); | ||
await tester.openPickerAndSelect<IsoValuePicker, IsoValue>('400'); | ||
expectPickerTitle<EquipmentProfilePicker>(mockEquipmentProfiles[0].name); | ||
expectPickerTitle<FilmPicker>(mockFilms[0].name); | ||
expectPickerTitle<IsoValuePicker>('400'); | ||
await tester.takePhoto(); | ||
await _expectMeteringState( | ||
tester, | ||
equipmentProfile: mockEquipmentProfiles[0], | ||
film: mockFilms[0], | ||
fastest: 'f/1.8 - 1/400', | ||
slowest: 'f/16 - 1/5', | ||
iso: '400', | ||
nd: 'None', | ||
ev: mockPhotoEv100 + 2, | ||
); | ||
|
||
/// Add ND to shoot another scene | ||
await tester.openPickerAndSelect<NdValuePicker, NdValue>('2'); | ||
await _expectMeteringStateAndMeasure( | ||
tester, | ||
equipmentProfile: mockEquipmentProfiles[0], | ||
film: mockFilms[0], | ||
fastest: 'f/1.8 - 1/200', | ||
slowest: 'f/16 - 1/2.5', | ||
iso: '400', | ||
nd: '2', | ||
ev: mockPhotoEv100 + 2 - 1, | ||
); | ||
|
||
/// Select another lens without ND | ||
await tester.openPickerAndSelect<EquipmentProfilePicker, EquipmentProfile>(mockEquipmentProfiles[1].name); | ||
await tester.openPickerAndSelect<NdValuePicker, NdValue>('None'); | ||
await _expectMeteringStateAndMeasure( | ||
tester, | ||
equipmentProfile: mockEquipmentProfiles[1], | ||
film: mockFilms[0], | ||
fastest: 'f/3.5 - 1/100', | ||
slowest: 'f/22 - 1/2.5', | ||
iso: '400', | ||
nd: 'None', | ||
ev: mockPhotoEv100 + 2, | ||
); | ||
|
||
/// Set another film and another ISO | ||
await tester.openPickerAndSelect<IsoValuePicker, IsoValue>('200'); | ||
await tester.openPickerAndSelect<FilmPicker, Film>(mockFilms[1].name); | ||
await _expectMeteringStateAndMeasure( | ||
tester, | ||
equipmentProfile: mockEquipmentProfiles[1], | ||
film: mockFilms[1], | ||
fastest: 'f/3.5 - 1/50', | ||
slowest: 'f/22 - 1/1.3', | ||
iso: '200', | ||
nd: 'None', | ||
ev: mockPhotoEv100 + 1, | ||
); | ||
}, | ||
); | ||
} | ||
|
||
extension EquipmentProfileActions on WidgetTester { | ||
Future<void> expandEquipmentProfileContainer(String name) async { | ||
await tap(find.text(name)); | ||
await pump(Dimens.durationM); | ||
} | ||
|
||
Future<void> setProfileName(String name) async { | ||
await enterText(find.byType(TextField), name); | ||
await pump(); | ||
await tapSaveButton(); | ||
} | ||
|
||
Future<void> setIsoValues(int profileIndex, List<IsoValue> values) => | ||
_openAndSetDialogFilterValues<IsoValue>(profileIndex, S.current.isoValues, values); | ||
Future<void> setNdValues(int profileIndex, List<NdValue> values) => | ||
_openAndSetDialogFilterValues<NdValue>(profileIndex, S.current.ndFilters, values); | ||
Future<void> _openAndSetDialogFilterValues<T extends PhotographyValue>( | ||
int profileIndex, | ||
String listTileTitle, | ||
List<T> valuesToSelect, { | ||
bool deselectAll = true, | ||
}) async { | ||
await tap(find.text(listTileTitle).at(profileIndex)); | ||
await pumpAndSettle(); | ||
await setDialogFilterValues(valuesToSelect, deselectAll: deselectAll); | ||
} | ||
|
||
Future<void> setApertureValues(int profileIndex, List<ApertureValue> values) => | ||
_setDialogRangePickerValues<ApertureValue>(profileIndex, S.current.apertureValues, values); | ||
|
||
Future<void> setShutterSpeedValues(int profileIndex, List<ShutterSpeedValue> values) => | ||
_setDialogRangePickerValues<ShutterSpeedValue>(profileIndex, S.current.shutterSpeedValues, values); | ||
} | ||
|
||
extension on WidgetTester { | ||
Future<void> openPickerAndSelect<P extends Widget, V>(String valueToSelect) async { | ||
await openAnimatedPicker<P>(); | ||
await tapDescendantTextOf<DialogPicker<V>>(valueToSelect); | ||
await tapSelectButton(); | ||
} | ||
|
||
Future<void> setDialogFilterValues<T>( | ||
List<T> valuesToSelect, { | ||
bool deselectAll = true, | ||
}) async { | ||
if (deselectAll) { | ||
await tap(find.byIcon(Icons.deselect)); | ||
await pump(); | ||
} | ||
for (final value in valuesToSelect) { | ||
final listTile = find.descendant(of: find.byType(CheckboxListTile), matching: find.text(value.toString())); | ||
await scrollUntilVisible( | ||
listTile, | ||
56, | ||
scrollable: find.descendant(of: find.byType(DialogFilter<T>), matching: find.byType(Scrollable)), | ||
); | ||
await tap(listTile); | ||
await pump(); | ||
} | ||
await tapSaveButton(); | ||
} | ||
|
||
Future<void> _setDialogRangePickerValues<T extends PhotographyValue>( | ||
int profileIndex, | ||
String listTileTitle, | ||
List<T> valuesToSelect, | ||
) async { | ||
await tap(find.text(listTileTitle).at(profileIndex)); | ||
await pumpAndSettle(); | ||
|
||
final dialog = widget<DialogRangePicker<T>>(find.byType(DialogRangePicker<T>)); | ||
final sliderFinder = find.byType(RangeSlider); | ||
final divisions = widget<RangeSlider>(sliderFinder).divisions!; | ||
final trackWidth = getSize(sliderFinder).width - (2 * Dimens.paddingL); | ||
final trackStep = trackWidth / divisions; | ||
|
||
final start = valuesToSelect.first; | ||
final oldStart = dialog.values.indexWhere((e) => e.value == dialog.selectedValues.first.value) * trackStep; | ||
final newStart = dialog.values.indexWhere((e) => e.value == start.value) * trackStep; | ||
await dragFrom( | ||
getTopLeft(sliderFinder) + Offset(Dimens.paddingL + oldStart, getSize(sliderFinder).height / 2), | ||
Offset(newStart - oldStart, 0), | ||
); | ||
await pump(); | ||
|
||
final end = valuesToSelect.last; | ||
final oldEnd = dialog.values.indexWhere((e) => e.value == dialog.selectedValues.last.value) * trackStep; | ||
final newEnd = dialog.values.indexWhere((e) => e.value == end.value) * trackStep; | ||
await dragFrom( | ||
getTopLeft(sliderFinder) + Offset(Dimens.paddingL + oldEnd, getSize(sliderFinder).height / 2), | ||
Offset(newEnd - oldEnd, 0), | ||
); | ||
await pump(); | ||
|
||
await tapSaveButton(); | ||
} | ||
} | ||
|
||
Future<void> _expectMeteringState( | ||
WidgetTester tester, { | ||
required EquipmentProfile equipmentProfile, | ||
required Film film, | ||
required String fastest, | ||
required String slowest, | ||
required String iso, | ||
required String nd, | ||
required double ev, | ||
String? reason, | ||
}) async { | ||
expectPickerTitle<EquipmentProfilePicker>(equipmentProfile.name); | ||
expectPickerTitle<FilmPicker>(film.name); | ||
expectExtremeExposurePairs(fastest, slowest); | ||
expectPickerTitle<IsoValuePicker>(iso); | ||
expectPickerTitle<NdValuePicker>(nd); | ||
expectExposurePairsListItem(tester, fastest.split(' - ')[0], fastest.split(' - ')[1]); | ||
await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile); | ||
expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]); | ||
expectMeasureButton(ev); | ||
} | ||
|
||
Future<void> _expectMeteringStateAndMeasure( | ||
WidgetTester tester, { | ||
required EquipmentProfile equipmentProfile, | ||
required Film film, | ||
required String fastest, | ||
required String slowest, | ||
required String iso, | ||
required String nd, | ||
required double ev, | ||
}) async { | ||
await _expectMeteringState( | ||
tester, | ||
equipmentProfile: equipmentProfile, | ||
film: film, | ||
fastest: fastest, | ||
slowest: slowest, | ||
iso: iso, | ||
nd: nd, | ||
ev: ev, | ||
); | ||
await tester.takePhoto(); | ||
await _expectMeteringState( | ||
tester, | ||
equipmentProfile: equipmentProfile, | ||
film: film, | ||
fastest: fastest, | ||
slowest: slowest, | ||
iso: iso, | ||
nd: nd, | ||
ev: ev, | ||
reason: | ||
'Metering screen state must be the same before and after the measurement assuming that the scene is exactly the same.', | ||
); | ||
} | ||
|
||
void expectMeasureButton(double ev) { | ||
find.descendant( | ||
of: find.byType(MeteringMeasureButton), | ||
matching: find.text('${ev.toStringAsFixed(1)}\n${S.current.ev}'), | ||
); | ||
} |
Oops, something went wrong.