Skip to content

Commit 0f15179

Browse files
@W-21933885: [MSDK Android] App Attestation Implementation (Add IdlingResource synchronization to ScreenLockActivityScenarioTest)
Prevent timeout failures in Firebase Test Lab by adding proper synchronization for ViewModel state changes and accessibility events. Tests now wait for actual conditions to be met rather than assuming immediate state changes.)
1 parent 86f4b77 commit 0f15179

3 files changed

Lines changed: 1333 additions & 44 deletions

File tree

libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/ScreenLockActivityScenarioTest.kt

Lines changed: 256 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import androidx.core.content.ContextCompat.getString
5959
import androidx.core.content.res.ResourcesCompat.getDrawable
6060
import androidx.test.core.app.ActivityScenario.launch
6161
import androidx.test.core.app.ApplicationProvider.getApplicationContext
62+
import androidx.test.espresso.IdlingRegistry
63+
import androidx.test.espresso.IdlingResource
6264
import androidx.test.ext.junit.runners.AndroidJUnit4
6365
import com.salesforce.androidsdk.R.drawable.sf__salesforce_logo
6466
import com.salesforce.androidsdk.R.string.sf__screen_lock_auth_error
@@ -83,11 +85,53 @@ import io.mockk.verify
8385
import org.junit.Assert.assertEquals
8486
import org.junit.Assert.assertFalse
8587
import org.junit.Assert.assertTrue
86-
import org.junit.Ignore
8788
import org.junit.Test
8889
import 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)
92136
class 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

Comments
 (0)