@@ -43,6 +43,10 @@ import androidx.compose.ui.test.onNodeWithContentDescription
4343import androidx.lifecycle.ViewModel
4444import androidx.lifecycle.ViewModelProvider
4545import androidx.lifecycle.viewmodel.CreationExtras
46+ import androidx.test.espresso.Espresso
47+ import androidx.test.espresso.IdlingPolicies
48+ import androidx.test.espresso.IdlingRegistry
49+ import androidx.test.espresso.IdlingResource
4650import androidx.test.rule.GrantPermissionRule
4751import com.salesforce.androidsdk.R
4852import com.salesforce.androidsdk.app.SalesforceSDKManager
@@ -56,13 +60,93 @@ import org.junit.Before
5660import org.junit.Ignore
5761import org.junit.Rule
5862import org.junit.Test
63+ import org.junit.rules.Timeout
64+ import java.util.concurrent.TimeUnit
65+
66+ /* *
67+ * IdlingResource that waits for a condition to become true.
68+ * Used to synchronize ViewModel state changes with test assertions.
69+ */
70+ class ViewModelIdlingResource (
71+ private val resourceName : String ,
72+ private val checkCondition : () -> Boolean
73+ ) : IdlingResource {
74+ @Volatile
75+ private var callback: IdlingResource .ResourceCallback ? = null
76+
77+ override fun getName (): String = resourceName
78+
79+ override fun isIdleNow (): Boolean {
80+ val idle = checkCondition()
81+ if (idle && callback != null ) {
82+ callback?.onTransitionToIdle()
83+ }
84+ return idle
85+ }
86+
87+ override fun registerIdleTransitionCallback (callback : IdlingResource .ResourceCallback ? ) {
88+ this .callback = callback
89+ }
90+ }
91+
92+ /* *
93+ * Helper function to wait for a condition using IdlingResource.
94+ * Automatically registers and unregisters the idling resource.
95+ * Uses Espresso's built-in synchronization mechanism instead of manual polling.
96+ *
97+ * @param resourceName Name for the idling resource (for debugging)
98+ * @param condition Lambda that returns true when the condition is met
99+ * @param timeoutSeconds Maximum time to wait for the condition (default: 10 seconds)
100+ * @param block Code to execute once the condition is met
101+ */
102+ inline fun <T > waitForCondition (
103+ resourceName : String ,
104+ noinline condition : () -> Boolean ,
105+ timeoutSeconds : Long = 10,
106+ block : () -> T
107+ ): T {
108+ val idlingResource = ViewModelIdlingResource (resourceName, condition)
109+
110+ // Set custom timeout for this wait operation
111+ val previousTimeout = IdlingPolicies .getMasterIdlingPolicy()
112+ IdlingPolicies .setMasterPolicyTimeout(timeoutSeconds, TimeUnit .SECONDS )
113+
114+ IdlingRegistry .getInstance().register(idlingResource)
115+ try {
116+ // Let Espresso handle the synchronization - it will poll the IdlingResource
117+ // and wait until isIdleNow() returns true, respecting the timeout policy
118+ Espresso .onIdle()
119+
120+ return block()
121+ } catch (e: Exception ) {
122+ // Provide better error message if timeout occurs
123+ if (e.message?.contains(" IdlingResource" ) == true ||
124+ e.message?.contains(" timeout" ) == true ) {
125+ throw AssertionError (" Timeout waiting for condition '$resourceName ' after ${timeoutSeconds} s" , e)
126+ }
127+ throw e
128+ } finally {
129+ IdlingRegistry .getInstance().unregister(idlingResource)
130+ // Restore previous timeout policy
131+ IdlingPolicies .setMasterPolicyTimeout(
132+ previousTimeout.idleTimeout,
133+ previousTimeout.idleTimeoutUnit
134+ )
135+ }
136+ }
59137
60- @Ignore
61138class TokenMigrationViewActivityTest {
62139
63140 @get:Rule
64141 val androidComposeTestRule = createAndroidComposeRule<ComponentActivity >()
65142
143+ /* *
144+ * Global timeout rule for all tests in this class.
145+ * Each test will timeout after 30 seconds to accommodate slower Firebase Test Lab devices.
146+ */
147+ @get:Rule
148+ val globalTimeout: Timeout = Timeout (30 , TimeUnit .SECONDS )
149+
66150 // TODO: Remove if when min SDK version is 33
67151 @get:Rule
68152 val permissionRule: GrantPermissionRule = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
@@ -172,11 +256,18 @@ class TokenMigrationViewActivityTest {
172256 }
173257 }
174258
259+ // Update background color
175260 androidComposeTestRule.runOnIdle {
176261 backgroundColor.value = White
177262 }
178263
179- androidComposeTestRule.runOnIdle {
264+ // Wait for ViewModel state to update
265+ waitForCondition(
266+ resourceName = " BackgroundColorUpdatedToWhite" ,
267+ condition = {
268+ backgroundColor.value == White
269+ }
270+ ) {
180271 assertEquals(
181272 " Background color should update to White" ,
182273 White ,
@@ -201,11 +292,18 @@ class TokenMigrationViewActivityTest {
201292 }
202293 }
203294
295+ // Update background color
204296 androidComposeTestRule.runOnIdle {
205297 backgroundColor.value = Red
206298 }
207299
208- androidComposeTestRule.runOnIdle {
300+ // Wait for ViewModel state to update
301+ waitForCondition(
302+ resourceName = " BackgroundColorUpdatedToRed" ,
303+ condition = {
304+ backgroundColor.value == Red
305+ }
306+ ) {
209307 assertEquals(
210308 " Background color should update to Red" ,
211309 Red ,
0 commit comments