1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat(mobile): Add integration tests (#1359)

This commit is contained in:
Matthias Rupp 2023-01-22 04:43:28 +01:00 committed by GitHub
parent e5d798581c
commit f4c90426a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 246 additions and 15 deletions

44
.github/workflows/test_mobile.yml vendored Normal file
View 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

2
.gitignore vendored
View file

@ -6,3 +6,5 @@
docker/upload docker/upload
uploads uploads
coverage coverage
mobile/gradle.properties

View file

@ -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",

View file

@ -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);
});
});
}

View 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);
});
}

View 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));
}
}

View file

@ -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()),
),
); );
} }

View file

@ -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,

View file

@ -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:

View file

@ -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