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 | ||||
| uploads | ||||
| coverage | ||||
| coverage | ||||
|  | ||||
| mobile/gradle.properties | ||||
|   | ||||
| @@ -114,8 +114,9 @@ | ||||
|   "library_page_new_album": "New album", | ||||
|   "login_form_button_text": "Login", | ||||
|   "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_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_url": "Invalid URL", | ||||
|   "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'; | ||||
|  | ||||
| void main() async { | ||||
|   await Hive.initFlutter(); | ||||
|   Hive.registerAdapter(HiveSavedLoginInfoAdapter()); | ||||
|   Hive.registerAdapter(HiveBackupAlbumsAdapter()); | ||||
|   Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); | ||||
|   Hive.registerAdapter(ImmichLoggerMessageAdapter()); | ||||
|   await initApp(); | ||||
|   runApp(getMainWidget()); | ||||
| } | ||||
|  | ||||
| Future<void> openBoxes() async { | ||||
|   await Future.wait([ | ||||
|     Hive.openBox<ImmichLoggerMessage>(immichLoggerBox), | ||||
|     Hive.openBox(userInfoBox), | ||||
| @@ -47,6 +46,16 @@ void main() async { | ||||
|     if (!Platform.isAndroid) Hive.openBox(backgroundBackupInfoBox), | ||||
|     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( | ||||
|     const SystemUiOverlayStyle( | ||||
| @@ -65,15 +74,15 @@ void main() async { | ||||
|  | ||||
|   // Initialize Immich Logger Service | ||||
|   ImmichLogger().init(); | ||||
| } | ||||
|  | ||||
|   runApp( | ||||
|     EasyLocalization( | ||||
|       supportedLocales: locales, | ||||
|       path: translationsPath, | ||||
|       useFallbackTranslations: true, | ||||
|       fallbackLocale: locales.first, | ||||
|       child: const ProviderScope(child: ImmichApp()), | ||||
|     ), | ||||
| Widget getMainWidget() { | ||||
|   return EasyLocalization( | ||||
|     supportedLocales: locales, | ||||
|     path: translationsPath, | ||||
|     useFallbackTranslations: true, | ||||
|     fallbackLocale: locales.first, | ||||
|     child: const ProviderScope(child: ImmichApp()), | ||||
|   ); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -223,6 +223,11 @@ class ServerEndpointInput extends StatelessWidget { | ||||
|         parsedUrl.host.isEmpty) { | ||||
|       return 'login_form_err_invalid_url'.tr(); | ||||
|     } | ||||
|  | ||||
|     if (!parsedUrl.scheme.startsWith("https")) { | ||||
|       return 'login_form_err_http_insecure'.tr(); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| @@ -234,6 +239,7 @@ class ServerEndpointInput extends StatelessWidget { | ||||
|         labelText: 'login_form_endpoint_url'.tr(), | ||||
|         border: const OutlineInputBorder(), | ||||
|         hintText: 'login_form_endpoint_hint'.tr(), | ||||
|         errorMaxLines: 4 | ||||
|       ), | ||||
|       validator: _validateInput, | ||||
|       autovalidateMode: AutovalidateMode.always, | ||||
|   | ||||
| @@ -307,6 +307,11 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.4.0" | ||||
|   flutter_driver: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.0" | ||||
|   flutter_hooks: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -392,6 +397,11 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.1.2" | ||||
|   fuchsia_remote_debug_protocol: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.0" | ||||
|   glob: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -504,6 +514,11 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.5.0" | ||||
|   integration_test: | ||||
|     dependency: "direct dev" | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.0" | ||||
|   intl: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1041,6 +1056,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "1.1.1" | ||||
|   sync_http: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sync_http | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "0.3.1" | ||||
|   synchronized: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1202,6 +1224,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.0.10" | ||||
|   vm_service: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: vm_service | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "9.0.0" | ||||
|   wakelock: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1251,6 +1280,13 @@ packages: | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|   webdriver: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webdriver | ||||
|       url: "https://pub.dartlang.org" | ||||
|     source: hosted | ||||
|     version: "3.0.0" | ||||
|   win32: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -57,6 +57,8 @@ dev_dependencies: | ||||
|   build_runner: ^2.2.1 | ||||
|   auto_route_generator: ^5.0.2 | ||||
|   flutter_launcher_icons: "^0.9.2" | ||||
|   integration_test: | ||||
|     sdk: flutter | ||||
|  | ||||
| flutter: | ||||
|   uses-material-design: true | ||||
|   | ||||
		Reference in New Issue
	
	Block a user