ci: run E2E suites in demo apps (Android)#381
Conversation
# Conflicts: # .github/actions/androidapp-road-test/action.yml # apps/AndroidApp/app/build.gradle.kts # apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt # apps/brownfield-example-shared-tests/e2e/detoxUtils.cjs # apps/brownfield-example-shared-tests/e2e/e2eTestIds.cjs
| - version: '54' | ||
| - version: '55' |
There was a problem hiding this comment.
Q: Does it mean we only test for Expo 54 now?
There was a problem hiding this comment.
No — Expo 54 is still covered; Expo 55 was moved to dedicated jobs, not dropped.
Expo 55 was removed from the shared matrix because a single job couldn’t reliably finish build + E2E within the timeout. iOS still uses one matrix for both versions (run-e2e: 'false' for 54, 'true' for 55) — Android mirrors that intent, with a split layout for 55.
| } catch (error: AssertionError) { | ||
| if ( | ||
| error.message?.contains( | ||
| "Pausing an activity that is not the current activity" | ||
| ) == true | ||
| ) { | ||
| Log.w( | ||
| "ReactNativeFragment", | ||
| "Ignoring stale onHostPause for ${activity?.javaClass?.simpleName}", | ||
| error | ||
| ) | ||
| return |
There was a problem hiding this comment.
Do we know why or what causes this? Is it only for Detox or also happen in real usage?
There was a problem hiding this comment.
What causes it: ReactFragment.onPause() forwards to the RN host (onHostPause). RN asserts that the pausing activity is the current RN host activity. The assertion text is:
"Pausing an activity that is not the current activity"
When it shows up in this project:
Brownfield embedding — MainActivity hosts RN via ReactNativeFragment inside Compose, not as the sole RN activity.
Tab / fragment lifecycle — switching embedded RN tabs pauses one fragment while RN may still consider another context current.
Detox / instrumentation — focus churn, ActivityTestRule, and the DetoxE2E window-focus workarounds in MainActivity can deliver stale onPause after RN has already moved on.
ReactNativeFragment.kt
Lines 72-89
override fun onPause() {
try {
super.onPause()
} catch (error: AssertionError) {
if (
error.message?.contains(
"Pausing an activity that is not the current activity"
) == true
) {
Log.w(
"ReactNativeFragment",
"Ignoring stale onHostPause for ${activity?.javaClass?.simpleName}",
error
)
return
}
throw error
}
}
Detox-only vs production: We’ve seen it most reliably under Detox (CI + emulator). The same class of race is possible in production brownfield apps with fragment-based RN and fast navigation, but we haven’t had production reports yet. The handler is narrow: only this specific assertion is swallowed; other errors are rethrown.
| class BrownfieldApplication : Application(), ReactApplication { | ||
| @Suppress("DEPRECATION") | ||
| override val reactNativeHost: ReactNativeHost | ||
| get() = ReactNativeHostManager.reactNativeHost |
There was a problem hiding this comment.
I believe you need to declare reactNativeHost only because of Expo54 as it contains the deprecated API usage.
If yes, then we can hold this PR until we remove Expo54, which will be done soon after #360
The reason I am suggesting to hold this PR is because we are shipping deprecated stuff as part of Expo config plugin for apps above SDK 54 and they do not require it.
There was a problem hiding this comment.
So the reactNativeHost override in BrownfieldApplication is effectively required for Expo 54 Detox; for Expo 55 and vanilla, reactHost is sufficient and matches RN’s direction (same as Detox #4895 around New Architecture).
Config plugin vs test app:
The deprecated pre55 template (ReactNativeHostWrapper, etc.) is only generated for Expo < 55 — not for SDK 55+ consumers.
BrownfieldApplication lives in the example AndroidApp, not in the published library API.
On holding until #360: Reasonable if the goal is zero deprecated surface before merge. Counter-arguments for merging now:
Android E2E targets Vanilla + Expo 55; Expo 54 is road-test only (no E2E).
We could drop the reactNativeHost override from shared BrownfieldApplication and move it to an expo54 flavor-specific source set once #360 lands, leaving reactHost only for 55+.
Proposed path: Merge E2E for 55/vanilla now, or gate on #360 — your call. If we proceed, a follow-up to flavor-split BrownfieldApplication (expo54 vs 55+) would address the deprecated API concern without blocking E2E.
There was a problem hiding this comment.
Thanks for the response, appreciate it. I would prefer to hold this PR until end of this week, in the meantime, we will get SDK 56 support merged and SDK 54 apps removal. Then we can rebase this PR and get rid of deprecated API stuff and get it merged.
There was a problem hiding this comment.
Sounds good to me thanks!
…roid/example/BrownfieldApplication.kt Co-authored-by: Hur Ali <hurali97@gmail.com>
Summary
This PR adds and stabilizes Android Detox E2E for the brownfield example apps (AndroidApp + embedded RN hosts), with a focus on Expo 55 reliability in CI.
CI pipeline
E2E test fixes (Expo 55)
Expo 55 tests were flaky/failing on CI (Pixel 6, headless emulator) because:
Approach: UIAutomator-first assertions via adb, resolution-aware scrolling, Detox swipes as primary with adb fallbacks, and tab navigation via UIAutomator content-desc (Home, postMessage API, Send message to Native).
Emulator / runner hardening
Tests covered (Expo 55 Android)