From 78a5fe2d37771b3febc9a2e1380aa5f1c49f9ebe Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Sun, 19 Feb 2023 17:50:36 +0000 Subject: [PATCH] test(app): fix integration test and improve reliability and speed (#1792) --- .github/workflows/test.yml | 62 +++++++++++++++---- .../test_utils/general_helper.dart | 17 ++++- .../test_utils/login_helper.dart | 52 ++++------------ 3 files changed, 77 insertions(+), 54 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 995e7b450c..0fb588fc11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,8 +86,9 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: - distribution: 'adopt' - java-version: '11' + distribution: 'zulu' + java-version: '12.x' + cache: 'gradle' - name: Cache android SDK uses: actions/cache@v3 id: android-sdk @@ -96,24 +97,59 @@ jobs: path: | /usr/local/lib/android/ ~/.android + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ./mobile/build/ + ./mobile/android/.gradle/ + key: ${{ runner.os }}-flutter-${{ hashFiles('**/*.gradle*', 'pubspec.lock') }} - name: Setup Android SDK if: steps.android-sdk.outputs.cache-hit != 'true' uses: android-actions/setup-android@v2 + - name: AVD cache + uses: actions/cache@v3 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-29 + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2.27.0 + with: + working-directory: ./mobile + cores: 2 + api-level: 29 + arch: x86_64 + profile: pixel + target: default + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." - name: Setup Flutter SDK uses: subosito/flutter-action@v2 with: channel: 'stable' flutter-version: '3.7.3' + cache: true - name: Run integration tests - uses: reactivecircus/android-emulator-runner@v2.27.0 + uses: Wandalen/wretry.action@master 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 + action: reactivecircus/android-emulator-runner@v2.27.0 + with: | + working-directory: ./mobile + cores: 2 + api-level: 29 + arch: x86_64 + profile: pixel + target: default + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: | + flutter pub get + flutter test integration_test + attempt_limit: 3 diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index 24b98c9617..a8c860c908 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; @@ -43,7 +45,6 @@ class ImmichTestHelper { // Load main Widget await tester.pumpWidget(app.getMainWidget(db)); // Post run tasks - await tester.pumpAndSettle(); await EasyLocalization.ensureInitialized(); } } @@ -62,3 +63,17 @@ void immichWidgetTest( semanticsEnabled: false, ); } + +Future pumpUntilFound( + WidgetTester tester, + Finder finder, { + Duration timeout = const Duration(seconds: 120), + }) async { + bool found = false; + final timer = Timer(timeout, () => throw TimeoutException("Pump until has timed out")); + while (found != true) { + await tester.pump(); + found = tester.any(finder); + } + timer.cancel(); +} diff --git a/mobile/integration_test/test_utils/login_helper.dart b/mobile/integration_test/test_utils/login_helper.dart index 5e6cb547c7..b0810a8ac6 100644 --- a/mobile/integration_test/test_utils/login_helper.dart +++ b/mobile/integration_test/test_utils/login_helper.dart @@ -2,33 +2,20 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'general_helper.dart'; + class ImmichTestLoginHelper { final WidgetTester tester; ImmichTestLoginHelper(this.tester); - Future waitForLoginScreen({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"); + Future waitForLoginScreen() async { + await pumpUntilFound(tester, find.text("Login")); } Future acknowledgeNewServerVersion() async { + await pumpUntilFound(tester, find.text("Acknowledge")); final result = find.text("Acknowledge"); - if (!tester.any(result)) { - return false; - } await tester.tap(result); await tester.pump(); @@ -43,17 +30,17 @@ class ImmichTestLoginHelper { }) async { final loginForms = find.byType(TextFormField); - await tester.pump(const Duration(milliseconds: 500)); await tester.enterText(loginForms.at(0), email); + await tester.pump(); - await tester.pump(const Duration(milliseconds: 500)); await tester.enterText(loginForms.at(1), password); + await tester.pump(); - await tester.pump(const Duration(milliseconds: 500)); await tester.enterText(loginForms.at(2), server); + await tester.pump(); await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); + await tester.pump(); } Future enterCredentialsOf(LoginCredentials credentials) async { @@ -65,32 +52,17 @@ class ImmichTestLoginHelper { } Future pressLoginButton() async { + await pumpUntilFound(tester, find.textContaining("login_form_button_text".tr())); final button = find.textContaining("login_form_button_text".tr()); await tester.tap(button); } Future assertLoginSuccess({int timeoutSeconds = 15}) async { - for (var i = 0; i < timeoutSeconds * 2; i++) { - if (tester.any(find.text("home_page_building_timeline".tr()))) { - return; - } - - await tester.pump(const Duration(milliseconds: 500)); - } - - fail("Login failed."); + await pumpUntilFound(tester, find.text("home_page_building_timeline".tr())); } Future assertLoginFailed({int timeoutSeconds = 15}) async { - for (var i = 0; i < timeoutSeconds * 2; i++) { - if (tester.any(find.text("login_form_failed_login".tr()))) { - return; - } - - await tester.pump(const Duration(milliseconds: 500)); - } - - fail("Timeout."); + await pumpUntilFound(tester, find.text("login_form_failed_login".tr())); } }