mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(mobile): Add integration tests (#1359)
This commit is contained in:
		
							
								
								
									
										44
									
								
								.github/workflows/test_mobile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								.github/workflows/test_mobile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | name: Flutter Integration Tests | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: [ "main" ] | ||||||
|  |   pull_request: | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   build: | ||||||
|  |     runs-on: macos-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-java@v2 | ||||||
|  |         with: | ||||||
|  |           distribution: 'adopt' | ||||||
|  |           java-version: '11' | ||||||
|  |       - name: Cache android SDK | ||||||
|  |         uses: actions/cache@v2 | ||||||
|  |         id: android-sdk | ||||||
|  |         with: | ||||||
|  |           key: android-sdk | ||||||
|  |           path: | | ||||||
|  |             /usr/local/lib/android/ | ||||||
|  |             ~/.android | ||||||
|  |       - name: Setup Android SDK | ||||||
|  |         if: steps.android-sdk.outputs.cache-hit != 'true' | ||||||
|  |         uses: android-actions/setup-android@v2 | ||||||
|  |       - name: Setup Flutter SDK | ||||||
|  |         uses: subosito/flutter-action@v1 | ||||||
|  |         with: | ||||||
|  |           channel: 'stable' | ||||||
|  |       - name: Run integration tests | ||||||
|  |         uses: reactivecircus/android-emulator-runner@v2.27.0 | ||||||
|  |         with: | ||||||
|  |           working-directory: ./mobile | ||||||
|  |           api-level: 29 | ||||||
|  |           arch: x86_64 | ||||||
|  |           profile: pixel | ||||||
|  |           target: default | ||||||
|  |           emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim | ||||||
|  |           disable-linux-hw-accel: false | ||||||
|  |           script: | | ||||||
|  |             flutter pub get | ||||||
|  |             flutter test integration_test | ||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -5,4 +5,6 @@ | |||||||
|  |  | ||||||
| docker/upload | docker/upload | ||||||
| uploads | uploads | ||||||
| coverage | coverage | ||||||
|  |  | ||||||
|  | mobile/gradle.properties | ||||||
|   | |||||||
| @@ -114,8 +114,9 @@ | |||||||
|   "library_page_new_album": "New album", |   "library_page_new_album": "New album", | ||||||
|   "login_form_button_text": "Login", |   "login_form_button_text": "Login", | ||||||
|   "login_form_email_hint": "youremail@email.com", |   "login_form_email_hint": "youremail@email.com", | ||||||
|   "login_form_endpoint_hint": "http://your-server-ip:port/", |   "login_form_endpoint_hint": "https://your-server-ip:port/", | ||||||
|   "login_form_endpoint_url": "Server Endpoint URL", |   "login_form_endpoint_url": "Server Endpoint URL", | ||||||
|  |   "login_form_err_http_insecure": "Http is unencrypted and therefore not recommended. Please use https unless you are using Immich exclusively in your home network.", | ||||||
|   "login_form_err_invalid_email": "Invalid Email", |   "login_form_err_invalid_email": "Invalid Email", | ||||||
|   "login_form_err_invalid_url": "Invalid URL", |   "login_form_err_invalid_url": "Invalid URL", | ||||||
|   "login_form_err_leading_whitespace": "Leading whitespace", |   "login_form_err_leading_whitespace": "Leading whitespace", | ||||||
|   | |||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter_test/flutter_test.dart'; | ||||||
|  |  | ||||||
|  | import '../test_utils/general_helper.dart'; | ||||||
|  | import '../test_utils/login_helper.dart'; | ||||||
|  |  | ||||||
|  | void main() async { | ||||||
|  |   await ImmichTestHelper.initialize(); | ||||||
|  |  | ||||||
|  |   group("Login input validation test", () { | ||||||
|  |     immichWidgetTest("Test http warning message", (tester) async { | ||||||
|  |       await ImmichTestLoginHelper.waitForLoginScreen(tester); | ||||||
|  |       await ImmichTestLoginHelper.acknowledgeNewServerVersion(tester); | ||||||
|  |  | ||||||
|  |       // Test https URL | ||||||
|  |       await ImmichTestLoginHelper.enterLoginCredentials( | ||||||
|  |         tester, | ||||||
|  |         server: "https://demo.immich.app/api", | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       await tester.pump(const Duration(milliseconds: 300)); | ||||||
|  |  | ||||||
|  |       expect(find.text("login_form_err_http_insecure".tr()), findsNothing); | ||||||
|  |  | ||||||
|  |       // Test http URL | ||||||
|  |       await ImmichTestLoginHelper.enterLoginCredentials( | ||||||
|  |         tester, | ||||||
|  |         server: "http://demo.immich.app/api", | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       await tester.pump(const Duration(milliseconds: 300)); | ||||||
|  |  | ||||||
|  |       expect(find.text("login_form_err_http_insecure".tr()), findsOneWidget); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								mobile/integration_test/test_utils/general_helper.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								mobile/integration_test/test_utils/general_helper.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  |  | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter_test/flutter_test.dart'; | ||||||
|  | import 'package:hive/hive.dart'; | ||||||
|  | import 'package:immich_mobile/main.dart'; | ||||||
|  | import 'package:integration_test/integration_test.dart'; | ||||||
|  |  | ||||||
|  | import 'package:immich_mobile/main.dart' as app; | ||||||
|  |  | ||||||
|  | class ImmichTestHelper { | ||||||
|  |  | ||||||
|  |   static Future<IntegrationTestWidgetsFlutterBinding> initialize() async { | ||||||
|  |     final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); | ||||||
|  |     binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; | ||||||
|  |  | ||||||
|  |     // Load hive, localization... | ||||||
|  |     await app.initApp(); | ||||||
|  |  | ||||||
|  |     return binding; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> loadApp(WidgetTester tester) async { | ||||||
|  |     // Clear all data from Hive | ||||||
|  |     await Hive.deleteFromDisk(); | ||||||
|  |     await app.openBoxes(); | ||||||
|  |     // Load main Widget | ||||||
|  |     await tester.pumpWidget(app.getMainWidget()); | ||||||
|  |     // Post run tasks | ||||||
|  |     await tester.pumpAndSettle(); | ||||||
|  |     await EasyLocalization.ensureInitialized(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void immichWidgetTest(String description, Future<void> Function(WidgetTester) test) { | ||||||
|  |   testWidgets(description, (widgetTester) async { | ||||||
|  |     await ImmichTestHelper.loadApp(widgetTester); | ||||||
|  |     await test(widgetTester); | ||||||
|  |   }); | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								mobile/integration_test/test_utils/login_helper.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								mobile/integration_test/test_utils/login_helper.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_test/flutter_test.dart'; | ||||||
|  |  | ||||||
|  | class ImmichTestLoginHelper { | ||||||
|  |   static Future<void> waitForLoginScreen(WidgetTester tester, | ||||||
|  |       {int timeoutSeconds = 20}) async { | ||||||
|  |     for (var i = 0; i < timeoutSeconds; i++) { | ||||||
|  |       // Search for "IMMICH" test in the app bar | ||||||
|  |       final result = find.text("IMMICH"); | ||||||
|  |       if (tester.any(result)) { | ||||||
|  |         // Wait 5s until everything settled | ||||||
|  |         await tester.pump(const Duration(seconds: 5)); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Wait 1s before trying again | ||||||
|  |       await Future.delayed(const Duration(seconds: 1)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fail("Timeout while waiting for login screen"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<bool> acknowledgeNewServerVersion(WidgetTester tester) async { | ||||||
|  |     final result = find.text("Acknowledge"); | ||||||
|  |     if (!tester.any(result)) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await tester.tap(result); | ||||||
|  |     await tester.pump(); | ||||||
|  |  | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static Future<void> enterLoginCredentials( | ||||||
|  |     WidgetTester tester, { | ||||||
|  |     String server = "", | ||||||
|  |     String email = "", | ||||||
|  |     String password = "", | ||||||
|  |   }) async { | ||||||
|  |     final loginForms = find.byType(TextFormField); | ||||||
|  |  | ||||||
|  |     await tester.pump(const Duration(milliseconds: 500)); | ||||||
|  |     await tester.enterText(loginForms.at(0), email); | ||||||
|  |  | ||||||
|  |     await tester.pump(const Duration(milliseconds: 500)); | ||||||
|  |     await tester.enterText(loginForms.at(1), password); | ||||||
|  |  | ||||||
|  |     await tester.pump(const Duration(milliseconds: 500)); | ||||||
|  |     await tester.enterText(loginForms.at(2), server); | ||||||
|  |  | ||||||
|  |     await tester.pump(const Duration(milliseconds: 500)); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -29,12 +29,11 @@ import 'package:immich_mobile/utils/immich_app_theme.dart'; | |||||||
| import 'constants/hive_box.dart'; | import 'constants/hive_box.dart'; | ||||||
|  |  | ||||||
| void main() async { | void main() async { | ||||||
|   await Hive.initFlutter(); |   await initApp(); | ||||||
|   Hive.registerAdapter(HiveSavedLoginInfoAdapter()); |   runApp(getMainWidget()); | ||||||
|   Hive.registerAdapter(HiveBackupAlbumsAdapter()); | } | ||||||
|   Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); |  | ||||||
|   Hive.registerAdapter(ImmichLoggerMessageAdapter()); |  | ||||||
|  |  | ||||||
|  | Future<void> openBoxes() async { | ||||||
|   await Future.wait([ |   await Future.wait([ | ||||||
|     Hive.openBox<ImmichLoggerMessage>(immichLoggerBox), |     Hive.openBox<ImmichLoggerMessage>(immichLoggerBox), | ||||||
|     Hive.openBox(userInfoBox), |     Hive.openBox(userInfoBox), | ||||||
| @@ -47,6 +46,16 @@ void main() async { | |||||||
|     if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox), |     if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox), | ||||||
|     EasyLocalization.ensureInitialized(), |     EasyLocalization.ensureInitialized(), | ||||||
|   ]); |   ]); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Future<void> initApp() async { | ||||||
|  |   await Hive.initFlutter(); | ||||||
|  |   Hive.registerAdapter(HiveSavedLoginInfoAdapter()); | ||||||
|  |   Hive.registerAdapter(HiveBackupAlbumsAdapter()); | ||||||
|  |   Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); | ||||||
|  |   Hive.registerAdapter(ImmichLoggerMessageAdapter()); | ||||||
|  |  | ||||||
|  |   await openBoxes(); | ||||||
|  |  | ||||||
|   SystemChrome.setSystemUIOverlayStyle( |   SystemChrome.setSystemUIOverlayStyle( | ||||||
|     const SystemUiOverlayStyle( |     const SystemUiOverlayStyle( | ||||||
| @@ -65,15 +74,15 @@ void main() async { | |||||||
|  |  | ||||||
|   // Initialize Immich Logger Service |   // Initialize Immich Logger Service | ||||||
|   ImmichLogger().init(); |   ImmichLogger().init(); | ||||||
|  | } | ||||||
|  |  | ||||||
|   runApp( | Widget getMainWidget() { | ||||||
|     EasyLocalization( |   return EasyLocalization( | ||||||
|       supportedLocales: locales, |     supportedLocales: locales, | ||||||
|       path: translationsPath, |     path: translationsPath, | ||||||
|       useFallbackTranslations: true, |     useFallbackTranslations: true, | ||||||
|       fallbackLocale: locales.first, |     fallbackLocale: locales.first, | ||||||
|       child: const ProviderScope(child: ImmichApp()), |     child: const ProviderScope(child: ImmichApp()), | ||||||
|     ), |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -223,6 +223,11 @@ class ServerEndpointInput extends StatelessWidget { | |||||||
|         parsedUrl.host.isEmpty) { |         parsedUrl.host.isEmpty) { | ||||||
|       return 'login_form_err_invalid_url'.tr(); |       return 'login_form_err_invalid_url'.tr(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (!parsedUrl.scheme.startsWith("https")) { | ||||||
|  |       return 'login_form_err_http_insecure'.tr(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -234,6 +239,7 @@ class ServerEndpointInput extends StatelessWidget { | |||||||
|         labelText: 'login_form_endpoint_url'.tr(), |         labelText: 'login_form_endpoint_url'.tr(), | ||||||
|         border: const OutlineInputBorder(), |         border: const OutlineInputBorder(), | ||||||
|         hintText: 'login_form_endpoint_hint'.tr(), |         hintText: 'login_form_endpoint_hint'.tr(), | ||||||
|  |         errorMaxLines: 4 | ||||||
|       ), |       ), | ||||||
|       validator: _validateInput, |       validator: _validateInput, | ||||||
|       autovalidateMode: AutovalidateMode.always, |       autovalidateMode: AutovalidateMode.always, | ||||||
|   | |||||||
| @@ -307,6 +307,11 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.4.0" |     version: "0.4.0" | ||||||
|  |   flutter_driver: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: flutter | ||||||
|  |     source: sdk | ||||||
|  |     version: "0.0.0" | ||||||
|   flutter_hooks: |   flutter_hooks: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -392,6 +397,11 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.2" |     version: "2.1.2" | ||||||
|  |   fuchsia_remote_debug_protocol: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: flutter | ||||||
|  |     source: sdk | ||||||
|  |     version: "0.0.0" | ||||||
|   glob: |   glob: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -504,6 +514,11 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.5.0" |     version: "2.5.0" | ||||||
|  |   integration_test: | ||||||
|  |     dependency: "direct dev" | ||||||
|  |     description: flutter | ||||||
|  |     source: sdk | ||||||
|  |     version: "0.0.0" | ||||||
|   intl: |   intl: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1041,6 +1056,13 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.1" |     version: "1.1.1" | ||||||
|  |   sync_http: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: sync_http | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.3.1" | ||||||
|   synchronized: |   synchronized: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1202,6 +1224,13 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.10" |     version: "2.0.10" | ||||||
|  |   vm_service: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: vm_service | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "9.0.0" | ||||||
|   wakelock: |   wakelock: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1251,6 +1280,13 @@ packages: | |||||||
|       url: "https://pub.dartlang.org" |       url: "https://pub.dartlang.org" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.2.0" |     version: "2.2.0" | ||||||
|  |   webdriver: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: webdriver | ||||||
|  |       url: "https://pub.dartlang.org" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.0.0" | ||||||
|   win32: |   win32: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|   | |||||||
| @@ -57,6 +57,8 @@ dev_dependencies: | |||||||
|   build_runner: ^2.2.1 |   build_runner: ^2.2.1 | ||||||
|   auto_route_generator: ^5.0.2 |   auto_route_generator: ^5.0.2 | ||||||
|   flutter_launcher_icons: "^0.9.2" |   flutter_launcher_icons: "^0.9.2" | ||||||
|  |   integration_test: | ||||||
|  |     sdk: flutter | ||||||
|  |  | ||||||
| flutter: | flutter: | ||||||
|   uses-material-design: true |   uses-material-design: true | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user