diff --git a/.github/workflows/react-native-sdk-e2e.yml b/.github/workflows/react-native-sdk-e2e.yml index a4cd639..db55ff1 100644 --- a/.github/workflows/react-native-sdk-e2e.yml +++ b/.github/workflows/react-native-sdk-e2e.yml @@ -121,8 +121,9 @@ jobs: adb shell settings put global window_animation_scale 0 adb shell settings put global transition_animation_scale 0 adb shell settings put global animator_duration_scale 0 + adb shell settings put global hide_error_dialogs 1 sleep 5 - ./gradlew :app:connectedDebugAndroidTest --no-daemon -Pandroid.testInstrumentationRunnerArguments.LOGIN_EMAIL="$LOGIN_EMAIL" -Pandroid.testInstrumentationRunnerArguments.LOGIN_PASSWORD="$LOGIN_PASSWORD" -Pandroid.testInstrumentationRunnerArguments.LOGIN_WRONG_PASSWORD="$LOGIN_WRONG_PASSWORD" -Pandroid.testInstrumentationRunnerArguments.TENANT_NAME_1="$TENANT_NAME_1" -Pandroid.testInstrumentationRunnerArguments.TENANT_NAME_2="$TENANT_NAME_2" -Pandroid.testInstrumentationRunnerArguments.GOOGLE_EMAIL="$GOOGLE_EMAIL" -Pandroid.testInstrumentationRunnerArguments.GOOGLE_PASSWORD="$GOOGLE_PASSWORD" + for attempt in 1 2 3; do echo "Android E2E attempt $attempt"; if ./gradlew :app:connectedDebugAndroidTest --no-daemon -Pandroid.testInstrumentationRunnerArguments.LOGIN_EMAIL="$LOGIN_EMAIL" -Pandroid.testInstrumentationRunnerArguments.LOGIN_PASSWORD="$LOGIN_PASSWORD" -Pandroid.testInstrumentationRunnerArguments.LOGIN_WRONG_PASSWORD="$LOGIN_WRONG_PASSWORD" -Pandroid.testInstrumentationRunnerArguments.TENANT_NAME_1="$TENANT_NAME_1" -Pandroid.testInstrumentationRunnerArguments.TENANT_NAME_2="$TENANT_NAME_2" -Pandroid.testInstrumentationRunnerArguments.GOOGLE_EMAIL="$GOOGLE_EMAIL" -Pandroid.testInstrumentationRunnerArguments.GOOGLE_PASSWORD="$GOOGLE_PASSWORD"; then exit 0; fi; echo "Android E2E attempt $attempt failed (likely emulator/launcher flake); retrying"; adb shell am force-stop com.frontegg.demo || true; adb shell settings put global hide_error_dialogs 1 || true; sleep 5; done; exit 1 - name: Collect JUnit reports if: always() diff --git a/example/android/app/src/androidTest/java/com/frontegg/demo/e2e/utils/UiTestInstrumentation.kt b/example/android/app/src/androidTest/java/com/frontegg/demo/e2e/utils/UiTestInstrumentation.kt index 6eaf9a0..50d0673 100644 --- a/example/android/app/src/androidTest/java/com/frontegg/demo/e2e/utils/UiTestInstrumentation.kt +++ b/example/android/app/src/androidTest/java/com/frontegg/demo/e2e/utils/UiTestInstrumentation.kt @@ -34,13 +34,24 @@ class UiTestInstrumentation( activityName: String = "com.frontegg.demo.MainActivity", applicationPackage: String = "com.frontegg.demo", ) { + // CI flake guard: stop the system from popping "… isn't responding" dialogs over the app + // when the emulator is slow under load (the launcher ANRs and hides the app under test). + runCatching { uiDevice.executeShellCommand("settings put global hide_error_dialogs 1") } + val intent = Intent(Intent.ACTION_MAIN).apply { setClassName(targetContext, activityName) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } - targetContext.startActivity(intent) - uiDevice.wait(Until.hasObject(By.pkg(applicationPackage).depth(0)), 5_000) + // A slow cold start can leave the launcher (not the app) foregrounded on the first try, + // so retry the launch until the app package is actually on top. + repeat(3) { + targetContext.startActivity(intent) + dismissBlockingSystemDialogs() + if (uiDevice.wait(Until.hasObject(By.pkg(applicationPackage).depth(0)), 15_000)) { + return + } + } } fun terminateApp(applicationPackage: String = "com.frontegg.demo") { @@ -55,11 +66,25 @@ class UiTestInstrumentation( val deadline = SystemClock.uptimeMillis() + timeout while (SystemClock.uptimeMillis() < deadline) { uiDevice.findObject(selector)?.let { return it } + dismissBlockingSystemDialogs() delay(250) } return null } + /** + * CI flake guard: under load the emulator's launcher (or the app) can ANR, and the + * system shows an "… isn't responding" dialog on top of the app, hiding the views + * under test. Tap "Wait" (android:id/aerr_wait) to dismiss it and let the process + * recover, so a transient ANR doesn't fail the whole suite. + */ + private fun dismissBlockingSystemDialogs() { + uiDevice.findObject(By.res("android:id/aerr_wait"))?.let { + it.click() + delay(500) + } + } + fun requireView(selector: BySelector, timeout: Long = defaultTimeoutMs): UiObject2 = waitForView(selector, timeout) ?: error("Timed out waiting for $selector after ${timeout}ms. ${dumpVisible()}")