Compare commits

..

No commits in common. "a70ce5012a22c507e0e9cae0545d98f9c52d1787" and "df32f6a30e9f04eb37ae5a9d4e8ff7ca9d284279" have entirely different histories.

8 changed files with 245 additions and 214 deletions

View file

@ -65,5 +65,64 @@ jobs:
flutter analyze lib --fatal-infos flutter analyze lib --fatal-infos
run-integration-tests: run-integration-tests:
uses: ./.github/workflows/run_integration_tests.yml name: Run integration tests
secrets: inherit timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-13]
runs-on: ${{ matrix.os }}
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_ANDROID }}" "android/app/google-services.json"
bash .github/scripts/restore_from_base64.sh "${{ secrets.GOOGLE_SERVICES_JSON_IOS }}" "ios/Runner/GoogleService-Info.plist"
- uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.10.0"
- name: Prepare flutter project
run: |
flutter --version
flutter pub get
flutter pub run intl_utils:generate
- name: Analyze project source
run: flutter analyze lib --fatal-infos
- name: Enable KVM
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Launch Android simulator & Run tests
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
target: default
arch: x86_64
profile: pixel_6
script: flutter test integration_test --flavor dev --dart-define cameraStubImage=assets/camera_stub_image.jpg
- name: Launch iOS simulator
uses: futureware-tech/simulator-action@v3
if: ${{ matrix.os == 'macos-13' }}
with:
model: "iPhone 15 Pro"
- name: Run tests
if: ${{ matrix.os == 'macos-13' }}
run: flutter test integration_test --flavor dev --dart-define cameraStubImage=assets/camera_stub_image.jpg

View file

