@@ -59,6 +59,8 @@ import androidx.core.content.ContextCompat.getString
5959import androidx.core.content.res.ResourcesCompat.getDrawable
6060import androidx.test.core.app.ActivityScenario.launch
6161import androidx.test.core.app.ApplicationProvider.getApplicationContext
62+ import androidx.test.espresso.IdlingRegistry
63+ import androidx.test.espresso.IdlingResource
6264import androidx.test.ext.junit.runners.AndroidJUnit4
6365import com.salesforce.androidsdk.R.drawable.sf__salesforce_logo
6466import com.salesforce.androidsdk.R.string.sf__screen_lock_auth_error
@@ -83,11 +85,53 @@ import io.mockk.verify
8385import org.junit.Assert.assertEquals
8486import org.junit.Assert.assertFalse
8587import org.junit.Assert.assertTrue
86- import org.junit.Ignore
8788import org.junit.Test
8889import org.junit.runner.RunWith
8990
90- @Ignore
91+ /* *
92+ * IdlingResource that waits for a condition to become true.
93+ * Used to synchronize ViewModel state changes with test assertions.
94+ */
95+ class ViewModelIdlingResource (
96+ private val resourceName : String ,
97+ private val checkCondition : () -> Boolean
98+ ) : IdlingResource {
99+ @Volatile
100+ private var callback: IdlingResource .ResourceCallback ? = null
101+
102+ override fun getName (): String = resourceName
103+
104+ override fun isIdleNow (): Boolean {
105+ val idle = checkCondition()
106+ if (idle && callback != null ) {
107+ callback?.onTransitionToIdle()
108+ }
109+ return idle
110+ }
111+
112+ override fun registerIdleTransitionCallback (callback : IdlingResource .ResourceCallback ? ) {
113+ this .callback = callback
114+ }
115+ }
116+
117+ /* *
118+ * Helper function to wait for a condition using IdlingResource.
119+ * Automatically registers and unregisters the idling resource.
120+ */
121+ inline fun <T > waitForCondition (
122+ resourceName : String ,
123+ noinline condition : () -> Boolean ,
124+ block : () -> T
125+ ): T {
126+ val idlingResource = ViewModelIdlingResource (resourceName, condition)
127+ IdlingRegistry .getInstance().register(idlingResource)
128+ try {
129+ return block()
130+ } finally {
131+ IdlingRegistry .getInstance().unregister(idlingResource)
132+ }
133+ }
134+
91135@RunWith(AndroidJUnit4 ::class )
92136class ScreenLockActivityScenarioTest {
93137
@@ -272,9 +316,20 @@ class ScreenLockActivityScenarioTest {
272316 )
273317
274318 verify(exactly = 1 ) { biometricPrompt.authenticate(any()) }
275- assertFalse(activity.viewModel.logoutButtonVisible.value)
276- assertFalse(activity.viewModel.setupButtonVisible.value)
277- assertFalse(activity.viewModel.setupMessageVisible.value)
319+
320+ // Wait for ViewModel state to stabilize before asserting
321+ waitForCondition(
322+ resourceName = " ViewModelStateStable" ,
323+ condition = {
324+ ! activity.viewModel.logoutButtonVisible.value &&
325+ ! activity.viewModel.setupButtonVisible.value &&
326+ ! activity.viewModel.setupMessageVisible.value
327+ }
328+ ) {
329+ assertFalse(activity.viewModel.logoutButtonVisible.value)
330+ assertFalse(activity.viewModel.setupButtonVisible.value)
331+ assertFalse(activity.viewModel.setupMessageVisible.value)
332+ }
278333 }
279334 }
280335 }
@@ -412,9 +467,27 @@ class ScreenLockActivityScenarioTest {
412467 biometricSetupActivityResultLauncher = biometricSetupActivityResultLauncher,
413468 sdkConfiguration = AndroidSdkConfigurationR ,
414469 )
415- activity.viewModel.setupButtonAction.value()
416470
417- assertEquals(activity.getString(sf__screen_lock_setup_required, activity.viewModel.appName()), activity.viewModel.setupMessageText.value)
471+ // Wait for ViewModel state to update before triggering action
472+ waitForCondition(
473+ resourceName = " ViewModelEnrollmentStateSet" ,
474+ condition = {
475+ activity.viewModel.setupButtonAction.value != null
476+ }
477+ ) {
478+ activity.viewModel.setupButtonAction.value()
479+ }
480+
481+ val expectedMessage = activity.getString(sf__screen_lock_setup_required, activity.viewModel.appName())
482+ waitForCondition(
483+ resourceName = " ViewModelSetupMessageSet" ,
484+ condition = {
485+ activity.viewModel.setupMessageText.value == expectedMessage
486+ }
487+ ) {
488+ assertEquals(expectedMessage, activity.viewModel.setupMessageText.value)
489+ }
490+
418491 verify(exactly = 1 ) { biometricSetupActivityResultLauncher.launch(capture(intent)) }
419492 assertEquals(ACTION_BIOMETRIC_ENROLL , intent.captured.action)
420493 assertEquals(activity.viewModel.biometricAuthenticators(), intent.captured.getIntExtra(EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED , - 1 ))
@@ -446,14 +519,37 @@ class ScreenLockActivityScenarioTest {
446519 biometricSetupActivityResultLauncher = biometricSetupActivityResultLauncher,
447520 sdkConfiguration = AndroidSdkConfigurationQ ,
448521 )
449- activity.viewModel.setupButtonAction.value()
450522
451- assertEquals(activity.getString(sf__screen_lock_setup_required, activity.viewModel.appName()), activity.viewModel.setupMessageText.value)
523+ // Wait for ViewModel state to update before triggering action
524+ waitForCondition(
525+ resourceName = " ViewModelEnrollmentStateSet" ,
526+ condition = {
527+ activity.viewModel.setupButtonAction.value != null
528+ }
529+ ) {
530+ activity.viewModel.setupButtonAction.value()
531+ }
532+
533+ val expectedMessage = activity.getString(sf__screen_lock_setup_required, activity.viewModel.appName())
534+ val expectedButtonLabel = activity.getString(sf__screen_lock_setup_button)
535+
536+ // Wait for ViewModel state to fully update
537+ waitForCondition(
538+ resourceName = " ViewModelSetupStateComplete" ,
539+ condition = {
540+ activity.viewModel.setupMessageText.value == expectedMessage &&
541+ activity.viewModel.setupButtonLabel.value == expectedButtonLabel &&
542+ activity.viewModel.setupButtonVisible.value
543+ }
544+ ) {
545+ assertEquals(expectedMessage, activity.viewModel.setupMessageText.value)
546+ assertEquals(expectedButtonLabel, activity.viewModel.setupButtonLabel.value)
547+ assertTrue(activity.viewModel.setupButtonVisible.value)
548+ }
549+
452550 verify(exactly = 1 ) { biometricSetupActivityResultLauncher.launch(capture(intent)) }
453551 assertEquals(ACTION_SET_NEW_PASSWORD , intent.captured.action)
454552 assertEquals(- 1 , intent.captured.getIntExtra(EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED , - 1 ))
455- assertEquals(activity.getString(sf__screen_lock_setup_button), activity.viewModel.setupButtonLabel.value)
456- assertTrue(activity.viewModel.setupButtonVisible.value)
457553 verify(exactly = 0 ) { biometricPrompt.authenticate(any()) }
458554 }
459555 }
@@ -590,7 +686,6 @@ class ScreenLockActivityScenarioTest {
590686 }
591687 }
592688
593- @Ignore
594689 @Test
595690 fun screenLockActivity_onAuthError_sendsAccessibilityEvent () {
596691 launch<ScreenLockActivity >(
@@ -612,14 +707,38 @@ class ScreenLockActivityScenarioTest {
612707 errString = errorString
613708 )
614709
615- verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
616- assertTrue(capturingSlot.captured.text.toString().contains(authenticationErrorString))
710+ // Wait for accessibility event to be sent
711+ waitForCondition(
712+ resourceName = " AccessibilityEventSent" ,
713+ condition = {
714+ try {
715+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(any()) }
716+ true
717+ } catch (e: AssertionError ) {
718+ false
719+ }
720+ }
721+ ) {
722+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
723+ assertTrue(capturingSlot.captured.text.toString().contains(authenticationErrorString))
724+ }
617725
618- assertEquals(errorString, activity.viewModel.setupMessageText.value)
619- assertTrue(activity.viewModel.logoutButtonVisible.value)
620- assertTrue(activity.viewModel.setupButtonVisible.value)
621- assertEquals(activity.getString(sf__screen_lock_retry_button), activity.viewModel.setupButtonLabel.value)
622- assertTrue(activity.viewModel.setupMessageVisible.value)
726+ // Wait for ViewModel state to update
727+ waitForCondition(
728+ resourceName = " ViewModelErrorStateSet" ,
729+ condition = {
730+ activity.viewModel.setupMessageText.value == errorString &&
731+ activity.viewModel.logoutButtonVisible.value &&
732+ activity.viewModel.setupButtonVisible.value &&
733+ activity.viewModel.setupMessageVisible.value
734+ }
735+ ) {
736+ assertEquals(errorString, activity.viewModel.setupMessageText.value)
737+ assertTrue(activity.viewModel.logoutButtonVisible.value)
738+ assertTrue(activity.viewModel.setupButtonVisible.value)
739+ assertEquals(activity.getString(sf__screen_lock_retry_button), activity.viewModel.setupButtonLabel.value)
740+ assertTrue(activity.viewModel.setupMessageVisible.value)
741+ }
623742 }
624743 }
625744 }
@@ -645,14 +764,38 @@ class ScreenLockActivityScenarioTest {
645764 errString = errorString
646765 )
647766
648- verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
649- assertTrue(capturingSlot.captured.text.toString().contains(authenticationErrorString))
767+ // Wait for accessibility event to be sent
768+ waitForCondition(
769+ resourceName = " AccessibilityEventSent" ,
770+ condition = {
771+ try {
772+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(any()) }
773+ true
774+ } catch (e: AssertionError ) {
775+ false
776+ }
777+ }
778+ ) {
779+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
780+ assertTrue(capturingSlot.captured.text.toString().contains(authenticationErrorString))
781+ }
650782
651- assertEquals(authenticationErrorString, activity.viewModel.setupMessageText.value)
652- assertTrue(activity.viewModel.logoutButtonVisible.value)
653- assertTrue(activity.viewModel.setupButtonVisible.value)
654- assertEquals(activity.getString(sf__screen_lock_retry_button), activity.viewModel.setupButtonLabel.value)
655- assertTrue(activity.viewModel.setupMessageVisible.value)
783+ // Wait for ViewModel state to update
784+ waitForCondition(
785+ resourceName = " ViewModelErrorStateSet" ,
786+ condition = {
787+ activity.viewModel.setupMessageText.value == authenticationErrorString &&
788+ activity.viewModel.logoutButtonVisible.value &&
789+ activity.viewModel.setupButtonVisible.value &&
790+ activity.viewModel.setupMessageVisible.value
791+ }
792+ ) {
793+ assertEquals(authenticationErrorString, activity.viewModel.setupMessageText.value)
794+ assertTrue(activity.viewModel.logoutButtonVisible.value)
795+ assertTrue(activity.viewModel.setupButtonVisible.value)
796+ assertEquals(activity.getString(sf__screen_lock_retry_button), activity.viewModel.setupButtonLabel.value)
797+ assertTrue(activity.viewModel.setupMessageVisible.value)
798+ }
656799 }
657800 }
658801 }
@@ -677,14 +820,37 @@ class ScreenLockActivityScenarioTest {
677820 screenLockManager = screenLockManager,
678821 )
679822
680- verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
681- assertTrue(capturingSlot.captured.text.toString().contains(activity.getString(sf__screen_lock_auth_success)))
823+ // Wait for accessibility event to be sent
824+ waitForCondition(
825+ resourceName = " AccessibilityEventSent" ,
826+ condition = {
827+ try {
828+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(any()) }
829+ true
830+ } catch (e: AssertionError ) {
831+ false
832+ }
833+ }
834+ ) {
835+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
836+ assertTrue(capturingSlot.captured.text.toString().contains(activity.getString(sf__screen_lock_auth_success)))
837+ }
682838
683839 verify(exactly = 1 ) { screenLockManager.onUnlock() }
684840
685- assertFalse(activity.viewModel.logoutButtonVisible.value)
686- assertFalse(activity.viewModel.setupButtonVisible.value)
687- assertFalse(activity.viewModel.setupMessageVisible.value)
841+ // Wait for ViewModel state to update
842+ waitForCondition(
843+ resourceName = " ViewModelSuccessStateSet" ,
844+ condition = {
845+ ! activity.viewModel.logoutButtonVisible.value &&
846+ ! activity.viewModel.setupButtonVisible.value &&
847+ ! activity.viewModel.setupMessageVisible.value
848+ }
849+ ) {
850+ assertFalse(activity.viewModel.logoutButtonVisible.value)
851+ assertFalse(activity.viewModel.setupButtonVisible.value)
852+ assertFalse(activity.viewModel.setupMessageVisible.value)
853+ }
688854 }
689855 }
690856 }
@@ -708,12 +874,35 @@ class ScreenLockActivityScenarioTest {
708874 screenLockManager = null ,
709875 )
710876
711- verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
712- assertTrue(capturingSlot.captured.text.toString().contains(activity.getString(sf__screen_lock_auth_success)))
877+ // Wait for accessibility event to be sent
878+ waitForCondition(
879+ resourceName = " AccessibilityEventSent" ,
880+ condition = {
881+ try {
882+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(any()) }
883+ true
884+ } catch (e: AssertionError ) {
885+ false
886+ }
887+ }
888+ ) {
889+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
890+ assertTrue(capturingSlot.captured.text.toString().contains(activity.getString(sf__screen_lock_auth_success)))
891+ }
713892
714- assertFalse(activity.viewModel.logoutButtonVisible.value)
715- assertFalse(activity.viewModel.setupButtonVisible.value)
716- assertFalse(activity.viewModel.setupMessageVisible.value)
893+ // Wait for ViewModel state to update
894+ waitForCondition(
895+ resourceName = " ViewModelSuccessStateSet" ,
896+ condition = {
897+ ! activity.viewModel.logoutButtonVisible.value &&
898+ ! activity.viewModel.setupButtonVisible.value &&
899+ ! activity.viewModel.setupMessageVisible.value
900+ }
901+ ) {
902+ assertFalse(activity.viewModel.logoutButtonVisible.value)
903+ assertFalse(activity.viewModel.setupButtonVisible.value)
904+ assertFalse(activity.viewModel.setupMessageVisible.value)
905+ }
717906 }
718907 }
719908 }
@@ -739,17 +928,40 @@ class ScreenLockActivityScenarioTest {
739928 sdkConfiguration = AndroidSdkConfigurationQ
740929 )
741930
742- verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
743- assertTrue(capturingSlot.captured.text.toString().contains(activity.getString(sf__screen_lock_auth_success)))
744- assertEquals(TYPE_WINDOW_STATE_CHANGED , capturingSlot.captured.eventType)
745- assertEquals(ScreenLockActivity ::class .java.name, capturingSlot.captured.className)
746- assertEquals(null , capturingSlot.captured.packageName)
931+ // Wait for accessibility event to be sent
932+ waitForCondition(
933+ resourceName = " AccessibilityEventSent" ,
934+ condition = {
935+ try {
936+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(any()) }
937+ true
938+ } catch (e: AssertionError ) {
939+ false
940+ }
941+ }
942+ ) {
943+ verify(exactly = 1 ) { accessibilityManager.sendAccessibilityEvent(capture(capturingSlot)) }
944+ assertTrue(capturingSlot.captured.text.toString().contains(activity.getString(sf__screen_lock_auth_success)))
945+ assertEquals(TYPE_WINDOW_STATE_CHANGED , capturingSlot.captured.eventType)
946+ assertEquals(ScreenLockActivity ::class .java.name, capturingSlot.captured.className)
947+ assertEquals(null , capturingSlot.captured.packageName)
948+ }
747949
748950 verify(exactly = 1 ) { screenLockManager.onUnlock() }
749951
750- assertFalse(activity.viewModel.logoutButtonVisible.value)
751- assertFalse(activity.viewModel.setupButtonVisible.value)
752- assertFalse(activity.viewModel.setupMessageVisible.value)
952+ // Wait for ViewModel state to update
953+ waitForCondition(
954+ resourceName = " ViewModelSuccessStateSet" ,
955+ condition = {
956+ ! activity.viewModel.logoutButtonVisible.value &&
957+ ! activity.viewModel.setupButtonVisible.value &&
958+ ! activity.viewModel.setupMessageVisible.value
959+ }
960+ ) {
961+ assertFalse(activity.viewModel.logoutButtonVisible.value)
962+ assertFalse(activity.viewModel.setupButtonVisible.value)
963+ assertFalse(activity.viewModel.setupMessageVisible.value)
964+ }
753965 }
754966 }
755967 }
0 commit comments