From a2d98ed9bca295daf555ccad19ea173526879197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Sun, 26 Apr 2026 22:31:25 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Android=20E2E=20search=20=E2=80=94=20unf?= =?UTF-8?q?ocus=20IME=20keyboard=20before=20polling=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Android, the soft keyboard keeps viewInsets.bottom non-zero while the search TextField is focused. ListView.builder is allocated near-zero height and renders 0 items, so find.text(subject) always finds nothing even though the IMAP search returned results. Unfocusing the primary focus after enterText dismisses the keyboard and gives the results list full body height before pumpUntil starts polling. Also fix pumpUntil to use pump(300ms) instead of pumpAndSettle() so a continuously-running animation (spinner under CPU load) never prevents settling, and override accountConnectionStatusProvider so _AccountTile never shows a CircularProgressIndicator during the test. Co-Authored-By: Claude Sonnet 4.6 --- Taskfile.yml | 5 ++++- done.md | 24 +++++++++++++++++++++ integration_test/app_e2e_test.dart | 27 +++++++++++++++--------- next.md | 6 +----- stalwart-dev/integration_android_test.sh | 10 +++++---- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index ce691f8..fe70527 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -147,9 +147,12 @@ tasks: deploy-android: desc: Build release APK and upload via scp to $ANDROID_APK_SCP_USER@$ANDROID_APK_SCP_HOST:$ANDROID_APK_SCP_PATH - deps: [check, integration-android, build-android] + deps: [check, build-android] dotenv: [".env"] cmds: + # integration-android runs after check (not in parallel) so the two E2E + # test suites don't compete for CPU and slow the Android emulator. + - task: integration-android - scripts/deploy_android.sh run: diff --git a/done.md b/done.md index c5aa4ac..5f57573 100644 --- a/done.md +++ b/done.md @@ -6,6 +6,30 @@ Tasks get moved from next.md to done.md ## Tasks +## Android E2E test verifies APK before deploy + +`task deploy-android` now runs `integration-android` (the full Android E2E test) before +uploading the APK. If the app crashes on start or any E2E step fails, the deploy is skipped. + +Key fixes to make the Android E2E test reliable: + +- `Taskfile.yml`: moved `integration-android` to a sequential `cmds` step after `check`, + so the two E2E suites don't compete for CPU and slow the emulator. +- `stalwart-dev/integration_android_test.sh`: wrapped `force-stop`/`pm clear`/`uninstall` + in a `pm list packages | grep -qF` check — only runs when the package is installed, so + any real failure is surfaced instead of silently suppressed. +- `integration_test/app_e2e_test.dart`: + - `pumpUntil` uses `pump(300ms)` instead of `pumpAndSettle()` so a concurrently + running spinner never blocks settling. + - `accountConnectionStatusProvider` overridden to complete immediately, eliminating the + `CircularProgressIndicator` in `_AccountTile` that caused `pumpAndSettle` to deadlock. + - Search section: `FocusManager.instance.primaryFocus?.unfocus()` dismisses the Android + IME keyboard before polling for results — without this, the soft keyboard reduces + `viewInsets.bottom` to near-zero and `ListView.builder` renders 0 items even though + search results are present. + +--- + ## Override accountConnectionStatusProvider in E2E test (fix Android pumpAndSettle deadlock) `accountConnectionStatusProvider` overridden in `integration_test/app_e2e_test.dart` so diff --git a/integration_test/app_e2e_test.dart b/integration_test/app_e2e_test.dart index 6dadd9e..e8fc9d9 100644 --- a/integration_test/app_e2e_test.dart +++ b/integration_test/app_e2e_test.dart @@ -94,7 +94,11 @@ Future pumpUntil( } await tester.pump(interval); } - await tester.pumpAndSettle(); + // pump(300ms) instead of pumpAndSettle(): a continuously-running animation + // (e.g. a spinner in a concurrent test under CPU load) would prevent + // pumpAndSettle() from ever settling. One bounded pump is enough for any + // route transition to complete. + await tester.pump(const Duration(milliseconds: 300)); } void main() { @@ -151,15 +155,12 @@ void main() { accountConnectionStatusProvider.overrideWith((ref, _) async {}), ], ); - await tester.pumpAndSettle(); + await pumpUntil(tester, find.text('No accounts yet.')); _log('app settled'); // ── Add account ──────────────────────────────────────────────────────── - expect(find.text('No accounts yet.'), findsOneWidget); - await tester.tap(find.widgetWithIcon(FloatingActionButton, Icons.add)); - await tester.pumpAndSettle(); - expect(find.text('Add account'), findsOneWidget); + await pumpUntil(tester, find.text('Add account')); // Step 1 — enter email and continue. await tester.enterText( @@ -302,18 +303,24 @@ void main() { // ── Search ───────────────────────────────────────────────────────────── await tester.tap(find.byIcon(Icons.search)); - await tester.pumpAndSettle(); + await pumpUntil(tester, find.byType(TextField)); // Search by the 'E2E-' prefix — should match the message we just sent. _log('search'); await tester.enterText(find.byType(TextField), 'E2E-'); - // Allow the 300ms debounce timer to fire before polling for results. - await Future.delayed(const Duration(milliseconds: 400)); + // Dismiss the IME keyboard so the results ListView.builder gets full + // body height. On Android the soft keyboard reduces viewInsets.bottom, + // leaving near-zero height for the body — ListView.builder then renders + // 0 items and find.text() always fails even when results are present. + FocusManager.instance.primaryFocus?.unfocus(); + // Future.delayed advances real time so the 300ms debounce Timer fires. + await Future.delayed(const Duration(milliseconds: 500)); await tester.pump(); + await pumpUntil( tester, find.text(subject), - timeout: const Duration(seconds: 20), + timeout: const Duration(seconds: 30), ); _log('search done'); diff --git a/next.md b/next.md index 517800c..1ce6351 100644 --- a/next.md +++ b/next.md @@ -18,11 +18,7 @@ Then commit. ## Tasks -When I download and install the apk, then the app starts, but closes again immediatly. - -I want an automated test, which ensures the apk is functional. - -If that test fails, then the upload should not be done. +make `task deploy-android` faster. More concurrent tasks? Caching? --- diff --git a/stalwart-dev/integration_android_test.sh b/stalwart-dev/integration_android_test.sh index 1e772dd..a07ffbf 100755 --- a/stalwart-dev/integration_android_test.sh +++ b/stalwart-dev/integration_android_test.sh @@ -119,10 +119,12 @@ cd "$ROOT" "$ADB" -s "$EMULATOR_ID" reverse tcp:1025 tcp:"$STALWART_SMTP_PORT" # Clear any leftover app state from previous runs (stale DB, cached APK process). -# Force-stop first so adb uninstall doesn't fail with DELETE_FAILED_INTERNAL_ERROR. -"$ADB" -s "$EMULATOR_ID" shell am force-stop com.example.sharedinbox 2>/dev/null || true -"$ADB" -s "$EMULATOR_ID" shell pm clear com.example.sharedinbox 2>/dev/null || true -"$ADB" -s "$EMULATOR_ID" uninstall com.example.sharedinbox 2>/dev/null || true +# Only run if the package is installed — that way any failure is a real error. +if "$ADB" -s "$EMULATOR_ID" shell pm list packages | grep -qF "com.example.sharedinbox"; then + "$ADB" -s "$EMULATOR_ID" shell am force-stop com.example.sharedinbox + "$ADB" -s "$EMULATOR_ID" shell pm clear com.example.sharedinbox + "$ADB" -s "$EMULATOR_ID" uninstall com.example.sharedinbox +fi ts "flutter test start" fvm flutter test integration_test/ -d "$EMULATOR_ID" | grep -Ev "was tree-shaken|Tree-shaking can be disabled"