@ -7,20 +7,14 @@ name: Run integration tests
on: on:
workflow_dispatch: workflow_dispatch:
workflow_call:
env:
BUILD_ARGS: --flavor dev --dart-define cameraStubImage=assets/camera_stub_image.jpg
TARGET: integration_test/run_all_tests.dart
jobs: jobs:
run-integration-tests: analyze_and_test:
name: Run integration tests name: Run integration tests
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
fail-fast: false
matrix: matrix:
os: [ubuntu-latest, macos-13] os: [ubuntu-latest, macos-11]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -31,30 +25,22 @@ jobs:
id: override-iap id: override-iap
run: bash ./.github/scripts/stub_iap.sh run: bash ./.github/scripts/stub_iap.sh
- name: Restore secrets - name: Restore constants.dart
run: | run: bash .github/scripts/restore_from_base64.sh "${{ secrets.CONSTANTS }}" "lib/constants.dart"
bash .github/scripts/restore_from_base64.sh "${{ secrets.CONSTANTS }}" "lib/constants.dart"
bash .github/scripts/restore_from_base64.sh "${{ secrets.GOOGLE_SERVICES_JSON_ANDROID }}" "android/app/google-services.json"
bash .github/scripts/restore_from_base64.sh "${{ secrets.GOOGLE_SERVICES_JSON_IOS }}" "ios/Runner/GoogleService-Info.plist"
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"
flutter-version: "3.10.0" flutter-version: "3.10.0"
- name: Build app - name: Prepare flutter project
run: | run: |
flutter --version flutter --version
flutter pub get flutter pub get
flutter pub run intl_utils:generate flutter pub run intl_utils:generate
flutter analyze lib --fatal-infos
if [ "${{ matrix.os }}" == "macos-13" ] - name: Analyze project source
then run: flutter analyze lib --fatal-infos
cd ios
pod install
cd ..
fi
flutter build ${{ matrix.os == 'ubuntu-latest' && 'apk --debug' || 'ios --no-codesign --simulator --debug' }} $BUILD_ARGS -t $TARGET
- name: Enable KVM - name: Enable KVM
if: ${{ matrix.os == 'ubuntu-latest' }} if: ${{ matrix.os == 'ubuntu-latest' }}
@ -70,14 +56,14 @@ jobs:
api-level: 33 api-level: 33
target: default target: default
arch: x86_64 arch: x86_64
profile: pixel_6 profile: Pixel 6
script: flutter test $TARGET $BUILD_ARGS script: flutter test integration_test --flavor dev --dart-define cameraStubImage=assets/camera_stub_image.jpg
- name: Launch iOS simulator - name: Launch iOS simulator
uses: futureware-tech/simulator-action@v3 uses: futureware-tech/simulator-action@v3
if: ${{ matrix.os == 'macos-13' }} if: ${{ matrix.os == 'macos-11' }}
with: with:
model: "iPhone 15 Pro" model: "iPhone 15"
- name: Run tests - name: Run tests
if: ${{ matrix.os == 'macos-13' }} if: ${{ matrix.os == 'macos-11' }}
run: flutter test $TARGET $BUILD_ARGS run: flutter test integration_test --flavor dev --dart-define cameraStubImage=assets/camera_stub_image.jpg

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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/ev_source_type.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
@ -14,7 +15,6 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container
import 'package:lightmeter/screens/metering/screen_metering.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart';
import 'package:lightmeter/screens/settings/screen_settings.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../integration_test/utils/widget_tester_actions.dart'; import '../integration_test/utils/widget_tester_actions.dart';
@ -22,164 +22,159 @@ import 'mocks/paid_features_mock.dart';
const _mockPhotoEv100 = 8.3; const _mockPhotoEv100 = 8.3;
@isTestGroup void main() {
void testToggleLayoutFeatures(String description) { IntegrationTestWidgetsFlutterBinding.ensureInitialized();
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(),
),
});
});
testWidgets( void mockSharedPrefs() {
'Equipment profile picker', SharedPreferences.setMockInitialValues({
(tester) async { /// Metering values
await tester.pumpApplication(selectedEquipmentProfileId: mockEquipmentProfiles.first.id); UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
await tester.takePhoto(); UserPreferencesService.meteringScreenLayoutKey: json.encode(
_expectPickerTitle<EquipmentProfilePicker>(mockEquipmentProfiles.first.name); {
_expectExtremeExposurePairs('f/1.8 - 1/100', 'f/16 - 1/1.3'); MeteringScreenLayoutFeature.equipmentProfiles: true,
_expectExposurePairsListItem(tester, 'f/1.8', '1/100'); MeteringScreenLayoutFeature.extremeExposurePairs: true,
await tester.scrollToTheLastExposurePair(mockEquipmentProfiles.first); MeteringScreenLayoutFeature.filmPicker: true,
_expectExposurePairsListItem(tester, 'f/16', '1/1.3'); }.toJson(),
),
});
}
// Disable layout feature setUp(() {
await tester.toggleLayoutFeature(S.current.meteringScreenLayoutHintEquipmentProfiles); mockSharedPrefs();
expect( });
find.byType(EquipmentProfilePicker),
findsNothing,
reason:
'Equipment profile picker must be hidden from the metering screen when the corresponding layout feature is disabled.',
);
_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(
tester,
'f/1.0',
'1/320',
reason:
'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset.',
);
await tester.scrollToTheLastExposurePair();
_expectExposurePairsListItem(
tester,
'f/45',
'6"',
reason:
'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset.',
);
// Enable layout feature testWidgets(
await tester.toggleLayoutFeature(S.current.meteringScreenLayoutHintEquipmentProfiles); 'Hide equipment profile picker',
_expectPickerTitle<EquipmentProfilePicker>( (tester) async {
S.current.none, await tester.pumpApplication(selectedEquipmentProfileId: mockEquipmentProfiles.first.id);
reason: 'Equipment profile must remain unselected when the corresponding layout feature is re-enabled.', await tester.takePhoto();
); _expectPickerTitle<EquipmentProfilePicker>(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');
// Disable layout feature
await tester.toggleLayoutFeature(S.current.meteringScreenLayoutHintEquipmentProfiles);
expect(
find.byType(EquipmentProfilePicker),
findsNothing,
reason:
'Equipment profile picker must be hidden from the metering screen when the corresponding layout feature is disabled.',
);
_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(
tester,
'f/1.0',
'1/320',
reason: 'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset.',
);
await tester.scrollToTheLastExposurePair();
_expectExposurePairsListItem(
tester,
'f/45',
'6"',
reason: 'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset.',
); );
testWidgets( // Enable layout feature
'Extreme exposure pairs container', await tester.toggleLayoutFeature(S.current.meteringScreenLayoutHintEquipmentProfiles);
(tester) async { _expectPickerTitle<EquipmentProfilePicker>(
await tester.pumpApplication(); S.current.none,
await tester.takePhoto(); reason: 'Equipment profile must remain unselected when the corresponding layout feature is re-enabled.',
_expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 6"'); );
_expectExposurePairsListItem(tester, 'f/1.0', '1/320'); },
await tester.scrollToTheLastExposurePair(); );
_expectExposurePairsListItem(tester, 'f/45', '6"');
// Disable layout feature testWidgets(
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs); 'Hide extreme exposure pairs container',
expect( (tester) async {
find.byType(ExtremeExposurePairsContainer), await tester.pumpApplication();
findsNothing, await tester.takePhoto();
reason: _expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 6"');
'Extreme exposure pairs container must be hidden from the metering screen when the corresponding layout feature is disabled.', _expectExposurePairsListItem(tester, 'f/1.0', '1/320');
); await tester.scrollToTheLastExposurePair();
_expectExposurePairsListItem( _expectExposurePairsListItem(tester, 'f/45', '6"');
tester,
'f/1.0',
'1/320',
reason:
'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.',
);
await tester.scrollToTheLastExposurePair();
_expectExposurePairsListItem(
tester,
'f/45',
'6"',
reason:
'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.',
);
// Enable layout feature // Disable layout feature
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs); await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs);
_expectExtremeExposurePairs( expect(
'f/1.0 - 1/320', find.byType(ExtremeExposurePairsContainer),
'f/45 - 6"', findsNothing,
reason: reason:
'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.', 'Extreme exposure pairs container must be hidden from the metering screen when the corresponding layout feature is disabled.',
); );
}, _expectExposurePairsListItem(
tester,
'f/1.0',
'1/320',
reason: 'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.',
);
await tester.scrollToTheLastExposurePair();
_expectExposurePairsListItem(
tester,
'f/45',
'6"',
reason: 'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.',
); );
testWidgets( // Enable layout feature
'Film picker', await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs);
(tester) async { _expectExtremeExposurePairs(
await tester.pumpApplication(selectedFilm: mockFilms.first); 'f/1.0 - 1/320',
await tester.takePhoto(); 'f/45 - 6"',
_expectPickerTitle<FilmPicker>(mockFilms.first.name); reason: 'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.',
_expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 12"'); );
_expectExposurePairsListItem(tester, 'f/1.0', '1/320'); },
await tester.scrollToTheLastExposurePair(); );
_expectExposurePairsListItem(tester, 'f/45', '12"');
// Disable layout feature testWidgets(
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker); 'Hide film picker',
expect( (tester) async {
find.byType(FilmPicker), await tester.pumpApplication(selectedFilm: mockFilms.first);
findsNothing, await tester.takePhoto();
reason: _expectPickerTitle<FilmPicker>(mockFilms.first.name);
'Film picker must be hidden from the metering screen when the corresponding layout feature is disabled.', _expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 12"');
); _expectExposurePairsListItem(tester, 'f/1.0', '1/320');
_expectExtremeExposurePairs( await tester.scrollToTheLastExposurePair();
'f/1.0 - 1/320', _expectExposurePairsListItem(tester, 'f/45', '12"');
'f/45 - 6"',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
);
_expectExposurePairsListItem(
tester,
'f/1.0',
'1/320',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
);
await tester.scrollToTheLastExposurePair();
_expectExposurePairsListItem(
tester,
'f/45',
'6"',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
);
// Enable layout feature // Disable layout feature
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker); await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker);
_expectPickerTitle<FilmPicker>( expect(
S.current.none, find.byType(FilmPicker),
reason: 'Film must remain unselected when the corresponding layout feature is re-enabled.', findsNothing,
); reason:
}, 'Film picker must be hidden from the metering screen when the corresponding layout feature is disabled.',
);
_expectExtremeExposurePairs(
'f/1.0 - 1/320',
'f/45 - 6"',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
);
_expectExposurePairsListItem(
tester,
'f/1.0',
'1/320',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
);
await tester.scrollToTheLastExposurePair();
_expectExposurePairsListItem(
tester,
'f/45',
'6"',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
);
// Enable layout feature
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker);
_expectPickerTitle<FilmPicker>(
S.current.none,
reason: 'Film must remain unselected when the corresponding layout feature is re-enabled.',
); );
}, },
); );

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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/ev_source_type.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
@ -15,30 +16,37 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
import 'package:lightmeter/screens/settings/screen_settings.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../integration_test/utils/widget_tester_actions.dart'; import '../integration_test/utils/widget_tester_actions.dart';
import 'mocks/iap_products_mock.dart'; import 'mocks/iap_products_mock.dart';
@isTest void main() {
void testPurchases(String description) { IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets(
description,
(tester) async {
SharedPreferences.setMockInitialValues({
/// Metering values
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
UserPreferencesService.showEv100Key: true,
UserPreferencesService.meteringScreenLayoutKey: json.encode(
{
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
}.toJson(),
),
});
void mockSharedPrefs() {
// ignore: invalid_use_of_visible_for_testing_member
SharedPreferences.setMockInitialValues({
/// Metering values
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
UserPreferencesService.showEv100Key: true,
UserPreferencesService.meteringScreenLayoutKey: json.encode(
{
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
}.toJson(),
),
});
}
setUpAll(() {
mockSharedPrefs();
});
testWidgets(
'Purchase & refund premium features',
(tester) async {
await tester.pumpApplication(productStatus: IAPProductStatus.purchasable); await tester.pumpApplication(productStatus: IAPProductStatus.purchasable);
await tester.takePhoto(); await tester.takePhoto();

View file

@ -1,11 +0,0 @@
import 'package:integration_test/integration_test.dart';
import 'metering_screen_layout_test.dart';
import 'purchases_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testPurchases('Purchase & refund premium features');
testToggleLayoutFeatures('Toggle metering screen layout features');
}

View file

@ -50,8 +50,6 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
communicationBloc, communicationBloc,
const CameraInitState(), const CameraInitState(),
) { ) {
// ignore: avoid_print
print('CameraContainerBloc');
_observer = _WidgetsBindingObserver(_appLifecycleStateObserver); _observer = _WidgetsBindingObserver(_appLifecycleStateObserver);
WidgetsBinding.instance.addObserver(_observer); WidgetsBinding.instance.addObserver(_observer);

View file

@ -5,10 +5,7 @@ class MockCameraContainerBloc extends CameraContainerBloc {
super._meteringInteractor, super._meteringInteractor,
super.communicationBloc, super.communicationBloc,
super._analytics, super._analytics,
) { );
// ignore: avoid_print
print('MockCameraContainerBloc');
}
@override @override
Future<void> _onRequestPermission(_, Emitter emit) async { Future<void> _onRequestPermission(_, Emitter emit) async {

View file

@ -52,7 +52,6 @@ dev_dependencies:
integration_test: integration_test:
sdk: flutter sdk: flutter
lint: 2.1.2 lint: 2.1.2
meta: 1.9.1
mocktail: 0.3.0 mocktail: 0.3.0
test: 1.24.1 test: 1.24.1